web-agent-bridge 3.0.0 → 3.2.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 (51) hide show
  1. package/LICENSE +51 -0
  2. package/README.ar.md +79 -0
  3. package/README.md +104 -4
  4. package/package.json +2 -1
  5. package/public/.well-known/ai-plugin.json +28 -0
  6. package/public/agent-workspace.html +3 -1
  7. package/public/ai.html +5 -3
  8. package/public/api.html +412 -0
  9. package/public/browser.html +4 -2
  10. package/public/cookies.html +4 -2
  11. package/public/dashboard.html +5 -3
  12. package/public/demo.html +1770 -1
  13. package/public/docs.html +6 -4
  14. package/public/growth.html +463 -0
  15. package/public/index.html +982 -738
  16. package/public/llms-full.txt +52 -1
  17. package/public/llms.txt +39 -0
  18. package/public/login.html +6 -4
  19. package/public/premium-dashboard.html +7 -5
  20. package/public/premium.html +6 -4
  21. package/public/privacy.html +4 -2
  22. package/public/register.html +6 -4
  23. package/public/score.html +263 -0
  24. package/public/terms.html +4 -2
  25. package/sdk/index.js +7 -1
  26. package/sdk/package.json +12 -1
  27. package/server/index.js +427 -375
  28. package/server/middleware/rateLimits.js +3 -3
  29. package/server/migrations/006_growth_suite.sql +138 -0
  30. package/server/routes/agent-workspace.js +162 -0
  31. package/server/routes/demo-showcase.js +332 -0
  32. package/server/routes/discovery.js +18 -7
  33. package/server/routes/gateway.js +157 -0
  34. package/server/routes/growth.js +962 -0
  35. package/server/routes/universal.js +9 -1
  36. package/server/routes/wab-api.js +16 -6
  37. package/server/services/api-key-engine.js +261 -0
  38. package/server/services/lfd.js +22 -3
  39. package/server/services/modules/affiliate-intelligence.js +93 -0
  40. package/server/services/modules/agent-firewall.js +90 -0
  41. package/server/services/modules/bounty.js +89 -0
  42. package/server/services/modules/collective-bargaining.js +92 -0
  43. package/server/services/modules/dark-pattern.js +66 -0
  44. package/server/services/modules/gov-intelligence.js +45 -0
  45. package/server/services/modules/neural.js +55 -0
  46. package/server/services/modules/notary.js +49 -0
  47. package/server/services/modules/price-time-machine.js +86 -0
  48. package/server/services/modules/protocol.js +104 -0
  49. package/server/services/premium.js +1 -1
  50. package/server/services/price-intelligence.js +2 -1
  51. package/server/services/vision.js +2 -2
