web-agent-bridge 3.2.0 → 3.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/LICENSE +72 -72
- package/README.ar.md +1286 -1152
- package/README.md +1764 -1635
- package/bin/agent-runner.js +474 -474
- package/bin/cli.js +237 -138
- package/bin/wab.js +80 -80
- package/examples/bidi-agent.js +119 -119
- package/examples/cross-site-agent.js +91 -91
- package/examples/mcp-agent.js +94 -94
- package/examples/next-app-router/README.md +44 -44
- package/examples/puppeteer-agent.js +108 -108
- package/examples/saas-dashboard/README.md +55 -55
- package/examples/shopify-hydrogen/README.md +74 -74
- package/examples/vision-agent.js +171 -171
- package/examples/wordpress-elementor/README.md +77 -77
- package/package.json +16 -3
- package/public/.well-known/agent-tools.json +180 -180
- package/public/.well-known/ai-assets.json +59 -59
- package/public/.well-known/security.txt +8 -0
- package/public/agent-workspace.html +349 -349
- package/public/ai.html +198 -198
- package/public/api.html +413 -412
- package/public/browser.html +486 -486
- package/public/commander-dashboard.html +243 -243
- package/public/cookies.html +210 -210
- package/public/css/agent-workspace.css +1713 -1713
- package/public/css/premium.css +317 -317
- package/public/css/styles.css +1235 -1235
- package/public/dashboard.html +706 -706
- package/public/dns.html +507 -0
- package/public/docs.html +587 -587
- package/public/feed.xml +89 -89
- package/public/growth.html +463 -463
- package/public/index.html +1070 -982
- package/public/integrations.html +556 -0
- package/public/js/agent-workspace.js +1740 -1740
- package/public/js/auth-nav.js +31 -31
- package/public/js/auth-redirect.js +12 -12
- package/public/js/cookie-consent.js +56 -56
- package/public/js/wab-demo-page.js +721 -721
- package/public/js/ws-client.js +74 -74
- package/public/llms-full.txt +360 -360
- package/public/llms.txt +125 -125
- package/public/login.html +85 -85
- package/public/mesh-dashboard.html +328 -328
- package/public/openapi.json +580 -580
- package/public/phone-shield.html +281 -0
- package/public/premium-dashboard.html +2489 -2489
- package/public/premium.html +793 -793
- package/public/privacy.html +297 -297
- package/public/register.html +105 -105
- package/public/robots.txt +87 -87
- package/public/script/wab-consent.d.ts +36 -36
- package/public/script/wab-consent.js +104 -104
- package/public/script/wab-schema.js +131 -131
- package/public/script/wab.d.ts +108 -108
- package/public/script/wab.min.js +580 -580
- package/public/security.txt +8 -0
- package/public/terms.html +256 -256
- package/script/ai-agent-bridge.js +1754 -1754
- package/sdk/README.md +99 -99
- package/sdk/agent-mesh.js +449 -449
- package/sdk/commander.js +262 -262
- package/sdk/index.d.ts +464 -464
- package/sdk/index.js +12 -1
- package/sdk/multi-agent.js +318 -318
- package/sdk/package.json +1 -1
- package/sdk/safety-shield.js +219 -0
- package/sdk/schema-discovery.js +83 -83
- package/server/adapters/index.js +520 -520
- package/server/config/plans.js +367 -367
- package/server/config/secrets.js +102 -102
- package/server/control-plane/index.js +301 -301
- package/server/data-plane/index.js +354 -354
- package/server/index.js +531 -427
- package/server/llm/index.js +404 -404
- package/server/middleware/adminAuth.js +35 -35
- package/server/middleware/auth.js +50 -50
- package/server/middleware/featureGate.js +88 -88
- package/server/middleware/rateLimits.js +100 -100
- package/server/middleware/sensitiveAction.js +157 -0
- package/server/migrations/001_add_analytics_indexes.sql +7 -7
- package/server/migrations/002_premium_features.sql +418 -418
- package/server/migrations/003_ads_integer_cents.sql +33 -33
- package/server/migrations/004_agent_os.sql +158 -158
- package/server/migrations/005_marketplace_metering.sql +126 -126
- package/server/models/adapters/index.js +33 -33
- package/server/models/adapters/mysql.js +183 -183
- package/server/models/adapters/postgresql.js +172 -172
- package/server/models/adapters/sqlite.js +7 -7
- package/server/models/db.js +681 -681
- package/server/observability/failure-analysis.js +337 -337
- package/server/observability/index.js +394 -394
- package/server/protocol/capabilities.js +223 -223
- package/server/protocol/index.js +243 -243
- package/server/protocol/schema.js +584 -584
- package/server/registry/certification.js +271 -271
- package/server/registry/index.js +326 -326
- package/server/routes/admin-premium.js +671 -671
- package/server/routes/admin.js +261 -261
- package/server/routes/ads.js +130 -130
- package/server/routes/agent-workspace.js +540 -540
- package/server/routes/api.js +150 -150
- package/server/routes/auth.js +71 -71
- package/server/routes/billing.js +45 -45
- package/server/routes/commander.js +316 -316
- package/server/routes/demo-showcase.js +332 -332
- package/server/routes/demo-store.js +154 -0
- package/server/routes/discovery.js +417 -417
- package/server/routes/gateway.js +173 -157
- package/server/routes/license.js +251 -240
- package/server/routes/mesh.js +469 -469
- package/server/routes/noscript.js +543 -543
- package/server/routes/premium-v2.js +686 -686
- package/server/routes/premium.js +724 -724
- package/server/routes/runtime.js +2148 -2147
- package/server/routes/sovereign.js +465 -385
- package/server/routes/universal.js +200 -185
- package/server/routes/wab-api.js +850 -501
- package/server/runtime/container-worker.js +111 -111
- package/server/runtime/container.js +448 -448
- package/server/runtime/distributed-worker.js +362 -362
- package/server/runtime/event-bus.js +210 -210
- package/server/runtime/index.js +253 -253
- package/server/runtime/queue.js +599 -599
- package/server/runtime/replay.js +666 -666
- package/server/runtime/sandbox.js +266 -266
- package/server/runtime/scheduler.js +534 -534
- package/server/runtime/session-engine.js +293 -293
- package/server/runtime/state-manager.js +188 -188
- package/server/security/cross-site-redactor.js +196 -0
- package/server/security/dry-run.js +180 -0
- package/server/security/human-gate-rate-limit.js +147 -0
- package/server/security/human-gate-transports.js +178 -0
- package/server/security/human-gate.js +281 -0
- package/server/security/index.js +368 -368
- package/server/security/intent-engine.js +245 -0
- package/server/security/reward-guard.js +171 -0
- package/server/security/rollback-store.js +239 -0
- package/server/security/token-scope.js +404 -0
- package/server/security/url-policy.js +139 -0
- package/server/services/agent-chat.js +506 -506
- package/server/services/agent-learning.js +601 -575
- package/server/services/agent-memory.js +625 -625
- package/server/services/agent-mesh.js +555 -539
- package/server/services/agent-symphony.js +717 -717
- package/server/services/agent-tasks.js +1807 -1807
- package/server/services/api-key-engine.js +292 -261
- package/server/services/cluster.js +894 -894
- package/server/services/commander.js +738 -738
- package/server/services/edge-compute.js +440 -440
- package/server/services/email.js +204 -204
- package/server/services/hosted-runtime.js +205 -205
- package/server/services/lfd.js +635 -635
- package/server/services/local-ai.js +389 -389
- package/server/services/marketplace.js +270 -270
- package/server/services/metering.js +182 -182
- package/server/services/modules/affiliate-intelligence.js +93 -93
- package/server/services/modules/agent-firewall.js +90 -90
- package/server/services/modules/bounty.js +89 -89
- package/server/services/modules/collective-bargaining.js +92 -92
- package/server/services/modules/dark-pattern.js +66 -66
- package/server/services/modules/gov-intelligence.js +45 -45
- package/server/services/modules/neural.js +55 -55
- package/server/services/modules/notary.js +49 -49
- package/server/services/modules/price-time-machine.js +86 -86
- package/server/services/modules/protocol.js +104 -104
- package/server/services/negotiation.js +439 -439
- package/server/services/plugins.js +771 -771
- package/server/services/price-intelligence.js +566 -566
- package/server/services/price-shield.js +1137 -1137
- package/server/services/reputation.js +465 -465
- package/server/services/search-engine.js +357 -357
- package/server/services/security.js +513 -513
- package/server/services/self-healing.js +843 -843
- package/server/services/sovereign-shield.js +542 -0
- package/server/services/stripe.js +192 -192
- package/server/services/swarm.js +788 -788
- package/server/services/universal-scraper.js +662 -661
- package/server/services/verification.js +481 -481
- package/server/services/vision.js +1163 -1163
- package/server/utils/cache.js +125 -125
- package/server/utils/migrate.js +81 -81
- package/server/utils/safe-fetch.js +228 -0
- package/server/utils/secureFields.js +50 -50
- package/server/ws.js +161 -161
- package/templates/artisan-marketplace.yaml +104 -104
- package/templates/book-price-scout.yaml +98 -98
- package/templates/electronics-price-tracker.yaml +108 -108
- package/templates/flight-deal-hunter.yaml +113 -113
- package/templates/freelancer-direct.yaml +116 -116
- package/templates/grocery-price-compare.yaml +93 -93
- package/templates/hotel-direct-booking.yaml +113 -113
- package/templates/local-services.yaml +98 -98
- package/templates/olive-oil-tunisia.yaml +88 -88
- package/templates/organic-farm-fresh.yaml +101 -101
- package/templates/restaurant-direct.yaml +97 -97
- package/public/score.html +0 -263
- package/server/migrations/006_growth_suite.sql +0 -138
- package/server/routes/growth.js +0 -962
- package/server/services/fairness-engine.js +0 -409
- package/server/services/fairness.js +0 -420
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Optional AES-256-GCM encryption for sensitive DB fields (e.g. SMTP password).
|
|
3
|
-
* Set CREDENTIALS_ENCRYPTION_KEY (any long random string) to enable at-rest encryption.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const crypto = require('crypto');
|
|
7
|
-
|
|
8
|
-
const PREFIX = 'enc:v1:';
|
|
9
|
-
|
|
10
|
-
function getKey() {
|
|
11
|
-
const raw = process.env.CREDENTIALS_ENCRYPTION_KEY;
|
|
12
|
-
if (!raw || String(raw).length < 8) return null;
|
|
13
|
-
return crypto.createHash('sha256').update(String(raw)).digest();
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function encryptOptional(plain) {
|
|
17
|
-
if (plain == null || plain === '') return plain;
|
|
18
|
-
const key = getKey();
|
|
19
|
-
if (!key) return plain;
|
|
20
|
-
const iv = crypto.randomBytes(12);
|
|
21
|
-
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
22
|
-
const enc = Buffer.concat([cipher.update(String(plain), 'utf8'), cipher.final()]);
|
|
23
|
-
const tag = cipher.getAuthTag();
|
|
24
|
-
return `${PREFIX}${iv.toString('hex')}:${tag.toString('hex')}:${enc.toString('hex')}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function decryptOptional(stored) {
|
|
28
|
-
if (stored == null || stored === '') return stored;
|
|
29
|
-
if (typeof stored !== 'string' || !stored.startsWith(PREFIX)) return stored;
|
|
30
|
-
const key = getKey();
|
|
31
|
-
if (!key) {
|
|
32
|
-
console.warn('[WAB] CREDENTIALS_ENCRYPTION_KEY missing; cannot decrypt stored credential');
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
try {
|
|
36
|
-
const rest = stored.slice(PREFIX.length);
|
|
37
|
-
const [ivHex, tagHex, dataHex] = rest.split(':');
|
|
38
|
-
const iv = Buffer.from(ivHex, 'hex');
|
|
39
|
-
const tag = Buffer.from(tagHex, 'hex');
|
|
40
|
-
const data = Buffer.from(dataHex, 'hex');
|
|
41
|
-
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
42
|
-
decipher.setAuthTag(tag);
|
|
43
|
-
return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8');
|
|
44
|
-
} catch (e) {
|
|
45
|
-
console.error('[WAB] Decrypt failed:', e.message);
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
module.exports = { encryptOptional, decryptOptional };
|
|
1
|
+
/**
|
|
2
|
+
* Optional AES-256-GCM encryption for sensitive DB fields (e.g. SMTP password).
|
|
3
|
+
* Set CREDENTIALS_ENCRYPTION_KEY (any long random string) to enable at-rest encryption.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
const PREFIX = 'enc:v1:';
|
|
9
|
+
|
|
10
|
+
function getKey() {
|
|
11
|
+
const raw = process.env.CREDENTIALS_ENCRYPTION_KEY;
|
|
12
|
+
if (!raw || String(raw).length < 8) return null;
|
|
13
|
+
return crypto.createHash('sha256').update(String(raw)).digest();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function encryptOptional(plain) {
|
|
17
|
+
if (plain == null || plain === '') return plain;
|
|
18
|
+
const key = getKey();
|
|
19
|
+
if (!key) return plain;
|
|
20
|
+
const iv = crypto.randomBytes(12);
|
|
21
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
22
|
+
const enc = Buffer.concat([cipher.update(String(plain), 'utf8'), cipher.final()]);
|
|
23
|
+
const tag = cipher.getAuthTag();
|
|
24
|
+
return `${PREFIX}${iv.toString('hex')}:${tag.toString('hex')}:${enc.toString('hex')}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function decryptOptional(stored) {
|
|
28
|
+
if (stored == null || stored === '') return stored;
|
|
29
|
+
if (typeof stored !== 'string' || !stored.startsWith(PREFIX)) return stored;
|
|
30
|
+
const key = getKey();
|
|
31
|
+
if (!key) {
|
|
32
|
+
console.warn('[WAB] CREDENTIALS_ENCRYPTION_KEY missing; cannot decrypt stored credential');
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const rest = stored.slice(PREFIX.length);
|
|
37
|
+
const [ivHex, tagHex, dataHex] = rest.split(':');
|
|
38
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
39
|
+
const tag = Buffer.from(tagHex, 'hex');
|
|
40
|
+
const data = Buffer.from(dataHex, 'hex');
|
|
41
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
42
|
+
decipher.setAuthTag(tag);
|
|
43
|
+
return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8');
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.error('[WAB] Decrypt failed:', e.message);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { encryptOptional, decryptOptional };
|
package/server/ws.js
CHANGED
|
@@ -1,161 +1,161 @@
|
|
|
1
|
-
const WebSocket = require('ws');
|
|
2
|
-
const { verifyUserToken, verifyAdminToken } = require('./config/secrets');
|
|
3
|
-
const { findSiteById } = require('./models/db');
|
|
4
|
-
const { isJWTRevoked, auditLog } = require('./services/security');
|
|
5
|
-
|
|
6
|
-
// Map of siteId → Set of WebSocket clients
|
|
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;
|
|
15
|
-
|
|
16
|
-
function setupWebSocket(server) {
|
|
17
|
-
const wss = new WebSocket.Server({ server, path: '/ws/analytics', maxPayload: MAX_MESSAGE_SIZE });
|
|
18
|
-
|
|
19
|
-
wss.on('connection', (ws, req) => {
|
|
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 = [];
|
|
40
|
-
|
|
41
|
-
ws.isAlive = true;
|
|
42
|
-
ws.on('pong', () => { ws.isAlive = true; });
|
|
43
|
-
|
|
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
|
-
|
|
56
|
-
try {
|
|
57
|
-
const msg = JSON.parse(data);
|
|
58
|
-
|
|
59
|
-
if (msg.type === 'auth') {
|
|
60
|
-
if (!msg.token || !msg.siteId) {
|
|
61
|
-
ws.send(JSON.stringify({ type: 'error', message: 'token and siteId required' }));
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
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
|
-
|
|
72
|
-
let decoded;
|
|
73
|
-
let isAdmin = false;
|
|
74
|
-
try {
|
|
75
|
-
decoded = verifyUserToken(msg.token);
|
|
76
|
-
} catch {
|
|
77
|
-
try {
|
|
78
|
-
decoded = verifyAdminToken(msg.token);
|
|
79
|
-
isAdmin = decoded.isAdmin === true;
|
|
80
|
-
} catch {
|
|
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' });
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (!isAdmin) {
|
|
88
|
-
const site = findSiteById.get(msg.siteId);
|
|
89
|
-
if (!site || site.user_id !== decoded.id) {
|
|
90
|
-
ws.send(JSON.stringify({ type: 'error', message: 'Forbidden: not your site' }));
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
clearTimeout(authTimer);
|
|
96
|
-
authenticatedSiteId = msg.siteId;
|
|
97
|
-
if (!siteClients.has(msg.siteId)) {
|
|
98
|
-
siteClients.set(msg.siteId, new Set());
|
|
99
|
-
}
|
|
100
|
-
siteClients.get(msg.siteId).add(ws);
|
|
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' }));
|
|
105
|
-
}
|
|
106
|
-
} catch (e) {
|
|
107
|
-
ws.send(JSON.stringify({ type: 'error', message: 'Invalid message or auth failed' }));
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
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
|
-
|
|
118
|
-
if (authenticatedSiteId && siteClients.has(authenticatedSiteId)) {
|
|
119
|
-
siteClients.get(authenticatedSiteId).delete(ws);
|
|
120
|
-
if (siteClients.get(authenticatedSiteId).size === 0) {
|
|
121
|
-
siteClients.delete(authenticatedSiteId);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
ws.on('error', () => {
|
|
127
|
-
clearTimeout(authTimer);
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
const interval = setInterval(() => {
|
|
132
|
-
wss.clients.forEach((ws) => {
|
|
133
|
-
if (!ws.isAlive) return ws.terminate();
|
|
134
|
-
ws.isAlive = false;
|
|
135
|
-
ws.ping();
|
|
136
|
-
});
|
|
137
|
-
}, 30000);
|
|
138
|
-
|
|
139
|
-
wss.on('close', () => clearInterval(interval));
|
|
140
|
-
|
|
141
|
-
return wss;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function broadcastAnalytic(siteId, eventData) {
|
|
145
|
-
const clients = siteClients.get(siteId);
|
|
146
|
-
if (!clients || clients.size === 0) return;
|
|
147
|
-
|
|
148
|
-
const message = JSON.stringify({
|
|
149
|
-
type: 'analytic',
|
|
150
|
-
timestamp: new Date().toISOString(),
|
|
151
|
-
...eventData
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
clients.forEach((ws) => {
|
|
155
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
156
|
-
ws.send(message);
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
module.exports = { setupWebSocket, broadcastAnalytic };
|
|
1
|
+
const WebSocket = require('ws');
|
|
2
|
+
const { verifyUserToken, verifyAdminToken } = require('./config/secrets');
|
|
3
|
+
const { findSiteById } = require('./models/db');
|
|
4
|
+
const { isJWTRevoked, auditLog } = require('./services/security');
|
|
5
|
+
|
|
6
|
+
// Map of siteId → Set of WebSocket clients
|
|
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;
|
|
15
|
+
|
|
16
|
+
function setupWebSocket(server) {
|
|
17
|
+
const wss = new WebSocket.Server({ server, path: '/ws/analytics', maxPayload: MAX_MESSAGE_SIZE });
|
|
18
|
+
|
|
19
|
+
wss.on('connection', (ws, req) => {
|
|
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 = [];
|
|
40
|
+
|
|
41
|
+
ws.isAlive = true;
|
|
42
|
+
ws.on('pong', () => { ws.isAlive = true; });
|
|
43
|
+
|
|
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
|
+
|
|
56
|
+
try {
|
|
57
|
+
const msg = JSON.parse(data);
|
|
58
|
+
|
|
59
|
+
if (msg.type === 'auth') {
|
|
60
|
+
if (!msg.token || !msg.siteId) {
|
|
61
|
+
ws.send(JSON.stringify({ type: 'error', message: 'token and siteId required' }));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
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
|
+
|
|
72
|
+
let decoded;
|
|
73
|
+
let isAdmin = false;
|
|
74
|
+
try {
|
|
75
|
+
decoded = verifyUserToken(msg.token);
|
|
76
|
+
} catch {
|
|
77
|
+
try {
|
|
78
|
+
decoded = verifyAdminToken(msg.token);
|
|
79
|
+
isAdmin = decoded.isAdmin === true;
|
|
80
|
+
} catch {
|
|
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' });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!isAdmin) {
|
|
88
|
+
const site = findSiteById.get(msg.siteId);
|
|
89
|
+
if (!site || site.user_id !== decoded.id) {
|
|
90
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Forbidden: not your site' }));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
clearTimeout(authTimer);
|
|
96
|
+
authenticatedSiteId = msg.siteId;
|
|
97
|
+
if (!siteClients.has(msg.siteId)) {
|
|
98
|
+
siteClients.set(msg.siteId, new Set());
|
|
99
|
+
}
|
|
100
|
+
siteClients.get(msg.siteId).add(ws);
|
|
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' }));
|
|
105
|
+
}
|
|
106
|
+
} catch (e) {
|
|
107
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Invalid message or auth failed' }));
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
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
|
+
|
|
118
|
+
if (authenticatedSiteId && siteClients.has(authenticatedSiteId)) {
|
|
119
|
+
siteClients.get(authenticatedSiteId).delete(ws);
|
|
120
|
+
if (siteClients.get(authenticatedSiteId).size === 0) {
|
|
121
|
+
siteClients.delete(authenticatedSiteId);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
ws.on('error', () => {
|
|
127
|
+
clearTimeout(authTimer);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const interval = setInterval(() => {
|
|
132
|
+
wss.clients.forEach((ws) => {
|
|
133
|
+
if (!ws.isAlive) return ws.terminate();
|
|
134
|
+
ws.isAlive = false;
|
|
135
|
+
ws.ping();
|
|
136
|
+
});
|
|
137
|
+
}, 30000);
|
|
138
|
+
|
|
139
|
+
wss.on('close', () => clearInterval(interval));
|
|
140
|
+
|
|
141
|
+
return wss;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function broadcastAnalytic(siteId, eventData) {
|
|
145
|
+
const clients = siteClients.get(siteId);
|
|
146
|
+
if (!clients || clients.size === 0) return;
|
|
147
|
+
|
|
148
|
+
const message = JSON.stringify({
|
|
149
|
+
type: 'analytic',
|
|
150
|
+
timestamp: new Date().toISOString(),
|
|
151
|
+
...eventData
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
clients.forEach((ws) => {
|
|
155
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
156
|
+
ws.send(message);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = { setupWebSocket, broadcastAnalytic };
|
|
@@ -1,104 +1,104 @@
|
|
|
1
|
-
# WAB Agent Template — Artisan Marketplace Scout
|
|
2
|
-
# Discovers handmade products from small artisans worldwide
|
|
3
|
-
# Run: npx wab-agent run artisan-marketplace.yaml
|
|
4
|
-
|
|
5
|
-
name: artisan-marketplace
|
|
6
|
-
version: 1.0.0
|
|
7
|
-
description: Discover and buy handmade products directly from artisans, not intermediaries
|
|
8
|
-
author: WAB Community
|
|
9
|
-
tags: [artisan, handmade, fair-trade, crafts, direct-buy]
|
|
10
|
-
|
|
11
|
-
goal: >
|
|
12
|
-
Find WAB-enabled artisan shops, evaluate product authenticity and craftsmanship,
|
|
13
|
-
compare prices against mass-produced alternatives, negotiate fair prices that
|
|
14
|
-
support artisans, and verify product descriptions are accurate.
|
|
15
|
-
|
|
16
|
-
target_sites:
|
|
17
|
-
discovery_method: wab-registry
|
|
18
|
-
category: artisan-crafts
|
|
19
|
-
region: global
|
|
20
|
-
fallback_urls: []
|
|
21
|
-
|
|
22
|
-
parameters:
|
|
23
|
-
- name: product_type
|
|
24
|
-
type: string
|
|
25
|
-
required: true
|
|
26
|
-
description: "Type of handmade product (e.g. pottery, textiles, jewelry, leather)"
|
|
27
|
-
- name: origin_country
|
|
28
|
-
type: string
|
|
29
|
-
description: Preferred country of origin
|
|
30
|
-
- name: max_price
|
|
31
|
-
type: number
|
|
32
|
-
default: 500
|
|
33
|
-
|
|
34
|
-
actions:
|
|
35
|
-
- name: discover
|
|
36
|
-
description: Find artisan shops selling the target product type
|
|
37
|
-
wab_action: discover
|
|
38
|
-
|
|
39
|
-
- name: browse_products
|
|
40
|
-
description: Browse available handmade products
|
|
41
|
-
wab_action: getProducts
|
|
42
|
-
params:
|
|
43
|
-
category: "{{product_type}}"
|
|
44
|
-
handmade: true
|
|
45
|
-
|
|
46
|
-
- name: verify_authenticity
|
|
47
|
-
description: Check product descriptions against photos
|
|
48
|
-
wab_action: verifyText
|
|
49
|
-
params:
|
|
50
|
-
fields: [material, origin, technique, dimensions]
|
|
51
|
-
|
|
52
|
-
- name: check_artisan_profile
|
|
53
|
-
description: Verify the artisan is a real person/workshop
|
|
54
|
-
wab_action: getArtisanProfile
|
|
55
|
-
|
|
56
|
-
- name: compare_prices
|
|
57
|
-
description: Compare with mass-produced alternatives for fair pricing
|
|
58
|
-
wab_action: getPrice
|
|
59
|
-
collect: true
|
|
60
|
-
|
|
61
|
-
- name: negotiate
|
|
62
|
-
description: Negotiate a fair price supporting the artisan
|
|
63
|
-
wab_action: negotiate
|
|
64
|
-
strategy: first_time
|
|
65
|
-
conditions:
|
|
66
|
-
proposed_discount: 5
|
|
67
|
-
fallback_strategy: instant_payment
|
|
68
|
-
|
|
69
|
-
- name: purchase
|
|
70
|
-
description: Purchase at agreed price
|
|
71
|
-
wab_action: buy
|
|
72
|
-
requires: [verify_authenticity]
|
|
73
|
-
|
|
74
|
-
fairness_rules:
|
|
75
|
-
prefer_local: false
|
|
76
|
-
prefer_small_business: true
|
|
77
|
-
max_price: "{{max_price}}"
|
|
78
|
-
currency: USD
|
|
79
|
-
min_reputation_score: 30
|
|
80
|
-
avoid_monopolies: true
|
|
81
|
-
support_artisans: true
|
|
82
|
-
|
|
83
|
-
negotiation:
|
|
84
|
-
enabled: true
|
|
85
|
-
max_rounds: 2
|
|
86
|
-
strategies:
|
|
87
|
-
- first_time
|
|
88
|
-
- instant_payment
|
|
89
|
-
- bulk_order
|
|
90
|
-
walk_away_threshold: 3
|
|
91
|
-
respect_artisan_floor: true
|
|
92
|
-
|
|
93
|
-
verification:
|
|
94
|
-
anti_hallucination: true
|
|
95
|
-
cross_check_prices: true
|
|
96
|
-
verify_descriptions: true
|
|
97
|
-
require_vision_match: true
|
|
98
|
-
max_price_variance: 0.15
|
|
99
|
-
|
|
100
|
-
output:
|
|
101
|
-
format: json
|
|
102
|
-
include: [artisan_name, product, material, origin, price, negotiated_price, authenticity_score, reputation]
|
|
103
|
-
sort_by: authenticity_score
|
|
104
|
-
limit: 15
|
|
1
|
+
# WAB Agent Template — Artisan Marketplace Scout
|
|
2
|
+
# Discovers handmade products from small artisans worldwide
|
|
3
|
+
# Run: npx wab-agent run artisan-marketplace.yaml
|
|
4
|
+
|
|
5
|
+
name: artisan-marketplace
|
|
6
|
+
version: 1.0.0
|
|
7
|
+
description: Discover and buy handmade products directly from artisans, not intermediaries
|
|
8
|
+
author: WAB Community
|
|
9
|
+
tags: [artisan, handmade, fair-trade, crafts, direct-buy]
|
|
10
|
+
|
|
11
|
+
goal: >
|
|
12
|
+
Find WAB-enabled artisan shops, evaluate product authenticity and craftsmanship,
|
|
13
|
+
compare prices against mass-produced alternatives, negotiate fair prices that
|
|
14
|
+
support artisans, and verify product descriptions are accurate.
|
|
15
|
+
|
|
16
|
+
target_sites:
|
|
17
|
+
discovery_method: wab-registry
|
|
18
|
+
category: artisan-crafts
|
|
19
|
+
region: global
|
|
20
|
+
fallback_urls: []
|
|
21
|
+
|
|
22
|
+
parameters:
|
|
23
|
+
- name: product_type
|
|
24
|
+
type: string
|
|
25
|
+
required: true
|
|
26
|
+
description: "Type of handmade product (e.g. pottery, textiles, jewelry, leather)"
|
|
27
|
+
- name: origin_country
|
|
28
|
+
type: string
|
|
29
|
+
description: Preferred country of origin
|
|
30
|
+
- name: max_price
|
|
31
|
+
type: number
|
|
32
|
+
default: 500
|
|
33
|
+
|
|
34
|
+
actions:
|
|
35
|
+
- name: discover
|
|
36
|
+
description: Find artisan shops selling the target product type
|
|
37
|
+
wab_action: discover
|
|
38
|
+
|
|
39
|
+
- name: browse_products
|
|
40
|
+
description: Browse available handmade products
|
|
41
|
+
wab_action: getProducts
|
|
42
|
+
params:
|
|
43
|
+
category: "{{product_type}}"
|
|
44
|
+
handmade: true
|
|
45
|
+
|
|
46
|
+
- name: verify_authenticity
|
|
47
|
+
description: Check product descriptions against photos
|
|
48
|
+
wab_action: verifyText
|
|
49
|
+
params:
|
|
50
|
+
fields: [material, origin, technique, dimensions]
|
|
51
|
+
|
|
52
|
+
- name: check_artisan_profile
|
|
53
|
+
description: Verify the artisan is a real person/workshop
|
|
54
|
+
wab_action: getArtisanProfile
|
|
55
|
+
|
|
56
|
+
- name: compare_prices
|
|
57
|
+
description: Compare with mass-produced alternatives for fair pricing
|
|
58
|
+
wab_action: getPrice
|
|
59
|
+
collect: true
|
|
60
|
+
|
|
61
|
+
- name: negotiate
|
|
62
|
+
description: Negotiate a fair price supporting the artisan
|
|
63
|
+
wab_action: negotiate
|
|
64
|
+
strategy: first_time
|
|
65
|
+
conditions:
|
|
66
|
+
proposed_discount: 5
|
|
67
|
+
fallback_strategy: instant_payment
|
|
68
|
+
|
|
69
|
+
- name: purchase
|
|
70
|
+
description: Purchase at agreed price
|
|
71
|
+
wab_action: buy
|
|
72
|
+
requires: [verify_authenticity]
|
|
73
|
+
|
|
74
|
+
fairness_rules:
|
|
75
|
+
prefer_local: false
|
|
76
|
+
prefer_small_business: true
|
|
77
|
+
max_price: "{{max_price}}"
|
|
78
|
+
currency: USD
|
|
79
|
+
min_reputation_score: 30
|
|
80
|
+
avoid_monopolies: true
|
|
81
|
+
support_artisans: true
|
|
82
|
+
|
|
83
|
+
negotiation:
|
|
84
|
+
enabled: true
|
|
85
|
+
max_rounds: 2
|
|
86
|
+
strategies:
|
|
87
|
+
- first_time
|
|
88
|
+
- instant_payment
|
|
89
|
+
- bulk_order
|
|
90
|
+
walk_away_threshold: 3
|
|
91
|
+
respect_artisan_floor: true
|
|
92
|
+
|
|
93
|
+
verification:
|
|
94
|
+
anti_hallucination: true
|
|
95
|
+
cross_check_prices: true
|
|
96
|
+
verify_descriptions: true
|
|
97
|
+
require_vision_match: true
|
|
98
|
+
max_price_variance: 0.15
|
|
99
|
+
|
|
100
|
+
output:
|
|
101
|
+
format: json
|
|
102
|
+
include: [artisan_name, product, material, origin, price, negotiated_price, authenticity_score, reputation]
|
|
103
|
+
sort_by: authenticity_score
|
|
104
|
+
limit: 15
|