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.
- package/LICENSE +12 -0
- package/README.ar.md +18 -0
- package/README.md +198 -1664
- package/bin/wab-init.js +223 -0
- package/examples/azure-dns-wab.js +83 -0
- package/examples/cloudflare-wab-dns.js +121 -0
- package/examples/cpanel-wab-dns.js +114 -0
- package/examples/dns-discovery-agent.js +166 -0
- package/examples/gcp-dns-wab.js +76 -0
- package/examples/governance-agent.js +169 -0
- package/examples/plesk-wab-dns.js +103 -0
- package/examples/route53-wab-dns.js +144 -0
- package/examples/safe-mode-agent.js +96 -0
- package/examples/wab-sign.js +74 -0
- package/examples/wab-verify.js +60 -0
- package/package.json +5 -5
- package/public/.well-known/wab.json +28 -0
- package/public/activate.html +368 -0
- package/public/adoption-metrics.html +188 -0
- package/public/api.html +1 -1
- package/public/azure-dns-integration.html +289 -0
- package/public/cloudflare-integration.html +380 -0
- package/public/cpanel-integration.html +398 -0
- package/public/css/styles.css +28 -0
- package/public/dashboard.html +1 -0
- package/public/dns.html +101 -172
- package/public/docs.html +1 -0
- package/public/gcp-dns-integration.html +318 -0
- package/public/growth.html +4 -2
- package/public/index.html +227 -31
- package/public/integrations.html +1 -1
- package/public/js/activate.js +145 -0
- package/public/js/auth-nav.js +34 -0
- package/public/js/dns.js +438 -0
- package/public/openapi.json +89 -0
- package/public/plesk-integration.html +375 -0
- package/public/premium.html +1 -1
- package/public/provider-onboarding.html +172 -0
- package/public/provider-sandbox.html +134 -0
- package/public/providers.html +359 -0
- package/public/registrar-integrations.html +141 -0
- package/public/robots.txt +12 -0
- package/public/route53-integration.html +531 -0
- package/public/shieldqr.html +231 -0
- package/public/sitemap.xml +6 -0
- package/public/wab-trust.html +200 -0
- package/public/wab-vs-protocols.html +210 -0
- package/public/whitepaper.html +449 -0
- package/sdk/auto-discovery.js +288 -0
- package/sdk/governance.js +262 -0
- package/sdk/index.js +13 -0
- package/sdk/package.json +2 -2
- package/sdk/safe-mode.js +221 -0
- package/server/index.js +144 -5
- package/server/migrations/007_governance.sql +106 -0
- package/server/migrations/008_plans.sql +144 -0
- package/server/migrations/009_shieldqr.sql +30 -0
- package/server/migrations/010_extended_trust.sql +33 -0
- package/server/models/adapters/mysql.js +1 -1
- package/server/models/adapters/postgresql.js +1 -1
- package/server/models/db.js +60 -1
- package/server/routes/admin-plans.js +76 -0
- package/server/routes/admin-premium.js +4 -2
- package/server/routes/admin-shieldqr.js +90 -0
- package/server/routes/admin-trust-monitor.js +83 -0
- package/server/routes/admin.js +289 -1
- package/server/routes/billing.js +16 -4
- package/server/routes/discovery.js +1933 -2
- package/server/routes/governance.js +208 -0
- package/server/routes/plans.js +33 -0
- package/server/routes/providers.js +650 -0
- package/server/routes/shieldqr.js +88 -0
- package/server/services/email.js +29 -0
- package/server/services/governance.js +466 -0
- package/server/services/plans.js +214 -0
- package/server/services/premium.js +1 -1
- package/server/services/provider-clients.js +740 -0
- package/server/services/shieldqr.js +322 -0
- package/server/services/ssl-inspector.js +42 -0
- package/server/services/ssl-monitor.js +167 -0
- package/server/services/stripe.js +18 -5
- package/server/services/vision.js +1 -1
- package/server/services/wab-crypto.js +178 -0
package/sdk/safe-mode.js
ADDED
|
@@ -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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
+
);
|