@http-client-toolkit/dashboard 0.1.0 → 1.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.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: "generic",
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: "Stats not available for this store type" };
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: "memory",
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: "sqlite",
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: "generic",
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: "Stats not available for this store type" };
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: "memory",
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: "sqlite",
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: "generic",
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: "Stats not available for this store type" };
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("Config updates not supported for this store type");
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: "memory",
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: "sqlite",
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 !== "object") return false;
258
+ if (!store || typeof store !== 'object') return false;
259
259
  const s = store;
260
- return typeof s.destroy === "function" && typeof s.getStats === "function" && typeof s.cleanup === "function" && typeof s.getLRUItems === "function";
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 !== "object") return false;
268
+ if (!store || typeof store !== 'object') return false;
264
269
  const s = store;
265
- return typeof s.destroy === "function" && typeof s.getStats === "function" && typeof s.close === "function";
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 !== "object") return false;
277
+ if (!store || typeof store !== 'object') return false;
269
278
  const s = store;
270
- return typeof s.destroy === "function" && typeof s.getStats === "function" && typeof s.listJobs === "function" && typeof s.cleanup === "function" && !("close" in s && typeof s.close === "function");
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 !== "object") return false;
288
+ if (!store || typeof store !== 'object') return false;
274
289
  const s = store;
275
- return typeof s.destroy === "function" && typeof s.getStats === "function" && typeof s.close === "function" && typeof s.listJobs === "function";
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 !== "object") return false;
298
+ if (!store || typeof store !== 'object') return false;
279
299
  const s = store;
280
- return typeof s.destroy === "function" && typeof s.getStats === "function" && typeof s.listResources === "function" && typeof s.setResourceConfig === "function" && !("close" in s && typeof s.close === "function");
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 !== "object") return false;
309
+ if (!store || typeof store !== 'object') return false;
284
310
  const s = store;
285
- return typeof s.destroy === "function" && typeof s.getStats === "function" && typeof s.close === "function" && typeof s.listResources === "function" && typeof s.setResourceConfig === "function";
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.object({
316
- name: z.string().min(1, "Client name must not be empty").regex(
317
- CLIENT_NAME_REGEX,
318
- "Client name must be URL-safe (a-z, 0-9, -, _)"
319
- ),
320
- cacheStore: z.custom().optional(),
321
- dedupeStore: z.custom().optional(),
322
- rateLimitStore: z.custom().optional()
323
- }).refine(
324
- (data) => data.cacheStore || data.dedupeStore || data.rateLimitStore,
325
- { message: "At least one store must be provided per client" }
326
- );
327
- var DashboardOptionsSchema = z.object({
328
- clients: z.array(ClientConfigSchema).min(1, "At least one client is required"),
329
- basePath: z.string().default("/"),
330
- pollIntervalMs: z.number().int().positive().default(5e3)
331
- }).refine(
332
- (data) => {
333
- const names = data.clients.map((c) => c.name);
334
- return new Set(names).size === names.length;
335
- },
336
- { message: "Client names must be unique" }
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("localhost")
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, "http://localhost");
413
+ const raw = req.url ?? '/';
414
+ const url = new URL(raw, 'http://localhost');
355
415
  let pathname = url.pathname;
