web-agent-bridge 3.2.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 +84 -72
- package/README.ar.md +1304 -1152
- package/README.md +298 -1635
- package/bin/agent-runner.js +474 -474
- package/bin/cli.js +237 -138
- package/bin/wab-init.js +223 -0
- package/bin/wab.js +80 -80
- package/examples/azure-dns-wab.js +83 -0
- package/examples/bidi-agent.js +119 -119
- package/examples/cloudflare-wab-dns.js +121 -0
- package/examples/cpanel-wab-dns.js +114 -0
- package/examples/cross-site-agent.js +91 -91
- 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/mcp-agent.js +94 -94
- package/examples/next-app-router/README.md +44 -44
- package/examples/plesk-wab-dns.js +103 -0
- package/examples/puppeteer-agent.js +108 -108
- package/examples/route53-wab-dns.js +144 -0
- package/examples/saas-dashboard/README.md +55 -55
- package/examples/safe-mode-agent.js +96 -0
- package/examples/shopify-hydrogen/README.md +74 -74
- package/examples/vision-agent.js +171 -171
- package/examples/wab-sign.js +74 -0
- package/examples/wab-verify.js +60 -0
- package/examples/wordpress-elementor/README.md +77 -77
- package/package.json +19 -6
- package/public/.well-known/agent-tools.json +180 -180
- package/public/.well-known/ai-assets.json +59 -59
- package/public/.well-known/security.txt +8 -0
- package/public/.well-known/wab.json +28 -0
- package/public/activate.html +368 -0
- package/public/adoption-metrics.html +188 -0
- package/public/agent-workspace.html +349 -349
- package/public/ai.html +198 -198
- package/public/api.html +413 -412
- package/public/azure-dns-integration.html +289 -0
- package/public/browser.html +486 -486
- package/public/cloudflare-integration.html +380 -0
- package/public/commander-dashboard.html +243 -243
- package/public/cookies.html +210 -210
- package/public/cpanel-integration.html +398 -0
- package/public/css/agent-workspace.css +1713 -1713
- package/public/css/premium.css +317 -317
- package/public/css/styles.css +1263 -1235
- package/public/dashboard.html +707 -706
- package/public/dns.html +436 -0
- package/public/docs.html +588 -587
- package/public/feed.xml +89 -89
- package/public/gcp-dns-integration.html +318 -0
- package/public/growth.html +465 -463
- package/public/index.html +1266 -982
- package/public/integrations.html +556 -0
- package/public/js/activate.js +145 -0
- package/public/js/agent-workspace.js +1740 -1740
- package/public/js/auth-nav.js +65 -31
- package/public/js/auth-redirect.js +12 -12
- package/public/js/cookie-consent.js +56 -56
- package/public/js/dns.js +438 -0
- package/public/js/wab-demo-page.js +721 -721
- package/public/js/ws-client.js +74 -74
- package/public/llms-full.txt +360 -360
- package/public/llms.txt +125 -125
- package/public/login.html +85 -85
- package/public/mesh-dashboard.html +328 -328
- package/public/openapi.json +669 -580
- package/public/phone-shield.html +281 -0
- package/public/plesk-integration.html +375 -0
- package/public/premium-dashboard.html +2489 -2489
- package/public/premium.html +793 -793
- package/public/privacy.html +297 -297
- package/public/provider-onboarding.html +172 -0
- package/public/provider-sandbox.html +134 -0
- package/public/providers.html +359 -0
- package/public/register.html +105 -105
- package/public/registrar-integrations.html +141 -0
- package/public/robots.txt +99 -87
- package/public/route53-integration.html +531 -0
- package/public/script/wab-consent.d.ts +36 -36
- package/public/script/wab-consent.js +104 -104
- package/public/script/wab-schema.js +131 -131
- package/public/script/wab.d.ts +108 -108
- package/public/script/wab.min.js +580 -580
- package/public/security.txt +8 -0
- package/public/shieldqr.html +231 -0
- package/public/sitemap.xml +6 -0
- package/public/terms.html +256 -256
- package/public/wab-trust.html +200 -0
- package/public/wab-vs-protocols.html +210 -0
- package/public/whitepaper.html +449 -0
- package/script/ai-agent-bridge.js +1754 -1754
- package/sdk/README.md +99 -99
- package/sdk/agent-mesh.js +449 -449
- package/sdk/auto-discovery.js +288 -0
- package/sdk/commander.js +262 -262
- package/sdk/governance.js +262 -0
- package/sdk/index.d.ts +464 -464
- package/sdk/index.js +25 -1
- package/sdk/multi-agent.js +318 -318
- package/sdk/package.json +2 -2
- package/sdk/safe-mode.js +221 -0
- package/sdk/safety-shield.js +219 -0
- package/sdk/schema-discovery.js +83 -83
- package/server/adapters/index.js +520 -520
- package/server/config/plans.js +367 -367
- package/server/config/secrets.js +102 -102
- package/server/control-plane/index.js +301 -301
- package/server/data-plane/index.js +354 -354
- package/server/index.js +670 -427
- package/server/llm/index.js +404 -404
- package/server/middleware/adminAuth.js +35 -35
- package/server/middleware/auth.js +50 -50
- package/server/middleware/featureGate.js +88 -88
- package/server/middleware/rateLimits.js +100 -100
- package/server/middleware/sensitiveAction.js +157 -0
- package/server/migrations/001_add_analytics_indexes.sql +7 -7
- package/server/migrations/002_premium_features.sql +418 -418
- package/server/migrations/003_ads_integer_cents.sql +33 -33
- package/server/migrations/004_agent_os.sql +158 -158
- package/server/migrations/005_marketplace_metering.sql +126 -126
- 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/index.js +33 -33
- package/server/models/adapters/mysql.js +183 -183
- package/server/models/adapters/postgresql.js +172 -172
- package/server/models/adapters/sqlite.js +7 -7
- package/server/models/db.js +740 -681
- package/server/observability/failure-analysis.js +337 -337
- package/server/observability/index.js +394 -394
- package/server/protocol/capabilities.js +223 -223
- package/server/protocol/index.js +243 -243
- package/server/protocol/schema.js +584 -584
- package/server/registry/certification.js +271 -271
- package/server/registry/index.js +326 -326
- package/server/routes/admin-plans.js +76 -0
- package/server/routes/admin-premium.js +673 -671
- package/server/routes/admin-shieldqr.js +90 -0
- package/server/routes/admin-trust-monitor.js +83 -0
- package/server/routes/admin.js +549 -261
- package/server/routes/ads.js +130 -130
- package/server/routes/agent-workspace.js +540 -540
- package/server/routes/api.js +150 -150
- package/server/routes/auth.js +71 -71
- package/server/routes/billing.js +57 -45
- package/server/routes/commander.js +316 -316
- package/server/routes/demo-showcase.js +332 -332
- package/server/routes/demo-store.js +154 -0
- package/server/routes/discovery.js +2348 -417
- package/server/routes/gateway.js +173 -157
- package/server/routes/governance.js +208 -0
- package/server/routes/license.js +251 -240
- package/server/routes/mesh.js +469 -469
- package/server/routes/noscript.js +543 -543
- package/server/routes/plans.js +33 -0
- package/server/routes/premium-v2.js +686 -686
- package/server/routes/premium.js +724 -724
- package/server/routes/providers.js +650 -0
- package/server/routes/runtime.js +2148 -2147
- package/server/routes/shieldqr.js +88 -0
- package/server/routes/sovereign.js +465 -385
- package/server/routes/universal.js +200 -185
- package/server/routes/wab-api.js +850 -501
- package/server/runtime/container-worker.js +111 -111
- package/server/runtime/container.js +448 -448
- package/server/runtime/distributed-worker.js +362 -362
- package/server/runtime/event-bus.js +210 -210
- package/server/runtime/index.js +253 -253
- package/server/runtime/queue.js +599 -599
- package/server/runtime/replay.js +666 -666
- package/server/runtime/sandbox.js +266 -266
- package/server/runtime/scheduler.js +534 -534
- package/server/runtime/session-engine.js +293 -293
- package/server/runtime/state-manager.js +188 -188
- package/server/security/cross-site-redactor.js +196 -0
- package/server/security/dry-run.js +180 -0
- package/server/security/human-gate-rate-limit.js +147 -0
- package/server/security/human-gate-transports.js +178 -0
- package/server/security/human-gate.js +281 -0
- package/server/security/index.js +368 -368
- package/server/security/intent-engine.js +245 -0
- package/server/security/reward-guard.js +171 -0
- package/server/security/rollback-store.js +239 -0
- package/server/security/token-scope.js +404 -0
- package/server/security/url-policy.js +139 -0
- package/server/services/agent-chat.js +506 -506
- package/server/services/agent-learning.js +601 -575
- package/server/services/agent-memory.js +625 -625
- package/server/services/agent-mesh.js +555 -539
- package/server/services/agent-symphony.js +717 -717
- package/server/services/agent-tasks.js +1807 -1807
- package/server/services/api-key-engine.js +292 -261
- package/server/services/cluster.js +894 -894
- package/server/services/commander.js +738 -738
- package/server/services/edge-compute.js +440 -440
- package/server/services/email.js +233 -204
- package/server/services/governance.js +466 -0
- package/server/services/hosted-runtime.js +205 -205
- package/server/services/lfd.js +635 -635
- package/server/services/local-ai.js +389 -389
- package/server/services/marketplace.js +270 -270
- package/server/services/metering.js +182 -182
- package/server/services/modules/affiliate-intelligence.js +93 -93
- package/server/services/modules/agent-firewall.js +90 -90
- package/server/services/modules/bounty.js +89 -89
- package/server/services/modules/collective-bargaining.js +92 -92
- package/server/services/modules/dark-pattern.js +66 -66
- package/server/services/modules/gov-intelligence.js +45 -45
- package/server/services/modules/neural.js +55 -55
- package/server/services/modules/notary.js +49 -49
- package/server/services/modules/price-time-machine.js +86 -86
- package/server/services/modules/protocol.js +104 -104
- package/server/services/negotiation.js +439 -439
- package/server/services/plans.js +214 -0
- package/server/services/plugins.js +771 -771
- package/server/services/premium.js +1 -1
- package/server/services/price-intelligence.js +566 -566
- package/server/services/price-shield.js +1137 -1137
- package/server/services/provider-clients.js +740 -0
- package/server/services/reputation.js +465 -465
- package/server/services/search-engine.js +357 -357
- package/server/services/security.js +513 -513
- package/server/services/self-healing.js +843 -843
- package/server/services/shieldqr.js +322 -0
- package/server/services/sovereign-shield.js +542 -0
- package/server/services/ssl-inspector.js +42 -0
- package/server/services/ssl-monitor.js +167 -0
- package/server/services/stripe.js +205 -192
- package/server/services/swarm.js +788 -788
- package/server/services/universal-scraper.js +662 -661
- package/server/services/verification.js +481 -481
- package/server/services/vision.js +1163 -1163
- package/server/services/wab-crypto.js +178 -0
- package/server/utils/cache.js +125 -125
- package/server/utils/migrate.js +81 -81
- package/server/utils/safe-fetch.js +228 -0
- package/server/utils/secureFields.js +50 -50
- package/server/ws.js +161 -161
- package/templates/artisan-marketplace.yaml +104 -104
- package/templates/book-price-scout.yaml +98 -98
- package/templates/electronics-price-tracker.yaml +108 -108
- package/templates/flight-deal-hunter.yaml +113 -113
- package/templates/freelancer-direct.yaml +116 -116
- package/templates/grocery-price-compare.yaml +93 -93
- package/templates/hotel-direct-booking.yaml +113 -113
- package/templates/local-services.yaml +98 -98
- package/templates/olive-oil-tunisia.yaml +88 -88
- package/templates/organic-farm-fresh.yaml +101 -101
- package/templates/restaurant-direct.yaml +97 -97
- package/public/score.html +0 -263
- package/server/migrations/006_growth_suite.sql +0 -138
- package/server/routes/growth.js +0 -962
- package/server/services/fairness-engine.js +0 -409
- package/server/services/fairness.js +0 -420
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 };
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAB Safety-Shield Client Helper (SPEC §8.10–§8.13)
|
|
3
|
+
*
|
|
4
|
+
* Wraps the 2-phase dry-run + human-gate protocol for HTTP API consumers
|
|
5
|
+
* so agents don't have to reimplement plan_id / confirmation_id juggling.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { SafetyShieldClient } = require('@webagentbridge/sdk/safety-shield');
|
|
9
|
+
* const client = new SafetyShieldClient({
|
|
10
|
+
* baseUrl: 'https://webagentbridge.com',
|
|
11
|
+
* sessionToken: '<bearer-token>',
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* // Single call — handles dry-run automatically, returns plan for review:
|
|
15
|
+
* const plan = await client.dryRun('deleteUser', { id: 42 });
|
|
16
|
+
* console.log(plan.simulation.summary);
|
|
17
|
+
*
|
|
18
|
+
* // Confirm with the plan_id (and code if human-gate engaged):
|
|
19
|
+
* const result = await client.confirmAction(plan, { code: '123456' });
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const DEFAULT_HEADERS = { 'Content-Type': 'application/json' };
|
|
25
|
+
|
|
26
|
+
class SafetyShieldClient {
|
|
27
|
+
/**
|
|
28
|
+
* @param {object} opts
|
|
29
|
+
* @param {string} opts.baseUrl — e.g. 'https://webagentbridge.com'
|
|
30
|
+
* @param {string} opts.sessionToken — Bearer token from /sessions
|
|
31
|
+
* @param {string} [opts.actionsPath='/api/wab/actions'] — endpoint root
|
|
32
|
+
* @param {string} [opts.humanGatePath='/api/wab/human-gate'] — endpoint root
|
|
33
|
+
* @param {function} [opts.fetchImpl=globalThis.fetch]
|
|
34
|
+
*/
|
|
35
|
+
constructor(opts = {}) {
|
|
36
|
+
if (!opts.baseUrl) throw new Error('SafetyShieldClient: baseUrl required');
|
|
37
|
+
if (!opts.sessionToken) throw new Error('SafetyShieldClient: sessionToken required');
|
|
38
|
+
this.baseUrl = String(opts.baseUrl).replace(/\/$/, '');
|
|
39
|
+
this.sessionToken = opts.sessionToken;
|
|
40
|
+
this.actionsPath = opts.actionsPath || '/api/wab/actions';
|
|
41
|
+
this.humanGatePath = opts.humanGatePath || '/api/wab/human-gate';
|
|
42
|
+
this.fetch = opts.fetchImpl || globalThis.fetch;
|
|
43
|
+
if (typeof this.fetch !== 'function') {
|
|
44
|
+
throw new Error('SafetyShieldClient: no fetch implementation available (Node 18+ or pass fetchImpl)');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_headers() {
|
|
49
|
+
return {
|
|
50
|
+
...DEFAULT_HEADERS,
|
|
51
|
+
Authorization: `Bearer ${this.sessionToken}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async _post(path, body) {
|
|
56
|
+
const res = await this.fetch(`${this.baseUrl}${path}`, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: this._headers(),
|
|
59
|
+
body: JSON.stringify(body || {}),
|
|
60
|
+
});
|
|
61
|
+
const text = await res.text();
|
|
62
|
+
let json = null;
|
|
63
|
+
try { json = text ? JSON.parse(text) : null; } catch { /* leave null */ }
|
|
64
|
+
return { status: res.status, ok: res.ok, body: json, raw: text };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async _get(path) {
|
|
68
|
+
const res = await this.fetch(`${this.baseUrl}${path}`, { headers: this._headers() });
|
|
69
|
+
const text = await res.text();
|
|
70
|
+
let json = null;
|
|
71
|
+
try { json = text ? JSON.parse(text) : null; } catch { /* leave null */ }
|
|
72
|
+
return { status: res.status, ok: res.ok, body: json };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Phase 1: Submit a dry-run for an action and return the plan envelope.
|
|
77
|
+
* Throws if the server returns an error other than a successful plan.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} actionName
|
|
80
|
+
* @param {object} [params]
|
|
81
|
+
* @returns {Promise<{plan_id:string, simulation:object, expires_at:string, raw:object}>}
|
|
82
|
+
*/
|
|
83
|
+
async dryRun(actionName, params = {}) {
|
|
84
|
+
const r = await this._post(`${this.actionsPath}/${encodeURIComponent(actionName)}`, {
|
|
85
|
+
params,
|
|
86
|
+
dry_run: true,
|
|
87
|
+
});
|
|
88
|
+
if (!r.ok) {
|
|
89
|
+
throw shieldError('dry_run_failed', r);
|
|
90
|
+
}
|
|
91
|
+
const result = (r.body && (r.body.result || r.body)) || {};
|
|
92
|
+
if (!result.plan_id) {
|
|
93
|
+
throw shieldError('dry_run_no_plan', r);
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
action: actionName,
|
|
97
|
+
params,
|
|
98
|
+
plan_id: result.plan_id,
|
|
99
|
+
simulation: result.simulation || null,
|
|
100
|
+
expires_at: result.expires_at || null,
|
|
101
|
+
raw: r.body,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Phase 2: Execute the action, automatically handling the human-gate
|
|
107
|
+
* loop if the server returns HUMAN_GATE_REQUIRED.
|
|
108
|
+
*
|
|
109
|
+
* If a 6-digit `code` is supplied AND a challenge is issued, the helper
|
|
110
|
+
* will call /human-gate/approve and then retry execution. If no code is
|
|
111
|
+
* supplied and a challenge is issued, the helper returns a `pending`
|
|
112
|
+
* envelope so the caller can prompt the human, then resume by calling
|
|
113
|
+
* `confirmAction(plan, { code })` again.
|
|
114
|
+
*
|
|
115
|
+
* @param {{action:string, params?:object, plan_id:string}} plan — from dryRun()
|
|
116
|
+
* @param {object} [opts]
|
|
117
|
+
* @param {string} [opts.code] — 6-digit human-gate code
|
|
118
|
+
* @param {string} [opts.confirmation_id] — pre-existing approval id
|
|
119
|
+
* @returns {Promise<object>}
|
|
120
|
+
*/
|
|
121
|
+
async confirmAction(plan, opts = {}) {
|
|
122
|
+
if (!plan || !plan.action || !plan.plan_id) {
|
|
123
|
+
throw new Error('confirmAction: plan must include {action, plan_id}');
|
|
124
|
+
}
|
|
125
|
+
const body = {
|
|
126
|
+
params: plan.params || {},
|
|
127
|
+
dry_run: false,
|
|
128
|
+
plan_id: plan.plan_id,
|
|
129
|
+
};
|
|
130
|
+
if (opts.confirmation_id) body.confirmation_id = opts.confirmation_id;
|
|
131
|
+
|
|
132
|
+
const r = await this._post(
|
|
133
|
+
`${this.actionsPath}/${encodeURIComponent(plan.action)}`,
|
|
134
|
+
body
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const code = r.body && r.body.error && r.body.error.code;
|
|
138
|
+
|
|
139
|
+
// Happy path — HTTP 2xx with no error envelope.
|
|
140
|
+
if (r.ok && !code) return r.body;
|
|
141
|
+
|
|
142
|
+
// Human-gate flow
|
|
143
|
+
if (code === 'HUMAN_GATE_REQUIRED' && r.status === 202) {
|
|
144
|
+
const err = r.body.error || {};
|
|
145
|
+
const challengeId = err.challenge_id || err.details?.challenge_id;
|
|
146
|
+
if (!challengeId) throw shieldError('human_gate_malformed', r);
|
|
147
|
+
|
|
148
|
+
// No code provided — bubble up so caller can prompt human.
|
|
149
|
+
if (!opts.code) {
|
|
150
|
+
return {
|
|
151
|
+
status: 'pending_human_gate',
|
|
152
|
+
challenge_id: challengeId,
|
|
153
|
+
expires_at: err.expires_at || err.details?.expires_at || null,
|
|
154
|
+
dispatched_to: err.dispatched_to || err.details?.dispatched_to || null,
|
|
155
|
+
plan,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Code supplied — approve and retry.
|
|
160
|
+
const approve = await this._post(`${this.humanGatePath}/approve`, {
|
|
161
|
+
challenge_id: challengeId,
|
|
162
|
+
code: String(opts.code),
|
|
163
|
+
});
|
|
164
|
+
if (!approve.ok) {
|
|
165
|
+
throw shieldError('human_gate_approve_failed', approve);
|
|
166
|
+
}
|
|
167
|
+
const confirmationId =
|
|
168
|
+
approve.body?.result?.confirmation_id ||
|
|
169
|
+
approve.body?.confirmation_id;
|
|
170
|
+
if (!confirmationId) throw shieldError('human_gate_no_confirmation', approve);
|
|
171
|
+
return this.confirmAction(plan, { confirmation_id: confirmationId });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (code === 'HUMAN_GATE_PENDING') {
|
|
175
|
+
// Retry by polling status.
|
|
176
|
+
return {
|
|
177
|
+
status: 'pending_human_gate',
|
|
178
|
+
challenge_id: opts.confirmation_id,
|
|
179
|
+
plan,
|
|
180
|
+
message: r.body.error.message,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
throw shieldError(code || 'execute_failed', r);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Convenience: dry-run + confirm in one call.
|
|
189
|
+
* If a human-gate is required and no code is passed, returns the pending
|
|
190
|
+
* envelope. Caller resumes by calling confirmAction(envelope.plan, {code}).
|
|
191
|
+
*/
|
|
192
|
+
async safeExecute(actionName, params, opts = {}) {
|
|
193
|
+
const plan = await this.dryRun(actionName, params);
|
|
194
|
+
return this.confirmAction(plan, opts);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Poll a human-gate challenge status (rarely needed — confirmAction
|
|
199
|
+
* handles the round-trip). Returns the raw status envelope.
|
|
200
|
+
*/
|
|
201
|
+
async humanGateStatus(challengeId) {
|
|
202
|
+
const r = await this._get(
|
|
203
|
+
`${this.humanGatePath}/${encodeURIComponent(challengeId)}/status`
|
|
204
|
+
);
|
|
205
|
+
return r.body;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function shieldError(code, response) {
|
|
210
|
+
const msg = response?.body?.error?.message ||
|
|
211
|
+
`WAB safety-shield error: ${code} (HTTP ${response?.status})`;
|
|
212
|
+
const err = new Error(msg);
|
|
213
|
+
err.code = code;
|
|
214
|
+
err.status = response?.status;
|
|
215
|
+
err.response = response?.body;
|
|
216
|
+
return err;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = { SafetyShieldClient };
|
package/sdk/schema-discovery.js
CHANGED
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Server-side / Node: extract schema.org Product nodes from HTML (JSON-LD blocks).
|
|
3
|
-
* No extra dependencies — regex-based script extraction (same semantics as browser WABSchema).
|
|
4
|
-
*
|
|
5
|
-
* @example
|
|
6
|
-
* const { extractProductsFromHtml, suggestWabActionsFromProducts } = require('./schema-discovery');
|
|
7
|
-
* const products = extractProductsFromHtml(htmlString);
|
|
8
|
-
* const hints = suggestWabActionsFromProducts(products);
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
function extractJsonLdBlocks(html) {
|
|
12
|
-
if (!html || typeof html !== 'string') return [];
|
|
13
|
-
const re = /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
14
|
-
const blocks = [];
|
|
15
|
-
let m;
|
|
16
|
-
while ((m = re.exec(html)) !== null) {
|
|
17
|
-
blocks.push(m[1].trim());
|
|
18
|
-
}
|
|
19
|
-
return blocks;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function flattenGraph(data) {
|
|
23
|
-
if (Array.isArray(data)) return data;
|
|
24
|
-
if (data && Array.isArray(data['@graph'])) return data['@graph'];
|
|
25
|
-
return [data];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @param {string} html
|
|
30
|
-
* @returns {Array<{ type: string, name?: string, sku?: string, offers?: unknown }>}
|
|
31
|
-
*/
|
|
32
|
-
function extractProductsFromHtml(html) {
|
|
33
|
-
const out = [];
|
|
34
|
-
const blocks = extractJsonLdBlocks(html);
|
|
35
|
-
for (const text of blocks) {
|
|
36
|
-
let data;
|
|
37
|
-
try {
|
|
38
|
-
data = JSON.parse(text);
|
|
39
|
-
} catch {
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
const items = flattenGraph(data);
|
|
43
|
-
for (const node of items) {
|
|
44
|
-
if (!node || typeof node !== 'object') continue;
|
|
45
|
-
let types = node['@type'];
|
|
46
|
-
if (typeof types === 'string') types = [types];
|
|
47
|
-
if (!Array.isArray(types)) types = [];
|
|
48
|
-
if (!types.includes('Product')) continue;
|
|
49
|
-
out.push({
|
|
50
|
-
type: 'Product',
|
|
51
|
-
name: node.name,
|
|
52
|
-
sku: node.sku,
|
|
53
|
-
offers: node.offers
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return out;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function suggestWabActionsFromProducts(products) {
|
|
61
|
-
const actions = [];
|
|
62
|
-
if (products.length) {
|
|
63
|
-
actions.push({
|
|
64
|
-
name: 'getProductFromSchema',
|
|
65
|
-
description: 'Structured products from schema.org JSON-LD',
|
|
66
|
-
source: 'schema.org'
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
if (products.some((p) => p.offers)) {
|
|
70
|
-
actions.push({
|
|
71
|
-
name: 'getOfferPrice',
|
|
72
|
-
description: 'Prices from schema.org Offer',
|
|
73
|
-
source: 'schema.org'
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
return actions;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
module.exports = {
|
|
80
|
-
extractJsonLdBlocks,
|
|
81
|
-
extractProductsFromHtml,
|
|
82
|
-
suggestWabActionsFromProducts
|
|
83
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Server-side / Node: extract schema.org Product nodes from HTML (JSON-LD blocks).
|
|
3
|
+
* No extra dependencies — regex-based script extraction (same semantics as browser WABSchema).
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* const { extractProductsFromHtml, suggestWabActionsFromProducts } = require('./schema-discovery');
|
|
7
|
+
* const products = extractProductsFromHtml(htmlString);
|
|
8
|
+
* const hints = suggestWabActionsFromProducts(products);
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
function extractJsonLdBlocks(html) {
|
|
12
|
+
if (!html || typeof html !== 'string') return [];
|
|
13
|
+
const re = /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
14
|
+
const blocks = [];
|
|
15
|
+
let m;
|
|
16
|
+
while ((m = re.exec(html)) !== null) {
|
|
17
|
+
blocks.push(m[1].trim());
|
|
18
|
+
}
|
|
19
|
+
return blocks;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function flattenGraph(data) {
|
|
23
|
+
if (Array.isArray(data)) return data;
|
|
24
|
+
if (data && Array.isArray(data['@graph'])) return data['@graph'];
|
|
25
|
+
return [data];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} html
|
|
30
|
+
* @returns {Array<{ type: string, name?: string, sku?: string, offers?: unknown }>}
|
|
31
|
+
*/
|
|
32
|
+
function extractProductsFromHtml(html) {
|
|
33
|
+
const out = [];
|
|
34
|
+
const blocks = extractJsonLdBlocks(html);
|
|
35
|
+
for (const text of blocks) {
|
|
36
|
+
let data;
|
|
37
|
+
try {
|
|
38
|
+
data = JSON.parse(text);
|
|
39
|
+
} catch {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const items = flattenGraph(data);
|
|
43
|
+
for (const node of items) {
|
|
44
|
+
if (!node || typeof node !== 'object') continue;
|
|
45
|
+
let types = node['@type'];
|
|
46
|
+
if (typeof types === 'string') types = [types];
|
|
47
|
+
if (!Array.isArray(types)) types = [];
|
|
48
|
+
if (!types.includes('Product')) continue;
|
|
49
|
+
out.push({
|
|
50
|
+
type: 'Product',
|
|
51
|
+
name: node.name,
|
|
52
|
+
sku: node.sku,
|
|
53
|
+
offers: node.offers
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function suggestWabActionsFromProducts(products) {
|
|
61
|
+
const actions = [];
|
|
62
|
+
if (products.length) {
|
|
63
|
+
actions.push({
|
|
64
|
+
name: 'getProductFromSchema',
|
|
65
|
+
description: 'Structured products from schema.org JSON-LD',
|
|
66
|
+
source: 'schema.org'
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (products.some((p) => p.offers)) {
|
|
70
|
+
actions.push({
|
|
71
|
+
name: 'getOfferPrice',
|
|
72
|
+
description: 'Prices from schema.org Offer',
|
|
73
|
+
source: 'schema.org'
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return actions;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
extractJsonLdBlocks,
|
|
81
|
+
extractProductsFromHtml,
|
|
82
|
+
suggestWabActionsFromProducts
|
|
83
|
+
};
|