web-agent-bridge 1.2.0 → 2.0.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 (94) hide show
  1. package/LICENSE +21 -21
  2. package/README.ar.md +446 -446
  3. package/README.md +780 -933
  4. package/bin/cli.js +80 -80
  5. package/bin/wab.js +80 -80
  6. package/examples/bidi-agent.js +119 -119
  7. package/examples/mcp-agent.js +94 -94
  8. package/examples/next-app-router/README.md +44 -0
  9. package/examples/puppeteer-agent.js +108 -108
  10. package/examples/saas-dashboard/README.md +55 -0
  11. package/examples/shopify-hydrogen/README.md +74 -0
  12. package/examples/vision-agent.js +171 -171
  13. package/examples/wordpress-elementor/README.md +77 -0
  14. package/package.json +69 -78
  15. package/public/.well-known/ai-assets.json +59 -0
  16. package/public/admin/login.html +84 -84
  17. package/public/ai.html +196 -0
  18. package/public/cookies.html +208 -208
  19. package/public/css/premium.css +317 -0
  20. package/public/css/styles.css +1235 -1235
  21. package/public/dashboard.html +704 -704
  22. package/public/demo.html +259 -0
  23. package/public/docs.html +585 -585
  24. package/public/feed.xml +89 -0
  25. package/public/index.html +495 -332
  26. package/public/js/auth-nav.js +31 -31
  27. package/public/js/auth-redirect.js +12 -12
  28. package/public/js/cookie-consent.js +56 -56
  29. package/public/js/wab-demo-page.js +721 -0
  30. package/public/js/ws-client.js +74 -74
  31. package/public/llms-full.txt +309 -0
  32. package/public/llms.txt +85 -0
  33. package/public/login.html +83 -83
  34. package/public/openapi.json +580 -0
  35. package/public/premium-dashboard.html +2487 -0
  36. package/public/premium.html +791 -0
  37. package/public/privacy.html +295 -295
  38. package/public/register.html +103 -103
  39. package/public/robots.txt +87 -0
  40. package/public/script/wab-consent.d.ts +36 -0
  41. package/public/script/wab-consent.js +104 -0
  42. package/public/script/wab-schema.js +131 -0
  43. package/public/script/wab.d.ts +108 -0
  44. package/public/script/wab.min.js +234 -0
  45. package/public/sitemap.xml +93 -0
  46. package/public/terms.html +254 -254
  47. package/public/video/tutorial.mp4 +0 -0
  48. package/script/ai-agent-bridge.js +1558 -1513
  49. package/sdk/README.md +55 -55
  50. package/sdk/index.d.ts +118 -0
  51. package/sdk/index.js +257 -203
  52. package/sdk/package.json +14 -14
  53. package/sdk/schema-discovery.js +83 -0
  54. package/server/config/secrets.js +94 -92
  55. package/server/index.js +0 -9
  56. package/server/middleware/adminAuth.js +30 -30
  57. package/server/middleware/auth.js +41 -41
  58. package/server/middleware/rateLimits.js +24 -24
  59. package/server/migrations/001_add_analytics_indexes.sql +7 -7
  60. package/server/migrations/002_premium_features.sql +418 -0
  61. package/server/models/adapters/index.js +33 -33
  62. package/server/models/adapters/mysql.js +183 -183
  63. package/server/models/adapters/postgresql.js +172 -172
  64. package/server/models/adapters/sqlite.js +7 -7
  65. package/server/models/db.js +561 -561
  66. package/server/routes/admin-premium.js +671 -0
  67. package/server/routes/admin.js +247 -247
  68. package/server/routes/api.js +131 -138
  69. package/server/routes/auth.js +51 -51
  70. package/server/routes/billing.js +45 -45
  71. package/server/routes/discovery.js +406 -329
  72. package/server/routes/license.js +240 -240
  73. package/server/routes/noscript.js +543 -543
  74. package/server/routes/premium-v2.js +686 -0
  75. package/server/routes/premium.js +724 -0
  76. package/server/routes/wab-api.js +476 -476
  77. package/server/services/agent-memory.js +625 -0
  78. package/server/services/email.js +204 -204
  79. package/server/services/fairness.js +420 -420
  80. package/server/services/plugins.js +747 -0
  81. package/server/services/premium.js +1883 -0
  82. package/server/services/self-healing.js +843 -0
  83. package/server/services/stripe.js +192 -192
  84. package/server/services/swarm.js +788 -0
  85. package/server/services/vision.js +871 -0
  86. package/server/utils/cache.js +125 -125
  87. package/server/utils/migrate.js +81 -81
  88. package/server/utils/secureFields.js +50 -50
  89. package/server/ws.js +101 -101
  90. package/docs/DEPLOY.md +0 -118
  91. package/docs/SPEC.md +0 -1540
  92. package/wab-mcp-adapter/README.md +0 -136
  93. package/wab-mcp-adapter/index.js +0 -555
  94. package/wab-mcp-adapter/package.json +0 -17
