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.
Files changed (38) hide show
  1. package/README.ar.md +506 -31
  2. package/README.md +574 -47
  3. package/bin/agent-runner.js +10 -1
  4. package/package.json +1 -1
  5. package/public/agent-workspace.html +347 -0
  6. package/public/browser.html +484 -0
  7. package/public/css/agent-workspace.css +1713 -0
  8. package/public/index.html +94 -0
  9. package/public/js/agent-workspace.js +1740 -0
  10. package/sdk/index.d.ts +83 -0
  11. package/sdk/index.js +115 -1
  12. package/sdk/package.json +1 -1
  13. package/server/config/secrets.js +13 -5
  14. package/server/index.js +183 -4
  15. package/server/middleware/adminAuth.js +6 -1
  16. package/server/middleware/auth.js +11 -2
  17. package/server/middleware/rateLimits.js +78 -2
  18. package/server/migrations/003_ads_integer_cents.sql +33 -0
  19. package/server/models/db.js +126 -25
  20. package/server/routes/admin.js +16 -2
  21. package/server/routes/ads.js +130 -0
  22. package/server/routes/agent-workspace.js +378 -0
  23. package/server/routes/api.js +21 -2
  24. package/server/routes/auth.js +26 -6
  25. package/server/routes/sovereign.js +78 -0
  26. package/server/routes/universal.js +177 -0
  27. package/server/routes/wab-api.js +20 -5
  28. package/server/services/agent-chat.js +506 -0
  29. package/server/services/agent-symphony.js +6 -0
  30. package/server/services/agent-tasks.js +1807 -0
  31. package/server/services/fairness-engine.js +409 -0
  32. package/server/services/plugins.js +27 -3
  33. package/server/services/price-intelligence.js +565 -0
  34. package/server/services/price-shield.js +1137 -0
  35. package/server/services/search-engine.js +357 -0
  36. package/server/services/security.js +513 -0
  37. package/server/services/universal-scraper.js +661 -0
  38. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "web-agent-bridge-sdk",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "SDK for building AI agents that interact with Web Agent Bridge (WAB)",
5
5
  "main": "index.js",
6
6
  "license": "MIT",
@@ -33,10 +33,12 @@ function isProd() {
33
33
  function assertSecretsAtStartup() {
34
34
  if (isTest()) return;
35
35
  if (isProd() && !process.env.JWT_SECRET) {
36
- _autoUserSecret = generateAutoSecret('JWT_SECRET');
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
- _autoAdminSecret = generateAutoSecret('JWT_SECRET_ADMIN');
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
- return process.env.JWT_SECRET || _autoUserSecret || 'dev-user-secret-change-in-development';
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 || process.env.JWT_SECRET || 'test-secret-key-for-testing-admin';
57
+ return process.env.JWT_SECRET_ADMIN || 'test-secret-key-for-testing-admin';
53
58
  }
54
- return process.env.JWT_SECRET_ADMIN || process.env.JWT_SECRET || _autoAdminSecret || _autoUserSecret || 'dev-admin-secret-change-in-development';
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: ["'none'"],
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: '12h' }
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: '7d' }
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
- req.user = verifyUserToken(token);
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
- * Stricter rate limits for license token / track endpoints (used inside license router).
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 = { licenseTokenLimiter, licenseTrackLimiter };
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);