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.
Files changed (256) hide show
  1. package/LICENSE +84 -72
  2. package/README.ar.md +1304 -1152
  3. package/README.md +298 -1635
  4. package/bin/agent-runner.js +474 -474
  5. package/bin/cli.js +237 -138
  6. package/bin/wab-init.js +223 -0
  7. package/bin/wab.js +80 -80
  8. package/examples/azure-dns-wab.js +83 -0
  9. package/examples/bidi-agent.js +119 -119
  10. package/examples/cloudflare-wab-dns.js +121 -0
  11. package/examples/cpanel-wab-dns.js +114 -0
  12. package/examples/cross-site-agent.js +91 -91
  13. package/examples/dns-discovery-agent.js +166 -0
  14. package/examples/gcp-dns-wab.js +76 -0
  15. package/examples/governance-agent.js +169 -0
  16. package/examples/mcp-agent.js +94 -94
  17. package/examples/next-app-router/README.md +44 -44
  18. package/examples/plesk-wab-dns.js +103 -0
  19. package/examples/puppeteer-agent.js +108 -108
  20. package/examples/route53-wab-dns.js +144 -0
  21. package/examples/saas-dashboard/README.md +55 -55
  22. package/examples/safe-mode-agent.js +96 -0
  23. package/examples/shopify-hydrogen/README.md +74 -74
  24. package/examples/vision-agent.js +171 -171
  25. package/examples/wab-sign.js +74 -0
  26. package/examples/wab-verify.js +60 -0
  27. package/examples/wordpress-elementor/README.md +77 -77
  28. package/package.json +19 -6
  29. package/public/.well-known/agent-tools.json +180 -180
  30. package/public/.well-known/ai-assets.json +59 -59
  31. package/public/.well-known/security.txt +8 -0
  32. package/public/.well-known/wab.json +28 -0
  33. package/public/activate.html +368 -0
  34. package/public/adoption-metrics.html +188 -0
  35. package/public/agent-workspace.html +349 -349
  36. package/public/ai.html +198 -198
  37. package/public/api.html +413 -412
  38. package/public/azure-dns-integration.html +289 -0
  39. package/public/browser.html +486 -486
  40. package/public/cloudflare-integration.html +380 -0
  41. package/public/commander-dashboard.html +243 -243
  42. package/public/cookies.html +210 -210
  43. package/public/cpanel-integration.html +398 -0
  44. package/public/css/agent-workspace.css +1713 -1713
  45. package/public/css/premium.css +317 -317
  46. package/public/css/styles.css +1263 -1235
  47. package/public/dashboard.html +707 -706
  48. package/public/dns.html +436 -0
  49. package/public/docs.html +588 -587
  50. package/public/feed.xml +89 -89
  51. package/public/gcp-dns-integration.html +318 -0
  52. package/public/growth.html +465 -463
  53. package/public/index.html +1266 -982
  54. package/public/integrations.html +556 -0
  55. package/public/js/activate.js +145 -0
  56. package/public/js/agent-workspace.js +1740 -1740
  57. package/public/js/auth-nav.js +65 -31
  58. package/public/js/auth-redirect.js +12 -12
  59. package/public/js/cookie-consent.js +56 -56
  60. package/public/js/dns.js +438 -0
  61. package/public/js/wab-demo-page.js +721 -721
  62. package/public/js/ws-client.js +74 -74
  63. package/public/llms-full.txt +360 -360
  64. package/public/llms.txt +125 -125
  65. package/public/login.html +85 -85
  66. package/public/mesh-dashboard.html +328 -328
  67. package/public/openapi.json +669 -580
  68. package/public/phone-shield.html +281 -0
  69. package/public/plesk-integration.html +375 -0
  70. package/public/premium-dashboard.html +2489 -2489
  71. package/public/premium.html +793 -793
  72. package/public/privacy.html +297 -297
  73. package/public/provider-onboarding.html +172 -0
  74. package/public/provider-sandbox.html +134 -0
  75. package/public/providers.html +359 -0
  76. package/public/register.html +105 -105
  77. package/public/registrar-integrations.html +141 -0
  78. package/public/robots.txt +99 -87
  79. package/public/route53-integration.html +531 -0
  80. package/public/script/wab-consent.d.ts +36 -36
  81. package/public/script/wab-consent.js +104 -104
  82. package/public/script/wab-schema.js +131 -131
  83. package/public/script/wab.d.ts +108 -108
  84. package/public/script/wab.min.js +580 -580
  85. package/public/security.txt +8 -0
  86. package/public/shieldqr.html +231 -0
  87. package/public/sitemap.xml +6 -0
  88. package/public/terms.html +256 -256
  89. package/public/wab-trust.html +200 -0
  90. package/public/wab-vs-protocols.html +210 -0
  91. package/public/whitepaper.html +449 -0
  92. package/script/ai-agent-bridge.js +1754 -1754
  93. package/sdk/README.md +99 -99
  94. package/sdk/agent-mesh.js +449 -449
  95. package/sdk/auto-discovery.js +288 -0
  96. package/sdk/commander.js +262 -262
  97. package/sdk/governance.js +262 -0
  98. package/sdk/index.d.ts +464 -464
  99. package/sdk/index.js +25 -1
  100. package/sdk/multi-agent.js +318 -318
  101. package/sdk/package.json +2 -2
  102. package/sdk/safe-mode.js +221 -0
  103. package/sdk/safety-shield.js +219 -0
  104. package/sdk/schema-discovery.js +83 -83
  105. package/server/adapters/index.js +520 -520
  106. package/server/config/plans.js +367 -367
  107. package/server/config/secrets.js +102 -102
  108. package/server/control-plane/index.js +301 -301
  109. package/server/data-plane/index.js +354 -354
  110. package/server/index.js +670 -427
  111. package/server/llm/index.js +404 -404
  112. package/server/middleware/adminAuth.js +35 -35
  113. package/server/middleware/auth.js +50 -50
  114. package/server/middleware/featureGate.js +88 -88
  115. package/server/middleware/rateLimits.js +100 -100
  116. package/server/middleware/sensitiveAction.js +157 -0
  117. package/server/migrations/001_add_analytics_indexes.sql +7 -7
  118. package/server/migrations/002_premium_features.sql +418 -418
  119. package/server/migrations/003_ads_integer_cents.sql +33 -33
  120. package/server/migrations/004_agent_os.sql +158 -158
  121. package/server/migrations/005_marketplace_metering.sql +126 -126
  122. package/server/migrations/007_governance.sql +106 -0
  123. package/server/migrations/008_plans.sql +144 -0
  124. package/server/migrations/009_shieldqr.sql +30 -0
  125. package/server/migrations/010_extended_trust.sql +33 -0
  126. package/server/models/adapters/index.js +33 -33
  127. package/server/models/adapters/mysql.js +183 -183
  128. package/server/models/adapters/postgresql.js +172 -172
  129. package/server/models/adapters/sqlite.js +7 -7
  130. package/server/models/db.js +740 -681
  131. package/server/observability/failure-analysis.js +337 -337
  132. package/server/observability/index.js +394 -394
  133. package/server/protocol/capabilities.js +223 -223
  134. package/server/protocol/index.js +243 -243
  135. package/server/protocol/schema.js +584 -584
  136. package/server/registry/certification.js +271 -271
  137. package/server/registry/index.js +326 -326
  138. package/server/routes/admin-plans.js +76 -0
  139. package/server/routes/admin-premium.js +673 -671
  140. package/server/routes/admin-shieldqr.js +90 -0
  141. package/server/routes/admin-trust-monitor.js +83 -0
  142. package/server/routes/admin.js +549 -261
  143. package/server/routes/ads.js +130 -130
  144. package/server/routes/agent-workspace.js +540 -540
  145. package/server/routes/api.js +150 -150
  146. package/server/routes/auth.js +71 -71
  147. package/server/routes/billing.js +57 -45
  148. package/server/routes/commander.js +316 -316
  149. package/server/routes/demo-showcase.js +332 -332
  150. package/server/routes/demo-store.js +154 -0
  151. package/server/routes/discovery.js +2348 -417
  152. package/server/routes/gateway.js +173 -157
  153. package/server/routes/governance.js +208 -0
  154. package/server/routes/license.js +251 -240
  155. package/server/routes/mesh.js +469 -469
  156. package/server/routes/noscript.js +543 -543
  157. package/server/routes/plans.js +33 -0
  158. package/server/routes/premium-v2.js +686 -686
  159. package/server/routes/premium.js +724 -724
  160. package/server/routes/providers.js +650 -0
  161. package/server/routes/runtime.js +2148 -2147
  162. package/server/routes/shieldqr.js +88 -0
  163. package/server/routes/sovereign.js +465 -385
  164. package/server/routes/universal.js +200 -185
  165. package/server/routes/wab-api.js +850 -501
  166. package/server/runtime/container-worker.js +111 -111
  167. package/server/runtime/container.js +448 -448
  168. package/server/runtime/distributed-worker.js +362 -362
  169. package/server/runtime/event-bus.js +210 -210
  170. package/server/runtime/index.js +253 -253
  171. package/server/runtime/queue.js +599 -599
  172. package/server/runtime/replay.js +666 -666
  173. package/server/runtime/sandbox.js +266 -266
  174. package/server/runtime/scheduler.js +534 -534
  175. package/server/runtime/session-engine.js +293 -293
  176. package/server/runtime/state-manager.js +188 -188
  177. package/server/security/cross-site-redactor.js +196 -0
  178. package/server/security/dry-run.js +180 -0
  179. package/server/security/human-gate-rate-limit.js +147 -0
  180. package/server/security/human-gate-transports.js +178 -0
  181. package/server/security/human-gate.js +281 -0
  182. package/server/security/index.js +368 -368
  183. package/server/security/intent-engine.js +245 -0
  184. package/server/security/reward-guard.js +171 -0
  185. package/server/security/rollback-store.js +239 -0
  186. package/server/security/token-scope.js +404 -0
  187. package/server/security/url-policy.js +139 -0
  188. package/server/services/agent-chat.js +506 -506
  189. package/server/services/agent-learning.js +601 -575
  190. package/server/services/agent-memory.js +625 -625
  191. package/server/services/agent-mesh.js +555 -539
  192. package/server/services/agent-symphony.js +717 -717
  193. package/server/services/agent-tasks.js +1807 -1807
  194. package/server/services/api-key-engine.js +292 -261
  195. package/server/services/cluster.js +894 -894
  196. package/server/services/commander.js +738 -738
  197. package/server/services/edge-compute.js +440 -440
  198. package/server/services/email.js +233 -204
  199. package/server/services/governance.js +466 -0
  200. package/server/services/hosted-runtime.js +205 -205
  201. package/server/services/lfd.js +635 -635
  202. package/server/services/local-ai.js +389 -389
  203. package/server/services/marketplace.js +270 -270
  204. package/server/services/metering.js +182 -182
  205. package/server/services/modules/affiliate-intelligence.js +93 -93
  206. package/server/services/modules/agent-firewall.js +90 -90
  207. package/server/services/modules/bounty.js +89 -89
  208. package/server/services/modules/collective-bargaining.js +92 -92
  209. package/server/services/modules/dark-pattern.js +66 -66
  210. package/server/services/modules/gov-intelligence.js +45 -45
  211. package/server/services/modules/neural.js +55 -55
  212. package/server/services/modules/notary.js +49 -49
  213. package/server/services/modules/price-time-machine.js +86 -86
  214. package/server/services/modules/protocol.js +104 -104
  215. package/server/services/negotiation.js +439 -439
  216. package/server/services/plans.js +214 -0
  217. package/server/services/plugins.js +771 -771
  218. package/server/services/premium.js +1 -1
  219. package/server/services/price-intelligence.js +566 -566
  220. package/server/services/price-shield.js +1137 -1137
  221. package/server/services/provider-clients.js +740 -0
  222. package/server/services/reputation.js +465 -465
  223. package/server/services/search-engine.js +357 -357
  224. package/server/services/security.js +513 -513
  225. package/server/services/self-healing.js +843 -843
  226. package/server/services/shieldqr.js +322 -0
  227. package/server/services/sovereign-shield.js +542 -0
  228. package/server/services/ssl-inspector.js +42 -0
  229. package/server/services/ssl-monitor.js +167 -0
  230. package/server/services/stripe.js +205 -192
  231. package/server/services/swarm.js +788 -788
  232. package/server/services/universal-scraper.js +662 -661
  233. package/server/services/verification.js +481 -481
  234. package/server/services/vision.js +1163 -1163
  235. package/server/services/wab-crypto.js +178 -0
  236. package/server/utils/cache.js +125 -125
  237. package/server/utils/migrate.js +81 -81
  238. package/server/utils/safe-fetch.js +228 -0
  239. package/server/utils/secureFields.js +50 -50
  240. package/server/ws.js +161 -161
  241. package/templates/artisan-marketplace.yaml +104 -104
  242. package/templates/book-price-scout.yaml +98 -98
  243. package/templates/electronics-price-tracker.yaml +108 -108
  244. package/templates/flight-deal-hunter.yaml +113 -113
  245. package/templates/freelancer-direct.yaml +116 -116
  246. package/templates/grocery-price-compare.yaml +93 -93
  247. package/templates/hotel-direct-booking.yaml +113 -113
  248. package/templates/local-services.yaml +98 -98
  249. package/templates/olive-oil-tunisia.yaml +88 -88
  250. package/templates/organic-farm-fresh.yaml +101 -101
  251. package/templates/restaurant-direct.yaml +97 -97
  252. package/public/score.html +0 -263
  253. package/server/migrations/006_growth_suite.sql +0 -138
  254. package/server/routes/growth.js +0 -962
  255. package/server/services/fairness-engine.js +0 -409
  256. package/server/services/fairness.js +0 -420
@@ -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 };
@@ -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
+ };