web-agent-bridge 2.3.0 → 2.4.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 (66) hide show
  1. package/README.ar.md +506 -31
  2. package/README.md +574 -47
  3. package/bin/agent-runner.js +10 -1
  4. package/package.json +12 -4
  5. package/public/agent-workspace.html +347 -0
  6. package/public/browser.html +484 -0
  7. package/public/commander-dashboard.html +243 -0
  8. package/public/css/agent-workspace.css +1713 -0
  9. package/public/css/premium.css +317 -317
  10. package/public/demo.html +259 -259
  11. package/public/index.html +738 -644
  12. package/public/js/agent-workspace.js +1740 -0
  13. package/public/mesh-dashboard.html +309 -382
  14. package/public/premium-dashboard.html +2487 -2487
  15. package/public/premium.html +791 -791
  16. package/public/script/wab.min.js +124 -87
  17. package/script/ai-agent-bridge.js +154 -84
  18. package/sdk/agent-mesh.js +287 -171
  19. package/sdk/commander.js +262 -0
  20. package/sdk/index.d.ts +83 -0
  21. package/sdk/index.js +374 -260
  22. package/sdk/package.json +1 -1
  23. package/server/config/secrets.js +13 -5
  24. package/server/index.js +191 -5
  25. package/server/middleware/adminAuth.js +6 -1
  26. package/server/middleware/auth.js +11 -2
  27. package/server/middleware/rateLimits.js +78 -2
  28. package/server/migrations/002_premium_features.sql +418 -418
  29. package/server/migrations/003_ads_integer_cents.sql +33 -0
  30. package/server/models/db.js +121 -1
  31. package/server/routes/admin-premium.js +671 -671
  32. package/server/routes/admin.js +16 -2
  33. package/server/routes/ads.js +130 -0
  34. package/server/routes/agent-workspace.js +378 -0
  35. package/server/routes/api.js +21 -2
  36. package/server/routes/auth.js +26 -6
  37. package/server/routes/commander.js +316 -0
  38. package/server/routes/mesh.js +370 -201
  39. package/server/routes/premium-v2.js +686 -686
  40. package/server/routes/premium.js +724 -724
  41. package/server/routes/sovereign.js +78 -0
  42. package/server/routes/universal.js +177 -0
  43. package/server/routes/wab-api.js +20 -5
  44. package/server/services/agent-chat.js +506 -0
  45. package/server/services/agent-learning.js +230 -77
  46. package/server/services/agent-memory.js +625 -625
  47. package/server/services/agent-mesh.js +260 -67
  48. package/server/services/agent-symphony.js +553 -517
  49. package/server/services/agent-tasks.js +1807 -0
  50. package/server/services/commander.js +738 -0
  51. package/server/services/edge-compute.js +440 -0
  52. package/server/services/fairness-engine.js +409 -0
  53. package/server/services/local-ai.js +389 -0
  54. package/server/services/plugins.js +771 -747
  55. package/server/services/price-intelligence.js +565 -0
  56. package/server/services/price-shield.js +1137 -0
  57. package/server/services/search-engine.js +357 -0
  58. package/server/services/security.js +513 -0
  59. package/server/services/self-healing.js +843 -843
  60. package/server/services/swarm.js +788 -788
  61. package/server/services/universal-scraper.js +661 -0
  62. package/server/services/vision.js +871 -871
  63. package/server/ws.js +61 -1
  64. package/public/admin/dashboard.html +0 -848
  65. package/public/admin/login.html +0 -84
  66. package/public/video/tutorial.mp4 +0 -0
@@ -12,6 +12,7 @@ const { authenticateToken } = require('../middleware/auth');
12
12
  const reputation = require('../services/reputation');
13
13
  const negotiation = require('../services/negotiation');
14
14
  const verification = require('../services/verification');
15
+ const priceShield = require('../services/price-shield');
15
16
 
16
17
  // ═══════════════════════════════════════════════════════════════════════
17
18
  // REPUTATION API
@@ -304,4 +305,81 @@ router.get('/dashboard/sovereign', authenticateToken, (req, res) => {
304
305
  res.json(dashboardData);
305
306
  });
306
307
 
