web-agent-bridge 3.16.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 (61) 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/dashboard.html +1 -0
  8. package/public/docs.html +113 -43
  9. package/public/index.html +142 -8
  10. package/public/key-rotation.html +184 -0
  11. package/public/llms.txt +54 -0
  12. package/public/notary.html +94 -0
  13. package/public/observatory.html +103 -0
  14. package/public/research.html +57 -0
  15. package/public/researchers.html +113 -0
  16. package/public/responsible-disclosure.html +294 -0
  17. package/public/robots.txt +17 -0
  18. package/public/security.html +157 -0
  19. package/public/threat-model.html +153 -0
  20. package/public/viral-coefficient.html +533 -0
  21. package/public/wab-dataset.html +501 -0
  22. package/public/wab-email.html +78 -0
  23. package/public/wab-lens.html +61 -0
  24. package/public/wab-p2p.html +96 -0
  25. package/public/wab-registry.html +481 -0
  26. package/public/wab-today.html +448 -0
  27. package/public/wab-uri.html +88 -0
  28. package/public/webhooks.html +181 -0
  29. package/script/ai-agent-bridge.js +24 -4
  30. package/server/index.js +1193 -827
  31. package/server/models/db.js +2 -1
  32. package/server/routes/admin-shieldlink.js +1 -1
  33. package/server/routes/admin-shieldqr.js +1 -1
  34. package/server/routes/admin-trust-monitor.js +1 -1
  35. package/server/routes/api-keys.js +2 -1
  36. package/server/routes/customer-shieldlink.js +1 -1
  37. package/server/routes/enterprise-mesh.js +2 -1
  38. package/server/routes/genius-bridge.js +256 -0
  39. package/server/routes/genius-gateway.js +137 -0
  40. package/server/routes/governance-saas.js +2 -1
  41. package/server/routes/notary.js +309 -0
  42. package/server/routes/observatory.js +109 -0
  43. package/server/routes/partners.js +2 -1
  44. package/server/routes/registry.js +352 -0
  45. package/server/routes/research.js +83 -0
  46. package/server/routes/ring4.js +2 -1
  47. package/server/routes/runtime.js +98 -25
  48. package/server/routes/security-researchers.js +161 -0
  49. package/server/routes/shieldqr.js +1 -1
  50. package/server/routes/traces.js +247 -0
  51. package/server/services/agent-tasks.js +9 -7
  52. package/server/services/email.js +50 -2
  53. package/server/services/marketplace.js +27 -8
  54. package/server/services/plans.js +1 -1
  55. package/server/services/shieldlink.js +1 -1
  56. package/server/services/ssl-ct-monitor.js +1 -1
  57. package/server/services/ssl-monitor.js +1 -1
  58. package/server/services/stripe.js +29 -4
  59. package/server/services/webhooks.js +61 -1
  60. package/server/utils/migrate.js +1 -1
  61. package/server/utils/safe-compare.js +26 -0
