web-agent-bridge 2.3.1 → 2.5.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.
Files changed (53) hide show
  1. package/README.ar.md +524 -31
  2. package/README.md +592 -47
  3. package/bin/agent-runner.js +10 -1
  4. package/package.json +1 -1
  5. package/public/agent-workspace.html +347 -0
  6. package/public/browser.html +484 -0
  7. package/public/css/agent-workspace.css +1713 -0
  8. package/public/index.html +94 -0
  9. package/public/js/agent-workspace.js +1740 -0
  10. package/sdk/index.d.ts +253 -0
  11. package/sdk/index.js +360 -1
  12. package/sdk/package.json +1 -1
  13. package/server/config/secrets.js +13 -5
  14. package/server/control-plane/index.js +301 -0
  15. package/server/data-plane/index.js +354 -0
  16. package/server/index.js +185 -4
  17. package/server/llm/index.js +404 -0
  18. package/server/middleware/adminAuth.js +6 -1
  19. package/server/middleware/auth.js +11 -2
  20. package/server/middleware/rateLimits.js +78 -2
  21. package/server/migrations/003_ads_integer_cents.sql +33 -0
  22. package/server/models/db.js +126 -25
  23. package/server/observability/index.js +394 -0
  24. package/server/protocol/capabilities.js +223 -0
  25. package/server/protocol/index.js +243 -0
  26. package/server/protocol/schema.js +584 -0
  27. package/server/registry/index.js +326 -0
  28. package/server/routes/admin.js +16 -2
  29. package/server/routes/ads.js +130 -0
  30. package/server/routes/agent-workspace.js +378 -0
  31. package/server/routes/api.js +21 -2
  32. package/server/routes/auth.js +26 -6
  33. package/server/routes/runtime.js +725 -0
  34. package/server/routes/sovereign.js +78 -0
  35. package/server/routes/universal.js +177 -0
  36. package/server/routes/wab-api.js +20 -5
  37. package/server/runtime/event-bus.js +210 -0
  38. package/server/runtime/index.js +233 -0
  39. package/server/runtime/sandbox.js +266 -0
  40. package/server/runtime/scheduler.js +395 -0
  41. package/server/runtime/state-manager.js +188 -0
  42. package/server/security/index.js +355 -0
  43. package/server/services/agent-chat.js +506 -0
  44. package/server/services/agent-symphony.js +6 -0
  45. package/server/services/agent-tasks.js +1807 -0
  46. package/server/services/fairness-engine.js +409 -0
  47. package/server/services/plugins.js +27 -3
  48. package/server/services/price-intelligence.js +565 -0
  49. package/server/services/price-shield.js +1137 -0
  50. package/server/services/search-engine.js +357 -0
  51. package/server/services/security.js +513 -0
  52. package/server/services/universal-scraper.js +661 -0
  53. package/server/ws.js +61 -1
package/server/ws.js CHANGED
@@ -1,20 +1,58 @@
1
1
  const WebSocket = require('ws');
2
2
  const { verifyUserToken, verifyAdminToken } = require('./config/secrets');
3
3
  const { findSiteById } = require('./models/db');
4
+ const { isJWTRevoked, auditLog } = require('./services/security');
4
5
 
5
6
  // Map of siteId → Set of WebSocket clients
6
7
  const siteClients = new Map();
8
+ // Per-IP connection tracking
9
+ const ipConnections = new Map();
10
+ const MAX_CONNECTIONS_PER_IP = 10;
11
+ const AUTH_TIMEOUT_MS = 10_000;
12
+ const MAX_MESSAGE_SIZE = 4096;
13
+ const MSG_RATE_WINDOW = 60_000;
14
+ const MSG_RATE_MAX = 30;
7
15
 
