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
@@ -0,0 +1,378 @@
1
+ /**
2
+ * WAB Agent Workspace — Server-side Route & API
3
+ * ════════════════════════════════════════════════════════════════════════
4
+ * Premium workspace endpoints for the 4-panel agent workspace.
5
+ * Handles subscriptions, workspace sessions, agent execution, and admin stats.
6
+ */
7
+
8
+ const express = require('express');
9
+ const router = express.Router();
10
+ const crypto = require('crypto');
11
+ const { authenticateToken } = require('../middleware/auth');
12
+ const { authenticateAdmin } = require('../middleware/adminAuth');
13
+ const { db } = require('../models/db');
14
+
15
+ // ─── Schema ──────────────────────────────────────────────────────────
16
+
17
+ db.exec(`
18
+ CREATE TABLE IF NOT EXISTS workspace_subscriptions (
19
+ id TEXT PRIMARY KEY,
20
+ user_id TEXT NOT NULL,
21
+ plan TEXT NOT NULL DEFAULT 'free' CHECK(plan IN ('free','starter','pro','enterprise')),
22
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','cancelled','expired','suspended')),
23
+ tasks_today INTEGER DEFAULT 0,
24
+ tasks_total INTEGER DEFAULT 0,
25
+ deals_completed INTEGER DEFAULT 0,
26
+ total_savings REAL DEFAULT 0,
27
+ last_task_date TEXT,
28
+ created_at TEXT DEFAULT (datetime('now')),
29
+ expires_at TEXT,
30
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
31
+ );
32
+
33
+ CREATE TABLE IF NOT EXISTS workspace_sessions (
34
+ id TEXT PRIMARY KEY,
35
+ user_id TEXT NOT NULL,
36
+ session_token TEXT NOT NULL,
37
+ active INTEGER DEFAULT 1,
38
+ panels_state TEXT DEFAULT '{}',
39
+ last_activity TEXT DEFAULT (datetime('now')),
40
+ created_at TEXT DEFAULT (datetime('now')),
41
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
42
+ );
43
+
44
+ CREATE TABLE IF NOT EXISTS workspace_deals (
45
+ id TEXT PRIMARY KEY,
46
+ user_id TEXT NOT NULL,
47
+ task_id TEXT,
48
+ offer_source TEXT,
49
+ offer_title TEXT,
50
+ original_price REAL,
51
+ final_price REAL,
52
+ savings REAL DEFAULT 0,
53
+ status TEXT DEFAULT 'presented' CHECK(status IN ('presented','clicked','agent_executing','login_required','completed','failed')),
54
+ deal_url TEXT,
55
+ requires_login INTEGER DEFAULT 0,
56
+ login_method TEXT,
57
+ created_at TEXT DEFAULT (datetime('now')),
58
+ completed_at TEXT,
59
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
60
+ );
61
+
62
+ CREATE TABLE IF NOT EXISTS workspace_analytics (
63
+ id TEXT PRIMARY KEY,
64
+ user_id TEXT,
65
+ event_type TEXT NOT NULL,
66
+ event_data TEXT DEFAULT '{}',
67
+ created_at TEXT DEFAULT (datetime('now'))
68
+ );
69
+
70
+ CREATE INDEX IF NOT EXISTS idx_ws_subs_user ON workspace_subscriptions(user_id);
71
+ CREATE INDEX IF NOT EXISTS idx_ws_sessions_user ON workspace_sessions(user_id);
72
+ CREATE INDEX IF NOT EXISTS idx_ws_deals_user ON workspace_deals(user_id);
73
+ CREATE INDEX IF NOT EXISTS idx_ws_analytics_type ON workspace_analytics(event_type);
74
+ CREATE INDEX IF NOT EXISTS idx_ws_analytics_date ON workspace_analytics(created_at);
75
+ `);
76
+
77
+ // ─── Plan Limits ─────────────────────────────────────────────────────
78
+
79
+ const PLAN_LIMITS = {
80
+ free: { dailyTasks: 5, maxResults: 3, negotiation: false, agentExecute: false },
81
+ starter: { dailyTasks: 30, maxResults: 10, negotiation: true, agentExecute: false },
82
+ pro: { dailyTasks: -1, maxResults: 20, negotiation: true, agentExecute: true },
83
+ enterprise: { dailyTasks: -1, maxResults: 50, negotiation: true, agentExecute: true },
84
+ };
85
+
86
+ // ─── Prepared Statements ─────────────────────────────────────────────
87
+
88
+ const stmts = {
89
+ getSub: db.prepare('SELECT * FROM workspace_subscriptions WHERE user_id = ? AND status = ?'),
90
+ insertSub: db.prepare(`INSERT INTO workspace_subscriptions (id, user_id, plan, status) VALUES (?, ?, ?, 'active')`),
91
+ updateSubPlan: db.prepare('UPDATE workspace_subscriptions SET plan = ?, status = ? WHERE user_id = ? AND status = ?'),
92
+ incrementTasks: db.prepare(`UPDATE workspace_subscriptions SET tasks_today = tasks_today + 1, tasks_total = tasks_total + 1, last_task_date = date('now') WHERE user_id = ? AND status = 'active'`),
93
+ resetDailyTasks: db.prepare(`UPDATE workspace_subscriptions SET tasks_today = 0 WHERE last_task_date < date('now')`),
94
+ addDealSavings: db.prepare(`UPDATE workspace_subscriptions SET deals_completed = deals_completed + 1, total_savings = total_savings + ? WHERE user_id = ? AND status = 'active'`),
95
+
96
+ insertSession: db.prepare('INSERT INTO workspace_sessions (id, user_id, session_token) VALUES (?, ?, ?)'),
97
+ getSession: db.prepare('SELECT * FROM workspace_sessions WHERE session_token = ? AND active = 1'),
98
+ endSession: db.prepare("UPDATE workspace_sessions SET active = 0 WHERE session_token = ?"),
99
+
100
+ insertDeal: db.prepare('INSERT INTO workspace_deals (id, user_id, task_id, offer_source, offer_title, original_price, final_price, savings, deal_url, requires_login) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'),
101
+ updateDealStatus: db.prepare('UPDATE workspace_deals SET status = ?, completed_at = datetime(\'now\') WHERE id = ?'),
102
+ getUserDeals: db.prepare('SELECT * FROM workspace_deals WHERE user_id = ? ORDER BY created_at DESC LIMIT ?'),
103
+
104
+ logEvent: db.prepare('INSERT INTO workspace_analytics (id, user_id, event_type, event_data) VALUES (?, ?, ?, ?)'),
105
+ };
106
+
107
+ // Reset daily task counts
108
+ try { stmts.resetDailyTasks.run(); } catch (_) {}
109
+
110
+ // ─── User Routes ─────────────────────────────────────────────────────
111
+
112
+ /**
113
+ * GET /api/workspace/subscription — Get user's workspace subscription
114
+ */
115
+ router.get('/subscription', authenticateToken, (req, res) => {
116
+ let sub = stmts.getSub.get(req.user.id, 'active');
117
+
118
+ if (!sub) {
119
+ // Auto-create free subscription
120
+ const id = crypto.randomUUID();
121
+ stmts.insertSub.run(id, req.user.id, 'free');
122
+ sub = stmts.getSub.get(req.user.id, 'active');
123
+ }
124
+
125
+ const limits = PLAN_LIMITS[sub.plan] || PLAN_LIMITS.free;
126
+ const remaining = limits.dailyTasks < 0 ? -1 : Math.max(0, limits.dailyTasks - (sub.tasks_today || 0));
127
+
128
+ res.json({
129
+ plan: sub.plan,
130
+ status: sub.status,
131
+ tasksToday: sub.tasks_today,
132
+ tasksTotal: sub.tasks_total,
133
+ dealsCompleted: sub.deals_completed,
134
+ totalSavings: sub.total_savings,
135
+ limits,
136
+ remainingTasks: remaining,
137
+ createdAt: sub.created_at,
138
+ });
139
+ });
140
+
141
+ /**
142
+ * POST /api/workspace/subscription — Upgrade/change plan
143
+ */
144
+ router.post('/subscription', authenticateToken, (req, res) => {
145
+ const { plan } = req.body;
146
+ if (!PLAN_LIMITS[plan]) return res.status(400).json({ error: 'Invalid plan' });
147
+
148
+ let sub = stmts.getSub.get(req.user.id, 'active');
149
+ if (sub) {
150
+ stmts.updateSubPlan.run(plan, 'active', req.user.id, 'active');
151
+ } else {
152
+ const id = crypto.randomUUID();
153
+ stmts.insertSub.run(id, req.user.id, plan);
154
+ }
155
+
156
+ logEvent(req.user.id, 'plan_changed', { plan });
157
+ res.json({ success: true, plan });
158
+ });
159
+
160
+ /**
161
+ * POST /api/workspace/check-limits — Check if user can execute a task
162
+ */
163
+ router.post('/check-limits', authenticateToken, (req, res) => {
164
+ const sub = stmts.getSub.get(req.user.id, 'active');
165
+ if (!sub) return res.json({ allowed: true, plan: 'free' }); // New user
166
+
167
+ const limits = PLAN_LIMITS[sub.plan] || PLAN_LIMITS.free;
168
+ if (limits.dailyTasks >= 0 && sub.tasks_today >= limits.dailyTasks) {
169
+ return res.json({
170
+ allowed: false,
171
+ reason: 'daily_limit',
172
+ plan: sub.plan,
173
+ limit: limits.dailyTasks,
174
+ used: sub.tasks_today,
175
+ });
176
+ }
177
+
178
+ stmts.incrementTasks.run(req.user.id);
179
+ res.json({ allowed: true, plan: sub.plan, remaining: limits.dailyTasks < 0 ? -1 : limits.dailyTasks - sub.tasks_today - 1 });
180
+ });
181
+
182
+ /**
183
+ * POST /api/workspace/deal — Record a deal action
184
+ */
185
+ router.post('/deal', authenticateToken, (req, res) => {
186
+ const { taskId, source, title, originalPrice, finalPrice, url, requiresLogin } = req.body;
187
+ const savings = originalPrice && finalPrice ? originalPrice - finalPrice : 0;
188
+ const id = crypto.randomUUID();
189
+
190
+ stmts.insertDeal.run(id, req.user.id, taskId || null, source || '', title || '', originalPrice || 0, finalPrice || 0, savings, url || '', requiresLogin ? 1 : 0);
191
+ logEvent(req.user.id, 'deal_created', { dealId: id, source, savings });
192
+
193
+ res.json({ dealId: id, savings });
194
+ });
195
+
196
+ /**
197
+ * POST /api/workspace/deal/:id/status — Update deal status
198
+ */
199
+ router.post('/deal/:id/status', authenticateToken, (req, res) => {
200
+ const { status } = req.body;
201
+ const valid = ['clicked', 'agent_executing', 'login_required', 'completed', 'failed'];
202
+ if (!valid.includes(status)) return res.status(400).json({ error: 'Invalid status' });
203
+
204
+ stmts.updateDealStatus.run(status, req.params.id);
205
+
206
+ if (status === 'completed') {
207
+ // Update savings in subscription
208
+ const deal = db.prepare('SELECT savings FROM workspace_deals WHERE id = ?').get(req.params.id);
209
+ if (deal) {
210
+ stmts.addDealSavings.run(deal.savings || 0, req.user.id);
211
+ }
212
+ }
213
+
214
+ logEvent(req.user.id, 'deal_status', { dealId: req.params.id, status });
215
+ res.json({ success: true });
216
+ });
217
+
218
+ /**
219
+ * GET /api/workspace/deals — Get user's deal history
220
+ */
221
+ router.get('/deals', authenticateToken, (req, res) => {
222
+ const limit = Math.min(parseInt(req.query.limit) || 20, 100);
223
+ const deals = stmts.getUserDeals.all(req.user.id, limit);
224
+ res.json({ deals });
225
+ });
226
+
227
+ /**
228
+ * POST /api/workspace/event — Log workspace analytics event
229
+ */
230
+ router.post('/event', authenticateToken, (req, res) => {
231
+ const { type, data } = req.body;
232
+ if (!type) return res.status(400).json({ error: 'Event type required' });
233
+ logEvent(req.user.id, type, data || {});
234
+ res.json({ ok: true });
235
+ });
236
+
237
+ // ─── Admin Routes ────────────────────────────────────────────────────
238
+
239
+ /**
240
+ * GET /api/workspace/admin/stats — Global workspace stats
241
+ */
242
+ router.get('/admin/stats', authenticateAdmin, (req, res) => {
243
+ const totalUsers = db.prepare('SELECT COUNT(*) as c FROM workspace_subscriptions').get()?.c || 0;
244
+ const activeToday = db.prepare("SELECT COUNT(*) as c FROM workspace_subscriptions WHERE last_task_date = date('now')").get()?.c || 0;
245
+ const totalTasks = db.prepare('SELECT SUM(tasks_total) as c FROM workspace_subscriptions').get()?.c || 0;
246
+ const totalDeals = db.prepare('SELECT SUM(deals_completed) as c FROM workspace_subscriptions').get()?.c || 0;
247
+ const totalSavings = db.prepare('SELECT SUM(total_savings) as c FROM workspace_subscriptions').get()?.c || 0;
248
+
249
+ const planBreakdown = db.prepare(`
250
+ SELECT plan, COUNT(*) as count, SUM(tasks_total) as tasks, SUM(total_savings) as savings
251
+ FROM workspace_subscriptions WHERE status = 'active' GROUP BY plan
252
+ `).all();
253
+
254
+ const recentEvents = db.prepare(`
255
+ SELECT event_type, COUNT(*) as count
256
+ FROM workspace_analytics
257
+ WHERE created_at > datetime('now', '-24 hours')
258
+ GROUP BY event_type ORDER BY count DESC LIMIT 10
259
+ `).all();
260
+
261
+ const dailyTasks = db.prepare(`
262
+ SELECT date(created_at) as day, COUNT(*) as count
263
+ FROM workspace_analytics
264
+ WHERE event_type IN ('task_started','deal_created') AND created_at > datetime('now', '-30 days')
265
+ GROUP BY day ORDER BY day
266
+ `).all();
267
+
268
+ res.json({
269
+ totalUsers,
270
+ activeToday,
271
+ totalTasks,
272
+ totalDeals,
273
+ totalSavings,
274
+ planBreakdown,
275
+ recentEvents,
276
+ dailyTasks,
277
+ });
278
+ });
279
+
280
+ /**
281
+ * GET /api/workspace/admin/subscriptions — List all subscriptions
282
+ */
283
+ router.get('/admin/subscriptions', authenticateAdmin, (req, res) => {
284
+ const page = Math.max(1, parseInt(req.query.page) || 1);
285
+ const limit = Math.min(parseInt(req.query.limit) || 50, 200);
286
+ const offset = (page - 1) * limit;
287
+
288
+ const subs = db.prepare(`
289
+ SELECT ws.*, u.email, u.name
290
+ FROM workspace_subscriptions ws
291
+ LEFT JOIN users u ON ws.user_id = u.id
292
+ ORDER BY ws.created_at DESC LIMIT ? OFFSET ?
293
+ `).all(limit, offset);
294
+
295
+ const total = db.prepare('SELECT COUNT(*) as c FROM workspace_subscriptions').get()?.c || 0;
296
+
297
+ res.json({ subscriptions: subs, total, page, pages: Math.ceil(total / limit) });
298
+ });
299
+
300
+ /**
301
+ * PUT /api/workspace/admin/subscription/:userId — Admin update subscription
302
+ */
303
+ router.put('/admin/subscription/:userId', authenticateAdmin, (req, res) => {
304
+ const { plan, status } = req.body;
305
+ if (plan && !PLAN_LIMITS[plan]) return res.status(400).json({ error: 'Invalid plan' });
306
+
307
+ if (plan) {
308
+ stmts.updateSubPlan.run(plan, status || 'active', req.params.userId, 'active');
309
+ }
310
+ if (status && !plan) {
311
+ db.prepare('UPDATE workspace_subscriptions SET status = ? WHERE user_id = ? AND status = ?')
312
+ .run(status, req.params.userId, 'active');
313
+ }
314
+
315
+ res.json({ success: true });
316
+ });
317
+
318
+ /**
319
+ * GET /api/workspace/admin/deals — List all deals
320
+ */
321
+ router.get('/admin/deals', authenticateAdmin, (req, res) => {
322
+ const page = Math.max(1, parseInt(req.query.page) || 1);
323
+ const limit = Math.min(parseInt(req.query.limit) || 50, 200);
324
+ const offset = (page - 1) * limit;
325
+
326
+ const deals = db.prepare(`
327
+ SELECT wd.*, u.email, u.name as user_name
328
+ FROM workspace_deals wd
329
+ LEFT JOIN users u ON wd.user_id = u.id
330
+ ORDER BY wd.created_at DESC LIMIT ? OFFSET ?
331
+ `).all(limit, offset);
332
+
333
+ const total = db.prepare('SELECT COUNT(*) as c FROM workspace_deals').get()?.c || 0;
334
+ const totalSavings = db.prepare('SELECT SUM(savings) as s FROM workspace_deals WHERE status = ?').get('completed')?.s || 0;
335
+
336
+ res.json({ deals, total, totalSavings, page, pages: Math.ceil(total / limit) });
337
+ });
338
+
339
+ /**
340
+ * GET /api/workspace/admin/analytics — Workspace analytics dashboard
341
+ */
342
+ router.get('/admin/analytics', authenticateAdmin, (req, res) => {
343
+ const days = Math.min(parseInt(req.query.days) || 30, 90);
344
+
345
+ const eventsByType = db.prepare(`
346
+ SELECT event_type, COUNT(*) as count
347
+ FROM workspace_analytics
348
+ WHERE created_at > datetime('now', '-${days} days')
349
+ GROUP BY event_type ORDER BY count DESC
350
+ `).all();
351
+
352
+ const dailyActivity = db.prepare(`
353
+ SELECT date(created_at) as day, COUNT(*) as events,
354
+ COUNT(DISTINCT user_id) as unique_users
355
+ FROM workspace_analytics
356
+ WHERE created_at > datetime('now', '-${days} days')
357
+ GROUP BY day ORDER BY day
358
+ `).all();
359
+
360
+ const topUsers = db.prepare(`
361
+ SELECT ws.user_id, u.email, u.name, ws.plan, ws.tasks_total, ws.deals_completed, ws.total_savings
362
+ FROM workspace_subscriptions ws
363
+ LEFT JOIN users u ON ws.user_id = u.id
364
+ ORDER BY ws.tasks_total DESC LIMIT 20
365
+ `).all();
366
+
367
+ res.json({ eventsByType, dailyActivity, topUsers });
368
+ });
369
+
370
+ // ─── Helpers ─────────────────────────────────────────────────────────
371
+
372
+ function logEvent(userId, type, data) {
373
+ try {
374
+ stmts.logEvent.run(crypto.randomUUID(), userId || null, type, JSON.stringify(data));
375
+ } catch (_) {}
376
+ }
377
+
378
+ module.exports = router;
@@ -1,12 +1,17 @@
1
1
  const express = require('express');
