web-agent-bridge 2.3.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ar.md +506 -31
- package/README.md +574 -47
- package/bin/agent-runner.js +10 -1
- package/package.json +1 -1
- package/public/agent-workspace.html +347 -0
- package/public/browser.html +484 -0
- package/public/css/agent-workspace.css +1713 -0
- package/public/index.html +94 -0
- package/public/js/agent-workspace.js +1740 -0
- package/sdk/index.d.ts +83 -0
- package/sdk/index.js +115 -1
- package/sdk/package.json +1 -1
- package/server/config/secrets.js +13 -5
- package/server/index.js +183 -4
- package/server/middleware/adminAuth.js +6 -1
- package/server/middleware/auth.js +11 -2
- package/server/middleware/rateLimits.js +78 -2
- package/server/migrations/003_ads_integer_cents.sql +33 -0
- package/server/models/db.js +126 -25
- package/server/routes/admin.js +16 -2
- package/server/routes/ads.js +130 -0
- package/server/routes/agent-workspace.js +378 -0
- package/server/routes/api.js +21 -2
- package/server/routes/auth.js +26 -6
- package/server/routes/sovereign.js +78 -0
- package/server/routes/universal.js +177 -0
- package/server/routes/wab-api.js +20 -5
- package/server/services/agent-chat.js +506 -0
- package/server/services/agent-symphony.js +6 -0
- package/server/services/agent-tasks.js +1807 -0
- package/server/services/fairness-engine.js +409 -0
- package/server/services/plugins.js +27 -3
- package/server/services/price-intelligence.js +565 -0
- package/server/services/price-shield.js +1137 -0
- package/server/services/search-engine.js +357 -0
- package/server/services/security.js +513 -0
- package/server/services/universal-scraper.js +661 -0
- package/server/ws.js +61 -1
package/sdk/index.d.ts
CHANGED
|
@@ -209,3 +209,86 @@ export declare class WABMultiAgent {
|
|
|
209
209
|
/** Close all browser sessions. */
|
|
210
210
|
close(): Promise<void>;
|
|
211
211
|
}
|
|
212
|
+
|
|
213
|
+
// ─── WABUniversalAgent — Works on ANY page, no bridge needed ───────────
|
|
214
|
+
|
|
215
|
+
export interface UniversalAnalysis {
|
|
216
|
+
url: string;
|
|
217
|
+
domain: string;
|
|
218
|
+
products?: Array<{
|
|
219
|
+
name?: string;
|
|
220
|
+
price?: number;
|
|
221
|
+
currency?: string;
|
|
222
|
+
originalPrice?: number;
|
|
223
|
+
rating?: number;
|
|
224
|
+
method?: string;
|
|
225
|
+
}>;
|
|
226
|
+
fairness?: {
|
|
227
|
+
total: number;
|
|
228
|
+
category: string;
|
|
229
|
+
breakdown: Record<string, number>;
|
|
230
|
+
wabBridge?: { installed: boolean; bonus?: number; hasNegotiation?: boolean };
|
|
231
|
+
platform?: { size: string; commission: number };
|
|
232
|
+
};
|
|
233
|
+
darkPatterns?: Array<{ type: string; severity?: string; matches?: string[] }>;
|
|
234
|
+
alerts?: Array<{ title: string; description?: string; severity?: string }>;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface UniversalDeal {
|
|
238
|
+
name?: string;
|
|
239
|
+
source?: string;
|
|
240
|
+
domain?: string;
|
|
241
|
+
priceUsd?: number;
|
|
242
|
+
rating?: number;
|
|
243
|
+
url?: string;
|
|
244
|
+
compositeScore?: number;
|
|
245
|
+
wabBridge?: boolean;
|
|
246
|
+
canNegotiate?: boolean;
|
|
247
|
+
fairness?: { total: number; category: string };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export interface UniversalDealsResult {
|
|
251
|
+
deals: UniversalDeal[];
|
|
252
|
+
insights?: Array<{ icon?: string; text: string }>;
|
|
253
|
+
sourcesChecked?: number;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export interface UniversalFairness {
|
|
257
|
+
domain: string;
|
|
258
|
+
total: number;
|
|
259
|
+
category: string;
|
|
260
|
+
breakdown: Record<string, number>;
|
|
261
|
+
wabBridge?: { installed: boolean; bonus?: number };
|
|
262
|
+
platform?: { size: string; commission: number };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export declare class WABUniversalAgent {
|
|
266
|
+
constructor(serverUrl?: string);
|
|
267
|
+
|
|
268
|
+
/** Extract products, prices, and metadata from any URL. */
|
|
269
|
+
extract(url: string): Promise<any>;
|
|
270
|
+
|
|
271
|
+
/** Full analysis: extract + fairness + fraud detection + dark patterns. */
|
|
272
|
+
analyze(url: string): Promise<UniversalAnalysis>;
|
|
273
|
+
|
|
274
|
+
/** Compare prices across multiple sources. */
|
|
275
|
+
compare(query: string, category?: string): Promise<any>;
|
|
276
|
+
|
|
277
|
+
/** Find and rank the best deals with fairness scoring. */
|
|
278
|
+
deals(query: string, category?: string, lang?: string): Promise<UniversalDealsResult>;
|
|
279
|
+
|
|
280
|
+
/** Get fairness score for a domain. */
|
|
281
|
+
fairness(domain: string): Promise<UniversalFairness>;
|
|
282
|
+
|
|
283
|
+
/** Detect dark patterns on a URL. */
|
|
284
|
+
darkPatterns(url: string): Promise<any>;
|
|
285
|
+
|
|
286
|
+
/** Get price history for a domain. */
|
|
287
|
+
priceHistory(domain: string): Promise<any>;
|
|
288
|
+
|
|
289
|
+
/** Get top fairness-scored sites. */
|
|
290
|
+
topFair(limit?: number): Promise<any>;
|
|
291
|
+
|
|
292
|
+
/** Get all known competing sources. */
|
|
293
|
+
sources(): Promise<any>;
|
|
294
|
+
}
|
package/sdk/index.js
CHANGED
|
@@ -254,7 +254,121 @@ class WABAgent {
|
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
/**
|
|
258
|
+
* WABUniversalAgent — Works on ANY page, no bridge script needed.
|
|
259
|
+
* Uses server-side extraction, analysis, and comparison APIs.
|
|
260
|
+
*/
|
|
261
|
+
class WABUniversalAgent {
|
|
262
|
+
/**
|
|
263
|
+
* @param {string} [serverUrl='http://localhost:3000'] — WAB server URL
|
|
264
|
+
*/
|
|
265
|
+
constructor(serverUrl = 'http://localhost:3000') {
|
|
266
|
+
this.serverUrl = serverUrl.replace(/\/$/, '');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** @private */
|
|
270
|
+
async _post(path, body) {
|
|
271
|
+
const res = await fetch(`${this.serverUrl}${path}`, {
|
|
272
|
+
method: 'POST',
|
|
273
|
+
headers: { 'Content-Type': 'application/json' },
|
|
274
|
+
body: JSON.stringify(body),
|
|
275
|
+
});
|
|
276
|
+
if (!res.ok) throw new Error(`WAB API error ${res.status}: ${await res.text()}`);
|
|
277
|
+
return res.json();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** @private */
|
|
281
|
+
async _get(path) {
|
|
282
|
+
const res = await fetch(`${this.serverUrl}${path}`);
|
|
283
|
+
if (!res.ok) throw new Error(`WAB API error ${res.status}: ${await res.text()}`);
|
|
284
|
+
return res.json();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Extract products, prices, and metadata from any URL.
|
|
289
|
+
* @param {string} url
|
|
290
|
+
* @returns {Promise<object>}
|
|
291
|
+
*/
|
|
292
|
+
async extract(url) {
|
|
293
|
+
return this._post('/api/universal/extract', { url });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Full analysis: extract + fairness + fraud detection + dark patterns.
|
|
298
|
+
* @param {string} url
|
|
299
|
+
* @returns {Promise<object>}
|
|
300
|
+
*/
|
|
301
|
+
async analyze(url) {
|
|
302
|
+
return this._post('/api/universal/analyze', { url });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Compare prices across multiple sources.
|
|
307
|
+
* @param {string} query — Product or service to search for
|
|
308
|
+
* @param {string} [category='product'] — 'product', 'hotel', 'flight'
|
|
309
|
+
* @returns {Promise<object>}
|
|
310
|
+
*/
|
|
311
|
+
async compare(query, category = 'product') {
|
|
312
|
+
return this._post('/api/universal/compare', { query, category });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Find and rank the best deals with fairness scoring.
|
|
317
|
+
* @param {string} query
|
|
318
|
+
* @param {string} [category='product']
|
|
319
|
+
* @param {string} [lang='en']
|
|
320
|
+
* @returns {Promise<object>}
|
|
321
|
+
*/
|
|
322
|
+
async deals(query, category = 'product', lang = 'en') {
|
|
323
|
+
return this._post('/api/universal/deals', { query, category, lang });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Get fairness score for a domain.
|
|
328
|
+
* @param {string} domain
|
|
329
|
+
* @returns {Promise<object>}
|
|
330
|
+
*/
|
|
331
|
+
async fairness(domain) {
|
|
332
|
+
return this._post('/api/universal/fairness', { domain });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Detect dark patterns on a URL.
|
|
337
|
+
* @param {string} url
|
|
338
|
+
* @returns {Promise<object>}
|
|
339
|
+
*/
|
|
340
|
+
async darkPatterns(url) {
|
|
341
|
+
return this._post('/api/universal/dark-patterns', { url });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Get price history for a domain.
|
|
346
|
+
* @param {string} domain
|
|
347
|
+
* @returns {Promise<object>}
|
|
348
|
+
*/
|
|
349
|
+
async priceHistory(domain) {
|
|
350
|
+
return this._get(`/api/universal/history?domain=${encodeURIComponent(domain)}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Get top fairness-scored sites.
|
|
355
|
+
* @param {number} [limit=20]
|
|
356
|
+
* @returns {Promise<object>}
|
|
357
|
+
*/
|
|
358
|
+
async topFair(limit = 20) {
|
|
359
|
+
return this._get(`/api/universal/top-fair?limit=${limit}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Get all known competing sources.
|
|
364
|
+
* @returns {Promise<object>}
|
|
365
|
+
*/
|
|
366
|
+
async sources() {
|
|
367
|
+
return this._get('/api/universal/sources');
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
257
371
|
const { WABMultiAgent } = require('./multi-agent');
|
|
258
372
|
const { WABAgentMesh } = require('./agent-mesh');
|
|
259
373
|
|
|
260
|
-
module.exports = { WABAgent, WABMultiAgent, WABAgentMesh };
|
|
374
|
+
module.exports = { WABAgent, WABUniversalAgent, WABMultiAgent, WABAgentMesh };
|
package/sdk/package.json
CHANGED
package/server/config/secrets.js
CHANGED
|
@@ -33,10 +33,12 @@ function isProd() {
|
|
|
33
33
|
function assertSecretsAtStartup() {
|
|
34
34
|
if (isTest()) return;
|
|
35
35
|
if (isProd() && !process.env.JWT_SECRET) {
|
|
36
|
-
|
|
36
|
+
console.error('[WAB] FATAL: JWT_SECRET is not set in production. Refusing to start with insecure defaults.');
|
|
37
|
+
process.exit(1);
|
|
37
38
|
}
|
|
38
39
|
if (isProd() && !process.env.JWT_SECRET_ADMIN) {
|
|
39
|
-
|
|
40
|
+
console.error('[WAB] FATAL: JWT_SECRET_ADMIN is not set in production. Refusing to start with insecure defaults.');
|
|
41
|
+
process.exit(1);
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
44
|
|
|
@@ -44,14 +46,20 @@ function getJwtUserSecret() {
|
|
|
44
46
|
if (isTest()) {
|
|
45
47
|
return process.env.JWT_SECRET || 'test-secret-key-for-testing';
|
|
46
48
|
}
|
|
47
|
-
|
|
49
|
+
if (process.env.JWT_SECRET) return process.env.JWT_SECRET;
|
|
50
|
+
// Dev mode: generate ephemeral secret per process (not hardcoded)
|
|
51
|
+
if (!_autoUserSecret) _autoUserSecret = generateAutoSecret('JWT_SECRET');
|
|
52
|
+
return _autoUserSecret;
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
function getJwtAdminSecret() {
|
|
51
56
|
if (isTest()) {
|
|
52
|
-
return process.env.JWT_SECRET_ADMIN ||
|
|
57
|
+
return process.env.JWT_SECRET_ADMIN || 'test-secret-key-for-testing-admin';
|
|
53
58
|
}
|
|
54
|
-
|
|
59
|
+
if (process.env.JWT_SECRET_ADMIN) return process.env.JWT_SECRET_ADMIN;
|
|
60
|
+
// Dev mode: generate separate ephemeral secret (never share with user secret)
|
|
61
|
+
if (!_autoAdminSecret) _autoAdminSecret = generateAutoSecret('JWT_SECRET_ADMIN');
|
|
62
|
+
return _autoAdminSecret;
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
function signUserToken(payload, options = {}) {
|
package/server/index.js
CHANGED
|
@@ -11,7 +11,10 @@ const rateLimit = require('express-rate-limit');
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const { setupWebSocket } = require('./ws');
|
|
13
13
|
const { runMigrations } = require('./utils/migrate');
|
|
14
|
-
const { maybeBootstrapAdmin } = require('./models/db');
|
|
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');
|
|
15
18
|
|
|
16
19
|
const authRoutes = require('./routes/auth');
|
|
17
20
|
const apiRoutes = require('./routes/api');
|
|
@@ -21,6 +24,14 @@ const billingRoutes = require('./routes/billing');
|
|
|
21
24
|
const sovereignRoutes = require('./routes/sovereign');
|
|
22
25
|
const meshRoutes = require('./routes/mesh');
|
|
23
26
|
const commanderRoutes = require('./routes/commander');
|
|
27
|
+
const adsRoutes = require('./routes/ads');
|
|
28
|
+
const wabApiRoutes = require('./routes/wab-api');
|
|
29
|
+
const noscriptRoutes = require('./routes/noscript');
|
|
30
|
+
const discoveryRoutes = require('./routes/discovery');
|
|
31
|
+
const premiumRoutes = require('./routes/premium');
|
|
32
|
+
const adminPremiumRoutes = require('./routes/admin-premium');
|
|
33
|
+
const workspaceRoutes = require('./routes/agent-workspace');
|
|
34
|
+
const universalRoutes = require('./routes/universal');
|
|
24
35
|
const { handleWebhookRequest } = require('./services/stripe');
|
|
25
36
|
|
|
26
37
|
const app = express();
|
|
@@ -62,11 +73,11 @@ app.use(
|
|
|
62
73
|
defaultSrc: ["'self'"],
|
|
63
74
|
scriptSrc,
|
|
64
75
|
scriptSrcAttr: scriptSrc,
|
|
65
|
-
styleSrc,
|
|
76
|
+
styleSrc: [...styleSrc, 'https://fonts.googleapis.com'],
|
|
66
77
|
imgSrc: ["'self'", 'data:', 'https:'],
|
|
67
78
|
connectSrc: ["'self'", 'ws:', 'wss:'],
|
|
68
|
-
fontSrc: ["'self'", 'https:', 'data:'],
|
|
69
|
-
frameSrc: ["'
|
|
79
|
+
fontSrc: ["'self'", 'https://fonts.gstatic.com', 'https:', 'data:'],
|
|
80
|
+
frameSrc: ["'self'", 'https:', 'http:'],
|
|
70
81
|
frameAncestors: ["'none'"],
|
|
71
82
|
objectSrc: ["'none'"],
|
|
72
83
|
baseUri: ["'self'"],
|
|
@@ -119,6 +130,51 @@ app.use('/api/billing', apiLimiter, billingRoutes);
|
|
|
119
130
|
app.use('/api/sovereign', apiLimiter, sovereignRoutes);
|
|
120
131
|
app.use('/api/mesh', apiLimiter, meshRoutes);
|
|
121
132
|
app.use('/api/commander', apiLimiter, commanderRoutes);
|
|
133
|
+
app.use('/api/ads', apiLimiter, adsRoutes);
|
|
134
|
+
app.use('/api/wab', wabApiRoutes);
|
|
135
|
+
app.use('/api/noscript', apiLimiter, noscriptRoutes);
|
|
136
|
+
app.use('/api/discovery', apiLimiter, discoveryRoutes);
|
|
137
|
+
app.use('/api/premium', apiLimiter, premiumRoutes);
|
|
138
|
+
app.use('/api/admin/premium', apiLimiter, adminPremiumRoutes);
|
|
139
|
+
app.use('/api/workspace', apiLimiter, workspaceRoutes);
|
|
140
|
+
app.use('/api/universal', apiLimiter, universalRoutes);
|
|
141
|
+
|
|
142
|
+
// ─── WAB Search Engine ────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
const searchLimiter = rateLimit({
|
|
145
|
+
windowMs: 60 * 1000,
|
|
146
|
+
max: 30,
|
|
147
|
+
standardHeaders: true,
|
|
148
|
+
legacyHeaders: false,
|
|
149
|
+
message: { error: 'Too many search requests, please slow down' }
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
app.get('/api/search', searchLimiter, async (req, res) => {
|
|
153
|
+
const q = (req.query.q || '').trim();
|
|
154
|
+
if (!q) return res.json({ results: [], cached: false });
|
|
155
|
+
if (q.length > 200) return res.status(400).json({ error: 'Query too long' });
|
|
156
|
+
const crypto = require('crypto');
|
|
157
|
+
const ipHash = crypto.createHash('sha256').update(req.ip || '').digest('hex').slice(0, 16);
|
|
158
|
+
const result = await search(q, ipHash);
|
|
159
|
+
res.json(result);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
app.get('/api/search/suggest', searchLimiter, (req, res) => {
|
|
163
|
+
const q = (req.query.q || '').trim();
|
|
164
|
+
if (!q) return res.json({ suggestions: [] });
|
|
165
|
+
const suggestions = getSuggestions(q, 8);
|
|
166
|
+
res.json({ suggestions });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
app.get('/api/search/trending', apiLimiter, (req, res) => {
|
|
170
|
+
const trending = getTrendingSearches(10);
|
|
171
|
+
res.json({ trending });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
app.get('/api/search/stats', apiLimiter, (req, res) => {
|
|
175
|
+
const stats = getSearchStats();
|
|
176
|
+
res.json(stats);
|
|
177
|
+
});
|
|
122
178
|
|
|
123
179
|
app.get('/dashboard', (req, res) => {
|
|
124
180
|
res.sendFile(path.join(__dirname, '..', 'public', 'dashboard.html'));
|
|
@@ -153,6 +209,125 @@ app.get('/terms', (req, res) => {
|
|
|
153
209
|
app.get('/cookies', (req, res) => {
|
|
154
210
|
res.sendFile(path.join(__dirname, '..', 'public', 'cookies.html'));
|
|
155
211
|
});
|
|
212
|
+
app.get('/browser', (req, res) => {
|
|
213
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'browser.html'));
|
|
214
|
+
});
|
|
215
|
+
app.get('/workspace', (req, res) => {
|
|
216
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'agent-workspace.html'));
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Browser downloads
|
|
220
|
+
app.use('/downloads', express.static(path.join(__dirname, '..', 'downloads'), {
|
|
221
|
+
maxAge: '1d',
|
|
222
|
+
setHeaders: (res, filePath) => {
|
|
223
|
+
res.set('Content-Disposition', 'attachment');
|
|
224
|
+
}
|
|
225
|
+
}));
|
|
226
|
+
|
|
227
|
+
// Agent chat endpoint for WAB Browser — Real AI Agent
|
|
228
|
+
const chatLimiter = rateLimit({
|
|
229
|
+
windowMs: 60 * 1000,
|
|
230
|
+
max: 20,
|
|
231
|
+
standardHeaders: true,
|
|
232
|
+
legacyHeaders: false,
|
|
233
|
+
message: { error: 'Too many messages, please slow down' }
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
app.post('/api/wab/agent-chat', chatLimiter, async (req, res) => {
|
|
237
|
+
const { message, context, sessionId, taskId, taskAction } = req.body || {};
|
|
238
|
+
if (!message || typeof message !== 'string') {
|
|
239
|
+
return res.status(400).json({ error: 'Message required' });
|
|
240
|
+
}
|
|
241
|
+
if (message.length > 3000) {
|
|
242
|
+
return res.status(400).json({ error: 'Message too long' });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const sid = sessionId || req.ip || 'anonymous';
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
// ── Task actions (user responding to an active task) ──
|
|
249
|
+
if (taskId && taskAction) {
|
|
250
|
+
if (taskAction === 'answer') {
|
|
251
|
+
const result = agentTasks.answerClarification(taskId, message);
|
|
252
|
+
if (result.status === 'planning') {
|
|
253
|
+
// Auto-execute after planning
|
|
254
|
+
const execResult = await agentTasks.executeTask(taskId);
|
|
255
|
+
return res.json({ ...execResult, type: 'task' });
|
|
256
|
+
}
|
|
257
|
+
return res.json({ ...result, type: 'task' });
|
|
258
|
+
}
|
|
259
|
+
if (taskAction === 'select') {
|
|
260
|
+
const idx = parseInt(message.replace(/\D/g, '')) - 1;
|
|
261
|
+
const result = agentTasks.selectOffer(taskId, idx);
|
|
262
|
+
return res.json({ ...result, type: 'task' });
|
|
263
|
+
}
|
|
264
|
+
if (taskAction === 'cancel') {
|
|
265
|
+
const result = agentTasks.cancelTask(taskId);
|
|
266
|
+
return res.json({ ...result, type: 'task' });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── Check if user wants to select from existing offers ──
|
|
271
|
+
if (!taskId) {
|
|
272
|
+
const selectMatch = message.match(/(?:اختر|اخت(?:ا|ي)ر|select|choose|pick)\s*(\d+)/i);
|
|
273
|
+
if (selectMatch) {
|
|
274
|
+
const tasks = agentTasks.getSessionTasks(sid, 1);
|
|
275
|
+
if (tasks.length > 0 && tasks[0].status === 'presenting') {
|
|
276
|
+
const idx = parseInt(selectMatch[1]) - 1;
|
|
277
|
+
const result = agentTasks.selectOffer(tasks[0].id, idx);
|
|
278
|
+
return res.json({ ...result, type: 'task' });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Detect URL paste — create URL negotiation task ──
|
|
284
|
+
const urlData = agentTasks.parseBookingUrl(message);
|
|
285
|
+
if (urlData) {
|
|
286
|
+
const task = agentTasks.createUrlTask(sid, message, urlData);
|
|
287
|
+
const execResult = await agentTasks.executeUrlTask(task.taskId);
|
|
288
|
+
return res.json({ ...execResult, type: 'task', urlData });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── Detect if this is a task-type request (booking, shopping, etc.) ──
|
|
292
|
+
const intent = agentTasks.detectIntent(message);
|
|
293
|
+
if (intent.confidence >= 0.7 && intent.intent !== 'general') {
|
|
294
|
+
const task = agentTasks.createTask(sid, message);
|
|
295
|
+
|
|
296
|
+
if (task.status === 'clarifying') {
|
|
297
|
+
return res.json({ ...task, type: 'task' });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// If requirements are complete, auto-execute
|
|
301
|
+
const execResult = await agentTasks.executeTask(task.taskId);
|
|
302
|
+
return res.json({ ...execResult, type: 'task' });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── Regular chat (not a task) ──
|
|
306
|
+
const chatContext = {
|
|
307
|
+
url: context?.url || '',
|
|
308
|
+
platform: context?.platform || 'unknown',
|
|
309
|
+
sessionId: sid,
|
|
310
|
+
};
|
|
311
|
+
const result = await agentChat(message, chatContext);
|
|
312
|
+
res.json(result);
|
|
313
|
+
} catch (err) {
|
|
314
|
+
console.error('[agent-chat] Error:', err.message);
|
|
315
|
+
res.json({ reply: '🤖 عذراً، حدث خطأ. حاول مرة أخرى.', type: 'text' });
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Agent task status & history
|
|
320
|
+
app.get('/api/wab/agent-task/:id', chatLimiter, (req, res) => {
|
|
321
|
+
const state = agentTasks.getTaskState(req.params.id);
|
|
322
|
+
if (!state) return res.status(404).json({ error: 'Task not found' });
|
|
323
|
+
res.json(state);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
app.get('/api/wab/agent-tasks', chatLimiter, (req, res) => {
|
|
327
|
+
const sid = req.query.sessionId || req.ip || 'anonymous';
|
|
328
|
+
const tasks = agentTasks.getSessionTasks(sid, 20);
|
|
329
|
+
res.json({ tasks });
|
|
330
|
+
});
|
|
156
331
|
|
|
157
332
|
const pkg = require('../package.json');
|
|
158
333
|
app.use(`/v${pkg.version.split('.')[0]}`, express.static(path.join(__dirname, '..', 'script')));
|
|
@@ -170,6 +345,10 @@ if (process.env.NODE_ENV !== 'test') {
|
|
|
170
345
|
console.log('Running database migrations...');
|
|
171
346
|
runMigrations();
|
|
172
347
|
maybeBootstrapAdmin();
|
|
348
|
+
initSearchEngine(db);
|
|
349
|
+
|
|
350
|
+
// Purge old search cache every hour
|
|
351
|
+
setInterval(purgeOldCache, 60 * 60 * 1000);
|
|
173
352
|
|
|
174
353
|
const server = http.createServer(app);
|
|
175
354
|
setupWebSocket(server);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
const { signAdminToken, verifyAdminToken } = require('../config/secrets');
|
|
2
|
+
const { isJWTRevoked } = require('../services/security');
|
|
2
3
|
|
|
3
4
|
function generateAdminToken(admin) {
|
|
4
5
|
return signAdminToken(
|
|
5
6
|
{ id: admin.id, email: admin.email, name: admin.name, role: admin.role, isAdmin: true },
|
|
6
|
-
{ expiresIn: '
|
|
7
|
+
{ expiresIn: '4h' }
|
|
7
8
|
);
|
|
8
9
|
}
|
|
9
10
|
|
|
@@ -16,11 +17,15 @@ function authenticateAdmin(req, res, next) {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
try {
|
|
20
|
+
if (isJWTRevoked(token)) {
|
|
21
|
+
return res.status(403).json({ error: 'Token has been revoked' });
|
|
22
|
+
}
|
|
19
23
|
const decoded = verifyAdminToken(token);
|
|
20
24
|
if (!decoded.isAdmin) {
|
|
21
25
|
return res.status(403).json({ error: 'Admin privileges required' });
|
|
22
26
|
}
|
|
23
27
|
req.admin = decoded;
|
|
28
|
+
req._rawToken = token;
|
|
24
29
|
next();
|
|
25
30
|
} catch (err) {
|
|
26
31
|
return res.status(403).json({ error: 'Invalid or expired admin token' });
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
const { signUserToken, verifyUserToken } = require('../config/secrets');
|
|
2
|
+
const { isJWTRevoked } = require('../services/security');
|
|
2
3
|
|
|
3
4
|
function generateToken(user) {
|
|
4
5
|
return signUserToken(
|
|
5
6
|
{ id: user.id, email: user.email, name: user.name },
|
|
6
|
-
{ expiresIn: '
|
|
7
|
+
{ expiresIn: '24h' }
|
|
7
8
|
);
|
|
8
9
|
}
|
|
9
10
|
|
|
@@ -16,8 +17,13 @@ function authenticateToken(req, res, next) {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
try {
|
|
20
|
+
// Check revocation list
|
|
21
|
+
if (isJWTRevoked(token)) {
|
|
22
|
+
return res.status(403).json({ error: 'Token has been revoked' });
|
|
23
|
+
}
|
|
19
24
|
const decoded = verifyUserToken(token);
|
|
20
25
|
req.user = decoded;
|
|
26
|
+
req._rawToken = token;
|
|
21
27
|
next();
|
|
22
28
|
} catch (err) {
|
|
23
29
|
return res.status(403).json({ error: 'Invalid or expired token' });
|
|
@@ -30,7 +36,10 @@ function optionalAuth(req, res, next) {
|
|
|
30
36
|
|
|
31
37
|
if (token) {
|
|
32
38
|
try {
|
|
33
|
-
|
|
39
|
+
if (!isJWTRevoked(token)) {
|
|
40
|
+
req.user = verifyUserToken(token);
|
|
41
|
+
req._rawToken = token;
|
|
42
|
+
}
|
|
34
43
|
} catch (e) {
|
|
35
44
|
// ignore invalid tokens for optional auth
|
|
36
45
|
}
|
|
@@ -1,9 +1,75 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Comprehensive rate limits for all security-sensitive endpoints.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const rateLimit = require('express-rate-limit');
|
|
6
6
|
|
|
7
|
+
// ─── Auth endpoints ──────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
const authLimiter = rateLimit({
|
|
10
|
+
windowMs: 15 * 60 * 1000,
|
|
11
|
+
max: 10,
|
|
12
|
+
standardHeaders: true,
|
|
13
|
+
legacyHeaders: false,
|
|
14
|
+
message: { error: 'Too many authentication attempts, please try again later' }
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const registerLimiter = rateLimit({
|
|
18
|
+
windowMs: 60 * 60 * 1000,
|
|
19
|
+
max: 5,
|
|
20
|
+
standardHeaders: true,
|
|
21
|
+
legacyHeaders: false,
|
|
22
|
+
message: { error: 'Too many registration attempts, please try again later' }
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const adminLoginLimiter = rateLimit({
|
|
26
|
+
windowMs: 15 * 60 * 1000,
|
|
27
|
+
max: 5,
|
|
28
|
+
standardHeaders: true,
|
|
29
|
+
legacyHeaders: false,
|
|
30
|
+
message: { error: 'Too many admin login attempts, please try again later' }
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ─── WAB API endpoints ───────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
const wabAuthenticateLimiter = rateLimit({
|
|
36
|
+
windowMs: 15 * 60 * 1000,
|
|
37
|
+
max: 20,
|
|
38
|
+
standardHeaders: true,
|
|
39
|
+
legacyHeaders: false,
|
|
40
|
+
keyGenerator: (req) => `${req.ip}:${req.body?.siteId || req.body?.apiKey || 'anon'}`,
|
|
41
|
+
message: { error: 'Too many WAB authentication attempts' }
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const wabActionLimiter = rateLimit({
|
|
45
|
+
windowMs: 60 * 1000,
|
|
46
|
+
max: 60,
|
|
47
|
+
standardHeaders: true,
|
|
48
|
+
legacyHeaders: false,
|
|
49
|
+
keyGenerator: (req) => `${req.ip}:${req.wabSession?.siteId || 'anon'}`,
|
|
50
|
+
message: { error: 'Too many action requests, please slow down' }
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ─── General API endpoints ───────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
const apiLimiter = rateLimit({
|
|
56
|
+
windowMs: 60 * 1000,
|
|
57
|
+
max: 100,
|
|
58
|
+
standardHeaders: true,
|
|
59
|
+
legacyHeaders: false,
|
|
60
|
+
message: { error: 'Too many requests, please try again later' }
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const searchLimiter = rateLimit({
|
|
64
|
+
windowMs: 60 * 1000,
|
|
65
|
+
max: 30,
|
|
66
|
+
standardHeaders: true,
|
|
67
|
+
legacyHeaders: false,
|
|
68
|
+
message: { error: 'Too many search requests' }
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ─── License endpoints (existing) ────────────────────────────────────
|
|
72
|
+
|
|
7
73
|
const licenseTokenLimiter = rateLimit({
|
|
8
74
|
windowMs: 15 * 60 * 1000,
|
|
9
75
|
max: 30,
|
|
@@ -21,4 +87,14 @@ const licenseTrackLimiter = rateLimit({
|
|
|
21
87
|
message: { error: 'Too many track requests, please try again later' }
|
|
22
88
|
});
|
|
23
89
|
|
|
24
|
-
module.exports = {
|
|
90
|
+
module.exports = {
|
|
91
|
+
authLimiter,
|
|
92
|
+
registerLimiter,
|
|
93
|
+
adminLoginLimiter,
|
|
94
|
+
wabAuthenticateLimiter,
|
|
95
|
+
wabActionLimiter,
|
|
96
|
+
apiLimiter,
|
|
97
|
+
searchLimiter,
|
|
98
|
+
licenseTokenLimiter,
|
|
99
|
+
licenseTrackLimiter,
|
|
100
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
-- Migration 003: Convert ads financial columns from REAL to INTEGER (cents)
|
|
2
|
+
-- This avoids floating-point precision issues in billing calculations.
|
|
3
|
+
--
|
|
4
|
+
-- NOTE: The wab_ads table in db.js now creates with INTEGER columns directly.
|
|
5
|
+
-- This migration only matters for databases created before this change.
|
|
6
|
+
-- On fresh databases, db.js already has the correct schema, so this is a no-op.
|
|
7
|
+
-- On existing databases, this migration was already applied.
|
|
8
|
+
|
|
9
|
+
-- Ensure the table and index exist (idempotent)
|
|
10
|
+
CREATE TABLE IF NOT EXISTS wab_ads (
|
|
11
|
+
id TEXT PRIMARY KEY,
|
|
12
|
+
title TEXT NOT NULL,
|
|
13
|
+
description TEXT,
|
|
14
|
+
image_url TEXT,
|
|
15
|
+
target_url TEXT NOT NULL,
|
|
16
|
+
advertiser_name TEXT NOT NULL,
|
|
17
|
+
advertiser_email TEXT NOT NULL,
|
|
18
|
+
status TEXT DEFAULT 'pending' CHECK(status IN ('pending','approved','rejected','paused','expired')),
|
|
19
|
+
position TEXT DEFAULT 'new-tab' CHECK(position IN ('new-tab','sidebar','search')),
|
|
20
|
+
budget_cents INTEGER DEFAULT 0,
|
|
21
|
+
spent_cents INTEGER DEFAULT 0,
|
|
22
|
+
cpc_cents INTEGER DEFAULT 5,
|
|
23
|
+
cpi_cents INTEGER DEFAULT 1,
|
|
24
|
+
impressions INTEGER DEFAULT 0,
|
|
25
|
+
clicks INTEGER DEFAULT 0,
|
|
26
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
27
|
+
approved_by TEXT,
|
|
28
|
+
approved_at TEXT,
|
|
29
|
+
expires_at TEXT,
|
|
30
|
+
FOREIGN KEY (approved_by) REFERENCES admins(id)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_wab_ads_status ON wab_ads(status);
|