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,288 @@
1
+ /**
2
+ * WAB SDK — Auto-Discovery Fallback
3
+ *
4
+ * For sites that haven't installed WAB yet (no /.well-known/wab.json,
5
+ * no _wab DNS TXT), this module produces a normalized capabilities envelope
6
+ * by parsing publicly available metadata:
7
+ * 1. /.well-known/wab.json (canonical)
8
+ * 2. <script type="application/ld+json"> (JSON-LD / Schema.org)
9
+ * 3. <meta property="og:*"> (OpenGraph)
10
+ * 4. <meta name="description"> / <title>
11
+ * 5. /sitemap.xml (URL inventory)
12
+ * 6. /robots.txt (allow/disallow + Sitemap directives)
13
+ *
14
+ * The resulting envelope shape mirrors a minimal wab.json so downstream
15
+ * code can treat unsigned sites uniformly:
16
+ *
17
+ * {
18
+ * ok: boolean,
19
+ * source: 'wab.json' | 'auto-discovery',
20
+ * site: { name, description, url },
21
+ * trust: { signed: false, ssl: { ... } },
22
+ * actions: [ { name, description, source } ],
23
+ * products: [ { name, sku, offers } ],
24
+ * sitemap: [ url, ... ],
25
+ * robots: { allow: [], disallow: [], sitemaps: [] }
26
+ * }
27
+ *
28
+ * Pure JS, no external deps. Works in Node (with global fetch).
29
+ */
30
+
31
+ const { extractJsonLdBlocks, extractProductsFromHtml, suggestWabActionsFromProducts } =
32
+ require('./schema-discovery');
33
+
34
+ /* ------------------------------------------------------------------ */
35
+ /* Helpers */
36
+ /* ------------------------------------------------------------------ */
37
+
38
+ function _abs(base, path) {
39
+ try {
40
+ return new URL(path, base).toString();
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ async function _fetchText(url, { timeoutMs = 8000 } = {}) {
47
+ if (typeof fetch !== 'function') {
48
+ throw new Error('global fetch() is required (Node 18+) for auto-discovery');
49
+ }
50
+ const ctrl = new AbortController();
51
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
52
+ try {
53
+ const r = await fetch(url, {
54
+ redirect: 'follow',
55
+ signal: ctrl.signal,
56
+ headers: { 'user-agent': 'wab-auto-discovery/1.0 (+https://webagentbridge.com)' }
57
+ });
58
+ if (!r.ok) return { ok: false, status: r.status, text: '' };
59
+ return { ok: true, status: r.status, text: await r.text() };
60
+ } catch (e) {
61
+ return { ok: false, status: 0, text: '', error: e.message };
62
+ } finally {
63
+ clearTimeout(t);
64
+ }
65
+ }
66
+
67
+ /* ------------------------------------------------------------------ */
68
+ /* HTML metadata extractors */
69
+ /* ------------------------------------------------------------------ */
70
+
71
+ function extractMetaTags(html) {
72
+ const out = { og: {}, twitter: {}, description: null, title: null };
73
+ if (!html) return out;
74
+
75
+ const titleM = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
76
+ if (titleM) out.title = titleM[1].trim();
77
+
78
+ const metaRe = /<meta\b[^>]*>/gi;
79
+ let m;
80
+ while ((m = metaRe.exec(html)) !== null) {
81
+ const tag = m[0];
82
+ const nameM = tag.match(/\bname\s*=\s*["']([^"']+)["']/i);
83
+ const propM = tag.match(/\bproperty\s*=\s*["']([^"']+)["']/i);
84
+ const contentM = tag.match(/\bcontent\s*=\s*["']([^"']*)["']/i);
85
+ if (!contentM) continue;
86
+ const content = contentM[1];
87
+ const key = (propM && propM[1]) || (nameM && nameM[1]) || '';
88
+ if (!key) continue;
89
+ const lk = key.toLowerCase();
90
+ if (lk === 'description') out.description = content;
91
+ else if (lk.startsWith('og:')) out.og[lk.slice(3)] = content;
92
+ else if (lk.startsWith('twitter:')) out.twitter[lk.slice(8)] = content;
93
+ }
94
+ return out;
95
+ }
96
+
97
+ /* ------------------------------------------------------------------ */
98
+ /* Sitemap / robots */
99
+ /* ------------------------------------------------------------------ */
100
+
101
+ function parseSitemap(xml, { limit = 200 } = {}) {
102
+ if (!xml) return [];
103
+ const urls = [];
104
+ const re = /<loc>\s*([^<\s]+)\s*<\/loc>/gi;
105
+ let m;
106
+ while ((m = re.exec(xml)) !== null && urls.length < limit) {
107
+ urls.push(m[1]);
108
+ }
109
+ return urls;
110
+ }
111
+
112
+ function parseRobots(text) {
113
+ const out = { allow: [], disallow: [], sitemaps: [], userAgents: [] };
114
+ if (!text) return out;
115
+ let currentUA = '*';
116
+ for (const raw of text.split(/\r?\n/)) {
117
+ const line = raw.replace(/#.*$/, '').trim();
118
+ if (!line) continue;
119
+ const idx = line.indexOf(':');
120
+ if (idx < 0) continue;
121
+ const key = line.slice(0, idx).trim().toLowerCase();
122
+ const val = line.slice(idx + 1).trim();
123
+ if (key === 'user-agent') {
124
+ currentUA = val;
125
+ if (!out.userAgents.includes(val)) out.userAgents.push(val);
126
+ } else if (key === 'allow') out.allow.push({ ua: currentUA, path: val });
127
+ else if (key === 'disallow') out.disallow.push({ ua: currentUA, path: val });
128
+ else if (key === 'sitemap') out.sitemaps.push(val);
129
+ }
130
+ return out;
131
+ }
132
+
133
+ /* ------------------------------------------------------------------ */
134
+ /* JSON-LD extras: WebSite + Organization + SearchAction */
135
+ /* ------------------------------------------------------------------ */
136
+
137
+ function extractSiteIdentity(html) {
138
+ const blocks = extractJsonLdBlocks(html);
139
+ const out = { name: null, description: null, url: null, search: null, organization: null };
140
+ for (const text of blocks) {
141
+ let data;
142
+ try { data = JSON.parse(text); } catch { continue; }
143
+ const items = Array.isArray(data) ? data : Array.isArray(data['@graph']) ? data['@graph'] : [data];
144
+ for (const node of items) {
145
+ if (!node || typeof node !== 'object') continue;
146
+ let types = node['@type'];
147
+ if (typeof types === 'string') types = [types];
148
+ if (!Array.isArray(types)) types = [];
149
+ if (types.includes('WebSite')) {
150
+ out.name = out.name || node.name;
151
+ out.url = out.url || node.url;
152
+ out.description = out.description || node.description;
153
+ const action = node.potentialAction;
154
+ if (action && (Array.isArray(action) ? action[0] : action)) {
155
+ const a = Array.isArray(action) ? action[0] : action;
156
+ if (a && (a['@type'] === 'SearchAction' || /SearchAction/.test(String(a['@type'])))) {
157
+ out.search = {
158
+ target: typeof a.target === 'string' ? a.target : (a.target && a.target.urlTemplate),
159
+ queryParam: a['query-input'] || a.queryInput || null
160
+ };
161
+ }
162
+ }
163
+ } else if (types.includes('Organization')) {
164
+ out.organization = out.organization || { name: node.name, url: node.url, logo: node.logo };
165
+ }
166
+ }
167
+ }
168
+ return out;
169
+ }
170
+
171
+ /* ------------------------------------------------------------------ */
172
+ /* Main entry */
173
+ /* ------------------------------------------------------------------ */
174
+
175
+ /**
176
+ * Discover capabilities for a site. Tries /.well-known/wab.json first;
177
+ * falls back to HTML/sitemap/robots scraping.
178
+ *
179
+ * @param {string} siteUrl e.g. "https://example.com"
180
+ * @param {object} [opts]
181
+ * @param {number} [opts.timeoutMs=8000]
182
+ * @param {number} [opts.sitemapLimit=200]
183
+ * @param {boolean} [opts.skipWabJson=false]
184
+ * @returns {Promise<object>} normalized envelope
185
+ */
186
+ async function discover(siteUrl, opts = {}) {
187
+ const timeoutMs = opts.timeoutMs || 8000;
188
+ const sitemapLimit = opts.sitemapLimit || 200;
189
+
190
+ let baseUrl;
191
+ try { baseUrl = new URL(siteUrl).origin; }
192
+ catch { return { ok: false, error: 'invalid_url', source: 'auto-discovery' }; }
193
+
194
+ // 1) Canonical wab.json — if present, use as authoritative source.
195
+ if (!opts.skipWabJson) {
196
+ const wabUrl = _abs(baseUrl, '/.well-known/wab.json');
197
+ const wabRes = await _fetchText(wabUrl, { timeoutMs });
198
+ if (wabRes.ok && wabRes.text) {
199
+ try {
200
+ const parsed = JSON.parse(wabRes.text);
201
+ return {
202
+ ok: true,
203
+ source: 'wab.json',
204
+ site: { name: parsed.site || parsed.name, description: parsed.description, url: baseUrl },
205
+ trust: { signed: !!parsed.sig, ...(parsed.trust || {}) },
206
+ actions: Array.isArray(parsed.actions) ? parsed.actions : [],
207
+ products: [],
208
+ sitemap: [],
209
+ robots: null,
210
+ raw: parsed
211
+ };
212
+ } catch { /* fall through to auto-discovery */ }
213
+ }
214
+ }
215
+
216
+ // 2) Fetch homepage HTML in parallel with sitemap + robots.
217
+ const [homeRes, sitemapRes, robotsRes] = await Promise.all([
218
+ _fetchText(baseUrl, { timeoutMs }),
219
+ _fetchText(_abs(baseUrl, '/sitemap.xml'), { timeoutMs }),
220
+ _fetchText(_abs(baseUrl, '/robots.txt'), { timeoutMs })
221
+ ]);
222
+
223
+ const html = homeRes.text || '';
224
+ const meta = extractMetaTags(html);
225
+ const ident = extractSiteIdentity(html);
226
+ const products = extractProductsFromHtml(html);
227
+ const robots = parseRobots(robotsRes.text || '');
228
+ let sitemap = parseSitemap(sitemapRes.text || '', { limit: sitemapLimit });
229
+
230
+ // Discover additional sitemaps from robots.
231
+ if (sitemap.length === 0 && robots.sitemaps.length) {
232
+ for (const sm of robots.sitemaps.slice(0, 3)) {
233
+ const r = await _fetchText(sm, { timeoutMs });
234
+ if (r.ok) sitemap = sitemap.concat(parseSitemap(r.text, { limit: sitemapLimit }));
235
+ if (sitemap.length >= sitemapLimit) break;
236
+ }
237
+ }
238
+
239
+ // Build action hints.
240
+ const actions = suggestWabActionsFromProducts(products);
241
+ if (ident.search && ident.search.target) {
242
+ actions.push({
243
+ name: 'searchSite',
244
+ description: 'Schema.org SearchAction: ' + ident.search.target,
245
+ source: 'schema.org/SearchAction',
246
+ template: ident.search.target
247
+ });
248
+ }
249
+ if (sitemap.length) {
250
+ actions.push({
251
+ name: 'browseSitemap',
252
+ description: `${sitemap.length} URLs discovered from sitemap.xml`,
253
+ source: 'sitemap.xml'
254
+ });
255
+ }
256
+ if (meta.og && meta.og.url) {
257
+ actions.push({
258
+ name: 'getOpenGraph',
259
+ description: 'OpenGraph metadata available',
260
+ source: 'opengraph'
261
+ });
262
+ }
263
+
264
+ return {
265
+ ok: true,
266
+ source: 'auto-discovery',
267
+ site: {
268
+ name: ident.name || meta.og.site_name || meta.title || baseUrl,
269
+ description: ident.description || meta.description || meta.og.description || null,
270
+ url: ident.url || meta.og.url || baseUrl
271
+ },
272
+ trust: { signed: false, auto: true },
273
+ actions,
274
+ products,
275
+ sitemap,
276
+ robots,
277
+ meta: { og: meta.og, twitter: meta.twitter, title: meta.title, description: meta.description },
278
+ organization: ident.organization || null
279
+ };
280
+ }
281
+
282
+ module.exports = {
283
+ discover,
284
+ extractMetaTags,
285
+ parseSitemap,
286
+ parseRobots,
287
+ extractSiteIdentity
288
+ };
@@ -0,0 +1,262 @@
1
+ /**
2
+ * WAB Agent Governance — Client SDK
3
+ * ─────────────────────────────────
4
+ * The Layer-3 piece that sits ABOVE the WAB Protocol.
5
+ *
6
+ * ┌─────────────────────────────────┐
7
+ * │ WABGovernance (this module) │ ← permissions / approval / audit
8
+ * ├─────────────────────────────────┤
9
+ * │ WAB Protocol (AICommands) │
10
+ * ├─────────────────────────────────┤
11
+ * │ Dynamic Shield (price / OCR) │
12
+ * └─────────────────────────────────┘
13
+ *
14
+ * Five governance primitives every agent should call:
15
+ *
16
+ * 1) Permission Boundary await gov.check({resource,action,scope,amount})
17
+ * 2) Human Approval Gate await gov.requestApproval({...}) + waitForDecision()
18
+ * 3) Audit Log await gov.audit({eventType,...})
19
+ * 4) Kill Switch await gov.isAlive() / await gov.kill('reason')
20
+ * 5) Spend / Rate Limits enforced server-side via policies
21
+ *
22
+ * Convenience: gov.guard(actionDesc, fn) wraps an action with all 5 at once.
23
+ *
24
+ * Usage:
25
+ * const { WABGovernance } = require('web-agent-bridge/sdk');
26
+ * const gov = new WABGovernance({
27
+ * apiBase: 'https://webagentbridge.com',
28
+ * agentId: process.env.AGENT_ID,
29
+ * agentToken: process.env.AGENT_TOKEN,
30
+ * });
31
+ *
32
+ * await gov.guard(
33
+ * { resource: 'stripe', action: 'write', scope: 'refunds', amount: 49.99, currency: 'USD' },
34
+ * async () => stripe.refunds.create({ ... }),
35
+ * );
36
+ */
37
+
38
+ 'use strict';
39
+
40
+ const DEFAULT_API = 'https://webagentbridge.com';
41
+
42
+ class WABGovernanceError extends Error {
43
+ constructor(message, decision) {
44
+ super(message);
45
+ this.name = 'WABGovernanceError';
46
+ this.decision = decision || null;
47
+ }
48
+ }
49
+
50
+ class WABGovernance {
51
+ /**
52
+ * @param {object} opts
53
+ * @param {string} opts.apiBase
54
+ * @param {string} opts.agentId
55
+ * @param {string} opts.agentToken
56
+ * @param {function} [opts.onApprovalRequired] — async (request) => 'approved'|'rejected'
57
+ * If provided, guard() will block until human decides; otherwise it
58
+ * polls the approval endpoint up to opts.approvalTimeoutMs.
59
+ * @param {number} [opts.approvalTimeoutMs=300000] — 5 min default
60
+ * @param {number} [opts.approvalPollMs=2000]
61
+ * @param {number} [opts.timeoutMs=10000]
62
+ * @param {function} [opts.fetch]
63
+ */
64
+ constructor(opts = {}) {
65
+ this.apiBase = (opts.apiBase || DEFAULT_API).replace(/\/+$/, '');
66
+ this.agentId = opts.agentId;
67
+ this.agentToken = opts.agentToken;
68
+ this.onApprovalRequired = typeof opts.onApprovalRequired === 'function' ? opts.onApprovalRequired : null;
69
+ this.approvalTimeoutMs = Number.isFinite(opts.approvalTimeoutMs) ? opts.approvalTimeoutMs : 300_000;
70
+ this.approvalPollMs = Number.isFinite(opts.approvalPollMs) ? opts.approvalPollMs : 2_000;
71
+ this.timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : 10_000;
72
+ this._fetch = opts.fetch || (typeof fetch !== 'undefined' ? fetch : null);
73
+ if (!this._fetch) {
74
+ try { this._fetch = require('node-fetch'); } catch { /* user must supply */ }
75
+ }
76
+ if (!this.agentId) throw new Error('WABGovernance: agentId required');
77
+ if (!this.agentToken) throw new Error('WABGovernance: agentToken required');
78
+ }
79
+
80
+ // ────────────────────────────────────── HTTP plumbing ────
81
+ async _req(method, path, body) {
82
+ if (!this._fetch) throw new Error('WABGovernance: fetch not available');
83
+ const ctl = (typeof AbortController !== 'undefined') ? new AbortController() : null;
84
+ const timer = ctl ? setTimeout(() => ctl.abort(), this.timeoutMs) : null;
85
+ try {
86
+ const r = await this._fetch(this.apiBase + path, {
87
+ method,
88
+ headers: {
89
+ 'content-type': 'application/json',
90
+ 'authorization': 'Bearer ' + this.agentToken,
91
+ },
92
+ body: body ? JSON.stringify(body) : undefined,
93
+ signal: ctl ? ctl.signal : undefined,
94
+ });
95
+ const text = await r.text();
96
+ const data = text ? safeParse(text) : null;
97
+ if (!r.ok) {
98
+ const e = new Error(`gov_http_${r.status}: ${data?.error || r.statusText}`);
99
+ e.status = r.status; e.body = data;
100
+ throw e;
101
+ }
102
+ return data;
103
+ } finally { if (timer) clearTimeout(timer); }
104
+ }
105
+
106
+ // ────────────────────────────────────────── lifecycle ────
107
+ /** Static helper to register a brand-new agent and capture its token. */
108
+ static async register({ apiBase = DEFAULT_API, displayName, ownerId, metadata, fetch: f } = {}) {
109
+ const fn = f || (typeof fetch !== 'undefined' ? fetch : require('node-fetch'));
110
+ const r = await fn(apiBase.replace(/\/+$/, '') + '/api/governance/agents', {
111
+ method: 'POST',
112
+ headers: { 'content-type': 'application/json' },
113
+ body: JSON.stringify({ display_name: displayName, owner_id: ownerId, metadata }),
114
+ });
115
+ if (!r.ok) throw new Error('register_failed_' + r.status);
116
+ return r.json(); // { agent_id, agent_token, message }
117
+ }
118
+
119
+ status() { return this._req('GET', `/api/governance/agents/${this.agentId}/status`); }
120
+ async isAlive() {
121
+ const s = await this.status().catch(() => null);
122
+ return !!s && s.status === 'alive';
123
+ }
124
+ kill(reason) { return this._req('POST', `/api/governance/agents/${this.agentId}/kill`, { reason }); }
125
+ revive(reason) { return this._req('POST', `/api/governance/agents/${this.agentId}/revive`, { reason }); }
126
+
127
+ // ─────────────────────────────────────── policies ────
128
+ policies() { return this._req('GET', `/api/governance/agents/${this.agentId}/policies`); }
129
+ definePolicy(p) { return this._req('POST', `/api/governance/agents/${this.agentId}/policies`, p); }
130
+ removePolicy(id) { return this._req('DELETE', `/api/governance/agents/${this.agentId}/policies/${id}`); }
131
+
132
+ // ────────────────────────────────────── decision ────
133
+ /** Pre-action permission check. */
134
+ check({ resource, action, scope, amount, currency }) {
135
+ return this._req('POST', `/api/governance/agents/${this.agentId}/check`,
136
+ { resource, action, scope, amount, currency });
137
+ }
138
+
139
+ /** Report executed action — feeds audit + spend tracker. */
140
+ reportExecute(payload) {
141
+ return this._req('POST', `/api/governance/agents/${this.agentId}/execute`, payload);
142
+ }
143
+
144
+ // ────────────────────────────────────── audit ────
145
+ audit({ eventType = 'note', resource, action, scope, decision, reason, params, result } = {}) {
146
+ // Convenience wrapper around reportExecute() for non-execute events.
147
+ return this._req('POST', `/api/governance/agents/${this.agentId}/execute`, {
148
+ resource: resource || 'note', action: action || eventType, scope,
149
+ decision: decision || 'allow', reason, params, result,
150
+ });
151
+ }
152
+ getAudit(opts = {}) {
153
+ const q = new URLSearchParams();
154
+ if (opts.limit) q.set('limit', String(opts.limit));
155
+ if (opts.since) q.set('since', opts.since);
156
+ if (opts.event) q.set('event', opts.event);
157
+ const qs = q.toString();
158
+ return this._req('GET', `/api/governance/agents/${this.agentId}/audit${qs ? '?' + qs : ''}`);
159
+ }
160
+ verifyAudit() {
161
+ return this._req('GET', `/api/governance/agents/${this.agentId}/audit/verify`);
162
+ }
163
+
164
+ // ─────────────────────────────────── approvals ────
165
+ requestApproval(payload) {
166
+ return this._req('POST', `/api/governance/agents/${this.agentId}/approvals`, payload);
167
+ }
168
+ pendingApprovals() {
169
+ return this._req('GET', `/api/governance/agents/${this.agentId}/approvals/pending`);
170
+ }
171
+ getApproval(requestId) {
172
+ return this._req('GET', `/api/governance/approvals/${requestId}`);
173
+ }
174
+ decideApproval(requestId, decision, note) {
175
+ return this._req('POST', `/api/governance/approvals/${requestId}/decide`,
176
+ { decision, note });
177
+ }
178
+
179
+ /** Block until an approval is decided or times out. */
180
+ async waitForDecision(requestId, { timeoutMs, pollMs } = {}) {
181
+ const tEnd = Date.now() + (timeoutMs || this.approvalTimeoutMs);
182
+ const step = pollMs || this.approvalPollMs;
183
+ while (Date.now() < tEnd) {
184
+ const r = await this.getApproval(requestId).catch(() => null);
185
+ if (r && r.status !== 'pending') return r;
186
+ await sleep(step);
187
+ }
188
+ return { request_id: requestId, status: 'timeout' };
189
+ }
190
+
191
+ // ────────────────────────────────────── guard (the big one) ────
192
+ /**
193
+ * Wrap an action. Runs the full governance pipeline:
194
+ * 1) check permission
195
+ * 2) if approval_required → request + wait for human
196
+ * 3) execute the function
197
+ * 4) record execute (audit + spend)
198
+ * 5) on throw → audit deny + rethrow
199
+ */
200
+ async guard(actionDesc, fn) {
201
+ const { resource, action, scope, amount, currency } = actionDesc || {};
202
+ if (!resource || !action) throw new WABGovernanceError('guard: resource and action required');
203
+
204
+ // 1) Pre-check
205
+ const v = await this.check({ resource, action, scope, amount, currency });
206
+
207
+ // 2) Approval gate
208
+ if (v.decision === 'approval_required') {
209
+ const req = await this.requestApproval({
210
+ resource, action, scope, amount, currency,
211
+ params: actionDesc.params || null,
212
+ reason: actionDesc.reason || 'policy_required_approval',
213
+ ttl_ms: actionDesc.ttlMs || null,
214
+ });
215
+ let outcome;
216
+ if (this.onApprovalRequired) {
217
+ outcome = await this.onApprovalRequired({ request_id: req.requestId, ...actionDesc });
218
+ if (outcome === 'approved' || outcome === 'rejected') {
219
+ await this.decideApproval(req.requestId, outcome, 'callback_decision');
220
+ }
221
+ }
222
+ const decided = await this.waitForDecision(req.requestId);
223
+ if (decided.status !== 'approved') {
224
+ throw new WABGovernanceError(
225
+ `Approval ${decided.status} for ${resource}/${action}`,
226
+ { ...v, approval: decided },
227
+ );
228
+ }
229
+ } else if (v.decision !== 'allow') {
230
+ throw new WABGovernanceError(
231
+ `Governance denied ${resource}/${action}: ${v.reason}`, v,
232
+ );
233
+ }
234
+
235
+ // 3) Execute
236
+ let result, error;
237
+ const t0 = Date.now();
238
+ try { result = await fn(v); }
239
+ catch (e) { error = e; }
240
+ const elapsed = Date.now() - t0;
241
+
242
+ // 4) Audit (always, even on error)
243
+ try {
244
+ await this.reportExecute({
245
+ resource, action, scope, amount, currency,
246
+ decision: error ? 'deny' : 'allow',
247
+ reason: error ? ('exec_error: ' + (error.message || 'unknown')) : 'guard_executed',
248
+ params: actionDesc.params || null,
249
+ result: error ? null : (typeof result === 'object' ? result : { value: result }),
250
+ });
251
+ } catch { /* audit best-effort; don't mask the original outcome */ }
252
+
253
+ if (error) throw error;
254
+ return { result, elapsed_ms: elapsed };
255
+ }
256
+ }
257
+
258
+ // ─────────────────────────────────────────── helpers ────
259
+ function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
260
+ function safeParse(s) { try { return JSON.parse(s); } catch { return null; } }
261
+
262
+ module.exports = { WABGovernance, WABGovernanceError };
package/sdk/index.js CHANGED
@@ -624,6 +624,12 @@ try { WABToolkit = require('../packages/langchain').WABToolkit; } catch {
624
624
 
625
625
  // SPEC §8.10–§8.13 client helper
626
626
  const { SafetyShieldClient } = require('./safety-shield');
627
+ // Phase 19 — Safe Mode trust gate
628
+ const { WABSafeMode, WABSafeModeError, POLICIES: WAB_SAFE_POLICIES } = require('./safe-mode');
629
+ // Phase 20 — Agent Governance Layer (permissions + approval + audit + kill-switch)
630
+ const { WABGovernance, WABGovernanceError } = require('./governance');
631
+ // Zero-Config Adoption — Auto-Discovery fallback for sites without /.well-known/wab.json
632
+ const autoDiscovery = require('./auto-discovery');
627
633
 
628
634
  module.exports = {
629
635
  WABAgent,
@@ -633,4 +639,11 @@ module.exports = {
633
639
  WABAgentOS,
634
640
  WABToolkit,
635
641
  SafetyShieldClient,
642
+ WABSafeMode,
643
+ WABSafeModeError,
644
+ WAB_SAFE_POLICIES,
645
+ WABGovernance,
646
+ WABGovernanceError,
647
+ autoDiscovery,
648
+ discover: autoDiscovery.discover,
636
649
  };
package/sdk/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "web-agent-bridge-sdk",
3
- "version": "3.1.0",
4
- "description": "SDK for building AI agents that interact with Web Agent Bridge (WAB)",
3
+ "version": "3.2.0",
4
+ "description": "SDK for building AI agents that interact with Web Agent Bridge (WAB) — includes auto-discovery fallback, ShieldQR verifier, and governance layer.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
7
7
  "license": "MIT",