web-agent-bridge 3.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +12 -0
  2. package/README.ar.md +18 -0
  3. package/README.md +198 -1664
  4. package/bin/wab-init.js +223 -0
  5. package/examples/azure-dns-wab.js +83 -0
  6. package/examples/cloudflare-wab-dns.js +121 -0
  7. package/examples/cpanel-wab-dns.js +114 -0
  8. package/examples/dns-discovery-agent.js +166 -0
  9. package/examples/gcp-dns-wab.js +76 -0
  10. package/examples/governance-agent.js +169 -0
  11. package/examples/plesk-wab-dns.js +103 -0
  12. package/examples/route53-wab-dns.js +144 -0
  13. package/examples/safe-mode-agent.js +96 -0
  14. package/examples/wab-sign.js +74 -0
  15. package/examples/wab-verify.js +60 -0
  16. package/package.json +5 -5
  17. package/public/.well-known/wab.json +28 -0
  18. package/public/activate.html +368 -0
  19. package/public/adoption-metrics.html +188 -0
  20. package/public/api.html +1 -1
  21. package/public/azure-dns-integration.html +289 -0
  22. package/public/cloudflare-integration.html +380 -0
  23. package/public/cpanel-integration.html +398 -0
  24. package/public/css/styles.css +28 -0
  25. package/public/dashboard.html +1 -0
  26. package/public/dns.html +101 -172
  27. package/public/docs.html +1 -0
  28. package/public/gcp-dns-integration.html +318 -0
  29. package/public/growth.html +4 -2
  30. package/public/index.html +227 -31
  31. package/public/integrations.html +1 -1
  32. package/public/js/activate.js +145 -0
  33. package/public/js/auth-nav.js +34 -0
  34. package/public/js/dns.js +438 -0
  35. package/public/openapi.json +89 -0
  36. package/public/plesk-integration.html +375 -0
  37. package/public/premium.html +1 -1
  38. package/public/provider-onboarding.html +172 -0
  39. package/public/provider-sandbox.html +134 -0
  40. package/public/providers.html +359 -0
  41. package/public/registrar-integrations.html +141 -0
  42. package/public/robots.txt +12 -0
  43. package/public/route53-integration.html +531 -0
  44. package/public/shieldqr.html +231 -0
  45. package/public/sitemap.xml +6 -0
  46. package/public/wab-trust.html +200 -0
  47. package/public/wab-vs-protocols.html +210 -0
  48. package/public/whitepaper.html +449 -0
  49. package/sdk/auto-discovery.js +288 -0
  50. package/sdk/governance.js +262 -0
  51. package/sdk/index.js +13 -0
  52. package/sdk/package.json +2 -2
  53. package/sdk/safe-mode.js +221 -0
  54. package/server/index.js +144 -5
  55. package/server/migrations/007_governance.sql +106 -0
  56. package/server/migrations/008_plans.sql +144 -0
  57. package/server/migrations/009_shieldqr.sql +30 -0
  58. package/server/migrations/010_extended_trust.sql +33 -0
  59. package/server/models/adapters/mysql.js +1 -1
  60. package/server/models/adapters/postgresql.js +1 -1
  61. package/server/models/db.js +60 -1
  62. package/server/routes/admin-plans.js +76 -0
  63. package/server/routes/admin-premium.js +4 -2
  64. package/server/routes/admin-shieldqr.js +90 -0
  65. package/server/routes/admin-trust-monitor.js +83 -0
  66. package/server/routes/admin.js +289 -1
  67. package/server/routes/billing.js +16 -4
  68. package/server/routes/discovery.js +1933 -2
  69. package/server/routes/governance.js +208 -0
  70. package/server/routes/plans.js +33 -0
  71. package/server/routes/providers.js +650 -0
  72. package/server/routes/shieldqr.js +88 -0
  73. package/server/services/email.js +29 -0
  74. package/server/services/governance.js +466 -0
  75. package/server/services/plans.js +214 -0
  76. package/server/services/premium.js +1 -1
  77. package/server/services/provider-clients.js +740 -0
  78. package/server/services/shieldqr.js +322 -0
  79. package/server/services/ssl-inspector.js +42 -0
  80. package/server/services/ssl-monitor.js +167 -0
  81. package/server/services/stripe.js +18 -5
  82. package/server/services/vision.js +1 -1
  83. package/server/services/wab-crypto.js +178 -0