package/server/index.js CHANGED
@@ -1,827 +1,1193 @@
1
- require('dotenv').config();
2
-
3
- const { assertSecretsAtStartup } = require('./config/secrets');
4
- assertSecretsAtStartup();
5
-
6
- const express = require('express');
7
- const http = require('http');
8
- const cors = require('cors');
9
- const helmet = require('helmet');
10
- const rateLimit = require('express-rate-limit');
11
- const path = require('path');
12
- const { setupWebSocket } = require('./ws');
13
- const { runMigrations } = require('./utils/migrate');
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');
18
- const { cluster } = require('./services/cluster');
19
-
20
- const authRoutes = require('./routes/auth');
21
- const apiRoutes = require('./routes/api');
22
- const licenseRoutes = require('./routes/license');
23
- const adminRoutes = require('./routes/admin');
24
- const billingRoutes = require('./routes/billing');
25
- const sovereignRoutes = require('./routes/sovereign');
26
- const meshRoutes = require('./routes/mesh');
27
- const commanderRoutes = require('./routes/commander');
28
- const adsRoutes = require('./routes/ads');
29
- const wabApiRoutes = require('./routes/wab-api');
30
- const noscriptRoutes = require('./routes/noscript');
31
- const discoveryRoutes = require('./routes/discovery');
32
- const providerRoutes = require('./routes/providers');
33
- const governanceRoutes = require('./routes/governance');
34
- const premiumRoutes = require('./routes/premium');
35
- const adminPremiumRoutes = require('./routes/admin-premium');
36
- const workspaceRoutes = require('./routes/agent-workspace');
37
- const universalRoutes = require('./routes/universal');
38
- const runtimeRoutes = require('./routes/runtime');
39
- const demoShowcaseRoutes = require('./routes/demo-showcase');
40
- const demoStoreRoutes = require('./routes/demo-store');
41
- const gatewayRoutes = require('./routes/gateway');
42
- let growthRoutes;
43
- try { growthRoutes = require('./routes/growth'); } catch { growthRoutes = require('express').Router(); }
44
- const { handleWebhookRequest } = require('./services/stripe');
45
- const { runtime } = require('./runtime');
46
-
47
- const app = express();
48
- const PORT = process.env.PORT || 3000;
49
-
50
- app.set('trust proxy', 1);
51
-
52
- const corsOrigins = (process.env.ALLOWED_ORIGINS
53
- || 'http://localhost:3000,http://127.0.0.1:3000,http://localhost:5173')
54
- .split(',')
55
- .map((s) => s.trim())
56
- .filter(Boolean);
57
-
58
- app.use(
59
- cors({
60
- origin(origin, callback) {
61
- if (!origin) return callback(null, true);
62
- if (corsOrigins.includes(origin)) return callback(null, true);
63
- if (process.env.NODE_ENV !== 'production' && /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i.test(origin)) {
64
- return callback(null, true);
65
- }
66
- return callback(null, false);
67
- },
68
- credentials: true
69
- })
70
- );
71
-
72
- const scriptSrc = process.env.CSP_ALLOW_UNSAFE_INLINE === 'false'
73
- ? ["'self'", 'https://unpkg.com', 'https://cdn.jsdelivr.net']
74
- : ["'self'", "'unsafe-inline'", 'https://unpkg.com', 'https://cdn.jsdelivr.net'];
75
- const styleSrc = process.env.CSP_ALLOW_UNSAFE_INLINE === 'false'
76
- ? ["'self'"]
77
- : ["'self'", "'unsafe-inline'"];
78
-
79
- // Per-request CSP nonce — exposed as res.locals.cspNonce for new pages opting into strict CSP.
80
- app.use((req, res, next) => {
81
- res.locals.cspNonce = require('crypto').randomBytes(16).toString('base64');
82
- next();
83
- });
84
-
85
- // CSP — tightened: HTTPS-only iframes, upgrade-insecure-requests, report endpoint.
86
- const cspReportUri = '/api/security/csp-report';
87
- app.use(
88
- helmet({
89
- contentSecurityPolicy: {
90
- directives: {
91
- defaultSrc: ["'self'"],
92
- // NOTE: Adding a nonce alongside 'unsafe-inline' makes browsers ignore
93
- // 'unsafe-inline' (CSP3 spec). All existing public/admin pages still
94
- // rely on inline <script> blocks, so we keep 'unsafe-inline' enforced
95
- // here and use the Report-Only policy below to track nonce migration.
96
- scriptSrc: scriptSrc,
97
- scriptSrcAttr: [...scriptSrc, "'unsafe-hashes'"],
98
- styleSrc: [...styleSrc, 'https://fonts.googleapis.com'],
99
- imgSrc: ["'self'", 'data:', 'https:'],
100
- connectSrc: ["'self'", 'https:', 'ws:', 'wss:'],
101
- fontSrc: ["'self'", 'https://fonts.gstatic.com', 'https:', 'data:'],
102
- frameSrc: ["'self'", 'https:'],
103
- frameAncestors: ["'none'"],
104
- objectSrc: ["'none'"],
105
- baseUri: ["'self'"],
106
- formAction: ["'self'"],
107
- upgradeInsecureRequests: process.env.NODE_ENV === 'production' ? [] : null,
108
- reportUri: [cspReportUri]
109
- }
110
- },
111
- crossOriginEmbedderPolicy: false,
112
- referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
113
- })
114
- );
115
-
116
- // Companion strict Report-Only CSP — surfaces every inline-script violation
117
- // without breaking existing pages, so we can migrate page-by-page to nonces.
118
- app.use((req, res, next) => {
119
- const nonce = res.locals.cspNonce;
120
- const strict = [
121
- "default-src 'self'",
122
- `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
123
- "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
124
- "img-src 'self' data: https:",
125
- "connect-src 'self' https: wss:",
126
- "font-src 'self' https://fonts.gstatic.com data:",
127
- "frame-src 'self' https:",
128
- "frame-ancestors 'none'",
129
- "object-src 'none'",
130
- "base-uri 'self'",
131
- "form-action 'self'",
132
- "upgrade-insecure-requests",
133
- `report-uri ${cspReportUri}`
134
- ].join('; ');
135
- res.setHeader('Content-Security-Policy-Report-Only', strict);
136
- next();
137
- });
138
-
139
- // CSP violation report sink (capped, in-memory ring buffer + console).
140
- const _cspReports = [];
141
- app.post('/api/security/csp-report', express.json({ type: ['application/csp-report', 'application/json'], limit: '32kb' }), (req, res) => {
142
- const report = req.body && (req.body['csp-report'] || req.body);
143
- if (report) {
144
- _cspReports.push({ at: new Date().toISOString(), ip: req.ip, report });
145
- if (_cspReports.length > 500) _cspReports.shift();
146
- if (process.env.NODE_ENV !== 'production') {
147
- console.warn('[CSP]', report['violated-directive'] || report.violatedDirective, '→', report['blocked-uri'] || report.blockedURI);
148
- }
149
- }
150
- res.status(204).end();
151
- });
152
- app.get('/api/security/csp-report/recent', (req, res) => {
153
- res.json({ count: _cspReports.length, reports: _cspReports.slice(-50) });
154
- });
155
-
156
- // ── Reward-guard + cross-site redactor admin views (token-gated) ──
157
- function _adminAuth(req, res, next) {
158
- const want = process.env.WAB_ADMIN_TOKEN;
159
- if (!want) return res.status(503).json({ error: 'WAB_ADMIN_TOKEN not configured' });
160
- const got = req.headers['x-wab-admin-token'] || req.query.token;
161
- if (got !== want) return res.status(401).json({ error: 'admin token required' });
162
- next();
163
- }
164
- app.get('/api/security/reward-audit/recent', _adminAuth, (req, res) => {
165
- try {
166
- const guard = require('./security/reward-guard');
167
- res.json({ stats: guard.getStats(), recent: guard.getRecentAudits(50, req.query.decision || null) });
168
- } catch (err) { res.status(500).json({ error: err.message }); }
169
- });
170
- app.get('/api/security/cross-site-transfers/recent', _adminAuth, (req, res) => {
171
- try {
172
- const r = require('./security/cross-site-redactor');
173
- res.json({ recent: r.getRecentTransfers(50, req.query.from || null) });
174
- } catch (err) { res.status(500).json({ error: err.message }); }
175
- });
176
- app.get('/api/security/url-policy/recent', _adminAuth, (req, res) => {
177
- try {
178
- const p = require('./security/url-policy');
179
- res.json({ recent: p.getRecentAudits(50, req.query.decision || null) });
180
- } catch (err) { res.status(500).json({ error: err.message }); }
181
- });
182
-
183
- app.post('/api/billing/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
184
- try {
185
- await handleWebhookRequest(req);
186
- res.json({ received: true });
187
- } catch (err) {
188
- console.error('Webhook error:', err.message);
189
- res.status(400).json({ error: err.message });
190
- }
191
- });
192
-
193
- app.use(express.json());
194
-
195
- // Global JSON parse error handler (catches malformed JSON from bots/scanners)
196
- app.use((err, req, res, next) => {
197
- if (err.type === 'entity.parse.failed' || err instanceof SyntaxError) {
198
- return res.status(400).json({ error: 'Invalid JSON', details: err.message });
199
- }
200
- next(err);
201
- });
202
-
203
- // Global error handler — catches all unhandled route errors
204
- // global-error-handler
205
- app.use((err, req, res, next) => {
206
- const status = err.status || err.statusCode || 500;
207
- const message = err.message || 'Internal Server Error';
208
- if (status >= 500) {
209
- console.error('[server] Unhandled error:', err.message, err.stack?.split('\n')[1] || '');
210
- }
211
- if (!res.headersSent) {
212
- res.status(status).json({ error: message });
213
- }
214
- });
215
-
216
- const apiLimiter = rateLimit({
217
- windowMs: 15 * 60 * 1000,
218
- max: 200,
219
- standardHeaders: true,
220
- legacyHeaders: false,
221
- message: { error: 'Too many requests, please try again later' }
222
- });
223
-
224
- const licenseLimiter = rateLimit({
225
- windowMs: 60 * 1000,
226
- max: 120,
227
- standardHeaders: true,
228
- legacyHeaders: false,
229
- keyGenerator: (req) => {
230
- const key = req.body?.licenseKey || req.body?.siteId || req.ip;
231
- return `${req.ip}:${key}`;
232
- }
233
- });
234
-
235
- // Visitor analytics — record every public page hit (HTML routes only) before
236
- // they're served by express.static. Skips assets, /api, /admin and other noise.
237
- try {
238
- const visitorTracker = require('./services/visitor-tracker');
239
- app.use(visitorTracker.middleware());
240
- } catch (e) {
241
- console.warn('[wab] visitor-tracker disabled:', e.message);
242
- }
243
-
244
- // Whitepaper guard — must run BEFORE express.static so we can apply strict headers
245
- // and intercept both /whitepaper and /whitepaper.html with the same protections.
246
- const whitepaperHandler = (req, res) => {
247
- res.set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
248
- res.set('Pragma', 'no-cache');
249
- res.set('Expires', '0');
250
- res.set('X-Frame-Options', 'DENY');
251
- res.set('X-Content-Type-Options', 'nosniff');
252
- res.set('Referrer-Policy', 'strict-origin-when-cross-origin');
253
- res.set('X-Robots-Tag', 'index, follow, noarchive, nosnippet, noimageindex');
254
- res.set('Content-Security-Policy', "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'");
255
- res.set('X-Copyright', 'All Rights Reserved (c) 2026 Web Agent Bridge - Reproduction Prohibited');
256
- res.sendFile(path.join(__dirname, '..', 'public', 'whitepaper.html'));
257
- };
258
- app.get(['/whitepaper', '/whitepaper.html'], whitepaperHandler);
259
-
260
- // WAB Trust artifact (signed Ed25519 wab.json) — served explicitly because
261
- // express.static skips dotfile directories like /.well-known by default.
262
- app.get('/.well-known/wab.json', (req, res) => {
263
- res.set('Cache-Control', 'public, max-age=300');
264
- res.set('Access-Control-Allow-Origin', '*');
265
- res.type('application/json');
266
- res.sendFile(path.join(__dirname, '..', 'public', '.well-known', 'wab.json'));
267
- });
268
-
269
- app.use(express.static(path.join(__dirname, '..', 'public'), {
270
- setHeaders(res, filePath) {
271
- if (filePath.endsWith('.html')) {
272
- res.setHeader('Cache-Control', 'no-cache, must-revalidate');
273
- }
274
- }
275
- }));
276
- app.use('/script', express.static(path.join(__dirname, '..', 'script')));
277
-
278
- app.use('/api/auth', apiLimiter, authRoutes);
279
- app.use('/api', apiLimiter, apiRoutes);
280
- app.use('/api/license', licenseLimiter, licenseRoutes);
281
- app.use('/api/admin', apiLimiter, adminRoutes);
282
- app.use('/api/billing', apiLimiter, billingRoutes);
283
- app.use('/api/sovereign', apiLimiter, sovereignRoutes);
284
- app.use('/api/mesh', apiLimiter, meshRoutes);
285
- app.use('/api/commander', apiLimiter, commanderRoutes);
286
- app.use('/api/ads', apiLimiter, adsRoutes);
287
- app.use('/api/wab', wabApiRoutes);
288
- app.use('/api/noscript', apiLimiter, noscriptRoutes);
289
- app.use('/api/discovery', apiLimiter, discoveryRoutes);
290
- app.use('/api/activate', apiLimiter, require('./routes/activate'));
291
-
292
- // ── WAB Advanced Features v1.0 ──────────────────────────────────────────────
293
- const { reputationRouter, collectiveRouter } = require('./routes/reputation');
294
- const { intentRouter, privacyRouter } = require('./routes/intent');
295
- const { cacheRouter, offlineRouter } = require('./routes/wab-cache');
296
- // Trust Graph tier gate — tags & meters anonymous + keyed traffic.
297
- // Mounted BEFORE the routers so it sees their requests.
298
- const { apiTierMiddleware } = require('./middleware/api-tier');
299
- app.use(['/api/reputation', '/api/truth', '/api/ring4/status'], apiTierMiddleware);
300
- app.use('/api/reputation', apiLimiter, reputationRouter);
301
- app.use('/api/collective', apiLimiter, collectiveRouter);
302
- app.use('/api/intent', apiLimiter, intentRouter);
303
- app.use('/api/privacy', apiLimiter, privacyRouter);
304
- app.use('/api/cache', apiLimiter, cacheRouter);
305
- app.use('/api/offline', apiLimiter, offlineRouter);
306
-
307
- // ── WAB Truth Layer v1.0 (Semantic Memory + Temporal Trust + Action Graphs + Reality Anchor) ──
308
- const { truthRouter } = require('./routes/truth-layer');
309
- app.use('/api/truth', apiLimiter, truthRouter);
310
-
311
- // ── WAB Ring 4 External Trust Verification (sovereign-agent trust API) ──
312
- const { ring4Router } = require('./routes/ring4');
313
- const { wabTrustMiddleware } = require('./middleware/wab-trust');
314
- app.use(wabTrustMiddleware);
315
- app.use('/api/ring4', apiLimiter, ring4Router);
316
-
317
- // ── Agent Transaction Primitive (ATP) v3.9.0 — intents · transactions · signed receipts ──
318
- app.use('/api/atp', apiLimiter, require('./routes/transactions'));
319
-
320
- // ── Site Revocations & Appeals v3.11.0 — public transparency + owner appeals ──
321
- app.use('/api/revocations', apiLimiter, require('./routes/revocations'));
322
-
323
- // ── Agent-Driven Adoption v3.12.0 — canonical LLM agent system prompt ──
324
- app.use('/api/agent', apiLimiter, require('./routes/agent-prompt'));
325
-
326
- // ── Network Effect v3.14.0 — trusted-domains snapshot + revocations feeds ──
327
- // (apiLimiter already applies via /api mount above; do not stack it here.)
328
- app.use('/api', require('./routes/network'));
329
-
330
- // ── Webhook Subscriptions v3.16.0 (Phase 4) — instant push for revocations ──
331
- app.use('/api/webhooks', apiLimiter, require('./routes/webhooks'));
332
-
333
- // ── WAB Commercial Foundations v3.8.0 (Partners · Trust Graph API · Governance SaaS · Enterprise Mesh) ──
334
- app.use('/api/partners', apiLimiter, require('./routes/partners'));
335
- app.use('/api/keys', apiLimiter, require('./routes/api-keys'));
336
- app.use('/api/governance-saas', apiLimiter, require('./routes/governance-saas'));
337
- app.use('/api/enterprise-mesh', apiLimiter, require('./routes/enterprise-mesh'));
338
- // Trust Graph tier gate is mounted earlier (before /api/reputation et al.)
339
- // ─────────────────────────────────────────────────────────────────────────────
340
-
341
- app.use('/api/providers', apiLimiter, providerRoutes);
342
- app.use('/api/governance', apiLimiter, governanceRoutes);
343
- app.use('/api/plans', apiLimiter, require('./routes/plans'));
344
- app.use('/api/admin/plans', apiLimiter, require('./routes/admin-plans'));
345
- app.use('/api/admin/shieldqr', apiLimiter, require('./routes/admin-shieldqr'));
346
- app.use('/api/admin/trust-monitor', apiLimiter, require('./routes/admin-trust-monitor'));
347
- // Optional premium modules — mounted only when present (open-source repo
348
- // excludes the ShieldLink stack which is a paid feature).
349
- function mountOptional(prefix, modPath) {
350
- try { app.use(prefix, apiLimiter, require(modPath)); }
351
- catch (e) {
352
- if (e.code === 'MODULE_NOT_FOUND' && e.message.includes(modPath)) {
353
- console.log(`[optional] ${prefix} not mounted (${modPath} not present)`);
354
- } else { throw e; }
355
- }
356
- }
357
- mountOptional('/api/admin/shieldlink', './routes/admin-shieldlink');
358
- app.use('/api/shieldqr', apiLimiter, require('./routes/shieldqr'));
359
- mountOptional('/api/shieldlink', './routes/shieldlink');
360
- mountOptional('/api/customer/shieldlink','./routes/customer-shieldlink');
361
- app.use('/api/adopt', apiLimiter, require('./routes/adopt'));
362
- app.use('/api/diagnose', apiLimiter, require('./routes/diagnose'));
363
- app.use('/api/admin/outreach', apiLimiter, require('./routes/admin-outreach'));
364
- app.use('/', apiLimiter, require('./routes/unsubscribe'));
365
- // Also expose well-known discovery endpoints at the canonical root paths so
366
- // agents can find them without the /api/discovery prefix (RFC 8615).
367
-
368
- // /activate WAB DNS Discovery activation guide (bilingual)
369
- app.get('/activate', noCache, (req, res) => {
370
- res.sendFile(path.join(__dirname, '..', 'public', 'activate.html'));
371
- });
372
-
373
- // /one-click interactive self-serve activation wizard (key-gen, sign, deploy via API)
374
- app.get(['/one-click', '/one-click.html', '/activate/one-click'], noCache, (req, res) => {
375
- res.sendFile(path.join(__dirname, '..', 'public', 'one-click.html'));
376
- });
377
-
378
- // /wab-features — WAB Advanced Features showcase (Reputation, Cache, Intent, Privacy, Collective, Offline)
379
- app.get(['/wab-features', '/features'], noCache, (req, res) => {
380
- res.sendFile(path.join(__dirname, '..', 'public', 'wab-features.html'));
381
- });
382
- // /wab-truth WAB Truth Layer showcase (Semantic Memory + Temporal Trust + Action Graphs + Reality Anchor)
383
- app.get(['/wab-truth', '/truth'], noCache, (req, res) => {
384
- res.sendFile(path.join(__dirname, '..', 'public', 'wab-truth.html'));
385
- });
386
- // /milestones Partners & Milestones (VEXR Ultra × WAB Ring 4 integration)
387
- app.get(['/milestones'], noCache, (req, res) => {
388
- res.sendFile(path.join(__dirname, '..', 'public', 'milestones.html'));
389
- });
390
- // /partners — Certified Partner Program (3 tiers · self-serve)
391
- app.get(['/partners', '/partners.html'], noCache, (req, res) => {
392
- res.sendFile(path.join(__dirname, '..', 'public', 'partners.html'));
393
- });
394
- // /trust-graph-api — Trust Graph API docs & self-serve key issuance
395
- app.get(['/trust-graph-api', '/trust-graph-api.html'], noCache, (req, res) => {
396
- res.sendFile(path.join(__dirname, '..', 'public', 'trust-graph-api.html'));
397
- });
398
- // /governance — Governance SaaS landing (EU AI Act audit trail)
399
- app.get(['/governance', '/governance.html'], noCache, (req, res) => {
400
- res.sendFile(path.join(__dirname, '..', 'public', 'governance.html'));
401
- });
402
- // /enterprise-mesh — Self-hosted Enterprise Mesh contact
403
- app.get(['/enterprise-mesh', '/enterprise-mesh.html', '/enterprise'], noCache, (req, res) => {
404
- res.sendFile(path.join(__dirname, '..', 'public', 'enterprise-mesh.html'));
405
- });
406
- // /ring4 Ring 4 Trust Handshake protocol docs
407
- app.get(['/ring4', '/trust-handshake'], noCache, (req, res) => {
408
- res.sendFile(path.join(__dirname, '..', 'public', 'ring4.html'));
409
- });
410
- // /refusals Public refusal log (anonymized constitutional refusal stats)
411
- app.get('/refusals', noCache, (req, res) => {
412
- res.sendFile(path.join(__dirname, '..', 'public', 'refusals.html'));
413
- });
414
- // /.well-known/jwks.json — standard JWKS discovery for OIDC/JWT ecosystem
415
- app.get('/.well-known/jwks.json', (req, res) => {
416
- try {
417
- const { _internals } = require('./routes/ring4');
418
- return res.json(_internals.buildJwks());
419
- } catch (e) {
420
- return res.status(503).json({ error: 'jwks_unavailable', detail: e.message });
421
- }
422
- });
423
- app.get('/shieldqr', noCache, (req, res) => {
424
- res.sendFile(path.join(__dirname, '..', 'public', 'shieldqr.html'));
425
- });
426
- // ── ShieldLink landing + Trust Preview redirect ──
427
- app.get('/shieldlink', noCache, (req, res) => {
428
- res.sendFile(path.join(__dirname, '..', 'public', 'shieldlink.html'));
429
- });
430
- app.get('/l/:token', noCache, (req, res) => {
431
- // Serve the Trust Preview page; the page calls /api/shieldlink/verify?token=
432
- res.sendFile(path.join(__dirname, '..', 'public', 'l-preview.html'));
433
- });
434
- app.get('/dashboard/shieldlink', noCache, (req, res) => {
435
- res.sendFile(path.join(__dirname, '..', 'public', 'dashboard-shieldlink.html'));
436
- });
437
- app.get('/activate-dns', noCache, (req, res) => {
438
- res.sendFile(path.join(__dirname, '..', 'public', 'activate.html'));
439
- });
440
- app.get('/provider-onboarding', noCache, (req, res) => {
441
- res.sendFile(path.join(__dirname, '..', 'public', 'provider-onboarding.html'));
442
- });
443
- app.get('/provider-sandbox', noCache, (req, res) => {
444
- res.sendFile(path.join(__dirname, '..', 'public', 'provider-sandbox.html'));
445
- });
446
- app.get('/cloudflare-integration', noCache, (req, res) => {
447
- res.sendFile(path.join(__dirname, '..', 'public', 'cloudflare-integration.html'));
448
- });
449
- app.get('/cpanel-integration', noCache, (req, res) => {
450
- res.sendFile(path.join(__dirname, '..', 'public', 'cpanel-integration.html'));
451
- });
452
- app.get('/route53-integration', noCache, (req, res) => {
453
- res.sendFile(path.join(__dirname, '..', 'public', 'route53-integration.html'));
454
- });
455
- app.get('/plesk-integration', noCache, (req, res) => {
456
- res.sendFile(path.join(__dirname, '..', 'public', 'plesk-integration.html'));
457
- });
458
- app.get('/gcp-dns-integration', noCache, (req, res) => {
459
- res.sendFile(path.join(__dirname, '..', 'public', 'gcp-dns-integration.html'));
460
- });
461
- app.get('/azure-dns-integration', noCache, (req, res) => {
462
- res.sendFile(path.join(__dirname, '..', 'public', 'azure-dns-integration.html'));
463
- });
464
- app.get('/registrar-integrations', noCache, (req, res) => {
465
- res.sendFile(path.join(__dirname, '..', 'public', 'registrar-integrations.html'));
466
- });
467
- app.get('/adoption-metrics', noCache, (req, res) => {
468
- res.sendFile(path.join(__dirname, '..', 'public', 'adoption-metrics.html'));
469
- });
470
- app.get('/adopt', noCache, (req, res) => {
471
- res.sendFile(path.join(__dirname, '..', 'public', 'adopt.html'));
472
- });
473
- app.get('/wab-trust', noCache, (req, res) => {
474
- res.sendFile(path.join(__dirname, '..', 'public', 'wab-trust.html'));
475
- });
476
- app.get('/wab-vs-protocols', noCache, (req, res) => {
477
- res.sendFile(path.join(__dirname, '..', 'public', 'wab-vs-protocols.html'));
478
- });
479
- app.use('/', apiLimiter, discoveryRoutes);
480
- app.use('/api/premium', apiLimiter, premiumRoutes);
481
- app.use('/api/admin/premium', apiLimiter, adminPremiumRoutes);
482
- app.use('/api/workspace', apiLimiter, workspaceRoutes);
483
- app.use('/api/universal', apiLimiter, universalRoutes);
484
- app.use('/api/os', apiLimiter, runtimeRoutes);
485
- app.use('/api/demo', apiLimiter, demoShowcaseRoutes);
486
- app.use('/api/growth', apiLimiter, growthRoutes);
487
- app.use('/api/v1', gatewayRoutes);
488
-
489
- // Convenience alias: /api/negotiate/* /api/sovereign/negotiation/*
490
- app.get('/api/negotiate', apiLimiter, (req, res) => {
491
- res.json({
492
- engine: 'WAB Negotiation Engine',
493
- endpoints: {
494
- 'POST /api/negotiate/rules': 'Create negotiation rules (auth required)',
495
- 'GET /api/negotiate/rules/:siteId': 'Get rules for a site',
496
- 'PUT /api/negotiate/rules/:ruleId': 'Update a rule (auth required)',
497
- 'POST /api/negotiate/sessions': 'Open negotiation session',
498
- 'POST /api/negotiate/sessions/:id/propose': 'Agent counter-offer',
499
- 'POST /api/negotiate/sessions/:id/confirm': 'Confirm deal',
500
- 'GET /api/negotiate/stats/:siteId': 'Negotiation stats',
501
- },
502
- });
503
- });
504
- app.use('/api/negotiate', apiLimiter, (req, res, next) => {
505
- req.url = '/negotiation' + req.url;
506
- sovereignRoutes(req, res, next);
507
- });
508
-
509
- // ─── WAB Search Engine ────────────────────────────────────────────────
510
-
511
- const searchLimiter = rateLimit({
512
- windowMs: 60 * 1000,
513
- max: 30,
514
- standardHeaders: true,
515
- legacyHeaders: false,
516
- message: { error: 'Too many search requests, please slow down' }
517
- });
518
-
519
- app.get('/api/search', searchLimiter, async (req, res) => {
520
- const q = (req.query.q || '').trim();
521
- if (!q) return res.json({ results: [], cached: false });
522
- if (q.length > 200) return res.status(400).json({ error: 'Query too long' });
523
- const crypto = require('crypto');
524
- const ipHash = crypto.createHash('sha256').update(req.ip || '').digest('hex').slice(0, 16);
525
- const result = await search(q, ipHash);
526
- res.json(result);
527
- });
528
-
529
- app.get('/api/search/suggest', searchLimiter, (req, res) => {
530
- const q = (req.query.q || '').trim();
531
- if (!q) return res.json({ suggestions: [] });
532
- const suggestions = getSuggestions(q, 8);
533
- res.json({ suggestions });
534
- });
535
-
536
- app.get('/api/search/trending', apiLimiter, (req, res) => {
537
- const trending = getTrendingSearches(10);
538
- res.json({ trending });
539
- });
540
-
541
- app.get('/api/search/stats', apiLimiter, (req, res) => {
542
- const stats = getSearchStats();
543
- res.json(stats);
544
- });
545
-
546
- // Prevent browsers from caching HTML page routes
547
- function noCache(req, res, next) {
548
- res.set('Cache-Control', 'no-cache, must-revalidate');
549
- next();
550
- }
551
-
552
- app.get('/dashboard', noCache, (req, res) => {
553
- res.sendFile(path.join(__dirname, '..', 'public', 'dashboard.html'));
554
- });
555
- app.get('/providers', noCache, (req, res) => {
556
- res.sendFile(path.join(__dirname, '..', 'public', 'providers.html'));
557
- });
558
- app.get('/mesh-dashboard', noCache, (req, res) => {
559
- res.sendFile(path.join(__dirname, '..', 'public', 'mesh-dashboard.html'));
560
- });
561
- app.get('/commander-dashboard', noCache, (req, res) => {
562
- res.sendFile(path.join(__dirname, '..', 'public', 'commander-dashboard.html'));
563
- });
564
- app.get('/docs', noCache, (req, res) => {
565
- res.sendFile(path.join(__dirname, '..', 'public', 'docs.html'));
566
- });
567
- app.get('/login', noCache, (req, res) => {
568
- res.sendFile(path.join(__dirname, '..', 'public', 'login.html'));
569
- });
570
- app.get('/register', noCache, (req, res) => {
571
- res.sendFile(path.join(__dirname, '..', 'public', 'register.html'));
572
- });
573
- app.get('/admin/login', noCache, (req, res) => {
574
- res.sendFile(path.join(__dirname, '..', 'public', 'admin', 'login.html'));
575
- });
576
- app.get('/admin', noCache, (req, res) => {
577
- res.sendFile(path.join(__dirname, '..', 'public', 'admin', 'dashboard.html'));
578
- });
579
- app.get('/admin/snapshots', noCache, (req, res) => {
580
- res.sendFile(path.join(__dirname, '..', 'public', 'admin', 'snapshots.html'));
581
- });
582
-
583
- // ─── Admin sub-pages (each backed by real API endpoints in /api/admin/*) ──
584
- ['users','sites','analytics','grants','payments','stripe','smtp','notifications','governance','discovery','trust','providers','plans','shieldqr','shieldlink','trust-monitor','outreach'].forEach((page) => {
585
- app.get('/admin/' + page, noCache, (req, res) => {
586
- res.sendFile(path.join(__dirname, '..', 'public', 'admin', page + '.html'));
587
- });
588
- });
589
- app.get('/privacy', noCache, (req, res) => {
590
- res.sendFile(path.join(__dirname, '..', 'public', 'privacy.html'));
591
- });
592
- app.get('/terms', noCache, (req, res) => {
593
- res.sendFile(path.join(__dirname, '..', 'public', 'terms.html'));
594
- });
595
- app.get('/cookies', noCache, (req, res) => {
596
- res.sendFile(path.join(__dirname, '..', 'public', 'cookies.html'));
597
- });
598
- app.get('/browser', noCache, (req, res) => {
599
- res.sendFile(path.join(__dirname, '..', 'public', 'browser.html'));
600
- });
601
- app.get('/workspace', noCache, (req, res) => {
602
- res.sendFile(path.join(__dirname, '..', 'public', 'agent-workspace.html'));
603
- });
604
- app.get('/growth', noCache, (req, res) => {
605
- res.sendFile(path.join(__dirname, '..', 'public', 'growth.html'));
606
- });
607
- app.get('/score', noCache, (req, res) => {
608
- res.sendFile(path.join(__dirname, '..', 'public', 'score.html'));
609
- });
610
- app.get('/sovereign', noCache, (req, res) => {
611
- res.sendFile(path.join(__dirname, '..', 'public', 'sovereign.html'));
612
- });
613
- app.get('/api', noCache, (req, res) => {
614
- res.sendFile(path.join(__dirname, '..', 'public', 'api.html'));
615
- });
616
-
617
- app.get('/phone-shield', noCache, (req, res) => {
618
- res.sendFile(path.join(__dirname, '..', 'public', 'phone-shield.html'));
619
- });
620
-
621
- app.get('/dns', noCache, (req, res) => {
622
- res.sendFile(path.join(__dirname, '..', 'public', 'dns.html'));
623
- });
624
-
625
- // /integrationsbilingual deploy landing page
626
- app.get('/integrations', noCache, (req, res) => {
627
- res.sendFile(path.join(__dirname, '..', 'public', 'integrations.html'));
628
- });
629
-
630
- // /demo interactive WAB Demo Store (new)
631
- app.use('/demo', demoStoreRoutes);
632
-
633
- // Browser downloads
634
- app.use('/downloads', express.static(path.join(__dirname, '..', 'downloads'), {
635
- maxAge: '1d',
636
- setHeaders: (res, filePath) => {
637
- // Shell scripts served as plain text for curl | bash usage
638
- if (filePath.endsWith('.sh')) {
639
- res.set('Content-Type', 'text/plain; charset=utf-8');
640
- } else {
641
- res.set('Content-Disposition', 'attachment');
642
- }
643
- }
644
- }));
645
-
646
- // WAB Discovery install shortcut: curl -fsSL https://webagentbridge.com/install | bash
647
- app.get('/install', (req, res) => {
648
- res.set('Content-Type', 'text/plain; charset=utf-8');
649
- res.sendFile(path.join(__dirname, '..', 'downloads', 'quick-wab.sh'));
650
- });
651
-
652
- // Agent chat endpoint for WAB Browser — Real AI Agent
653
- const chatLimiter = rateLimit({
654
- windowMs: 60 * 1000,
655
- max: 20,
656
- standardHeaders: true,
657
- legacyHeaders: false,
658
- message: { error: 'Too many messages, please slow down' }
659
- });
660
-
661
- app.post('/api/wab/agent-chat', chatLimiter, async (req, res) => {
662
- const { message, context, sessionId, taskId, taskAction } = req.body || {};
663
- if (!message || typeof message !== 'string') {
664
- return res.status(400).json({ error: 'Message required' });
665
- }
666
- if (message.length > 3000) {
667
- return res.status(400).json({ error: 'Message too long' });
668
- }
669
-
670
- const sid = sessionId || req.ip || 'anonymous';
671
-
672
- try {
673
- // ── Task actions (user responding to an active task) ──
674
- if (taskId && taskAction) {
675
- if (taskAction === 'answer') {
676
- const result = agentTasks.answerClarification(taskId, message);
677
- if (result.status === 'planning') {
678
- // Auto-execute after planning
679
- const execResult = await agentTasks.executeTask(taskId);
680
- return res.json({ ...execResult, type: 'task' });
681
- }
682
- return res.json({ ...result, type: 'task' });
683
- }
684
- if (taskAction === 'select') {
685
- const idx = parseInt(message.replace(/\D/g, '')) - 1;
686
- const result = agentTasks.selectOffer(taskId, idx);
687
- return res.json({ ...result, type: 'task' });
688
- }
689
- if (taskAction === 'cancel') {
690
- const result = agentTasks.cancelTask(taskId);
691
- return res.json({ ...result, type: 'task' });
692
- }
693
- }
694
-
695
- // ── Check if user wants to select from existing offers ──
696
- if (!taskId) {
697
- const selectMatch = message.match(/(?:اختر|اخت(?:ا|ي)ر|select|choose|pick)\s*(\d+)/i);
698
- if (selectMatch) {
699
- const tasks = agentTasks.getSessionTasks(sid, 1);
700
- if (tasks.length > 0 && tasks[0].status === 'presenting') {
701
- const idx = parseInt(selectMatch[1]) - 1;
702
- const result = agentTasks.selectOffer(tasks[0].id, idx);
703
- return res.json({ ...result, type: 'task' });
704
- }
705
- }
706
- }
707
-
708
- // ── Detect URL paste create URL negotiation task ──
709
- const urlData = agentTasks.parseBookingUrl(message);
710
- if (urlData) {
711
- const task = agentTasks.createUrlTask(sid, message, urlData);
712
- const execResult = await agentTasks.executeUrlTask(task.taskId);
713
- return res.json({ ...execResult, type: 'task', urlData });
714
- }
715
-
716
- // ── Detect if this is a task-type request (booking, shopping, etc.) ──
717
- const intent = agentTasks.detectIntent(message);
718
- if (intent.confidence >= 0.7 && intent.intent !== 'general') {
719
- const task = agentTasks.createTask(sid, message);
720
-
721
- if (task.status === 'clarifying') {
722
- return res.json({ ...task, type: 'task' });
723
- }
724
-
725
- // If requirements are complete, auto-execute
726
- const execResult = await agentTasks.executeTask(task.taskId);
727
- return res.json({ ...execResult, type: 'task' });
728
- }
729
-
730
- // ── Regular chat (not a task) ──
731
- const chatContext = {
732
- url: context?.url || '',
733
- platform: context?.platform || 'unknown',
734
- sessionId: sid,
735
- };
736
- const result = await agentChat(message, chatContext);
737
- res.json(result);
738
- } catch (err) {
739
- console.error('[agent-chat] Error:', err.message);
740
- res.json({ reply: '🤖 عذراً، حدث خطأ. حاول مرة أخرى.', type: 'text' });
741
- }
742
- });
743
-
744
- // Agent task status & history
745
- app.get('/api/wab/agent-task/:id', chatLimiter, (req, res) => {
746
- const state = agentTasks.getTaskState(req.params.id);
747
- if (!state) return res.status(404).json({ error: 'Task not found' });
748
- res.json(state);
749
- });
750
-
751
- app.get('/api/wab/agent-tasks', chatLimiter, (req, res) => {
752
- const sid = req.query.sessionId || req.ip || 'anonymous';
753
- const tasks = agentTasks.getSessionTasks(sid, 20);
754
- res.json({ tasks });
755
- });
756
-
757
- const pkg = require('../package.json');
758
- app.use(`/v${pkg.version.split('.')[0]}`, express.static(path.join(__dirname, '..', 'script')));
759
- app.use('/latest', express.static(path.join(__dirname, '..', 'script')));
760
-
761
- app.get('*', (req, res) => {
762
- // API routes always return JSON 404
763
- if (req.path.startsWith('/api/')) {
764
- return res.status(404).json({ error: 'Not found', path: req.path });
765
- }
766
- if (req.accepts('html')) {
767
- res.sendFile(path.join(__dirname, '..', 'public', 'index.html'));
768
- } else {
769
- res.status(404).json({ error: 'Not found' });
770
- }
771
- });
772
-
773
-
774
- // Prevent PM2 restarts from uncaught errors — log and continue
775
- process.on('uncaughtException', (err) => {
776
- console.error('[process] uncaughtException:', err.message);
777
- });
778
- process.on('unhandledRejection', (reason) => {
779
- console.error('[process] unhandledRejection:', reason?.message || reason);
780
- });
781
-
782
- if (process.env.NODE_ENV !== 'test') {
783
- console.log('Running database migrations...');
784
- runMigrations();
785
- maybeBootstrapAdmin();
786
- initSearchEngine(db);
787
-
788
- // Purge old search cache every hour
789
- setInterval(purgeOldCache, 60 * 60 * 1000);
790
-
791
- const server = http.createServer(app);
792
- setupWebSocket(server);
793
-
794
- // Start Agent OS runtime
795
- runtime.start();
796
-
797
- // Start Cluster Orchestrator
798
- cluster.start();
799
-
800
- // Start the SSL Health Monitor cron (Extended Trust Layer).
801
- try { require('./services/ssl-monitor').start(); } catch (e) { console.warn('[ssl-monitor] start failed:', e.message); }
802
-
803
- // Start the Certificate Transparency Monitor (opt-in via WAB_CT_MONITOR=true).
804
- try { require('./services/ssl-ct-monitor').start(); } catch (e) { console.warn('[ct-monitor] start failed:', e.message); }
805
-
806
- // Start the ATP commission billing timer (opt-in via WAB_COMMISSION_BILLING_INTERVAL_HOURS).
807
- try {
808
- const r = require('./services/commission-billing').startPeriodicBilling();
809
- if (r) console.log(`[commission-billing] periodic cycle every ${r.intervalHours}h`);
810
- } catch (e) { console.warn('[commission-billing] start failed:', e.message); }
811
-
812
- // Start the revocation appeal-window sweep (opt-in via WAB_REVOCATION_SWEEP_INTERVAL_HOURS).
813
- try {
814
- const r = require('./services/revocations').startPeriodicSweep();
815
- if (r) console.log(`[revocations] periodic sweep every ${r.intervalHours}h`);
816
- } catch (e) { console.warn('[revocations] sweep start failed:', e.message); }
817
-
818
- server.listen(PORT, () => {
819
- console.log(`\n ╔══════════════════════════════════════════╗`);
820
- console.log(` ║ Web Agent Bridge v${pkg.version} ║`);
821
- console.log(` ║ Server running on http://localhost:${PORT} ║`);
822
- console.log(` ║ WebSocket: ws://localhost:${PORT}/ws/analytics ║`);
823
- console.log(` ╚══════════════════════════════════════════╝\n`);
824
- });
825
- }
826
-
827
- module.exports = app;
1
+ require('dotenv').config();
2
+
3
+ const { assertSecretsAtStartup } = require('./config/secrets');
4
+ assertSecretsAtStartup();
5
+
6
+ const express = require('express');
7
+ const http = require('http');
8
+ const cors = require('cors');
9
+ const helmet = require('helmet');
10
+ const rateLimit = require('express-rate-limit');
11
+ const path = require('path');
12
+ const { setupWebSocket } = require('./ws');
13
+ const { runMigrations } = require('./utils/migrate');
14
+ const { safeFetch } = require('./utils/safe-fetch');
15
+ const { maybeBootstrapAdmin, db } = require('./models/db');
16
+ const { initSearchEngine, search, getSuggestions, getTrendingSearches, getSearchStats, purgeOldCache } = require('./services/search-engine');
17
+ const { processMessage: agentChat } = require('./services/agent-chat');
18
+ const agentTasks = require('./services/agent-tasks');
19
+ const { cluster } = require('./services/cluster');
20
+
21
+ const authRoutes = require('./routes/auth');
22
+ const apiRoutes = require('./routes/api');
23
+ const licenseRoutes = require('./routes/license');
24
+ const adminRoutes = require('./routes/admin');
25
+ const billingRoutes = require('./routes/billing');
26
+ const geniusGateway = require('./routes/genius-gateway');
27
+ const sovereignRoutes = require('./routes/sovereign');
28
+ const meshRoutes = require('./routes/mesh');
29
+ const commanderRoutes = require('./routes/commander');
30
+ const adsRoutes = require('./routes/ads');
31
+ const wabApiRoutes = require('./routes/wab-api');
32
+ const noscriptRoutes = require('./routes/noscript');
33
+ const discoveryRoutes = require('./routes/discovery');
34
+ const providerRoutes = require('./routes/providers');
35
+ const governanceRoutes = require('./routes/governance');
36
+ const premiumRoutes = require('./routes/premium');
37
+ const adminPremiumRoutes = require('./routes/admin-premium');
38
+ const workspaceRoutes = require('./routes/agent-workspace');
39
+ const universalRoutes = require('./routes/universal');
40
+ const runtimeRoutes = require('./routes/runtime');
41
+ const demoShowcaseRoutes = require('./routes/demo-showcase');
42
+ const demoStoreRoutes = require('./routes/demo-store');
43
+ const gatewayRoutes = require('./routes/gateway');
44
+ let growthRoutes;
45
+ try { growthRoutes = require('./routes/growth'); } catch { growthRoutes = require('express').Router(); }
46
+ const { handleWebhookRequest } = require('./services/stripe');
47
+ const { runtime } = require('./runtime');
48
+
49
+ const app = express();
50
+ const PORT = process.env.PORT || 3000;
51
+
52
+ app.set('trust proxy', 1);
53
+
54
+ const corsOrigins = (process.env.ALLOWED_ORIGINS
55
+ || 'http://localhost:3000,http://127.0.0.1:3000,http://localhost:5173')
56
+ .split(',')
57
+ .map((s) => s.trim())
58
+ .filter(Boolean);
59
+
60
+ app.use(
61
+ cors({
62
+ origin(origin, callback) {
63
+ if (!origin) return callback(null, true);
64
+ if (corsOrigins.includes(origin)) return callback(null, true);
65
+ if (process.env.NODE_ENV !== 'production' && /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i.test(origin)) {
66
+ return callback(null, true);
67
+ }
68
+ return callback(null, false);
69
+ },
70
+ credentials: true
71
+ })
72
+ );
73
+
74
+ const scriptSrc = process.env.CSP_ALLOW_UNSAFE_INLINE === 'false'
75
+ ? ["'self'", 'https://unpkg.com', 'https://cdn.jsdelivr.net']
76
+ : ["'self'", "'unsafe-inline'", 'https://unpkg.com', 'https://cdn.jsdelivr.net'];
77
+ const styleSrc = process.env.CSP_ALLOW_UNSAFE_INLINE === 'false'
78
+ ? ["'self'"]
79
+ : ["'self'", "'unsafe-inline'"];
80
+
81
+ // Per-request CSP nonce — exposed as res.locals.cspNonce for new pages opting into strict CSP.
82
+ app.use((req, res, next) => {
83
+ res.locals.cspNonce = require('crypto').randomBytes(16).toString('base64');
84
+ next();
85
+ });
86
+
87
+ // CSP — tightened: HTTPS-only iframes, upgrade-insecure-requests, report endpoint.
88
+ const cspReportUri = '/api/security/csp-report';
89
+ app.use(
90
+ helmet({
91
+ contentSecurityPolicy: {
92
+ directives: {
93
+ defaultSrc: ["'self'"],
94
+ // NOTE: Adding a nonce alongside 'unsafe-inline' makes browsers ignore
95
+ // 'unsafe-inline' (CSP3 spec). All existing public/admin pages still
96
+ // rely on inline <script> blocks, so we keep 'unsafe-inline' enforced
97
+ // here and use the Report-Only policy below to track nonce migration.
98
+ scriptSrc: scriptSrc,
99
+ scriptSrcAttr: [...scriptSrc, "'unsafe-hashes'"],
100
+ styleSrc: [...styleSrc, 'https://fonts.googleapis.com'],
101
+ imgSrc: ["'self'", 'data:', 'https:'],
102
+ connectSrc: ["'self'", 'https:', 'ws:', 'wss:'],
103
+ fontSrc: ["'self'", 'https://fonts.gstatic.com', 'https:', 'data:'],
104
+ frameSrc: ["'self'", 'https:'],
105
+ frameAncestors: ["'none'"],
106
+ objectSrc: ["'none'"],
107
+ baseUri: ["'self'"],
108
+ formAction: ["'self'"],
109
+ upgradeInsecureRequests: process.env.NODE_ENV === 'production' ? [] : null,
110
+ reportUri: [cspReportUri]
111
+ }
112
+ },
113
+ crossOriginEmbedderPolicy: false,
114
+ referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
115
+ })
116
+ );
117
+
118
+ // Companion strict Report-Only CSP — surfaces every inline-script violation
119
+ // without breaking existing pages, so we can migrate page-by-page to nonces.
120
+ app.use((req, res, next) => {
121
+ const nonce = res.locals.cspNonce;
122
+ const strict = [
123
+ "default-src 'self'",
124
+ `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
125
+ "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
126
+ "img-src 'self' data: https:",
127
+ "connect-src 'self' https: wss:",
128
+ "font-src 'self' https://fonts.gstatic.com data:",
129
+ "frame-src 'self' https:",
130
+ "frame-ancestors 'none'",
131
+ "object-src 'none'",
132
+ "base-uri 'self'",
133
+ "form-action 'self'",
134
+ "upgrade-insecure-requests",
135
+ `report-uri ${cspReportUri}`
136
+ ].join('; ');
137
+ res.setHeader('Content-Security-Policy-Report-Only', strict);
138
+ next();
139
+ });
140
+
141
+ // CSP violation report sink (capped, in-memory ring buffer + console).
142
+ const _cspReports = [];
143
+ app.post('/api/security/csp-report', express.json({ type: ['application/csp-report', 'application/json'], limit: '32kb' }), (req, res) => {
144
+ const report = req.body && (req.body['csp-report'] || req.body);
145
+ if (report) {
146
+ _cspReports.push({ at: new Date().toISOString(), ip: req.ip, report });
147
+ if (_cspReports.length > 500) _cspReports.shift();
148
+ if (process.env.NODE_ENV !== 'production') {
149
+ console.warn('[CSP]', report['violated-directive'] || report.violatedDirective, '→', report['blocked-uri'] || report.blockedURI);
150
+ }
151
+ }
152
+ res.status(204).end();
153
+ });
154
+ app.get('/api/security/csp-report/recent', (req, res) => {
155
+ res.json({ count: _cspReports.length, reports: _cspReports.slice(-50) });
156
+ });
157
+
158
+ // ── Reward-guard + cross-site redactor admin views (token-gated) ──
159
+ function _adminAuth(req, res, next) {
160
+ const { safeEqual } = require('./utils/safe-compare');
161
+ const want = process.env.WAB_ADMIN_TOKEN;
162
+ if (!want) return res.status(503).json({ error: 'WAB_ADMIN_TOKEN not configured' });
163
+ const got = req.headers['x-wab-admin-token'] || req.query.token;
164
+ if (!safeEqual(got, want)) return res.status(401).json({ error: 'admin token required' });
165
+ next();
166
+ }
167
+ app.get('/api/security/reward-audit/recent', _adminAuth, (req, res) => {
168
+ try {
169
+ const guard = require('./security/reward-guard');
170
+ res.json({ stats: guard.getStats(), recent: guard.getRecentAudits(50, req.query.decision || null) });
171
+ } catch (err) { res.status(500).json({ error: err.message }); }
172
+ });
173
+ app.get('/api/security/cross-site-transfers/recent', _adminAuth, (req, res) => {
174
+ try {
175
+ const r = require('./security/cross-site-redactor');
176
+ res.json({ recent: r.getRecentTransfers(50, req.query.from || null) });
177
+ } catch (err) { res.status(500).json({ error: err.message }); }
178
+ });
179
+ app.get('/api/security/url-policy/recent', _adminAuth, (req, res) => {
180
+ try {
181
+ const p = require('./security/url-policy');
182
+ res.json({ recent: p.getRecentAudits(50, req.query.decision || null) });
183
+ } catch (err) { res.status(500).json({ error: err.message }); }
184
+ });
185
+
186
+ app.post('/api/billing/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
187
+ try {
188
+ await handleWebhookRequest(req);
189
+ res.json({ received: true });
190
+ } catch (err) {
191
+ console.error('Webhook error:', err.message);
192
+ res.status(400).json({ error: err.message });
193
+ }
194
+ });
195
+
196
+ app.use(express.json());
197
+
198
+ // Global JSON parse error handler (catches malformed JSON from bots/scanners)
199
+ app.use((err, req, res, next) => {
200
+ if (err.type === 'entity.parse.failed' || err instanceof SyntaxError) {
201
+ return res.status(400).json({ error: 'Invalid JSON', details: err.message });
202
+ }
203
+ next(err);
204
+ });
205
+
206
+ // Global error handler catches all unhandled route errors
207
+ // global-error-handler
208
+ app.use((err, req, res, next) => {
209
+ const status = err.status || err.statusCode || 500;
210
+ const message = err.message || 'Internal Server Error';
211
+ if (status >= 500) {
212
+ console.error('[server] Unhandled error:', err.message, err.stack?.split('\n')[1] || '');
213
+ }
214
+ if (!res.headersSent) {
215
+ res.status(status).json({ error: message });
216
+ }
217
+ });
218
+
219
+ const apiLimiter = rateLimit({
220
+ windowMs: 15 * 60 * 1000,
221
+ max: 200,
222
+ standardHeaders: true,
223
+ legacyHeaders: false,
224
+ message: { error: 'Too many requests, please try again later' }
225
+ });
226
+
227
+ const licenseLimiter = rateLimit({
228
+ windowMs: 60 * 1000,
229
+ max: 120,
230
+ standardHeaders: true,
231
+ legacyHeaders: false,
232
+ keyGenerator: (req) => {
233
+ const key = req.body?.licenseKey || req.body?.siteId || req.ip;
234
+ return `${req.ip}:${key}`;
235
+ }
236
+ });
237
+
238
+ // Visitor analytics — record every public page hit (HTML routes only) before
239
+ // they're served by express.static. Skips assets, /api, /admin and other noise.
240
+ try {
241
+ const visitorTracker = require('./services/visitor-tracker');
242
+ app.use(visitorTracker.middleware());
243
+ } catch (e) {
244
+ console.warn('[wab] visitor-tracker disabled:', e.message);
245
+ }
246
+
247
+ // Whitepaper guard must run BEFORE express.static so we can apply strict headers
248
+ // and intercept both /whitepaper and /whitepaper.html with the same protections.
249
+ const whitepaperHandler = (req, res) => {
250
+ res.set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
251
+ res.set('Pragma', 'no-cache');
252
+ res.set('Expires', '0');
253
+ res.set('X-Frame-Options', 'DENY');
254
+ res.set('X-Content-Type-Options', 'nosniff');
255
+ res.set('Referrer-Policy', 'strict-origin-when-cross-origin');
256
+ res.set('X-Robots-Tag', 'index, follow, noarchive, nosnippet, noimageindex');
257
+ res.set('Content-Security-Policy', "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'");
258
+ res.set('X-Copyright', 'All Rights Reserved (c) 2026 Web Agent Bridge - Reproduction Prohibited');
259
+ res.sendFile(path.join(__dirname, '..', 'public', 'whitepaper.html'));
260
+ };
261
+ app.get(['/whitepaper', '/whitepaper.html'], whitepaperHandler);
262
+
263
+ // WAB Trust artifact (signed Ed25519 wab.json) — served explicitly because
264
+ // express.static skips dotfile directories like /.well-known by default.
265
+ // We compose: signed trust payload (untouched, from disk) + a top-level
266
+ // `actions` map so structural-agent platforms (e.g. The Code Genius) can
267
+ // discover and execute the public API surface without DOM scraping.
268
+ const WAB_ACTIONS_MANIFEST = {
269
+ v: '1.0',
270
+ name: 'Web Agent Bridge',
271
+ description: 'Structural API surface for agents — registry discovery, trust verification, reputation queries, and ShieldQR scanning.',
272
+ endpoint: 'https://webagentbridge.com',
273
+ actions: {
274
+ discover_sites: {
275
+ id: 'discover_sites',
276
+ description: 'Search the WAB registry for sites by intent tag, ring, or domain pattern.',
277
+ url: '/api/registry/discover',
278
+ method: 'GET',
279
+ inputs: {
280
+ intent: { type: 'string', required: false, description: 'Intent tag to filter by (e.g. "shop", "news")' },
281
+ ring: { type: 'number', required: false, description: 'Minimum trust ring (0–4)' },
282
+ limit: { type: 'number', required: false, description: 'Max results (default 20)' },
283
+ },
284
+ },
285
+ list_sites: {
286
+ id: 'list_sites',
287
+ description: 'List all active WAB-enabled sites in the public registry.',
288
+ url: '/api/registry/list',
289
+ method: 'GET',
290
+ inputs: {
291
+ limit: { type: 'number', required: false, description: 'Page size (default 50)' },
292
+ offset: { type: 'number', required: false, description: 'Page offset' },
293
+ },
294
+ },
295
+ get_registry_stats: {
296
+ id: 'get_registry_stats',
297
+ description: 'Get aggregated stats about the WAB network (total sites, rings distribution, top intents).',
298
+ url: '/api/registry/stats',
299
+ method: 'GET',
300
+ },
301
+ suggest_peers: {
302
+ id: 'suggest_peers',
303
+ description: 'Get peer-site suggestions for cross-discovery (gossip protocol).',
304
+ url: '/api/registry/suggest',
305
+ method: 'GET',
306
+ inputs: {
307
+ domain: { type: 'string', required: false, description: 'Seed domain for similarity search' },
308
+ },
309
+ },
310
+ list_plans: {
311
+ id: 'list_plans',
312
+ description: 'List all available WAB subscription plans with prices and features.',
313
+ url: '/api/plans',
314
+ method: 'GET',
315
+ },
316
+ get_plan: {
317
+ id: 'get_plan',
318
+ description: 'Fetch a specific plan by ID.',
319
+ url: '/api/plans/:id',
320
+ method: 'GET',
321
+ inputs: {
322
+ id: { type: 'string', required: true, description: 'Plan ID' },
323
+ },
324
+ },
325
+ scan_qr: {
326
+ id: 'scan_qr',
327
+ description: 'Verify a ShieldQR code returns trust ring, issuer, and risk score for the encoded URL.',
328
+ url: '/api/shieldqr/scan',
329
+ method: 'POST',
330
+ inputs: {
331
+ url: { type: 'string', required: true, description: 'URL decoded from the QR' },
332
+ },
333
+ },
334
+ recent_scans: {
335
+ id: 'recent_scans',
336
+ description: 'Get the most recent public ShieldQR scan reports.',
337
+ url: '/api/shieldqr/recent',
338
+ method: 'GET',
339
+ },
340
+ },
341
+ privacy: {
342
+ allowed: ['registry queries', 'public trust metadata', 'plan listings'],
343
+ disallowed: ['admin endpoints', 'billing webhooks', 'individual user data'],
344
+ },
345
+ };
346
+
347
+ let _trustPayloadCache = null;
348
+ function loadTrustPayload() {
349
+ if (_trustPayloadCache) return _trustPayloadCache;
350
+ try {
351
+ const raw = require('fs').readFileSync(
352
+ path.join(__dirname, '..', 'public', '.well-known', 'wab.json'), 'utf8');
353
+ _trustPayloadCache = JSON.parse(raw);
354
+ } catch { _trustPayloadCache = {}; }
355
+ return _trustPayloadCache;
356
+ }
357
+
358
+ app.get('/.well-known/wab.json', (req, res) => {
359
+ res.set('Cache-Control', 'public, max-age=300');
360
+ res.set('Access-Control-Allow-Origin', '*');
361
+ res.type('application/json');
362
+ // Merge signed trust artifact (untouched) with action manifest
363
+ res.json({ ...loadTrustPayload(), ...WAB_ACTIONS_MANIFEST });
364
+ });
365
+
366
+ // WAB Beacon /.wab compact machine-readable trust signal for AI agents.
367
+ // Agents following the Gossip / Spider Protocol read this to learn:
368
+ // ring, score, manifest, registry, and a list of peer WAB-enabled sites.
369
+ app.get('/.wab', (req, res) => {
370
+ const { currentPublicKey, currentFingerprint } = require('./routes/notary');
371
+ const registry = require('./routes/registry');
372
+ // top 10 verified or highest-scored registry entries as peer hints
373
+ let peers = [];
374
+ try {
375
+ const regData = JSON.parse(
376
+ require('fs').readFileSync(
377
+ require('path').join(__dirname, '..', 'data', 'registry.json'), 'utf8')
378
+ );
379
+ if (Array.isArray(regData)) {
380
+ peers = regData
381
+ .filter(e => e.active !== false)
382
+ .sort((a, b) => (b.score || 0) - (a.score || 0) || (b.trust_ring || 0) - (a.trust_ring || 0))
383
+ .slice(0, 10)
384
+ .map(e => ({ domain: e.domain, trust_ring: e.trust_ring, intent_tags: (e.intent_tags || []).slice(0, 5) }));
385
+ }
386
+ } catch { /* registry may be empty */ }
387
+ res.set('Cache-Control', 'public, max-age=120');
388
+ res.set('Access-Control-Allow-Origin', '*');
389
+ res.json({
390
+ v: 1,
391
+ protocol: 'wab/3.19',
392
+ ring: 4,
393
+ score: 94,
394
+ manifest: 'https://webagentbridge.com/.well-known/wab.json',
395
+ registry: 'https://webagentbridge.com/api/registry/discover',
396
+ spider_report: 'https://webagentbridge.com/api/registry/report',
397
+ notary_fp: typeof currentFingerprint === 'function' ? currentFingerprint() : null,
398
+ next: peers,
399
+ generated_at: new Date().toISOString(),
400
+ });
401
+ });
402
+
403
+ // Discovery headers — advertised on every HTML/JSON response so AI agents,
404
+ // crawlers and MCP-style clients can find the WAB manifest and llms.txt
405
+ // without having to scrape. Mirrors the Mintlify X-Llms-Txt convention and
406
+ // adds X-WAB-Discovery alongside RFC 8288 Link rels.
407
+ // Phase 8: X-WAB-Enabled + X-WAB-Trust-Ring + X-WAB-Score + X-WAB-Registry
408
+ // signal preference to any agent that reads HTTP response headers.
409
+ // Phase 9: X-WAB-Next-Sites — WAB DNA header, top peers from registry (cached 60s)
410
+ // so every HTTP response carries network topology. This is the WAB Gossip DNA.
411
+ let _nextSitesCache = { value: '', exp: 0 };
412
+ function getNextSitesDNA() {
413
+ if (Date.now() < _nextSitesCache.exp) return _nextSitesCache.value;
414
+ try {
415
+ const reg = JSON.parse(
416
+ require('fs').readFileSync(path.join(__dirname, '..', 'data', 'registry.json'), 'utf8')
417
+ );
418
+ const top = Array.isArray(reg)
419
+ ? reg.filter(e => e.active !== false)
420
+ .sort((a, b) => (b.trust_ring || 0) - (a.trust_ring || 0) || (b.score || 0) - (a.score || 0))
421
+ .slice(0, 5).map(e => e.domain).join(', ')
422
+ : '';
423
+ _nextSitesCache = { value: top, exp: Date.now() + 60000 };
424
+ return top;
425
+ } catch { return ''; }
426
+ }
427
+ app.use((req, res, next) => {
428
+ res.set('X-WAB-Discovery', '/.well-known/wab.json');
429
+ res.set('X-WAB-Enabled', 'true');
430
+ res.set('X-WAB-Trust-Ring', '4');
431
+ res.set('X-WAB-Score', '94');
432
+ res.set('X-WAB-Registry', '/api/registry/discover');
433
+ res.set('X-Llms-Txt', '/llms.txt');
434
+ const nextSites = getNextSitesDNA();
435
+ if (nextSites) res.set('X-WAB-Next-Sites', nextSites);
436
+ res.append('Link', '</.well-known/wab.json>; rel="wab-manifest"; type="application/json"');
437
+ res.append('Link', '</.wab>; rel="wab-beacon"; type="application/json"');
438
+ res.append('Link', '</llms.txt>; rel="llms-txt"; type="text/plain"');
439
+ res.append('Link', '</llms-full.txt>; rel="llms-full-txt"; type="text/plain"');
440
+ next();
441
+ });
442
+
443
+ // WAB compliance badge embeddable SVG. Usage:
444
+ // <img src="https://webagentbridge.com/badge/example.com.svg">
445
+ // Returns green/amber/red based on whether the domain publishes a reachable
446
+ // /.well-known/wab.json (and optionally an Ed25519 signature). Result is
447
+ // cached in-process for 10 minutes to keep this endpoint cheap and DoS-safe.
448
+ const _badgeCache = new Map();
449
+ function _badgeSvg(label, value, color) {
450
+ const labelW = 56;
451
+ const valueW = Math.max(48, value.length * 7 + 14);
452
+ const total = labelW + valueW;
453
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${total}" height="20" role="img" aria-label="${label}: ${value}">
454
+ <linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset=".1" stop-color="#aaa" stop-opacity=".1"/><stop offset=".9" stop-opacity=".3"/><stop offset="1" stop-opacity=".5"/></linearGradient>
455
+ <mask id="m"><rect width="${total}" height="20" rx="3" fill="#fff"/></mask>
456
+ <g mask="url(#m)">
457
+ <rect width="${labelW}" height="20" fill="#1f2937"/>
458
+ <rect x="${labelW}" width="${valueW}" height="20" fill="${color}"/>
459
+ <rect width="${total}" height="20" fill="url(#b)"/>
460
+ </g>
461
+ <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
462
+ <text x="${labelW/2}" y="14">${label}</text>
463
+ <text x="${labelW + valueW/2}" y="14">${value}</text>
464
+ </g>
465
+ </svg>`;
466
+ }
467
+ app.get('/badge/:domain', async (req, res) => {
468
+ let host = String(req.params.domain || '').replace(/\.svg$/i, '').trim().toLowerCase();
469
+ host = host.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
470
+ if (!/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(host)) {
471
+ res.type('image/svg+xml').set('Cache-Control', 'public, max-age=60');
472
+ return res.send(_badgeSvg('WAB', 'invalid', '#9ca3af'));
473
+ }
474
+ const cached = _badgeCache.get(host);
475
+ if (cached && cached.exp > Date.now()) {
476
+ res.type('image/svg+xml').set('Cache-Control', 'public, max-age=600').set('Access-Control-Allow-Origin', '*');
477
+ return res.send(cached.svg);
478
+ }
479
+ let value = 'unknown', color = '#9ca3af';
480
+ try {
481
+ // SSRF-safe: safeFetch resolves DNS and blocks private/loopback/link-local IPs,
482
+ // re-validates on each redirect hop. This defeats nip.io-style tricks where
483
+ // the hostname "looks public" but resolves to 127.0.0.1.
484
+ const r = await safeFetch(`https://${host}/.well-known/wab.json`, {}, { timeoutMs: 3500, maxBytes: 256 * 1024, requireHttps: true });
485
+ if (r.ok) {
486
+ const j = await r.json().catch(() => null);
487
+ const signed = !!(j && (j.signature || (j.trust && j.trust.signed)));
488
+ value = signed ? 'verified' : 'enabled';
489
+ color = signed ? '#10b981' : '#f59e0b';
490
+ } else {
491
+ value = 'missing'; color = '#ef4444';
492
+ }
493
+ } catch (_) {
494
+ value = 'missing'; color = '#ef4444';
495
+ }
496
+ const svg = _badgeSvg('WAB', value, color);
497
+ _badgeCache.set(host, { svg, exp: Date.now() + 10 * 60 * 1000 });
498
+ res.type('image/svg+xml').set('Cache-Control', 'public, max-age=600').set('Access-Control-Allow-Origin', '*');
499
+ return res.send(svg);
500
+ });
501
+
502
+ app.use(express.static(path.join(__dirname, '..', 'public'), {
503
+ setHeaders(res, filePath) {
504
+ if (filePath.endsWith('.html')) {
505
+ res.setHeader('Cache-Control', 'no-cache, must-revalidate');
506
+ }
507
+ }
508
+ }));
509
+ app.use('/script', express.static(path.join(__dirname, '..', 'script')));
510
+
511
+ app.use('/api/auth', apiLimiter, authRoutes);
512
+ app.use('/api', apiLimiter, apiRoutes);
513
+ app.use('/api/license', licenseLimiter, licenseRoutes);
514
+ app.use('/api/admin', apiLimiter, adminRoutes);
515
+ app.use('/api/billing', apiLimiter, billingRoutes);
516
+ // genius-platform payment gateway uses WAB's Stripe service (internal proxy)
517
+ app.use('/api/genius', geniusGateway);
518
+ app.use('/api/sovereign', apiLimiter, sovereignRoutes);
519
+ app.use('/api/mesh', apiLimiter, meshRoutes);
520
+ app.use('/api/commander', apiLimiter, commanderRoutes);
521
+ app.use('/api/ads', apiLimiter, adsRoutes);
522
+ app.use('/api/wab', wabApiRoutes);
523
+ app.use('/api/noscript', apiLimiter, noscriptRoutes);
524
+ app.use('/api/discovery', apiLimiter, discoveryRoutes);
525
+ app.use('/api/activate', apiLimiter, require('./routes/activate'));
526
+
527
+ // ── WAB Advanced Features v1.0 ──────────────────────────────────────────────
528
+ const { reputationRouter, collectiveRouter } = require('./routes/reputation');
529
+ const { intentRouter, privacyRouter } = require('./routes/intent');
530
+ const { cacheRouter, offlineRouter } = require('./routes/wab-cache');
531
+ // Trust Graph tier gate tags & meters anonymous + keyed traffic.
532
+ // Mounted BEFORE the routers so it sees their requests.
533
+ const { apiTierMiddleware } = require('./middleware/api-tier');
534
+ app.use(['/api/reputation', '/api/truth', '/api/ring4/status'], apiTierMiddleware);
535
+ app.use('/api/reputation', apiLimiter, reputationRouter);
536
+ app.use('/api/collective', apiLimiter, collectiveRouter);
537
+ app.use('/api/intent', apiLimiter, intentRouter);
538
+ app.use('/api/privacy', apiLimiter, privacyRouter);
539
+ app.use('/api/cache', apiLimiter, cacheRouter);
540
+ app.use('/api/offline', apiLimiter, offlineRouter);
541
+
542
+ // ── WAB Truth Layer v1.0 (Semantic Memory + Temporal Trust + Action Graphs + Reality Anchor) ──
543
+ const { truthRouter } = require('./routes/truth-layer');
544
+ app.use('/api/truth', apiLimiter, truthRouter);
545
+
546
+ // ── WAB Ring 4 External Trust Verification (sovereign-agent trust API) ──
547
+ const { ring4Router } = require('./routes/ring4');
548
+ const { wabTrustMiddleware } = require('./middleware/wab-trust');
549
+ app.use(wabTrustMiddleware);
550
+ app.use('/api/ring4', apiLimiter, ring4Router);
551
+
552
+ // ── Agent Transaction Primitive (ATP) v3.9.0 — intents · transactions · signed receipts ──
553
+ app.use('/api/atp', apiLimiter, require('./routes/transactions'));
554
+
555
+ // ── Site Revocations & Appeals v3.11.0 public transparency + owner appeals ──
556
+ app.use('/api/revocations', apiLimiter, require('./routes/revocations'));
557
+
558
+ // ── Agent-Driven Adoption v3.12.0 canonical LLM agent system prompt ──
559
+ app.use('/api/agent', apiLimiter, require('./routes/agent-prompt'));
560
+
561
+ // ── Network Effect v3.14.0 — trusted-domains snapshot + revocations feeds ──
562
+ // (apiLimiter already applies via /api mount above; do not stack it here.)
563
+ app.use('/api', require('./routes/network'));
564
+
565
+ // ── Webhook Subscriptions v3.16.0 (Phase 4) instant push for revocations ──
566
+ app.use('/api/webhooks', apiLimiter, require('./routes/webhooks'));
567
+
568
+ // ── WAB Commercial Foundations v3.8.0 (Partners · Trust Graph API · Governance SaaS · Enterprise Mesh) ──
569
+ app.use('/api/partners', apiLimiter, require('./routes/partners'));
570
+ app.use('/api/keys', apiLimiter, require('./routes/api-keys'));
571
+ app.use('/api/governance-saas', apiLimiter, require('./routes/governance-saas'));
572
+ app.use('/api/enterprise-mesh', apiLimiter, require('./routes/enterprise-mesh'));
573
+ // Trust Graph tier gate is mounted earlier (before /api/reputation et al.)
574
+ // ─────────────────────────────────────────────────────────────────────────────
575
+
576
+ app.use('/api/providers', apiLimiter, providerRoutes);
577
+ app.use('/api/governance', apiLimiter, governanceRoutes);
578
+ app.use('/api/plans', apiLimiter, require('./routes/plans'));
579
+ app.use('/api/admin/plans', apiLimiter, require('./routes/admin-plans'));
580
+ app.use('/api/admin/shieldqr', apiLimiter, require('./routes/admin-shieldqr'));
581
+ app.use('/api/admin/trust-monitor', apiLimiter, require('./routes/admin-trust-monitor'));
582
+ // Optional premium modules — mounted only when present (open-source repo
583
+ // excludes the ShieldLink stack which is a paid feature).
584
+ function mountOptional(prefix, modPath) {
585
+ try { app.use(prefix, apiLimiter, require(modPath)); }
586
+ catch (e) {
587
+ if (e.code === 'MODULE_NOT_FOUND' && e.message.includes(modPath)) {
588
+ console.log(`[optional] ${prefix} not mounted (${modPath} not present)`);
589
+ } else { throw e; }
590
+ }
591
+ }
592
+ mountOptional('/api/admin/shieldlink', './routes/admin-shieldlink');
593
+ app.use('/api/shieldqr', apiLimiter, require('./routes/shieldqr'));
594
+ mountOptional('/api/shieldlink', './routes/shieldlink');
595
+ mountOptional('/api/customer/shieldlink','./routes/customer-shieldlink');
596
+ app.use('/api/adopt', apiLimiter, require('./routes/adopt'));
597
+ app.use('/api/diagnose', apiLimiter, require('./routes/diagnose'));
598
+ app.use('/api/admin/outreach', apiLimiter, require('./routes/admin-outreach'));
599
+ app.use('/', apiLimiter, require('./routes/unsubscribe'));
600
+ // Also expose well-known discovery endpoints at the canonical root paths so
601
+ // agents can find them without the /api/discovery prefix (RFC 8615).
602
+
603
+ // /activate — WAB DNS Discovery activation guide (bilingual)
604
+ app.get('/activate', noCache, (req, res) => {
605
+ res.sendFile(path.join(__dirname, '..', 'public', 'activate.html'));
606
+ });
607
+
608
+ // /one-click — interactive self-serve activation wizard (key-gen, sign, deploy via API)
609
+ app.get(['/one-click', '/one-click.html', '/activate/one-click'], noCache, (req, res) => {
610
+ res.sendFile(path.join(__dirname, '..', 'public', 'one-click.html'));
611
+ });
612
+
613
+ // /wab-features — WAB Advanced Features showcase (Reputation, Cache, Intent, Privacy, Collective, Offline)
614
+ app.get(['/wab-features', '/features'], noCache, (req, res) => {
615
+ res.sendFile(path.join(__dirname, '..', 'public', 'wab-features.html'));
616
+ });
617
+ // /wab-truth WAB Truth Layer showcase (Semantic Memory + Temporal Trust + Action Graphs + Reality Anchor)
618
+ app.get(['/wab-truth', '/truth'], noCache, (req, res) => {
619
+ res.sendFile(path.join(__dirname, '..', 'public', 'wab-truth.html'));
620
+ });
621
+ // /milestones Partners & Milestones (VEXR Ultra × WAB Ring 4 integration)
622
+ app.get(['/milestones'], noCache, (req, res) => {
623
+ res.sendFile(path.join(__dirname, '..', 'public', 'milestones.html'));
624
+ });
625
+ // /partnersCertified Partner Program (3 tiers · self-serve)
626
+ app.get(['/partners', '/partners.html'], noCache, (req, res) => {
627
+ res.sendFile(path.join(__dirname, '..', 'public', 'partners.html'));
628
+ });
629
+ // /trust-graph-api — Trust Graph API docs & self-serve key issuance
630
+ app.get(['/trust-graph-api', '/trust-graph-api.html'], noCache, (req, res) => {
631
+ res.sendFile(path.join(__dirname, '..', 'public', 'trust-graph-api.html'));
632
+ });
633
+ // /governance — Governance SaaS landing (EU AI Act audit trail)
634
+ app.get(['/governance', '/governance.html'], noCache, (req, res) => {
635
+ res.sendFile(path.join(__dirname, '..', 'public', 'governance.html'));
636
+ });
637
+ // /enterprise-mesh Self-hosted Enterprise Mesh contact
638
+ app.get(['/enterprise-mesh', '/enterprise-mesh.html', '/enterprise'], noCache, (req, res) => {
639
+ res.sendFile(path.join(__dirname, '..', 'public', 'enterprise-mesh.html'));
640
+ });
641
+ // /ring4 — Ring 4 Trust Handshake protocol docs
642
+ app.get(['/ring4', '/trust-handshake'], noCache, (req, res) => {
643
+ res.sendFile(path.join(__dirname, '..', 'public', 'ring4.html'));
644
+ });
645
+ // /refusals — Public refusal log (anonymized constitutional refusal stats)
646
+ app.get('/refusals', noCache, (req, res) => {
647
+ res.sendFile(path.join(__dirname, '..', 'public', 'refusals.html'));
648
+ });
649
+ // Trust & protocol pages
650
+ app.get(['/security', '/security.html'], noCache, (req, res) => {
651
+ res.sendFile(path.join(__dirname, '..', 'public', 'security.html'));
652
+ });
653
+ app.get(['/threat-model', '/threat-model.html'], noCache, (req, res) => {
654
+ res.sendFile(path.join(__dirname, '..', 'public', 'threat-model.html'));
655
+ });
656
+ app.get(['/responsible-disclosure', '/responsible-disclosure.html'], noCache, (req, res) => {
657
+ res.sendFile(path.join(__dirname, '..', 'public', 'responsible-disclosure.html'));
658
+ });
659
+ app.get(['/researchers', '/researchers.html', '/hall-of-fame'], noCache, (req, res) => {
660
+ res.sendFile(path.join(__dirname, '..', 'public', 'researchers.html'));
661
+ });
662
+ app.get(['/key-rotation', '/key-rotation.html'], noCache, (req, res) => {
663
+ res.sendFile(path.join(__dirname, '..', 'public', 'key-rotation.html'));
664
+ });
665
+ app.get(['/atp-semantics', '/atp-semantics.html'], noCache, (req, res) => {
666
+ res.sendFile(path.join(__dirname, '..', 'public', 'atp-semantics.html'));
667
+ });
668
+ app.get(['/benchmarks', '/benchmarks.html'], noCache, (req, res) => {
669
+ res.sendFile(path.join(__dirname, '..', 'public', 'benchmarks.html'));
670
+ });
671
+ app.get(['/wab-today', '/wab-today.html', '/architecture'], noCache, (req, res) => {
672
+ res.sendFile(path.join(__dirname, '..', 'public', 'wab-today.html'));
673
+ });
674
+
675
+ // ── WAB Ecosystem v3.18.0 — Observatory · Notary · Research · URI scheme · Lens ──
676
+ app.use('/api/notary', apiLimiter, require('./routes/notary'));
677
+ app.use('/api/observatory', apiLimiter, require('./routes/observatory'));
678
+ app.use('/api/research', apiLimiter, require('./routes/research'));
679
+ app.use('/api/security-researchers', apiLimiter, require('./routes/security-researchers'));
680
+
681
+ // ── WAB Spider Network v3.19.0 — Public Registry + Spider Protocol ──
682
+ app.use('/api/registry', apiLimiter, require('./routes/registry'));
683
+
684
+ // ── WAB Self-Propagating Protocol v3.20.0 — Training Signal + Viral Stats ──
685
+ app.use('/api/traces', apiLimiter, require('./routes/traces'));
686
+
687
+ app.get(['/observatory', '/observatory.html'], noCache, (req, res) => {
688
+ res.sendFile(path.join(__dirname, '..', 'public', 'observatory.html'));
689
+ });
690
+ app.get(['/notary', '/notary.html'], noCache, (req, res) => {
691
+ res.sendFile(path.join(__dirname, '..', 'public', 'notary.html'));
692
+ });
693
+ app.get(['/research', '/research.html'], noCache, (req, res) => {
694
+ res.sendFile(path.join(__dirname, '..', 'public', 'research.html'));
695
+ });
696
+ app.get(['/wab-uri', '/wab-uri.html'], noCache, (req, res) => {
697
+ res.sendFile(path.join(__dirname, '..', 'public', 'wab-uri.html'));
698
+ });
699
+ app.get(['/wab-email', '/wab-email.html'], noCache, (req, res) => {
700
+ res.sendFile(path.join(__dirname, '..', 'public', 'wab-email.html'));
701
+ });
702
+ app.get(['/wab-p2p', '/wab-p2p.html'], noCache, (req, res) => {
703
+ res.sendFile(path.join(__dirname, '..', 'public', 'wab-p2p.html'));
704
+ });
705
+ app.get(['/wab-lens', '/wab-lens.html'], noCache, (req, res) => {
706
+ res.sendFile(path.join(__dirname, '..', 'public', 'wab-lens.html'));
707
+ });
708
+ app.get(['/wab-registry', '/wab-registry.html', '/registry'], noCache, (req, res) => {
709
+ res.sendFile(path.join(__dirname, '..', 'public', 'wab-registry.html'));
710
+ });
711
+ app.get(['/wab-dataset', '/wab-dataset.html', '/dataset'], noCache, (req, res) => {
712
+ res.sendFile(path.join(__dirname, '..', 'public', 'wab-dataset.html'));
713
+ });
714
+ app.get(['/viral-coefficient', '/viral-coefficient.html', '/viral'], noCache, (req, res) => {
715
+ res.sendFile(path.join(__dirname, '..', 'public', 'viral-coefficient.html'));
716
+ });
717
+
718
+ // /resolve?u=wab://host/action?... universal handler for the wab:// URI scheme.
719
+ // Parses the URI, fetches the target manifest, validates the action and shows
720
+ // a confirmation page. Renders an inline HTML response so it works without JS.
721
+ app.get('/resolve', async (req, res) => {
722
+ const raw = String(req.query.u || '');
723
+ const esc = (s) => String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
724
+ function page(title, body) {
725
+ res.type('html').send(`<!doctype html><html><head><meta charset="utf-8"><title>${esc(title)}</title>
726
+ <style>body{font:14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0b0f17;color:#e5e7eb;margin:0;padding:48px 24px;max-width:640px;margin-inline:auto}
727
+ h1{font-size:22px}a{color:#60a5fa}code,pre{background:#0d1320;border:1px solid #1f2937;border-radius:6px;padding:2px 6px}
728
+ pre{padding:12px;overflow-x:auto;font-size:12px}.err{color:#ef4444}.ok{color:#10b981}
729
+ button,a.btn{display:inline-block;background:#60a5fa;color:#0b0f17;border:0;padding:10px 18px;border-radius:6px;font:inherit;font-weight:600;cursor:pointer;text-decoration:none;margin-top:12px}
730
+ </style></head><body>${body}<p style="margin-top:32px;font-size:12px;color:#9ca3af"><a href="/wab-uri">About the wab:// URI scheme</a></p></body></html>`);
731
+ }
732
+ if (!/^wab:\/\//i.test(raw)) {
733
+ return page('Invalid wab:// URI', `<h1 class="err">Invalid wab:// URI</h1><p>The <code>u</code> parameter must start with <code>wab://</code>.</p>`);
734
+ }
735
+ let host, action, params;
736
+ try {
737
+ const u = new URL(raw.replace(/^wab:\/\//i, 'https://'));
738
+ host = u.hostname.toLowerCase();
739
+ action = u.pathname.replace(/^\/+/, '').split('/')[0] || '';
740
+ params = Object.fromEntries(u.searchParams.entries());
741
+ if (!/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(host) || !action) throw new Error('bad uri');
742
+ } catch (e) {
743
+ return page('Invalid wab:// URI', `<h1 class="err">Could not parse</h1><pre>${esc(raw)}</pre>`);
744
+ }
745
+ let manifest = null;
746
+ try {
747
+ // SSRF-safe: routes through safeFetch which DNS-resolves and blocks
748
+ // private/loopback/link-local addresses (nip.io, AWS metadata, etc.)
749
+ const r = await safeFetch(`https://${host}/.well-known/wab.json`, {}, { timeoutMs: 4000, maxBytes: 256 * 1024, requireHttps: true });
750
+ if (r.ok) manifest = await r.json().catch(() => null);
751
+ } catch (_) {}
752
+ if (!manifest) {
753
+ return page('Manifest not found', `<h1 class="err">${esc(host)} does not publish a WAB manifest</h1>
754
+ <p>The wab:// URI cannot be resolved because <code>https://${esc(host)}/.well-known/wab.json</code> is not reachable.</p>`);
755
+ }
756
+ const actions = Array.isArray(manifest.actions) ? manifest.actions : [];
757
+ const match = actions.find(a => a && a.id === action);
758
+ if (!match) {
759
+ return page('Action not found', `<h1 class="err">Unknown action <code>${esc(action)}</code></h1>
760
+ <p>${esc(host)} publishes a manifest, but no action with id <code>${esc(action)}</code> is declared.</p>
761
+ <p>Known actions: ${actions.map(a => `<code>${esc(a.id||'')}</code>`).join(', ') || '<em>none</em>'}</p>`);
762
+ }
763
+ const signed = !!(manifest.signature || (manifest.trust && manifest.trust.signed));
764
+ return page(`Confirm: ${action} on ${host}`, `
765
+ <h1>Confirm action</h1>
766
+ <p>You are about to invoke <code>${esc(action)}</code> on <strong>${esc(host)}</strong>${signed ? ' <span class="ok">✓ signed manifest</span>' : ''}.</p>
767
+ <h3 style="margin-top:24px;font-size:14px">Parameters</h3>
768
+ <pre>${esc(JSON.stringify(params, null, 2))}</pre>
769
+ <h3 style="margin-top:24px;font-size:14px">Endpoint</h3>
770
+ <pre>${esc(match.method || 'POST')} ${esc(match.endpoint || '')}</pre>
771
+ <form method="${esc(match.safe ? 'GET' : 'POST')}" action="${esc(match.endpoint || '#')}">
772
+ ${Object.entries(params).map(([k,v]) => `<input type="hidden" name="${esc(k)}" value="${esc(v)}">`).join('')}
773
+ <button type="submit">Proceed</button>
774
+ <a class="btn" style="background:transparent;color:#e5e7eb;border:1px solid #1f2937;margin-left:6px" href="javascript:history.back()">Cancel</a>
775
+ </form>`);
776
+ });
777
+ // /.well-known/jwks.json — standard JWKS discovery for OIDC/JWT ecosystem
778
+ app.get('/.well-known/jwks.json', (req, res) => {
779
+ try {
780
+ const { _internals } = require('./routes/ring4');
781
+ return res.json(_internals.buildJwks());
782
+ } catch (e) {
783
+ return res.status(503).json({ error: 'jwks_unavailable', detail: e.message });
784
+ }
785
+ });
786
+ app.get('/shieldqr', noCache, (req, res) => {
787
+ res.sendFile(path.join(__dirname, '..', 'public', 'shieldqr.html'));
788
+ });
789
+ // ── ShieldLink landing + Trust Preview redirect ──
790
+ app.get('/shieldlink', noCache, (req, res) => {
791
+ res.sendFile(path.join(__dirname, '..', 'public', 'shieldlink.html'));
792
+ });
793
+ app.get('/l/:token', noCache, (req, res) => {
794
+ // Serve the Trust Preview page; the page calls /api/shieldlink/verify?token=
795
+ res.sendFile(path.join(__dirname, '..', 'public', 'l-preview.html'));
796
+ });
797
+ app.get('/dashboard/shieldlink', noCache, (req, res) => {
798
+ res.sendFile(path.join(__dirname, '..', 'public', 'dashboard-shieldlink.html'));
799
+ });
800
+ app.get('/activate-dns', noCache, (req, res) => {
801
+ res.sendFile(path.join(__dirname, '..', 'public', 'activate.html'));
802
+ });
803
+ app.get('/provider-onboarding', noCache, (req, res) => {
804
+ res.sendFile(path.join(__dirname, '..', 'public', 'provider-onboarding.html'));
805
+ });
806
+ app.get('/provider-sandbox', noCache, (req, res) => {
807
+ res.sendFile(path.join(__dirname, '..', 'public', 'provider-sandbox.html'));
808
+ });
809
+ app.get('/cloudflare-integration', noCache, (req, res) => {
810
+ res.sendFile(path.join(__dirname, '..', 'public', 'cloudflare-integration.html'));
811
+ });
812
+ app.get('/cpanel-integration', noCache, (req, res) => {
813
+ res.sendFile(path.join(__dirname, '..', 'public', 'cpanel-integration.html'));
814
+ });
815
+ app.get('/route53-integration', noCache, (req, res) => {
816
+ res.sendFile(path.join(__dirname, '..', 'public', 'route53-integration.html'));
817
+ });
818
+ app.get('/plesk-integration', noCache, (req, res) => {
819
+ res.sendFile(path.join(__dirname, '..', 'public', 'plesk-integration.html'));
820
+ });
821
+ app.get('/gcp-dns-integration', noCache, (req, res) => {
822
+ res.sendFile(path.join(__dirname, '..', 'public', 'gcp-dns-integration.html'));
823
+ });
824
+ app.get('/azure-dns-integration', noCache, (req, res) => {
825
+ res.sendFile(path.join(__dirname, '..', 'public', 'azure-dns-integration.html'));
826
+ });
827
+ app.get('/registrar-integrations', noCache, (req, res) => {
828
+ res.sendFile(path.join(__dirname, '..', 'public', 'registrar-integrations.html'));
829
+ });
830
+ app.get('/adoption-metrics', noCache, (req, res) => {
831
+ res.sendFile(path.join(__dirname, '..', 'public', 'adoption-metrics.html'));
832
+ });
833
+ app.get('/adopt', noCache, (req, res) => {
834
+ res.sendFile(path.join(__dirname, '..', 'public', 'adopt.html'));
835
+ });
836
+ app.get('/wab-trust', noCache, (req, res) => {
837
+ res.sendFile(path.join(__dirname, '..', 'public', 'wab-trust.html'));
838
+ });
839
+ app.get('/wab-vs-protocols', noCache, (req, res) => {
840
+ res.sendFile(path.join(__dirname, '..', 'public', 'wab-vs-protocols.html'));
841
+ });
842
+ app.use('/', apiLimiter, discoveryRoutes);
843
+ app.use('/api/premium', apiLimiter, premiumRoutes);
844
+ app.use('/api/admin/premium', apiLimiter, adminPremiumRoutes);
845
+ app.use('/api/workspace', apiLimiter, workspaceRoutes);
846
+ app.use('/api/universal', apiLimiter, universalRoutes);
847
+ app.use('/api/os', apiLimiter, runtimeRoutes);
848
+ app.use('/api/demo', apiLimiter, demoShowcaseRoutes);
849
+ app.use('/api/growth', apiLimiter, growthRoutes);
850
+ app.use('/api/v1', gatewayRoutes);
851
+
852
+ // Convenience alias: /api/negotiate/* → /api/sovereign/negotiation/*
853
+ app.get('/api/negotiate', apiLimiter, (req, res) => {
854
+ res.json({
855
+ engine: 'WAB Negotiation Engine',
856
+ endpoints: {
857
+ 'POST /api/negotiate/rules': 'Create negotiation rules (auth required)',
858
+ 'GET /api/negotiate/rules/:siteId': 'Get rules for a site',
859
+ 'PUT /api/negotiate/rules/:ruleId': 'Update a rule (auth required)',
860
+ 'POST /api/negotiate/sessions': 'Open negotiation session',
861
+ 'POST /api/negotiate/sessions/:id/propose': 'Agent counter-offer',
862
+ 'POST /api/negotiate/sessions/:id/confirm': 'Confirm deal',
863
+ 'GET /api/negotiate/stats/:siteId': 'Negotiation stats',
864
+ },
865
+ });
866
+ });
867
+ app.use('/api/negotiate', apiLimiter, (req, res, next) => {
868
+ req.url = '/negotiation' + req.url;
869
+ sovereignRoutes(req, res, next);
870
+ });
871
+
872
+ // ─── WAB Search Engine ────────────────────────────────────────────────
873
+
874
+ const searchLimiter = rateLimit({
875
+ windowMs: 60 * 1000,
876
+ max: 30,
877
+ standardHeaders: true,
878
+ legacyHeaders: false,
879
+ message: { error: 'Too many search requests, please slow down' }
880
+ });
881
+
882
+ app.get('/api/search', searchLimiter, async (req, res) => {
883
+ const q = (req.query.q || '').trim();
884
+ if (!q) return res.json({ results: [], cached: false });
885
+ if (q.length > 200) return res.status(400).json({ error: 'Query too long' });
886
+ const crypto = require('crypto');
887
+ const ipHash = crypto.createHash('sha256').update(req.ip || '').digest('hex').slice(0, 16);
888
+ const result = await search(q, ipHash);
889
+ res.json(result);
890
+ });
891
+
892
+ app.get('/api/search/suggest', searchLimiter, (req, res) => {
893
+ const q = (req.query.q || '').trim();
894
+ if (!q) return res.json({ suggestions: [] });
895
+ const suggestions = getSuggestions(q, 8);
896
+ res.json({ suggestions });
897
+ });
898
+
899
+ app.get('/api/search/trending', apiLimiter, (req, res) => {
900
+ const trending = getTrendingSearches(10);
901
+ res.json({ trending });
902
+ });
903
+
904
+ app.get('/api/search/stats', apiLimiter, (req, res) => {
905
+ const stats = getSearchStats();
906
+ res.json(stats);
907
+ });
908
+
909
+ // Prevent browsers from caching HTML page routes
910
+ function noCache(req, res, next) {
911
+ res.set('Cache-Control', 'no-cache, must-revalidate');
912
+ next();
913
+ }
914
+
915
+ app.get('/dashboard', noCache, (req, res) => {
916
+ res.sendFile(path.join(__dirname, '..', 'public', 'dashboard.html'));
917
+ });
918
+ app.get('/providers', noCache, (req, res) => {
919
+ res.sendFile(path.join(__dirname, '..', 'public', 'providers.html'));
920
+ });
921
+ app.get('/mesh-dashboard', noCache, (req, res) => {
922
+ res.sendFile(path.join(__dirname, '..', 'public', 'mesh-dashboard.html'));
923
+ });
924
+ app.get('/commander-dashboard', noCache, (req, res) => {
925
+ res.sendFile(path.join(__dirname, '..', 'public', 'commander-dashboard.html'));
926
+ });
927
+ app.get('/docs', noCache, (req, res) => {
928
+ res.sendFile(path.join(__dirname, '..', 'public', 'docs.html'));
929
+ });
930
+ app.get('/login', noCache, (req, res) => {
931
+ res.sendFile(path.join(__dirname, '..', 'public', 'login.html'));
932
+ });
933
+ app.get('/register', noCache, (req, res) => {
934
+ res.sendFile(path.join(__dirname, '..', 'public', 'register.html'));
935
+ });
936
+ app.get('/admin/login', noCache, (req, res) => {
937
+ res.sendFile(path.join(__dirname, '..', 'public', 'admin', 'login.html'));
938
+ });
939
+ app.get('/admin', noCache, (req, res) => {
940
+ res.sendFile(path.join(__dirname, '..', 'public', 'admin', 'dashboard.html'));
941
+ });
942
+ app.get('/admin/snapshots', noCache, (req, res) => {
943
+ res.sendFile(path.join(__dirname, '..', 'public', 'admin', 'snapshots.html'));
944
+ });
945
+
946
+ // ─── Admin sub-pages (each backed by real API endpoints in /api/admin/*) ──
947
+ ['users','sites','analytics','grants','payments','stripe','smtp','notifications','governance','discovery','trust','providers','plans','shieldqr','shieldlink','trust-monitor','outreach'].forEach((page) => {
948
+ app.get('/admin/' + page, noCache, (req, res) => {
949
+ res.sendFile(path.join(__dirname, '..', 'public', 'admin', page + '.html'));
950
+ });
951
+ });
952
+ app.get('/privacy', noCache, (req, res) => {
953
+ res.sendFile(path.join(__dirname, '..', 'public', 'privacy.html'));
954
+ });
955
+ app.get('/terms', noCache, (req, res) => {
956
+ res.sendFile(path.join(__dirname, '..', 'public', 'terms.html'));
957
+ });
958
+ app.get('/cookies', noCache, (req, res) => {
959
+ res.sendFile(path.join(__dirname, '..', 'public', 'cookies.html'));
960
+ });
961
+ app.get('/browser', noCache, (req, res) => {
962
+ res.sendFile(path.join(__dirname, '..', 'public', 'browser.html'));
963
+ });
964
+ app.get('/workspace', noCache, (req, res) => {
965
+ res.sendFile(path.join(__dirname, '..', 'public', 'agent-workspace.html'));
966
+ });
967
+ app.get('/growth', noCache, (req, res) => {
968
+ res.sendFile(path.join(__dirname, '..', 'public', 'growth.html'));
969
+ });
970
+ app.get('/score', noCache, (req, res) => {
971
+ res.sendFile(path.join(__dirname, '..', 'public', 'score.html'));
972
+ });
973
+ app.get('/sovereign', noCache, (req, res) => {
974
+ res.sendFile(path.join(__dirname, '..', 'public', 'sovereign.html'));
975
+ });
976
+ app.get('/api', noCache, (req, res) => {
977
+ res.sendFile(path.join(__dirname, '..', 'public', 'api.html'));
978
+ });
979
+
980
+ app.get('/phone-shield', noCache, (req, res) => {
981
+ res.sendFile(path.join(__dirname, '..', 'public', 'phone-shield.html'));
982
+ });
983
+
984
+ app.get('/dns', noCache, (req, res) => {
985
+ res.sendFile(path.join(__dirname, '..', 'public', 'dns.html'));
986
+ });
987
+
988
+ // /integrations — bilingual deploy landing page
989
+ app.get('/integrations', noCache, (req, res) => {
990
+ res.sendFile(path.join(__dirname, '..', 'public', 'integrations.html'));
991
+ });
992
+
993
+ // /demo — interactive WAB Demo Store (new)
994
+ app.use('/demo', demoStoreRoutes);
995
+
996
+ // Browser downloads
997
+ app.use('/downloads', express.static(path.join(__dirname, '..', 'downloads'), {
998
+ maxAge: '1d',
999
+ setHeaders: (res, filePath) => {
1000
+ // Shell scripts served as plain text for curl | bash usage
1001
+ if (filePath.endsWith('.sh')) {
1002
+ res.set('Content-Type', 'text/plain; charset=utf-8');
1003
+ } else {
1004
+ res.set('Content-Disposition', 'attachment');
1005
+ }
1006
+ }
1007
+ }));
1008
+
1009
+ // WAB Discovery install shortcut: curl -fsSL https://webagentbridge.com/install | bash
1010
+ app.get('/install', (req, res) => {
1011
+ res.set('Content-Type', 'text/plain; charset=utf-8');
1012
+ res.sendFile(path.join(__dirname, '..', 'downloads', 'quick-wab.sh'));
1013
+ });
1014
+
1015
+ // Agent chat endpoint for WAB Browser — Real AI Agent
1016
+ const chatLimiter = rateLimit({
1017
+ windowMs: 60 * 1000,
1018
+ max: 20,
1019
+ standardHeaders: true,
1020
+ legacyHeaders: false,
1021
+ message: { error: 'Too many messages, please slow down' }
1022
+ });
1023
+
1024
+ app.post('/api/wab/agent-chat', chatLimiter, async (req, res) => {
1025
+ const { message, context, sessionId, taskId, taskAction } = req.body || {};
1026
+ if (!message || typeof message !== 'string') {
1027
+ return res.status(400).json({ error: 'Message required' });
1028
+ }
1029
+ if (message.length > 3000) {
1030
+ return res.status(400).json({ error: 'Message too long' });
1031
+ }
1032
+
1033
+ const sid = sessionId || req.ip || 'anonymous';
1034
+
1035
+ try {
1036
+ // ── Task actions (user responding to an active task) ──
1037
+ if (taskId && taskAction) {
1038
+ if (taskAction === 'answer') {
1039
+ const result = agentTasks.answerClarification(taskId, message);
1040
+ if (result.status === 'planning') {
1041
+ // Auto-execute after planning
1042
+ const execResult = await agentTasks.executeTask(taskId);
1043
+ return res.json({ ...execResult, type: 'task' });
1044
+ }
1045
+ return res.json({ ...result, type: 'task' });
1046
+ }
1047
+ if (taskAction === 'select') {
1048
+ const idx = parseInt(message.replace(/\D/g, '')) - 1;
1049
+ const result = agentTasks.selectOffer(taskId, idx);
1050
+ return res.json({ ...result, type: 'task' });
1051
+ }
1052
+ if (taskAction === 'cancel') {
1053
+ const result = agentTasks.cancelTask(taskId);
1054
+ return res.json({ ...result, type: 'task' });
1055
+ }
1056
+ }
1057
+
1058
+ // ── Check if user wants to select from existing offers ──
1059
+ if (!taskId) {
1060
+ const selectMatch = message.match(/(?:اختر|اخت(?:ا|ي)ر|select|choose|pick)\s*(\d+)/i);
1061
+ if (selectMatch) {
1062
+ const tasks = agentTasks.getSessionTasks(sid, 1);
1063
+ if (tasks.length > 0 && tasks[0].status === 'presenting') {
1064
+ const idx = parseInt(selectMatch[1]) - 1;
1065
+ const result = agentTasks.selectOffer(tasks[0].id, idx);
1066
+ return res.json({ ...result, type: 'task' });
1067
+ }
1068
+ }
1069
+ }
1070
+
1071
+ // ── Detect URL paste — create URL negotiation task ──
1072
+ const urlData = agentTasks.parseBookingUrl(message);
1073
+ if (urlData) {
1074
+ const task = agentTasks.createUrlTask(sid, message, urlData);
1075
+ const execResult = await agentTasks.executeUrlTask(task.taskId);
1076
+ return res.json({ ...execResult, type: 'task', urlData });
1077
+ }
1078
+
1079
+ // ── Detect if this is a task-type request (booking, shopping, etc.) ──
1080
+ const intent = agentTasks.detectIntent(message);
1081
+ if (intent.confidence >= 0.7 && intent.intent !== 'general') {
1082
+ const task = agentTasks.createTask(sid, message);
1083
+
1084
+ if (task.status === 'clarifying') {
1085
+ return res.json({ ...task, type: 'task' });
1086
+ }
1087
+
1088
+ // If requirements are complete, auto-execute
1089
+ const execResult = await agentTasks.executeTask(task.taskId);
1090
+ return res.json({ ...execResult, type: 'task' });
1091
+ }
1092
+
1093
+ // ── Regular chat (not a task) ──
1094
+ const chatContext = {
1095
+ url: context?.url || '',
1096
+ platform: context?.platform || 'unknown',
1097
+ sessionId: sid,
1098
+ };
1099
+ const result = await agentChat(message, chatContext);
1100
+ res.json(result);
1101
+ } catch (err) {
1102
+ console.error('[agent-chat] Error:', err.message);
1103
+ res.json({ reply: '🤖 عذراً، حدث خطأ. حاول مرة أخرى.', type: 'text' });
1104
+ }
1105
+ });
1106
+
1107
+ // Agent task status & history
1108
+ app.get('/api/wab/agent-task/:id', chatLimiter, (req, res) => {
1109
+ const state = agentTasks.getTaskState(req.params.id);
1110
+ if (!state) return res.status(404).json({ error: 'Task not found' });
1111
+ res.json(state);
1112
+ });
1113
+
1114
+ app.get('/api/wab/agent-tasks', chatLimiter, (req, res) => {
1115
+ const sid = req.query.sessionId || req.ip || 'anonymous';
1116
+ const tasks = agentTasks.getSessionTasks(sid, 20);
1117
+ res.json({ tasks });
1118
+ });
1119
+
1120
+ const pkg = require('../package.json');
1121
+ app.use(`/v${pkg.version.split('.')[0]}`, express.static(path.join(__dirname, '..', 'script')));
1122
+ app.use('/latest', express.static(path.join(__dirname, '..', 'script')));
1123
+
1124
+ app.get('*', (req, res) => {
1125
+ // API routes always return JSON 404
1126
+ if (req.path.startsWith('/api/')) {
1127
+ return res.status(404).json({ error: 'Not found', path: req.path });
1128
+ }
1129
+ if (req.accepts('html')) {
1130
+ res.sendFile(path.join(__dirname, '..', 'public', 'index.html'));
1131
+ } else {
1132
+ res.status(404).json({ error: 'Not found' });
1133
+ }
1134
+ });
1135
+
1136
+
1137
+ // Prevent PM2 restarts from uncaught errors — log and continue
1138
+ process.on('uncaughtException', (err) => {
1139
+ console.error('[process] uncaughtException:', err.message);
1140
+ });
1141
+ process.on('unhandledRejection', (reason) => {
1142
+ console.error('[process] unhandledRejection:', reason?.message || reason);
1143
+ });
1144
+
1145
+ // Run migrations on every load (including tests) so worker-isolated DBs have
1146
+ // a complete schema before the first request.
1147
+ runMigrations();
1148
+
1149
+ if (process.env.NODE_ENV !== 'test') {
1150
+ console.log('Running database migrations...');
1151
+ maybeBootstrapAdmin();
1152
+ initSearchEngine(db);
1153
+
1154
+ // Purge old search cache every hour
1155
+ setInterval(purgeOldCache, 60 * 60 * 1000);
1156
+
1157
+ const server = http.createServer(app);
1158
+ setupWebSocket(server);
1159
+
1160
+ // Start Agent OS runtime
1161
+ runtime.start();
1162
+
1163
+ // Start Cluster Orchestrator
1164
+ cluster.start();
1165
+
1166
+ // Start the SSL Health Monitor cron (Extended Trust Layer).
1167
+ try { require('./services/ssl-monitor').start(); } catch (e) { console.warn('[ssl-monitor] start failed:', e.message); }
1168
+
1169
+ // Start the Certificate Transparency Monitor (opt-in via WAB_CT_MONITOR=true).
1170
+ try { require('./services/ssl-ct-monitor').start(); } catch (e) { console.warn('[ct-monitor] start failed:', e.message); }
1171
+
1172
+ // Start the ATP commission billing timer (opt-in via WAB_COMMISSION_BILLING_INTERVAL_HOURS).
1173
+ try {
1174
+ const r = require('./services/commission-billing').startPeriodicBilling();
1175
+ if (r) console.log(`[commission-billing] periodic cycle every ${r.intervalHours}h`);
1176
+ } catch (e) { console.warn('[commission-billing] start failed:', e.message); }
1177
+
1178
+ // Start the revocation appeal-window sweep (opt-in via WAB_REVOCATION_SWEEP_INTERVAL_HOURS).
1179
+ try {
1180
+ const r = require('./services/revocations').startPeriodicSweep();
1181
+ if (r) console.log(`[revocations] periodic sweep every ${r.intervalHours}h`);
1182
+ } catch (e) { console.warn('[revocations] sweep start failed:', e.message); }
1183
+
1184
+ server.listen(PORT, () => {
1185
+ console.log(`\n ╔══════════════════════════════════════════╗`);
1186
+ console.log(` ║ Web Agent Bridge v${pkg.version} ║`);
1187
+ console.log(` ║ Server running on http://localhost:${PORT} ║`);
1188
+ console.log(` ║ WebSocket: ws://localhost:${PORT}/ws/analytics ║`);
1189
+ console.log(` ╚══════════════════════════════════════════╝\n`);
1190
+ });
1191
+ }
1192
+
1193
+ module.exports = app;