@@ -0,0 +1,1883 @@
1
+ const { db } = require('../models/db');
2
+ const { v4: uuidv4 } = require('uuid');
3
+ const crypto = require('crypto');
4
+ const bcrypt = require('bcryptjs');
5
+ const path = require('path');
6
+ const fs = require('fs');
7
+ const http = require('http');
8
+ const https = require('https');
9
+ const { URL } = require('url');
10
+
11
+ // ═══════════════════════════════════════════════════════════════════════
12
+ // 1. Agent Traffic Intelligence
13
+ // ═══════════════════════════════════════════════════════════════════════
14
+
15
+ const BOT_PATTERNS = [
16
+ { pattern: /googlebot/i, name: 'Googlebot', platform: 'Google', type: 'friendly' },
17
+ { pattern: /bingbot/i, name: 'Bingbot', platform: 'Microsoft', type: 'friendly' },
18
+ { pattern: /yandexbot/i, name: 'YandexBot', platform: 'Yandex', type: 'friendly' },
19
+ { pattern: /baiduspider/i, name: 'Baiduspider', platform: 'Baidu', type: 'friendly' },
20
+ { pattern: /duckduckbot/i, name: 'DuckDuckBot', platform: 'DuckDuckGo', type: 'friendly' },
21
+ { pattern: /slurp/i, name: 'Slurp', platform: 'Yahoo', type: 'friendly' },
22
+ { pattern: /facebot|facebookexternalhit/i, name: 'Facebot', platform: 'Meta', type: 'friendly' },
23
+ { pattern: /twitterbot/i, name: 'Twitterbot', platform: 'Twitter/X', type: 'friendly' },
24
+ { pattern: /linkedinbot/i, name: 'LinkedInBot', platform: 'LinkedIn', type: 'friendly' },
25
+ { pattern: /applebot/i, name: 'Applebot', platform: 'Apple', type: 'friendly' },
26
+ { pattern: /gptbot/i, name: 'GPTBot', platform: 'OpenAI', type: 'friendly' },
27
+ { pattern: /chatgpt-user/i, name: 'ChatGPT-User', platform: 'OpenAI', type: 'friendly' },
28
+ { pattern: /oai-searchbot/i, name: 'OAI-SearchBot', platform: 'OpenAI', type: 'friendly' },
29
+ { pattern: /claude-web/i, name: 'Claude-Web', platform: 'Anthropic', type: 'friendly' },
30
+ { pattern: /claudebot/i, name: 'ClaudeBot', platform: 'Anthropic', type: 'friendly' },
31
+ { pattern: /anthropic-ai/i, name: 'Anthropic-AI', platform: 'Anthropic', type: 'friendly' },
32
+ { pattern: /cohere-ai/i, name: 'Cohere-AI', platform: 'Cohere', type: 'friendly' },
33
+ { pattern: /perplexitybot/i, name: 'PerplexityBot', platform: 'Perplexity', type: 'friendly' },
34
+ { pattern: /gemini/i, name: 'Gemini', platform: 'Google', type: 'friendly' },
35
+ { pattern: /petalbot/i, name: 'PetalBot', platform: 'Huawei', type: 'friendly' },
36
+ { pattern: /semrushbot/i, name: 'SemrushBot', platform: 'Semrush', type: 'suspicious' },
37
+ { pattern: /ahrefsbot/i, name: 'AhrefsBot', platform: 'Ahrefs', type: 'suspicious' },
38
+ { pattern: /mj12bot/i, name: 'MJ12Bot', platform: 'Majestic', type: 'suspicious' },
39
+ { pattern: /dotbot/i, name: 'DotBot', platform: 'Moz', type: 'suspicious' },
40
+ { pattern: /scrapy/i, name: 'Scrapy', platform: 'Unknown', type: 'aggressive' },
41
+ { pattern: /httpclient/i, name: 'HTTPClient', platform: 'Unknown', type: 'suspicious' },
42
+ { pattern: /python-requests/i, name: 'Python-Requests', platform: 'Python', type: 'suspicious' },
43
+ { pattern: /curl\//i, name: 'cURL', platform: 'CLI', type: 'suspicious' },
44
+ { pattern: /wget\//i, name: 'Wget', platform: 'CLI', type: 'suspicious' },
45
+ { pattern: /go-http-client/i, name: 'Go-HTTP-Client', platform: 'Go', type: 'suspicious' },
46
+ { pattern: /java\//i, name: 'Java-HTTP', platform: 'Java', type: 'suspicious' },
47
+ { pattern: /headlesschrome/i, name: 'HeadlessChrome', platform: 'Puppeteer', type: 'aggressive' },
48
+ { pattern: /phantomjs/i, name: 'PhantomJS', platform: 'PhantomJS', type: 'aggressive' },
49
+ ];
50
+
51
+ function parseUserAgent(uaString) {
52
+ if (!uaString) return { agentName: 'Unknown', platform: 'Unknown', agentType: 'unknown' };
53
+
54
+ for (const bot of BOT_PATTERNS) {
55
+ if (bot.pattern.test(uaString)) {
56
+ return { agentName: bot.name, platform: bot.platform, agentType: bot.type };
57
+ }
58
+ }
59
+
60
+ let platform = 'Unknown';
61
+ if (/windows/i.test(uaString)) platform = 'Windows';
62
+ else if (/macintosh|mac os/i.test(uaString)) platform = 'macOS';
63
+ else if (/linux/i.test(uaString)) platform = 'Linux';
64
+ else if (/android/i.test(uaString)) platform = 'Android';
65
+ else if (/iphone|ipad/i.test(uaString)) platform = 'iOS';
66
+
67
+ let agentName = 'Browser';
68
+ if (/chrome/i.test(uaString) && !/edge|opr/i.test(uaString)) agentName = 'Chrome';
69
+ else if (/firefox/i.test(uaString)) agentName = 'Firefox';
70
+ else if (/safari/i.test(uaString) && !/chrome/i.test(uaString)) agentName = 'Safari';
71
+ else if (/edg/i.test(uaString)) agentName = 'Edge';
72
+ else if (/opr|opera/i.test(uaString)) agentName = 'Opera';
73
+
74
+ return { agentName, platform, agentType: 'unknown' };
75
+ }
76
+
77
+ function hashIP(ip) {
78
+ return crypto.createHash('sha256').update(ip + (process.env.IP_SALT || 'wab-salt')).digest('hex').substring(0, 16);
79
+ }
80
+
81
+ function recordAgentVisit(siteId, { userAgent, ip, country }) {
82
+ const { agentName, platform, agentType } = parseUserAgent(userAgent);
83
+ const ipHash = hashIP(ip || '0.0.0.0');
84
+
85
+ const existing = db.prepare(
86
+ `SELECT id, total_requests FROM agent_profiles WHERE site_id = ? AND agent_signature = ? AND ip_hash = ?`
87
+ ).get(siteId, agentName, ipHash);
88
+
89
+ if (existing) {
90
+ db.prepare(
91
+ `UPDATE agent_profiles SET total_requests = total_requests + 1, last_seen = datetime('now'), country = COALESCE(?, country), platform = COALESCE(?, platform) WHERE id = ?`
92
+ ).run(country || null, platform, existing.id);
93
+ return { id: existing.id, new: false };
94
+ }
95
+
96
+ const id = uuidv4();
97
+ db.prepare(
98
+ `INSERT INTO agent_profiles (id, site_id, agent_signature, agent_type, platform, country, ip_hash, last_seen, total_requests) VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), 1)`
99
+ ).run(id, siteId, agentName, agentType, platform, country || null, ipHash);
100
+ return { id, new: true };
101
+ }
102
+
103
+ function getAgentProfiles(siteId, { limit = 50, offset = 0, type } = {}) {
104
+ if (type) {
105
+ return db.prepare(
106
+ `SELECT * FROM agent_profiles WHERE site_id = ? AND agent_type = ? ORDER BY last_seen DESC LIMIT ? OFFSET ?`
107
+ ).all(siteId, type, limit, offset);
108
+ }
109
+ return db.prepare(
110
+ `SELECT * FROM agent_profiles WHERE site_id = ? ORDER BY last_seen DESC LIMIT ? OFFSET ?`
111
+ ).all(siteId, limit, offset);
112
+ }
113
+
114
+ function getAnomalyAlerts(siteId, { limit = 50, acknowledged } = {}) {
115
+ if (acknowledged !== undefined) {
116
+ return db.prepare(
117
+ `SELECT * FROM anomaly_alerts WHERE site_id = ? AND acknowledged = ? ORDER BY created_at DESC LIMIT ?`
118
+ ).all(siteId, acknowledged ? 1 : 0, limit);
119
+ }
120
+ return db.prepare(
121
+ `SELECT * FROM anomaly_alerts WHERE site_id = ? ORDER BY created_at DESC LIMIT ?`
122
+ ).all(siteId, limit);
123
+ }
124
+
125
+ function checkForAnomalies(siteId) {
126
+ const alerts = [];
127
+
128
+ const recentCount = db.prepare(
129
+ `SELECT COUNT(*) as c FROM agent_profiles WHERE site_id = ? AND last_seen >= datetime('now', '-1 minute')`
130
+ ).get(siteId).c;
131
+
132
+ if (recentCount > 1000) {
133
+ const id = db.prepare(
134
+ `INSERT INTO anomaly_alerts (site_id, alert_type, severity, message, metadata) VALUES (?, 'traffic_spike', 'high', ?, ?)`
135
+ ).run(siteId, `Traffic spike detected: ${recentCount} requests in the last minute`, JSON.stringify({ count: recentCount }));
136
+ alerts.push({ id: id.lastInsertRowid, type: 'traffic_spike', severity: 'high', count: recentCount });
137
+ }
138
+
139
+ const suspiciousAgents = db.prepare(
140
+ `SELECT agent_signature, COUNT(*) as c FROM agent_profiles WHERE site_id = ? AND agent_type IN ('aggressive', 'suspicious') AND first_seen >= datetime('now', '-1 hour') GROUP BY agent_signature HAVING c >= 5`
141
+ ).all(siteId);
142
+
143
+ for (const agent of suspiciousAgents) {
144
+ const alreadyAlerted = db.prepare(
145
+ `SELECT id FROM anomaly_alerts WHERE site_id = ? AND alert_type = 'suspicious_agent' AND message LIKE ? AND created_at >= datetime('now', '-1 hour')`
146
+ ).get(siteId, `%${agent.agent_signature}%`);
147
+
148
+ if (!alreadyAlerted) {
149
+ const res = db.prepare(
150
+ `INSERT INTO anomaly_alerts (site_id, alert_type, severity, message, metadata) VALUES (?, 'suspicious_agent', 'medium', ?, ?)`
151
+ ).run(siteId, `Suspicious agent activity: ${agent.agent_signature} (${agent.c} profiles in last hour)`, JSON.stringify({ agent: agent.agent_signature, count: agent.c }));
152
+ alerts.push({ id: res.lastInsertRowid, type: 'suspicious_agent', severity: 'medium', agent: agent.agent_signature });
153
+ }
154
+ }
155
+
156
+ const aggressiveCount = db.prepare(
157
+ `SELECT COUNT(*) as c FROM agent_profiles WHERE site_id = ? AND agent_type = 'aggressive' AND last_seen >= datetime('now', '-10 minutes')`
158
+ ).get(siteId).c;
159
+
160
+ if (aggressiveCount > 10) {
161
+ const res = db.prepare(
162
+ `INSERT INTO anomaly_alerts (site_id, alert_type, severity, message, metadata) VALUES (?, 'aggressive_swarm', 'critical', ?, ?)`
163
+ ).run(siteId, `Aggressive bot swarm: ${aggressiveCount} aggressive agents in last 10 minutes`, JSON.stringify({ count: aggressiveCount }));
164
+ alerts.push({ id: res.lastInsertRowid, type: 'aggressive_swarm', severity: 'critical', count: aggressiveCount });
165
+ }
166
+
167
+ return alerts;
168
+ }
169
+
170
+ function acknowledgeAlert(alertId, siteId) {
171
+ const result = db.prepare(
172
+ `UPDATE anomaly_alerts SET acknowledged = 1 WHERE id = ? AND site_id = ?`
173
+ ).run(alertId, siteId);
174
+ return result.changes > 0;
175
+ }
176
+
177
+ function getTrafficStats(siteId, days = 30) {
178
+ const since = new Date(Date.now() - days * 86400000).toISOString();
179
+
180
+ const totalAgents = db.prepare(
181
+ `SELECT COUNT(*) as c FROM agent_profiles WHERE site_id = ? AND last_seen >= ?`
182
+ ).get(siteId, since).c;
183
+
184
+ const byType = db.prepare(
185
+ `SELECT agent_type, COUNT(*) as count, SUM(total_requests) as requests FROM agent_profiles WHERE site_id = ? AND last_seen >= ? GROUP BY agent_type`
186
+ ).all(siteId, since);
187
+
188
+ const byCountry = db.prepare(
189
+ `SELECT country, COUNT(*) as count FROM agent_profiles WHERE site_id = ? AND last_seen >= ? AND country IS NOT NULL GROUP BY country ORDER BY count DESC LIMIT 20`
190
+ ).all(siteId, since);
191
+
192
+ const byPlatform = db.prepare(
193
+ `SELECT platform, COUNT(*) as count FROM agent_profiles WHERE site_id = ? AND last_seen >= ? GROUP BY platform ORDER BY count DESC`
194
+ ).all(siteId, since);
195
+
196
+ const totalRequests = db.prepare(
197
+ `SELECT COALESCE(SUM(total_requests), 0) as c FROM agent_profiles WHERE site_id = ? AND last_seen >= ?`
198
+ ).get(siteId, since).c;
199
+
200
+ return { totalAgents, totalRequests, byType, byCountry, byPlatform };
201
+ }
202
+
203
+ // ═══════════════════════════════════════════════════════════════════════
204
+ // 2. Advanced Exploit Shield
205
+ // ═══════════════════════════════════════════════════════════════════════
206
+
207
+ function logSecurityEvent(siteId, { eventType, severity, agentSignature, ipHash, details }) {
208
+ const result = db.prepare(
209
+ `INSERT INTO security_events (site_id, event_type, severity, agent_signature, ip_hash, details) VALUES (?, ?, ?, ?, ?, ?)`
210
+ ).run(siteId, eventType, severity || 'medium', agentSignature || null, ipHash || null, JSON.stringify(details || {}));
211
+ return { id: result.lastInsertRowid };
212
+ }
213
+
214
+ function getSecurityEvents(siteId, { limit = 50, severity, since } = {}) {
215
+ const conditions = ['site_id = ?'];
216
+ const params = [siteId];
217
+
218
+ if (severity) {
219
+ conditions.push('severity = ?');
220
+ params.push(severity);
221
+ }
222
+ if (since) {
223
+ conditions.push('created_at >= ?');
224
+ params.push(since);
225
+ }
226
+
227
+ params.push(limit);
228
+ return db.prepare(
229
+ `SELECT * FROM security_events WHERE ${conditions.join(' AND ')} ORDER BY created_at DESC LIMIT ?`
230
+ ).all(...params);
231
+ }
232
+
233
+ function blockAgent(siteId, { agentSignature, reason, expiresAt }) {
234
+ const id = uuidv4();
235
+ db.prepare(
236
+ `INSERT INTO blocked_agents (id, site_id, agent_signature, reason, expires_at) VALUES (?, ?, ?, ?, ?)`
237
+ ).run(id, siteId, agentSignature, reason || null, expiresAt || null);
238
+ return { id, agentSignature, reason };
239
+ }
240
+
241
+ function unblockAgent(blockId, siteId) {
242
+ const result = db.prepare(
243
+ `UPDATE blocked_agents SET active = 0 WHERE id = ? AND site_id = ?`
244
+ ).run(blockId, siteId);
245
+ return result.changes > 0;
246
+ }
247
+
248
+ function isAgentBlocked(siteId, agentSignature) {
249
+ const row = db.prepare(
250
+ `SELECT id FROM blocked_agents WHERE site_id = ? AND agent_signature = ? AND active = 1 AND (expires_at IS NULL OR expires_at > datetime('now'))`
251
+ ).get(siteId, agentSignature);
252
+ return !!row;
253
+ }
254
+
255
+ function getBlockedAgents(siteId) {
256
+ return db.prepare(
257
+ `SELECT * FROM blocked_agents WHERE site_id = ? AND active = 1 AND (expires_at IS NULL OR expires_at > datetime('now')) ORDER BY blocked_at DESC`
258
+ ).all(siteId);
259
+ }
260
+
261
+ function getSecurityReport(siteId, days = 30) {
262
+ const since = new Date(Date.now() - days * 86400000).toISOString();
263
+
264
+ const eventsByType = db.prepare(
265
+ `SELECT event_type, COUNT(*) as count FROM security_events WHERE site_id = ? AND created_at >= ? GROUP BY event_type ORDER BY count DESC`
266
+ ).all(siteId, since);
267
+
268
+ const severityDist = db.prepare(
269
+ `SELECT severity, COUNT(*) as count FROM security_events WHERE site_id = ? AND created_at >= ? GROUP BY severity`
270
+ ).all(siteId, since);
271
+
272
+ const topBlocked = db.prepare(
273
+ `SELECT agent_signature, COUNT(*) as count FROM blocked_agents WHERE site_id = ? AND blocked_at >= ? GROUP BY agent_signature ORDER BY count DESC LIMIT 10`
274
+ ).all(siteId, since);
275
+
276
+ const timeline = db.prepare(
277
+ `SELECT date(created_at) as day, COUNT(*) as count FROM security_events WHERE site_id = ? AND created_at >= ? GROUP BY day ORDER BY day`
278
+ ).all(siteId, since);
279
+
280
+ const totalEvents = db.prepare(
281
+ `SELECT COUNT(*) as c FROM security_events WHERE site_id = ? AND created_at >= ?`
282
+ ).get(siteId, since).c;
283
+
284
+ const activeBlocks = db.prepare(
285
+ `SELECT COUNT(*) as c FROM blocked_agents WHERE site_id = ? AND active = 1`
286
+ ).get(siteId).c;
287
+
288
+ return { totalEvents, activeBlocks, eventsByType, severityDist, topBlocked, timeline };
289
+ }
290
+
291
+ const INJECTION_PATTERNS = [
292
+ /[;|`$]/, /\.\.\//,
293
+ /<script/i, /javascript:/i, /on\w+\s*=/i,
294
+ /select\s+.+\s+from\s/i, /union\s+select/i, /drop\s+table/i,
295
+ /insert\s+into/i, /delete\s+from/i, /update\s+\w+\s+set/i, /alter\s+table/i,
296
+ /eval\s*\(/, /exec\s*\(/, /require\s*\(/, /import\s*\(/,
297
+ ];
298
+
299
+ function autoDetectThreats(siteId, { ip, userAgent, action }) {
300
+ const ipHash = hashIP(ip || '0.0.0.0');
301
+ const { agentName } = parseUserAgent(userAgent);
302
+
303
+ if (isAgentBlocked(siteId, agentName)) {
304
+ return { blocked: true, reason: 'Agent is on the blocklist' };
305
+ }
306
+
307
+ if (action) {
308
+ for (const pat of INJECTION_PATTERNS) {
309
+ if (pat.test(action)) {
310
+ logSecurityEvent(siteId, {
311
+ eventType: 'injection_attempt',
312
+ severity: 'high',
313
+ agentSignature: agentName,
314
+ ipHash,
315
+ details: { action, pattern: pat.toString() },
316
+ });
317
+ return { blocked: true, reason: 'Potential injection attack detected in action' };
318
+ }
319
+ }
320
+ }
321
+
322
+ const recentFromIp = db.prepare(
323
+ `SELECT COUNT(*) as c FROM security_events WHERE site_id = ? AND ip_hash = ? AND created_at >= datetime('now', '-1 minute')`
324
+ ).get(siteId, ipHash).c;
325
+
326
+ if (recentFromIp > 100) {
327
+ logSecurityEvent(siteId, {
328
+ eventType: 'rate_limit_exceeded',
329
+ severity: 'high',
330
+ agentSignature: agentName,
331
+ ipHash,
332
+ details: { requestsPerMinute: recentFromIp },
333
+ });
334
+ return { blocked: true, reason: 'Rate limit exceeded from this IP' };
335
+ }
336
+
337
+ return { blocked: false, reason: null };
338
+ }
339
+
340
+ // ═══════════════════════════════════════════════════════════════════════
341
+ // 3. Smart Actions Library
342
+ // ═══════════════════════════════════════════════════════════════════════
343
+
344
+ function getActionPacks({ platform, tierRequired } = {}) {
345
+ const conditions = [];
346
+ const params = [];
347
+
348
+ if (platform) {
349
+ conditions.push('platform = ?');
350
+ params.push(platform);
351
+ }
352
+ if (tierRequired) {
353
+ conditions.push('tier_required = ?');
354
+ params.push(tierRequired);
355
+ }
356
+
357
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
358
+ return db.prepare(
359
+ `SELECT * FROM action_packs ${where} ORDER BY name`
360
+ ).all(...params);
361
+ }
362
+
363
+ function getActionPack(packId) {
364
+ return db.prepare(`SELECT * FROM action_packs WHERE id = ?`).get(packId) || null;
365
+ }
366
+
367
+ function installPack(siteId, packId, config = {}) {
368
+ const existing = db.prepare(
369
+ `SELECT id FROM installed_packs WHERE site_id = ? AND pack_id = ?`
370
+ ).get(siteId, packId);
371
+
372
+ if (existing) {
373
+ db.prepare(`UPDATE installed_packs SET config = ? WHERE id = ?`).run(JSON.stringify(config), existing.id);
374
+ return { id: existing.id, updated: true };
375
+ }
376
+
377
+ const id = uuidv4();
378
+ db.prepare(
379
+ `INSERT INTO installed_packs (id, site_id, pack_id, config) VALUES (?, ?, ?, ?)`
380
+ ).run(id, siteId, packId, JSON.stringify(config));
381
+ return { id, updated: false };
382
+ }
383
+
384
+ function uninstallPack(installId, siteId) {
385
+ const result = db.prepare(
386
+ `DELETE FROM installed_packs WHERE id = ? AND site_id = ?`
387
+ ).run(installId, siteId);
388
+ return result.changes > 0;
389
+ }
390
+
391
+ function getInstalledPacks(siteId) {
392
+ return db.prepare(
393
+ `SELECT ip.*, ap.name, ap.platform, ap.description, ap.version, ap.icon, ap.tier_required FROM installed_packs ip JOIN action_packs ap ON ip.pack_id = ap.id WHERE ip.site_id = ? ORDER BY ip.installed_at DESC`
394
+ ).all(siteId);
395
+ }
396
+
397
+ function getPackActions(packId) {
398
+ const pack = db.prepare(`SELECT actions_json FROM action_packs WHERE id = ?`).get(packId);
399
+ if (!pack) return null;
400
+ try {
401
+ return JSON.parse(pack.actions_json);
402
+ } catch {
403
+ return [];
404
+ }
405
+ }
406
+
407
+ // ═══════════════════════════════════════════════════════════════════════
408
+ // 4. Custom AI Agents
409
+ // ═══════════════════════════════════════════════════════════════════════
410
+
411
+ function parseSchedule(schedule) {
412
+ if (!schedule) return null;
413
+ const now = new Date();
414
+
415
+ const everyHoursMatch = schedule.match(/^every\s+(\d+)h$/i);
416
+ if (everyHoursMatch) {
417
+ const hours = parseInt(everyHoursMatch[1], 10);
418
+ return new Date(now.getTime() + hours * 3600000);
419
+ }
420
+
421
+ const everyMinMatch = schedule.match(/^every\s+(\d+)m$/i);
422
+ if (everyMinMatch) {
423
+ const mins = parseInt(everyMinMatch[1], 10);
424
+ return new Date(now.getTime() + mins * 60000);
425
+ }
426
+
427
+ const dailyMatch = schedule.match(/^daily\s+(\d{1,2}):(\d{2})$/i);
428
+ if (dailyMatch) {
429
+ const h = parseInt(dailyMatch[1], 10);
430
+ const m = parseInt(dailyMatch[2], 10);
431
+ const next = new Date(now);
432
+ next.setHours(h, m, 0, 0);
433
+ if (next <= now) next.setDate(next.getDate() + 1);
434
+ return next;
435
+ }
436
+
437
+ const weeklyMatch = schedule.match(/^weekly\s+(mon|tue|wed|thu|fri|sat|sun)\s+(\d{1,2}):(\d{2})$/i);
438
+ if (weeklyMatch) {
439
+ const dayMap = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 };
440
+ const targetDay = dayMap[weeklyMatch[1].toLowerCase()];
441
+ const h = parseInt(weeklyMatch[2], 10);
442
+ const m = parseInt(weeklyMatch[3], 10);
443
+ const next = new Date(now);
444
+ next.setHours(h, m, 0, 0);
445
+ const currentDay = now.getDay();
446
+ let daysUntil = targetDay - currentDay;
447
+ if (daysUntil < 0 || (daysUntil === 0 && next <= now)) daysUntil += 7;
448
+ next.setDate(next.getDate() + daysUntil);
449
+ return next;
450
+ }
451
+
452
+ return null;
453
+ }
454
+
455
+ function createAgent(userId, siteId, { name, description, steps, schedule }) {
456
+ const id = uuidv4();
457
+ const nextRun = schedule ? parseSchedule(schedule) : null;
458
+ db.prepare(
459
+ `INSERT INTO custom_agents (id, user_id, site_id, name, description, steps_json, schedule, next_run) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
460
+ ).run(id, userId, siteId, name, description || null, JSON.stringify(steps || []), schedule || null, nextRun ? nextRun.toISOString() : null);
461
+ return { id, name, description, schedule, next_run: nextRun ? nextRun.toISOString() : null };
462
+ }
463
+
464
+ function updateAgent(agentId, userId, updates) {
465
+ const fields = [];
466
+ const params = [];
467
+
468
+ if (updates.name !== undefined) { fields.push('name = ?'); params.push(updates.name); }
469
+ if (updates.description !== undefined) { fields.push('description = ?'); params.push(updates.description); }
470
+ if (updates.steps !== undefined) { fields.push('steps_json = ?'); params.push(JSON.stringify(updates.steps)); }
471
+ if (updates.schedule !== undefined) {
472
+ fields.push('schedule = ?');
473
+ params.push(updates.schedule);
474
+ const nextRun = parseSchedule(updates.schedule);
475
+ fields.push('next_run = ?');
476
+ params.push(nextRun ? nextRun.toISOString() : null);
477
+ }
478
+ if (updates.status !== undefined) { fields.push('status = ?'); params.push(updates.status); }
479
+
480
+ if (fields.length === 0) return false;
481
+ params.push(agentId, userId);
482
+ const result = db.prepare(
483
+ `UPDATE custom_agents SET ${fields.join(', ')} WHERE id = ? AND user_id = ?`
484
+ ).run(...params);
485
+ return result.changes > 0;
486
+ }
487
+
488
+ function deleteAgent(agentId, userId) {
489
+ const result = db.prepare(
490
+ `DELETE FROM custom_agents WHERE id = ? AND user_id = ?`
491
+ ).run(agentId, userId);
492
+ return result.changes > 0;
493
+ }
494
+
495
+ function getAgents(userId, siteId) {
496
+ if (siteId) {
497
+ return db.prepare(
498
+ `SELECT * FROM custom_agents WHERE user_id = ? AND site_id = ? ORDER BY created_at DESC`
499
+ ).all(userId, siteId);
500
+ }
501
+ return db.prepare(
502
+ `SELECT * FROM custom_agents WHERE user_id = ? ORDER BY created_at DESC`
503
+ ).all(userId);
504
+ }
505
+
506
+ function getAgent(agentId, userId) {
507
+ return db.prepare(
508
+ `SELECT * FROM custom_agents WHERE id = ? AND user_id = ?`
509
+ ).get(agentId, userId) || null;
510
+ }
511
+
512
+ async function runAgent(agentId, userId) {
513
+ const agent = db.prepare(
514
+ `SELECT * FROM custom_agents WHERE id = ? AND user_id = ?`
515
+ ).get(agentId, userId);
516
+ if (!agent) return null;
517
+
518
+ const site = db.prepare(`SELECT * FROM sites WHERE id = ?`).get(agent.site_id);
519
+ const baseUrl = site && site.domain ? (site.domain.startsWith('http') ? site.domain : `https://${site.domain}`) : null;
520
+
521
+ const runId = uuidv4();
522
+ db.prepare(
523
+ `INSERT INTO agent_runs (id, agent_id, status) VALUES (?, ?, 'running')`
524
+ ).run(runId, agentId);
525
+
526
+ let steps;
527
+ try { steps = JSON.parse(agent.steps_json); } catch { steps = []; }
528
+
529
+ const results = [];
530
+ let failed = false;
531
+ let lastResponse = null;
532
+
533
+ for (let i = 0; i < steps.length; i++) {
534
+ const step = steps[i];
535
+ const actionName = step.action || step.name || `step_${i}`;
536
+ const stepStart = Date.now();
537
+
538
+ try {
539
+ let output = null;
540
+
541
+ if (actionName === 'navigate' || actionName === 'goto' || actionName === 'fetch') {
542
+ const targetUrl = step.url || step.value || (baseUrl ? `${baseUrl}${step.path || '/'}` : null);
543
+ if (!targetUrl) throw new Error('No URL specified for navigate step');
544
+ const controller = new AbortController();
545
+ const timeout = setTimeout(() => controller.abort(), step.timeoutMs || 15000);
546
+ try {
547
+ const resp = await fetch(targetUrl, {
548
+ method: step.method || 'GET',
549
+ headers: { 'User-Agent': 'WAB-Agent/1.0', ...(step.headers || {}) },
550
+ body: step.body ? (typeof step.body === 'string' ? step.body : JSON.stringify(step.body)) : undefined,
551
+ signal: controller.signal,
552
+ redirect: 'follow',
553
+ });
554
+ clearTimeout(timeout);
555
+ const contentType = resp.headers.get('content-type') || '';
556
+ let body;
557
+ if (contentType.includes('json')) {
558
+ body = await resp.json();
559
+ } else {
560
+ const text = await resp.text();
561
+ body = text.slice(0, 5000);
562
+ }
563
+ lastResponse = body;
564
+ output = { status: resp.status, statusText: resp.statusText, contentType, bodyLength: typeof body === 'string' ? body.length : JSON.stringify(body).length, url: resp.url };
565
+ if (resp.status >= 400) throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
566
+ } catch (fetchErr) {
567
+ clearTimeout(timeout);
568
+ throw fetchErr;
569
+ }
570
+
571
+ } else if (actionName === 'click' || actionName === 'fill' || actionName === 'select' || actionName === 'submit') {
572
+ if (!baseUrl) throw new Error('Site has no domain configured for DOM actions');
573
+ const wabUrl = `${baseUrl}/api/wab/actions/${actionName}`;
574
+ const controller = new AbortController();
575
+ const timeout = setTimeout(() => controller.abort(), step.timeoutMs || 10000);
576
+ try {
577
+ const resp = await fetch(wabUrl, {
578
+ method: 'POST',
579
+ headers: { 'Content-Type': 'application/json', 'User-Agent': 'WAB-Agent/1.0' },
580
+ body: JSON.stringify({ selector: step.selector, value: step.value, siteId: agent.site_id }),
581
+ signal: controller.signal,
582
+ });
583
+ clearTimeout(timeout);
584
+ output = await resp.json().catch(() => ({ status: resp.status }));
585
+ if (resp.status >= 400) throw new Error(`WAB action ${actionName} failed: ${resp.status}`);
586
+ } catch (fetchErr) {
587
+ clearTimeout(timeout);
588
+ throw fetchErr;
589
+ }
590
+
591
+ } else if (actionName === 'wait' || actionName === 'delay' || actionName === 'sleep') {
592
+ const ms = Math.min(step.waitMs || step.ms || step.value || 1000, 30000);
593
+ await new Promise(resolve => setTimeout(resolve, ms));
594
+ output = { waited: ms };
595
+
596
+ } else if (actionName === 'extract' || actionName === 'read') {
597
+ if (!baseUrl) throw new Error('Site has no domain configured');
598
+ const readUrl = `${baseUrl}/api/wab/read`;
599
+ const resp = await fetch(readUrl, {
600
+ method: 'POST',
601
+ headers: { 'Content-Type': 'application/json', 'User-Agent': 'WAB-Agent/1.0' },
602
+ body: JSON.stringify({ selector: step.selector, siteId: agent.site_id }),
603
+ });
604
+ output = await resp.json().catch(() => ({ status: resp.status }));
605
+
606
+ } else if (actionName === 'assert' || actionName === 'check') {
607
+ const actual = lastResponse;
608
+ const expected = step.expected || step.value;
609
+ if (expected && typeof actual === 'string' && !actual.includes(expected)) {
610
+ throw new Error(`Assertion failed: response does not contain "${expected}"`);
611
+ }
612
+ if (expected && typeof actual === 'object' && step.path) {
613
+ const val = step.path.split('.').reduce((o, k) => o && o[k], actual);
614
+ if (String(val) !== String(expected)) throw new Error(`Assertion failed: ${step.path} = ${val}, expected ${expected}`);
615
+ }
616
+ output = { assertion: 'passed', actual: typeof actual === 'string' ? actual.slice(0, 200) : actual };
617
+
618
+ } else if (actionName === 'log') {
619
+ output = { logged: step.value || step.message || '' };
620
+
621
+ } else {
622
+ if (baseUrl) {
623
+ const resp = await fetch(`${baseUrl}/api/wab/actions/${actionName}`, {
624
+ method: 'POST',
625
+ headers: { 'Content-Type': 'application/json', 'User-Agent': 'WAB-Agent/1.0' },
626
+ body: JSON.stringify({ ...step, siteId: agent.site_id }),
627
+ }).catch(e => ({ status: 0, statusText: e.message, json: () => Promise.resolve({ error: e.message }) }));
628
+ output = typeof resp.json === 'function' ? await resp.json().catch(() => ({})) : { status: resp.status };
629
+ } else {
630
+ output = { warning: `Unknown action "${actionName}" and no domain to send WAB command` };
631
+ }
632
+ }
633
+
634
+ const duration = Date.now() - stepStart;
635
+ results.push({ step: i, action: actionName, status: 'success', output, durationMs: duration });
636
+ } catch (err) {
637
+ const duration = Date.now() - stepStart;
638
+ results.push({ step: i, action: actionName, status: 'failed', error: err.message, durationMs: duration });
639
+ failed = true;
640
+ break;
641
+ }
642
+ }
643
+
644
+ const finalStatus = failed ? 'failed' : 'success';
645
+ db.prepare(
646
+ `UPDATE agent_runs SET status = ?, finished_at = datetime('now'), result_json = ? WHERE id = ?`
647
+ ).run(finalStatus, JSON.stringify({ steps: results }), runId);
648
+
649
+ db.prepare(
650
+ `UPDATE custom_agents SET run_count = run_count + 1, last_run = datetime('now') WHERE id = ?`
651
+ ).run(agentId);
652
+
653
+ if (agent.schedule) {
654
+ updateNextRun(agentId, agent.schedule);
655
+ }
656
+
657
+ return { id: runId, agentId, status: finalStatus, steps: results };
658
+ }
659
+
660
+ function getAgentRuns(agentId, { limit = 20 } = {}) {
661
+ return db.prepare(
662
+ `SELECT * FROM agent_runs WHERE agent_id = ? ORDER BY started_at DESC LIMIT ?`
663
+ ).all(agentId, limit);
664
+ }
665
+
666
+ function getScheduledAgents() {
667
+ return db.prepare(
668
+ `SELECT * FROM custom_agents WHERE schedule IS NOT NULL AND status = 'active' AND next_run <= datetime('now')`
669
+ ).all();
670
+ }
671
+
672
+ function updateNextRun(agentId, schedule) {
673
+ const next = parseSchedule(schedule);
674
+ db.prepare(
675
+ `UPDATE custom_agents SET next_run = ? WHERE id = ?`
676
+ ).run(next ? next.toISOString() : null, agentId);
677
+ }
678
+
679
+ // ═══════════════════════════════════════════════════════════════════════
680
+ // 5. Webhooks & CRM
681
+ // ═══════════════════════════════════════════════════════════════════════
682
+
683
+ function createWebhook(siteId, { name, url, events, secret }) {
684
+ const id = uuidv4();
685
+ const webhookSecret = secret || crypto.randomBytes(32).toString('hex');
686
+ db.prepare(
687
+ `INSERT INTO webhook_endpoints (id, site_id, name, url, events, secret) VALUES (?, ?, ?, ?, ?, ?)`
688
+ ).run(id, siteId, name || 'Webhook', url, JSON.stringify(events || ['*']), webhookSecret);
689
+ return { id, name: name || 'Webhook', url, secret: webhookSecret };
690
+ }
691
+
692
+ function updateWebhook(webhookId, siteId, updates) {
693
+ const fields = [];
694
+ const params = [];
695
+
696
+ if (updates.name !== undefined) { fields.push('name = ?'); params.push(updates.name); }
697
+ if (updates.url !== undefined) { fields.push('url = ?'); params.push(updates.url); }
698
+ if (updates.events !== undefined) { fields.push('events = ?'); params.push(JSON.stringify(updates.events)); }
699
+ if (updates.secret !== undefined) { fields.push('secret = ?'); params.push(updates.secret); }
700
+ if (updates.active !== undefined) { fields.push('active = ?'); params.push(updates.active ? 1 : 0); }
701
+
702
+ if (fields.length === 0) return false;
703
+ params.push(webhookId, siteId);
704
+ const result = db.prepare(
705
+ `UPDATE webhook_endpoints SET ${fields.join(', ')} WHERE id = ? AND site_id = ?`
706
+ ).run(...params);
707
+ return result.changes > 0;
708
+ }
709
+
710
+ function deleteWebhook(webhookId, siteId) {
711
+ const result = db.prepare(
712
+ `DELETE FROM webhook_endpoints WHERE id = ? AND site_id = ?`
713
+ ).run(webhookId, siteId);
714
+ return result.changes > 0;
715
+ }
716
+
717
+ function getWebhooks(siteId) {
718
+ return db.prepare(
719
+ `SELECT * FROM webhook_endpoints WHERE site_id = ? ORDER BY created_at DESC`
720
+ ).all(siteId);
721
+ }
722
+
723
+ function triggerWebhooks(siteId, eventType, payload) {
724
+ const webhooks = db.prepare(
725
+ `SELECT * FROM webhook_endpoints WHERE site_id = ? AND active = 1`
726
+ ).all(siteId);
727
+
728
+ const results = [];
729
+ for (const wh of webhooks) {
730
+ let events;
731
+ try { events = JSON.parse(wh.events); } catch { events = ['*']; }
732
+ if (!events.includes('*') && !events.includes(eventType)) continue;
733
+
734
+ const body = JSON.stringify({ event: eventType, payload, timestamp: new Date().toISOString() });
735
+
736
+ let signature = null;
737
+ if (wh.secret) {
738
+ signature = crypto.createHmac('sha256', wh.secret).update(body).digest('hex');
739
+ }
740
+
741
+ const promise = new Promise((resolve) => {
742
+ try {
743
+ const parsed = new URL(wh.url);
744
+ const mod = parsed.protocol === 'https:' ? https : http;
745
+ const req = mod.request(parsed, {
746
+ method: 'POST',
747
+ headers: {
748
+ 'Content-Type': 'application/json',
749
+ 'X-WAB-Signature': signature || '',
750
+ 'X-WAB-Event': eventType,
751
+ },
752
+ timeout: 10000,
753
+ }, (res) => {
754
+ let data = '';
755
+ res.on('data', (chunk) => { data += chunk; });
756
+ res.on('end', () => {
757
+ db.prepare(
758
+ `INSERT INTO webhook_logs (webhook_id, event_type, payload, response_code, response_body) VALUES (?, ?, ?, ?, ?)`
759
+ ).run(wh.id, eventType, body, res.statusCode, data.substring(0, 2000));
760
+
761
+ db.prepare(
762
+ `UPDATE webhook_endpoints SET last_triggered = datetime('now'), failure_count = CASE WHEN ? >= 400 THEN failure_count + 1 ELSE 0 END WHERE id = ?`
763
+ ).run(res.statusCode, wh.id);
764
+
765
+ resolve({ webhookId: wh.id, status: res.statusCode, success: res.statusCode < 400 });
766
+ });
767
+ });
768
+
769
+ req.on('error', (err) => {
770
+ db.prepare(
771
+ `INSERT INTO webhook_logs (webhook_id, event_type, payload, response_code, response_body) VALUES (?, ?, ?, ?, ?)`
772
+ ).run(wh.id, eventType, body, 0, err.message);
773
+
774
+ db.prepare(
775
+ `UPDATE webhook_endpoints SET last_triggered = datetime('now'), failure_count = failure_count + 1 WHERE id = ?`
776
+ ).run(wh.id);
777
+
778
+ resolve({ webhookId: wh.id, status: 0, success: false, error: err.message });
779
+ });
780
+
781
+ req.on('timeout', () => { req.destroy(); });
782
+ req.write(body);
783
+ req.end();
784
+ } catch (err) {
785
+ db.prepare(
786
+ `INSERT INTO webhook_logs (webhook_id, event_type, payload, response_code, response_body) VALUES (?, ?, ?, ?, ?)`
787
+ ).run(wh.id, eventType, body, 0, err.message);
788
+
789
+ resolve({ webhookId: wh.id, status: 0, success: false, error: err.message });
790
+ }
791
+ });
792
+
793
+ results.push(promise);
794
+ }
795
+
796
+ return Promise.allSettled(results).then((settled) =>
797
+ settled.map((r) => r.status === 'fulfilled' ? r.value : { success: false, error: r.reason })
798
+ );
799
+ }
800
+
801
+ function getWebhookLogs(webhookId, { limit = 50 } = {}) {
802
+ return db.prepare(
803
+ `SELECT * FROM webhook_logs WHERE webhook_id = ? ORDER BY created_at DESC LIMIT ?`
804
+ ).all(webhookId, limit);
805
+ }
806
+
807
+ function addCrmIntegration(siteId, { provider, config }) {
808
+ const id = uuidv4();
809
+ db.prepare(
810
+ `INSERT INTO crm_integrations (id, site_id, provider, config) VALUES (?, ?, ?, ?)`
811
+ ).run(id, siteId, provider, JSON.stringify(config || {}));
812
+ return { id, provider };
813
+ }
814
+
815
+ function updateCrmIntegration(integrationId, siteId, updates) {
816
+ const fields = [];
817
+ const params = [];
818
+
819
+ if (updates.provider !== undefined) { fields.push('provider = ?'); params.push(updates.provider); }
820
+ if (updates.config !== undefined) { fields.push('config = ?'); params.push(JSON.stringify(updates.config)); }
821
+ if (updates.active !== undefined) { fields.push('active = ?'); params.push(updates.active ? 1 : 0); }
822
+
823
+ if (fields.length === 0) return false;
824
+ params.push(integrationId, siteId);
825
+ const result = db.prepare(
826
+ `UPDATE crm_integrations SET ${fields.join(', ')} WHERE id = ? AND site_id = ?`
827
+ ).run(...params);
828
+ return result.changes > 0;
829
+ }
830
+
831
+ function deleteCrmIntegration(integrationId, siteId) {
832
+ const result = db.prepare(
833
+ `DELETE FROM crm_integrations WHERE id = ? AND site_id = ?`
834
+ ).run(integrationId, siteId);
835
+ return result.changes > 0;
836
+ }
837
+
838
+ function getCrmIntegrations(siteId) {
839
+ return db.prepare(
840
+ `SELECT * FROM crm_integrations WHERE site_id = ? ORDER BY created_at DESC`
841
+ ).all(siteId);
842
+ }
843
+
844
+ // ═══════════════════════════════════════════════════════════════════════
845
+ // 6. Multi-Tenant
846
+ // ═══════════════════════════════════════════════════════════════════════
847
+
848
+ function inviteSubUser(parentUserId, { email, name, password, role, siteAccess, quotaActionsMonth }) {
849
+ const id = uuidv4();
850
+ const hashed = bcrypt.hashSync(password, 12);
851
+ db.prepare(
852
+ `INSERT INTO sub_users (id, parent_user_id, email, password, name, role, site_access, quota_actions_month) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
853
+ ).run(id, parentUserId, email, hashed, name, role || 'viewer', JSON.stringify(siteAccess || ['*']), quotaActionsMonth || null);
854
+ return { id, email, name, role: role || 'viewer' };
855
+ }
856
+
857
+ function getSubUsers(parentUserId) {
858
+ const rows = db.prepare(
859
+ `SELECT id, parent_user_id, email, name, role, site_access, quota_actions_month, actions_used_month, invited_at, accepted_at, active FROM sub_users WHERE parent_user_id = ? ORDER BY invited_at DESC`
860
+ ).all(parentUserId);
861
+ return rows;
862
+ }
863
+
864
+ function updateSubUser(subUserId, parentUserId, updates) {
865
+ const fields = [];
866
+ const params = [];
867
+
868
+ if (updates.name !== undefined) { fields.push('name = ?'); params.push(updates.name); }
869
+ if (updates.email !== undefined) { fields.push('email = ?'); params.push(updates.email); }
870
+ if (updates.role !== undefined) { fields.push('role = ?'); params.push(updates.role); }
871
+ if (updates.siteAccess !== undefined) { fields.push('site_access = ?'); params.push(JSON.stringify(updates.siteAccess)); }
872
+ if (updates.quotaActionsMonth !== undefined) { fields.push('quota_actions_month = ?'); params.push(updates.quotaActionsMonth); }
873
+ if (updates.active !== undefined) { fields.push('active = ?'); params.push(updates.active ? 1 : 0); }
874
+ if (updates.password !== undefined) { fields.push('password = ?'); params.push(bcrypt.hashSync(updates.password, 12)); }
875
+
876
+ if (fields.length === 0) return false;
877
+ params.push(subUserId, parentUserId);
878
+ const result = db.prepare(
879
+ `UPDATE sub_users SET ${fields.join(', ')} WHERE id = ? AND parent_user_id = ?`
880
+ ).run(...params);
881
+ return result.changes > 0;
882
+ }
883
+
884
+ function deleteSubUser(subUserId, parentUserId) {
885
+ const result = db.prepare(
886
+ `DELETE FROM sub_users WHERE id = ? AND parent_user_id = ?`
887
+ ).run(subUserId, parentUserId);
888
+ return result.changes > 0;
889
+ }
890
+
891
+ function loginSubUser({ email, password }) {
892
+ const user = db.prepare(`SELECT * FROM sub_users WHERE email = ? AND active = 1`).get(email);
893
+ if (!user) return null;
894
+ if (!bcrypt.compareSync(password, user.password)) return null;
895
+ if (!user.accepted_at) {
896
+ db.prepare(`UPDATE sub_users SET accepted_at = datetime('now') WHERE id = ?`).run(user.id);
897
+ }
898
+ return { id: user.id, parentUserId: user.parent_user_id, email: user.email, name: user.name, role: user.role };
899
+ }
900
+
901
+ function checkSubUserAccess(subUserId, siteId) {
902
+ const user = db.prepare(
903
+ `SELECT site_access, active FROM sub_users WHERE id = ?`
904
+ ).get(subUserId);
905
+ if (!user || !user.active) return false;
906
+
907
+ let access;
908
+ try { access = JSON.parse(user.site_access); } catch { access = []; }
909
+ return access.includes('*') || access.includes(siteId);
910
+ }
911
+
912
+ function incrementSubUserUsage(subUserId) {
913
+ const user = db.prepare(
914
+ `SELECT quota_actions_month, actions_used_month FROM sub_users WHERE id = ? AND active = 1`
915
+ ).get(subUserId);
916
+ if (!user) return { allowed: false, remaining: 0 };
917
+
918
+ if (user.quota_actions_month !== null && user.actions_used_month >= user.quota_actions_month) {
919
+ return { allowed: false, remaining: 0 };
920
+ }
921
+
922
+ db.prepare(`UPDATE sub_users SET actions_used_month = actions_used_month + 1 WHERE id = ?`).run(subUserId);
923
+
924
+ const remaining = user.quota_actions_month !== null
925
+ ? user.quota_actions_month - user.actions_used_month - 1
926
+ : null;
927
+ return { allowed: true, remaining };
928
+ }
929
+
930
+ function resetMonthlyUsage() {
931
+ const result = db.prepare(`UPDATE sub_users SET actions_used_month = 0`).run();
932
+ return result.changes;
933
+ }
934
+
935
+ // ═══════════════════════════════════════════════════════════════════════
936
+ // 7. Support Tickets
937
+ // ═══════════════════════════════════════════════════════════════════════
938
+
939
+ const SLA_HOURS = { low: 72, normal: 48, high: 24, urgent: 4 };
940
+
941
+ function createTicket(userId, { subject, priority, category }) {
942
+ const id = uuidv4();
943
+ const prio = priority || 'normal';
944
+ const slaHours = SLA_HOURS[prio] || 48;
945
+ const slaDeadline = new Date(Date.now() + slaHours * 3600000).toISOString();
946
+
947
+ db.prepare(
948
+ `INSERT INTO support_tickets (id, user_id, subject, priority, category, sla_deadline) VALUES (?, ?, ?, ?, ?, ?)`
949
+ ).run(id, userId, subject, prio, category || null, slaDeadline);
950
+
951
+ db.prepare(
952
+ `INSERT INTO ticket_messages (ticket_id, sender_type, sender_id, message) VALUES (?, 'bot', 'system', ?)`
953
+ ).run(id, `Ticket created. Priority: ${prio}. We'll get back to you within ${slaHours} hours.`);
954
+
955
+ return { id, subject, priority: prio, slaDeadline };
956
+ }
957
+
958
+ function getTickets(userId, { status, limit = 50 } = {}) {
959
+ if (status) {
960
+ return db.prepare(
961
+ `SELECT t.*, (SELECT COUNT(*) FROM ticket_messages WHERE ticket_id = t.id) as message_count FROM support_tickets t WHERE t.user_id = ? AND t.status = ? ORDER BY t.updated_at DESC LIMIT ?`
962
+ ).all(userId, status, limit);
963
+ }
964
+ return db.prepare(
965
+ `SELECT t.*, (SELECT COUNT(*) FROM ticket_messages WHERE ticket_id = t.id) as message_count FROM support_tickets t WHERE t.user_id = ? ORDER BY t.updated_at DESC LIMIT ?`
966
+ ).all(userId, limit);
967
+ }
968
+
969
+ function getTicket(ticketId, userId) {
970
+ return db.prepare(
971
+ `SELECT t.*, (SELECT COUNT(*) FROM ticket_messages WHERE ticket_id = t.id) as message_count FROM support_tickets t WHERE t.id = ? AND t.user_id = ?`
972
+ ).get(ticketId, userId) || null;
973
+ }
974
+
975
+ function updateTicketStatus(ticketId, userId, status) {
976
+ const fields = ['status = ?', 'updated_at = datetime(\'now\')'];
977
+ const params = [status];
978
+
979
+ if (status === 'resolved' || status === 'closed') {
980
+ fields.push('resolved_at = datetime(\'now\')');
981
+ }
982
+
983
+ params.push(ticketId, userId);
984
+ const result = db.prepare(
985
+ `UPDATE support_tickets SET ${fields.join(', ')} WHERE id = ? AND user_id = ?`
986
+ ).run(...params);
987
+ return result.changes > 0;
988
+ }
989
+
990
+ function addTicketMessage(ticketId, { senderType, senderId, message }) {
991
+ const result = db.prepare(
992
+ `INSERT INTO ticket_messages (ticket_id, sender_type, sender_id, message) VALUES (?, ?, ?, ?)`
993
+ ).run(ticketId, senderType, senderId || null, message);
994
+
995
+ db.prepare(
996
+ `UPDATE support_tickets SET updated_at = datetime('now') WHERE id = ?`
997
+ ).run(ticketId);
998
+
999
+ return { id: result.lastInsertRowid };
1000
+ }
1001
+
1002
+ function getTicketMessages(ticketId) {
1003
+ return db.prepare(
1004
+ `SELECT * FROM ticket_messages WHERE ticket_id = ? ORDER BY created_at ASC`
1005
+ ).all(ticketId);
1006
+ }
1007
+
1008
+ function getTicketStats(userId) {
1009
+ const rows = db.prepare(
1010
+ `SELECT status, COUNT(*) as count FROM support_tickets WHERE user_id = ? GROUP BY status`
1011
+ ).all(userId);
1012
+ const stats = { open: 0, in_progress: 0, waiting: 0, resolved: 0, closed: 0 };
1013
+ for (const r of rows) stats[r.status] = r.count;
1014
+ stats.total = Object.values(stats).reduce((a, b) => a + b, 0);
1015
+ return stats;
1016
+ }
1017
+
1018
+ const FAQ_ENTRIES = [
1019
+ { keywords: ['install', 'setup', 'getting started', 'start'], response: 'To get started, add the bridge script to your website by including `<script src="your-cdn-url/ai-agent-bridge.js"></script>` before the closing </body> tag. Then configure your license key in the dashboard.' },
1020
+ { keywords: ['license', 'key', 'activate'], response: 'You can find your license key in the Dashboard under Site Settings. Copy the key and add it to your bridge script configuration: `WebAgentBridge.init({ licenseKey: "YOUR-KEY" })`.' },
1021
+ { keywords: ['price', 'pricing', 'cost', 'plan', 'tier', 'upgrade'], response: 'We offer Free, Starter ($19/mo), Pro ($49/mo), and Enterprise ($149/mo) plans. Visit the Billing section in your dashboard to upgrade. Each tier unlocks additional features and higher rate limits.' },
1022
+ { keywords: ['rate limit', 'throttle', 'too many requests', '429'], response: 'Rate limits depend on your plan: Free (60/min), Starter (200/min), Pro (1000/min), Enterprise (unlimited). If you\'re hitting limits, consider upgrading or optimizing your agent\'s request frequency.' },
1023
+ { keywords: ['block', 'bot', 'agent', 'security'], response: 'You can block specific agents in the Exploit Shield section. Go to Security → Blocked Agents and add the agent signature you want to block. You can also enable auto-detection to automatically block suspicious agents.' },
1024
+ { keywords: ['webhook', 'notification', 'callback'], response: 'Set up webhooks in the Webhooks section. Add a URL endpoint, select the events you want to listen to, and we\'ll send POST requests with signed payloads whenever those events occur.' },
1025
+ { keywords: ['api', 'endpoint', 'rest', 'integration'], response: 'Our REST API is available at your dashboard URL under /api/v1/. Authenticate with your API key in the X-API-Key header. Full API documentation is available in the docs section of your dashboard.' },
1026
+ { keywords: ['cancel', 'refund', 'subscription'], response: 'You can cancel your subscription at any time from the Billing section. Your features will remain active until the end of the current billing period. For refund requests, please specify the reason and our team will review it.' },
1027
+ { keywords: ['custom', 'script', 'bridge', 'modify'], response: 'Use the Custom Bridge Script feature to customize your bridge script. You can add plugins, custom CSS/JS, enable AMP compatibility, and toggle minification. Changes are built and deployed automatically.' },
1028
+ { keywords: ['analytics', 'traffic', 'stats', 'report'], response: 'Analytics are available in the Traffic Intelligence section. You can view agent profiles, traffic statistics by type/country/platform, and set up anomaly alerts for unusual traffic patterns.' },
1029
+ { keywords: ['stealth', 'detection', 'anti-bot'], response: 'Stealth Mode lets you configure human-like behavior patterns for your agents including typing speed, mouse movement, scroll behavior, and click delays. This helps agents pass anti-bot detection systems.' },
1030
+ { keywords: ['sandbox', 'test', 'benchmark'], response: 'The Sandbox feature lets you create isolated test environments. You can simulate traffic, run benchmarks (rate limiting, response time, throughput), and compare before/after results without affecting production.' },
1031
+ { keywords: ['password', 'login', 'account', 'reset'], response: 'To reset your password, use the "Forgot Password" link on the login page. If you\'re having trouble accessing your account, contact support with your registered email address.' },
1032
+ { keywords: ['cdn', 'domain', 'ssl', 'cache'], response: 'CDN settings are available in the CDN section. You can configure a custom domain, manage SSL, set cache TTL, and choose edge locations. CDN stats show bandwidth usage, requests, and cache hit rates.' },
1033
+ { keywords: ['audit', 'compliance', 'gdpr', 'hipaa', 'log'], response: 'The Audit & Compliance section provides detailed action logs, GDPR/HIPAA/SOC2 compliance modes, configurable retention policies, and export capabilities (CSV/JSON). Enable auto-purge to automatically clean old logs.' },
1034
+ ];
1035
+
1036
+ function generateBotResponse(message) {
1037
+ if (!message) return null;
1038
+ const lower = message.toLowerCase();
1039
+
1040
+ for (const faq of FAQ_ENTRIES) {
1041
+ for (const keyword of faq.keywords) {
1042
+ if (lower.includes(keyword)) {
1043
+ return faq.response;
1044
+ }
1045
+ }
1046
+ }
1047
+
1048
+ return null;
1049
+ }
1050
+
1051
+ // ═══════════════════════════════════════════════════════════════════════
1052
+ // 8. Custom Bridge Script
1053
+ // ═══════════════════════════════════════════════════════════════════════
1054
+
1055
+ function getScriptConfig(siteId) {
1056
+ return db.prepare(
1057
+ `SELECT * FROM custom_scripts WHERE site_id = ?`
1058
+ ).get(siteId) || null;
1059
+ }
1060
+
1061
+ function updateScriptConfig(siteId, { plugins, minified, ampCompatible, autoPatch, customCss, customJs }) {
1062
+ const existing = db.prepare(`SELECT id FROM custom_scripts WHERE site_id = ?`).get(siteId);
1063
+
1064
+ if (existing) {
1065
+ const fields = [];
1066
+ const params = [];
1067
+ if (plugins !== undefined) { fields.push('plugins_json = ?'); params.push(JSON.stringify(plugins)); }
1068
+ if (minified !== undefined) { fields.push('minified = ?'); params.push(minified ? 1 : 0); }
1069
+ if (ampCompatible !== undefined) { fields.push('amp_compatible = ?'); params.push(ampCompatible ? 1 : 0); }
1070
+ if (autoPatch !== undefined) { fields.push('auto_patch = ?'); params.push(autoPatch ? 1 : 0); }
1071
+ if (customCss !== undefined) { fields.push('custom_css = ?'); params.push(customCss); }
1072
+ if (customJs !== undefined) { fields.push('custom_js = ?'); params.push(customJs); }
1073
+
1074
+ if (fields.length === 0) return existing.id;
1075
+ params.push(siteId);
1076
+ db.prepare(`UPDATE custom_scripts SET ${fields.join(', ')} WHERE site_id = ?`).run(...params);
1077
+ return existing.id;
1078
+ }
1079
+
1080
+ const id = uuidv4();
1081
+ db.prepare(
1082
+ `INSERT INTO custom_scripts (id, site_id, plugins_json, minified, amp_compatible, auto_patch, custom_css, custom_js) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
1083
+ ).run(
1084
+ id, siteId,
1085
+ JSON.stringify(plugins || []),
1086
+ minified !== undefined ? (minified ? 1 : 0) : 1,
1087
+ ampCompatible ? 1 : 0,
1088
+ autoPatch !== undefined ? (autoPatch ? 1 : 0) : 1,
1089
+ customCss || null,
1090
+ customJs || null
1091
+ );
1092
+ return id;
1093
+ }
1094
+
1095
+ const AVAILABLE_PLUGINS = [
1096
+ { id: 'consent-guard', name: 'Consent Guard', description: 'Require user consent before agent actions', code: `if(!window.__wabConsent){window.WebAgentBridge&&WebAgentBridge.on("beforeAction",function(e){if(!window.__wabConsent){e.preventDefault();console.warn("WAB: action blocked – user consent required")}});}` },
1097
+ { id: 'session-recorder', name: 'Session Recorder', description: 'Record agent interaction sessions for replay', code: `(function(){var s=[];window.WebAgentBridge&&WebAgentBridge.on("action",function(e){s.push({t:Date.now(),a:e.action,d:e.detail})});window.__wabSessionLog=s})();` },
1098
+ { id: 'dark-mode', name: 'Dark Mode Support', description: 'Automatically adapt agent overlay UI to dark mode', code: `(function(){if(window.matchMedia&&window.matchMedia("(prefers-color-scheme:dark)").matches){document.documentElement.classList.add("wab-dark")}})();` },
1099
+ { id: 'error-tracker', name: 'Error Tracker', description: 'Capture and report agent runtime errors', code: `window.addEventListener("error",function(e){if(e.filename&&e.filename.indexOf("agent-bridge")>-1){console.error("WAB Error:",e.message);window.WebAgentBridge&&WebAgentBridge.emit("error",{msg:e.message,line:e.lineno})}});` },
1100
+ { id: 'geo-restrict', name: 'Geo Restriction', description: 'Restrict agent access based on geolocation', code: `(function(){window.WebAgentBridge&&WebAgentBridge.on("init",function(){fetch("https://ipapi.co/json/").then(function(r){return r.json()}).then(function(d){window.__wabGeo=d.country_code})})})();` },
1101
+ { id: 'rate-limiter-ui', name: 'Rate Limiter UI', description: 'Show visual feedback when rate limit is approaching', code: `(function(){var c=0;window.WebAgentBridge&&WebAgentBridge.on("action",function(){c++;if(c>50){var el=document.getElementById("wab-rate-warn");if(!el){el=document.createElement("div");el.id="wab-rate-warn";el.style.cssText="position:fixed;top:0;left:0;right:0;background:#f59e0b;color:#000;padding:8px;text-align:center;z-index:99999";el.textContent="Rate limit approaching";document.body.appendChild(el)}}});setInterval(function(){c=0;var el=document.getElementById("wab-rate-warn");if(el)el.remove()},60000)})();` },
1102
+ ];
1103
+
1104
+ function buildScript(siteId) {
1105
+ const scriptPath = path.join(__dirname, '..', '..', 'script', 'ai-agent-bridge.js');
1106
+ let baseScript;
1107
+ try {
1108
+ baseScript = fs.readFileSync(scriptPath, 'utf8');
1109
+ } catch {
1110
+ return { error: 'Base script not found' };
1111
+ }
1112
+
1113
+ const config = db.prepare(`SELECT * FROM custom_scripts WHERE site_id = ?`).get(siteId);
1114
+ if (!config) return { script: baseScript, hash: crypto.createHash('md5').update(baseScript).digest('hex') };
1115
+
1116
+ let script = baseScript;
1117
+ let plugins = [];
1118
+ try { plugins = JSON.parse(config.plugins_json); } catch { /* use empty */ }
1119
+
1120
+ const pluginCode = plugins
1121
+ .map((pid) => {
1122
+ const plug = AVAILABLE_PLUGINS.find((p) => p.id === pid);
1123
+ return plug ? `\n/* Plugin: ${plug.name} */\n${plug.code}` : '';
1124
+ })
1125
+ .filter(Boolean)
1126
+ .join('\n');
1127
+
1128
+ if (pluginCode) {
1129
+ const insertPoint = script.lastIndexOf('}(');
1130
+ if (insertPoint > -1) {
1131
+ script = script.substring(0, insertPoint) + pluginCode + '\n' + script.substring(insertPoint);
1132
+ } else {
1133
+ script += '\n' + pluginCode;
1134
+ }
1135
+ }
1136
+
1137
+ if (config.custom_js) {
1138
+ script += `\n/* Custom JS */\n${config.custom_js}\n`;
1139
+ }
1140
+
1141
+ if (config.custom_css) {
1142
+ const cssInjector = `\n(function(){var s=document.createElement("style");s.textContent=${JSON.stringify(config.custom_css)};document.head.appendChild(s)})();\n`;
1143
+ script += cssInjector;
1144
+ }
1145
+
1146
+ if (config.amp_compatible) {
1147
+ script = script.replace(/document\.write\(/g, '/* AMP: disabled */ // document.write(');
1148
+ script = `/* AMP Compatible */\n${script}`;
1149
+ }
1150
+
1151
+ if (config.minified) {
1152
+ script = script
1153
+ .replace(/\/\*[\s\S]*?\*\//g, '')
1154
+ .replace(/\/\/[^\n]*/g, '')
1155
+ .replace(/\n\s*\n/g, '\n')
1156
+ .replace(/^\s+/gm, '')
1157
+ .split('\n')
1158
+ .filter((l) => l.trim())
1159
+ .join('\n');
1160
+ }
1161
+
1162
+ const hash = crypto.createHash('md5').update(script).digest('hex');
1163
+
1164
+ db.prepare(
1165
+ `UPDATE custom_scripts SET last_built = datetime('now'), script_hash = ? WHERE site_id = ?`
1166
+ ).run(hash, siteId);
1167
+
1168
+ return { script, hash, size: Buffer.byteLength(script, 'utf8') };
1169
+ }
1170
+
1171
+ function getAvailablePlugins() {
1172
+ return AVAILABLE_PLUGINS.map(({ id, name, description }) => ({ id, name, description }));
1173
+ }
1174
+
1175
+ // ═══════════════════════════════════════════════════════════════════════
1176
+ // 9. Stealth Mode
1177
+ // ═══════════════════════════════════════════════════════════════════════
1178
+
1179
+ function getStealthProfile(siteId) {
1180
+ return db.prepare(
1181
+ `SELECT * FROM stealth_profiles WHERE site_id = ? AND active = 1 ORDER BY created_at DESC LIMIT 1`
1182
+ ).get(siteId) || null;
1183
+ }
1184
+
1185
+ function upsertStealthProfile(siteId, profile) {
1186
+ const existing = db.prepare(`SELECT id FROM stealth_profiles WHERE site_id = ? AND active = 1`).get(siteId);
1187
+
1188
+ if (existing) {
1189
+ const fields = [];
1190
+ const params = [];
1191
+
1192
+ if (profile.name !== undefined) { fields.push('name = ?'); params.push(profile.name); }
1193
+ if (profile.typingSpeedMin !== undefined) { fields.push('typing_speed_min = ?'); params.push(profile.typingSpeedMin); }
1194
+ if (profile.typingSpeedMax !== undefined) { fields.push('typing_speed_max = ?'); params.push(profile.typingSpeedMax); }
1195
+ if (profile.mouseSpeed !== undefined) { fields.push('mouse_speed = ?'); params.push(profile.mouseSpeed); }
1196
+ if (profile.scrollBehavior !== undefined) { fields.push('scroll_behavior = ?'); params.push(profile.scrollBehavior); }
1197
+ if (profile.clickDelayMin !== undefined) { fields.push('click_delay_min = ?'); params.push(profile.clickDelayMin); }
1198
+ if (profile.clickDelayMax !== undefined) { fields.push('click_delay_max = ?'); params.push(profile.clickDelayMax); }
1199
+ if (profile.antiDetection !== undefined) { fields.push('anti_detection_json = ?'); params.push(JSON.stringify(profile.antiDetection)); }
1200
+
1201
+ if (fields.length > 0) {
1202
+ params.push(existing.id);
1203
+ db.prepare(`UPDATE stealth_profiles SET ${fields.join(', ')} WHERE id = ?`).run(...params);
1204
+ }
1205
+ return existing.id;
1206
+ }
1207
+
1208
+ const id = uuidv4();
1209
+ db.prepare(
1210
+ `INSERT INTO stealth_profiles (id, site_id, name, typing_speed_min, typing_speed_max, mouse_speed, scroll_behavior, click_delay_min, click_delay_max, anti_detection_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1211
+ ).run(
1212
+ id, siteId,
1213
+ profile.name || 'Default',
1214
+ profile.typingSpeedMin || 30,
1215
+ profile.typingSpeedMax || 120,
1216
+ profile.mouseSpeed || 'natural',
1217
+ profile.scrollBehavior || 'eased',
1218
+ profile.clickDelayMin || 50,
1219
+ profile.clickDelayMax || 400,
1220
+ JSON.stringify(profile.antiDetection || {})
1221
+ );
1222
+ return id;
1223
+ }
1224
+
1225
+ function getAntiDetectionConfig(siteId) {
1226
+ const profile = db.prepare(
1227
+ `SELECT anti_detection_json FROM stealth_profiles WHERE site_id = ? AND active = 1 ORDER BY created_at DESC LIMIT 1`
1228
+ ).get(siteId);
1229
+ if (!profile) return {};
1230
+ try { return JSON.parse(profile.anti_detection_json); } catch { return {}; }
1231
+ }
1232
+
1233
+ function generateStealthScript(siteId) {
1234
+ const profile = getStealthProfile(siteId);
1235
+ if (!profile) return '/* No stealth profile configured */';
1236
+
1237
+ let antiDetection;
1238
+ try { antiDetection = JSON.parse(profile.anti_detection_json || '{}'); } catch { antiDetection = {}; }
1239
+
1240
+ return `(function(){
1241
+ 'use strict';
1242
+ var cfg = {
1243
+ typingSpeedMin: ${profile.typing_speed_min},
1244
+ typingSpeedMax: ${profile.typing_speed_max},
1245
+ mouseSpeed: '${profile.mouse_speed}',
1246
+ scrollBehavior: '${profile.scroll_behavior}',
1247
+ clickDelayMin: ${profile.click_delay_min},
1248
+ clickDelayMax: ${profile.click_delay_max}
1249
+ };
1250
+
1251
+ function rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
1252
+
1253
+ function humanType(el, text, cb) {
1254
+ var i = 0;
1255
+ function next() {
1256
+ if (i >= text.length) { if (cb) cb(); return; }
1257
+ var delay = rand(cfg.typingSpeedMin, cfg.typingSpeedMax);
1258
+ if (Math.random() < 0.05) delay += rand(200, 600);
1259
+ setTimeout(function() {
1260
+ el.value = (el.value || '') + text[i];
1261
+ el.dispatchEvent(new Event('input', { bubbles: true }));
1262
+ i++;
1263
+ next();
1264
+ }, delay);
1265
+ }
1266
+ next();
1267
+ }
1268
+
1269
+ function humanClick(el, cb) {
1270
+ var delay = rand(cfg.clickDelayMin, cfg.clickDelayMax);
1271
+ var rect = el.getBoundingClientRect();
1272
+ var x = rect.left + rand(2, rect.width - 2);
1273
+ var y = rect.top + rand(2, rect.height - 2);
1274
+ el.dispatchEvent(new MouseEvent('mousemove', { clientX: x, clientY: y, bubbles: true }));
1275
+ setTimeout(function() {
1276
+ el.dispatchEvent(new MouseEvent('mousedown', { clientX: x, clientY: y, bubbles: true }));
1277
+ setTimeout(function() {
1278
+ el.dispatchEvent(new MouseEvent('mouseup', { clientX: x, clientY: y, bubbles: true }));
1279
+ el.dispatchEvent(new MouseEvent('click', { clientX: x, clientY: y, bubbles: true }));
1280
+ if (cb) cb();
1281
+ }, rand(30, 80));
1282
+ }, delay);
1283
+ }
1284
+
1285
+ function humanScroll(target, cb) {
1286
+ var current = window.scrollY;
1287
+ var distance = target - current;
1288
+ var steps = Math.abs(distance) < 200 ? 10 : 30;
1289
+ var step = 0;
1290
+ var ease = cfg.scrollBehavior === 'eased';
1291
+ function tick() {
1292
+ if (step >= steps) { if (cb) cb(); return; }
1293
+ step++;
1294
+ var t = step / steps;
1295
+ var pos = ease ? current + distance * (t * t * (3 - 2 * t)) : current + distance * t;
1296
+ window.scrollTo(0, pos);
1297
+ requestAnimationFrame(tick);
1298
+ }
1299
+ tick();
1300
+ }
1301
+
1302
+ ${antiDetection.hideWebdriver ? 'Object.defineProperty(navigator, "webdriver", { get: function() { return false; } });' : ''}
1303
+ ${antiDetection.spoofPlugins ? 'Object.defineProperty(navigator, "plugins", { get: function() { return [1,2,3]; } });' : ''}
1304
+ ${antiDetection.spoofLanguages ? 'Object.defineProperty(navigator, "languages", { get: function() { return ["en-US","en"]; } });' : ''}
1305
+ ${antiDetection.mockPermissions ? 'if(navigator.permissions){var origQuery=navigator.permissions.query.bind(navigator.permissions);navigator.permissions.query=function(p){if(p.name==="notifications")return Promise.resolve({state:"prompt"});return origQuery(p)};}' : ''}
1306
+
1307
+ window.__wabStealth = { humanType: humanType, humanClick: humanClick, humanScroll: humanScroll, config: cfg };
1308
+ })();`;
1309
+ }
1310
+
1311
+ // ═══════════════════════════════════════════════════════════════════════
1312
+ // 10. CDN
1313
+ // ═══════════════════════════════════════════════════════════════════════
1314
+
1315
+ function getCdnConfig(siteId) {
1316
+ return db.prepare(`SELECT * FROM cdn_configs WHERE site_id = ?`).get(siteId) || null;
1317
+ }
1318
+
1319
+ function upsertCdnConfig(siteId, config) {
1320
+ const existing = db.prepare(`SELECT id FROM cdn_configs WHERE site_id = ?`).get(siteId);
1321
+
1322
+ if (existing) {
1323
+ const fields = [];
1324
+ const params = [];
1325
+
1326
+ if (config.customDomain !== undefined) { fields.push('custom_domain = ?'); params.push(config.customDomain); }
1327
+ if (config.sslStatus !== undefined) { fields.push('ssl_status = ?'); params.push(config.sslStatus); }
1328
+ if (config.edgeLocations !== undefined) { fields.push('edge_locations = ?'); params.push(JSON.stringify(config.edgeLocations)); }
1329
+ if (config.cacheTtl !== undefined) { fields.push('cache_ttl = ?'); params.push(config.cacheTtl); }
1330
+ if (config.active !== undefined) { fields.push('active = ?'); params.push(config.active ? 1 : 0); }
1331
+
1332
+ if (fields.length > 0) {
1333
+ params.push(existing.id);
1334
+ db.prepare(`UPDATE cdn_configs SET ${fields.join(', ')} WHERE id = ?`).run(...params);
1335
+ }
1336
+ return existing.id;
1337
+ }
1338
+
1339
+ const id = uuidv4();
1340
+ db.prepare(
1341
+ `INSERT INTO cdn_configs (id, site_id, custom_domain, ssl_status, edge_locations, cache_ttl) VALUES (?, ?, ?, ?, ?, ?)`
1342
+ ).run(
1343
+ id, siteId,
1344
+ config.customDomain || null,
1345
+ config.sslStatus || 'pending',
1346
+ JSON.stringify(config.edgeLocations || ['us-east', 'eu-west']),
1347
+ config.cacheTtl || 86400
1348
+ );
1349
+ return id;
1350
+ }
1351
+
1352
+ function recordCdnHit(cdnId, region, bandwidth) {
1353
+ const today = new Date().toISOString().split('T')[0];
1354
+ const existing = db.prepare(
1355
+ `SELECT id, requests, bandwidth as bw, cache_hits FROM cdn_stats WHERE cdn_id = ? AND region = ? AND date(recorded_at) = ?`
1356
+ ).get(cdnId, region, today);
1357
+
1358
+ if (existing) {
1359
+ db.prepare(
1360
+ `UPDATE cdn_stats SET requests = requests + 1, bandwidth = bandwidth + ?, cache_hits = cache_hits + 1 WHERE id = ?`
1361
+ ).run(bandwidth || 0, existing.id);
1362
+ } else {
1363
+ db.prepare(
1364
+ `INSERT INTO cdn_stats (cdn_id, region, requests, bandwidth, cache_hits) VALUES (?, ?, 1, ?, 1)`
1365
+ ).run(cdnId, region, bandwidth || 0);
1366
+ }
1367
+
1368
+ db.prepare(
1369
+ `UPDATE cdn_configs SET requests_count = requests_count + 1, bandwidth_used = bandwidth_used + ? WHERE id = ?`
1370
+ ).run(bandwidth || 0, cdnId);
1371
+ }
1372
+
1373
+ function getCdnStats(cdnId, days = 30) {
1374
+ const since = new Date(Date.now() - days * 86400000).toISOString();
1375
+
1376
+ const daily = db.prepare(
1377
+ `SELECT date(recorded_at) as day, SUM(requests) as requests, SUM(bandwidth) as bandwidth, SUM(cache_hits) as cache_hits FROM cdn_stats WHERE cdn_id = ? AND recorded_at >= ? GROUP BY day ORDER BY day`
1378
+ ).all(cdnId, since);
1379
+
1380
+ const byRegion = db.prepare(
1381
+ `SELECT region, SUM(requests) as requests, SUM(bandwidth) as bandwidth, AVG(avg_latency_ms) as avg_latency FROM cdn_stats WHERE cdn_id = ? AND recorded_at >= ? GROUP BY region ORDER BY requests DESC`
1382
+ ).all(cdnId, since);
1383
+
1384
+ const totals = db.prepare(
1385
+ `SELECT COALESCE(SUM(requests), 0) as requests, COALESCE(SUM(bandwidth), 0) as bandwidth, COALESCE(SUM(cache_hits), 0) as cache_hits FROM cdn_stats WHERE cdn_id = ? AND recorded_at >= ?`
1386
+ ).get(cdnId, since);
1387
+
1388
+ return { daily, byRegion, totals };
1389
+ }
1390
+
1391
+ function generateCdnUrl(siteId, customDomain) {
1392
+ if (customDomain) {
1393
+ return `https://${customDomain}/bridge/${siteId}/ai-agent-bridge.js`;
1394
+ }
1395
+ return `https://cdn.webagentbridge.com/bridge/${siteId}/ai-agent-bridge.js`;
1396
+ }
1397
+
1398
+ // ═══════════════════════════════════════════════════════════════════════
1399
+ // 11. Audit & Compliance
1400
+ // ═══════════════════════════════════════════════════════════════════════
1401
+
1402
+ function logAudit(siteId, { userId, action, resourceType, resourceId, details, ipAddress, userAgent }) {
1403
+ const result = db.prepare(
1404
+ `INSERT INTO audit_logs (site_id, user_id, action, resource_type, resource_id, details, ip_address, user_agent) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
1405
+ ).run(siteId, userId || null, action, resourceType || null, resourceId || null, JSON.stringify(details || {}), ipAddress || null, userAgent || null);
1406
+ return { id: result.lastInsertRowid };
1407
+ }
1408
+
1409
+ function getAuditLogs(siteId, { limit = 50, offset = 0, action, since, until } = {}) {
1410
+ const conditions = ['site_id = ?'];
1411
+ const params = [siteId];
1412
+
1413
+ if (action) {
1414
+ conditions.push('action = ?');
1415
+ params.push(action);
1416
+ }
1417
+ if (since) {
1418
+ conditions.push('created_at >= ?');
1419
+ params.push(since);
1420
+ }
1421
+ if (until) {
1422
+ conditions.push('created_at <= ?');
1423
+ params.push(until);
1424
+ }
1425
+
1426
+ const countResult = db.prepare(
1427
+ `SELECT COUNT(*) as total FROM audit_logs WHERE ${conditions.join(' AND ')}`
1428
+ ).get(...params);
1429
+
1430
+ const rows = db.prepare(
1431
+ `SELECT * FROM audit_logs WHERE ${conditions.join(' AND ')} ORDER BY created_at DESC LIMIT ? OFFSET ?`
1432
+ ).all(...params, limit, offset);
1433
+
1434
+ return { rows, total: countResult.total, limit, offset };
1435
+ }
1436
+
1437
+ function getComplianceSettings(siteId) {
1438
+ return db.prepare(`SELECT * FROM compliance_settings WHERE site_id = ?`).get(siteId) || null;
1439
+ }
1440
+
1441
+ function upsertComplianceSettings(siteId, settings) {
1442
+ const existing = db.prepare(`SELECT id FROM compliance_settings WHERE site_id = ?`).get(siteId);
1443
+
1444
+ if (existing) {
1445
+ const fields = [];
1446
+ const params = [];
1447
+
1448
+ if (settings.retentionDays !== undefined) { fields.push('retention_days = ?'); params.push(settings.retentionDays); }
1449
+ if (settings.hipaaMode !== undefined) { fields.push('hipaa_mode = ?'); params.push(settings.hipaaMode ? 1 : 0); }
1450
+ if (settings.gdprMode !== undefined) { fields.push('gdpr_mode = ?'); params.push(settings.gdprMode ? 1 : 0); }
1451
+ if (settings.soc2Mode !== undefined) { fields.push('soc2_mode = ?'); params.push(settings.soc2Mode ? 1 : 0); }
1452
+ if (settings.autoPurge !== undefined) { fields.push('auto_purge = ?'); params.push(settings.autoPurge ? 1 : 0); }
1453
+
1454
+ if (fields.length > 0) {
1455
+ params.push(existing.id);
1456
+ db.prepare(`UPDATE compliance_settings SET ${fields.join(', ')} WHERE id = ?`).run(...params);
1457
+ }
1458
+ return existing.id;
1459
+ }
1460
+
1461
+ const id = uuidv4();
1462
+ db.prepare(
1463
+ `INSERT INTO compliance_settings (id, site_id, retention_days, hipaa_mode, gdpr_mode, soc2_mode, auto_purge) VALUES (?, ?, ?, ?, ?, ?, ?)`
1464
+ ).run(
1465
+ id, siteId,
1466
+ settings.retentionDays || 90,
1467
+ settings.hipaaMode ? 1 : 0,
1468
+ settings.gdprMode ? 1 : 0,
1469
+ settings.soc2Mode ? 1 : 0,
1470
+ settings.autoPurge !== undefined ? (settings.autoPurge ? 1 : 0) : 1
1471
+ );
1472
+ return id;
1473
+ }
1474
+
1475
+ function exportAuditLogs(siteId, { format = 'json', since, until } = {}) {
1476
+ const conditions = ['site_id = ?'];
1477
+ const params = [siteId];
1478
+
1479
+ if (since) { conditions.push('created_at >= ?'); params.push(since); }
1480
+ if (until) { conditions.push('created_at <= ?'); params.push(until); }
1481
+
1482
+ const rows = db.prepare(
1483
+ `SELECT * FROM audit_logs WHERE ${conditions.join(' AND ')} ORDER BY created_at DESC`
1484
+ ).all(...params);
1485
+
1486
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1487
+
1488
+ if (format === 'csv') {
1489
+ const headers = ['id', 'site_id', 'user_id', 'action', 'resource_type', 'resource_id', 'details', 'ip_address', 'user_agent', 'created_at'];
1490
+ const csvLines = [headers.join(',')];
1491
+ for (const row of rows) {
1492
+ const line = headers.map((h) => {
1493
+ const val = row[h] != null ? String(row[h]) : '';
1494
+ return val.includes(',') || val.includes('"') || val.includes('\n')
1495
+ ? `"${val.replace(/"/g, '""')}"`
1496
+ : val;
1497
+ });
1498
+ csvLines.push(line.join(','));
1499
+ }
1500
+ return { data: csvLines.join('\n'), filename: `audit-logs-${timestamp}.csv`, contentType: 'text/csv' };
1501
+ }
1502
+
1503
+ return { data: JSON.stringify(rows, null, 2), filename: `audit-logs-${timestamp}.json`, contentType: 'application/json' };
1504
+ }
1505
+
1506
+ function purgeOldLogs(siteId) {
1507
+ const settings = db.prepare(`SELECT retention_days, auto_purge FROM compliance_settings WHERE site_id = ?`).get(siteId);
1508
+ const retentionDays = settings ? settings.retention_days : 90;
1509
+
1510
+ if (settings && !settings.auto_purge) return { deleted: 0, skipped: true };
1511
+
1512
+ const result = db.prepare(
1513
+ `DELETE FROM audit_logs WHERE site_id = ? AND created_at < datetime('now', ? || ' days')`
1514
+ ).run(siteId, `-${retentionDays}`);
1515
+
1516
+ return { deleted: result.changes, retentionDays };
1517
+ }
1518
+
1519
+ // ═══════════════════════════════════════════════════════════════════════
1520
+ // 12. Sandbox
1521
+ // ═══════════════════════════════════════════════════════════════════════
1522
+
1523
+ function createSandbox(siteId, { name } = {}) {
1524
+ const site = db.prepare(`SELECT config, tier, domain FROM sites WHERE id = ?`).get(siteId);
1525
+ const snapshot = site ? JSON.stringify({ config: site.config, tier: site.tier, domain: site.domain, snapshotAt: new Date().toISOString() }) : '{}';
1526
+
1527
+ const id = uuidv4();
1528
+ db.prepare(
1529
+ `INSERT INTO sandbox_environments (id, site_id, name, config_snapshot) VALUES (?, ?, ?, ?)`
1530
+ ).run(id, siteId, name || 'Default Sandbox', snapshot);
1531
+ return { id, name: name || 'Default Sandbox', config_snapshot: snapshot };
1532
+ }
1533
+
1534
+ function getSandboxes(siteId) {
1535
+ return db.prepare(
1536
+ `SELECT * FROM sandbox_environments WHERE site_id = ? ORDER BY created_at DESC`
1537
+ ).all(siteId);
1538
+ }
1539
+
1540
+ function deleteSandbox(sandboxId, siteId) {
1541
+ const result = db.prepare(
1542
+ `DELETE FROM sandbox_environments WHERE id = ? AND site_id = ?`
1543
+ ).run(sandboxId, siteId);
1544
+ return result.changes > 0;
1545
+ }
1546
+
1547
+ async function simulateTraffic(sandboxId, { agentCount = 10, duration = 60, actionsPerAgent = 5 } = {}) {
1548
+ const sandbox = db.prepare(`SELECT * FROM sandbox_environments WHERE id = ?`).get(sandboxId);
1549
+ if (!sandbox) return null;
1550
+
1551
+ const site = db.prepare(`SELECT domain, config FROM sites WHERE id = ?`).get(sandbox.site_id);
1552
+ const baseUrl = site && site.domain ? (site.domain.startsWith('http') ? site.domain : `https://${site.domain}`) : 'http://localhost:3003';
1553
+
1554
+ const wabEndpoints = [
1555
+ { path: '/api/wab/ping', method: 'GET', type: 'friendly' },
1556
+ { path: '/api/wab/discover', method: 'POST', type: 'friendly' },
1557
+ { path: '/api/wab/page-info', method: 'POST', type: 'friendly' },
1558
+ { path: '/api/wab/read', method: 'POST', type: 'aggressive' },
1559
+ { path: '/api/wab/actions', method: 'GET', type: 'friendly' },
1560
+ { path: '/', method: 'GET', type: 'friendly' },
1561
+ ];
1562
+ const userAgents = [
1563
+ 'WAB-Agent/1.0 (Google)', 'WAB-Agent/1.0 (OpenAI)', 'WAB-Agent/1.0 (Anthropic)',
1564
+ 'Python-urllib/3.11', 'curl/8.0', 'Mozilla/5.0 (WAB Test)',
1565
+ ];
1566
+
1567
+ let totalActions = 0;
1568
+ let successCount = 0;
1569
+ let failCount = 0;
1570
+ const typeDist = {};
1571
+ const responseTimes = [];
1572
+ const statusCodes = {};
1573
+
1574
+ const cappedAgents = Math.min(agentCount, 50);
1575
+ const cappedActions = Math.min(actionsPerAgent, 10);
1576
+
1577
+ for (let i = 0; i < cappedAgents; i++) {
1578
+ const endpoint = wabEndpoints[i % wabEndpoints.length];
1579
+ const ua = userAgents[i % userAgents.length];
1580
+ typeDist[endpoint.type] = (typeDist[endpoint.type] || 0) + 1;
1581
+
1582
+ for (let j = 0; j < cappedActions; j++) {
1583
+ totalActions++;
1584
+ const start = Date.now();
1585
+ try {
1586
+ const controller = new AbortController();
1587
+ const timeout = setTimeout(() => controller.abort(), 5000);
1588
+ const resp = await fetch(`${baseUrl}${endpoint.path}`, {
1589
+ method: endpoint.method,
1590
+ headers: { 'User-Agent': ua, 'Content-Type': 'application/json' },
1591
+ body: endpoint.method === 'POST' ? JSON.stringify({ siteId: sandbox.site_id }) : undefined,
1592
+ signal: controller.signal,
1593
+ });
1594
+ clearTimeout(timeout);
1595
+ const elapsed = Date.now() - start;
1596
+ responseTimes.push(elapsed);
1597
+ statusCodes[resp.status] = (statusCodes[resp.status] || 0) + 1;
1598
+ if (resp.status < 400) successCount++;
1599
+ else failCount++;
1600
+ } catch (_) {
1601
+ failCount++;
1602
+ responseTimes.push(Date.now() - start);
1603
+ }
1604
+ }
1605
+ }
1606
+
1607
+ db.prepare(
1608
+ `UPDATE sandbox_environments SET traffic_generated = traffic_generated + ? WHERE id = ?`
1609
+ ).run(totalActions, sandboxId);
1610
+
1611
+ const avgResponseTime = responseTimes.length > 0
1612
+ ? Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length)
1613
+ : 0;
1614
+ const maxResponseTime = responseTimes.length > 0 ? Math.max(...responseTimes) : 0;
1615
+ const minResponseTime = responseTimes.length > 0 ? Math.min(...responseTimes) : 0;
1616
+
1617
+ return {
1618
+ sandboxId,
1619
+ agentCount: cappedAgents,
1620
+ totalActions,
1621
+ duration,
1622
+ typeDist,
1623
+ successCount,
1624
+ failCount,
1625
+ successRate: totalActions > 0 ? Math.round((successCount / totalActions) * 10000) / 100 : 0,
1626
+ avgResponseTimeMs: avgResponseTime,
1627
+ minResponseTimeMs: minResponseTime,
1628
+ maxResponseTimeMs: maxResponseTime,
1629
+ statusCodes,
1630
+ avgActionsPerAgent: cappedAgents > 0 ? (totalActions / cappedAgents).toFixed(2) : '0',
1631
+ simulatedAt: new Date().toISOString(),
1632
+ };
1633
+ }
1634
+
1635
+ async function runBenchmark(sandboxId, { benchmarkType } = {}) {
1636
+ const sandbox = db.prepare(`SELECT * FROM sandbox_environments WHERE id = ?`).get(sandboxId);
1637
+ if (!sandbox) return null;
1638
+
1639
+ const site = db.prepare(`SELECT domain FROM sites WHERE id = ?`).get(sandbox.site_id);
1640
+ const baseUrl = site && site.domain ? (site.domain.startsWith('http') ? site.domain : `https://${site.domain}`) : 'http://localhost:3003';
1641
+
1642
+ let beforeValue, afterValue;
1643
+ const type = benchmarkType || 'response_time';
1644
+
1645
+ const measureResponseTime = async (url, count) => {
1646
+ const times = [];
1647
+ for (let i = 0; i < count; i++) {
1648
+ const start = Date.now();
1649
+ try {
1650
+ const ctrl = new AbortController();
1651
+ const t = setTimeout(() => ctrl.abort(), 5000);
1652
+ await fetch(url, { signal: ctrl.signal, headers: { 'User-Agent': 'WAB-Benchmark/1.0' } });
1653
+ clearTimeout(t);
1654
+ times.push(Date.now() - start);
1655
+ } catch (_) {
1656
+ times.push(Date.now() - start);
1657
+ }
1658
+ }
1659
+ return times.length > 0 ? times.reduce((a, b) => a + b, 0) / times.length : 0;
1660
+ };
1661
+
1662
+ const measureThroughput = async (url, durationMs) => {
1663
+ let count = 0;
1664
+ const end = Date.now() + durationMs;
1665
+ while (Date.now() < end) {
1666
+ try {
1667
+ const ctrl = new AbortController();
1668
+ const t = setTimeout(() => ctrl.abort(), 2000);
1669
+ await fetch(url, { signal: ctrl.signal, headers: { 'User-Agent': 'WAB-Benchmark/1.0' } });
1670
+ clearTimeout(t);
1671
+ } catch (_) { /* count it anyway */ }
1672
+ count++;
1673
+ }
1674
+ return Math.round(count / (durationMs / 1000));
1675
+ };
1676
+
1677
+ switch (type) {
1678
+ case 'rate_limit': {
1679
+ const previous = db.prepare(
1680
+ `SELECT after_value FROM sandbox_benchmarks WHERE sandbox_id = ? AND benchmark_type = 'rate_limit' ORDER BY recorded_at DESC LIMIT 1`
1681
+ ).get(sandboxId);
1682
+ beforeValue = previous ? previous.after_value : 60;
1683
+ let accepted = 0;
1684
+ const burstCount = 100;
1685
+ const promises = [];
1686
+ for (let i = 0; i < burstCount; i++) {
1687
+ promises.push(
1688
+ fetch(`${baseUrl}/api/wab/ping`, { headers: { 'User-Agent': 'WAB-Benchmark/1.0' } })
1689
+ .then(r => { if (r.status < 429) accepted++; })
1690
+ .catch(() => {})
1691
+ );
1692
+ }
1693
+ await Promise.allSettled(promises);
1694
+ afterValue = accepted;
1695
+ break;
1696
+ }
1697
+ case 'response_time': {
1698
+ beforeValue = await measureResponseTime(`${baseUrl}/`, 5);
1699
+ beforeValue = Math.round(beforeValue * 100) / 100;
1700
+ afterValue = await measureResponseTime(`${baseUrl}/api/wab/ping`, 10);
1701
+ afterValue = Math.round(afterValue * 100) / 100;
1702
+ break;
1703
+ }
1704
+ case 'throughput': {
1705
+ beforeValue = await measureThroughput(`${baseUrl}/`, 3000);
1706
+ afterValue = await measureThroughput(`${baseUrl}/api/wab/ping`, 3000);
1707
+ break;
1708
+ }
1709
+ default: {
1710
+ beforeValue = await measureResponseTime(`${baseUrl}/`, 3);
1711
+ beforeValue = Math.round(beforeValue * 100) / 100;
1712
+ afterValue = Math.random() * 100;
1713
+ }
1714
+ }
1715
+
1716
+ const result = db.prepare(
1717
+ `INSERT INTO sandbox_benchmarks (sandbox_id, benchmark_type, before_value, after_value) VALUES (?, ?, ?, ?)`
1718
+ ).run(sandboxId, type, beforeValue, afterValue);
1719
+
1720
+ return {
1721
+ id: result.lastInsertRowid,
1722
+ sandboxId,
1723
+ benchmarkType: type,
1724
+ beforeValue: Math.round(beforeValue * 100) / 100,
1725
+ afterValue: Math.round(afterValue * 100) / 100,
1726
+ improvement: Math.round(((beforeValue - afterValue) / beforeValue) * 10000) / 100,
1727
+ unit: type === 'rate_limit' ? 'req/min' : type === 'response_time' ? 'ms' : 'req/s',
1728
+ recordedAt: new Date().toISOString(),
1729
+ };
1730
+ }
1731
+
1732
+ function getBenchmarks(sandboxId) {
1733
+ return db.prepare(
1734
+ `SELECT * FROM sandbox_benchmarks WHERE sandbox_id = ? ORDER BY recorded_at DESC`
1735
+ ).all(sandboxId);
1736
+ }
1737
+
1738
+ function compareBenchmarks(sandboxId) {
1739
+ const types = ['rate_limit', 'response_time', 'throughput'];
1740
+ const comparison = {};
1741
+
1742
+ for (const type of types) {
1743
+ const rows = db.prepare(
1744
+ `SELECT * FROM sandbox_benchmarks WHERE sandbox_id = ? AND benchmark_type = ? ORDER BY recorded_at DESC LIMIT 2`
1745
+ ).all(sandboxId, type);
1746
+
1747
+ if (rows.length >= 2) {
1748
+ const latest = rows[0];
1749
+ const previous = rows[1];
1750
+ comparison[type] = {
1751
+ latest: { before: latest.before_value, after: latest.after_value, recordedAt: latest.recorded_at },
1752
+ previous: { before: previous.before_value, after: previous.after_value, recordedAt: previous.recorded_at },
1753
+ delta: latest.after_value - previous.after_value,
1754
+ improved: type === 'throughput'
1755
+ ? latest.after_value > previous.after_value
1756
+ : latest.after_value < previous.after_value,
1757
+ };
1758
+ } else if (rows.length === 1) {
1759
+ comparison[type] = {
1760
+ latest: { before: rows[0].before_value, after: rows[0].after_value, recordedAt: rows[0].recorded_at },
1761
+ previous: null,
1762
+ delta: null,
1763
+ improved: null,
1764
+ };
1765
+ }
1766
+ }
1767
+
1768
+ return comparison;
1769
+ }
1770
+
1771
+ // ═══════════════════════════════════════════════════════════════════════
1772
+ // Exports
1773
+ // ═══════════════════════════════════════════════════════════════════════
1774
+
1775
+ module.exports = {
1776
+ // 1. Agent Traffic Intelligence
1777
+ parseUserAgent,
1778
+ hashIP,
1779
+ recordAgentVisit,
1780
+ getAgentProfiles,
1781
+ getAnomalyAlerts,
1782
+ checkForAnomalies,
1783
+ acknowledgeAlert,
1784
+ getTrafficStats,
1785
+
1786
+ // 2. Advanced Exploit Shield
1787
+ logSecurityEvent,
1788
+ getSecurityEvents,
1789
+ blockAgent,
1790
+ unblockAgent,
1791
+ isAgentBlocked,
1792
+ getBlockedAgents,
1793
+ getSecurityReport,
1794
+ autoDetectThreats,
1795
+
1796
+ // 3. Smart Actions Library
1797
+ getActionPacks,
1798
+ getActionPack,
1799
+ installPack,
1800
+ uninstallPack,
1801
+ getInstalledPacks,
1802
+ getPackActions,
1803
+
1804
+ // 4. Custom AI Agents
1805
+ createAgent,
1806
+ updateAgent,
1807
+ deleteAgent,
1808
+ getAgents,
1809
+ getAgent,
1810
+ runAgent,
1811
+ getAgentRuns,
1812
+ getScheduledAgents,
1813
+ updateNextRun,
1814
+ parseSchedule,
1815
+
1816
+ // 5. Webhooks & CRM
1817
+ createWebhook,
1818
+ updateWebhook,
1819
+ deleteWebhook,
1820
+ getWebhooks,
1821
+ triggerWebhooks,
1822
+ getWebhookLogs,
1823
+ addCrmIntegration,
1824
+ updateCrmIntegration,
1825
+ deleteCrmIntegration,
1826
+ getCrmIntegrations,
1827
+
1828
+ // 6. Multi-Tenant
1829
+ inviteSubUser,
1830
+ getSubUsers,
1831
+ updateSubUser,
1832
+ deleteSubUser,
1833
+ loginSubUser,
1834
+ checkSubUserAccess,
1835
+ incrementSubUserUsage,
1836
+ resetMonthlyUsage,
1837
+
1838
+ // 7. Support Tickets
1839
+ createTicket,
1840
+ getTickets,
1841
+ getTicket,
1842
+ updateTicketStatus,
1843
+ addTicketMessage,
1844
+ getTicketMessages,
1845
+ getTicketStats,
1846
+ generateBotResponse,
1847
+
1848
+ // 8. Custom Bridge Script
1849
+ getScriptConfig,
1850
+ updateScriptConfig,
1851
+ buildScript,
1852
+ getAvailablePlugins,
1853
+
1854
+ // 9. Stealth Mode
1855
+ getStealthProfile,
1856
+ upsertStealthProfile,
1857
+ getAntiDetectionConfig,
1858
+ generateStealthScript,
1859
+
1860
+ // 10. CDN
1861
+ getCdnConfig,
1862
+ upsertCdnConfig,
1863
+ recordCdnHit,
1864
+ getCdnStats,
1865
+ generateCdnUrl,
1866
+
1867
+ // 11. Audit & Compliance
1868
+ logAudit,
1869
+ getAuditLogs,
1870
+ getComplianceSettings,
1871
+ upsertComplianceSettings,
1872
+ exportAuditLogs,
1873
+ purgeOldLogs,
1874
+
1875
+ // 12. Sandbox
1876
+ createSandbox,
1877
+ getSandboxes,
1878
+ deleteSandbox,
1879
+ simulateTraffic,
1880
+ runBenchmark,
1881
+ getBenchmarks,
1882
+ compareBenchmarks,
1883
+ };