356
- if (basePath !== "/" && pathname.startsWith(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(":")) continue;
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("data", (chunk) => {
436
+ let body = '';
437
+ req.on('data', (chunk) => {
378
438
  body += chunk.toString();
379
439
  });
380
- req.on("end", () => {
440
+ req.on('end', () => {
381
441
  try {
382
442
  resolve(JSON.parse(body));
383
443
  } catch {
384
- reject(new Error("Invalid JSON body"));
444
+ reject(new Error('Invalid JSON body'));
385
445
  }
386
446
  });
387
- req.on("error", reject);
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
- "Content-Type": "application/json",
396
- "Content-Length": Buffer.byteLength(body),
397
- "Cache-Control": "no-store"
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, "Not found", 404);
465
+ sendError(res, 'Not found', 404);
406
466
  }
407
467
  function sendMethodNotAllowed(res) {
408
- sendError(res, "Method not allowed", 405);
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 : "Unknown error");
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("page") ?? "0", 10);
423
- const limit = parseInt(query.get("limit") ?? "50", 10);
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 : "Unknown error");
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, "/cache/entries/:hash");
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 : "Unknown error");
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, "/cache/entries/:hash");
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 : "Unknown error");
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 : "Unknown error");
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 : "Unknown error");
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("page") ?? "0", 10);
481
- const limit = parseInt(query.get("limit") ?? "50", 10);
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 : "Unknown error");
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, "/dedup/jobs/:hash");
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 : "Unknown error");
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 ? { type: client.cache.type, capabilities: client.cache.capabilities } : null,
510
- dedup: client.dedup ? { type: client.dedup.type, capabilities: client.dedup.capabilities } : null,
511
- rateLimit: client.rateLimit ? {
512
- type: client.rateLimit.type,
513
- capabilities: client.rateLimit.capabilities
514
- } : null
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: "ok",
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 : "Unknown error");
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 : "Unknown error");
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, "/rate-limit/resources/:name");
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 : "Unknown error");
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, "/rate-limit/resources/:name/config");
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 : "Unknown error");
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, "/rate-limit/resources/:name/reset");
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 : "Unknown error");
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 ? { type: client.cache.type, capabilities: client.cache.capabilities } : null,
595
- dedup: client.dedup ? { type: client.dedup.type, capabilities: client.dedup.capabilities } : null,
596
- rateLimit: client.rateLimit ? {
597
- type: client.rateLimit.type,
598
- capabilities: client.rateLimit.capabilities
599
- } : null
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() ?? "GET";
611
- if (pathname === "/api/health" && method === "GET") {
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 === "/api/clients" && method === "GET") {
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("/api/")) {
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("/cache")) {
710
+ if (subPath.startsWith('/cache')) {
639
711
  if (!client.cache) {
640
- sendError(res, "Cache store not configured", 404);
712
+ sendError(res, 'Cache store not configured', 404);
641
713
  return true;
642
714
  }
643
- if (subPath === "/cache/stats" && method === "GET") {
715
+ if (subPath === '/cache/stats' && method === 'GET') {
644
716
  await handleCacheStats(res, client.cache);
645
717
  return true;
646
718
  }
647
- if (subPath === "/cache/entries" && method === "GET") {
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 === "/cache/entries" && method === "DELETE") {
723
+ if (subPath === '/cache/entries' && method === 'DELETE') {
652
724
  await handleClearCache(res, client.cache);
653
725
  return true;
654
726
  }
655
- const isSingleEntry = subPath.startsWith("/cache/entries/") && subPath.split("/").length === 4;
656
- if (isSingleEntry && method === "GET") {
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 === "DELETE") {
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("/dedup")) {
742
+ if (subPath.startsWith('/dedup')) {
670
743
  if (!client.dedup) {
671
- sendError(res, "Dedup store not configured", 404);
744
+ sendError(res, 'Dedup store not configured', 404);
672
745
  return true;
673
746
  }
674
- if (subPath === "/dedup/stats" && method === "GET") {
747
+ if (subPath === '/dedup/stats' && method === 'GET') {
675
748
  await handleDedupeStats(res, client.dedup);
676
749
  return true;
677
750
  }
678
- if (subPath === "/dedup/jobs" && method === "GET") {
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 = subPath.startsWith("/dedup/jobs/") && subPath.split("/").length === 4;
683
- if (isSingleJob && method === "GET") {
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("/rate-limit")) {
766
+ if (subPath.startsWith('/rate-limit')) {
693
767
  if (!client.rateLimit) {
694
- sendError(res, "Rate limit store not configured", 404);
768
+ sendError(res, 'Rate limit store not configured', 404);
695
769
  return true;
696
770
  }
697
- if (subPath === "/rate-limit/stats" && method === "GET") {
771
+ if (subPath === '/rate-limit/stats' && method === 'GET') {
698
772
  await handleRateLimitStats(res, client.rateLimit);
699
773
  return true;
700
774
  }
701
- if (subPath === "/rate-limit/resources" && method === "GET") {
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("/config") && method === "PUT") {
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("/reset") && method === "POST") {
783
+ if (subPath.endsWith('/reset') && method === 'POST') {
710
784
  await handleResetRateLimitResource(res, client.rateLimit, subPath);
711
785
  return true;
712
786
  }
713
- const isSingleResource = subPath.startsWith("/rate-limit/resources/") && subPath.split("/").length === 4;
714
- if (isSingleResource && method === "GET") {
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
- ".html": "text/html",
728
- ".js": "application/javascript",
729
- ".css": "text/css",
730
- ".json": "application/json",
731
- ".png": "image/png",
732
- ".svg": "image/svg+xml",
733
- ".ico": "image/x-icon",
734
- ".woff": "font/woff",
735
- ".woff2": "font/woff2"
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 !== "undefined" ? __dirname : process.cwd();
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, "..", "dist", "client");
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(), "index.html");
830
+ const indexPath = join(getClientDir(), 'index.html');
755
831
  if (existsSync(indexPath)) {
756
- cachedIndexHtml = readFileSync(indexPath, "utf-8");
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 !== "/" && pathname !== "/index.html") {
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] ?? "application/octet-stream";
856
+ const mimeType = MIME_TYPES[ext] ?? 'application/octet-stream';
781
857
  res.writeHead(200, {
782
- "Content-Type": mimeType,
783
- "Content-Length": content.length,
784
- "Cache-Control": pathname.includes("/assets/") ? "public, max-age=31536000, immutable" : "no-cache"
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
- "Content-Type": "text/html",
795
- "Content-Length": Buffer.byteLength(html),
796
- "Cache-Control": "no-cache"
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
- clients.set(clientConfig.name, {
808
- name: clientConfig.name,
809
- cache: clientConfig.cacheStore ? detectCacheAdapter(clientConfig.cacheStore) : void 0,
810
- dedup: clientConfig.dedupeStore ? detectDedupeAdapter(clientConfig.dedupeStore) : void 0,
811
- rateLimit: clientConfig.rateLimitStore ? detectRateLimitAdapter(clientConfig.rateLimitStore) : void 0
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).then((handled) => {
822
- if (!handled) {
823
- serveStatic(res, pathname);
824
- }
825
- }).catch(() => {
826
- res.writeHead(500, { "Content-Type": "application/json" });
827
- res.end(JSON.stringify({ error: "Internal server error" }));
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
- "Content-Type": "application/json",
833
- "Cache-Control": "no-store"
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
- ".html": "text/html",
843
- ".js": "application/javascript",
844
- ".css": "text/css",
845
- ".json": "application/json",
846
- ".png": "image/png",
847
- ".svg": "image/svg+xml",
848
- ".ico": "image/x-icon",
849
- ".woff": "font/woff",
850
- ".woff2": "font/woff2"
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 !== "undefined" ? __dirname : process.cwd();
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, "..", "dist", "client");
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(), "index.html");
955
+ const indexPath = join(getClientDir2(), 'index.html');
870
956
  if (existsSync(indexPath)) {
871
- cachedIndexHtml2 = readFileSync(indexPath, "utf-8");
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 !== "/" && pathname !== "/index.html") {
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] ?? "application/octet-stream";
981
+ const mimeType = MIME_TYPES2[ext] ?? 'application/octet-stream';
896
982
  return new Response(content, {
897
983
  status: 200,
898
984
  headers: {
899
- "Content-Type": mimeType,
900
- "Content-Length": String(content.length),
901
- "Cache-Control": pathname.includes("/assets/") ? "public, max-age=31536000, immutable" : "no-cache"
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
- "Content-Type": "text/html",
913
- "Cache-Control": "no-cache"
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 ? { type: client.cache.type, capabilities: client.cache.capabilities } : null,
921
- dedup: client.dedup ? { type: client.dedup.type, capabilities: client.dedup.capabilities } : null,
922
- rateLimit: client.rateLimit ? {
923
- type: client.rateLimit.type,
924
- capabilities: client.rateLimit.capabilities
925
- } : null
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 === "/api/health" && method === "GET") {
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: "ok",
1029
+ status: 'ok',
937
1030
  clients,
938
- pollIntervalMs: ctx.pollIntervalMs
1031
+ pollIntervalMs: ctx.pollIntervalMs,
939
1032
  });
940
1033
  }
941
- if (pathname === "/api/clients" && method === "GET") {
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("/api/")) {
959
- return errorResponse("Not found", 404);
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("/cache")) {
965
- if (!client.cache) return errorResponse("Cache store not configured", 404);
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("/dedup")) {
969
- if (!client.dedup) return errorResponse("Dedup store not configured", 404);
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("/rate-limit")) {
1065
+ if (subPath.startsWith('/rate-limit')) {
973
1066
  if (!client.rateLimit)
974
- return errorResponse("Rate limit store not configured", 404);
1067
+ return errorResponse('Rate limit store not configured', 404);
975
1068
  return routeRateLimit(request, subPath, method, client.rateLimit);
976
1069
  }
977
- return errorResponse("Not found", 404);
1070
+ return errorResponse('Not found', 404);
978
1071
  }
979
1072
  async function routeCache(pathname, method, adapter, query) {
980
1073
  try {
981
- if (pathname === "/cache/stats" && method === "GET") {
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 === "/cache/entries" && method === "GET") {
986
- const page = parseInt(query.get("page") ?? "0", 10);
987
- const limit = parseInt(query.get("limit") ?? "50", 10);
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 === "/cache/entries" && method === "DELETE") {
1083
+ if (pathname === '/cache/entries' && method === 'DELETE') {
991
1084
  await adapter.clearAll();
992
1085
  return jsonResponse({ cleared: true });
993
1086
  }
994
- const isSingleEntry = pathname.startsWith("/cache/entries/") && pathname.split("/").length === 4;
995
- if (isSingleEntry && method === "GET") {
996
- const hash = extractParam(pathname, "/cache/entries/:hash");
997
- if (!hash) return errorResponse("Not found", 404);
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("Not found", 404);
1094
+ if (entry === void 0) return errorResponse('Not found', 404);
1000
1095
  return jsonResponse({ hash, value: entry });
1001
1096
  }
1002
- if (isSingleEntry && method === "DELETE") {
1003
- const hash = extractParam(pathname, "/cache/entries/:hash");
1004
- if (!hash) return errorResponse("Not found", 404);
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("Method not allowed", 405);
1104
+ return errorResponse('Method not allowed', 405);
1010
1105
  }
1011
1106
  } catch (err) {
1012
- return errorResponse(err instanceof Error ? err.message : "Unknown error");
1107
+ return errorResponse(err instanceof Error ? err.message : 'Unknown error');
1013
1108
  }
1014
- return errorResponse("Not found", 404);
1109
+ return errorResponse('Not found', 404);
1015
1110
  }
1016
1111
  async function routeDedup(pathname, method, adapter, query) {
1017
1112
  try {
1018
- if (pathname === "/dedup/stats" && method === "GET") {
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 === "/dedup/jobs" && method === "GET") {
1023
- const page = parseInt(query.get("page") ?? "0", 10);
1024
- const limit = parseInt(query.get("limit") ?? "50", 10);
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 = pathname.startsWith("/dedup/jobs/") && pathname.split("/").length === 4;
1028
- if (isSingleJob && method === "GET") {
1029
- const hash = extractParam(pathname, "/dedup/jobs/:hash");
1030
- if (!hash) return errorResponse("Not found", 404);
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("Not found", 404);
1128
+ if (!job) return errorResponse('Not found', 404);
1033
1129
  return jsonResponse(job);
1034
1130
  }
1035
1131
  if (isSingleJob) {
1036
- return errorResponse("Method not allowed", 405);
1132
+ return errorResponse('Method not allowed', 405);
1037
1133
  }
1038
1134
  } catch (err) {
1039
- return errorResponse(err instanceof Error ? err.message : "Unknown error");
1135
+ return errorResponse(err instanceof Error ? err.message : 'Unknown error');
1040
1136
  }
1041
- return errorResponse("Not found", 404);
1137
+ return errorResponse('Not found', 404);
1042
1138
  }
1043
1139
  async function routeRateLimit(request, pathname, method, adapter) {
1044
1140
  try {
1045
- if (pathname === "/rate-limit/stats" && method === "GET") {
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 === "/rate-limit/resources" && method === "GET") {
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("/config") && method === "PUT") {
1054
- const name = extractParam(pathname, "/rate-limit/resources/:name/config");
1055
- if (!name) return errorResponse("Not found", 404);
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("/reset") && method === "POST") {
1061
- const name = extractParam(pathname, "/rate-limit/resources/:name/reset");
1062
- if (!name) return errorResponse("Not found", 404);
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 = pathname.startsWith("/rate-limit/resources/") && pathname.split("/").length === 4;
1067
- if (isSingleResource && method === "GET") {
1068
- const name = extractParam(pathname, "/rate-limit/resources/:name");
1069
- if (!name) return errorResponse("Not found", 404);
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("Method not allowed", 405);
1172
+ return errorResponse('Method not allowed', 405);
1075
1173
  }
1076
1174
  } catch (err) {
1077
- return errorResponse(err instanceof Error ? err.message : "Unknown error");
1175
+ return errorResponse(err instanceof Error ? err.message : 'Unknown error');
1078
1176
  }
1079
- return errorResponse("Not found", 404);
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
- clients.set(clientConfig.name, {
1086
- name: clientConfig.name,
1087
- cache: clientConfig.cacheStore ? detectCacheAdapter(clientConfig.cacheStore) : void 0,
1088
- dedup: clientConfig.dedupeStore ? detectDedupeAdapter(clientConfig.dedupeStore) : void 0,
1089
- rateLimit: clientConfig.rateLimitStore ? detectRateLimitAdapter(clientConfig.rateLimitStore) : void 0
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 !== "/" && pathname.startsWith(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("Internal server error", 500);
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("error", reject);
1228
+ server.on('error', reject);
1124
1229
  server.listen(opts.port, opts.host, () => {
1125
1230
  const addr = server.address();
1126
- const url = typeof addr === "string" ? addr : `http://${opts.host}:${opts.port}`;
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