web-agent-bridge 2.3.1 → 2.5.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 (53) hide show
  1. package/README.ar.md +524 -31
  2. package/README.md +592 -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 +253 -0
  11. package/sdk/index.js +360 -1
  12. package/sdk/package.json +1 -1
  13. package/server/config/secrets.js +13 -5
  14. package/server/control-plane/index.js +301 -0
  15. package/server/data-plane/index.js +354 -0
  16. package/server/index.js +185 -4
  17. package/server/llm/index.js +404 -0
  18. package/server/middleware/adminAuth.js +6 -1
  19. package/server/middleware/auth.js +11 -2
  20. package/server/middleware/rateLimits.js +78 -2
  21. package/server/migrations/003_ads_integer_cents.sql +33 -0
  22. package/server/models/db.js +126 -25
  23. package/server/observability/index.js +394 -0
  24. package/server/protocol/capabilities.js +223 -0
  25. package/server/protocol/index.js +243 -0
  26. package/server/protocol/schema.js +584 -0
  27. package/server/registry/index.js +326 -0
  28. package/server/routes/admin.js +16 -2
  29. package/server/routes/ads.js +130 -0
  30. package/server/routes/agent-workspace.js +378 -0
  31. package/server/routes/api.js +21 -2
  32. package/server/routes/auth.js +26 -6
  33. package/server/routes/runtime.js +725 -0
  34. package/server/routes/sovereign.js +78 -0
  35. package/server/routes/universal.js +177 -0
  36. package/server/routes/wab-api.js +20 -5
  37. package/server/runtime/event-bus.js +210 -0
  38. package/server/runtime/index.js +233 -0
  39. package/server/runtime/sandbox.js +266 -0
  40. package/server/runtime/scheduler.js +395 -0
  41. package/server/runtime/state-manager.js +188 -0
  42. package/server/security/index.js +355 -0
  43. package/server/services/agent-chat.js +506 -0
  44. package/server/services/agent-symphony.js +6 -0
  45. package/server/services/agent-tasks.js +1807 -0
  46. package/server/services/fairness-engine.js +409 -0
  47. package/server/services/plugins.js +27 -3
  48. package/server/services/price-intelligence.js +565 -0
  49. package/server/services/price-shield.js +1137 -0
  50. package/server/services/search-engine.js +357 -0
  51. package/server/services/security.js +513 -0
  52. package/server/services/universal-scraper.js +661 -0
  53. package/server/ws.js +61 -1
@@ -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);
@@ -168,6 +168,43 @@ db.exec(`
168
168
  CREATE INDEX IF NOT EXISTS idx_stripe_subs_user ON stripe_subscriptions(user_id);
169
169
  CREATE INDEX IF NOT EXISTS idx_payments_user ON payments(user_id);
170
170
  CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications_log(user_id);
171
+
172
+ CREATE TABLE IF NOT EXISTS wab_ads (
173
+ id TEXT PRIMARY KEY,
174
+ title TEXT NOT NULL,
175
+ description TEXT,
176
+ image_url TEXT,
177
+ target_url TEXT NOT NULL,
178
+ advertiser_name TEXT NOT NULL,
179
+ advertiser_email TEXT NOT NULL,
180
+ status TEXT DEFAULT 'pending' CHECK(status IN ('pending','approved','rejected','paused','expired')),
181
+ position TEXT DEFAULT 'new-tab' CHECK(position IN ('new-tab','sidebar','search')),
182
+ budget_cents INTEGER DEFAULT 0,
183
+ spent_cents INTEGER DEFAULT 0,
184
+ cpc_cents INTEGER DEFAULT 5,
185
+ cpi_cents INTEGER DEFAULT 1,
186
+ impressions INTEGER DEFAULT 0,
187
+ clicks INTEGER DEFAULT 0,
188
+ created_at TEXT DEFAULT (datetime('now')),
189
+ approved_by TEXT,
190
+ approved_at TEXT,
191
+ expires_at TEXT,
192
+ FOREIGN KEY (approved_by) REFERENCES admins(id)
193
+ );
194
+
195
+ CREATE TABLE IF NOT EXISTS ad_events (
196
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
197
+ ad_id TEXT NOT NULL,
198
+ event_type TEXT NOT NULL CHECK(event_type IN ('impression','click')),
199
+ platform TEXT DEFAULT 'browser',
200
+ ip_hash TEXT,
201
+ created_at TEXT DEFAULT (datetime('now')),
202
+ FOREIGN KEY (ad_id) REFERENCES wab_ads(id) ON DELETE CASCADE
203
+ );
204
+
205
+ CREATE INDEX IF NOT EXISTS idx_wab_ads_status ON wab_ads(status);
206
+ CREATE INDEX IF NOT EXISTS idx_ad_events_ad ON ad_events(ad_id);
207
+ CREATE INDEX IF NOT EXISTS idx_ad_events_created ON ad_events(created_at);
171
208
  `);