308
+ // ═══════════════════════════════════════════════════════════════════════
309
+ // DYNAMIC PRICING SHIELD API
310
+ // ═══════════════════════════════════════════════════════════════════════
311
+
312
+ // Get available identity personas
313
+ router.get('/price-shield/personas', (req, res) => {
314
+ res.json(priceShield.getPersonas());
315
+ });
316
+
317
+ // Create a new price scan
318
+ router.post('/price-shield/scans', (req, res) => {
319
+ const { siteId, url, itemName, category } = req.body;
320
+ if (!url) {
321
+ return res.status(400).json({ error: 'url is required' });
322
+ }
323
+ const result = priceShield.createScan({ siteId, url, itemName, category });
324
+ res.json(result);
325
+ });
326
+
327
+ // Record a probe result for a scan
328
+ router.post('/price-shield/scans/:scanId/probes', (req, res) => {
329
+ const { personaId, priceText, currency, responseHeaders, cookiesReceived, durationMs } = req.body;
330
+ if (!personaId || !priceText) {
331
+ return res.status(400).json({ error: 'personaId and priceText are required' });
332
+ }
333
+ const result = priceShield.recordProbe(req.params.scanId, {
334
+ personaId, priceText, currency, responseHeaders, cookiesReceived, durationMs
335
+ });
336
+ if (result.error) return res.status(400).json(result);
337
+ res.json(result);
338
+ });
339
+
340
+ // Analyze a scan (after probes are recorded)
341
+ router.post('/price-shield/scans/:scanId/analyze', (req, res) => {
342
+ const result = priceShield.analyzeScan(req.params.scanId);
343
+ if (result.error) return res.status(400).json(result);
344
+ res.json(result);
345
+ });
346
+
347
+ // Quick scan — all-in-one (provide probes + get analysis)
348
+ router.post('/price-shield/quick-scan', (req, res) => {
349
+ const { url, itemName, siteId, category, probes } = req.body;
350
+ if (!url || !probes || !Array.isArray(probes) || probes.length < 2) {
351
+ return res.status(400).json({ error: 'url and at least 2 probes are required' });
352
+ }
353
+ const result = priceShield.quickScan({ url, itemName, siteId, category, probes });
354
+ if (result.error) return res.status(400).json(result);
355
+ res.json(result);
356
+ });
357
+
358
+ // Get scan report
359
+ router.get('/price-shield/scans/:scanId', (req, res) => {
360
+ const result = priceShield.getScanReport(req.params.scanId);
361
+ if (result.error) return res.status(404).json(result);
362
+ res.json(result);
363
+ });
364
+
365
+ // Get global price shield statistics
366
+ router.get('/price-shield/stats', (req, res) => {
367
+ res.json(priceShield.getGlobalStats());
368
+ });
369
+
370
+ // Get price history for a URL
371
+ router.get('/price-shield/history', (req, res) => {
372
+ const url = req.query.url;
373
+ if (!url) return res.status(400).json({ error: 'url query parameter is required' });
374
+ const limit = Math.min(parseInt(req.query.limit) || 30, 100);
375
+ res.json(priceShield.getPriceHistory(url, limit));
376
+ });
377
+
378
+ // Get manipulation log for a site
379
+ router.get('/price-shield/manipulations/:siteId', (req, res) => {
380
+ const limit = Math.min(parseInt(req.query.limit) || 20, 100);
381
+ const result = priceShield.getGlobalStats();
382
+ res.json(result.topManipulators.find(m => m.siteId === req.params.siteId) || { siteId: req.params.siteId, incidents: 0 });
383
+ });
384
+
307
385
  module.exports = router;
