web-agent-bridge 2.3.1 → 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.
- package/README.ar.md +506 -31
- package/README.md +574 -47
- package/bin/agent-runner.js +10 -1
- package/package.json +1 -1
- package/public/agent-workspace.html +347 -0
- package/public/browser.html +484 -0
- package/public/css/agent-workspace.css +1713 -0
- package/public/index.html +94 -0
- package/public/js/agent-workspace.js +1740 -0
- package/sdk/index.d.ts +83 -0
- package/sdk/index.js +115 -1
- package/sdk/package.json +1 -1
- package/server/config/secrets.js +13 -5
- package/server/index.js +183 -4
- package/server/middleware/adminAuth.js +6 -1
- package/server/middleware/auth.js +11 -2
- package/server/middleware/rateLimits.js +78 -2
- package/server/migrations/003_ads_integer_cents.sql +33 -0
- package/server/models/db.js +126 -25
- package/server/routes/admin.js +16 -2
- package/server/routes/ads.js +130 -0
- package/server/routes/agent-workspace.js +378 -0
- package/server/routes/api.js +21 -2
- package/server/routes/auth.js +26 -6
- package/server/routes/sovereign.js +78 -0
- package/server/routes/universal.js +177 -0
- package/server/routes/wab-api.js +20 -5
- package/server/services/agent-chat.js +506 -0
- package/server/services/agent-symphony.js +6 -0
- package/server/services/agent-tasks.js +1807 -0
- package/server/services/fairness-engine.js +409 -0
- package/server/services/plugins.js +27 -3
- package/server/services/price-intelligence.js +565 -0
- package/server/services/price-shield.js +1137 -0
- package/server/services/search-engine.js +357 -0
- package/server/services/security.js +513 -0
- package/server/services/universal-scraper.js +661 -0
- package/server/ws.js +61 -1
|
@@ -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;
|
package/server/routes/wab-api.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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;
|