web-agent-bridge 3.17.0 → 3.20.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 (58) hide show
  1. package/README.ar.md +27 -8
  2. package/README.md +95 -0
  3. package/bin/wab-init.js +38 -0
  4. package/package.json +1 -1
  5. package/public/atp-semantics.html +216 -0
  6. package/public/benchmarks.html +151 -0
  7. package/public/docs.html +113 -43
  8. package/public/index.html +142 -8
  9. package/public/key-rotation.html +184 -0
  10. package/public/llms.txt +54 -0
  11. package/public/notary.html +94 -0
  12. package/public/observatory.html +103 -0
  13. package/public/research.html +57 -0
  14. package/public/researchers.html +113 -0
  15. package/public/responsible-disclosure.html +294 -0
  16. package/public/robots.txt +17 -0
  17. package/public/security.html +157 -0
  18. package/public/threat-model.html +153 -0
  19. package/public/viral-coefficient.html +533 -0
  20. package/public/wab-dataset.html +501 -0
  21. package/public/wab-email.html +78 -0
  22. package/public/wab-lens.html +61 -0
  23. package/public/wab-p2p.html +96 -0
  24. package/public/wab-registry.html +481 -0
  25. package/public/wab-today.html +448 -0
  26. package/public/wab-uri.html +88 -0
  27. package/script/ai-agent-bridge.js +24 -4
  28. package/server/index.js +1193 -827
  29. package/server/models/db.js +2 -1
  30. package/server/routes/admin-shieldlink.js +1 -1
  31. package/server/routes/admin-shieldqr.js +1 -1
  32. package/server/routes/admin-trust-monitor.js +1 -1
  33. package/server/routes/api-keys.js +2 -1
  34. package/server/routes/customer-shieldlink.js +1 -1
  35. package/server/routes/enterprise-mesh.js +2 -1
  36. package/server/routes/genius-bridge.js +256 -0
  37. package/server/routes/genius-gateway.js +137 -0
  38. package/server/routes/governance-saas.js +2 -1
  39. package/server/routes/notary.js +309 -0
  40. package/server/routes/observatory.js +109 -0
  41. package/server/routes/partners.js +2 -1
  42. package/server/routes/registry.js +352 -0
  43. package/server/routes/research.js +83 -0
  44. package/server/routes/ring4.js +2 -1
  45. package/server/routes/runtime.js +98 -25
  46. package/server/routes/security-researchers.js +161 -0
  47. package/server/routes/shieldqr.js +1 -1
  48. package/server/routes/traces.js +247 -0
  49. package/server/services/agent-tasks.js +9 -7
  50. package/server/services/email.js +50 -2
  51. package/server/services/marketplace.js +27 -8
  52. package/server/services/plans.js +1 -1
  53. package/server/services/shieldlink.js +1 -1
  54. package/server/services/ssl-ct-monitor.js +1 -1
  55. package/server/services/ssl-monitor.js +1 -1
  56. package/server/services/stripe.js +29 -4
  57. package/server/utils/migrate.js +1 -1
  58. package/server/utils/safe-compare.js +26 -0
@@ -16,6 +16,7 @@
16
16
 
17
17
  const crypto = require('crypto');
18
18
  const { db } = require('../models/db');
19
+ const { safeFetch } = require('../utils/safe-fetch');
19
20
 
20
21
  // ─── Schema ──────────────────────────────────────────────────────────
21
22
 