@@ -0,0 +1,221 @@
1
+ /**
2
+ * WAB Safe Mode — Agent-side trust gate.
3
+ *
4
+ * Splits the web into Trusted (WAB + valid signature) vs Untrusted, and
5
+ * gives the agent a single function to ask before any action:
6
+ * await safeMode.evaluate(domain) → { level, verdict, allow_execute,
7
+ * allow_read, reason }
8
+ *
9
+ * Trust levels:
10
+ * 3 — DNS + Ed25519 signature valid + telemetry score ≥ 60 (full execute)
11
+ * 2 — DNS + valid wab.json (no signature OR no telemetry) (limited execute)
12
+ * 1 — Resolves but no _wab record / score below threshold (read-only)
13
+ * 0 — Compliance verdict = deny / suspicious (block)
14
+ *
15
+ * Usage (Node):
16
+ * const { WABSafeMode } = require('web-agent-bridge/sdk');
17
+ * const safe = new WABSafeMode({ apiBase: 'https://webagentbridge.com' });
18
+ * const v = await safe.evaluate('example.com');
19
+ * if (v.allow_execute) await agent.execute(...);
20
+ * else if (v.allow_read) await agent.readOnly(...);
21
+ * else throw new Error('Blocked by Safe Mode: ' + v.reason);
22
+ */
23
+
24
+ 'use strict';
25
+
26
+ const DEFAULT_API = 'https://webagentbridge.com';
27
+
28
+ const POLICIES = {
29
+ strict: { require_dnssec: true, require_signature: true, min_score: 75 },
30
+ standard: { require_dnssec: false, require_signature: true, min_score: 60 },
31
+ permissive: { require_dnssec: false, require_signature: false, min_score: 40 },
32
+ };
33
+
34
+ class WABSafeMode {
35
+ /**
36
+ * @param {object} [opts]
37
+ * @param {string} [opts.apiBase='https://webagentbridge.com']
38
+ * @param {'strict'|'standard'|'permissive'} [opts.policy='standard']
39
+ * @param {number} [opts.cacheTtlMs=60000] — verdicts cached this long
40
+ * @param {number} [opts.timeoutMs=8000]
41
+ * @param {function} [opts.fetch] — fetch impl (defaults to global fetch)
42
+ */
43
+ constructor(opts = {}) {
44
+ this.apiBase = (opts.apiBase || DEFAULT_API).replace(/\/+$/, '');
45
+ this.policy = POLICIES[opts.policy] ? opts.policy : 'standard';
46
+ this.cacheTtl = Number.isFinite(opts.cacheTtlMs) ? opts.cacheTtlMs : 60_000;
47
+ this.timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : 8_000;
48
+ this._fetch = opts.fetch || (typeof fetch !== 'undefined' ? fetch : null);
49
+ this._cache = new Map(); // domain → { at, value }
50
+ if (!this._fetch) {
51
+ // Node ≤ 17 fallback
52
+ try { this._fetch = require('node-fetch'); } catch { /* user must supply */ }
53
+ }
54
+ }
55
+
56
+ /** Normalises a domain or URL to bare hostname. */
57
+ static normalizeDomain(input) {
58
+ if (!input || typeof input !== 'string') return null;
59
+ let s = input.trim().toLowerCase();
60
+ s = s.replace(/^https?:\/\//, '').replace(/\/.*$/, '').replace(/^www\./, '');
61
+ return /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/.test(s) ? s : null;
62
+ }
63
+
64
+ async _get(path) {
65
+ if (!this._fetch) throw new Error('Safe Mode requires fetch (Node 18+ or pass opts.fetch)');
66
+ const ctl = (typeof AbortController !== 'undefined') ? new AbortController() : null;
67
+ const timer = ctl ? setTimeout(() => ctl.abort(), this.timeoutMs) : null;
68
+ try {
69
+ const r = await this._fetch(this.apiBase + path, ctl ? { signal: ctl.signal } : {});
70
+ if (!r.ok) return null;
71
+ return await r.json();
72
+ } catch { return null; }
73
+ finally { if (timer) clearTimeout(timer); }
74
+ }
75
+
76
+ /**
77
+ * Evaluate a domain and produce a verdict the agent can act on.
78
+ * @param {string} domain
79
+ * @param {object} [opts]
80
+ * @param {boolean} [opts.live=false] — force a live trust check (skip cache).
81
+ * @returns {Promise<{
82
+ * domain: string, level: 0|1|2|3,
83
+ * verdict: 'allow'|'restrict'|'deny',
84
+ * allow_execute: boolean, allow_read: boolean,
85
+ * score: number, score_label: string,
86
+ * reason: string, reasons: Array,
87
+ * trust: object|null, score_detail: object|null,
88
+ * compliance: object|null,
89
+ * evaluated_at: string,
90
+ * }>}
91
+ */
92
+ async evaluate(domain, opts = {}) {
93
+ const d = WABSafeMode.normalizeDomain(domain);
94
+ if (!d) {
95
+ return this._verdict(domain, 0, 'deny', 0, 'unrated',
96
+ 'invalid_domain', [{ code: 'invalid_domain', severity: 'deny' }],
97
+ null, null, null);
98
+ }
99
+
100
+ const cached = this._cache.get(d);
101
+ if (!opts.live && cached && (Date.now() - cached.at) < this.cacheTtl) return cached.value;
102
+
103
+ // Optionally trigger a live trust check first so compliance has fresh data.
104
+ let trust = null;
105
+ if (opts.live) {
106
+ trust = await this._get(`/api/discovery/trust/${encodeURIComponent(d)}`);
107
+ }
108
+
109
+ const [score, compliance] = await Promise.all([
110
+ this._get(`/api/discovery/score/${encodeURIComponent(d)}`),
111
+ this._get(`/api/discovery/compliance/${encodeURIComponent(d)}?policy=${this.policy}`),
112
+ ]);
113
+
114
+ // Derive trust level
115
+ let level = 1;
116
+ let reasonCode = 'no_signal';
117
+ if (compliance) {
118
+ if (compliance.verdict === 'deny') { level = 0; reasonCode = 'compliance_deny'; }
119
+ else if (compliance.verdict === 'restrict') { level = 1; reasonCode = 'compliance_restrict'; }
120
+ else { // allow
121
+ const sigRate = score?.signature_valid_rate ?? compliance.signature_valid_rate ?? 0;
122
+ const sc = compliance.score ?? score?.score ?? 0;
123
+ if (sigRate > 0.5 && sc >= 60) { level = 3; reasonCode = 'trusted_full'; }
124
+ else { level = 2; reasonCode = 'trusted_limited'; }
125
+ }
126
+ } else {
127
+ level = 1;
128
+ reasonCode = 'no_compliance_record';
129
+ }
130
+
131
+ const verdict = compliance?.verdict || (level === 0 ? 'deny' : level >= 2 ? 'allow' : 'restrict');
132
+ const value = this._verdict(
133
+ d, level, verdict,
134
+ compliance?.score ?? score?.score ?? 0,
135
+ compliance?.score_label ?? score?.label ?? 'unrated',
136
+ reasonCode,
137
+ compliance?.reasons || [],
138
+ trust, score, compliance,
139
+ );
140
+
141
+ this._cache.set(d, { at: Date.now(), value });
142
+ return value;
143
+ }
144
+
145
+ _verdict(domain, level, verdict, score, label, reason, reasons, trust, scoreDetail, compliance) {
146
+ const allow_execute = level >= 2 && verdict === 'allow';
147
+ const allow_read = level >= 1 && verdict !== 'deny';
148
+ return {
149
+ domain,
150
+ level,
151
+ verdict,
152
+ allow_execute,
153
+ allow_read,
154
+ score,
155
+ score_label: label,
156
+ reason,
157
+ reasons,
158
+ trust,
159
+ score_detail: scoreDetail,
160
+ compliance,
161
+ policy: this.policy,
162
+ evaluated_at: new Date().toISOString(),
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Wrap an async action so it only runs if Safe Mode allows execute on the
168
+ * given domain. Throws WABSafeModeError otherwise.
169
+ */
170
+ async guardExecute(domain, action) {
171
+ const v = await this.evaluate(domain);
172
+ if (!v.allow_execute) {
173
+ const err = new WABSafeModeError(
174
+ `Safe Mode blocked execute on ${v.domain} (level ${v.level}, verdict ${v.verdict}, ${v.reason})`,
175
+ v,
176
+ );
177
+ throw err;
178
+ }
179
+ return await action(v);
180
+ }
181
+
182
+ /** Read-only variant: throws only if level === 0. */
183
+ async guardRead(domain, action) {
184
+ const v = await this.evaluate(domain);
185
+ if (!v.allow_read) {
186
+ throw new WABSafeModeError(
187
+ `Safe Mode blocked read on ${v.domain} (level ${v.level}, verdict ${v.verdict})`,
188
+ v,
189
+ );
190
+ }
191
+ return await action(v);
192
+ }
193
+
194
+ /** Picks the highest-trust domain from a candidate list. */
195
+ async pickBest(domains) {
196
+ const evals = await Promise.all(
197
+ (domains || []).map((d) => this.evaluate(d).catch(() => null)),
198
+ );
199
+ const sorted = evals.filter(Boolean).sort((a, b) => {
200
+ if (b.level !== a.level) return b.level - a.level;
201
+ return (b.score || 0) - (a.score || 0);
202
+ });
203
+ return sorted[0] || null;
204
+ }
205
+
206
+ clearCache(domain) {
207
+ if (domain) this._cache.delete(WABSafeMode.normalizeDomain(domain));
208
+ else this._cache.clear();
209
+ }
210
+ }
211
+
212
+ class WABSafeModeError extends Error {
213
+ constructor(message, verdict) {
214
+ super(message);
215
+ this.name = 'WABSafeModeError';
216
+ this.code = 'WAB_SAFE_MODE_BLOCKED';
217
+ this.verdict = verdict;
218
+ }
219
+ }
220
+
221
+ module.exports = { WABSafeMode, WABSafeModeError, POLICIES };
package/server/index.js CHANGED
@@ -29,6 +29,8 @@ const adsRoutes = require('./routes/ads');
29
29
  const wabApiRoutes = require('./routes/wab-api');
30
30
  const noscriptRoutes = require('./routes/noscript');
31
31
  const discoveryRoutes = require('./routes/discovery');
32
+ const providerRoutes = require('./routes/providers');
33
+ const governanceRoutes = require('./routes/governance');
32
34
  const premiumRoutes = require('./routes/premium');
33
35
  const adminPremiumRoutes = require('./routes/admin-premium');
34
36
  const workspaceRoutes = require('./routes/agent-workspace');
@@ -68,8 +70,8 @@ app.use(
68
70
  );
69
71
 
70
72
  const scriptSrc = process.env.CSP_ALLOW_UNSAFE_INLINE === 'false'
71
- ? ["'self'"]
72
- : ["'self'", "'unsafe-inline'"];
73
+ ? ["'self'", 'https://unpkg.com', 'https://cdn.jsdelivr.net']
74
+ : ["'self'", "'unsafe-inline'", 'https://unpkg.com', 'https://cdn.jsdelivr.net'];
73
75
  const styleSrc = process.env.CSP_ALLOW_UNSAFE_INLINE === 'false'
74
76
  ? ["'self'"]
75
77
  : ["'self'", "'unsafe-inline'"];
@@ -87,8 +89,12 @@ app.use(
87
89
  contentSecurityPolicy: {
88
90
  directives: {
89
91
  defaultSrc: ["'self'"],
90
- scriptSrc: [...scriptSrc, (req, res) => `'nonce-${res.locals.cspNonce}'`],
91
- scriptSrcAttr: scriptSrc,
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'"],
92
98
  styleSrc: [...styleSrc, 'https://fonts.googleapis.com'],
93
99
  imgSrc: ["'self'", 'data:', 'https:'],
94
100
  connectSrc: ["'self'", 'https:', 'ws:', 'wss:'],
@@ -186,6 +192,27 @@ app.post('/api/billing/webhook', express.raw({ type: 'application/json' }), asyn
186
192
 
187
193
  app.use(express.json());
188
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
+
189
216
  const apiLimiter = rateLimit({
190
217
  windowMs: 15 * 60 * 1000,
191
218
  max: 200,
@@ -205,6 +232,31 @@ const licenseLimiter = rateLimit({
205
232
  }
206
233
  });
207
234
 
235
+ // Whitepaper guard — must run BEFORE express.static so we can apply strict headers
236
+ // and intercept both /whitepaper and /whitepaper.html with the same protections.
237
+ const whitepaperHandler = (req, res) => {
238
+ res.set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
239
+ res.set('Pragma', 'no-cache');
240
+ res.set('Expires', '0');
241
+ res.set('X-Frame-Options', 'DENY');
242
+ res.set('X-Content-Type-Options', 'nosniff');
243
+ res.set('Referrer-Policy', 'strict-origin-when-cross-origin');
244
+ res.set('X-Robots-Tag', 'index, follow, noarchive, nosnippet, noimageindex');
245
+ 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'");
246
+ res.set('X-Copyright', 'All Rights Reserved (c) 2026 Web Agent Bridge - Reproduction Prohibited');
247
+ res.sendFile(path.join(__dirname, '..', 'public', 'whitepaper.html'));
248
+ };
249
+ app.get(['/whitepaper', '/whitepaper.html'], whitepaperHandler);
250
+
251
+ // WAB Trust artifact (signed Ed25519 wab.json) — served explicitly because
252
+ // express.static skips dotfile directories like /.well-known by default.
253
+ app.get('/.well-known/wab.json', (req, res) => {
254
+ res.set('Cache-Control', 'public, max-age=300');
255
+ res.set('Access-Control-Allow-Origin', '*');
256
+ res.type('application/json');
257
+ res.sendFile(path.join(__dirname, '..', 'public', '.well-known', 'wab.json'));
258
+ });
259
+
208
260
  app.use(express.static(path.join(__dirname, '..', 'public'), {
209
261
  setHeaders(res, filePath) {
210
262
  if (filePath.endsWith('.html')) {
@@ -226,8 +278,62 @@ app.use('/api/ads', apiLimiter, adsRoutes);
226
278
  app.use('/api/wab', wabApiRoutes);
227
279
  app.use('/api/noscript', apiLimiter, noscriptRoutes);
228
280
  app.use('/api/discovery', apiLimiter, discoveryRoutes);
281
+ app.use('/api/providers', apiLimiter, providerRoutes);
282
+ app.use('/api/governance', apiLimiter, governanceRoutes);
283
+ app.use('/api/plans', apiLimiter, require('./routes/plans'));
284
+ app.use('/api/admin/plans', apiLimiter, require('./routes/admin-plans'));
285
+ app.use('/api/admin/shieldqr', apiLimiter, require('./routes/admin-shieldqr'));
286
+ app.use('/api/admin/trust-monitor', apiLimiter, require('./routes/admin-trust-monitor'));
287
+ app.use('/api/shieldqr', apiLimiter, require('./routes/shieldqr'));
229
288
  // Also expose well-known discovery endpoints at the canonical root paths so
230
289
  // agents can find them without the /api/discovery prefix (RFC 8615).
290
+
291
+ // /activate — WAB DNS Discovery activation guide (bilingual)
292
+ app.get('/activate', noCache, (req, res) => {
293
+ res.sendFile(path.join(__dirname, '..', 'public', 'activate.html'));
294
+ });
295
+ app.get('/shieldqr', noCache, (req, res) => {
296
+ res.sendFile(path.join(__dirname, '..', 'public', 'shieldqr.html'));
297
+ });
298
+ app.get('/activate-dns', noCache, (req, res) => {
299
+ res.sendFile(path.join(__dirname, '..', 'public', 'activate.html'));
300
+ });
301
+ app.get('/provider-onboarding', noCache, (req, res) => {
302
+ res.sendFile(path.join(__dirname, '..', 'public', 'provider-onboarding.html'));
303
+ });
304
+ app.get('/provider-sandbox', noCache, (req, res) => {
305
+ res.sendFile(path.join(__dirname, '..', 'public', 'provider-sandbox.html'));
306
+ });
307
+ app.get('/cloudflare-integration', noCache, (req, res) => {
308
+ res.sendFile(path.join(__dirname, '..', 'public', 'cloudflare-integration.html'));
309
+ });
310
+ app.get('/cpanel-integration', noCache, (req, res) => {
311
+ res.sendFile(path.join(__dirname, '..', 'public', 'cpanel-integration.html'));
312
+ });
313
+ app.get('/route53-integration', noCache, (req, res) => {
314
+ res.sendFile(path.join(__dirname, '..', 'public', 'route53-integration.html'));
315
+ });
316
+ app.get('/plesk-integration', noCache, (req, res) => {
317
+ res.sendFile(path.join(__dirname, '..', 'public', 'plesk-integration.html'));
318
+ });
319
+ app.get('/gcp-dns-integration', noCache, (req, res) => {
320
+ res.sendFile(path.join(__dirname, '..', 'public', 'gcp-dns-integration.html'));
321
+ });
322
+ app.get('/azure-dns-integration', noCache, (req, res) => {
323
+ res.sendFile(path.join(__dirname, '..', 'public', 'azure-dns-integration.html'));
324
+ });
325
+ app.get('/registrar-integrations', noCache, (req, res) => {
326
+ res.sendFile(path.join(__dirname, '..', 'public', 'registrar-integrations.html'));
327
+ });
328
+ app.get('/adoption-metrics', noCache, (req, res) => {
329
+ res.sendFile(path.join(__dirname, '..', 'public', 'adoption-metrics.html'));
330
+ });
331
+ app.get('/wab-trust', noCache, (req, res) => {
332
+ res.sendFile(path.join(__dirname, '..', 'public', 'wab-trust.html'));
333
+ });
334
+ app.get('/wab-vs-protocols', noCache, (req, res) => {
335
+ res.sendFile(path.join(__dirname, '..', 'public', 'wab-vs-protocols.html'));
336
+ });
231
337
  app.use('/', apiLimiter, discoveryRoutes);
232
338
  app.use('/api/premium', apiLimiter, premiumRoutes);
233
339
  app.use('/api/admin/premium', apiLimiter, adminPremiumRoutes);
@@ -304,6 +410,9 @@ function noCache(req, res, next) {
304
410
  app.get('/dashboard', noCache, (req, res) => {
305
411
  res.sendFile(path.join(__dirname, '..', 'public', 'dashboard.html'));
306
412
  });
413
+ app.get('/providers', noCache, (req, res) => {
414
+ res.sendFile(path.join(__dirname, '..', 'public', 'providers.html'));
415
+ });
307
416
  app.get('/mesh-dashboard', noCache, (req, res) => {
308
417
  res.sendFile(path.join(__dirname, '..', 'public', 'mesh-dashboard.html'));
309
418
  });
@@ -328,6 +437,13 @@ app.get('/admin', noCache, (req, res) => {
328
437
  app.get('/admin/snapshots', noCache, (req, res) => {
329
438
  res.sendFile(path.join(__dirname, '..', 'public', 'admin', 'snapshots.html'));
330
439
  });
440
+
441
+ // ─── Admin sub-pages (each backed by real API endpoints in /api/admin/*) ──
442
+ ['users','sites','analytics','grants','payments','stripe','smtp','notifications','governance','discovery','trust','providers','plans','shieldqr','trust-monitor'].forEach((page) => {
443
+ app.get('/admin/' + page, noCache, (req, res) => {
444
+ res.sendFile(path.join(__dirname, '..', 'public', 'admin', page + '.html'));
445
+ });
446
+ });
331
447
  app.get('/privacy', noCache, (req, res) => {
332
448
  res.sendFile(path.join(__dirname, '..', 'public', 'privacy.html'));
333
449
  });
@@ -376,10 +492,21 @@ app.use('/demo', demoStoreRoutes);
376
492
  app.use('/downloads', express.static(path.join(__dirname, '..', 'downloads'), {
377
493
  maxAge: '1d',
378
494
  setHeaders: (res, filePath) => {
379
- res.set('Content-Disposition', 'attachment');
495
+ // Shell scripts served as plain text for curl | bash usage
496
+ if (filePath.endsWith('.sh')) {
497
+ res.set('Content-Type', 'text/plain; charset=utf-8');
498
+ } else {
499
+ res.set('Content-Disposition', 'attachment');
500
+ }
380
501
  }
381
502
  }));
382
503
 
504
+ // WAB Discovery install shortcut: curl -fsSL https://webagentbridge.com/install | bash
505
+ app.get('/install', (req, res) => {
506
+ res.set('Content-Type', 'text/plain; charset=utf-8');
507
+ res.sendFile(path.join(__dirname, '..', 'downloads', 'quick-wab.sh'));
508
+ });
509
+
383
510
  // Agent chat endpoint for WAB Browser — Real AI Agent
384
511
  const chatLimiter = rateLimit({
385
512
  windowMs: 60 * 1000,
@@ -501,6 +628,15 @@ app.get('*', (req, res) => {
501
628
  }
502
629
  });
503
630
 
631
+
632
+ // Prevent PM2 restarts from uncaught errors — log and continue
633
+ process.on('uncaughtException', (err) => {
634
+ console.error('[process] uncaughtException:', err.message);
635
+ });
636
+ process.on('unhandledRejection', (reason) => {
637
+ console.error('[process] unhandledRejection:', reason?.message || reason);
638
+ });
639
+
504
640
  if (process.env.NODE_ENV !== 'test') {
505
641
  console.log('Running database migrations...');
506
642
  runMigrations();
@@ -519,6 +655,9 @@ if (process.env.NODE_ENV !== 'test') {
519
655
  // Start Cluster Orchestrator
520
656
  cluster.start();
521
657
 
658
+ // Start the SSL Health Monitor cron (Extended Trust Layer).
659
+ try { require('./services/ssl-monitor').start(); } catch (e) { console.warn('[ssl-monitor] start failed:', e.message); }
660
+
522
661
  server.listen(PORT, () => {
523
662
  console.log(`\n ╔══════════════════════════════════════════╗`);
524
663
  console.log(` ║ Web Agent Bridge v${pkg.version} ║`);
@@ -0,0 +1,106 @@
1
+ -- ═══════════════════════════════════════════════════════════════════
2
+ -- WAB Agent Governance Layer
3
+ -- Permission Boundaries · Approval Gates · Tamper-Evident Audit Log
4
+ -- Kill Switch · Spend Limits
5
+ -- ═══════════════════════════════════════════════════════════════════
6
+
7
+ -- Agents registered for governance (one row per agent identity).
8
+ CREATE TABLE IF NOT EXISTS gov_agents (
9
+ agent_id TEXT PRIMARY KEY,
10
+ owner_id TEXT, -- user_id of owner (nullable for unauthed)
11
+ display_name TEXT,
12
+ token_hash TEXT NOT NULL, -- sha256(agent_token); used to authenticate the agent
13
+ status TEXT NOT NULL DEFAULT 'alive' CHECK(status IN ('alive','killed','suspended')),
14
+ killed_at TEXT,
15
+ killed_reason TEXT,
16
+ metadata TEXT, -- JSON
17
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
18
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
19
+ );
20
+
21
+ -- Permission policies. One row = one rule. Evaluated allow-list style.
22
+ CREATE TABLE IF NOT EXISTS gov_policies (
23
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
24
+ agent_id TEXT NOT NULL,
25
+ resource TEXT NOT NULL, -- e.g. "stripe", "gmail", "clickup", "domain:example.com"
26
+ action TEXT NOT NULL, -- "read" | "write" | "execute" | "*"
27
+ scope TEXT, -- optional: e.g. "refunds", "inbox", "tasks/123"
28
+ max_amount REAL, -- monetary cap per single action
29
+ currency TEXT DEFAULT 'USD',
30
+ daily_cap REAL, -- monetary cap per 24h rolling
31
+ per_call_rate INTEGER, -- max calls per minute
32
+ requires_approval INTEGER NOT NULL DEFAULT 0, -- 1 = always send to human gate
33
+ effect TEXT NOT NULL DEFAULT 'allow' CHECK(effect IN ('allow','deny')),
34
+ expires_at TEXT,
35
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
36
+ FOREIGN KEY (agent_id) REFERENCES gov_agents(agent_id) ON DELETE CASCADE
37
+ );
38
+ CREATE INDEX IF NOT EXISTS idx_gov_policies_agent ON gov_policies(agent_id);
39
+ CREATE INDEX IF NOT EXISTS idx_gov_policies_lookup ON gov_policies(agent_id, resource, action);
40
+
41
+ -- Append-only audit log with HMAC hash chain (tamper-evident).
42
+ -- prev_hash → hash chain links every entry; breaking the chain detects tampering.
43
+ CREATE TABLE IF NOT EXISTS gov_audit (
44
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
45
+ agent_id TEXT NOT NULL,
46
+ ts TEXT NOT NULL DEFAULT (datetime('now')),
47
+ event_type TEXT NOT NULL, -- 'check' | 'execute' | 'deny' | 'approval_request' | 'approval_decision' | 'kill' | 'policy_change' | 'note'
48
+ resource TEXT,
49
+ action TEXT,
50
+ scope TEXT,
51
+ amount REAL,
52
+ currency TEXT,
53
+ decision TEXT, -- 'allow' | 'deny' | 'pending' | 'approved' | 'rejected'
54
+ reason TEXT,
55
+ params_json TEXT, -- redacted parameter snapshot
56
+ result_json TEXT,
57
+ prev_hash TEXT, -- prior entry's hash
58
+ hash TEXT NOT NULL, -- HMAC(secret, prev_hash || row_payload)
59
+ FOREIGN KEY (agent_id) REFERENCES gov_agents(agent_id) ON DELETE CASCADE
60
+ );
61
+ CREATE INDEX IF NOT EXISTS idx_gov_audit_agent_ts ON gov_audit(agent_id, ts);
62
+ CREATE INDEX IF NOT EXISTS idx_gov_audit_event ON gov_audit(agent_id, event_type);
63
+
64
+ -- Approval requests. Async — agent requests, human resolves later.
65
+ CREATE TABLE IF NOT EXISTS gov_approvals (
66
+ request_id TEXT PRIMARY KEY,
67
+ agent_id TEXT NOT NULL,
68
+ resource TEXT NOT NULL,
69
+ action TEXT NOT NULL,
70
+ scope TEXT,
71
+ amount REAL,
72
+ currency TEXT,
73
+ params_json TEXT,
74
+ reason TEXT, -- why approval is required
75
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','approved','rejected','expired','cancelled')),
76
+ decided_by TEXT, -- user_id of approver
77
+ decided_at TEXT,
78
+ decided_note TEXT,
79
+ expires_at TEXT, -- auto-expire pending requests
80
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
81
+ FOREIGN KEY (agent_id) REFERENCES gov_agents(agent_id) ON DELETE CASCADE
82
+ );
83
+ CREATE INDEX IF NOT EXISTS idx_gov_approvals_pending ON gov_approvals(agent_id, status);
84
+
85
+ -- Spend tracker (per agent, per resource, sliding window).
86
+ -- Rebuilt rolling-style; we just append on every monetary action.
87
+ CREATE TABLE IF NOT EXISTS gov_spend (
88
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
89
+ agent_id TEXT NOT NULL,
90
+ resource TEXT NOT NULL,
91
+ amount REAL NOT NULL,
92
+ currency TEXT NOT NULL DEFAULT 'USD',
93
+ ts TEXT NOT NULL DEFAULT (datetime('now')),
94
+ ref TEXT, -- audit_id or external ref
95
+ FOREIGN KEY (agent_id) REFERENCES gov_agents(agent_id) ON DELETE CASCADE
96
+ );
97
+ CREATE INDEX IF NOT EXISTS idx_gov_spend_window ON gov_spend(agent_id, resource, ts);
98
+
99
+ -- Rate-limit token buckets (lightweight; we keep counters).
100
+ CREATE TABLE IF NOT EXISTS gov_rate (
101
+ agent_id TEXT NOT NULL,
102
+ resource TEXT NOT NULL,
103
+ window_start TEXT NOT NULL, -- ISO timestamp (minute-resolution)
104
+ count INTEGER NOT NULL DEFAULT 0,
105
+ PRIMARY KEY (agent_id, resource, window_start)
106
+ );