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