@@ -0,0 +1,177 @@
1
+ /**
2
+ * WAB Universal Agent — Server Routes
3
+ * ═══════════════════════════════════════════════════════════════════
4
+ * API endpoints for the Universal Agent mode.
5
+ * Used by: WAB Browser, Chrome Extension, direct API calls.
6
+ *
7
+ * All endpoints work WITHOUT requiring the target site to install any script.
8
+ */
9
+
10
+ const express = require('express');
11
+ const router = express.Router();
12
+ const scraper = require('../services/universal-scraper');
13
+ const priceIntel = require('../services/price-intelligence');
14
+ const fairness = require('../services/fairness-engine');
15
+
16
+ // ─── POST /api/universal/extract ─────────────────────────────────────
17
+ // Extract prices/products from a URL (server-side fetch)
18
+ router.post('/extract', async (req, res) => {
19
+ const { url } = req.body;
20
+ if (!url) return res.status(400).json({ error: 'URL required' });
21
+
22
+ try {
23
+ const result = await scraper.fetchAndExtract(url);
24
+ res.json(result);
25
+ } catch (err) {
26
+ res.status(500).json({ error: err.message });
27
+ }
28
+ });
29
+
30
+ // ─── POST /api/universal/analyze ─────────────────────────────────────
31
+ // Full analysis: extract + fraud detection + trust score
32
+ // Accepts either a URL (server-side fetch) or pre-extracted data (from browser)
33
+ router.post('/analyze', async (req, res) => {
34
+ const { url, extraction } = req.body;
35
+
36
+ try {
37
+ let result;
38
+
39
+ if (extraction) {
40
+ // Data already extracted by browser/extension
41
+ const processed = scraper.processBrowserExtraction(extraction);
42
+ result = await priceIntel.analyzePrice(extraction.url || url || '', processed);
43
+
44
+ // Add dark pattern detection if text available
45
+ if (extraction.darkPatterns) {
46
+ result.darkPatterns = extraction.darkPatterns;
47
+ }
48
+ } else if (url) {
49
+ // Server-side fetch and analyze
50
+ result = await priceIntel.analyzePrice(url);
51
+ } else {
52
+ return res.status(400).json({ error: 'URL or extraction data required' });
53
+ }
54
+
55
+ // Add fairness score for the domain
56
+ if (result.domain) {
57
+ result.fairness = fairness.calculateFairnessScore(result.domain, {
58
+ fraudAlerts: (result.alerts || []).length,
59
+ });
60
+ }
61
+
62
+ res.json(result);
63
+ } catch (err) {
64
+ res.status(500).json({ error: err.message });
65
+ }
66
+ });
67
+
68
+ // ─── POST /api/universal/compare ─────────────────────────────────────
69
+ // Compare prices across multiple sources
70
+ router.post('/compare', async (req, res) => {
71
+ const { query, category, maxSources } = req.body;
72
+ if (!query) return res.status(400).json({ error: 'Query required' });
73
+
74
+ try {
75
+ const result = await priceIntel.compareAcrossSources(
76
+ query,
77
+ category || 'product',
78
+ { maxSources: maxSources || 8 }
79
+ );
80
+
81
+ // Apply fairness ranking
82
+ if (result.results && result.results.length > 0) {
83
+ result.results = fairness.rankWithFairness(result.results, {
84
+ avgPrice: result.avgPrice,
85
+ });
86
+ }
87
+
88
+ res.json(result);
89
+ } catch (err) {
90
+ res.status(500).json({ error: err.message });
91
+ }
92
+ });
93
+
94
+ // ─── POST /api/universal/deals ───────────────────────────────────────
95
+ // Find best deals with fairness ranking + fraud detection
96
+ router.post('/deals', async (req, res) => {
97
+ const { query, category, lang } = req.body;
98
+ if (!query) return res.status(400).json({ error: 'Query required' });
99
+
100
+ try {
101
+ const result = await priceIntel.findBestDeals(
102
+ query,
103
+ category || 'product',
104
+ { lang: lang || 'en' }
105
+ );
106
+
107
+ // Apply fairness ranking to deals
108
+ if (result.deals && result.deals.length > 0) {
109
+ result.deals = fairness.rankWithFairness(result.deals, {
110
+ avgPrice: result.deals.reduce((s, d) => s + (d.priceUsd || 0), 0) / result.deals.length,
111
+ });
112
+ }
113
+
114
+ res.json(result);
115
+ } catch (err) {
116
+ res.status(500).json({ error: err.message });
117
+ }
118
+ });
119
+
120
+ // ─── POST /api/universal/fairness ────────────────────────────────────
121
+ // Get fairness score for a domain
122
+ router.post('/fairness', (req, res) => {
123
+ const { domain, url } = req.body;
124
+ const d = domain || (url ? (() => { try { return new URL(url).hostname; } catch (_) { return ''; } })() : '');
125
+ if (!d) return res.status(400).json({ error: 'Domain or URL required' });
126
+
127
+ const score = fairness.calculateFairnessScore(d);
128
+ res.json(score);
129
+ });
130
+
131
+ // ─── POST /api/universal/dark-patterns ───────────────────────────────
132
+ // Detect dark patterns in page text
133
+ router.post('/dark-patterns', (req, res) => {
134
+ const { text, lang } = req.body;
135
+ if (!text) return res.status(400).json({ error: 'Text required' });
136
+
137
+ const patterns = fairness.detectDarkPatterns(text, lang || 'en');
138
+ res.json({ patterns, count: patterns.length });
139
+ });
140
+
141
+ // ─── GET /api/universal/history ──────────────────────────────────────
142
+ // Get price history for a URL
143
+ router.get('/history', (req, res) => {
144
+ const { url } = req.query;
145
+ if (!url) return res.status(400).json({ error: 'URL required' });
146
+
147
+ const history = scraper.getPriceHistory(url, 30);
148
+ res.json({ url, history });
149
+ });
150
+
151
+ // ─── GET /api/universal/top-fair ─────────────────────────────────────
152
+ // Get top fairness-ranked sites
153
+ router.get('/top-fair', (req, res) => {
154
+ const limit = parseInt(req.query.limit) || 20;
155
+ const sites = fairness.getTopFairSites(limit);
156
+ res.json({ sites });
157
+ });
158
+
159
+ // ─── GET /api/universal/extraction-script ────────────────────────────
160
+ // Get the browser extraction script (for dynamic injection)
161
+ router.get('/extraction-script', (req, res) => {
162
+ res.set('Content-Type', 'application/javascript');
163
+ res.send(scraper.getBrowserExtractionScript());
164
+ });
165
+
166
+ // ─── GET /api/universal/sources ──────────────────────────────────────
167
+ // List all competing sources by category
168
+ router.get('/sources', (req, res) => {
169
+ const { category } = req.query;
170
+ if (category && priceIntel.COMPETING_SOURCES[category]) {
171
+ res.json({ category, sources: priceIntel.COMPETING_SOURCES[category] });
172
+ } else {
173
+ res.json(priceIntel.COMPETING_SOURCES);
174
+ }
175
+ });
176
+
177
+ module.exports = router;
@@ -9,8 +9,11 @@
9
9
 
