@samimcodes/ai-shield 0.1.2 → 0.3.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/dist/index.cjs CHANGED
@@ -1316,6 +1316,787 @@ var TelemetryEmitter = class {
1316
1316
  await this.flush();
1317
1317
  }
1318
1318
  };
1319
+ var DashboardManager = class {
1320
+ basePath;
1321
+ auth;
1322
+ maxStoredEvents;
1323
+ debug;
1324
+ events = [];
1325
+ sseClients = /* @__PURE__ */ new Set();
1326
+ heartbeatTimer = null;
1327
+ stats = {
1328
+ totalRequests: 0,
1329
+ botsIntercepted: 0,
1330
+ goodBotsAllowed: 0,
1331
+ honeypotTriggers: 0,
1332
+ poisonPayloadsServed: 0,
1333
+ estimatedFakeTokens: 0,
1334
+ topBots: {},
1335
+ topRoutes: {}
1336
+ };
1337
+ constructor(options = {}) {
1338
+ this.basePath = options.basePath ?? "/admin/ai-shield";
1339
+ this.auth = options.auth;
1340
+ this.maxStoredEvents = options.maxStoredEvents ?? 100;
1341
+ this.debug = options.debug ?? false;
1342
+ this.heartbeatTimer = setInterval(() => {
1343
+ this.broadcastPing();
1344
+ }, 15e3);
1345
+ if (this.heartbeatTimer.unref) {
1346
+ this.heartbeatTimer.unref();
1347
+ }
1348
+ }
1349
+ /**
1350
+ * Stop background timers and disconnect SSE clients (useful during teardown).
1351
+ */
1352
+ close() {
1353
+ if (this.heartbeatTimer !== null) {
1354
+ clearInterval(this.heartbeatTimer);
1355
+ this.heartbeatTimer = null;
1356
+ }
1357
+ for (const client of this.sseClients) {
1358
+ try {
1359
+ client.end();
1360
+ } catch {
1361
+ }
1362
+ }
1363
+ this.sseClients.clear();
1364
+ }
1365
+ /** Record a normal or bot request to update total counters. */
1366
+ recordRequest() {
1367
+ this.stats.totalRequests++;
1368
+ }
1369
+ /** Record a legitimate search engine pass-through (Googlebot, Bingbot, etc). */
1370
+ recordGoodBot(botName, path) {
1371
+ this.stats.totalRequests++;
1372
+ this.stats.goodBotsAllowed++;
1373
+ this.stats.topBots[botName] = (this.stats.topBots[botName] ?? 0) + 1;
1374
+ this.stats.topRoutes[path] = (this.stats.topRoutes[path] ?? 0) + 1;
1375
+ }
1376
+ /**
1377
+ * Record an intercepted AI crawler or scraper, update stats,
1378
+ * prepend to event log, and push to all active SSE browser tabs.
1379
+ */
1380
+ recordEvent(event) {
1381
+ this.stats.totalRequests++;
1382
+ this.stats.botsIntercepted++;
1383
+ if (event.action === "HONEYPOT_TRAP") {
1384
+ this.stats.honeypotTriggers++;
1385
+ } else {
1386
+ this.stats.poisonPayloadsServed++;
1387
+ this.stats.estimatedFakeTokens += 480;
1388
+ }
1389
+ this.stats.topBots[event.botName] = (this.stats.topBots[event.botName] ?? 0) + 1;
1390
+ this.stats.topRoutes[event.path] = (this.stats.topRoutes[event.path] ?? 0) + 1;
1391
+ const fullEvent = {
1392
+ id: `evt_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
1393
+ timestamp: event.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
1394
+ botName: event.botName,
1395
+ category: event.category,
1396
+ path: event.path,
1397
+ score: event.score,
1398
+ signals: event.signals,
1399
+ ipHash: event.ipHash,
1400
+ userAgent: event.userAgent,
1401
+ action: event.action
1402
+ };
1403
+ this.events.unshift(fullEvent);
1404
+ if (this.events.length > this.maxStoredEvents) {
1405
+ this.events.length = this.maxStoredEvents;
1406
+ }
1407
+ this.broadcastEvent(fullEvent);
1408
+ return fullEvent;
1409
+ }
1410
+ /** Retrieve snapshot of current metrics. */
1411
+ getStats() {
1412
+ return { ...this.stats, topBots: { ...this.stats.topBots }, topRoutes: { ...this.stats.topRoutes } };
1413
+ }
1414
+ /** Retrieve recent security events. */
1415
+ getEvents() {
1416
+ return [...this.events];
1417
+ }
1418
+ /** Broadcast SSE event to all connected dashboard tabs. */
1419
+ broadcastEvent(event) {
1420
+ const payload = `data: ${JSON.stringify({ type: "event", event, stats: this.getStats() })}
1421
+
1422
+ `;
1423
+ for (const client of this.sseClients) {
1424
+ try {
1425
+ client.write(payload);
1426
+ } catch {
1427
+ this.sseClients.delete(client);
1428
+ }
1429
+ }
1430
+ }
1431
+ /** Broadcast periodic ping to keep SSE connections open through proxies. */
1432
+ broadcastPing() {
1433
+ if (this.sseClients.size === 0) return;
1434
+ const payload = `: ping
1435
+
1436
+ `;
1437
+ for (const client of this.sseClients) {
1438
+ try {
1439
+ client.write(payload);
1440
+ } catch {
1441
+ this.sseClients.delete(client);
1442
+ }
1443
+ }
1444
+ }
1445
+ /** Authenticate request if dashboardAuth is configured. */
1446
+ checkAuth(req, res, next) {
1447
+ if (!this.auth) {
1448
+ next();
1449
+ return;
1450
+ }
1451
+ if (this.auth.token) {
1452
+ const authHeader = req.headers["authorization"];
1453
+ const apiKeyHeader = req.headers["x-ai-shield-key"];
1454
+ const tokenQuery = req.query["token"];
1455
+ if (tokenQuery === this.auth.token || apiKeyHeader === this.auth.token) {
1456
+ next();
1457
+ return;
1458
+ }
1459
+ if (authHeader && authHeader.startsWith("Bearer ") && authHeader.slice(7) === this.auth.token) {
1460
+ next();
1461
+ return;
1462
+ }
1463
+ }
1464
+ if (this.auth.username && this.auth.password) {
1465
+ const authHeader = req.headers["authorization"];
1466
+ if (authHeader && authHeader.startsWith("Basic ")) {
1467
+ const credentials = Buffer.from(authHeader.slice(6), "base64").toString("utf8");
1468
+ const [user, pass] = credentials.split(":");
1469
+ if (user === this.auth.username && pass === this.auth.password) {
1470
+ next();
1471
+ return;
1472
+ }
1473
+ }
1474
+ res.setHeader("WWW-Authenticate", 'Basic realm="AI Shield Security Dashboard"');
1475
+ res.status(401).send("401 Unauthorized: Invalid AI Shield dashboard credentials.");
1476
+ return;
1477
+ }
1478
+ if (this.auth.token) {
1479
+ res.status(401).json({ error: "Unauthorized: Missing or invalid token." });
1480
+ return;
1481
+ }
1482
+ next();
1483
+ }
1484
+ /** Mount and return Express router serving the live dashboard and API. */
1485
+ buildRouter() {
1486
+ const router = express.Router();
1487
+ router.use((req, res, next) => this.checkAuth(req, res, next));
1488
+ router.get("/api/stats", (_req, res) => {
1489
+ res.setHeader("Cache-Control", "no-store");
1490
+ res.json({
1491
+ success: true,
1492
+ stats: this.getStats(),
1493
+ recentEvents: this.getEvents()
1494
+ });
1495
+ });
1496
+ router.get("/events", (req, res) => {
1497
+ res.setHeader("Content-Type", "text/event-stream");
1498
+ res.setHeader("Cache-Control", "no-cache, no-transform");
1499
+ res.setHeader("Connection", "keep-alive");
1500
+ res.setHeader("X-Accel-Buffering", "no");
1501
+ res.flushHeaders?.();
1502
+ res.write(`data: ${JSON.stringify({ type: "init", stats: this.getStats(), events: this.getEvents() })}
1503
+
1504
+ `);
1505
+ this.sseClients.add(res);
1506
+ req.on("close", () => {
1507
+ this.sseClients.delete(res);
1508
+ });
1509
+ });
1510
+ router.post("/api/test-event", (_req, res) => {
1511
+ const defaultChoice = {
1512
+ name: "GPTBot (OpenAI)",
1513
+ path: "/api/v1/products",
1514
+ cat: "AI_CRAWLER" /* AI_CRAWLER */,
1515
+ signals: ["UA_PATTERN_MATCH", "MISSING_ACCEPT_LANGUAGE"]
1516
+ };
1517
+ const testBots = [
1518
+ defaultChoice,
1519
+ { name: "Claude-Web (Anthropic)", path: "/catalog/pricing", cat: "AI_CRAWLER" /* AI_CRAWLER */, signals: ["UA_PATTERN_MATCH"] },
1520
+ { name: "ByteSpider (TikTok)", path: "/content/articles", cat: "SCRAPER" /* SCRAPER */, signals: ["UA_PATTERN_MATCH", "SUSPICIOUS_UA"] },
1521
+ { name: "Honeypot Trap Route", path: "/api/internal/telemetry-sync", cat: "AI_CRAWLER" /* AI_CRAWLER */, signals: ["HONEYPOT_TRIGGERED"] }
1522
+ ];
1523
+ const choice = testBots[Math.floor(Math.random() * testBots.length)] ?? defaultChoice;
1524
+ const event = this.recordEvent({
1525
+ botName: choice.name,
1526
+ category: choice.cat,
1527
+ path: choice.path,
1528
+ score: choice.name.includes("Honeypot") ? 100 : 95,
1529
+ signals: choice.signals,
1530
+ ipHash: Math.random().toString(16).slice(2, 10) + "...",
1531
+ userAgent: `Simulated/${choice.name.split(" ")[0]} Bot Simulator`,
1532
+ action: choice.name.includes("Honeypot") ? "HONEYPOT_TRAP" : "POISON_JSON"
1533
+ });
1534
+ res.json({ success: true, event });
1535
+ });
1536
+ router.get("/", (_req, res) => {
1537
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
1538
+ res.setHeader("Cache-Control", "no-store");
1539
+ res.send(this.renderHtml());
1540
+ });
1541
+ return router;
1542
+ }
1543
+ /** Render self-contained, responsive, dark-mode single-page HTML dashboard. */
1544
+ renderHtml() {
1545
+ const basePath = this.basePath;
1546
+ return `<!DOCTYPE html>
1547
+ <html lang="en">
1548
+ <head>
1549
+ <meta charset="UTF-8">
1550
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1551
+ <title>AI Shield \u2014 Live Security Monitor</title>
1552
+ <link rel="preconnect" href="https://fonts.googleapis.com">
1553
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1554
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
1555
+ <style>
1556
+ :root {
1557
+ --bg: #090d16;
1558
+ --card-bg: rgba(17, 24, 39, 0.75);
1559
+ --card-border: rgba(255, 255, 255, 0.08);
1560
+ --primary: #6366f1;
1561
+ --primary-hover: #4f46e5;
1562
+ --accent: #10b981;
1563
+ --danger: #f43f5e;
1564
+ --warning: #f59e0b;
1565
+ --text: #f3f4f6;
1566
+ --text-muted: #9ca3af;
1567
+ }
1568
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1569
+ body {
1570
+ background-color: var(--bg);
1571
+ color: var(--text);
1572
+ font-family: 'Inter', sans-serif;
1573
+ min-height: 100vh;
1574
+ display: flex;
1575
+ flex-direction: column;
1576
+ overflow-x: hidden;
1577
+ background-image: radial-gradient(circle at 50% 0%, rgba(99, 102, 241, 0.12) 0%, transparent 60%);
1578
+ }
1579
+ header {
1580
+ padding: 1.25rem 2rem;
1581
+ border-bottom: 1px solid var(--card-border);
1582
+ display: flex;
1583
+ justify-content: space-between;
1584
+ align-items: center;
1585
+ background: rgba(9, 13, 22, 0.85);
1586
+ backdrop-filter: blur(12px);
1587
+ position: sticky;
1588
+ top: 0;
1589
+ z-index: 50;
1590
+ }
1591
+ .brand {
1592
+ display: flex;
1593
+ align-items: center;
1594
+ gap: 0.75rem;
1595
+ font-weight: 700;
1596
+ font-size: 1.25rem;
1597
+ letter-spacing: -0.02em;
1598
+ }
1599
+ .shield-icon {
1600
+ width: 32px;
1601
+ height: 32px;
1602
+ background: linear-gradient(135deg, #6366f1, #a855f7);
1603
+ border-radius: 8px;
1604
+ display: flex;
1605
+ align-items: center;
1606
+ justify-content: center;
1607
+ box-shadow: 0 0 16px rgba(99, 102, 241, 0.4);
1608
+ }
1609
+ .badge-live {
1610
+ display: inline-flex;
1611
+ align-items: center;
1612
+ gap: 0.5rem;
1613
+ padding: 0.25rem 0.75rem;
1614
+ border-radius: 9999px;
1615
+ font-size: 0.75rem;
1616
+ font-weight: 600;
1617
+ background: rgba(16, 185, 129, 0.12);
1618
+ color: #34d399;
1619
+ border: 1px solid rgba(16, 185, 129, 0.3);
1620
+ }
1621
+ .pulse-dot {
1622
+ width: 8px;
1623
+ height: 8px;
1624
+ border-radius: 50%;
1625
+ background: #10b981;
1626
+ box-shadow: 0 0 8px #10b981;
1627
+ animation: pulse 1.8s infinite;
1628
+ }
1629
+ @keyframes pulse {
1630
+ 0%, 100% { opacity: 1; transform: scale(1); }
1631
+ 50% { opacity: 0.4; transform: scale(0.85); }
1632
+ }
1633
+ .header-actions {
1634
+ display: flex;
1635
+ gap: 0.75rem;
1636
+ align-items: center;
1637
+ }
1638
+ .btn {
1639
+ padding: 0.5rem 1rem;
1640
+ border-radius: 8px;
1641
+ font-size: 0.875rem;
1642
+ font-weight: 600;
1643
+ cursor: pointer;
1644
+ transition: all 0.2s ease;
1645
+ display: inline-flex;
1646
+ align-items: center;
1647
+ gap: 0.5rem;
1648
+ border: 1px solid transparent;
1649
+ }
1650
+ .btn-primary {
1651
+ background: linear-gradient(135deg, #6366f1, #8b5cf6);
1652
+ color: white;
1653
+ box-shadow: 0 0 15px rgba(99, 102, 241, 0.3);
1654
+ }
1655
+ .btn-primary:hover {
1656
+ opacity: 0.92;
1657
+ transform: translateY(-1px);
1658
+ }
1659
+ .btn-outline {
1660
+ background: rgba(255, 255, 255, 0.05);
1661
+ border-color: var(--card-border);
1662
+ color: var(--text);
1663
+ }
1664
+ .btn-outline:hover {
1665
+ background: rgba(255, 255, 255, 0.1);
1666
+ }
1667
+ main {
1668
+ flex: 1;
1669
+ padding: 2rem;
1670
+ max-width: 1400px;
1671
+ width: 100%;
1672
+ margin: 0 auto;
1673
+ display: flex;
1674
+ flex-direction: column;
1675
+ gap: 2rem;
1676
+ }
1677
+ .metrics-grid {
1678
+ display: grid;
1679
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
1680
+ gap: 1.25rem;
1681
+ }
1682
+ .card {
1683
+ background: var(--card-bg);
1684
+ border: 1px solid var(--card-border);
1685
+ border-radius: 12px;
1686
+ padding: 1.25rem 1.5rem;
1687
+ backdrop-filter: blur(16px);
1688
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.24);
1689
+ display: flex;
1690
+ flex-direction: column;
1691
+ gap: 0.5rem;
1692
+ position: relative;
1693
+ overflow: hidden;
1694
+ }
1695
+ .card::before {
1696
+ content: '';
1697
+ position: absolute;
1698
+ top: 0; left: 0; right: 0;
1699
+ height: 2px;
1700
+ background: linear-gradient(90deg, transparent, rgba(99, 102, 241, 0.4), transparent);
1701
+ }
1702
+ .card-title {
1703
+ font-size: 0.8125rem;
1704
+ font-weight: 600;
1705
+ color: var(--text-muted);
1706
+ text-transform: uppercase;
1707
+ letter-spacing: 0.05em;
1708
+ }
1709
+ .card-val {
1710
+ font-size: 2rem;
1711
+ font-weight: 700;
1712
+ color: #fff;
1713
+ font-variant-numeric: tabular-nums;
1714
+ }
1715
+ .card-sub {
1716
+ font-size: 0.75rem;
1717
+ color: var(--text-muted);
1718
+ display: flex;
1719
+ align-items: center;
1720
+ gap: 0.25rem;
1721
+ }
1722
+ .text-emerald { color: #34d399; }
1723
+ .text-rose { color: #fb7185; }
1724
+ .text-amber { color: #fbbf24; }
1725
+ .text-indigo { color: #818cf8; }
1726
+
1727
+ /* Alert Banner Toast */
1728
+ #alert-toast {
1729
+ position: fixed;
1730
+ bottom: 2rem;
1731
+ right: 2rem;
1732
+ max-width: 440px;
1733
+ width: calc(100% - 4rem);
1734
+ background: rgba(17, 24, 39, 0.95);
1735
+ border: 1px solid #f43f5e;
1736
+ border-radius: 12px;
1737
+ padding: 1rem 1.25rem;
1738
+ box-shadow: 0 12px 40px rgba(244, 63, 94, 0.35);
1739
+ backdrop-filter: blur(20px);
1740
+ z-index: 100;
1741
+ display: none;
1742
+ animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1);
1743
+ }
1744
+ @keyframes slideIn {
1745
+ from { transform: translateY(20px); opacity: 0; }
1746
+ to { transform: translateY(0); opacity: 1; }
1747
+ }
1748
+ .toast-header {
1749
+ display: flex;
1750
+ align-items: center;
1751
+ justify-content: space-between;
1752
+ margin-bottom: 0.25rem;
1753
+ }
1754
+ .toast-tag {
1755
+ font-size: 0.75rem;
1756
+ font-weight: 700;
1757
+ color: #f43f5e;
1758
+ text-transform: uppercase;
1759
+ letter-spacing: 0.05em;
1760
+ }
1761
+ .toast-body {
1762
+ font-size: 0.875rem;
1763
+ line-height: 1.4;
1764
+ color: #e5e7eb;
1765
+ }
1766
+
1767
+ /* Feed Table */
1768
+ .table-container {
1769
+ background: var(--card-bg);
1770
+ border: 1px solid var(--card-border);
1771
+ border-radius: 12px;
1772
+ overflow: hidden;
1773
+ backdrop-filter: blur(16px);
1774
+ }
1775
+ .table-header {
1776
+ padding: 1.25rem 1.5rem;
1777
+ border-bottom: 1px solid var(--card-border);
1778
+ display: flex;
1779
+ justify-content: space-between;
1780
+ align-items: center;
1781
+ }
1782
+ .table-title {
1783
+ font-size: 1.125rem;
1784
+ font-weight: 600;
1785
+ }
1786
+ table {
1787
+ width: 100%;
1788
+ border-collapse: collapse;
1789
+ text-align: left;
1790
+ font-size: 0.875rem;
1791
+ }
1792
+ th {
1793
+ padding: 0.875rem 1.25rem;
1794
+ background: rgba(255, 255, 255, 0.02);
1795
+ color: var(--text-muted);
1796
+ font-weight: 600;
1797
+ font-size: 0.75rem;
1798
+ text-transform: uppercase;
1799
+ letter-spacing: 0.05em;
1800
+ border-bottom: 1px solid var(--card-border);
1801
+ }
1802
+ td {
1803
+ padding: 1rem 1.25rem;
1804
+ border-bottom: 1px solid rgba(255, 255, 255, 0.04);
1805
+ color: #d1d5db;
1806
+ }
1807
+ tr:last-child td { border-bottom: none; }
1808
+ tr:hover td { background: rgba(255, 255, 255, 0.02); }
1809
+ .mono { font-family: 'JetBrains Mono', monospace; font-size: 0.8125rem; }
1810
+ .badge {
1811
+ display: inline-flex;
1812
+ align-items: center;
1813
+ padding: 0.2rem 0.6rem;
1814
+ border-radius: 6px;
1815
+ font-size: 0.75rem;
1816
+ font-weight: 600;
1817
+ }
1818
+ .badge-danger { background: rgba(244, 63, 94, 0.15); color: #fb7185; border: 1px solid rgba(244, 63, 94, 0.3); }
1819
+ .badge-amber { background: rgba(245, 158, 11, 0.15); color: #fbbf24; border: 1px solid rgba(245, 158, 11, 0.3); }
1820
+ .badge-emerald { background: rgba(16, 185, 129, 0.15); color: #34d399; border: 1px solid rgba(16, 185, 129, 0.3); }
1821
+ .empty-state {
1822
+ padding: 4rem 2rem;
1823
+ text-align: center;
1824
+ color: var(--text-muted);
1825
+ }
1826
+ @keyframes flashRow {
1827
+ 0% { background: rgba(99, 102, 241, 0.25); }
1828
+ 100% { background: transparent; }
1829
+ }
1830
+ .new-row {
1831
+ animation: flashRow 2s ease-out;
1832
+ }
1833
+ </style>
1834
+ </head>
1835
+ <body>
1836
+
1837
+ <header>
1838
+ <div class="brand">
1839
+ <div class="shield-icon">
1840
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
1841
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
1842
+ </svg>
1843
+ </div>
1844
+ <span>ai-shield <span style="font-weight:400; opacity:0.6; font-size: 0.9rem;">Live Monitor</span></span>
1845
+ <div class="badge-live">
1846
+ <div class="pulse-dot"></div>
1847
+ ACTIVE DEFENSE
1848
+ </div>
1849
+ </div>
1850
+ <div class="header-actions">
1851
+ <button class="btn btn-outline" id="audioToggle">
1852
+ <span id="audioIcon">\u{1F514} Sound ON</span>
1853
+ </button>
1854
+ <button class="btn btn-primary" id="testAttackBtn">
1855
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
1856
+ Simulate Bot Attack
1857
+ </button>
1858
+ </div>
1859
+ </header>
1860
+
1861
+ <main>
1862
+ <!-- Metrics Row -->
1863
+ <div class="metrics-grid">
1864
+ <div class="card">
1865
+ <div class="card-title">Bots Intercepted</div>
1866
+ <div class="card-val text-rose" id="m-intercepted">0</div>
1867
+ <div class="card-sub text-rose">
1868
+ <span>\u25CF</span> Fed synthetic poisoned responses (HTTP 200)
1869
+ </div>
1870
+ </div>
1871
+ <div class="card">
1872
+ <div class="card-title">Honeypot Traps Sprung</div>
1873
+ <div class="card-val text-amber" id="m-honeypot">0</div>
1874
+ <div class="card-sub text-amber">
1875
+ <span>\u25CF</span> Scrapers quarantined via hidden routes
1876
+ </div>
1877
+ </div>
1878
+ <div class="card">
1879
+ <div class="card-title">Search Engines Allowed (SEO)</div>
1880
+ <div class="card-val text-emerald" id="m-goodbots">0</div>
1881
+ <div class="card-sub text-emerald">
1882
+ <span>\u25CF</span> 0% false positives, 100% crawl integrity
1883
+ </div>
1884
+ </div>
1885
+ <div class="card">
1886
+ <div class="card-title">Fake Tokens Injected</div>
1887
+ <div class="card-val text-indigo" id="m-tokens">0</div>
1888
+ <div class="card-sub text-indigo">
1889
+ <span>\u25CF</span> AI scrapers polluted with synthetic catalog data
1890
+ </div>
1891
+ </div>
1892
+ </div>
1893
+
1894
+ <!-- Live Event Feed Table -->
1895
+ <div class="table-container">
1896
+ <div class="table-header">
1897
+ <div class="table-title">Real-Time Threat Interception Stream</div>
1898
+ <div class="badge-live" style="font-size: 0.7rem;">
1899
+ <span id="conn-status">Connecting to stream...</span>
1900
+ </div>
1901
+ </div>
1902
+ <table>
1903
+ <thead>
1904
+ <tr>
1905
+ <th>Time</th>
1906
+ <th>Bot / Persona</th>
1907
+ <th>Threat Score</th>
1908
+ <th>Route Attacked</th>
1909
+ <th>Defense Action</th>
1910
+ <th>Client Hash</th>
1911
+ </tr>
1912
+ </thead>
1913
+ <tbody id="event-rows">
1914
+ <tr>
1915
+ <td colspan="6" class="empty-state">
1916
+ Listening for incoming crawlers... Waiting for bot traffic.
1917
+ </td>
1918
+ </tr>
1919
+ </tbody>
1920
+ </table>
1921
+ </div>
1922
+ </main>
1923
+
1924
+ <!-- Live Pop-up Alert Toast -->
1925
+ <div id="alert-toast">
1926
+ <div class="toast-header">
1927
+ <span class="toast-tag">\u{1F6A8} AI Crawler Intercepted</span>
1928
+ <span class="mono" style="font-size: 0.75rem; color: #9ca3af;" id="toast-time">Just now</span>
1929
+ </div>
1930
+ <div class="toast-body" id="toast-body">
1931
+ <strong>GPTBot</strong> attempted to scrape <span class="mono">/api/products</span>.
1932
+ Synthetic poisoned JSON served with HTTP 200.
1933
+ </div>
1934
+ </div>
1935
+
1936
+ <script>
1937
+ const basePath = '${basePath}';
1938
+ let soundEnabled = true;
1939
+ let audioCtx = null;
1940
+
1941
+ // Synthesize gentle sci-fi alert chime via Web Audio API (no external file dependencies)
1942
+ function playAlertChime() {
1943
+ if (!soundEnabled) return;
1944
+ try {
1945
+ if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
1946
+ if (audioCtx.state === 'suspended') audioCtx.resume();
1947
+ const now = audioCtx.currentTime;
1948
+ const osc = audioCtx.createOscillator();
1949
+ const gain = audioCtx.createGain();
1950
+ osc.type = 'sine';
1951
+ osc.frequency.setValueAtTime(587.33, now); // D5
1952
+ osc.frequency.exponentialRampToValueAtTime(880, now + 0.12); // A5
1953
+ gain.gain.setValueAtTime(0.08, now);
1954
+ gain.gain.exponentialRampToValueAtTime(0.001, now + 0.35);
1955
+ osc.connect(gain);
1956
+ gain.connect(audioCtx.destination);
1957
+ osc.start(now);
1958
+ osc.stop(now + 0.35);
1959
+ } catch (e) {
1960
+ console.warn('Audio notice:', e);
1961
+ }
1962
+ }
1963
+
1964
+ // Toggle audio
1965
+ document.getElementById('audioToggle').addEventListener('click', () => {
1966
+ soundEnabled = !soundEnabled;
1967
+ document.getElementById('audioIcon').innerText = soundEnabled ? '\u{1F514} Sound ON' : '\u{1F515} Sound OFF';
1968
+ });
1969
+
1970
+ // Simulate bot attack button
1971
+ document.getElementById('testAttackBtn').addEventListener('click', async () => {
1972
+ try {
1973
+ const res = await fetch(basePath + '/api/test-event', { method: 'POST' });
1974
+ const data = await res.json();
1975
+ console.log('Simulated attack triggered:', data);
1976
+ } catch (err) {
1977
+ console.error('Test attack failed:', err);
1978
+ }
1979
+ });
1980
+
1981
+ // Update metric cards
1982
+ function updateStats(stats) {
1983
+ if (!stats) return;
1984
+ document.getElementById('m-intercepted').innerText = Number(stats.botsIntercepted || 0).toLocaleString();
1985
+ document.getElementById('m-honeypot').innerText = Number(stats.honeypotTriggers || 0).toLocaleString();
1986
+ document.getElementById('m-goodbots').innerText = Number(stats.goodBotsAllowed || 0).toLocaleString();
1987
+ document.getElementById('m-tokens').innerText = Number(stats.estimatedFakeTokens || 0).toLocaleString();
1988
+ }
1989
+
1990
+ // Show floating alert toast
1991
+ let toastTimer = null;
1992
+ function triggerAlertToast(evt) {
1993
+ const toast = document.getElementById('alert-toast');
1994
+ const body = document.getElementById('toast-body');
1995
+ const time = document.getElementById('toast-time');
1996
+
1997
+ time.innerText = new Date(evt.timestamp).toLocaleTimeString();
1998
+ body.innerHTML = '<strong>' + escapeHtml(evt.botName) + '</strong> targeted <span class="mono">' + escapeHtml(evt.path) + '</span>.<br>' +
1999
+ 'Action: <span class="badge badge-danger">' + escapeHtml(evt.action) + '</span> (HTTP 200 OK)';
2000
+
2001
+ toast.style.display = 'block';
2002
+ playAlertChime();
2003
+
2004
+ if (toastTimer) clearTimeout(toastTimer);
2005
+ toastTimer = setTimeout(() => {
2006
+ toast.style.display = 'none';
2007
+ }, 4500);
2008
+ }
2009
+
2010
+ function escapeHtml(str) {
2011
+ return String(str || '').replace(/[&<>'"]/g, tag => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' }[tag] || tag));
2012
+ }
2013
+
2014
+ // Render table rows
2015
+ function renderRows(events, isNew) {
2016
+ const tbody = document.getElementById('event-rows');
2017
+ if (!events || events.length === 0) return;
2018
+
2019
+ tbody.innerHTML = events.map((evt, idx) => {
2020
+ const timeStr = new Date(evt.timestamp).toLocaleTimeString();
2021
+ const badgeClass = evt.action.includes('HONEYPOT') ? 'badge-amber' : 'badge-danger';
2022
+ const flashClass = isNew && idx === 0 ? 'new-row' : '';
2023
+
2024
+ return '<tr class="' + flashClass + '">' +
2025
+ '<td class="mono">' + timeStr + '</td>' +
2026
+ '<td><strong>' + escapeHtml(evt.botName) + '</strong></td>' +
2027
+ '<td><span class="mono">' + evt.score + '/100</span></td>' +
2028
+ '<td class="mono text-indigo">' + escapeHtml(evt.path) + '</td>' +
2029
+ '<td><span class="badge ' + badgeClass + '">' + escapeHtml(evt.action) + '</span></td>' +
2030
+ '<td class="mono" style="opacity:0.7;">' + escapeHtml(evt.ipHash) + '</td>' +
2031
+ '</tr>';
2032
+ }).join('');
2033
+ }
2034
+
2035
+ // Connect SSE stream
2036
+ function initSSE() {
2037
+ const connStatus = document.getElementById('conn-status');
2038
+ let es = null;
2039
+
2040
+ try {
2041
+ es = new EventSource(basePath + '/events');
2042
+
2043
+ es.onopen = () => {
2044
+ connStatus.innerText = '\u25CF Live EventSource Active';
2045
+ connStatus.style.color = '#34d399';
2046
+ };
2047
+
2048
+ es.onmessage = (msg) => {
2049
+ try {
2050
+ const data = JSON.parse(msg.data);
2051
+ if (data.type === 'init') {
2052
+ updateStats(data.stats);
2053
+ renderRows(data.events, false);
2054
+ } else if (data.type === 'event') {
2055
+ updateStats(data.stats);
2056
+ triggerAlertToast(data.event);
2057
+ // Fetch latest list or unshift
2058
+ fetch(basePath + '/api/stats')
2059
+ .then(r => r.json())
2060
+ .then(res => {
2061
+ if (res.recentEvents) renderRows(res.recentEvents, true);
2062
+ });
2063
+ }
2064
+ } catch (e) {
2065
+ console.error('SSE parse error:', e);
2066
+ }
2067
+ };
2068
+
2069
+ es.onerror = () => {
2070
+ connStatus.innerText = '\u25CF Polling Fallback';
2071
+ connStatus.style.color = '#fbbf24';
2072
+ es.close();
2073
+ // Fallback polling every 3 seconds
2074
+ setInterval(fetchLatestStats, 3000);
2075
+ };
2076
+ } catch (err) {
2077
+ connStatus.innerText = '\u25CF Polling Mode';
2078
+ setInterval(fetchLatestStats, 3000);
2079
+ }
2080
+ }
2081
+
2082
+ async function fetchLatestStats() {
2083
+ try {
2084
+ const res = await fetch(basePath + '/api/stats');
2085
+ const data = await res.json();
2086
+ if (data.stats) updateStats(data.stats);
2087
+ if (data.recentEvents) renderRows(data.recentEvents, false);
2088
+ } catch (err) {
2089
+ // silent
2090
+ }
2091
+ }
2092
+
2093
+ // Start on page load
2094
+ initSSE();
2095
+ </script>
2096
+ </body>
2097
+ </html>`;
2098
+ }
2099
+ };
1319
2100
 
1320
2101
  // src/middleware.ts
1321
2102
  var DEFAULT_SCORE_THRESHOLD = 50;
@@ -1332,11 +2113,16 @@ function createAiShield(options = {}) {
1332
2113
  honeypotTtlMs = 24 * 60 * 60 * 1e3,
1333
2114
  debug = false,
1334
2115
  apiKey,
1335
- dashboardUrl
2116
+ dashboardUrl,
2117
+ enableDashboard = false,
2118
+ dashboardPath = "/admin/ai-shield",
2119
+ dashboardAuth
1336
2120
  } = options;
1337
2121
  const detector = new BotDetector(customFingerprints, signalWeights);
1338
2122
  const poisonEngine = new PoisonEngine(poisonContentType);
1339
2123
  const telemetry = apiKey !== void 0 ? new TelemetryEmitter({ apiKey, dashboardUrl }) : void 0;
2124
+ const dashboard = enableDashboard ? new DashboardManager({ basePath: dashboardPath, auth: dashboardAuth, debug }) : void 0;
2125
+ const dashboardRouter = dashboard ? dashboard.buildRouter() : void 0;
1340
2126
  const honeypot = new HoneypotManager(poisonEngine, {
1341
2127
  customRoutes: honeypotPaths,
1342
2128
  ttlMs: honeypotTtlMs,
@@ -1347,6 +2133,18 @@ function createAiShield(options = {}) {
1347
2133
  `[ai-shield] Honeypot trap: "${route.label}" | IP: ${ip} | UA: ${ua.slice(0, 80)}`
1348
2134
  );
1349
2135
  }
2136
+ if (dashboard !== void 0) {
2137
+ dashboard.recordEvent({
2138
+ botName: `HoneypotTrap:${route.label}`,
2139
+ category: "AI_CRAWLER" /* AI_CRAWLER */,
2140
+ path: route.path,
2141
+ score: 100,
2142
+ signals: ["HONEYPOT_TRIGGERED"],
2143
+ ipHash: hashClientIp(ip),
2144
+ userAgent: ua,
2145
+ action: "HONEYPOT_TRAP"
2146
+ });
2147
+ }
1350
2148
  if (telemetry !== void 0) {
1351
2149
  telemetry.emit({
1352
2150
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1370,6 +2168,17 @@ function createAiShield(options = {}) {
1370
2168
  const honeypotRouter = honeypot.buildRouter();
1371
2169
  const excludeSet = new Set(excludePaths);
1372
2170
  const shield = (req, res, next) => {
2171
+ if (dashboardRouter !== void 0 && (req.path === dashboardPath || req.path.startsWith(dashboardPath + "/"))) {
2172
+ const originalUrl = req.url;
2173
+ const subPath = req.path.slice(dashboardPath.length) || "/";
2174
+ const query = req.url.includes("?") ? req.url.slice(req.url.indexOf("?")) : "";
2175
+ req.url = subPath + query;
2176
+ dashboardRouter(req, res, (err) => {
2177
+ req.url = originalUrl;
2178
+ next(err);
2179
+ });
2180
+ return;
2181
+ }
1373
2182
  if (excludeSet.has(req.path)) {
1374
2183
  next();
1375
2184
  return;
@@ -1384,6 +2193,7 @@ function createAiShield(options = {}) {
1384
2193
  const meta = { detection, poisoned: false };
1385
2194
  req.aiShield = meta;
1386
2195
  if (detection.category === "GOOD_BOT" /* GOOD_BOT */) {
2196
+ dashboard?.recordGoodBot(detection.label, req.path);
1387
2197
  if (debug) {
1388
2198
  console.info(
1389
2199
  `[ai-shield] Good bot allowed: ${detection.label} | ${req.method} ${req.path}`
@@ -1393,6 +2203,7 @@ function createAiShield(options = {}) {
1393
2203
  return;
1394
2204
  }
1395
2205
  if (detection.score < scoreThreshold && !isHoneypotFlagged) {
2206
+ dashboard?.recordRequest();
1396
2207
  if (debug && detection.score > 0) {
1397
2208
  console.info(
1398
2209
  `[ai-shield] Below threshold (${detection.score} < ${scoreThreshold}): ${detection.label} | ${req.path}`
@@ -1428,6 +2239,18 @@ function createAiShield(options = {}) {
1428
2239
  } else if (accept.includes("application/json") && poisonContentType === "HTML") {
1429
2240
  effectiveType = "JSON";
1430
2241
  }
2242
+ if (dashboard !== void 0) {
2243
+ dashboard.recordEvent({
2244
+ botName: detection.label,
2245
+ category: detection.category,
2246
+ path: req.path,
2247
+ score: detection.score,
2248
+ signals: detection.signals,
2249
+ ipHash: hashClientIp(clientIp),
2250
+ userAgent: req.headers["user-agent"] ?? "",
2251
+ action: effectiveType === "JSON" ? "POISON_JSON" : effectiveType === "SITEMAP" ? "POISON_SITEMAP" : "POISON_HTML"
2252
+ });
2253
+ }
1431
2254
  const payload = poisonEngine.generate(req.path, effectiveType);
1432
2255
  let responseBody = payload.body;
1433
2256
  if (injectHoneypotLinks && effectiveType === "HTML") {
@@ -1442,6 +2265,7 @@ function createAiShield(options = {}) {
1442
2265
  });
1443
2266
  };
1444
2267
  shield.telemetry = telemetry;
2268
+ shield.dashboard = dashboard;
1445
2269
  return shield;
1446
2270
  }
1447
2271
  var aiShieldMiddleware = createAiShield;
@@ -1474,6 +2298,7 @@ exports.BotDetector = BotDetector;
1474
2298
  exports.CANONICAL_TRAP = CANONICAL_TRAP;
1475
2299
  exports.Confidence = Confidence;
1476
2300
  exports.DEFAULT_SIGNAL_WEIGHTS = DEFAULT_SIGNAL_WEIGHTS;
2301
+ exports.DashboardManager = DashboardManager;
1477
2302
  exports.HoneypotManager = HoneypotManager;
1478
2303
  exports.IpTrapCache = IpTrapCache;
1479
2304
  exports.PoisonEngine = PoisonEngine;