8
16
  function setupWebSocket(server) {
9
- const wss = new WebSocket.Server({ server, path: '/ws/analytics' });
17
+ const wss = new WebSocket.Server({ server, path: '/ws/analytics', maxPayload: MAX_MESSAGE_SIZE });
10
18
 
11
19
  wss.on('connection', (ws, req) => {
12
20
  let authenticatedSiteId = null;
21
+ const clientIP = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.socket.remoteAddress || 'unknown';
22
+
23
+ // ── Per-IP connection limit ──
24
+ const currentCount = ipConnections.get(clientIP) || 0;
25
+ if (currentCount >= MAX_CONNECTIONS_PER_IP) {
26
+ ws.close(1013, 'Too many connections');
27
+ return;
28
+ }
29
+ ipConnections.set(clientIP, currentCount + 1);
30
+
31
+ // ── Auth timeout — close if not authenticated within 10s ──
32
+ const authTimer = setTimeout(() => {
33
+ if (!authenticatedSiteId) {
34
+ ws.close(4001, 'Authentication timeout');
35
+ }
36
+ }, AUTH_TIMEOUT_MS);
37
+
38
+ // ── Message rate limiter ──
39
+ const msgTimestamps = [];
13
40
 
14
41
  ws.isAlive = true;
15
42
  ws.on('pong', () => { ws.isAlive = true; });
16
43
 
17
44
  ws.on('message', (data) => {
45
+ // Rate limit messages
46
+ const now = Date.now();
47
+ msgTimestamps.push(now);
48
+ while (msgTimestamps.length > 0 && msgTimestamps[0] < now - MSG_RATE_WINDOW) {
49
+ msgTimestamps.shift();
50
+ }
51
+ if (msgTimestamps.length > MSG_RATE_MAX) {
52
+ ws.close(4008, 'Message rate limit exceeded');
53
+ return;
54
+ }
55
+
18
56
  try {
19
57
  const msg = JSON.parse(data);
20
58
 
@@ -24,6 +62,13 @@ function setupWebSocket(server) {
24
62
  return;
25
63
  }
26
64
 
65
+ // Check JWT revocation before verifying
66
+ if (isJWTRevoked(msg.token)) {
67
+ ws.send(JSON.stringify({ type: 'error', message: 'Token has been revoked' }));
68
+ ws.close(4003, 'Token revoked');
69
+ return;
70
+ }
71
+
27
72
  let decoded;
28
73
  let isAdmin = false;
29
74
  try {
@@ -34,6 +79,7 @@ function setupWebSocket(server) {
34
79
  isAdmin = decoded.isAdmin === true;
35
80
  } catch {
36
81
  ws.send(JSON.stringify({ type: 'error', message: 'Invalid message or auth failed' }));
82
+ auditLog({ actorType: 'system', action: 'ws_auth_failed', ip: clientIP, outcome: 'denied', severity: 'warning' });
37
83
  return;
38
84
  }
39
85
  }
@@ -46,12 +92,16 @@ function setupWebSocket(server) {
46
92
  }
47
93
  }
48
94
 
95
+ clearTimeout(authTimer);
49
96
  authenticatedSiteId = msg.siteId;
50
97
  if (!siteClients.has(msg.siteId)) {
51
98
  siteClients.set(msg.siteId, new Set());
52
99
  }
53
100
  siteClients.get(msg.siteId).add(ws);
54
101
  ws.send(JSON.stringify({ type: 'auth:success', siteId: msg.siteId }));
102
+ } else if (!authenticatedSiteId) {
103
+ // Reject all non-auth messages before authentication
104
+ ws.send(JSON.stringify({ type: 'error', message: 'Authentication required' }));
55
105
  }
56
106
  } catch (e) {
57
107
  ws.send(JSON.stringify({ type: 'error', message: 'Invalid message or auth failed' }));
@@ -59,6 +109,12 @@ function setupWebSocket(server) {
59
109
  });
60
110
 
61
111
  ws.on('close', () => {
112
+ clearTimeout(authTimer);
113
+ // Decrement IP connection count
114
+ const count = ipConnections.get(clientIP) || 1;
115
+ if (count <= 1) ipConnections.delete(clientIP);
116
+ else ipConnections.set(clientIP, count - 1);
117
+
62
118
  if (authenticatedSiteId && siteClients.has(authenticatedSiteId)) {
63
119
  siteClients.get(authenticatedSiteId).delete(ws);
64
120
  if (siteClients.get(authenticatedSiteId).size === 0) {
@@ -66,6 +122,10 @@ function setupWebSocket(server) {
66
122
  }
67
123
  }
68
124
  });
125
+
126
+ ws.on('error', () => {
127
+ clearTimeout(authTimer);
128
+ });
69
129
  });
70
130
 
71
131
  const interval = setInterval(() => {