@@ -362,21 +363,22 @@ async function executeUrlTask(taskId) {
362
363
  _addMessage(taskId, 'agent', analyzeMsg, { type: 'progress', step: 'analyze' });
363
364
  updates.push({ type: 'progress', step: 'analyze', message: analyzeMsg });
364
365
 
365
- // Try to fetch the original page to get the current price
366
+ // Try to fetch the original page to get the current price.
367
+ // SSRF-hardened: safeFetch blocks private/internal IPs, caps body size,
368
+ // enforces a timeout, and re-validates every redirect hop.
366
369
  let originalPrice = null;
367
370
  try {
368
- const controller = new AbortController();
369
- const timeout = setTimeout(() => controller.abort(), 12000);
370
- const res = await fetch(urlData.url, {
371
+ const res = await safeFetch(urlData.url, {
371
372
  headers: {
372
373
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
373
374
  'Accept': 'text/html',
374
375
  'Accept-Language': 'ar,en-US;q=0.9,en;q=0.8',
375
376
  },
376
- signal: controller.signal,
377
- redirect: 'follow',
377
+ }, {
378
+ timeoutMs: 12000,
379
+ maxBytes: 2 * 1024 * 1024,
380
+ allowedContentTypes: ['text/html', 'application/xhtml+xml'],
378
381
  });
379
- clearTimeout(timeout);
380
382
 
381
383
  if (res.ok) {
382
384
  const html = await res.text();
@@ -4,8 +4,48 @@
4
4
  */
5
5
 
6
6
  const nodemailer = require('nodemailer');
7
+ const crypto = require('crypto');
7
8
  const { getSmtpSettings, logNotification } = require('../models/db');
8
9
 
10
+ // WAB email headers (RFC 5322 X-* extension headers) — let receiving agents
11
+ // verify that a transactional email came from a WAB-enabled domain. Spec at
12
+ // https://webagentbridge.com/wab-email. Signing is opt-in via env:
13
+ // WAB_EMAIL_HOST — the domain that owns the wab.json manifest
14
+ // WAB_EMAIL_SIGNING_KEY — base64 Ed25519 private key (32 bytes raw)
15
+ const WAB_EMAIL_HOST = process.env.WAB_EMAIL_HOST || 'webagentbridge.com';
16
+ const WAB_EMAIL_KEY = process.env.WAB_EMAIL_SIGNING_KEY || '';
17
+ let _wabSigningKey = null;
18
+ function _getSigningKey() {
19
+ if (_wabSigningKey !== null) return _wabSigningKey;
20
+ if (!WAB_EMAIL_KEY) { _wabSigningKey = false; return false; }
21
+ try {
22
+ const raw = Buffer.from(WAB_EMAIL_KEY, 'base64');
23
+ if (raw.length !== 32) throw new Error('expected 32-byte ed25519 seed');
24
+ const der = Buffer.concat([Buffer.from('302e020100300506032b657004220420', 'hex'), raw]);
25
+ _wabSigningKey = crypto.createPrivateKey({ key: der, format: 'der', type: 'pkcs8' });
26
+ } catch (e) {
27
+ console.warn('[wab-email] signing disabled:', e.message);
28
+ _wabSigningKey = false;
29
+ }
30
+ return _wabSigningKey;
31
+ }
32
+ function buildWabHeaders({ from, to, subject, template, messageId, receipt }) {
33
+ const headers = {
34
+ 'X-WAB-Manifest': `https://${WAB_EMAIL_HOST}/.well-known/wab.json`,
35
+ 'X-WAB-Action': `email.${template || 'transactional'}`
36
+ };
37
+ if (receipt) headers['X-WAB-Receipt'] = receipt;
38
+ const key = _getSigningKey();
39
+ if (key) {
40
+ const digest = crypto.createHash('sha256')
41
+ .update(`${from}\n${to}\n${subject}\n${messageId}`)
42
+ .digest();
43
+ const sig = crypto.sign(null, digest, key).toString('base64');
44
+ headers['X-WAB-Signature'] = `ed25519=${sig}`;
45
+ }
46
+ return headers;
47
+ }
48
+
9
49
  function escapeHtml(s) {
10
50
  if (s == null) return '';
11
51
  return String(s)
@@ -269,11 +309,19 @@ async function sendEmail({ to, template, data, userId }) {
269
309
  const { subject, html } = tmpl(data);
270
310
 
271
311
  try {
312
+ const from = `"${settings.from_name}" <${settings.from_email}>`;
313
+ const messageId = `<${crypto.randomBytes(12).toString('hex')}@${WAB_EMAIL_HOST}>`;
314
+ const wabHeaders = buildWabHeaders({
315
+ from: settings.from_email, to, subject, template,
316
+ messageId, receipt: data && data.wab_receipt
317
+ });
272
318
  await transport.sendMail({
273
- from: `"${settings.from_name}" <${settings.from_email}>`,
319
+ from,
274
320
  to,
275
321
  subject,
276
- html
322
+ html,
323
+ messageId,
324
+ headers: wabHeaders
277
325
  });
278
326
  logNotification({ userId, emailTo: to, template, subject, status: 'sent' });
279
327
  return { success: true };
@@ -125,29 +125,48 @@ class MarketplaceEngine {
125
125
  };
126
126
 
127
127
  this._purchases.set(purchaseId, purchase);
128
- listing.installs++;
129
128
 
130
- // Track earnings
131
- if (sellerEarning > 0) {
132
- const earnings = this._earnings.get(listing.sellerId) || { total: 0, pending: 0, paid: 0 };
133
- earnings.total += sellerEarning;
134
- earnings.pending += sellerEarning;
135
- this._earnings.set(listing.sellerId, earnings);
129
+ // Only credit installs / revenue / seller earnings once the purchase is
130
+ // actually completed. For paid listings the purchase starts in
131
+ // 'pending_payment' and must be promoted via completePayment(). Crediting
132
+ // here would let any caller who can reach POST /marketplace/:id/purchase
133
+ // inflate a seller's earnings without ever paying.
134
+ if (purchase.status === 'completed') {
135
+ this._creditCompletedPurchase(listing, purchase);
136
136
  }
137
137
 
138
- listing.revenue += listing.price;
139
138
  bus.emit('marketplace.purchased', { purchaseId, listingId, buyerId, price: listing.price });
140
139
  return purchase;
141
140
  }
142
141
 
142
+ /**
143
+ * Credit installs / revenue / seller earnings for a completed purchase.
144
+ * Idempotent guard: stamps purchase._credited so a double-call is a no-op.
145
+ */
146
+ _creditCompletedPurchase(listing, purchase) {
147
+ if (purchase._credited) return;
148
+ listing.installs++;
149
+ listing.revenue += purchase.price;
150
+ if (purchase.sellerEarning > 0) {
151
+ const earnings = this._earnings.get(listing.sellerId) || { total: 0, pending: 0, paid: 0 };
152
+ earnings.total += purchase.sellerEarning;
153
+ earnings.pending += purchase.sellerEarning;
154
+ this._earnings.set(listing.sellerId, earnings);
155
+ }
156
+ purchase._credited = true;
157
+ }
158
+
143
159
  /**
144
160
  * Complete payment for a purchase
145
161
  */
146
162
  completePayment(purchaseId) {
147
163
  const purchase = this._purchases.get(purchaseId);
148
164
  if (!purchase) throw new Error('Purchase not found');
165
+ if (purchase.status === 'completed') return purchase;
149
166
  purchase.status = 'completed';
150
167
  purchase.completedAt = Date.now();
168
+ const listing = this._listings.get(purchase.listingId);
169
+ if (listing) this._creditCompletedPurchase(listing, purchase);
151
170
  return purchase;
152
171
  }
153
172
 
@@ -24,7 +24,7 @@ function db() {
24
24
  const DATA_DIR = process.env.NODE_ENV === 'test'
25
25
  ? path.join(__dirname, '..', '..', 'data-test')
26
26
  : (process.env.DATA_DIR || path.join(__dirname, '..', '..', 'data'));
27
- const dbFile = process.env.NODE_ENV === 'test' ? 'wab-test.db' : 'wab.db';
27
+ const dbFile = process.env.NODE_ENV === 'test' ? `wab-test-${process.env.JEST_WORKER_ID || '1'}.db` : 'wab.db';
28
28
  _db = new Database(path.join(DATA_DIR, dbFile));
29
29
  return _db;
30
30
  }
@@ -40,7 +40,7 @@ const { encryptOptional, decryptOptional } = require('../utils/secureFields');
40
40
  const DATA_DIR = process.env.NODE_ENV === 'test'
41
41
  ? path.join(__dirname, '..', '..', 'data-test')
42
42
  : (process.env.DATA_DIR || path.join(__dirname, '..', '..', 'data'));
43
- const DB_FILE = process.env.NODE_ENV === 'test' ? 'wab-test.db' : 'wab.db';
43
+ const DB_FILE = process.env.NODE_ENV === 'test' ? `wab-test-${process.env.JEST_WORKER_ID || '1'}.db` : 'wab.db';
44
44
  let _db = null;
45
45
  function db() {
46
46
  if (!_db) _db = new Database(path.join(DATA_DIR, DB_FILE));
@@ -21,7 +21,7 @@ const Database = require('better-sqlite3');
21
21
  const DATA_DIR = process.env.NODE_ENV === 'test'
22
22
  ? path.join(__dirname, '..', '..', 'data-test')
23
23
  : (process.env.DATA_DIR || path.join(__dirname, '..', '..', 'data'));
24
- const DB_FILE = process.env.NODE_ENV === 'test' ? 'wab-test.db' : 'wab.db';
24
+ const DB_FILE = process.env.NODE_ENV === 'test' ? `wab-test-${process.env.JEST_WORKER_ID || '1'}.db` : 'wab.db';
25
25
 
26
26
  let _db = null;
27
27
  function db() {
@@ -17,7 +17,7 @@ const ssl = require('./ssl-inspector');
17
17
  const DATA_DIR = process.env.NODE_ENV === 'test'
18
18
  ? path.join(__dirname, '..', '..', 'data-test')
19
19
  : (process.env.DATA_DIR || path.join(__dirname, '..', '..', 'data'));
20
- const DB_FILE = process.env.NODE_ENV === 'test' ? 'wab-test.db' : 'wab.db';
20
+ const DB_FILE = process.env.NODE_ENV === 'test' ? `wab-test-${process.env.JEST_WORKER_ID || '1'}.db` : 'wab.db';
21
21
 
22
22
  let _db = null;
23
23
  function db() {
@@ -26,10 +26,11 @@ function getStripe() {
26
26
 
27
27
  function getStripePrices() {
28
28
  return {
29
- starter: process.env.STRIPE_PRICE_STARTER || getPlatformSetting('stripe_price_starter'),
30
- pro: process.env.STRIPE_PRICE_PRO || getPlatformSetting('stripe_price_pro'),
31
- business: process.env.STRIPE_PRICE_BUSINESS || getPlatformSetting('stripe_price_business'),
32
- enterprise: process.env.STRIPE_PRICE_ENTERPRISE || getPlatformSetting('stripe_price_enterprise')
29
+ starter: process.env.STRIPE_PRICE_STARTER || getPlatformSetting('stripe_price_starter'),
30
+ freelancer: process.env.STRIPE_PRICE_FREELANCER || getPlatformSetting('stripe_price_freelancer'),
31
+ pro: process.env.STRIPE_PRICE_PRO || getPlatformSetting('stripe_price_pro'),
32
+ business: process.env.STRIPE_PRICE_BUSINESS || getPlatformSetting('stripe_price_business'),
33
+ enterprise: process.env.STRIPE_PRICE_ENTERPRISE || getPlatformSetting('stripe_price_enterprise'),
33
34
  };
34
35
  }
35
36
 
@@ -332,6 +333,30 @@ function handleWebhookRequest(req) {
332
333
  if (!s) throw new Error('Stripe not configured');
333
334
  const event = s.webhooks.constructEvent(raw, sig, whSecret);
334
335
  handleWebhookEvent(event);
336
+
337
+ // ── Forward genius-tagged events to genius-platform (same webhook, same secret) ──
338
+ const obj = event.data && event.data.object;
339
+ const geniusOrgId = obj && obj.metadata && obj.metadata.genius_org_id;
340
+ if (geniusOrgId) {
341
+ const callbackUrl = `${process.env.GENIUS_CALLBACK_URL || 'http://localhost:3004'}/api/wab/billing-callback`;
342
+ (async () => {
343
+ try {
344
+ const fetchFn = globalThis.fetch || (await import('node-fetch').then(m => m.default).catch(() => null));
345
+ if (!fetchFn) return;
346
+ await fetchFn(callbackUrl, {
347
+ method: 'POST',
348
+ headers: {
349
+ 'Content-Type': 'application/json',
350
+ 'X-Internal-Secret': process.env.GENIUS_BRIDGE_SECRET || '',
351
+ },
352
+ body: JSON.stringify({ type: event.type, data: event.data }),
353
+ });
354
+ console.log(`[stripe] forwarded ${event.type} → genius-platform (org: ${geniusOrgId})`);
355
+ } catch (e) {
356
+ console.error('[stripe] genius billing-callback failed (non-fatal):', e.message);
357
+ }
358
+ })();
359
+ }
335
360
  }
336
361
 
337
362
  module.exports = {
@@ -12,7 +12,7 @@ const DATA_DIR = process.env.NODE_ENV === 'test'
12
12
  : (process.env.DATA_DIR || path.join(__dirname, '..', '..', 'data'));
13
13
  if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
14
14
 
15
- const dbFile = process.env.NODE_ENV === 'test' ? 'wab-test.db' : 'wab.db';
15
+ const dbFile = process.env.NODE_ENV === 'test' ? `wab-test-${process.env.JEST_WORKER_ID || '1'}.db` : 'wab.db';
16
16
  const db = new Database(path.join(DATA_DIR, dbFile));
17
17
 
18
18
  // Ensure migrations tracking table exists
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ /**
6
+ * Constant-time comparison for secrets / bearer tokens.
7
+ *
8
+ * Both operands are hashed to a fixed-length SHA-256 digest before the
9
+ * timing-safe compare, so this never throws on length mismatch and does not
10
+ * leak the secret length through early termination. Non-string inputs (missing
11
+ * headers, undefined env vars) return false instead of throwing.
12
+ *
13
+ * Use this for ALL static-token / shared-secret equality checks instead of
14
+ * `a === b` / `a !== b`, which short-circuit on the first differing byte and
15
+ * are therefore vulnerable to remote timing analysis.
16
+ */
17
+ function safeEqual(a, b) {
18
+ if (typeof a !== 'string' || typeof b !== 'string' || a.length === 0 || b.length === 0) {
19
+ return false;
20
+ }
21
+ const ha = crypto.createHash('sha256').update(a, 'utf8').digest();
22
+ const hb = crypto.createHash('sha256').update(b, 'utf8').digest();
23
+ return crypto.timingSafeEqual(ha, hb);
24
+ }
25
+
26
+ module.exports = { safeEqual };