package/server/index.js CHANGED
@@ -1,375 +1,427 @@
1
- require('dotenv').config();
2
-
3
- const { assertSecretsAtStartup } = require('./config/secrets');
4
- assertSecretsAtStartup();
5
-
6
- const express = require('express');
7
- const http = require('http');
8
- const cors = require('cors');
9
- const helmet = require('helmet');
10
- const rateLimit = require('express-rate-limit');
11
- const path = require('path');
12
- const { setupWebSocket } = require('./ws');
13
- const { runMigrations } = require('./utils/migrate');
14
- const { maybeBootstrapAdmin, db } = require('./models/db');
15
- const { initSearchEngine, search, getSuggestions, getTrendingSearches, getSearchStats, purgeOldCache } = require('./services/search-engine');
16
- const { processMessage: agentChat } = require('./services/agent-chat');
17
- const agentTasks = require('./services/agent-tasks');
18
- const { cluster } = require('./services/cluster');
19
-
20
- const authRoutes = require('./routes/auth');
21
- const apiRoutes = require('./routes/api');
22
- const licenseRoutes = require('./routes/license');
23
- const adminRoutes = require('./routes/admin');
24
- const billingRoutes = require('./routes/billing');
25
- const sovereignRoutes = require('./routes/sovereign');
26
- const meshRoutes = require('./routes/mesh');
27
- const commanderRoutes = require('./routes/commander');
28
- const adsRoutes = require('./routes/ads');
29
- const wabApiRoutes = require('./routes/wab-api');
30
- const noscriptRoutes = require('./routes/noscript');
31
- const discoveryRoutes = require('./routes/discovery');
32
- const premiumRoutes = require('./routes/premium');
33
- const adminPremiumRoutes = require('./routes/admin-premium');
34
- const workspaceRoutes = require('./routes/agent-workspace');
35
- const universalRoutes = require('./routes/universal');
36
- const runtimeRoutes = require('./routes/runtime');
37
- const { handleWebhookRequest } = require('./services/stripe');
38
- const { runtime } = require('./runtime');
39
-
40
- const app = express();
41
- const PORT = process.env.PORT || 3000;
42
-
43
- app.set('trust proxy', 1);
44
-
45
- const corsOrigins = (process.env.ALLOWED_ORIGINS
46
- || 'http://localhost:3000,http://127.0.0.1:3000,http://localhost:5173')
47
- .split(',')
48
- .map((s) => s.trim())
49
- .filter(Boolean);
50
-
51
- app.use(
52
- cors({
53
- origin(origin, callback) {
54
- if (!origin) return callback(null, true);
55
- if (corsOrigins.includes(origin)) return callback(null, true);
56
- if (process.env.NODE_ENV !== 'production' && /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i.test(origin)) {
57
- return callback(null, true);
58
- }
59
- return callback(null, false);
60
- },
61
- credentials: true
62
- })
63
- );
64
-
65
- const scriptSrc = process.env.CSP_ALLOW_UNSAFE_INLINE === 'false'
66
- ? ["'self'"]
67
- : ["'self'", "'unsafe-inline'"];
68
- const styleSrc = process.env.CSP_ALLOW_UNSAFE_INLINE === 'false'
69
- ? ["'self'"]
70
- : ["'self'", "'unsafe-inline'"];
71
-
72
- app.use(
73
- helmet({
74
- contentSecurityPolicy: {
75
- directives: {
76
- defaultSrc: ["'self'"],
77
- scriptSrc,
78
- scriptSrcAttr: scriptSrc,
79
- styleSrc: [...styleSrc, 'https://fonts.googleapis.com'],
80
- imgSrc: ["'self'", 'data:', 'https:'],
81
- connectSrc: ["'self'", 'ws:', 'wss:'],
82
- fontSrc: ["'self'", 'https://fonts.gstatic.com', 'https:', 'data:'],
83
- frameSrc: ["'self'", 'https:', 'http:'],
84
- frameAncestors: ["'none'"],
85
- objectSrc: ["'none'"],
86
- baseUri: ["'self'"],
87
- formAction: ["'self'"]
88
- }
89
- },
90
- crossOriginEmbedderPolicy: false
91
- })
92
- );
93
-
94
- app.post('/api/billing/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
95
- try {
96
- await handleWebhookRequest(req);
97
- res.json({ received: true });
98
- } catch (err) {
99
- console.error('Webhook error:', err.message);
100
- res.status(400).json({ error: err.message });
101
- }
102
- });
103
-
104
- app.use(express.json());
105
-
106
- const apiLimiter = rateLimit({
107
- windowMs: 15 * 60 * 1000,
108
- max: 200,
109
- standardHeaders: true,
110
- legacyHeaders: false,
111
- message: { error: 'Too many requests, please try again later' }
112
- });
113
-
114
- const licenseLimiter = rateLimit({
115
- windowMs: 60 * 1000,
116
- max: 120,
117
- standardHeaders: true,
118
- legacyHeaders: false,
119
- keyGenerator: (req) => {
120
- const key = req.body?.licenseKey || req.body?.siteId || req.ip;
121
- return `${req.ip}:${key}`;
122
- }
123
- });
124
-
125
- app.use(express.static(path.join(__dirname, '..', 'public')));
126
- app.use('/script', express.static(path.join(__dirname, '..', 'script')));
127
-
128
- app.use('/api/auth', apiLimiter, authRoutes);
129
- app.use('/api', apiLimiter, apiRoutes);
130
- app.use('/api/license', licenseLimiter, licenseRoutes);
131
- app.use('/api/admin', apiLimiter, adminRoutes);
132
- app.use('/api/billing', apiLimiter, billingRoutes);
133
- app.use('/api/sovereign', apiLimiter, sovereignRoutes);
134
- app.use('/api/mesh', apiLimiter, meshRoutes);
135
- app.use('/api/commander', apiLimiter, commanderRoutes);
136
- app.use('/api/ads', apiLimiter, adsRoutes);
137
- app.use('/api/wab', wabApiRoutes);
138
- app.use('/api/noscript', apiLimiter, noscriptRoutes);
139
- app.use('/api/discovery', apiLimiter, discoveryRoutes);
140
- app.use('/api/premium', apiLimiter, premiumRoutes);
141
- app.use('/api/admin/premium', apiLimiter, adminPremiumRoutes);
142
- app.use('/api/workspace', apiLimiter, workspaceRoutes);
143
- app.use('/api/universal', apiLimiter, universalRoutes);
144
- app.use('/api/os', apiLimiter, runtimeRoutes);
145
-
146
- // ─── WAB Search Engine ────────────────────────────────────────────────
147
-
148
- const searchLimiter = rateLimit({
149
- windowMs: 60 * 1000,
150
- max: 30,
151
- standardHeaders: true,
152
- legacyHeaders: false,
153
- message: { error: 'Too many search requests, please slow down' }
154
- });
155
-
156
- app.get('/api/search', searchLimiter, async (req, res) => {
157
- const q = (req.query.q || '').trim();
158
- if (!q) return res.json({ results: [], cached: false });
159
- if (q.length > 200) return res.status(400).json({ error: 'Query too long' });
160
- const crypto = require('crypto');
161
- const ipHash = crypto.createHash('sha256').update(req.ip || '').digest('hex').slice(0, 16);
162
- const result = await search(q, ipHash);
163
- res.json(result);
164
- });
165
-
166
- app.get('/api/search/suggest', searchLimiter, (req, res) => {
167
- const q = (req.query.q || '').trim();
168
- if (!q) return res.json({ suggestions: [] });
169
- const suggestions = getSuggestions(q, 8);
170
- res.json({ suggestions });
171
- });
172
-
173
- app.get('/api/search/trending', apiLimiter, (req, res) => {
174
- const trending = getTrendingSearches(10);
175
- res.json({ trending });
176
- });
177
-
178
- app.get('/api/search/stats', apiLimiter, (req, res) => {
179
- const stats = getSearchStats();
180
- res.json(stats);
181
- });
182
-
183
- app.get('/dashboard', (req, res) => {
184
- res.sendFile(path.join(__dirname, '..', 'public', 'dashboard.html'));
185
- });
186
- app.get('/mesh-dashboard', (req, res) => {
187
- res.sendFile(path.join(__dirname, '..', 'public', 'mesh-dashboard.html'));
188
- });
189
- app.get('/commander-dashboard', (req, res) => {
190
- res.sendFile(path.join(__dirname, '..', 'public', 'commander-dashboard.html'));
191
- });
192
- app.get('/docs', (req, res) => {
193
- res.sendFile(path.join(__dirname, '..', 'public', 'docs.html'));
194
- });
195
- app.get('/login', (req, res) => {
196
- res.sendFile(path.join(__dirname, '..', 'public', 'login.html'));
197
- });
198
- app.get('/register', (req, res) => {
199
- res.sendFile(path.join(__dirname, '..', 'public', 'register.html'));
200
- });
201
- app.get('/admin/login', (req, res) => {
202
- res.sendFile(path.join(__dirname, '..', 'public', 'admin', 'login.html'));
203
- });
204
- app.get('/admin', (req, res) => {
205
- res.sendFile(path.join(__dirname, '..', 'public', 'admin', 'dashboard.html'));
206
- });
207
- app.get('/privacy', (req, res) => {
208
- res.sendFile(path.join(__dirname, '..', 'public', 'privacy.html'));
209
- });
210
- app.get('/terms', (req, res) => {
211
- res.sendFile(path.join(__dirname, '..', 'public', 'terms.html'));
212
- });
213
- app.get('/cookies', (req, res) => {
214
- res.sendFile(path.join(__dirname, '..', 'public', 'cookies.html'));
215
- });
216
- app.get('/browser', (req, res) => {
217
- res.sendFile(path.join(__dirname, '..', 'public', 'browser.html'));
218
- });
219
- app.get('/workspace', (req, res) => {
220
- res.sendFile(path.join(__dirname, '..', 'public', 'agent-workspace.html'));
221
- });
222
-
223
- // Browser downloads
224
- app.use('/downloads', express.static(path.join(__dirname, '..', 'downloads'), {
225
- maxAge: '1d',
226
- setHeaders: (res, filePath) => {
227
- res.set('Content-Disposition', 'attachment');
228
- }
229
- }));
230
-
231
- // Agent chat endpoint for WAB Browser — Real AI Agent
232
- const chatLimiter = rateLimit({
233
- windowMs: 60 * 1000,
234
- max: 20,
235
- standardHeaders: true,
236
- legacyHeaders: false,
237
- message: { error: 'Too many messages, please slow down' }
238
- });
239
-
240
- app.post('/api/wab/agent-chat', chatLimiter, async (req, res) => {
241
- const { message, context, sessionId, taskId, taskAction } = req.body || {};
242
- if (!message || typeof message !== 'string') {
243
- return res.status(400).json({ error: 'Message required' });
244
- }
245
- if (message.length > 3000) {
246
- return res.status(400).json({ error: 'Message too long' });
247
- }
248
-
249
- const sid = sessionId || req.ip || 'anonymous';
250
-
251
- try {
252
- // ── Task actions (user responding to an active task) ──
253
- if (taskId && taskAction) {
254
- if (taskAction === 'answer') {
255
- const result = agentTasks.answerClarification(taskId, message);
256
- if (result.status === 'planning') {
257
- // Auto-execute after planning
258
- const execResult = await agentTasks.executeTask(taskId);
259
- return res.json({ ...execResult, type: 'task' });
260
- }
261
- return res.json({ ...result, type: 'task' });
262
- }
263
- if (taskAction === 'select') {
264
- const idx = parseInt(message.replace(/\D/g, '')) - 1;
265
- const result = agentTasks.selectOffer(taskId, idx);
266
- return res.json({ ...result, type: 'task' });
267
- }
268
- if (taskAction === 'cancel') {
269
- const result = agentTasks.cancelTask(taskId);
270
- return res.json({ ...result, type: 'task' });
271
- }
272
- }
273
-
274
- // ── Check if user wants to select from existing offers ──
275
- if (!taskId) {
276
- const selectMatch = message.match(/(?:اختر|اخت(?:ا|ي)ر|select|choose|pick)\s*(\d+)/i);
277
- if (selectMatch) {
278
- const tasks = agentTasks.getSessionTasks(sid, 1);
279
- if (tasks.length > 0 && tasks[0].status === 'presenting') {
280
- const idx = parseInt(selectMatch[1]) - 1;
281
- const result = agentTasks.selectOffer(tasks[0].id, idx);
282
- return res.json({ ...result, type: 'task' });
283
- }
284
- }
285
- }
286
-
287
- // ── Detect URL paste — create URL negotiation task ──
288
- const urlData = agentTasks.parseBookingUrl(message);
289
- if (urlData) {
290
- const task = agentTasks.createUrlTask(sid, message, urlData);
291
- const execResult = await agentTasks.executeUrlTask(task.taskId);
292
- return res.json({ ...execResult, type: 'task', urlData });
293
- }
294
-
295
- // ── Detect if this is a task-type request (booking, shopping, etc.) ──
296
- const intent = agentTasks.detectIntent(message);
297
- if (intent.confidence >= 0.7 && intent.intent !== 'general') {
298
- const task = agentTasks.createTask(sid, message);
299
-
300
- if (task.status === 'clarifying') {
301
- return res.json({ ...task, type: 'task' });
302
- }
303
-
304
- // If requirements are complete, auto-execute
305
- const execResult = await agentTasks.executeTask(task.taskId);
306
- return res.json({ ...execResult, type: 'task' });
307
- }
308
-
309
- // ── Regular chat (not a task) ──
310
- const chatContext = {
311
- url: context?.url || '',
312
- platform: context?.platform || 'unknown',
313
- sessionId: sid,
314
- };
315
- const result = await agentChat(message, chatContext);
316
- res.json(result);
317
- } catch (err) {
318
- console.error('[agent-chat] Error:', err.message);
319
- res.json({ reply: '🤖 عذراً، حدث خطأ. حاول مرة أخرى.', type: 'text' });
320
- }
321
- });
322
-
323
- // Agent task status & history
324
- app.get('/api/wab/agent-task/:id', chatLimiter, (req, res) => {
325
- const state = agentTasks.getTaskState(req.params.id);
326
- if (!state) return res.status(404).json({ error: 'Task not found' });
327
- res.json(state);
328
- });
329
-
330
- app.get('/api/wab/agent-tasks', chatLimiter, (req, res) => {
331
- const sid = req.query.sessionId || req.ip || 'anonymous';
332
- const tasks = agentTasks.getSessionTasks(sid, 20);
333
- res.json({ tasks });
334
- });
335
-
336
- const pkg = require('../package.json');
337
- app.use(`/v${pkg.version.split('.')[0]}`, express.static(path.join(__dirname, '..', 'script')));
338
- app.use('/latest', express.static(path.join(__dirname, '..', 'script')));
339
-
340
- app.get('*', (req, res) => {
341
- if (req.accepts('html')) {
342
- res.sendFile(path.join(__dirname, '..', 'public', 'index.html'));
343
- } else {
344
- res.status(404).json({ error: 'Not found' });
345
- }
346
- });
347
-
348
- if (process.env.NODE_ENV !== 'test') {
349
- console.log('Running database migrations...');
350
- runMigrations();
351
- maybeBootstrapAdmin();
352
- initSearchEngine(db);
353
-
354
- // Purge old search cache every hour
355
- setInterval(purgeOldCache, 60 * 60 * 1000);
356
-
357
- const server = http.createServer(app);
358
- setupWebSocket(server);
359
-
360
- // Start Agent OS runtime
361
- runtime.start();
362
-
363
- // Start Cluster Orchestrator
364
- cluster.start();
365
-
366
- server.listen(PORT, () => {
367
- console.log(`\n ╔══════════════════════════════════════════╗`);
368
- console.log(` ║ Web Agent Bridge v${pkg.version} ║`);
369
- console.log(` ║ Server running on http://localhost:${PORT} ║`);
370
- console.log(` ║ WebSocket: ws://localhost:${PORT}/ws/analytics ║`);
371
- console.log(` ╚══════════════════════════════════════════╝\n`);
372
- });
373
- }
374
-
375
- module.exports = app;
1
+ require('dotenv').config();
2
+
3
+ const { assertSecretsAtStartup } = require('./config/secrets');
4
+ assertSecretsAtStartup();
5
+
6
+ const express = require('express');
7
+ const http = require('http');
8
+ const cors = require('cors');
9
+ const helmet = require('helmet');
10
+ const rateLimit = require('express-rate-limit');
11
+ const path = require('path');
12
+ const { setupWebSocket } = require('./ws');
13
+ const { runMigrations } = require('./utils/migrate');
14
+ const { maybeBootstrapAdmin, db } = require('./models/db');
15
+ const { initSearchEngine, search, getSuggestions, getTrendingSearches, getSearchStats, purgeOldCache } = require('./services/search-engine');
16
+ const { processMessage: agentChat } = require('./services/agent-chat');
17
+ const agentTasks = require('./services/agent-tasks');
18
+ const { cluster } = require('./services/cluster');
19
+
20
+ const authRoutes = require('./routes/auth');
21
+ const apiRoutes = require('./routes/api');
22
+ const licenseRoutes = require('./routes/license');
23
+ const adminRoutes = require('./routes/admin');
24
+ const billingRoutes = require('./routes/billing');
25
+ const sovereignRoutes = require('./routes/sovereign');
26
+ const meshRoutes = require('./routes/mesh');
27
+ const commanderRoutes = require('./routes/commander');
28
+ const adsRoutes = require('./routes/ads');
29
+ const wabApiRoutes = require('./routes/wab-api');
30
+ const noscriptRoutes = require('./routes/noscript');
31
+ const discoveryRoutes = require('./routes/discovery');
32
+ const premiumRoutes = require('./routes/premium');
33
+ const adminPremiumRoutes = require('./routes/admin-premium');
34
+ const workspaceRoutes = require('./routes/agent-workspace');
35
+ const universalRoutes = require('./routes/universal');
36
+ const runtimeRoutes = require('./routes/runtime');
37
+ const demoShowcaseRoutes = require('./routes/demo-showcase');
38
+ const gatewayRoutes = require('./routes/gateway');
39
+ let growthRoutes;
40
+ try { growthRoutes = require('./routes/growth'); } catch { growthRoutes = require('express').Router(); }
41
+ const { handleWebhookRequest } = require('./services/stripe');
42
+ const { runtime } = require('./runtime');
43
+
44
+ const app = express();
45
+ const PORT = process.env.PORT || 3000;
46
+
47
+ app.set('trust proxy', 1);
48
+
49
+ const corsOrigins = (process.env.ALLOWED_ORIGINS
50
+ || 'http://localhost:3000,http://127.0.0.1:3000,http://localhost:5173')
51
+ .split(',')
52
+ .map((s) => s.trim())
53
+ .filter(Boolean);
54
+
55
+ app.use(
56
+ cors({
57
+ origin(origin, callback) {
58
+ if (!origin) return callback(null, true);
59
+ if (corsOrigins.includes(origin)) return callback(null, true);
60
+ if (process.env.NODE_ENV !== 'production' && /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i.test(origin)) {
61
+ return callback(null, true);
62
+ }
63
+ return callback(null, false);
64
+ },
65
+ credentials: true
66
+ })
67
+ );
68
+
69
+ const scriptSrc = process.env.CSP_ALLOW_UNSAFE_INLINE === 'false'
70
+ ? ["'self'"]
71
+ : ["'self'", "'unsafe-inline'"];
72
+ const styleSrc = process.env.CSP_ALLOW_UNSAFE_INLINE === 'false'
73
+ ? ["'self'"]
74
+ : ["'self'", "'unsafe-inline'"];
75
+
76
+ app.use(
77
+ helmet({
78
+ contentSecurityPolicy: {
79
+ directives: {
80
+ defaultSrc: ["'self'"],
81
+ scriptSrc,
82
+ scriptSrcAttr: scriptSrc,
83
+ styleSrc: [...styleSrc, 'https://fonts.googleapis.com'],
84
+ imgSrc: ["'self'", 'data:', 'https:'],
85
+ connectSrc: ["'self'", 'ws:', 'wss:'],
86
+ fontSrc: ["'self'", 'https://fonts.gstatic.com', 'https:', 'data:'],
87
+ frameSrc: ["'self'", 'https:', 'http:'],
88
+ frameAncestors: ["'none'"],
89
+ objectSrc: ["'none'"],
90
+ baseUri: ["'self'"],
91
+ formAction: ["'self'"]
92
+ }
93
+ },
94
+ crossOriginEmbedderPolicy: false
95
+ })
96
+ );
97
+
98
+ app.post('/api/billing/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
99
+ try {
100
+ await handleWebhookRequest(req);
101
+ res.json({ received: true });
102
+ } catch (err) {
103
+ console.error('Webhook error:', err.message);
104
+ res.status(400).json({ error: err.message });
105
+ }
106
+ });
107
+
108
+ app.use(express.json());
109
+
110
+ const apiLimiter = rateLimit({
111
+ windowMs: 15 * 60 * 1000,
112
+ max: 200,
113
+ standardHeaders: true,
114
+ legacyHeaders: false,
115
+ message: { error: 'Too many requests, please try again later' }
116
+ });
117
+
118
+ const licenseLimiter = rateLimit({
119
+ windowMs: 60 * 1000,
120
+ max: 120,
121
+ standardHeaders: true,
122
+ legacyHeaders: false,
123
+ keyGenerator: (req) => {
124
+ const key = req.body?.licenseKey || req.body?.siteId || req.ip;
125
+ return `${req.ip}:${key}`;
126
+ }
127
+ });
128
+
129
+ app.use(express.static(path.join(__dirname, '..', 'public'), {
130
+ setHeaders(res, filePath) {
131
+ if (filePath.endsWith('.html')) {
132
+ res.setHeader('Cache-Control', 'no-cache, must-revalidate');
133
+ }
134
+ }
135
+ }));
136
+ app.use('/script', express.static(path.join(__dirname, '..', 'script')));
137
+
138
+ app.use('/api/auth', apiLimiter, authRoutes);
139
+ app.use('/api', apiLimiter, apiRoutes);
140
+ app.use('/api/license', licenseLimiter, licenseRoutes);
141
+ app.use('/api/admin', apiLimiter, adminRoutes);
142
+ app.use('/api/billing', apiLimiter, billingRoutes);
143
+ app.use('/api/sovereign', apiLimiter, sovereignRoutes);
144
+ app.use('/api/mesh', apiLimiter, meshRoutes);
145
+ app.use('/api/commander', apiLimiter, commanderRoutes);
146
+ app.use('/api/ads', apiLimiter, adsRoutes);
147
+ app.use('/api/wab', wabApiRoutes);
148
+ app.use('/api/noscript', apiLimiter, noscriptRoutes);
149
+ app.use('/api/discovery', apiLimiter, discoveryRoutes);
150
+ app.use('/api/premium', apiLimiter, premiumRoutes);
151
+ app.use('/api/admin/premium', apiLimiter, adminPremiumRoutes);
152
+ app.use('/api/workspace', apiLimiter, workspaceRoutes);
153
+ app.use('/api/universal', apiLimiter, universalRoutes);
154
+ app.use('/api/os', apiLimiter, runtimeRoutes);
155
+ app.use('/api/demo', apiLimiter, demoShowcaseRoutes);
156
+ app.use('/api/growth', apiLimiter, growthRoutes);
157
+ app.use('/api/v1', gatewayRoutes);
158
+
159
+ // Convenience alias: /api/negotiate/* /api/sovereign/negotiation/*
160
+ app.get('/api/negotiate', apiLimiter, (req, res) => {
161
+ res.json({
162
+ engine: 'WAB Negotiation Engine',
163
+ endpoints: {
164
+ 'POST /api/negotiate/rules': 'Create negotiation rules (auth required)',
165
+ 'GET /api/negotiate/rules/:siteId': 'Get rules for a site',
166
+ 'PUT /api/negotiate/rules/:ruleId': 'Update a rule (auth required)',
167
+ 'POST /api/negotiate/sessions': 'Open negotiation session',
168
+ 'POST /api/negotiate/sessions/:id/propose': 'Agent counter-offer',
169
+ 'POST /api/negotiate/sessions/:id/confirm': 'Confirm deal',
170
+ 'GET /api/negotiate/stats/:siteId': 'Negotiation stats',
171
+ },
172
+ });
173
+ });
174
+ app.use('/api/negotiate', apiLimiter, (req, res, next) => {
175
+ req.url = '/negotiation' + req.url;
176
+ sovereignRoutes(req, res, next);
177
+ });
178
+
179
+ // ─── WAB Search Engine ────────────────────────────────────────────────
180
+
181
+ const searchLimiter = rateLimit({
182
+ windowMs: 60 * 1000,
183
+ max: 30,
184
+ standardHeaders: true,
185
+ legacyHeaders: false,
186
+ message: { error: 'Too many search requests, please slow down' }
187
+ });
188
+
189
+ app.get('/api/search', searchLimiter, async (req, res) => {
190
+ const q = (req.query.q || '').trim();
191
+ if (!q) return res.json({ results: [], cached: false });
192
+ if (q.length > 200) return res.status(400).json({ error: 'Query too long' });
193
+ const crypto = require('crypto');
194
+ const ipHash = crypto.createHash('sha256').update(req.ip || '').digest('hex').slice(0, 16);
195
+ const result = await search(q, ipHash);
196
+ res.json(result);
197
+ });
198
+
199
+ app.get('/api/search/suggest', searchLimiter, (req, res) => {
200
+ const q = (req.query.q || '').trim();
201
+ if (!q) return res.json({ suggestions: [] });
202
+ const suggestions = getSuggestions(q, 8);
203
+ res.json({ suggestions });
204
+ });
205
+
206
+ app.get('/api/search/trending', apiLimiter, (req, res) => {
207
+ const trending = getTrendingSearches(10);
208
+ res.json({ trending });
209
+ });
210
+
211
+ app.get('/api/search/stats', apiLimiter, (req, res) => {
212
+ const stats = getSearchStats();
213
+ res.json(stats);
214
+ });
215
+
216
+ // Prevent browsers from caching HTML page routes
217
+ function noCache(req, res, next) {
218
+ res.set('Cache-Control', 'no-cache, must-revalidate');
219
+ next();
220
+ }
221
+
222
+ app.get('/dashboard', noCache, (req, res) => {
223
+ res.sendFile(path.join(__dirname, '..', 'public', 'dashboard.html'));
224
+ });
225
+ app.get('/mesh-dashboard', noCache, (req, res) => {
226
+ res.sendFile(path.join(__dirname, '..', 'public', 'mesh-dashboard.html'));
227
+ });
228
+ app.get('/commander-dashboard', noCache, (req, res) => {
229
+ res.sendFile(path.join(__dirname, '..', 'public', 'commander-dashboard.html'));
230
+ });
231
+ app.get('/docs', noCache, (req, res) => {
232
+ res.sendFile(path.join(__dirname, '..', 'public', 'docs.html'));
233
+ });
234
+ app.get('/login', noCache, (req, res) => {
235
+ res.sendFile(path.join(__dirname, '..', 'public', 'login.html'));
236
+ });
237
+ app.get('/register', noCache, (req, res) => {
238
+ res.sendFile(path.join(__dirname, '..', 'public', 'register.html'));
239
+ });
240
+ app.get('/admin/login', noCache, (req, res) => {
241
+ res.sendFile(path.join(__dirname, '..', 'public', 'admin', 'login.html'));
242
+ });
243
+ app.get('/admin', noCache, (req, res) => {
244
+ res.sendFile(path.join(__dirname, '..', 'public', 'admin', 'dashboard.html'));
245
+ });
246
+ app.get('/privacy', noCache, (req, res) => {
247
+ res.sendFile(path.join(__dirname, '..', 'public', 'privacy.html'));
248
+ });
249
+ app.get('/terms', noCache, (req, res) => {
250
+ res.sendFile(path.join(__dirname, '..', 'public', 'terms.html'));
251
+ });
252
+ app.get('/cookies', noCache, (req, res) => {
253
+ res.sendFile(path.join(__dirname, '..', 'public', 'cookies.html'));
254
+ });
255
+ app.get('/browser', noCache, (req, res) => {
256
+ res.sendFile(path.join(__dirname, '..', 'public', 'browser.html'));
257
+ });
258
+ app.get('/workspace', noCache, (req, res) => {
259
+ res.sendFile(path.join(__dirname, '..', 'public', 'agent-workspace.html'));
260
+ });
261
+ app.get('/growth', noCache, (req, res) => {
262
+ res.sendFile(path.join(__dirname, '..', 'public', 'growth.html'));
263
+ });
264
+ app.get('/score', noCache, (req, res) => {
265
+ res.sendFile(path.join(__dirname, '..', 'public', 'score.html'));
266
+ });
267
+ app.get('/api', noCache, (req, res) => {
268
+ res.sendFile(path.join(__dirname, '..', 'public', 'api.html'));
269
+ });
270
+
271
+ // Browser downloads
272
+ app.use('/downloads', express.static(path.join(__dirname, '..', 'downloads'), {
273
+ maxAge: '1d',
274
+ setHeaders: (res, filePath) => {
275
+ res.set('Content-Disposition', 'attachment');
276
+ }
277
+ }));
278
+
279
+ // Agent chat endpoint for WAB Browser Real AI Agent
280
+ const chatLimiter = rateLimit({
281
+ windowMs: 60 * 1000,
282
+ max: 20,
283
+ standardHeaders: true,
284
+ legacyHeaders: false,
285
+ message: { error: 'Too many messages, please slow down' }
286
+ });
287
+
288
+ app.post('/api/wab/agent-chat', chatLimiter, async (req, res) => {
289
+ const { message, context, sessionId, taskId, taskAction } = req.body || {};
290
+ if (!message || typeof message !== 'string') {
291
+ return res.status(400).json({ error: 'Message required' });
292
+ }
293
+ if (message.length > 3000) {
294
+ return res.status(400).json({ error: 'Message too long' });
295
+ }
296
+
297
+ const sid = sessionId || req.ip || 'anonymous';
298
+
299
+ try {
300
+ // ── Task actions (user responding to an active task) ──
301
+ if (taskId && taskAction) {
302
+ if (taskAction === 'answer') {
303
+ const result = agentTasks.answerClarification(taskId, message);
304
+ if (result.status === 'planning') {
305
+ // Auto-execute after planning
306
+ const execResult = await agentTasks.executeTask(taskId);
307
+ return res.json({ ...execResult, type: 'task' });
308
+ }
309
+ return res.json({ ...result, type: 'task' });
310
+ }
311
+ if (taskAction === 'select') {
312
+ const idx = parseInt(message.replace(/\D/g, '')) - 1;
313
+ const result = agentTasks.selectOffer(taskId, idx);
314
+ return res.json({ ...result, type: 'task' });
315
+ }
316
+ if (taskAction === 'cancel') {
317
+ const result = agentTasks.cancelTask(taskId);
318
+ return res.json({ ...result, type: 'task' });
319
+ }
320
+ }
321
+
322
+ // ── Check if user wants to select from existing offers ──
323
+ if (!taskId) {
324
+ const selectMatch = message.match(/(?:اختر|اخت(?:ا|ي)ر|select|choose|pick)\s*(\d+)/i);
325
+ if (selectMatch) {
326
+ const tasks = agentTasks.getSessionTasks(sid, 1);
327
+ if (tasks.length > 0 && tasks[0].status === 'presenting') {
328
+ const idx = parseInt(selectMatch[1]) - 1;
329
+ const result = agentTasks.selectOffer(tasks[0].id, idx);
330
+ return res.json({ ...result, type: 'task' });
331
+ }
332
+ }
333
+ }
334
+
335
+ // ── Detect URL paste — create URL negotiation task ──
336
+ const urlData = agentTasks.parseBookingUrl(message);
337
+ if (urlData) {
338
+ const task = agentTasks.createUrlTask(sid, message, urlData);
339
+ const execResult = await agentTasks.executeUrlTask(task.taskId);
340
+ return res.json({ ...execResult, type: 'task', urlData });
341
+ }
342
+
343
+ // ── Detect if this is a task-type request (booking, shopping, etc.) ──
344
+ const intent = agentTasks.detectIntent(message);
345
+ if (intent.confidence >= 0.7 && intent.intent !== 'general') {
346
+ const task = agentTasks.createTask(sid, message);
347
+
348
+ if (task.status === 'clarifying') {
349
+ return res.json({ ...task, type: 'task' });
350
+ }
351
+
352
+ // If requirements are complete, auto-execute
353
+ const execResult = await agentTasks.executeTask(task.taskId);
354
+ return res.json({ ...execResult, type: 'task' });
355
+ }
356
+
357
+ // ── Regular chat (not a task) ──
358
+ const chatContext = {
359
+ url: context?.url || '',
360
+ platform: context?.platform || 'unknown',
361
+ sessionId: sid,
362
+ };
363
+ const result = await agentChat(message, chatContext);
364
+ res.json(result);
365
+ } catch (err) {
366
+ console.error('[agent-chat] Error:', err.message);
367
+ res.json({ reply: '🤖 عذراً، حدث خطأ. حاول مرة أخرى.', type: 'text' });
368
+ }
369
+ });
370
+
371
+ // Agent task status & history
372
+ app.get('/api/wab/agent-task/:id', chatLimiter, (req, res) => {
373
+ const state = agentTasks.getTaskState(req.params.id);
374
+ if (!state) return res.status(404).json({ error: 'Task not found' });
375
+ res.json(state);
376
+ });
377
+
378
+ app.get('/api/wab/agent-tasks', chatLimiter, (req, res) => {
379
+ const sid = req.query.sessionId || req.ip || 'anonymous';
380
+ const tasks = agentTasks.getSessionTasks(sid, 20);
381
+ res.json({ tasks });
382
+ });
383
+
384
+ const pkg = require('../package.json');
385
+ app.use(`/v${pkg.version.split('.')[0]}`, express.static(path.join(__dirname, '..', 'script')));
386
+ app.use('/latest', express.static(path.join(__dirname, '..', 'script')));
387
+
388
+ app.get('*', (req, res) => {
389
+ // API routes always return JSON 404
390
+ if (req.path.startsWith('/api/')) {
391
+ return res.status(404).json({ error: 'Not found', path: req.path });
392
+ }
393
+ if (req.accepts('html')) {
394
+ res.sendFile(path.join(__dirname, '..', 'public', 'index.html'));
395
+ } else {
396
+ res.status(404).json({ error: 'Not found' });
397
+ }
398
+ });
399
+
400
+ if (process.env.NODE_ENV !== 'test') {
401
+ console.log('Running database migrations...');
402
+ runMigrations();
403
+ maybeBootstrapAdmin();
404
+ initSearchEngine(db);
405
+
406
+ // Purge old search cache every hour
407
+ setInterval(purgeOldCache, 60 * 60 * 1000);
408
+
409
+ const server = http.createServer(app);
410
+ setupWebSocket(server);
411
+
412
+ // Start Agent OS runtime
413
+ runtime.start();
414
+
415
+ // Start Cluster Orchestrator
416
+ cluster.start();
417
+
418
+ server.listen(PORT, () => {
419
+ console.log(`\n ╔══════════════════════════════════════════╗`);
420
+ console.log(` ║ Web Agent Bridge v${pkg.version} ║`);
421
+ console.log(` ║ Server running on http://localhost:${PORT} ║`);
422
+ console.log(` ║ WebSocket: ws://localhost:${PORT}/ws/analytics ║`);
423
+ console.log(` ╚══════════════════════════════════════════╝\n`);
424
+ });
425
+ }
426
+
427
+ module.exports = app;