10
10
  const express = require('express');
11
11
  const router = express.Router();
12
+ const crypto = require('crypto');
12
13
  const { findSiteById, findSiteByLicense, recordAnalytic, db } = require('../models/db');
13
14
  const { broadcastAnalytic } = require('../ws');
15
+ const { wabAuthenticateLimiter, wabActionLimiter, searchLimiter } = require('../middleware/rateLimits');
16
+ const { auditLog } = require('../services/security');
14
17
  const {
15
18
  calculateNeutralityScore,
16
19
  fairnessWeightedSearch,
@@ -82,7 +85,7 @@ function buildErrorResponse(id, code, message) {
82
85
  // POST /api/wab/authenticate — session token exchange
83
86
  // ═════════════════════════════════════════════════════════════════════
84
87
 
85
- router.post('/authenticate', (req, res) => {
88
+ router.post('/authenticate', wabAuthenticateLimiter, (req, res) => {
86
89
  try {
87
90
  const { siteId, apiKey, meta } = req.body;
88
91
  if (!siteId && !apiKey) {
@@ -91,12 +94,22 @@ router.post('/authenticate', (req, res) => {
91
94
 
92
95
  let site;
93
96
  if (apiKey) {
94
- site = db.prepare('SELECT * FROM sites WHERE api_key = ? AND active = 1').get(apiKey);
97
+ // Timing-safe API key lookup: hash the provided key and compare against stored hashes
98
+ // to prevent timing attacks on the raw key comparison
99
+ const allActive = db.prepare('SELECT * FROM sites WHERE active = 1 AND api_key IS NOT NULL').all();
100
+ site = allActive.find(s => {
101
+ if (!s.api_key) return false;
102
+ const a = Buffer.from(s.api_key);
103
+ const b = Buffer.from(apiKey);
104
+ if (a.length !== b.length) return false;
105
+ return crypto.timingSafeEqual(a, b);
106
+ }) || null;
95
107
  } else {
96
108
  site = findSiteById.get(siteId);
97
109
  }
98
110
 
99
111
  if (!site) {
112
+ auditLog({ actorType: 'agent', action: 'wab_auth_failed', details: { siteId }, ip: req.ip, outcome: 'denied', severity: 'warning' });
100
113
  return res.status(404).json(buildErrorResponse(null, 'not_found', 'Site not found or invalid credentials'));
101
114
  }
102
115
 
@@ -105,7 +118,9 @@ router.post('/authenticate', (req, res) => {
105
118
  try {
106
119
  const reqDomain = new URL(origin).hostname.replace(/^www\./, '');
107
120
  const siteDomain = site.domain.replace(/^www\./, '');
108
- if (reqDomain !== siteDomain && reqDomain !== 'localhost' && reqDomain !== '127.0.0.1') {
121
+ const isProduction = process.env.NODE_ENV === 'production';
122
+ const isLocalhost = reqDomain === 'localhost' || reqDomain === '127.0.0.1';
123
+ if (reqDomain !== siteDomain && !(isLocalhost && !isProduction)) {
109
124
  return res.status(403).json(buildErrorResponse(null, 'origin_mismatch', 'Origin does not match site domain'));
110
125
  }
111
126
  } catch (_) {}
@@ -193,7 +208,7 @@ router.get('/actions', (req, res) => {
193
208
  // POST /api/wab/actions/:name — execute action (with tracking)
194
209
  // ═════════════════════════════════════════════════════════════════════
195
210
 
196
- router.post('/actions/:name', requireSession, (req, res) => {
211
+ router.post('/actions/:name', requireSession, wabActionLimiter, (req, res) => {
197
212
  try {
198
213
  const actionName = req.params.name;
199
214
  const site = findSiteById.get(req.wabSession.siteId);
@@ -333,7 +348,7 @@ router.get('/page-info', (req, res) => {
333
348
  // GET /api/wab/search — fairness-weighted search (MCP adapter uses this)
334
349
  // ═════════════════════════════════════════════════════════════════════
335
350
 
336
- router.get('/search', (req, res) => {
351
+ router.get('/search', searchLimiter, (req, res) => {
337
352
  try {
338
353
  const query = req.query.q || '';
339
354
  const category = req.query.category || null;