@http-client-toolkit/dashboard 0.1.0

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