web-agent-bridge 1.1.0 → 1.1.1

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.
@@ -50,21 +50,30 @@ async function main() {
50
50
  // Step 3: Execute built-in tools
51
51
  console.log('\n3. Executing wab_get_page_info...');
52
52
  try {
53
- const info = await adapter.executeTool('wab_get_page_info', {});
54
- console.log(` Title: ${info.title || 'N/A'}`);
55
- console.log(` Version: ${info.bridgeVersion || 'N/A'}`);
53
+ const infoResult = await adapter.executeTool('wab_get_page_info', {});
54
+ const info = infoResult.content;
55
+ if (infoResult.is_error) {
56
+ console.log(` Error: ${info.error}`);
57
+ } else {
58
+ console.log(` Title: ${info.title || 'N/A'}`);
59
+ console.log(` Version: ${info.bridgeVersion || 'N/A'}`);
60
+ console.log(` Domain: ${info.domain || 'N/A'}`);
61
+ }
56
62
  } catch (err) {
57
- console.log(` (Requires bridge script on page: ${err.message})`);
63
+ console.log(` Error: ${err.message}`);
58
64
  }
59
65
 
60
66
  // Step 4: Search the fairness registry
61
67
  console.log('\n4. Fairness-weighted search (demo):');
62
68
  try {
63
- const search = await adapter.executeTool('wab_fairness_search', {
69
+ const searchResult = await adapter.executeTool('wab_fairness_search', {
64
70
  query: 'e-commerce',
65
71
  limit: 5
66
72
  });
67
- if (search.results?.length) {
73
+ const search = searchResult.content;
74
+ if (searchResult.is_error) {
75
+ console.log(` Error: ${search.error}`);
76
+ } else if (search.results?.length) {
68
77
  search.results.forEach(r => {
69
78
  console.log(` - ${r.name} (${r.domain}) — score: ${r.final_score}`);
70
79
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "web-agent-bridge",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Open protocol and runtime for AI agents to interact with websites — standardized discovery, commands, and fairness layer for the Agentic Web",
5
5
  "main": "server/index.js",
6
6
  "bin": {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Web Agent Bridge v1.1.0
2
+ * Web Agent Bridge v1.1.1
3
3
  * Open protocol + runtime for AI agent ↔ website interaction
4
4
  * https://github.com/web-agent-bridge
5
5
  * License: MIT
@@ -7,7 +7,7 @@
7
7
  (function (global) {
8
8
  'use strict';
9
9
 
10
- const VERSION = '1.1.0';
10
+ const VERSION = '1.1.1';
11
11
  const PROTOCOL_VERSION = '1.0';
12
12
  const LICENSING_SERVER = 'https://api.webagentbridge.com';
13
13
  const DISCOVERY_PATHS = ['/agent-bridge.json', '/.well-known/wab.json'];
package/server/index.js CHANGED
@@ -20,6 +20,7 @@ const adminRoutes = require('./routes/admin');
20
20
  const billingRoutes = require('./routes/billing');
21
21
  const noscriptRoutes = require('./routes/noscript');
22
22
  const discoveryRoutes = require('./routes/discovery');
23
+ const wabApiRoutes = require('./routes/wab-api');
23
24
  const { handleWebhookRequest } = require('./services/stripe');
24
25
 
25
26
  const app = express();
@@ -114,6 +115,7 @@ app.use('/api/license', licenseLimiter, licenseRoutes);
114
115
  app.use('/api/admin', apiLimiter, adminRoutes);
115
116
  app.use('/api/billing', apiLimiter, billingRoutes);
116
117
  app.use('/api/noscript', noscriptRoutes);
118
+ app.use('/api/wab', wabApiRoutes);
117
119
  app.use('/', discoveryRoutes);
118
120
 
119
121
  app.get('/dashboard', (req, res) => {
@@ -15,7 +15,7 @@ const {
15
15
  generateFairnessReport
16
16
  } = require('../services/fairness');
17
17
 
18
- const WAB_VERSION = '1.1.0';
18
+ const WAB_VERSION = '1.1.1';
19
19
 
20
20
  // ─── Helpers ─────────────────────────────────────────────────────────
21
21
 
@@ -77,10 +77,10 @@ function buildDiscoveryDocument(site) {
77
77
  },
78
78
  agent_access: {
79
79
  bridge_script: '/script/ai-agent-bridge.js',
80
- api_base: '/api/license',
80
+ api_base: '/api/wab',
81
81
  websocket: '/ws/analytics',
82
- noscript: '/api/noscript',
83
- discovery: '/api/discovery'
82
+ noscript: `/api/noscript/bridge/${site.id}`,
83
+ discovery: `/api/discovery/${site.id}`
84
84
  },
85
85
  fairness: {
86
86
  is_independent: dirEntry ? !!dirEntry.is_independent : false,
@@ -95,10 +95,15 @@ function buildDiscoveryDocument(site) {
95
95
  sandbox: true
96
96
  },
97
97
  endpoints: {
98
+ authenticate: '/api/wab/authenticate',
99
+ discover: `/api/wab/discover?siteId=${site.id}`,
100
+ actions: `/api/wab/actions?siteId=${site.id}`,
101
+ execute: '/api/wab/actions/{actionName}',
102
+ read: '/api/wab/read',
103
+ page_info: `/api/wab/page-info?siteId=${site.id}`,
104
+ search: '/api/wab/search',
105
+ ping: '/api/wab/ping',
98
106
  token_exchange: '/api/license/token',
99
- verify: '/api/license/verify',
100
- track: '/api/license/track',
101
- actions: `/api/discovery/${site.id}`,
102
107
  bridge_page: `/api/noscript/bridge/${site.id}`
103
108
  }
104
109
  };
@@ -13,7 +13,7 @@ const TRANSPARENT_GIF = Buffer.from(
13
13
  'base64'
14
14
  );
15
15
 
16
- const WAB_VERSION = '1.1.0';
16
+ const WAB_VERSION = '1.1.1';
17
17
 
18
18
  // ─── Rate limiter for pixel endpoint (300 req/min per IP) ────────────
19
19
  const pixelLimiter = rateLimit({
@@ -0,0 +1,476 @@
1
+ /**
2
+ * WAB Protocol HTTP Transport — RESTful endpoints that implement the
3
+ * WAB command protocol over HTTP for remote agents and the MCP adapter.
4
+ *
5
+ * Every command from the WAB spec (docs/SPEC.md §5) is accessible here
6
+ * so agents that cannot run JavaScript in a browser can still interact
7
+ * with WAB-enabled sites via standard HTTP requests.
8
+ */
9
+
10
+ const express = require('express');
11
+ const router = express.Router();
12
+ const { findSiteById, findSiteByLicense, recordAnalytic, db } = require('../models/db');
13
+ const { broadcastAnalytic } = require('../ws');
14
+ const {
15
+ calculateNeutralityScore,
16
+ fairnessWeightedSearch,
17
+ getDirectoryListings,
18
+ generateFairnessReport
19
+ } = require('../services/fairness');
20
+
21
+ const WAB_VERSION = '1.1.1';
22
+ const PROTOCOL_VERSION = '1.0';
23
+
24
+ // ─── Session management ──────────────────────────────────────────────
25
+ const sessions = new Map();
26
+ const SESSION_TTL = 3600_000;
27
+
28
+ setInterval(() => {
29
+ const now = Date.now();
30
+ for (const [token, data] of sessions) {
31
+ if (now > data.expiresAt) sessions.delete(token);
32
+ }
33
+ }, 300_000);
34
+
35
+ function generateSessionToken() {
36
+ const bytes = require('crypto').randomBytes(32);
37
+ return bytes.toString('hex');
38
+ }
39
+
40
+ function requireSession(req, res, next) {
41
+ const auth = req.get('Authorization');
42
+ if (!auth || !auth.startsWith('Bearer ')) {
43
+ return res.status(401).json({
44
+ type: 'error',
45
+ error: { code: 'auth_required', message: 'Bearer token required in Authorization header' }
46
+ });
47
+ }
48
+ const token = auth.slice(7);
49
+ const session = sessions.get(token);
50
+ if (!session || Date.now() > session.expiresAt) {
51
+ sessions.delete(token);
52
+ return res.status(401).json({
53
+ type: 'error',
54
+ error: { code: 'session_expired', message: 'Session expired or invalid' }
55
+ });
56
+ }
57
+ req.wabSession = session;
58
+ next();
59
+ }
60
+
61
+ // ─── Helper: resolve site from request ───────────────────────────────
62
+ function resolveSite(req) {
63
+ if (req.wabSession) return findSiteById.get(req.wabSession.siteId);
64
+ const siteId = req.query.siteId || req.body?.siteId;
65
+ if (siteId) return findSiteById.get(siteId);
66
+ return null;
67
+ }
68
+
69
+ function parseSiteConfig(site) {
70
+ try { return JSON.parse(site.config || '{}'); } catch (_) { return {}; }
71
+ }
72
+
73
+ function buildCommandResponse(id, result) {
74
+ return { id: id || null, type: 'success', protocol: PROTOCOL_VERSION, result };
75
+ }
76
+
77
+ function buildErrorResponse(id, code, message) {
78
+ return { id: id || null, type: 'error', protocol: PROTOCOL_VERSION, error: { code, message } };
79
+ }
80
+
81
+ // ═════════════════════════════════════════════════════════════════════
82
+ // POST /api/wab/authenticate — session token exchange
83
+ // ═════════════════════════════════════════════════════════════════════
84
+
85
+ router.post('/authenticate', (req, res) => {
86
+ try {
87
+ const { siteId, apiKey, meta } = req.body;
88
+ if (!siteId && !apiKey) {
89
+ return res.status(400).json(buildErrorResponse(null, 'invalid_argument', 'siteId or apiKey required'));
90
+ }
91
+
92
+ let site;
93
+ if (apiKey) {
94
+ site = db.prepare('SELECT * FROM sites WHERE api_key = ? AND active = 1').get(apiKey);
95
+ } else {
96
+ site = findSiteById.get(siteId);
97
+ }
98
+
99
+ if (!site) {
100
+ return res.status(404).json(buildErrorResponse(null, 'not_found', 'Site not found or invalid credentials'));
101
+ }
102
+
103
+ const origin = req.get('origin') || '';
104
+ if (origin) {
105
+ try {
106
+ const reqDomain = new URL(origin).hostname.replace(/^www\./, '');
107
+ const siteDomain = site.domain.replace(/^www\./, '');
108
+ if (reqDomain !== siteDomain && reqDomain !== 'localhost' && reqDomain !== '127.0.0.1') {
109
+ return res.status(403).json(buildErrorResponse(null, 'origin_mismatch', 'Origin does not match site domain'));
110
+ }
111
+ } catch (_) {}
112
+ }
113
+
114
+ const token = generateSessionToken();
115
+ sessions.set(token, {
116
+ siteId: site.id,
117
+ tier: site.tier,
118
+ domain: site.domain,
119
+ agentMeta: meta || {},
120
+ createdAt: Date.now(),
121
+ expiresAt: Date.now() + SESSION_TTL
122
+ });
123
+
124
+ res.json(buildCommandResponse(null, {
125
+ authenticated: true,
126
+ token,
127
+ siteId: site.id,
128
+ tier: site.tier,
129
+ expiresIn: SESSION_TTL / 1000,
130
+ permissions: parseSiteConfig(site).agentPermissions || {}
131
+ }));
132
+ } catch (err) {
133
+ res.status(500).json(buildErrorResponse(null, 'internal', 'Authentication failed'));
134
+ }
135
+ });
136
+
137
+ // ═════════════════════════════════════════════════════════════════════
138
+ // GET /api/wab/discover — full discovery document
139
+ // ═════════════════════════════════════════════════════════════════════
140
+
141
+ router.get('/discover', (req, res) => {
142
+ try {
143
+ const site = resolveSite(req);
144
+ if (!site || !site.active) {
145
+ const domain = (req.get('origin') ? new URL(req.get('origin')).hostname : req.get('host')?.split(':')[0]) || '';
146
+ const byDomain = db.prepare(
147
+ 'SELECT * FROM sites WHERE LOWER(REPLACE(domain, "www.", "")) = ? AND active = 1 LIMIT 1'
148
+ ).get(domain.toLowerCase().replace(/^www\./, ''));
149
+
150
+ if (!byDomain) {
151
+ return res.status(404).json(buildErrorResponse(null, 'not_found', 'No WAB site found'));
152
+ }
153
+ return res.json(buildCommandResponse(null, buildDiscovery(byDomain)));
154
+ }
155
+ res.json(buildCommandResponse(null, buildDiscovery(site)));
156
+ } catch (err) {
157
+ res.status(500).json(buildErrorResponse(null, 'internal', 'Discovery failed'));
158
+ }
159
+ });
160
+
161
+ // ═════════════════════════════════════════════════════════════════════
162
+ // GET /api/wab/actions — list actions
163
+ // ═════════════════════════════════════════════════════════════════════
164
+
165
+ router.get('/actions', (req, res) => {
166
+ try {
167
+ const site = resolveSite(req);
168
+ if (!site) return res.status(400).json(buildErrorResponse(null, 'invalid_argument', 'siteId required'));
169
+
170
+ const config = parseSiteConfig(site);
171
+ const perms = config.agentPermissions || {};
172
+ const category = req.query.category;
173
+
174
+ const actions = Object.entries(perms)
175
+ .filter(([, v]) => v)
176
+ .map(([name]) => ({
177
+ name,
178
+ description: `Permission: ${name}`,
179
+ trigger: name === 'click' ? 'click' : name === 'fillForms' ? 'fill_and_submit' : name === 'scroll' ? 'scroll' : 'api',
180
+ category: name === 'navigate' ? 'navigation' : 'general',
181
+ requiresAuth: ['apiAccess', 'automatedLogin', 'extractData'].includes(name)
182
+ }));
183
+
184
+ const filtered = category ? actions.filter(a => a.category === category) : actions;
185
+
186
+ res.json(buildCommandResponse(req.query.id || null, { actions: filtered, total: filtered.length }));
187
+ } catch (err) {
188
+ res.status(500).json(buildErrorResponse(null, 'internal', 'Failed to list actions'));
189
+ }
190
+ });
191
+
192
+ // ═════════════════════════════════════════════════════════════════════
193
+ // POST /api/wab/actions/:name — execute action (with tracking)
194
+ // ═════════════════════════════════════════════════════════════════════
195
+
196
+ router.post('/actions/:name', requireSession, (req, res) => {
197
+ try {
198
+ const actionName = req.params.name;
199
+ const site = findSiteById.get(req.wabSession.siteId);
200
+ if (!site) return res.status(404).json(buildErrorResponse(req.body?.id, 'not_found', 'Site not found'));
201
+
202
+ const config = parseSiteConfig(site);
203
+ const perms = config.agentPermissions || {};
204
+
205
+ const permMap = {
206
+ click: 'click', fill_and_submit: 'fillForms', scroll: 'scroll',
207
+ navigate: 'navigate', api: 'apiAccess', read: 'readContent', extract: 'extractData'
208
+ };
209
+ const requiredPerm = permMap[actionName] || actionName;
210
+
211
+ if (!perms[requiredPerm] && !perms[actionName]) {
212
+ return res.status(403).json(buildErrorResponse(req.body?.id, 'permission_denied',
213
+ `Action "${actionName}" is not permitted by site configuration`));
214
+ }
215
+
216
+ recordAnalytic({
217
+ siteId: site.id,
218
+ actionName,
219
+ agentId: req.wabSession.agentMeta?.name || 'mcp-agent',
220
+ triggerType: 'wab_api',
221
+ success: true,
222
+ metadata: { params: req.body?.params || {}, transport: 'http' }
223
+ });
224
+
225
+ broadcastAnalytic(site.id, {
226
+ actionName,
227
+ agentId: req.wabSession.agentMeta?.name || 'mcp-agent',
228
+ triggerType: 'wab_api',
229
+ success: true
230
+ });
231
+
232
+ res.json(buildCommandResponse(req.body?.id, {
233
+ success: true,
234
+ action: actionName,
235
+ siteId: site.id,
236
+ executed_at: new Date().toISOString(),
237
+ note: 'Server-side action recorded. For DOM interactions, use the bridge script in-browser.'
238
+ }));
239
+ } catch (err) {
240
+ res.status(500).json(buildErrorResponse(req.body?.id, 'internal', 'Action execution failed'));
241
+ }
242
+ });
243
+
244
+ // ═════════════════════════════════════════════════════════════════════
245
+ // POST /api/wab/read — read content (selector-based, requires in-browser)
246
+ // ═════════════════════════════════════════════════════════════════════
247
+
248
+ router.post('/read', requireSession, (req, res) => {
249
+ try {
250
+ const { selector, id } = req.body;
251
+ if (!selector) {
252
+ return res.status(400).json(buildErrorResponse(id, 'invalid_argument', 'selector is required'));
253
+ }
254
+
255
+ const site = findSiteById.get(req.wabSession.siteId);
256
+ if (!site) return res.status(404).json(buildErrorResponse(id, 'not_found', 'Site not found'));
257
+
258
+ const config = parseSiteConfig(site);
259
+ if (!config.agentPermissions?.readContent) {
260
+ return res.status(403).json(buildErrorResponse(id, 'permission_denied', 'readContent not enabled'));
261
+ }
262
+
263
+ recordAnalytic({
264
+ siteId: site.id,
265
+ actionName: 'readContent',
266
+ agentId: req.wabSession.agentMeta?.name || 'mcp-agent',
267
+ triggerType: 'wab_api',
268
+ success: true,
269
+ metadata: { selector, transport: 'http' }
270
+ });
271
+
272
+ res.json(buildCommandResponse(id, {
273
+ success: true,
274
+ selector,
275
+ note: 'Content reading via HTTP returns metadata only. Use the bridge script in-browser or the noscript bridge for rendered content.',
276
+ bridge_page: `/api/noscript/bridge/${site.id}`,
277
+ noscript_endpoints: {
278
+ pixel: `/api/noscript/pixel/${site.id}`,
279
+ css: `/api/noscript/css/${site.id}`,
280
+ bridge: `/api/noscript/bridge/${site.id}`
281
+ }
282
+ }));
283
+ } catch (err) {
284
+ res.status(500).json(buildErrorResponse(null, 'internal', 'Read failed'));
285
+ }
286
+ });
287
+
288
+ // ═════════════════════════════════════════════════════════════════════
289
+ // GET /api/wab/page-info — get page/site metadata
290
+ // ═════════════════════════════════════════════════════════════════════
291
+
292
+ router.get('/page-info', (req, res) => {
293
+ try {
294
+ const site = resolveSite(req);
295
+ if (!site) return res.status(400).json(buildErrorResponse(null, 'invalid_argument', 'siteId required'));
296
+
297
+ const config = parseSiteConfig(site);
298
+ const neutralityScore = calculateNeutralityScore(site);
299
+
300
+ res.json(buildCommandResponse(req.query.id || null, {
301
+ title: site.name,
302
+ domain: site.domain,
303
+ url: `https://${site.domain}`,
304
+ tier: site.tier,
305
+ bridgeVersion: WAB_VERSION,
306
+ protocol: PROTOCOL_VERSION,
307
+ permissions: config.agentPermissions || {},
308
+ restrictions: config.restrictions || {},
309
+ security: {
310
+ sandboxActive: true,
311
+ sessionRequired: true,
312
+ originValidation: true,
313
+ rateLimit: config.restrictions?.rateLimit?.maxCallsPerMinute || 60
314
+ },
315
+ fairness: {
316
+ neutralityScore,
317
+ isIndependent: false
318
+ },
319
+ endpoints: {
320
+ discover: `/api/wab/discover?siteId=${site.id}`,
321
+ actions: `/api/wab/actions?siteId=${site.id}`,
322
+ authenticate: '/api/wab/authenticate',
323
+ bridge: `/api/noscript/bridge/${site.id}`,
324
+ discovery: `/api/discovery/${site.id}`
325
+ }
326
+ }));
327
+ } catch (err) {
328
+ res.status(500).json(buildErrorResponse(null, 'internal', 'Failed to get page info'));
329
+ }
330
+ });
331
+
332
+ // ═════════════════════════════════════════════════════════════════════
333
+ // GET /api/wab/search — fairness-weighted search (MCP adapter uses this)
334
+ // ═════════════════════════════════════════════════════════════════════
335
+
336
+ router.get('/search', (req, res) => {
337
+ try {
338
+ const query = req.query.q || '';
339
+ const category = req.query.category || null;
340
+ const limit = Math.min(parseInt(req.query.limit) || 10, 100);
341
+
342
+ let sql = `
343
+ SELECT s.*, d.category, d.tags, d.is_independent, d.commission_rate,
344
+ d.direct_benefit, d.neutrality_score, d.trust_signature
345
+ FROM wab_directory d
346
+ JOIN sites s ON d.site_id = s.id AND s.active = 1
347
+ WHERE d.listed = 1
348
+ `;
349
+ const params = [];
350
+
351
+ if (category) {
352
+ sql += ' AND d.category = ?';
353
+ params.push(category);
354
+ }
355
+
356
+ sql += ' ORDER BY d.neutrality_score DESC LIMIT ?';
357
+ params.push(limit * 3);
358
+
359
+ const candidates = db.prepare(sql).all(...params);
360
+ const results = fairnessWeightedSearch(query, candidates).slice(0, limit);
361
+
362
+ res.json(buildCommandResponse(req.query.id || null, {
363
+ query,
364
+ total: results.length,
365
+ fairness_applied: true,
366
+ results: results.map(r => ({
367
+ siteId: r.id,
368
+ name: r.name,
369
+ domain: r.domain,
370
+ description: r.description || '',
371
+ category: r.category || 'general',
372
+ tier: r.tier,
373
+ neutrality_score: r._neutralityScore,
374
+ is_independent: r._isIndependent,
375
+ relevance_score: r._relevance,
376
+ fairness_boost: r._fairnessBoost,
377
+ final_score: r._finalScore,
378
+ endpoints: {
379
+ discover: `/api/wab/discover?siteId=${r.id}`,
380
+ actions: `/api/wab/actions?siteId=${r.id}`,
381
+ bridge: `/api/noscript/bridge/${r.id}`
382
+ }
383
+ }))
384
+ }));
385
+ } catch (err) {
386
+ res.status(500).json(buildErrorResponse(null, 'internal', 'Search failed'));
387
+ }
388
+ });
389
+
390
+ // ═════════════════════════════════════════════════════════════════════
391
+ // GET /api/wab/ping — health check
392
+ // ═════════════════════════════════════════════════════════════════════
393
+
394
+ router.get('/ping', (_req, res) => {
395
+ res.json(buildCommandResponse(null, {
396
+ pong: true,
397
+ version: WAB_VERSION,
398
+ protocol: PROTOCOL_VERSION,
399
+ timestamp: Date.now(),
400
+ status: 'healthy'
401
+ }));
402
+ });
403
+
404
+ // ─── Discovery document builder ──────────────────────────────────────
405
+
406
+ function buildDiscovery(site) {
407
+ const config = parseSiteConfig(site);
408
+ const perms = config.agentPermissions || {};
409
+ const features = config.features || {};
410
+
411
+ const commands = Object.entries(perms)
412
+ .filter(([, v]) => v)
413
+ .map(([name]) => ({
414
+ name,
415
+ trigger: name === 'click' ? 'click' : name === 'fillForms' ? 'fill_and_submit' : name === 'scroll' ? 'scroll' : 'api',
416
+ requiresAuth: ['apiAccess', 'automatedLogin', 'extractData'].includes(name)
417
+ }));
418
+
419
+ const featureList = ['auto_discovery', 'noscript_fallback', 'wab_protocol_api'];
420
+ if (features.advancedAnalytics) featureList.push('advanced_analytics');
421
+ if (features.realTimeUpdates) featureList.push('real_time_updates');
422
+
423
+ const dirEntry = db.prepare('SELECT * FROM wab_directory WHERE site_id = ?').get(site.id);
424
+
425
+ return {
426
+ wab_version: WAB_VERSION,
427
+ protocol: PROTOCOL_VERSION,
428
+ generated_at: new Date().toISOString(),
429
+ provider: {
430
+ name: site.name,
431
+ domain: site.domain,
432
+ category: dirEntry?.category || 'general',
433
+ description: site.description || ''
434
+ },
435
+ capabilities: {
436
+ commands,
437
+ permissions: perms,
438
+ tier: site.tier,
439
+ transport: ['js_global', 'http', 'websocket'],
440
+ features: featureList
441
+ },
442
+ agent_access: {
443
+ bridge_script: '/script/ai-agent-bridge.js',
444
+ api_base: '/api/wab',
445
+ websocket: '/ws/analytics',
446
+ noscript: `/api/noscript/bridge/${site.id}`,
447
+ discovery: `/api/discovery/${site.id}`
448
+ },
449
+ fairness: {
450
+ is_independent: dirEntry ? !!dirEntry.is_independent : false,
451
+ commission_rate: dirEntry ? dirEntry.commission_rate : 0,
452
+ direct_benefit: dirEntry ? (dirEntry.direct_benefit || '') : '',
453
+ neutrality_score: calculateNeutralityScore(site)
454
+ },
455
+ security: {
456
+ session_required: true,
457
+ origin_validation: true,
458
+ rate_limit: config.restrictions?.rateLimit?.maxCallsPerMinute || 60,
459
+ sandbox: true
460
+ },
461
+ endpoints: {
462
+ authenticate: '/api/wab/authenticate',
463
+ discover: `/api/wab/discover?siteId=${site.id}`,
464
+ actions: `/api/wab/actions?siteId=${site.id}`,
465
+ execute: '/api/wab/actions/{actionName}',
466
+ read: '/api/wab/read',
467
+ page_info: `/api/wab/page-info?siteId=${site.id}`,
468
+ search: '/api/wab/search',
469
+ ping: '/api/wab/ping',
470
+ token_exchange: '/api/license/token',
471
+ bridge_page: `/api/noscript/bridge/${site.id}`
472
+ }
473
+ };
474
+ }
475
+
476
+ module.exports = router;
@@ -5,7 +5,7 @@
5
5
 
6
6
  const { db } = require('../models/db');
7
7
 
8
- const WAB_VERSION = '1.1.0';
8
+ const WAB_VERSION = '1.1.1';
9
9
 
10
10
  // ─── Directory Table (created lazily) ────────────────────────────────
11
11
 
@@ -39,7 +39,7 @@ adapter.close();
39
39
  | `siteId` | `string` | `null` | WAB site identifier |
40
40
  | `apiKey` | `string` | `null` | API key for authenticated requests |
41
41
  | `transport` | `string` | `'http'` | Transport type: `http`, `websocket`, or `direct` |
42
- | `registryUrl` | `string` | `https://registry.webagentbridge.com` | WAB fairness registry URL |
42
+ | `registryUrl` | `string` | `https://webagentbridge.com` | WAB fairness registry URL |
43
43
  | `page` | `object` | — | Puppeteer/Playwright page (required for `direct`) |
44
44
  | `wsUrl` | `string` | auto | WebSocket URL (required for `websocket` if no `siteUrl`) |
45
45
  | `timeout` | `number` | `15000` | Request timeout in milliseconds |
@@ -11,7 +11,7 @@
11
11
  'use strict';
12
12
 
13
13
  const DISCOVERY_PATHS = ['/agent-bridge.json', '/.well-known/wab.json'];
14
- const DEFAULT_REGISTRY = 'https://registry.webagentbridge.com';
14
+ const DEFAULT_REGISTRY = 'https://webagentbridge.com';
15
15
  const DEFAULT_TIMEOUT_MS = 15_000;
16
16
 
17
17
  // ---------------------------------------------------------------------------
@@ -320,20 +320,35 @@ class WABMCPAdapter {
320
320
  }
321
321
  }
322
322
 
323
+ if (this.siteId) {
324
+ try {
325
+ this._discovery = await jsonFetch(
326
+ resolveUrl(base, `/api/discovery/${this.siteId}`), {}, this.timeout
327
+ );
328
+ this._extractActions(this._discovery);
329
+ return this._discovery;
330
+ } catch (err) { lastError = err; }
331
+ }
332
+
323
333
  try {
324
- this._discovery = await this._transport.request('/api/wab/discover');
334
+ this._discovery = await jsonFetch(
335
+ resolveUrl(base, '/api/wab/discover'), {}, this.timeout
336
+ );
337
+ if (this._discovery.result) this._discovery = this._discovery.result;
325
338
  this._extractActions(this._discovery);
326
339
  return this._discovery;
327
- } catch (err) {
328
- lastError = err;
329
- }
340
+ } catch (err) { lastError = err; }
330
341
 
331
342
  throw new Error(`WAB discovery failed for ${base}: ${lastError?.message}`);
332
343
  }
333
344
 
334
345
  /** @private */
335
346
  _extractActions(doc) {
336
- this._siteActions = doc.actions || doc.capabilities?.actions || [];
347
+ const actions = doc.actions || doc.capabilities?.commands || doc.capabilities?.actions || [];
348
+ this._siteActions = Array.isArray(actions) ? actions.map(a => {
349
+ if (typeof a === 'string') return { name: a, description: `Permission: ${a}`, trigger: 'api' };
350
+ return a;
351
+ }) : [];
337
352
  }
338
353
 
339
354
  // -----------------------------------------------------------------------
@@ -430,7 +445,7 @@ class WABMCPAdapter {
430
445
  method: 'POST',
431
446
  headers: { 'Content-Type': 'application/json', ...headers },
432
447
  body: JSON.stringify({ params: params || {} }),
433
- }, this.timeout);
448
+ }, this.timeout).then(r => r.result || r);
434
449
  }
435
450
 
436
451
  return this._transport.request(`/api/wab/actions/${name}`, { name, data: params || {} });
@@ -446,7 +461,7 @@ class WABMCPAdapter {
446
461
  method: 'POST',
447
462
  headers: { 'Content-Type': 'application/json', ...this._authHeaders() },
448
463
  body: JSON.stringify({ selector }),
449
- }, this.timeout);
464
+ }, this.timeout).then(r => r.result || r);
450
465
  }
451
466
 
452
467
  return this._transport.request('/api/wab/read', { selector });
@@ -455,7 +470,11 @@ class WABMCPAdapter {
455
470
  /** @private */
456
471
  async _getPageInfo() {
457
472
  if (this._transport instanceof HTTPTransport) {
458
- return jsonFetch(resolveUrl(this.siteUrl, '/api/wab/page-info'), { headers: this._authHeaders() }, this.timeout);
473
+ const siteParam = this.siteId ? `?siteId=${this.siteId}` : '';
474
+ return jsonFetch(
475
+ resolveUrl(this.siteUrl, `/api/wab/page-info${siteParam}`),
476
+ { headers: this._authHeaders() }, this.timeout
477
+ ).then(r => r.result || r);
459
478
  }
460
479
  return this._transport.request('/api/wab/page-info');
461
480
  }
@@ -474,10 +493,12 @@ class WABMCPAdapter {
474
493
  * @returns {Promise<object>}
475
494
  */
476
495
  async _fairnessSearch(query, category, limit = 10) {
477
- const params = new URLSearchParams({ q: query, limit: String(limit) });
496
+ const params = new URLSearchParams({ q: query || '', limit: String(limit) });
478
497
  if (category) params.set('category', category);
479
498
 
480
- return jsonFetch(`${this.registryUrl}/api/search?${params}`, {}, this.timeout);
499
+ const base = this.siteUrl || this.registryUrl;
500
+ const result = await jsonFetch(`${base.replace(/\/+$/, '')}/api/wab/search?${params}`, {}, this.timeout);
501
+ return result.result || result;
481
502
  }
482
503
 
483
504
  // -----------------------------------------------------------------------
@@ -488,7 +509,11 @@ class WABMCPAdapter {
488
509
  async _authenticate(apiKey, meta) {
489
510
  if (!apiKey) throw new Error('apiKey is required');
490
511
 
491
- const payload = { apiKey, ...(meta ? { meta } : {}) };
512
+ const payload = {
513
+ apiKey,
514
+ ...(this.siteId ? { siteId: this.siteId } : {}),
515
+ ...(meta ? { meta } : {})
516
+ };
492
517
 
493
518
  if (this._transport instanceof HTTPTransport) {
494
519
  const result = await jsonFetch(resolveUrl(this.siteUrl, '/api/wab/authenticate'), {
@@ -496,13 +521,15 @@ class WABMCPAdapter {
496
521
  headers: { 'Content-Type': 'application/json' },
497
522
  body: JSON.stringify(payload),
498
523
  }, this.timeout);
499
- if (result.token) this._sessionToken = result.token;
500
- return result;
524
+ const data = result.result || result;
525
+ if (data.token) this._sessionToken = data.token;
526
+ return data;
501
527
  }
502
528
 
503
529
  const result = await this._transport.request('/api/wab/authenticate', payload);
504
- if (result.token) this._sessionToken = result.token;
505
- return result;
530
+ const data = result.result || result;
531
+ if (data.token) this._sessionToken = data.token;
532
+ return data;
506
533
  }
507
534
 
508
535
  /** @private Build auth headers from session token and/or API key. */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wab-mcp-adapter",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "MCP adapter for Web Agent Bridge — expose WAB site capabilities as MCP tools",
5
5
  "main": "index.js",
6
6
  "keywords": ["wab", "mcp", "ai-agent", "model-context-protocol", "web-agent-bridge"],