172
209
 
173
210
  function generateLicenseKey() {
@@ -218,6 +255,7 @@ const findSitesByUser = db.prepare(`SELECT * FROM sites WHERE user_id = ? ORDER
218
255
  const findSiteById = db.prepare(`SELECT * FROM sites WHERE id = ?`);
219
256
  const findSiteByLicense = db.prepare(`SELECT * FROM sites WHERE license_key = ? AND active = 1`);
220
257
  const findSiteByDomainAndLicense = db.prepare(`SELECT * FROM sites WHERE domain = ? AND license_key = ? AND active = 1`);
258
+ const findSiteByDomain = db.prepare(`SELECT * FROM sites WHERE domain = ? AND active = 1 LIMIT 1`);
221
259
  const updateSiteConfig = db.prepare(`UPDATE sites SET config = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?`);
222
260
  const updateSiteTier = db.prepare(`UPDATE sites SET tier = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?`);
223
261
  const deleteSite = db.prepare(`UPDATE sites SET active = 0, updated_at = datetime('now') WHERE id = ? AND user_id = ?`);
@@ -287,36 +325,18 @@ function verifyLicense(domain, licenseKey) {
287
325
  }
288
326
 
289
327
  // ─── Admin Operations ─────────────────────────────────────────────────
290
- function normalizeAdminEmail(email) {
291
- if (email == null) return '';
292
- return String(email).trim().toLowerCase();
293
- }
294
-
295
328
  function createAdmin({ email, password, name, role }) {
296
- const normEmail = normalizeAdminEmail(email);
297
- if (!normEmail) throw new Error('Admin email required');
298
329
  const id = uuidv4();
299
330
  const hashed = bcrypt.hashSync(password, 12);
300
- db.prepare(`INSERT INTO admins (id, email, password, name, role) VALUES (?, ?, ?, ?, ?)`).run(id, normEmail, hashed, name, role || 'admin');
301
- return { id, email: normEmail, name, role: role || 'admin' };
331
+ db.prepare(`INSERT INTO admins (id, email, password, name, role) VALUES (?, ?, ?, ?, ?)`).run(id, email, hashed, name, role || 'admin');
332
+ return { id, email, name, role: role || 'admin' };
302
333
  }
303
334
 
304
335
  function loginAdmin({ email, password }) {
305
- const normEmail = normalizeAdminEmail(email);
306
- if (!normEmail || password == null || password === '') return null;
307
- const admin = db.prepare(`SELECT * FROM admins WHERE LOWER(TRIM(email)) = ?`).get(normEmail);
336
+ const admin = db.prepare(`SELECT * FROM admins WHERE email = ?`).get(email);
308
337
  if (!admin) return null;
309
338
  if (!bcrypt.compareSync(password, admin.password)) return null;
310
- return { id: admin.id, email: normEmail, name: admin.name, role: admin.role };
311
- }
312
-
313
- /** CLI / ops only: set password for an existing admin row by email. */
314
- function resetAdminPassword(email, newPassword) {
315
- const normEmail = normalizeAdminEmail(email);
316
- if (!normEmail) return false;
317
- const hashed = bcrypt.hashSync(newPassword, 12);
318
- const r = db.prepare(`UPDATE admins SET password = ? WHERE LOWER(TRIM(email)) = ?`).run(hashed, normEmail);
319
- return r.changes > 0;
339
+ return { id: admin.id, email: admin.email, name: admin.name, role: admin.role };
320
340
  }
321
341
 
322
342
  function findAdminById(id) {
@@ -331,7 +351,7 @@ function maybeBootstrapAdmin() {
331
351
  if (isTest) return;
332
352
  const count = db.prepare(`SELECT COUNT(*) as c FROM admins`).get().c;
333
353
  if (count > 0) return;
334
- const email = normalizeAdminEmail(process.env.BOOTSTRAP_ADMIN_EMAIL);
354
+ const email = process.env.BOOTSTRAP_ADMIN_EMAIL;
335
355
  const password = process.env.BOOTSTRAP_ADMIN_PASSWORD;
336
356
  if (!email || !password) {
337
357
  console.warn('[WAB] No admin accounts. Set BOOTSTRAP_ADMIN_EMAIL and BOOTSTRAP_ADMIN_PASSWORD for first boot, or run: node scripts/create-admin.js <email> <password>');
@@ -524,6 +544,77 @@ function setPlatformSetting(key, value) {
524
544
  db.prepare(`INSERT OR REPLACE INTO platform_settings (key, value, updated_at) VALUES (?, ?, datetime('now'))`).run(key, value);
525
545
  }
526
546
 
547
+ // ─── Ads Operations ──────────────────────────────────────────────────
548
+ function submitAd({ title, description, imageUrl, targetUrl, advertiserName, advertiserEmail, position, budgetCents, cpcCents, cpiCents, expiresAt }) {
549
+ const id = uuidv4();
550
+ db.prepare(`INSERT INTO wab_ads (id, title, description, image_url, target_url, advertiser_name, advertiser_email, position, budget_cents, cpc_cents, cpi_cents, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, title, description || '', imageUrl || '', targetUrl, advertiserName, advertiserEmail, position || 'new-tab', budgetCents || 0, cpcCents || 5, cpiCents || 1, expiresAt || null);
551
+ return { id, title, advertiserName, status: 'pending' };
552
+ }
553
+
554
+ function getActiveAds(position) {
555
+ let q = `SELECT id, title, description, image_url, target_url, advertiser_name, position FROM wab_ads WHERE status = 'approved' AND (expires_at IS NULL OR expires_at > datetime('now')) AND (budget_cents <= 0 OR spent_cents < budget_cents)`;
556
+ const params = [];
557
+ if (position) { q += ` AND position = ?`; params.push(position); }
558
+ q += ` ORDER BY created_at DESC LIMIT 10`;
559
+ return db.prepare(q).all(...params);
560
+ }
561
+
562
+ function getAllAds() {
563
+ return db.prepare(`SELECT * FROM wab_ads ORDER BY created_at DESC`).all();
564
+ }
565
+
566
+ function getPendingAds() {
567
+ return db.prepare(`SELECT * FROM wab_ads WHERE status = 'pending' ORDER BY created_at ASC`).all();
568
+ }
569
+
570
+ function getAdById(id) {
571
+ return db.prepare(`SELECT * FROM wab_ads WHERE id = ?`).get(id);
572
+ }
573
+
574
+ function updateAdStatus(id, status, adminId) {
575
+ const sets = ['status = ?'];
576
+ const params = [status];
577
+ if (status === 'approved') {
578
+ sets.push('approved_by = ?', 'approved_at = datetime(\'now\')');
579
+ params.push(adminId);
580
+ }
581
+ params.push(id);
582
+ db.prepare(`UPDATE wab_ads SET ${sets.join(', ')} WHERE id = ?`).run(...params);
583
+ }
584
+
585
+ function deleteAd(id) {
586
+ db.prepare(`DELETE FROM ad_events WHERE ad_id = ?`).run(id);
587
+ db.prepare(`DELETE FROM wab_ads WHERE id = ?`).run(id);
588
+ }
589
+
590
+ function recordAdEvent(adId, eventType, ipHash) {
591
+ // Deduplicate: skip if same ip+ad+event in last 60s
592
+ const recent = db.prepare(`SELECT 1 FROM ad_events WHERE ad_id = ? AND event_type = ? AND ip_hash = ? AND created_at > datetime('now', '-60 seconds') LIMIT 1`).get(adId, eventType, ipHash || '');
593
+ if (recent) return;
594
+ db.prepare(`INSERT INTO ad_events (ad_id, event_type, ip_hash) VALUES (?, ?, ?)`).run(adId, eventType, ipHash || null);
595
+ if (eventType === 'click') {
596
+ const ad = db.prepare(`SELECT cpc_cents FROM wab_ads WHERE id = ?`).get(adId);
597
+ if (ad) {
598
+ db.prepare(`UPDATE wab_ads SET clicks = clicks + 1, spent_cents = spent_cents + ? WHERE id = ?`).run(ad.cpc_cents, adId);
599
+ }
600
+ } else {
601
+ const ad = db.prepare(`SELECT cpi_cents FROM wab_ads WHERE id = ?`).get(adId);
602
+ if (ad) {
603
+ db.prepare(`UPDATE wab_ads SET impressions = impressions + 1, spent_cents = spent_cents + ? WHERE id = ?`).run(ad.cpi_cents, adId);
604
+ }
605
+ }
606
+ }
607
+
608
+ function getAdStats() {
609
+ const total = db.prepare(`SELECT COUNT(*) as c FROM wab_ads`).get().c;
610
+ const pending = db.prepare(`SELECT COUNT(*) as c FROM wab_ads WHERE status = 'pending'`).get().c;
611
+ const approved = db.prepare(`SELECT COUNT(*) as c FROM wab_ads WHERE status = 'approved'`).get().c;
612
+ const totalImpressions = db.prepare(`SELECT COALESCE(SUM(impressions), 0) as c FROM wab_ads`).get().c;
613
+ const totalClicks = db.prepare(`SELECT COALESCE(SUM(clicks), 0) as c FROM wab_ads`).get().c;
614
+ const totalRevenueCents = db.prepare(`SELECT COALESCE(SUM(spent_cents), 0) as c FROM wab_ads`).get().c;
615
+ return { total, pending, approved, totalImpressions, totalClicks, totalRevenueCents };
616
+ }
617
+
527
618
  module.exports = {
528
619
  db,
529
620
  registerUser,
@@ -534,6 +625,7 @@ module.exports = {
534
625
  findSitesByUser,
535
626
  findSiteById,
536
627
  findSiteByLicense,
628
+ findSiteByDomain,
537
629
  updateSiteConfig,
538
630
  updateSiteTier,
539
631
  deleteSite,
@@ -546,7 +638,6 @@ module.exports = {
546
638
  // Admin
547
639
  createAdmin,
548
640
  loginAdmin,
549
- resetAdminPassword,
550
641
  findAdminById,
551
642
  maybeBootstrapAdmin,
552
643
  getAllUsers,
@@ -576,5 +667,15 @@ module.exports = {
576
667
  getNotificationLogs,
577
668
  // Platform
578
669
  getPlatformSetting,
579
- setPlatformSetting
670
+ setPlatformSetting,
671
+ // Ads
672
+ submitAd,
673
+ getActiveAds,
674
+ getAllAds,
675
+ getPendingAds,
676
+ getAdById,
677
+ updateAdStatus,
678
+ deleteAd,
679
+ recordAdEvent,
680
+ getAdStats
580
681
  };