2
2
  const router = express.Router();
3
3
  const { authenticateToken } = require('../middleware/auth');
4
+ const { apiLimiter } = require('../middleware/rateLimits');
5
+ const { validateDomain, validateSiteConfig, sanitizeInput, auditLog } = require('../services/security');
4
6
  const {
5
7
  addSite, findSitesByUser, findSiteById,
6
8
  updateSiteConfig, updateSiteTier, deleteSite,
7
9
  getAnalyticsBySite, getAnalyticsTimeline
8
10
  } = require('../models/db');
9
11
 
12
+ // Apply general API rate limit to all routes
13
+ router.use(apiLimiter);
14
+
10
15
  // ─── Sites ──────────────────────────────────────────────────────────────
11
16
 
12
17
  router.get('/sites', authenticateToken, (req, res) => {
@@ -26,8 +31,16 @@ router.post('/sites', authenticateToken, (req, res) => {
26
31
  return res.status(400).json({ error: 'Domain and name are required' });
27
32
  }
28
33
 
34
+ if (!validateDomain(domain)) {
35
+ return res.status(400).json({ error: 'Invalid domain format. Must be a valid hostname (e.g., example.com)' });
36
+ }
37
+
38
+ const cleanName = sanitizeInput(name, 200);
39
+ const cleanDesc = description ? sanitizeInput(description, 500) : undefined;
40
+
29
41
  try {
30
- const site = addSite({ userId: req.user.id, domain, name, description, tier });
42
+ const site = addSite({ userId: req.user.id, domain: domain.toLowerCase().trim(), name: cleanName, description: cleanDesc, tier });
43
+ auditLog({ actorType: 'user', actorId: String(req.user.id), action: 'site_created', resource: 'site', resourceId: String(site.id), ip: req.ip });
31
44
  res.status(201).json({ site });
32
45
  } catch (err) {
33
46
  res.status(500).json({ error: 'Failed to create site' });
@@ -46,11 +59,17 @@ router.put('/sites/:id/config', authenticateToken, (req, res) => {
46
59
  const { config } = req.body;
47
60
  if (!config) return res.status(400).json({ error: 'Config is required' });
48
61
 
62
+ const validation = validateSiteConfig(config);
63
+ if (!validation.valid) {
64
+ return res.status(400).json({ error: validation.error });
65
+ }
66
+
49
67
  try {
50
- const r = updateSiteConfig.run(JSON.stringify(config), req.params.id, req.user.id);
68
+ const r = updateSiteConfig.run(JSON.stringify(validation.config), req.params.id, req.user.id);
51
69
  if (r.changes === 0) {
52
70
  return res.status(404).json({ error: 'Site not found' });
53
71
  }
72
+ auditLog({ actorType: 'user', actorId: String(req.user.id), action: 'site_config_updated', resource: 'site', resourceId: req.params.id, ip: req.ip });
54
73
  res.json({ success: true });
55
74
  } catch (err) {
56
75
  res.status(500).json({ error: 'Failed to update config' });
@@ -2,21 +2,31 @@ const express = require('express');
2
2
  const router = express.Router();
3
3
  const { registerUser, loginUser, findUserById } = require('../models/db');
4
4
  const { generateToken, authenticateToken } = require('../middleware/auth');
5
+ const { authLimiter, registerLimiter } = require('../middleware/rateLimits');
6
+ const { validateEmail, sanitizeInput, auditLog, revokeJWT } = require('../services/security');
5
7
 
6
- router.post('/register', (req, res) => {
8
+ router.post('/register', registerLimiter, (req, res) => {
7
9
  const { email, password, name, company } = req.body;
8
10
 
9
11
  if (!email || !password || !name) {
10
12
  return res.status(400).json({ error: 'Email, password, and name are required' });
11
13
  }
12
14
 
13
- if (password.length < 8) {
14
- return res.status(400).json({ error: 'Password must be at least 8 characters' });
15
+ if (!validateEmail(email)) {
16
+ return res.status(400).json({ error: 'Invalid email format' });
15
17
  }
16
18
 
19
+ if (password.length < 8 || password.length > 128) {
20
+ return res.status(400).json({ error: 'Password must be between 8 and 128 characters' });
21
+ }
22
+
23
+ const cleanName = sanitizeInput(name, 100);
24
+ const cleanCompany = company ? sanitizeInput(company, 100) : undefined;
25
+
17
26
  try {
18
- const user = registerUser({ email, password, name, company });
27
+ const user = registerUser({ email: email.toLowerCase().trim(), password, name: cleanName, company: cleanCompany });
19
28
  const token = generateToken(user);
29
+ auditLog({ actorType: 'user', actorId: String(user.id), action: 'register', ip: req.ip });
20
30
  res.status(201).json({ user, token });
21
31
  } catch (err) {
22
32
  if (err.message.includes('UNIQUE constraint')) {
@@ -26,22 +36,32 @@ router.post('/register', (req, res) => {
26
36
  }
27
37
  });
28
38
 
29
- router.post('/login', (req, res) => {
39
+ router.post('/login', authLimiter, (req, res) => {
30
40
  const { email, password } = req.body;
31
41
 
32
42
  if (!email || !password) {
33
43
  return res.status(400).json({ error: 'Email and password are required' });
34
44
  }
35
45
 
36
- const user = loginUser({ email, password });
46
+ const user = loginUser({ email: email.toLowerCase().trim(), password });
37
47
  if (!user) {
48
+ auditLog({ actorType: 'user', action: 'login_failed', details: { email: email.toLowerCase().trim() }, ip: req.ip, outcome: 'denied', severity: 'warning' });
38
49
  return res.status(401).json({ error: 'Invalid email or password' });
39
50
  }
40
51
 
41
52
  const token = generateToken(user);
53
+ auditLog({ actorType: 'user', actorId: String(user.id), action: 'login', ip: req.ip });
42
54
  res.json({ user, token });
43
55
  });
44
56
 
57
+ router.post('/logout', authenticateToken, (req, res) => {
58
+ if (req._rawToken) {
59
+ revokeJWT(req._rawToken, 'user_logout');
60
+ auditLog({ actorType: 'user', actorId: String(req.user.id), action: 'logout', ip: req.ip });
61
+ }
62
+ res.json({ success: true });
63
+ });
64
+
45
65
  router.get('/me', authenticateToken, (req, res) => {
46
66
  const user = findUserById.get(req.user.id);
47
67
  if (!user) return res.status(404).json({ error: 'User not found' });
@@ -12,6 +12,7 @@ const { authenticateToken } = require('../middleware/auth');
12
12
  const reputation = require('../services/reputation');
13
13
  const negotiation = require('../services/negotiation');
14
14
  const verification = require('../services/verification');
15
+ const priceShield = require('../services/price-shield');
15
16
 
16
17
  // ═══════════════════════════════════════════════════════════════════════
17
18
  // REPUTATION API
@@ -304,4 +305,81 @@ router.get('/dashboard/sovereign', authenticateToken, (req, res) => {
304
305
  res.json(dashboardData);
305
306
  });
306
307
 
308
+ // ═══════════════════════════════════════════════════════════════════════
309
+ // DYNAMIC PRICING SHIELD API
310
+ // ═══════════════════════════════════════════════════════════════════════
311
+
312
+ // Get available identity personas
313
+ router.get('/price-shield/personas', (req, res) => {
314
+ res.json(priceShield.getPersonas());
315
+ });
316
+
317
+ // Create a new price scan
318
+ router.post('/price-shield/scans', (req, res) => {
319
+ const { siteId, url, itemName, category } = req.body;
320
+ if (!url) {
321
+ return res.status(400).json({ error: 'url is required' });
322
+ }
323
+ const result = priceShield.createScan({ siteId, url, itemName, category });
324
+ res.json(result);
325
+ });
326
+
327
+ // Record a probe result for a scan
328
+ router.post('/price-shield/scans/:scanId/probes', (req, res) => {
329
+ const { personaId, priceText, currency, responseHeaders, cookiesReceived, durationMs } = req.body;
330
+ if (!personaId || !priceText) {
331
+ return res.status(400).json({ error: 'personaId and priceText are required' });
332
+ }
333
+ const result = priceShield.recordProbe(req.params.scanId, {
334
+ personaId, priceText, currency, responseHeaders, cookiesReceived, durationMs
335
+ });
336
+ if (result.error) return res.status(400).json(result);
337
+ res.json(result);
338
+ });
339
+
340
+ // Analyze a scan (after probes are recorded)
341
+ router.post('/price-shield/scans/:scanId/analyze', (req, res) => {
342
+ const result = priceShield.analyzeScan(req.params.scanId);
343
+ if (result.error) return res.status(400).json(result);
344
+ res.json(result);
345
+ });
346
+
347
+ // Quick scan — all-in-one (provide probes + get analysis)
348
+ router.post('/price-shield/quick-scan', (req, res) => {
349
+ const { url, itemName, siteId, category, probes } = req.body;
350
+ if (!url || !probes || !Array.isArray(probes) || probes.length < 2) {
351
+ return res.status(400).json({ error: 'url and at least 2 probes are required' });
352
+ }
353
+ const result = priceShield.quickScan({ url, itemName, siteId, category, probes });
354
+ if (result.error) return res.status(400).json(result);
355
+ res.json(result);
356
+ });
357
+
358
+ // Get scan report
359
+ router.get('/price-shield/scans/:scanId', (req, res) => {
360
+ const result = priceShield.getScanReport(req.params.scanId);
361
+ if (result.error) return res.status(404).json(result);
362
+ res.json(result);
363
+ });
364
+
365
+ // Get global price shield statistics
366
+ router.get('/price-shield/stats', (req, res) => {
367
+ res.json(priceShield.getGlobalStats());
368
+ });
369
+
370
+ // Get price history for a URL
371
+ router.get('/price-shield/history', (req, res) => {
372
+ const url = req.query.url;
373
+ if (!url) return res.status(400).json({ error: 'url query parameter is required' });
374
+ const limit = Math.min(parseInt(req.query.limit) || 30, 100);
375
+ res.json(priceShield.getPriceHistory(url, limit));
376
+ });
377
+
378
+ // Get manipulation log for a site
379
+ router.get('/price-shield/manipulations/:siteId', (req, res) => {
380
+ const limit = Math.min(parseInt(req.query.limit) || 20, 100);
381
+ const result = priceShield.getGlobalStats();
382
+ res.json(result.topManipulators.find(m => m.siteId === req.params.siteId) || { siteId: req.params.siteId, incidents: 0 });
383
+ });
384
+
307
385
  module.exports = router;