web-agent-bridge 3.4.0 → 3.9.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 (315) hide show
  1. package/LICENSE +84 -84
  2. package/README.ar.md +1565 -1304
  3. package/README.md +171 -298
  4. package/bin/agent-runner.js +474 -474
  5. package/bin/cli.js +237 -237
  6. package/bin/wab-init.js +244 -223
  7. package/bin/wab.js +80 -80
  8. package/examples/azure-dns-wab.js +83 -83
  9. package/examples/bidi-agent.js +119 -119
  10. package/examples/cloudflare-wab-dns.js +121 -121
  11. package/examples/cpanel-wab-dns.js +114 -114
  12. package/examples/cross-site-agent.js +91 -91
  13. package/examples/dns-discovery-agent.js +166 -166
  14. package/examples/gcp-dns-wab.js +76 -76
  15. package/examples/governance-agent.js +169 -169
  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 -103
  19. package/examples/puppeteer-agent.js +108 -108
  20. package/examples/route53-wab-dns.js +144 -144
  21. package/examples/saas-dashboard/README.md +55 -55
  22. package/examples/safe-mode-agent.js +96 -96
  23. package/examples/self-discovery.js +106 -0
  24. package/examples/shopify-hydrogen/README.md +74 -74
  25. package/examples/vision-agent.js +171 -171
  26. package/examples/wab-sign.js +74 -74
  27. package/examples/wab-verify.js +60 -60
  28. package/examples/wordpress-elementor/README.md +77 -77
  29. package/package.json +93 -93
  30. package/public/.well-known/agent-tools.json +180 -180
  31. package/public/.well-known/ai-assets.json +59 -59
  32. package/public/.well-known/security.txt +8 -8
  33. package/public/.well-known/wab.json +28 -28
  34. package/public/activate.html +448 -368
  35. package/public/adopt.html +236 -0
  36. package/public/adoption-metrics.html +188 -188
  37. package/public/agent-workspace.html +359 -349
  38. package/public/ai.html +198 -198
  39. package/public/api.html +397 -413
  40. package/public/atp.html +171 -0
  41. package/public/azure-dns-integration.html +289 -289
  42. package/public/browser.html +486 -486
  43. package/public/cloudflare-integration.html +380 -380
  44. package/public/commander-dashboard.html +243 -243
  45. package/public/cookies.html +210 -210
  46. package/public/cpanel-integration.html +398 -398
  47. package/public/css/agent-workspace.css +1713 -1713
  48. package/public/css/premium.css +317 -317
  49. package/public/css/styles.css +1401 -1263
  50. package/public/dashboard-shieldlink.html +295 -0
  51. package/public/dashboard.html +711 -707
  52. package/public/dns.html +436 -436
  53. package/public/docs.html +588 -588
  54. package/public/enterprise-mesh.ar.html +80 -0
  55. package/public/enterprise-mesh.html +81 -0
  56. package/public/feed.xml +89 -89
  57. package/public/gcp-dns-integration.html +318 -318
  58. package/public/governance.ar.html +70 -0
  59. package/public/governance.html +69 -0
  60. package/public/growth.html +465 -465
  61. package/public/index.html +1372 -1266
  62. package/public/integrations.html +556 -556
  63. package/public/js/activate.js +449 -145
  64. package/public/js/agent-workspace.js +1740 -1740
  65. package/public/js/auth-nav.js +117 -65
  66. package/public/js/auth-redirect.js +12 -12
  67. package/public/js/cookie-consent.js +56 -56
  68. package/public/js/dns.js +438 -438
  69. package/public/js/wab-demo-page.js +721 -721
  70. package/public/js/ws-client.js +74 -74
  71. package/public/l-preview.html +242 -0
  72. package/public/llms-full.txt +360 -360
  73. package/public/llms.txt +125 -125
  74. package/public/login.html +85 -85
  75. package/public/mesh-dashboard.html +328 -328
  76. package/public/milestones.html +346 -0
  77. package/public/one-click.html +779 -0
  78. package/public/openapi.json +669 -669
  79. package/public/partners.ar.html +145 -0
  80. package/public/partners.html +143 -0
  81. package/public/phone-shield.html +281 -281
  82. package/public/plesk-integration.html +375 -375
  83. package/public/premium-dashboard.html +2489 -2489
  84. package/public/premium.html +793 -793
  85. package/public/privacy.html +297 -297
  86. package/public/provider-onboarding.html +172 -172
  87. package/public/provider-sandbox.html +134 -134
  88. package/public/providers.html +359 -359
  89. package/public/refusals.html +172 -0
  90. package/public/register.html +105 -105
  91. package/public/registrar-integrations.html +141 -141
  92. package/public/ring4.html +292 -0
  93. package/public/robots.txt +99 -99
  94. package/public/route53-integration.html +531 -531
  95. package/public/score.html +263 -0
  96. package/public/script/wab-consent.d.ts +36 -36
  97. package/public/script/wab-consent.js +104 -104
  98. package/public/script/wab-schema.js +131 -131
  99. package/public/script/wab.d.ts +108 -108
  100. package/public/script/wab.min.js +580 -580
  101. package/public/security.txt +8 -8
  102. package/public/shieldlink.html +244 -0
  103. package/public/shieldqr.html +231 -231
  104. package/public/sitemap.xml +13 -1
  105. package/public/terms.html +256 -256
  106. package/public/trust-graph-api.ar.html +92 -0
  107. package/public/trust-graph-api.html +91 -0
  108. package/public/wab-features.html +560 -0
  109. package/public/wab-trust.html +200 -200
  110. package/public/wab-truth.html +375 -0
  111. package/public/wab-vs-protocols.html +210 -210
  112. package/public/whitepaper.html +449 -449
  113. package/script/ai-agent-bridge.js +1754 -1754
  114. package/sdk/README.md +99 -99
  115. package/sdk/agent-mesh.js +449 -449
  116. package/sdk/atp.js +103 -0
  117. package/sdk/auto-discovery.js +301 -288
  118. package/sdk/commander.js +262 -262
  119. package/sdk/governance.js +262 -262
  120. package/sdk/index.d.ts +464 -464
  121. package/sdk/index.js +653 -649
  122. package/sdk/multi-agent.js +318 -318
  123. package/sdk/safe-mode.js +221 -221
  124. package/sdk/safety-shield.js +219 -219
  125. package/sdk/schema-discovery.js +83 -83
  126. package/server/adapters/index.js +520 -520
  127. package/server/config/plans.js +412 -367
  128. package/server/config/secrets.js +102 -102
  129. package/server/control-plane/index.js +301 -301
  130. package/server/data-plane/index.js +354 -354
  131. package/server/index.js +793 -670
  132. package/server/llm/index.js +404 -404
  133. package/server/middleware/adminAuth.js +35 -35
  134. package/server/middleware/api-tier.js +170 -0
  135. package/server/middleware/auth.js +50 -50
  136. package/server/middleware/featureGate.js +88 -88
  137. package/server/middleware/rateLimits.js +100 -100
  138. package/server/middleware/sensitiveAction.js +157 -157
  139. package/server/middleware/wab-trust.js +141 -0
  140. package/server/migrations/001_add_analytics_indexes.sql +7 -7
  141. package/server/migrations/002_premium_features.sql +418 -418
  142. package/server/migrations/003_ads_integer_cents.sql +33 -33
  143. package/server/migrations/004_agent_os.sql +158 -158
  144. package/server/migrations/005_marketplace_metering.sql +126 -126
  145. package/server/migrations/006_growth_suite.sql +138 -0
  146. package/server/migrations/007_governance.sql +106 -106
  147. package/server/migrations/008_plans.sql +144 -144
  148. package/server/migrations/009_shieldqr.sql +30 -30
  149. package/server/migrations/010_extended_trust.sql +33 -33
  150. package/server/migrations/011_outreach.sql +47 -0
  151. package/server/migrations/012_shieldlink.sql +116 -0
  152. package/server/migrations/013_ct_monitor.sql +13 -0
  153. package/server/migrations/014_wab_advanced_features.sql +128 -0
  154. package/server/migrations/015_wab_truth_layer.sql +101 -0
  155. package/server/migrations/016_ring4_external_trust.sql +84 -0
  156. package/server/migrations/017_ring4_extensions.sql +69 -0
  157. package/server/migrations/018_commercial_foundations.sql +167 -0
  158. package/server/migrations/019_unify_tier_constraints.sql +133 -0
  159. package/server/migrations/020_agent_transaction_primitive.sql +119 -0
  160. package/server/models/adapters/index.js +33 -33
  161. package/server/models/adapters/mysql.js +183 -183
  162. package/server/models/adapters/postgresql.js +172 -172
  163. package/server/models/adapters/sqlite.js +7 -7
  164. package/server/models/db.js +740 -740
  165. package/server/observability/failure-analysis.js +337 -337
  166. package/server/observability/index.js +394 -394
  167. package/server/protocol/capabilities.js +223 -223
  168. package/server/protocol/index.js +243 -243
  169. package/server/protocol/schema.js +584 -584
  170. package/server/registry/certification.js +271 -271
  171. package/server/registry/index.js +326 -326
  172. package/server/routes/activate.js +478 -0
  173. package/server/routes/admin-outreach.js +239 -0
  174. package/server/routes/admin-plans.js +76 -76
  175. package/server/routes/admin-premium.js +674 -673
  176. package/server/routes/admin-shieldlink.js +137 -0
  177. package/server/routes/admin-shieldqr.js +90 -90
  178. package/server/routes/admin-trust-monitor.js +139 -83
  179. package/server/routes/admin.js +550 -549
  180. package/server/routes/adopt.js +61 -0
  181. package/server/routes/ads.js +130 -130
  182. package/server/routes/agent-workspace.js +540 -540
  183. package/server/routes/api-keys.js +127 -0
  184. package/server/routes/api.js +150 -150
  185. package/server/routes/auth.js +71 -71
  186. package/server/routes/billing.js +57 -57
  187. package/server/routes/commander.js +316 -316
  188. package/server/routes/customer-shieldlink.js +133 -0
  189. package/server/routes/demo-showcase.js +332 -332
  190. package/server/routes/demo-store.js +154 -154
  191. package/server/routes/diagnose.js +373 -0
  192. package/server/routes/discovery.js +2348 -2348
  193. package/server/routes/enterprise-mesh.js +170 -0
  194. package/server/routes/gateway.js +173 -173
  195. package/server/routes/governance-saas.js +203 -0
  196. package/server/routes/governance.js +208 -208
  197. package/server/routes/growth.js +1048 -0
  198. package/server/routes/intent.js +328 -0
  199. package/server/routes/license.js +251 -251
  200. package/server/routes/mesh.js +469 -469
  201. package/server/routes/noscript.js +543 -543
  202. package/server/routes/partners.js +201 -0
  203. package/server/routes/plans.js +33 -33
  204. package/server/routes/premium-v2.js +686 -686
  205. package/server/routes/premium.js +724 -724
  206. package/server/routes/providers.js +650 -650
  207. package/server/routes/reputation.js +411 -0
  208. package/server/routes/ring4.js +885 -0
  209. package/server/routes/runtime.js +2148 -2148
  210. package/server/routes/shieldlink.js +70 -0
  211. package/server/routes/shieldqr.js +88 -88
  212. package/server/routes/sovereign.js +465 -465
  213. package/server/routes/transactions.js +233 -0
  214. package/server/routes/truth-layer.js +670 -0
  215. package/server/routes/universal.js +200 -200
  216. package/server/routes/unsubscribe.js +51 -0
  217. package/server/routes/wab-api.js +850 -850
  218. package/server/routes/wab-cache.js +282 -0
  219. package/server/runtime/container-worker.js +111 -111
  220. package/server/runtime/container.js +448 -448
  221. package/server/runtime/distributed-worker.js +362 -362
  222. package/server/runtime/event-bus.js +210 -210
  223. package/server/runtime/index.js +253 -253
  224. package/server/runtime/queue.js +599 -599
  225. package/server/runtime/replay.js +666 -666
  226. package/server/runtime/sandbox.js +266 -266
  227. package/server/runtime/scheduler.js +534 -534
  228. package/server/runtime/session-engine.js +293 -293
  229. package/server/runtime/state-manager.js +188 -188
  230. package/server/secrets/wab-signing-key.pem +3 -0
  231. package/server/secrets/wab-signing-pub.pem +3 -0
  232. package/server/security/cross-site-redactor.js +196 -196
  233. package/server/security/dry-run.js +180 -180
  234. package/server/security/human-gate-rate-limit.js +147 -147
  235. package/server/security/human-gate-transports.js +178 -178
  236. package/server/security/human-gate.js +281 -281
  237. package/server/security/index.js +368 -368
  238. package/server/security/intent-engine.js +245 -245
  239. package/server/security/reward-guard.js +171 -171
  240. package/server/security/rollback-store.js +239 -239
  241. package/server/security/token-scope.js +404 -404
  242. package/server/security/url-policy.js +139 -139
  243. package/server/services/adoption-agent.js +182 -0
  244. package/server/services/agent-chat.js +506 -506
  245. package/server/services/agent-learning.js +601 -601
  246. package/server/services/agent-memory.js +625 -625
  247. package/server/services/agent-mesh.js +555 -555
  248. package/server/services/agent-symphony.js +717 -717
  249. package/server/services/agent-tasks.js +1807 -1807
  250. package/server/services/api-key-engine.js +292 -292
  251. package/server/services/cluster.js +894 -894
  252. package/server/services/commander.js +738 -738
  253. package/server/services/edge-compute.js +440 -440
  254. package/server/services/email.js +233 -233
  255. package/server/services/fairness-engine.js +409 -0
  256. package/server/services/fairness.js +420 -0
  257. package/server/services/governance.js +466 -466
  258. package/server/services/hosted-runtime.js +205 -205
  259. package/server/services/lfd.js +635 -635
  260. package/server/services/local-ai.js +389 -389
  261. package/server/services/marketplace.js +270 -270
  262. package/server/services/metering.js +182 -182
  263. package/server/services/modules/affiliate-intelligence.js +93 -93
  264. package/server/services/modules/agent-firewall.js +90 -90
  265. package/server/services/modules/bounty.js +89 -89
  266. package/server/services/modules/collective-bargaining.js +92 -92
  267. package/server/services/modules/dark-pattern.js +66 -66
  268. package/server/services/modules/gov-intelligence.js +45 -45
  269. package/server/services/modules/neural.js +55 -55
  270. package/server/services/modules/notary.js +49 -49
  271. package/server/services/modules/price-time-machine.js +86 -86
  272. package/server/services/modules/protocol.js +104 -104
  273. package/server/services/negotiation.js +439 -439
  274. package/server/services/outreach-agent.js +312 -0
  275. package/server/services/plans.js +214 -214
  276. package/server/services/plugins.js +771 -771
  277. package/server/services/price-intelligence.js +566 -566
  278. package/server/services/price-shield.js +1137 -1137
  279. package/server/services/provider-clients.js +740 -740
  280. package/server/services/reputation.js +465 -465
  281. package/server/services/search-engine.js +357 -357
  282. package/server/services/security.js +513 -513
  283. package/server/services/self-healing.js +843 -843
  284. package/server/services/shieldlink.js +492 -0
  285. package/server/services/shieldqr.js +322 -322
  286. package/server/services/sovereign-shield.js +542 -542
  287. package/server/services/ssl-ct-monitor.js +224 -0
  288. package/server/services/ssl-inspector.js +42 -42
  289. package/server/services/ssl-monitor.js +167 -167
  290. package/server/services/stripe.js +206 -205
  291. package/server/services/swarm.js +788 -788
  292. package/server/services/transactions.js +525 -0
  293. package/server/services/universal-scraper.js +662 -662
  294. package/server/services/verification.js +481 -481
  295. package/server/services/vision.js +1163 -1163
  296. package/server/services/wab-crypto.js +178 -178
  297. package/server/utils/cache.js +125 -125
  298. package/server/utils/migrate.js +81 -81
  299. package/server/utils/safe-fetch.js +228 -228
  300. package/server/utils/secureFields.js +50 -50
  301. package/server/ws.js +161 -161
  302. package/templates/artisan-marketplace.yaml +104 -104
  303. package/templates/book-price-scout.yaml +98 -98
  304. package/templates/electronics-price-tracker.yaml +108 -108
  305. package/templates/flight-deal-hunter.yaml +113 -113
  306. package/templates/freelancer-direct.yaml +116 -116
  307. package/templates/grocery-price-compare.yaml +93 -93
  308. package/templates/hotel-direct-booking.yaml +113 -113
  309. package/templates/local-services.yaml +98 -98
  310. package/templates/olive-oil-tunisia.yaml +88 -88
  311. package/templates/organic-farm-fresh.yaml +101 -101
  312. package/templates/restaurant-direct.yaml +97 -97
  313. package/templates/ring4/banking-sovereign.yaml +55 -0
  314. package/templates/ring4/ecommerce-sovereign.yaml +58 -0
  315. package/templates/ring4/healthcare-sovereign.yaml +60 -0
@@ -1,1754 +1,1754 @@
1
- /**
2
- * Web Agent Bridge v2.0.0
3
- * Open-source middleware for AI agent ↔ website interaction
4
- * https://github.com/web-agent-bridge
5
- * License: MIT
6
- */
7
- (function (global) {
8
- 'use strict';
9
-
10
- const VERSION = '2.2.0';
11
- const LICENSING_SERVER = 'https://api.webagentbridge.com';
12
-
13
- // ─── Default Configuration ────────────────────────────────────────────
14
- const DEFAULT_CONFIG = {
15
- agentPermissions: {
16
- readContent: true,
17
- click: true,
18
- fillForms: false,
19
- scroll: true,
20
- navigate: false,
21
- apiAccess: false,
22
- automatedLogin: false,
23
- extractData: false
24
- },
25
- restrictions: {
26
- allowedSelectors: [],
27
- blockedSelectors: ['.private', '[data-private]', '[data-no-agent]'],
28
- requireLoginForActions: [],
29
- rateLimit: { maxCallsPerMinute: 60 }
30
- },
31
- logging: {
32
- enabled: false,
33
- level: 'basic'
34
- },
35
- subscriptionTier: 'free',
36
- licenseKey: null,
37
- /** Public site id from dashboard (preferred). Used with configEndpoint for token exchange — license key stays off the page. */
38
- siteId: null,
39
- /** Base URL for /api/license/* (verify, track). Default: current origin, else LICENSING_SERVER. */
40
- apiBaseUrl: null,
41
- features: {
42
- advancedAnalytics: false,
43
- realTimeUpdates: false,
44
- customActions: false,
45
- webhooks: false,
46
- /** Send execute events to POST /api/license/track (populates admin analytics). */
47
- reportUsage: true
48
- }
49
- };
50
-
51
- // ─── Rate Limiter ─────────────────────────────────────────────────────
52
- class RateLimiter {
53
- constructor(maxPerMinute) {
54
- this.maxPerMinute = maxPerMinute;
55
- this.calls = [];
56
- }
57
-
58
- check() {
59
- const now = Date.now();
60
- this.calls = this.calls.filter(t => now - t < 60000);
61
- if (this.calls.length >= this.maxPerMinute) {
62
- return false;
63
- }
64
- this.calls.push(now);
65
- return true;
66
- }
67
-
68
- get remaining() {
69
- const now = Date.now();
70
- this.calls = this.calls.filter(t => now - t < 60000);
71
- return Math.max(0, this.maxPerMinute - this.calls.length);
72
- }
73
- }
74
-
75
- // ─── Logger ───────────────────────────────────────────────────────────
76
- class BridgeLogger {
77
- constructor(config) {
78
- this.enabled = config.enabled;
79
- this.level = config.level;
80
- this.logs = [];
81
- }
82
-
83
- log(action, details, level = 'basic') {
84
- if (!this.enabled) return;
85
- if (level === 'detailed' && this.level !== 'detailed') return;
86
-
87
- const entry = {
88
- timestamp: new Date().toISOString(),
89
- action,
90
- details,
91
- level
92
- };
93
- this.logs.push(entry);
94
-
95
- if (this.logs.length > 1000) {
96
- this.logs = this.logs.slice(-500);
97
- }
98
- }
99
-
100
- getLogs(filter) {
101
- if (!filter) return [...this.logs];
102
- return this.logs.filter(l => l.action === filter);
103
- }
104
-
105
- clear() {
106
- this.logs = [];
107
- }
108
- }
109
-
110
- // ─── Security Sandbox ──────────────────────────────────────────────────
111
- // Isolates the bridge with origin validation, session tokens, and audit log
112
- class SecuritySandbox {
113
- constructor(config) {
114
- this._sessionToken = this._generateToken();
115
- this._allowedOrigins = config.security?.allowedOrigins || [location.origin];
116
- this._auditLog = [];
117
- this._maxAuditEntries = 500;
118
- this._commandCounter = 0;
119
- this._blockedCommands = new Set();
120
- this._escalationAttempts = 0;
121
- this._maxEscalationAttempts = 5;
122
- this._locked = false;
123
- }
124
-
125
- _generateToken() {
126
- const arr = new Uint8Array(32);
127
- if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
128
- crypto.getRandomValues(arr);
129
- } else {
130
- for (let i = 0; i < 32; i++) arr[i] = Math.floor(Math.random() * 256);
131
- }
132
- return Array.from(arr, b => b.toString(16).padStart(2, '0')).join('');
133
- }
134
-
135
- get sessionToken() { return this._sessionToken; }
136
-
137
- validateOrigin(origin) {
138
- if (!origin) return this._allowedOrigins.includes(location.origin);
139
- return this._allowedOrigins.includes(origin);
140
- }
141
-
142
- audit(action, details, status = 'ok') {
143
- const entry = {
144
- id: ++this._commandCounter,
145
- timestamp: Date.now(),
146
- action,
147
- status,
148
- fingerprint: details?.agentId || 'anonymous'
149
- };
150
- this._auditLog.push(entry);
151
- if (this._auditLog.length > this._maxAuditEntries) {
152
- this._auditLog = this._auditLog.slice(-250);
153
- }
154
- return entry;
155
- }
156
-
157
- getAuditLog(limit = 50) {
158
- return this._auditLog.slice(-limit);
159
- }
160
-
161
- checkEscalation(requestedTier, currentTier) {
162
- const tierLevel = { free: 0, starter: 1, pro: 2, enterprise: 3 };
163
- if ((tierLevel[requestedTier] || 0) > (tierLevel[currentTier] || 0)) {
164
- this._escalationAttempts++;
165
- this.audit('escalation_attempt', { requestedTier, currentTier }, 'blocked');
166
- if (this._escalationAttempts >= this._maxEscalationAttempts) {
167
- this._locked = true;
168
- this.audit('bridge_locked', { reason: 'Too many escalation attempts' }, 'critical');
169
- }
170
- return false;
171
- }
172
- return true;
173
- }
174
-
175
- get isLocked() { return this._locked; }
176
-
177
- validateCommand(command) {
178
- if (this._locked) return { valid: false, error: 'Bridge is locked due to security violations' };
179
- if (typeof command !== 'object' || !command) return { valid: false, error: 'Invalid command format' };
180
- if (typeof command.method !== 'string') return { valid: false, error: 'Command method must be a string' };
181
- if (command.method.length > 200) return { valid: false, error: 'Command method too long' };
182
- if (this._blockedCommands.has(command.method)) return { valid: false, error: 'Command is blocked' };
183
- return { valid: true };
184
- }
185
- }
186
-
187
- // ─── Self-Healing Selectors ───────────────────────────────────────────
188
- // Resilient element resolution for SPAs with dynamic DOM
189
- class SelfHealingSelector {
190
- constructor() {
191
- this._fingerprints = new Map(); // name → element fingerprint
192
- this._healingStats = { healed: 0, failed: 0 };
193
- }
194
-
195
- fingerprint(el) {
196
- if (!el) return null;
197
- return {
198
- tag: el.tagName.toLowerCase(),
199
- id: el.id || null,
200
- classes: Array.from(el.classList),
201
- text: (el.textContent || '').trim().slice(0, 100),
202
- ariaLabel: el.getAttribute('aria-label') || null,
203
- name: el.getAttribute('name') || null,
204
- type: el.getAttribute('type') || null,
205
- role: el.getAttribute('role') || null,
206
- dataTestId: el.getAttribute('data-testid') || null,
207
- dataWabId: el.getAttribute('data-wab-id') || null,
208
- href: el.tagName === 'A' ? el.getAttribute('href') : null,
209
- placeholder: el.getAttribute('placeholder') || null,
210
- parentTag: el.parentElement ? el.parentElement.tagName.toLowerCase() : null,
211
- index: el.parentElement ? Array.from(el.parentElement.children).indexOf(el) : -1
212
- };
213
- }
214
-
215
- store(actionName, selector) {
216
- const el = safeQuerySelector(selector);
217
- if (el) {
218
- this._fingerprints.set(actionName, {
219
- selector,
220
- fp: this.fingerprint(el),
221
- storedAt: Date.now()
222
- });
223
- }
224
- }
225
-
226
- resolve(actionName, originalSelector) {
227
- // Try original selector first
228
- const el = safeQuerySelector(originalSelector);
229
- if (el) return { element: el, selector: originalSelector, healed: false };
230
-
231
- // Try self-healing
232
- const stored = this._fingerprints.get(actionName);
233
- if (!stored || !stored.fp) {
234
- this._healingStats.failed++;
235
- return null;
236
- }
237
-
238
- const healed = this._heal(stored.fp);
239
- if (healed) {
240
- this._healingStats.healed++;
241
- return healed;
242
- }
243
-
244
- this._healingStats.failed++;
245
- return null;
246
- }
247
-
248
- _heal(fp) {
249
- // Strategy 1: data-wab-id (most stable)
250
- if (fp.dataWabId) {
251
- const el = safeQuerySelector(`[data-wab-id="${fp.dataWabId}"]`);
252
- if (el) return { element: el, selector: `[data-wab-id="${fp.dataWabId}"]`, healed: true, strategy: 'data-wab-id' };
253
- }
254
-
255
- // Strategy 2: data-testid
256
- if (fp.dataTestId) {
257
- const el = safeQuerySelector(`[data-testid="${fp.dataTestId}"]`);
258
- if (el) return { element: el, selector: `[data-testid="${fp.dataTestId}"]`, healed: true, strategy: 'data-testid' };
259
- }
260
-
261
- // Strategy 3: id (may have changed)
262
- if (fp.id) {
263
- const el = safeQuerySelector(`#${CSS.escape(fp.id)}`);
264
- if (el) return { element: el, selector: `#${CSS.escape(fp.id)}`, healed: true, strategy: 'id' };
265
- }
266
-
267
- // Strategy 4: aria-label (semantic, usually stable)
268
- if (fp.ariaLabel) {
269
- const sel = `${fp.tag}[aria-label="${CSS.escape(fp.ariaLabel)}"]`;
270
- const el = safeQuerySelector(sel);
271
- if (el) return { element: el, selector: sel, healed: true, strategy: 'aria-label' };
272
- }
273
-
274
- // Strategy 5: name attribute
275
- if (fp.name) {
276
- const sel = `${fp.tag}[name="${CSS.escape(fp.name)}"]`;
277
- const el = safeQuerySelector(sel);
278
- if (el) return { element: el, selector: sel, healed: true, strategy: 'name' };
279
- }
280
-
281
- // Strategy 6: text content matching (fuzzy)
282
- if (fp.text && fp.text.length > 0) {
283
- const candidates = safeQuerySelectorAll(fp.tag);
284
- const target = fp.text.toLowerCase();
285
- let bestMatch = null;
286
- let bestScore = 0;
287
-
288
- for (const candidate of candidates) {
289
- const candidateText = (candidate.textContent || '').trim().toLowerCase();
290
- if (!candidateText) continue;
291
-
292
- const score = this._textSimilarity(target, candidateText);
293
- if (score > 0.7 && score > bestScore) {
294
- bestScore = score;
295
- bestMatch = candidate;
296
- }
297
- }
298
-
299
- if (bestMatch) {
300
- const healedSel = this._buildSelectorFor(bestMatch);
301
- return { element: bestMatch, selector: healedSel, healed: true, strategy: 'text-fuzzy', confidence: bestScore };
302
- }
303
- }
304
-
305
- // Strategy 7: role + position heuristic
306
- if (fp.role && fp.parentTag) {
307
- const candidates = safeQuerySelectorAll(`${fp.parentTag} > ${fp.tag}[role="${fp.role}"]`);
308
- if (candidates.length > 0 && fp.index >= 0 && fp.index < candidates.length) {
309
- const el = candidates[fp.index];
310
- const sel = this._buildSelectorFor(el);
311
- return { element: el, selector: sel, healed: true, strategy: 'role-position' };
312
- }
313
- }
314
-
315
- return null;
316
- }
317
-
318
- _textSimilarity(a, b) {
319
- if (a === b) return 1;
320
- const longer = a.length > b.length ? a : b;
321
- const shorter = a.length > b.length ? b : a;
322
- if (longer.length === 0) return 1;
323
- if (longer.includes(shorter) || shorter.includes(longer)) {
324
- return shorter.length / longer.length;
325
- }
326
- // Simple bigram similarity
327
- const bigramsA = new Set();
328
- for (let i = 0; i < a.length - 1; i++) bigramsA.add(a.slice(i, i + 2));
329
- let matches = 0;
330
- for (let i = 0; i < b.length - 1; i++) {
331
- if (bigramsA.has(b.slice(i, i + 2))) matches++;
332
- }
333
- const total = Math.max(bigramsA.size, b.length - 1);
334
- return total > 0 ? matches / total : 0;
335
- }
336
-
337
- _buildSelectorFor(el) {
338
- if (el.id) return `#${CSS.escape(el.id)}`;
339
- if (el.dataset.wabId) return `[data-wab-id="${el.dataset.wabId}"]`;
340
- const tag = el.tagName.toLowerCase();
341
- const attrs = ['data-testid', 'aria-label', 'name'];
342
- for (const attr of attrs) {
343
- const val = el.getAttribute(attr);
344
- if (val && document.querySelectorAll(`${tag}[${attr}="${CSS.escape(val)}"]`).length === 1) {
345
- return `${tag}[${attr}="${CSS.escape(val)}"]`;
346
- }
347
- }
348
- return null;
349
- }
350
-
351
- getStats() {
352
- return { ...this._healingStats, tracked: this._fingerprints.size };
353
- }
354
- }
355
-
356
- // ─── Stealth / Human-like Interaction ─────────────────────────────────
357
- // ⚠️ ETHICAL USE POLICY:
358
- // Stealth mode simulates human-like interaction patterns for LEGITIMATE uses:
359
- // - Accessibility testing - QA automation on YOUR OWN sites
360
- // - UX research with consent - Authorized penetration testing
361
- // DO NOT use to bypass anti-bot protections on sites you do not own or control.
362
- // Misuse violates the MIT license terms and is the user's legal responsibility.
363
- const Stealth = {
364
- _enabled: false,
365
- _consentGiven: false,
366
-
367
- enable(consent) {
368
- if (consent !== true) {
369
- console.warn('[WAB] Stealth mode requires explicit consent: stealth.enable(true)');
370
- return;
371
- }
372
- this._consentGiven = true;
373
- this._enabled = true;
374
- },
375
- disable() { this._enabled = false; },
376
- get isEnabled() { return this._enabled; },
377
-
378
- // Random delay between min and max ms
379
- delay(min = 50, max = 300) {
380
- if (!this._enabled) return Promise.resolve();
381
- const duration = min + Math.floor(Math.random() * (max - min));
382
- return new Promise(resolve => setTimeout(resolve, duration));
383
- },
384
-
385
- // Simulate a full mouse event chain: mouseover → mouseenter → mousemove → mousedown → mouseup → click
386
- async simulateClick(el) {
387
- if (!el) return;
388
- const rect = el.getBoundingClientRect();
389
- const x = rect.left + rect.width * (0.3 + Math.random() * 0.4);
390
- const y = rect.top + rect.height * (0.3 + Math.random() * 0.4);
391
- const eventOpts = { bubbles: true, cancelable: true, clientX: x, clientY: y, view: window };
392
-
393
- el.dispatchEvent(new MouseEvent('mouseover', eventOpts));
394
- el.dispatchEvent(new MouseEvent('mouseenter', { ...eventOpts, bubbles: false }));
395
- await this.delay(30, 80);
396
- el.dispatchEvent(new MouseEvent('mousemove', eventOpts));
397
- await this.delay(40, 120);
398
- el.dispatchEvent(new MouseEvent('mousedown', eventOpts));
399
- await this.delay(50, 150);
400
- el.dispatchEvent(new MouseEvent('mouseup', eventOpts));
401
- el.dispatchEvent(new MouseEvent('click', eventOpts));
402
- },
403
-
404
- // Simulate human-like typing with variable delays
405
- async simulateTyping(el, text) {
406
- if (!el) return;
407
- el.focus();
408
- el.dispatchEvent(new Event('focus', { bubbles: true }));
409
- el.value = '';
410
-
411
- for (let i = 0; i < text.length; i++) {
412
- const char = text[i];
413
- el.dispatchEvent(new KeyboardEvent('keydown', { key: char, bubbles: true }));
414
- el.value += char;
415
- el.dispatchEvent(new Event('input', { bubbles: true }));
416
- el.dispatchEvent(new KeyboardEvent('keyup', { key: char, bubbles: true }));
417
- await this.delay(30, 120); // Human typing speed: 30-120ms per key
418
- }
419
-
420
- el.dispatchEvent(new Event('change', { bubbles: true }));
421
- },
422
-
423
- // Simulate natural scrolling (variable speed, easing)
424
- async simulateScroll(el, direction = 'down') {
425
- const target = el || document.documentElement;
426
- const distance = 300 + Math.floor(Math.random() * 400);
427
- const steps = 5 + Math.floor(Math.random() * 5);
428
- const stepSize = distance / steps;
429
-
430
- for (let i = 0; i < steps; i++) {
431
- const delta = direction === 'down' ? stepSize : -stepSize;
432
- if (el && el !== document.documentElement) {
433
- el.scrollTop += delta;
434
- } else {
435
- window.scrollBy(0, delta);
436
- }
437
- await this.delay(15, 50);
438
- }
439
- }
440
- };
441
-
442
- // ─── Element Utilities ────────────────────────────────────────────────
443
- function isElementAllowed(selector, config) {
444
- const { allowedSelectors, blockedSelectors } = config.restrictions;
445
-
446
- if (blockedSelectors.length > 0) {
447
- const el = document.querySelector(selector);
448
- if (el) {
449
- for (const blocked of blockedSelectors) {
450
- if (el.matches(blocked) || el.closest(blocked)) return false;
451
- }
452
- }
453
- }
454
-
455
- if (allowedSelectors.length > 0) {
456
- const el = document.querySelector(selector);
457
- if (el) {
458
- for (const allowed of allowedSelectors) {
459
- if (el.matches(allowed) || el.closest(allowed)) return true;
460
- }
461
- return false;
462
- }
463
- }
464
-
465
- return true;
466
- }
467
-
468
- function safeQuerySelector(selector) {
469
- try {
470
- return document.querySelector(selector);
471
- } catch (e) {
472
- return null;
473
- }
474
- }
475
-
476
- function safeQuerySelectorAll(selector) {
477
- try {
478
- return Array.from(document.querySelectorAll(selector));
479
- } catch (e) {
480
- return [];
481
- }
482
- }
483
-
484
- // ─── Action Registry ──────────────────────────────────────────────────
485
- class ActionRegistry {
486
- constructor() {
487
- this.actions = new Map();
488
- }
489
-
490
- register(action) {
491
- if (!action.name || !action.description) {
492
- throw new Error('Action must have a name and description');
493
- }
494
- this.actions.set(action.name, {
495
- name: action.name,
496
- description: action.description,
497
- trigger: action.trigger || 'click',
498
- selector: action.selector || null,
499
- fields: action.fields || null,
500
- submitSelector: action.submitSelector || null,
501
- endpoint: action.endpoint || null,
502
- method: action.method || 'GET',
503
- requiresAuth: action.requiresAuth || false,
504
- category: action.category || 'general',
505
- params: action.params || null,
506
- handler: action.handler || null,
507
- metadata: action.metadata || {}
508
- });
509
- }
510
-
511
- unregister(name) {
512
- return this.actions.delete(name);
513
- }
514
-
515
- get(name) {
516
- return this.actions.get(name) || null;
517
- }
518
-
519
- list() {
520
- return Array.from(this.actions.values()).map(a => ({
521
- name: a.name,
522
- description: a.description,
523
- trigger: a.trigger,
524
- category: a.category,
525
- requiresAuth: a.requiresAuth,
526
- params: a.params,
527
- fields: a.fields ? a.fields.map(f => ({ name: f.name, type: f.type, required: f.required !== false })) : null
528
- }));
529
- }
530
-
531
- getByCategory(category) {
532
- return this.list().filter(a => a.category === category);
533
- }
534
-
535
- toJSON() {
536
- return this.list();
537
- }
538
- }
539
-
540
- // ─── Event Emitter ────────────────────────────────────────────────────
541
- class BridgeEventEmitter {
542
- constructor() {
543
- this.listeners = {};
544
- }
545
-
546
- on(event, callback) {
547
- if (!this.listeners[event]) this.listeners[event] = [];
548
- this.listeners[event].push(callback);
549
- return () => this.off(event, callback);
550
- }
551
-
552
- off(event, callback) {
553
- if (!this.listeners[event]) return;
554
- this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
555
- }
556
-
557
- emit(event, data) {
558
- if (!this.listeners[event]) return;
559
- this.listeners[event].forEach(cb => {
560
- try { cb(data); } catch (e) { console.error(`[WAB] Event handler error for "${event}":`, e); }
561
- });
562
- }
563
- }
564
-
565
- // ─── Main Bridge Class ────────────────────────────────────────────────
566
- class WebAgentBridge {
567
- constructor(userConfig) {
568
- this.config = this._mergeConfig(DEFAULT_CONFIG, userConfig || {});
569
- this.registry = new ActionRegistry();
570
- this.rateLimiter = new RateLimiter(this.config.restrictions.rateLimit.maxCallsPerMinute);
571
- this.logger = new BridgeLogger(this.config.logging);
572
- this.events = new BridgeEventEmitter();
573
- this.security = new SecuritySandbox(this.config);
574
- this.healer = new SelfHealingSelector();
575
- this.stealth = Stealth;
576
- this.authenticated = false;
577
- this.agentInfo = null;
578
- this._licenseVerified = null;
579
- this._ready = false;
580
- this._readyCallbacks = [];
581
- this._mutationObserver = null;
582
-
583
- // Enable stealth mode if configured (requires consent: true)
584
- if (this.config.stealth?.enabled && this.config.stealth?.consent === true) {
585
- this.stealth.enable(true);
586
- }
587
-
588
- this._init();
589
- }
590
-
591
- // ── Initialization ──────────────────────────────────────────────────
592
- async _init() {
593
- if (this.config.configEndpoint && (this.config.siteId || this.config._licenseKey || this.config.licenseKey)) {
594
- await this._secureLicenseExchange();
595
- } else if (this.config.licenseKey) {
596
- await this._verifyLicense();
597
- } else {
598
- this._licenseVerified = { tier: 'free', valid: true };
599
- }
600
-
601
- this._autoDiscoverActions();
602
- this._storeFingerprints();
603
- this._setupSPAObserver();
604
- this._ready = true;
605
- this._readyCallbacks.forEach(cb => cb());
606
- this._readyCallbacks = [];
607
- this.events.emit('ready', { version: VERSION, tier: this.getEffectiveTier() });
608
- this.logger.log('init', { version: VERSION, tier: this.getEffectiveTier(), security: 'sandbox-active' });
609
- }
610
-
611
- // Secure license exchange: POST license key to server, get session token back
612
- // License key is transmitted once via POST (not visible in page source)
613
- async _secureLicenseExchange() {
614
- try {
615
- const endpoint = this.config.configEndpoint;
616
- const body = this.config.siteId
617
- ? { siteId: this.config.siteId }
618
- : { licenseKey: this.config._licenseKey || this.config.licenseKey };
619
- const res = await fetch(endpoint, {
620
- method: 'POST',
621
- headers: { 'Content-Type': 'application/json' },
622
- body: JSON.stringify(body)
623
- });
624
- if (res.ok) {
625
- const data = await res.json();
626
- this._licenseVerified = { tier: data.tier, valid: true, sessionToken: data.sessionToken };
627
- delete this.config._licenseKey;
628
- if (data.expiresIn) {
629
- if (this._tokenRefreshTimer) clearTimeout(this._tokenRefreshTimer);
630
- this._tokenRefreshTimer = setTimeout(() => {
631
- this._secureLicenseExchange();
632
- }, Math.floor(data.expiresIn * 800));
633
- }
634
- } else {
635
- this._licenseVerified = { tier: 'free', valid: false, error: 'Token exchange failed' };
636
- }
637
- } catch (e) {
638
- this._licenseVerified = { tier: this.config.subscriptionTier || 'free', valid: false, error: 'Offline' };
639
- }
640
- }
641
-
642
- // Store fingerprints for all discovered actions (self-healing)
643
- _storeFingerprints() {
644
- for (const action of this.registry.actions.values()) {
645
- if (action.selector) {
646
- this.healer.store(action.name, action.selector);
647
- }
648
- }
649
- }
650
-
651
- // Watch for SPA DOM changes and re-discover actions
652
- _setupSPAObserver() {
653
- if (this._mutationObserver) this._mutationObserver.disconnect();
654
-
655
- let debounceTimer = null;
656
- this._mutationObserver = new MutationObserver(() => {
657
- if (debounceTimer) clearTimeout(debounceTimer);
658
- debounceTimer = setTimeout(() => {
659
- this._autoDiscoverActions();
660
- this._storeFingerprints();
661
- this.events.emit('dom:changed', { actionsCount: this.registry.actions.size });
662
- }, 500);
663
- });
664
-
665
- this._mutationObserver.observe(document.body, {
666
- childList: true,
667
- subtree: true,
668
- attributes: false
669
- });
670
- }
671
-
672
- onReady(callback) {
673
- if (this._ready) {
674
- callback();
675
- } else {
676
- this._readyCallbacks.push(callback);
677
- }
678
- }
679
-
680
- _mergeConfig(defaults, overrides) {
681
- const result = {};
682
- for (const key of Object.keys(defaults)) {
683
- if (typeof defaults[key] === 'object' && defaults[key] !== null && !Array.isArray(defaults[key])) {
684
- result[key] = this._mergeConfig(defaults[key], overrides[key] || {});
685
- } else {
686
- result[key] = overrides[key] !== undefined ? overrides[key] : defaults[key];
687
- }
688
- }
689
- for (const key of Object.keys(overrides)) {
690
- if (!(key in defaults)) {
691
- result[key] = overrides[key];
692
- }
693
- }
694
- return result;
695
- }
696
-
697
- _getLicenseApiBase() {
698
- const c = this.config;
699
- if (c.apiBaseUrl) return String(c.apiBaseUrl).replace(/\/$/, '');
700
- if (typeof global.location !== 'undefined' && location && location.origin) return location.origin;
701
- return LICENSING_SERVER;
702
- }
703
-
704
- /**
705
- * Report action execution to WAB (fills platform analytics in admin).
706
- */
707
- _maybeReportUsage(actionName, triggerType, success) {
708
- try {
709
- const sessionToken = this._licenseVerified && this._licenseVerified.sessionToken;
710
- if (!sessionToken || (this.config.features && this.config.features.reportUsage === false)) return;
711
- const base = this._getLicenseApiBase();
712
- const payload = JSON.stringify({
713
- sessionToken,
714
- actionName,
715
- triggerType: triggerType || 'unknown',
716
- success: success !== false
717
- });
718
- fetch(`${base}/api/license/track`, {
719
- method: 'POST',
720
- headers: { 'Content-Type': 'application/json' },
721
- body: payload,
722
- keepalive: true,
723
- mode: 'cors',
724
- credentials: 'omit'
725
- }).catch(function () {});
726
- } catch (e) { /* ignore */ }
727
- }
728
-
729
- // ── License Verification ────────────────────────────────────────────
730
- async _verifyLicense() {
731
- try {
732
- const res = await fetch(`${this._getLicenseApiBase()}/api/license/verify`, {
733
- method: 'POST',
734
- headers: { 'Content-Type': 'application/json' },
735
- body: JSON.stringify({
736
- domain: location.hostname,
737
- licenseKey: this.config.licenseKey
738
- })
739
- });
740
- if (res.ok) {
741
- this._licenseVerified = await res.json();
742
- } else {
743
- this._licenseVerified = { tier: 'free', valid: false, error: 'License verification failed' };
744
- }
745
- } catch (e) {
746
- this._licenseVerified = { tier: this.config.subscriptionTier || 'free', valid: false, error: 'Offline' };
747
- }
748
- }
749
-
750
- getEffectiveTier() {
751
- if (this._licenseVerified && this._licenseVerified.valid) {
752
- return this._licenseVerified.tier;
753
- }
754
- return 'free';
755
- }
756
-
757
- // ── Auto-discover Page Actions ──────────────────────────────────────
758
- _autoDiscoverActions() {
759
- const buttons = safeQuerySelectorAll('button, [role="button"], input[type="submit"], a.btn, a.button');
760
- buttons.forEach((el, i) => {
761
- const text = (el.textContent || el.value || '').trim();
762
- if (!text) return;
763
-
764
- const selector = this._generateSelector(el);
765
- if (!selector || !isElementAllowed(selector, this.config)) return;
766
-
767
- const name = this._slugify(text) || `action_${i}`;
768
- if (!this.registry.get(name)) {
769
- this.registry.register({
770
- name,
771
- description: `Click: ${text}`,
772
- trigger: 'click',
773
- selector,
774
- category: 'auto-discovered'
775
- });
776
- }
777
- });
778
-
779
- const forms = safeQuerySelectorAll('form');
780
- forms.forEach((form, i) => {
781
- const formSelector = this._generateSelector(form);
782
- if (!formSelector || !isElementAllowed(formSelector, this.config)) return;
783
-
784
- const fields = Array.from(form.querySelectorAll('input, textarea, select'))
785
- .filter(f => f.type !== 'hidden' && f.type !== 'submit')
786
- .map(f => ({
787
- name: f.name || f.id || f.placeholder || `field_${Math.random().toString(36).slice(2, 6)}`,
788
- selector: this._generateSelector(f),
789
- type: f.type || 'text',
790
- required: f.required,
791
- placeholder: f.placeholder || ''
792
- }));
793
-
794
- const submitBtn = form.querySelector('[type="submit"], button:not([type])');
795
- const formName = form.id || form.name || `form_${i}`;
796
-
797
- this.registry.register({
798
- name: `fill_${this._slugify(formName)}`,
799
- description: `Fill and submit form: ${formName}`,
800
- trigger: 'fill_and_submit',
801
- selector: formSelector,
802
- fields,
803
- submitSelector: submitBtn ? this._generateSelector(submitBtn) : null,
804
- category: 'auto-discovered'
805
- });
806
- });
807
-
808
- const links = safeQuerySelectorAll('nav a, [role="navigation"] a');
809
- links.forEach((el, i) => {
810
- const text = (el.textContent || '').trim();
811
- if (!text) return;
812
-
813
- const selector = this._generateSelector(el);
814
- if (!selector || !isElementAllowed(selector, this.config)) return;
815
-
816
- const name = `nav_${this._slugify(text)}` || `nav_${i}`;
817
- if (!this.registry.get(name)) {
818
- this.registry.register({
819
- name,
820
- description: `Navigate: ${text}`,
821
- trigger: 'click',
822
- selector,
823
- category: 'navigation'
824
- });
825
- }
826
- });
827
-
828
- this._autoDiscoverCommerceAndBookingActions();
829
- this._autoDiscoverStructuredActions();
830
- }
831
-
832
- _autoDiscoverCommerceAndBookingActions() {
833
- const clickable = safeQuerySelectorAll('button, [role="button"], a, input[type="submit"], input[type="button"]');
834
- clickable.forEach((el, i) => {
835
- const text = (el.textContent || el.value || el.getAttribute('aria-label') || '').trim();
836
- if (!text) return;
837
-
838
- const intent = this._inferActionIntent(text);
839
- if (!intent) return;
840
-
841
- const selector = this._generateSelector(el);
842
- if (!selector || !isElementAllowed(selector, this.config)) return;
843
-
844
- const name = `${intent}_${this._slugify(text) || i}`;
845
- if (this.registry.get(name)) return;
846
-
847
- this.registry.register({
848
- name,
849
- description: `${intent.replace(/_/g, ' ')}: ${text}`,
850
- trigger: 'click',
851
- selector,
852
- category: intent.startsWith('book') ? 'booking' : 'commerce',
853
- metadata: { discoveredBy: 'intent' }
854
- });
855
- });
856
-
857
- const forms = safeQuerySelectorAll('form');
858
- forms.forEach((form, i) => {
859
- const formText = `${form.id || ''} ${form.name || ''} ${form.className || ''} ${(form.getAttribute('aria-label') || '')}`.toLowerCase();
860
- const hasBookingSignals = /(book|reservation|reserve|appointment|schedule)/.test(formText)
861
- || !!form.querySelector('input[type="date"], input[type="time"], input[name*="date" i], input[name*="time" i]');
862
- if (!hasBookingSignals) return;
863
-
864
- const formSelector = this._generateSelector(form);
865
- if (!formSelector || !isElementAllowed(formSelector, this.config)) return;
866
-
867
- const fields = Array.from(form.querySelectorAll('input, textarea, select'))
868
- .filter(f => f.type !== 'hidden' && f.type !== 'submit')
869
- .map(f => ({
870
- name: f.name || f.id || f.placeholder || `field_${Math.random().toString(36).slice(2, 6)}`,
871
- selector: this._generateSelector(f),
872
- type: f.type || 'text',
873
- required: f.required,
874
- placeholder: f.placeholder || ''
875
- }));
876
-
877
- const submitBtn = form.querySelector('[type="submit"], button:not([type])');
878
- const actionName = `book_${this._slugify(form.id || form.name || `form_${i}`)}`;
879
- if (this.registry.get(actionName)) return;
880
-
881
- this.registry.register({
882
- name: actionName,
883
- description: `Book or reserve via form: ${form.id || form.name || `form_${i}`}`,
884
- trigger: 'fill_and_submit',
885
- selector: formSelector,
886
- fields,
887
- submitSelector: submitBtn ? this._generateSelector(submitBtn) : null,
888
- category: 'booking',
889
- metadata: { discoveredBy: 'booking-form-signals' }
890
- });
891
- });
892
- }
893
-
894
- _autoDiscoverStructuredActions() {
895
- const products = this._scanStructuredProducts();
896
- if (!products.length) return;
897
-
898
- if (!this.registry.get('get_structured_products')) {
899
- this.registry.register({
900
- name: 'get_structured_products',
901
- description: 'Return product entities discovered from schema.org and microdata',
902
- trigger: 'read',
903
- category: 'structured-data',
904
- params: [
905
- { name: 'limit', type: 'number', required: false }
906
- ],
907
- handler: (params) => {
908
- const limit = Number(params?.limit || products.length);
909
- const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, products.length) : products.length;
910
- return { success: true, count: products.length, products: products.slice(0, safeLimit) };
911
- },
912
- metadata: { discoveredBy: 'schema' }
913
- });
914
- }
915
-
916
- if (!this.registry.get('get_structured_prices')) {
917
- this.registry.register({
918
- name: 'get_structured_prices',
919
- description: 'Return normalized offer prices discovered from structured product data',
920
- trigger: 'read',
921
- category: 'structured-data',
922
- handler: () => {
923
- const prices = products
924
- .map(p => ({
925
- name: p.name || null,
926
- sku: p.sku || null,
927
- price: p.price || null,
928
- priceCurrency: p.priceCurrency || null,
929
- availability: p.availability || null,
930
- source: p.source
931
- }))
932
- .filter(p => p.price != null || p.availability != null);
933
- return { success: true, count: prices.length, offers: prices };
934
- },
935
- metadata: { discoveredBy: 'schema' }
936
- });
937
- }
938
- }
939
-
940
- _scanStructuredProducts() {
941
- const products = [];
942
-
943
- const jsonLdScripts = safeQuerySelectorAll('script[type="application/ld+json"]');
944
- jsonLdScripts.forEach((script, idx) => {
945
- const text = script.textContent;
946
- if (!text || !text.trim()) return;
947
- try {
948
- const parsed = JSON.parse(text);
949
- const graph = Array.isArray(parsed) ? parsed : (Array.isArray(parsed?.['@graph']) ? parsed['@graph'] : [parsed]);
950
- graph.forEach(node => {
951
- if (!node || typeof node !== 'object') return;
952
- const types = Array.isArray(node['@type']) ? node['@type'] : [node['@type']];
953
- if (!types.includes('Product')) return;
954
- const offer = Array.isArray(node.offers) ? node.offers[0] : node.offers;
955
- products.push({
956
- name: node.name || null,
957
- sku: node.sku || null,
958
- price: offer?.price || null,
959
- priceCurrency: offer?.priceCurrency || null,
960
- availability: offer?.availability || null,
961
- source: `jsonld:${idx}`
962
- });
963
- });
964
- } catch (e) {
965
- // Ignore invalid JSON-LD blocks.
966
- }
967
- });
968
-
969
- const microProducts = safeQuerySelectorAll('[itemtype*="schema.org/Product"]');
970
- microProducts.forEach((el, idx) => {
971
- const readProp = (prop) => {
972
- const propEl = el.querySelector(`[itemprop="${prop}"]`);
973
- if (!propEl) return null;
974
- if (propEl.content) return propEl.content;
975
- if (propEl.getAttribute('content')) return propEl.getAttribute('content');
976
- if (propEl.value) return propEl.value;
977
- return (propEl.textContent || '').trim() || null;
978
- };
979
-
980
- const offerRoot = el.querySelector('[itemprop="offers"]');
981
- const readOfferProp = (prop) => {
982
- if (!offerRoot) return null;
983
- const node = offerRoot.querySelector(`[itemprop="${prop}"]`);
984
- if (!node) return null;
985
- return node.getAttribute('content') || node.content || (node.textContent || '').trim() || null;
986
- };
987
-
988
- products.push({
989
- name: readProp('name'),
990
- sku: readProp('sku'),
991
- price: readOfferProp('price') || readProp('price'),
992
- priceCurrency: readOfferProp('priceCurrency'),
993
- availability: readOfferProp('availability'),
994
- source: `microdata:${idx}`
995
- });
996
- });
997
-
998
- const unique = [];
999
- const seen = new Set();
1000
- for (const item of products) {
1001
- const key = `${item.name || ''}|${item.sku || ''}|${item.price || ''}|${item.source}`;
1002
- if (seen.has(key)) continue;
1003
- seen.add(key);
1004
- unique.push(item);
1005
- }
1006
- return unique;
1007
- }
1008
-
1009
- _inferActionIntent(text) {
1010
- const t = String(text || '').toLowerCase();
1011
- if (!t) return null;
1012
- if (/(add\s*to\s*cart|buy\s*now|add\s*bag|add\s*basket)/.test(t)) return 'add_to_cart';
1013
- if (/(checkout|proceed\s*to\s*checkout|pay\s*now)/.test(t)) return 'checkout';
1014
- if (/(book\s*now|reserve|reservation|schedule|appointment)/.test(t)) return 'book_now';
1015
- if (/(get\s*price|check\s*price|view\s*price)/.test(t)) return 'get_price';
1016
- return null;
1017
- }
1018
-
1019
- _generateSelector(el) {
1020
- if (el.id) return `#${CSS.escape(el.id)}`;
1021
- if (el.dataset.wabId) return `[data-wab-id="${el.dataset.wabId}"]`;
1022
-
1023
- const classes = Array.from(el.classList).filter(c => !c.match(/^(js-|is-|has-)/));
1024
- if (classes.length > 0) {
1025
- const sel = `${el.tagName.toLowerCase()}.${classes.map(c => CSS.escape(c)).join('.')}`;
1026
- if (document.querySelectorAll(sel).length === 1) return sel;
1027
- }
1028
-
1029
- const attrs = ['name', 'data-testid', 'aria-label', 'title', 'role'];
1030
- for (const attr of attrs) {
1031
- const val = el.getAttribute(attr);
1032
- if (val) {
1033
- const sel = `${el.tagName.toLowerCase()}[${attr}="${CSS.escape(val)}"]`;
1034
- if (document.querySelectorAll(sel).length === 1) return sel;
1035
- }
1036
- }
1037
-
1038
- const path = [];
1039
- let current = el;
1040
- while (current && current !== document.body) {
1041
- let tag = current.tagName.toLowerCase();
1042
- if (current.id) {
1043
- path.unshift(`#${CSS.escape(current.id)}`);
1044
- break;
1045
- }
1046
- const parent = current.parentElement;
1047
- if (parent) {
1048
- const siblings = Array.from(parent.children).filter(c => c.tagName === current.tagName);
1049
- if (siblings.length > 1) {
1050
- tag += `:nth-of-type(${siblings.indexOf(current) + 1})`;
1051
- }
1052
- }
1053
- path.unshift(tag);
1054
- current = parent;
1055
- }
1056
- return path.join(' > ');
1057
- }
1058
-
1059
- _slugify(text) {
1060
- return text
1061
- .toLowerCase()
1062
- .replace(/[^\w\s-]/g, '')
1063
- .replace(/[\s_]+/g, '_')
1064
- .replace(/^-+|-+$/g, '')
1065
- .slice(0, 40);
1066
- }
1067
-
1068
- // ── Agent Authentication ────────────────────────────────────────────
1069
- authenticate(agentKey, agentMeta = {}) {
1070
- this.logger.log('authenticate', { agentKey: agentKey ? '***' : null });
1071
- this.events.emit('agent:authenticate', { agentMeta });
1072
- this.authenticated = true;
1073
- this.agentInfo = { key: agentKey, ...agentMeta, authenticatedAt: new Date().toISOString() };
1074
- return { success: true, permissions: this._getEffectivePermissions() };
1075
- }
1076
-
1077
- // ── Permission Checks ───────────────────────────────────────────────
1078
- _getEffectivePermissions() {
1079
- const perms = { ...this.config.agentPermissions };
1080
- const tier = this.getEffectiveTier();
1081
-
1082
- if (tier === 'free') {
1083
- perms.apiAccess = false;
1084
- perms.automatedLogin = false;
1085
- perms.extractData = false;
1086
- }
1087
-
1088
- return perms;
1089
- }
1090
-
1091
- _checkPermission(action) {
1092
- const perms = this._getEffectivePermissions();
1093
-
1094
- switch (action) {
1095
- case 'click': return perms.click;
1096
- case 'fill_and_submit': return perms.fillForms;
1097
- case 'scroll': return perms.scroll;
1098
- case 'navigate': return perms.navigate;
1099
- case 'api': return perms.apiAccess;
1100
- case 'read': return perms.readContent;
1101
- case 'extract': return perms.extractData;
1102
- default: return perms.click;
1103
- }
1104
- }
1105
-
1106
- // ── Core: Execute Action ────────────────────────────────────────────
1107
- async execute(actionName, params = {}) {
1108
- // Security: check if bridge is locked
1109
- if (this.security.isLocked) {
1110
- return { success: false, error: 'Bridge locked due to security violations' };
1111
- }
1112
-
1113
- if (!this.rateLimiter.check()) {
1114
- const error = { success: false, error: 'Rate limit exceeded', retryAfter: 60 };
1115
- this.logger.log('rate_limit', { action: actionName });
1116
- this.security.audit('rate_limit', { action: actionName }, 'blocked');
1117
- this.events.emit('error', error);
1118
- return error;
1119
- }
1120
-
1121
- const action = this.registry.get(actionName);
1122
- if (!action) {
1123
- return { success: false, error: `Action "${actionName}" not found`, available: this.registry.list().map(a => a.name) };
1124
- }
1125
-
1126
- if (!this._checkPermission(action.trigger)) {
1127
- this.security.audit('permission_denied', { action: actionName, trigger: action.trigger }, 'blocked');
1128
- return { success: false, error: `Permission denied for trigger type: ${action.trigger}`, tier: this.getEffectiveTier() };
1129
- }
1130
-
1131
- if (action.requiresAuth && !this.authenticated) {
1132
- return { success: false, error: 'Authentication required for this action' };
1133
- }
1134
-
1135
- const loginRequired = this.config.restrictions.requireLoginForActions || [];
1136
- if (loginRequired.includes(action.trigger) && !this.authenticated) {
1137
- return { success: false, error: 'Login required for this action type' };
1138
- }
1139
-
1140
- // Self-healing: resolve selector if original is broken
1141
- if (action.selector) {
1142
- const resolved = this.healer.resolve(actionName, action.selector);
1143
- if (resolved && resolved.healed) {
1144
- action.selector = resolved.selector;
1145
- this.logger.log('self_heal', { action: actionName, strategy: resolved.strategy, confidence: resolved.confidence }, 'detailed');
1146
- this.events.emit('selector:healed', { action: actionName, strategy: resolved.strategy });
1147
- }
1148
- }
1149
-
1150
- this.logger.log('execute', { action: actionName, params }, 'basic');
1151
- this.security.audit('execute', { action: actionName, agentId: this.agentInfo?.key });
1152
- this.events.emit('action:before', { action: actionName, params });
1153
-
1154
- try {
1155
- let result;
1156
-
1157
- if (action.handler) {
1158
- result = await action.handler(params);
1159
- } else {
1160
- switch (action.trigger) {
1161
- case 'click':
1162
- result = await this._executeClick(action);
1163
- break;
1164
- case 'fill_and_submit':
1165
- result = await this._executeFillAndSubmit(action, params);
1166
- break;
1167
- case 'scroll':
1168
- result = await this._executeScroll(action);
1169
- break;
1170
- case 'api':
1171
- result = await this._executeApi(action, params);
1172
- break;
1173
- default:
1174
- result = { success: false, error: `Unknown trigger: ${action.trigger}` };
1175
- }
1176
- }
1177
-
1178
- this.events.emit('action:after', { action: actionName, result });
1179
- this.logger.log('execute_result', { action: actionName, success: result.success }, 'detailed');
1180
- this._maybeReportUsage(actionName, action.trigger, !!(result && result.success !== false));
1181
- return result;
1182
-
1183
- } catch (err) {
1184
- const error = { success: false, error: err.message };
1185
- this.events.emit('error', { action: actionName, error: err.message });
1186
- this.logger.log('execute_error', { action: actionName, error: err.message });
1187
- this._maybeReportUsage(actionName, action.trigger, false);
1188
- return error;
1189
- }
1190
- }
1191
-
1192
- async _executeClick(action) {
1193
- if (!action.selector) return { success: false, error: 'No selector defined' };
1194
- if (!isElementAllowed(action.selector, this.config)) {
1195
- return { success: false, error: 'Element is blocked by restrictions' };
1196
- }
1197
-
1198
- // Self-healing: try to find element even if selector is stale
1199
- let el = safeQuerySelector(action.selector);
1200
- if (!el) {
1201
- const resolved = this.healer.resolve(action.name, action.selector);
1202
- if (resolved) {
1203
- el = resolved.element;
1204
- } else {
1205
- return { success: false, error: `Element not found: ${action.selector}` };
1206
- }
1207
- }
1208
-
1209
- // Stealth: use human-like click simulation
1210
- if (this.stealth.isEnabled) {
1211
- await this.stealth.delay(100, 400);
1212
- await this.stealth.simulateClick(el);
1213
- } else {
1214
- el.click();
1215
- }
1216
-
1217
- return { success: true, action: 'click', selector: action.selector };
1218
- }
1219
-
1220
- async _executeFillAndSubmit(action, params) {
1221
- if (!action.fields) return { success: false, error: 'No fields defined' };
1222
-
1223
- const results = [];
1224
- for (const field of action.fields) {
1225
- if (!isElementAllowed(field.selector, this.config)) {
1226
- results.push({ field: field.name, success: false, error: 'Element blocked' });
1227
- continue;
1228
- }
1229
-
1230
- let el = safeQuerySelector(field.selector);
1231
- if (!el) {
1232
- // Self-healing: try to find field by name/placeholder
1233
- const healedField = this.healer.resolve(`field_${field.name}`, field.selector);
1234
- if (healedField) {
1235
- el = healedField.element;
1236
- } else {
1237
- results.push({ field: field.name, success: false, error: 'Element not found' });
1238
- continue;
1239
- }
1240
- }
1241
-
1242
- const value = params[field.name];
1243
- if (value !== undefined) {
1244
- if (this.stealth.isEnabled) {
1245
- await this.stealth.delay(200, 600);
1246
- await this.stealth.simulateTyping(el, String(value));
1247
- } else {
1248
- el.focus();
1249
- el.value = value;
1250
- el.dispatchEvent(new Event('input', { bubbles: true }));
1251
- el.dispatchEvent(new Event('change', { bubbles: true }));
1252
- }
1253
- results.push({ field: field.name, success: true });
1254
- } else if (field.required !== false) {
1255
- results.push({ field: field.name, success: false, error: 'Value required but not provided' });
1256
- }
1257
- }
1258
-
1259
- if (action.submitSelector) {
1260
- let submitEl = safeQuerySelector(action.submitSelector);
1261
- if (submitEl) {
1262
- if (this.stealth.isEnabled) {
1263
- await this.stealth.delay(300, 800);
1264
- await this.stealth.simulateClick(submitEl);
1265
- } else {
1266
- submitEl.click();
1267
- }
1268
- results.push({ field: '_submit', success: true });
1269
- }
1270
- }
1271
-
1272
- return { success: results.every(r => r.success), results };
1273
- }
1274
-
1275
- async _executeScroll(action) {
1276
- if (action.selector) {
1277
- const el = safeQuerySelector(action.selector);
1278
- if (el) {
1279
- if (this.stealth.isEnabled) {
1280
- await this.stealth.simulateScroll(el);
1281
- } else {
1282
- el.scrollIntoView({ behavior: 'smooth', block: 'center' });
1283
- }
1284
- return { success: true, action: 'scroll', selector: action.selector };
1285
- }
1286
- return { success: false, error: `Element not found: ${action.selector}` };
1287
- }
1288
- if (this.stealth.isEnabled) {
1289
- await this.stealth.simulateScroll(null, 'down');
1290
- } else {
1291
- window.scrollBy({ top: 500, behavior: 'smooth' });
1292
- }
1293
- return { success: true, action: 'scroll', direction: 'down' };
1294
- }
1295
-
1296
- async _executeApi(action, params) {
1297
- if (this.getEffectiveTier() === 'free') {
1298
- return { success: false, error: 'API access requires a premium subscription' };
1299
- }
1300
-
1301
- const url = new URL(action.endpoint, location.origin);
1302
- const options = { method: action.method || 'GET', headers: { 'Content-Type': 'application/json' } };
1303
-
1304
- if (action.method !== 'GET' && params) {
1305
- options.body = JSON.stringify(params);
1306
- }
1307
-
1308
- const res = await fetch(url.toString(), options);
1309
- const data = await res.json().catch(() => null);
1310
- return { success: res.ok, status: res.status, data };
1311
- }
1312
-
1313
- // ── Content Reading ─────────────────────────────────────────────────
1314
- readContent(selector) {
1315
- if (!this._checkPermission('read')) {
1316
- return { success: false, error: 'readContent permission denied' };
1317
- }
1318
-
1319
- if (!isElementAllowed(selector, this.config)) {
1320
- return { success: false, error: 'Element is blocked by restrictions' };
1321
- }
1322
-
1323
- const el = safeQuerySelector(selector);
1324
- if (!el) return { success: false, error: 'Element not found' };
1325
-
1326
- return {
1327
- success: true,
1328
- text: el.textContent.trim(),
1329
- html: el.innerHTML,
1330
- attributes: Object.fromEntries(
1331
- Array.from(el.attributes).map(a => [a.name, a.value])
1332
- )
1333
- };
1334
- }
1335
-
1336
- getPageInfo() {
1337
- return {
1338
- title: document.title,
1339
- url: location.href,
1340
- domain: location.hostname,
1341
- lang: document.documentElement.lang || 'unknown',
1342
- bridgeVersion: VERSION,
1343
- tier: this.getEffectiveTier(),
1344
- permissions: this._getEffectivePermissions(),
1345
- actionsCount: this.registry.actions.size,
1346
- rateLimitRemaining: this.rateLimiter.remaining,
1347
- security: {
1348
- sandboxActive: true,
1349
- locked: this.security.isLocked,
1350
- sessionToken: this.security.sessionToken
1351
- },
1352
- selfHealing: this.healer.getStats(),
1353
- stealthMode: this.stealth.isEnabled
1354
- };
1355
- }
1356
-
1357
- // ── Custom Action Registration ──────────────────────────────────────
1358
- registerAction(actionDef) {
1359
- this.registry.register(actionDef);
1360
- this.events.emit('action:registered', { name: actionDef.name });
1361
- this.logger.log('register_action', { name: actionDef.name });
1362
- }
1363
-
1364
- unregisterAction(name) {
1365
- this.registry.unregister(name);
1366
- this.events.emit('action:unregistered', { name });
1367
- }
1368
-
1369
- // ── Discovery / Info ────────────────────────────────────────────────
1370
- getActions(category) {
1371
- if (category) return this.registry.getByCategory(category);
1372
- return this.registry.list();
1373
- }
1374
-
1375
- getAction(name) {
1376
- return this.registry.get(name);
1377
- }
1378
-
1379
- // ── Waiting Utilities for Agents ────────────────────────────────────
1380
- waitForElement(selector, timeout = 10000) {
1381
- return new Promise((resolve, reject) => {
1382
- const el = safeQuerySelector(selector);
1383
- if (el) return resolve(el);
1384
-
1385
- const observer = new MutationObserver(() => {
1386
- const found = safeQuerySelector(selector);
1387
- if (found) {
1388
- observer.disconnect();
1389
- clearTimeout(timer);
1390
- resolve(found);
1391
- }
1392
- });
1393
-
1394
- observer.observe(document.body, { childList: true, subtree: true });
1395
-
1396
- const timer = setTimeout(() => {
1397
- observer.disconnect();
1398
- reject(new Error(`Timeout: element "${selector}" not found within ${timeout}ms`));
1399
- }, timeout);
1400
- });
1401
- }
1402
-
1403
- waitForNavigation(timeout = 15000) {
1404
- return new Promise((resolve) => {
1405
- const start = location.href;
1406
- const check = setInterval(() => {
1407
- if (location.href !== start) {
1408
- clearInterval(check);
1409
- clearTimeout(timer);
1410
- resolve({ from: start, to: location.href });
1411
- }
1412
- }, 200);
1413
- const timer = setTimeout(() => {
1414
- clearInterval(check);
1415
- resolve({ from: start, to: start, timedOut: true });
1416
- }, timeout);
1417
- });
1418
- }
1419
-
1420
- // ── Lifecycle ───────────────────────────────────────────────────────
1421
- refresh() {
1422
- this.registry = new ActionRegistry();
1423
- this._autoDiscoverActions();
1424
- this._storeFingerprints();
1425
- this.events.emit('refresh');
1426
- this.logger.log('refresh', {});
1427
- }
1428
-
1429
- // ── Agent Mesh Protocol (Client-Side) ───────────────────────────────
1430
-
1431
- async _meshPost(path, body) {
1432
- const base = this._resolveApiBase();
1433
- const res = await fetch(`${base}/api/mesh${path}`, {
1434
- method: 'POST',
1435
- headers: { 'Content-Type': 'application/json' },
1436
- body: body ? JSON.stringify(body) : undefined
1437
- });
1438
- if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.error || res.statusText); }
1439
- return res.json();
1440
- }
1441
-
1442
- async _meshGet(path) {
1443
- const base = this._resolveApiBase();
1444
- const res = await fetch(`${base}/api/mesh${path}`);
1445
- if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.error || res.statusText); }
1446
- return res.json();
1447
- }
1448
-
1449
- async _meshDelete(path) {
1450
- const base = this._resolveApiBase();
1451
- const res = await fetch(`${base}/api/mesh${path}`, { method: 'DELETE' });
1452
- if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.error || res.statusText); }
1453
- return res.json();
1454
- }
1455
-
1456
- async meshJoin(role, displayName, capabilities) {
1457
- const data = await this._meshPost('/agents', { siteId: this.config.siteId, role, displayName, capabilities });
1458
- this._meshAgentId = data.agent.id;
1459
- this._meshHeartbeat = setInterval(() => {
1460
- this._meshPost(`/agents/${this._meshAgentId}/heartbeat`).catch(() => {});
1461
- }, 30000);
1462
- this.events.emit('mesh:joined', data.agent);
1463
- return data.agent;
1464
- }
1465
-
1466
- async meshLeave() {
1467
- if (this._meshHeartbeat) { clearInterval(this._meshHeartbeat); this._meshHeartbeat = null; }
1468
- if (this._meshAgentId) {
1469
- await this._meshDelete(`/agents/${this._meshAgentId}`).catch(() => {});
1470
- this._meshAgentId = null;
1471
- }
1472
- }
1473
-
1474
- async meshPublish(channel, messageType, subject, payload, opts) {
1475
- if (!this._meshAgentId) throw new Error('Must call meshJoin() first');
1476
- return (await this._meshPost('/messages', {
1477
- channelName: channel || 'general', senderId: this._meshAgentId,
1478
- targetId: opts?.targetId, type: messageType, subject, payload,
1479
- priority: opts?.priority, ttl: opts?.ttl
1480
- })).message;
1481
- }
1482
-
1483
- async meshReceive(limit) {
1484
- if (!this._meshAgentId) throw new Error('Must call meshJoin() first');
1485
- return (await this._meshGet(`/messages?agentId=${this._meshAgentId}&limit=${limit || 50}`)).messages;
1486
- }
1487
-
1488
- async meshAcknowledge(messageId) {
1489
- return this._meshPost(`/messages/${encodeURIComponent(messageId)}/acknowledge`);
1490
- }
1491
-
1492
- async meshUnread() {
1493
- if (!this._meshAgentId) throw new Error('Must call meshJoin() first');
1494
- return this._meshGet(`/agents/${this._meshAgentId}/unread`);
1495
- }
1496
-
1497
- async meshShareKnowledge(type, key, value, opts) {
1498
- if (!this._meshAgentId) throw new Error('Must call meshJoin() first');
1499
- return (await this._meshPost('/knowledge', {
1500
- agentId: this._meshAgentId, type, domain: opts?.domain,
1501
- key, value, confidence: opts?.confidence, source: opts?.source
1502
- })).knowledge;
1503
- }
1504
-
1505
- async meshQueryKnowledge(params) {
1506
- const qs = new URLSearchParams(params || {}).toString();
1507
- return (await this._meshGet(`/knowledge?${qs}`)).knowledge;
1508
- }
1509
-
1510
- async meshSearchKnowledge(query, limit) {
1511
- return (await this._meshGet(`/knowledge/search/${encodeURIComponent(query)}?limit=${limit || 20}`)).knowledge;
1512
- }
1513
-
1514
- async meshAlert(subject, details, priority) {
1515
- if (!this._meshAgentId) throw new Error('Must call meshJoin() first');
1516
- return (await this._meshPost('/alert', { senderId: this._meshAgentId, subject, details, priority })).message;
1517
- }
1518
-
1519
- async meshCreateVote(subject, options, deadlineSeconds) {
1520
- if (!this._meshAgentId) throw new Error('Must call meshJoin() first');
1521
- return (await this._meshPost('/votes', { senderId: this._meshAgentId, subject, options, deadlineSeconds })).vote;
1522
- }
1523
-
1524
- async meshCastVote(voteMessageId, choice, weight, reason) {
1525
- if (!this._meshAgentId) throw new Error('Must call meshJoin() first');
1526
- return (await this._meshPost(`/votes/${encodeURIComponent(voteMessageId)}/cast`, {
1527
- voterId: this._meshAgentId, choice, weight, reason
1528
- })).result;
1529
- }
1530
-
1531
- async meshTallyVote(voteMessageId) {
1532
- return (await this._meshGet(`/votes/${encodeURIComponent(voteMessageId)}/tally`)).tally;
1533
- }
1534
-
1535
- async symphonyPerform(template, inputData, schema) {
1536
- const data = await this._meshPost('/symphony/compose', {
1537
- siteId: this.config.siteId, template, inputData, schema
1538
- });
1539
- this.events.emit('symphony:completed', data);
1540
- return data;
1541
- }
1542
-
1543
- async learnRecord(domain, action, context, features) {
1544
- if (!this._meshAgentId) throw new Error('Must call meshJoin() first');
1545
- return this._meshPost('/learning/decisions', {
1546
- siteId: this.config.siteId, agentId: this._meshAgentId, domain, action, context, features
1547
- });
1548
- }
1549
-
1550
- async learnFeedback(decisionId, outcome, reward) {
1551
- return this._meshPost('/learning/feedback', { decisionId, outcome, reward });
1552
- }
1553
-
1554
- async learnRecommend(domain, actions, context) {
1555
- if (!this._meshAgentId) throw new Error('Must call meshJoin() first');
1556
- return this._meshPost('/learning/recommend', {
1557
- siteId: this.config.siteId, agentId: this._meshAgentId, domain, actions, context
1558
- });
1559
- }
1560
-
1561
- // ── Commander Agent Protocol ────────────────────────────────────────
1562
-
1563
- async _cmdPost(path, body) {
1564
- const base = this.config.serverUrl || '';
1565
- const res = await fetch(`${base}/api/commander${path}`, {
1566
- method: 'POST', headers: { 'Content-Type': 'application/json' },
1567
- body: JSON.stringify(body)
1568
- });
1569
- if (!res.ok) throw new Error(`Commander POST ${path} failed: ${res.status}`);
1570
- return res.json();
1571
- }
1572
-
1573
- async _cmdGet(path) {
1574
- const base = this.config.serverUrl || '';
1575
- const res = await fetch(`${base}/api/commander${path}`);
1576
- if (!res.ok) throw new Error(`Commander GET ${path} failed: ${res.status}`);
1577
- return res.json();
1578
- }
1579
-
1580
- /** Launch a mission — decompose a goal and execute it. */
1581
- async commanderLaunch(goal, options) {
1582
- const data = await this._cmdPost('/missions/launch', {
1583
- siteId: this.config.siteId, goal,
1584
- title: options?.title || goal.substring(0, 80),
1585
- strategy: options?.strategy,
1586
- priority: options?.priority, context: options?.context
1587
- });
1588
- this.events.emit('commander:mission', data.mission);
1589
- return data.mission;
1590
- }
1591
-
1592
- /** Get commander + edge + local AI stats. */
1593
- async commanderStats() {
1594
- return this._cmdGet(`/stats?siteId=${encodeURIComponent(this.config.siteId || 'default')}`);
1595
- }
1596
-
1597
- /** Register an edge computing node. */
1598
- async edgeRegisterNode(hostname, hardware, capabilities) {
1599
- return this._cmdPost('/edge/nodes', {
1600
- siteId: this.config.siteId, hostname, hardware, capabilities
1601
- });
1602
- }
1603
-
1604
- /** Submit a task to the edge computing queue. */
1605
- async edgeSubmitTask(taskType, payload, options) {
1606
- return this._cmdPost('/edge/tasks', { taskType, payload, ...options });
1607
- }
1608
-
1609
- /** Discover local AI models (Ollama, llama.cpp, etc.). */
1610
- async localAIDiscover(customEndpoints) {
1611
- return this._cmdPost('/local-ai/discover', {
1612
- siteId: this.config.siteId, customEndpoints
1613
- });
1614
- }
1615
-
1616
- /** Run inference on a local AI model. */
1617
- async localAIInfer(prompt, options) {
1618
- return this._cmdPost('/local-ai/infer', {
1619
- siteId: this.config.siteId, prompt, ...options
1620
- });
1621
- }
1622
-
1623
- destroy() {
1624
- this.events.emit('destroy');
1625
- if (this._meshHeartbeat) { clearInterval(this._meshHeartbeat); this._meshHeartbeat = null; }
1626
- this._meshAgentId = null;
1627
- if (this._mutationObserver) {
1628
- this._mutationObserver.disconnect();
1629
- this._mutationObserver = null;
1630
- }
1631
- this.registry = new ActionRegistry();
1632
- this.logger.clear();
1633
- delete global.AICommands;
1634
- delete global.WebAgentBridge;
1635
- delete global.__wab_bidi;
1636
- }
1637
-
1638
- // ── Serialization ───────────────────────────────────────────────────
1639
- toJSON() {
1640
- return {
1641
- version: VERSION,
1642
- page: this.getPageInfo(),
1643
- actions: this.getActions()
1644
- };
1645
- }
1646
-
1647
- // ── WebDriver BiDi Compatibility ────────────────────────────────────
1648
- // Exposes a standardized protocol for agents using WebDriver BiDi
1649
- toBiDi() {
1650
- return {
1651
- type: 'wab:context',
1652
- version: VERSION,
1653
- context: {
1654
- url: location.href,
1655
- title: document.title,
1656
- browsingContext: typeof window !== 'undefined' ? window.name || 'default' : 'default'
1657
- },
1658
- capabilities: {
1659
- actions: this.getActions().map(a => ({
1660
- id: a.name,
1661
- type: a.trigger === 'click' ? 'pointerDown' : a.trigger === 'fill_and_submit' ? 'key' : a.trigger,
1662
- description: a.description,
1663
- parameters: a.fields ? a.fields.map(f => ({ name: f.name, type: f.type, required: f.required })) : undefined
1664
- })),
1665
- permissions: this._getEffectivePermissions(),
1666
- tier: this.getEffectiveTier()
1667
- }
1668
- };
1669
- }
1670
-
1671
- // Execute via BiDi-style command
1672
- async executeBiDi(command) {
1673
- // Security: validate command
1674
- const validation = this.security.validateCommand(command || {});
1675
- if (!validation.valid) {
1676
- return { id: command?.id, error: { code: 'security error', message: validation.error } };
1677
- }
1678
-
1679
- if (!command || !command.method) {
1680
- return { id: command?.id, error: { code: 'invalid argument', message: 'Missing method' } };
1681
- }
1682
-
1683
- this.security.audit('bidi_command', { method: command.method });
1684
- const responseBase = { id: command.id || null, type: 'success' };
1685
-
1686
- switch (command.method) {
1687
- case 'wab.getContext':
1688
- return { ...responseBase, result: this.toBiDi() };
1689
-
1690
- case 'wab.getActions':
1691
- return { ...responseBase, result: this.getActions(command.params?.category) };
1692
-
1693
- case 'wab.executeAction':
1694
- if (!command.params?.name) {
1695
- return { id: command.id, error: { code: 'invalid argument', message: 'Action name required' } };
1696
- }
1697
- const result = await this.execute(command.params.name, command.params.data || {});
1698
- return { ...responseBase, result };
1699
-
1700
- case 'wab.readContent':
1701
- if (!command.params?.selector) {
1702
- return { id: command.id, error: { code: 'invalid argument', message: 'Selector required' } };
1703
- }
1704
- return { ...responseBase, result: this.readContent(command.params.selector) };
1705
-
1706
- case 'wab.getPageInfo':
1707
- return { ...responseBase, result: this.getPageInfo() };
1708
-
1709
- default:
1710
- return { id: command.id, error: { code: 'unknown command', message: `Unknown method: ${command.method}` } };
1711
- }
1712
- }
1713
- }
1714
-
1715
- // ─── Auto-initialize ──────────────────────────────────────────────────
1716
- function autoInit() {
1717
- const config = global.AIBridgeConfig || {};
1718
-
1719
- const scriptTag = document.currentScript || document.querySelector('script[data-wab-config]');
1720
- if (scriptTag) {
1721
- const dataConfig = scriptTag.getAttribute('data-config') || scriptTag.getAttribute('data-wab-config');
1722
- if (dataConfig) {
1723
- try {
1724
- Object.assign(config, JSON.parse(dataConfig));
1725
- } catch (e) {
1726
- console.error('[WAB] Invalid data-config JSON:', e);
1727
- }
1728
- }
1729
- }
1730
-
1731
- const bridge = new WebAgentBridge(config);
1732
-
1733
- global.AICommands = bridge;
1734
- global.WebAgentBridge = WebAgentBridge;
1735
-
1736
- // WebDriver BiDi compatibility: expose via __wab_bidi channel
1737
- global.__wab_bidi = {
1738
- version: VERSION,
1739
- send: async (command) => bridge.executeBiDi(command),
1740
- getContext: () => bridge.toBiDi()
1741
- };
1742
-
1743
- if (typeof CustomEvent !== 'undefined') {
1744
- document.dispatchEvent(new CustomEvent('wab:ready', { detail: { version: VERSION } }));
1745
- }
1746
- }
1747
-
1748
- if (document.readyState === 'loading') {
1749
- document.addEventListener('DOMContentLoaded', autoInit);
1750
- } else {
1751
- autoInit();
1752
- }
1753
-
1754
- })(typeof window !== 'undefined' ? window : globalThis);
1
+ /**
2
+ * Web Agent Bridge v2.0.0
3
+ * Open-source middleware for AI agent ↔ website interaction
4
+ * https://github.com/web-agent-bridge
5
+ * License: MIT
6
+ */
7
+ (function (global) {
8
+ 'use strict';
9
+
10
+ const VERSION = '2.2.0';
11
+ const LICENSING_SERVER = 'https://api.webagentbridge.com';
12
+
13
+ // ─── Default Configuration ────────────────────────────────────────────
14
+ const DEFAULT_CONFIG = {
15
+ agentPermissions: {
16
+ readContent: true,
17
+ click: true,
18
+ fillForms: false,
19
+ scroll: true,
20
+ navigate: false,
21
+ apiAccess: false,
22
+ automatedLogin: false,
23
+ extractData: false
24
+ },
25
+ restrictions: {
26
+ allowedSelectors: [],
27
+ blockedSelectors: ['.private', '[data-private]', '[data-no-agent]'],
28
+ requireLoginForActions: [],
29
+ rateLimit: { maxCallsPerMinute: 60 }
30
+ },
31
+ logging: {
32
+ enabled: false,
33
+ level: 'basic'
34
+ },
35
+ subscriptionTier: 'free',
36
+ licenseKey: null,
37
+ /** Public site id from dashboard (preferred). Used with configEndpoint for token exchange — license key stays off the page. */
38
+ siteId: null,
39
+ /** Base URL for /api/license/* (verify, track). Default: current origin, else LICENSING_SERVER. */
40
+ apiBaseUrl: null,
41
+ features: {
42
+ advancedAnalytics: false,
43
+ realTimeUpdates: false,
44
+ customActions: false,
45
+ webhooks: false,
46
+ /** Send execute events to POST /api/license/track (populates admin analytics). */
47
+ reportUsage: true
48
+ }
49
+ };
50
+
51
+ // ─── Rate Limiter ─────────────────────────────────────────────────────
52
+ class RateLimiter {
53
+ constructor(maxPerMinute) {
54
+ this.maxPerMinute = maxPerMinute;
55
+ this.calls = [];
56
+ }
57
+
58
+ check() {
59
+ const now = Date.now();
60
+ this.calls = this.calls.filter(t => now - t < 60000);
61
+ if (this.calls.length >= this.maxPerMinute) {
62
+ return false;
63
+ }
64
+ this.calls.push(now);
65
+ return true;
66
+ }
67
+
68
+ get remaining() {
69
+ const now = Date.now();
70
+ this.calls = this.calls.filter(t => now - t < 60000);
71
+ return Math.max(0, this.maxPerMinute - this.calls.length);
72
+ }
73
+ }
74
+
75
+ // ─── Logger ───────────────────────────────────────────────────────────
76
+ class BridgeLogger {
77
+ constructor(config) {
78
+ this.enabled = config.enabled;
79
+ this.level = config.level;
80
+ this.logs = [];
81
+ }
82
+
83
+ log(action, details, level = 'basic') {
84
+ if (!this.enabled) return;
85
+ if (level === 'detailed' && this.level !== 'detailed') return;
86
+
87
+ const entry = {
88
+ timestamp: new Date().toISOString(),
89
+ action,
90
+ details,
91
+ level
92
+ };
93
+ this.logs.push(entry);
94
+
95
+ if (this.logs.length > 1000) {
96
+ this.logs = this.logs.slice(-500);
97
+ }
98
+ }
99
+
100
+ getLogs(filter) {
101
+ if (!filter) return [...this.logs];
102
+ return this.logs.filter(l => l.action === filter);
103
+ }
104
+
105
+ clear() {
106
+ this.logs = [];
107
+ }
108
+ }
109
+
110
+ // ─── Security Sandbox ──────────────────────────────────────────────────
111
+ // Isolates the bridge with origin validation, session tokens, and audit log
112
+ class SecuritySandbox {
113
+ constructor(config) {
114
+ this._sessionToken = this._generateToken();
115
+ this._allowedOrigins = config.security?.allowedOrigins || [location.origin];
116
+ this._auditLog = [];
117
+ this._maxAuditEntries = 500;
118
+ this._commandCounter = 0;
119
+ this._blockedCommands = new Set();
120
+ this._escalationAttempts = 0;
121
+ this._maxEscalationAttempts = 5;
122
+ this._locked = false;
123
+ }
124
+
125
+ _generateToken() {
126
+ const arr = new Uint8Array(32);
127
+ if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
128
+ crypto.getRandomValues(arr);
129
+ } else {
130
+ for (let i = 0; i < 32; i++) arr[i] = Math.floor(Math.random() * 256);
131
+ }
132
+ return Array.from(arr, b => b.toString(16).padStart(2, '0')).join('');
133
+ }
134
+
135
+ get sessionToken() { return this._sessionToken; }
136
+
137
+ validateOrigin(origin) {
138
+ if (!origin) return this._allowedOrigins.includes(location.origin);
139
+ return this._allowedOrigins.includes(origin);
140
+ }
141
+
142
+ audit(action, details, status = 'ok') {
143
+ const entry = {
144
+ id: ++this._commandCounter,
145
+ timestamp: Date.now(),
146
+ action,
147
+ status,
148
+ fingerprint: details?.agentId || 'anonymous'
149
+ };
150
+ this._auditLog.push(entry);
151
+ if (this._auditLog.length > this._maxAuditEntries) {
152
+ this._auditLog = this._auditLog.slice(-250);
153
+ }
154
+ return entry;
155
+ }
156
+
157
+ getAuditLog(limit = 50) {
158
+ return this._auditLog.slice(-limit);
159
+ }
160
+
161
+ checkEscalation(requestedTier, currentTier) {
162
+ const tierLevel = { free: 0, starter: 1, pro: 2, enterprise: 3 };
163
+ if ((tierLevel[requestedTier] || 0) > (tierLevel[currentTier] || 0)) {
164
+ this._escalationAttempts++;
165
+ this.audit('escalation_attempt', { requestedTier, currentTier }, 'blocked');
166
+ if (this._escalationAttempts >= this._maxEscalationAttempts) {
167
+ this._locked = true;
168
+ this.audit('bridge_locked', { reason: 'Too many escalation attempts' }, 'critical');
169
+ }
170
+ return false;
171
+ }
172
+ return true;
173
+ }
174
+
175
+ get isLocked() { return this._locked; }
176
+
177
+ validateCommand(command) {
178
+ if (this._locked) return { valid: false, error: 'Bridge is locked due to security violations' };
179
+ if (typeof command !== 'object' || !command) return { valid: false, error: 'Invalid command format' };
180
+ if (typeof command.method !== 'string') return { valid: false, error: 'Command method must be a string' };
181
+ if (command.method.length > 200) return { valid: false, error: 'Command method too long' };
182
+ if (this._blockedCommands.has(command.method)) return { valid: false, error: 'Command is blocked' };
183
+ return { valid: true };
184
+ }
185
+ }
186
+
187
+ // ─── Self-Healing Selectors ───────────────────────────────────────────
188
+ // Resilient element resolution for SPAs with dynamic DOM
189
+ class SelfHealingSelector {
190
+ constructor() {
191
+ this._fingerprints = new Map(); // name → element fingerprint
192
+ this._healingStats = { healed: 0, failed: 0 };
193
+ }
194
+
195
+ fingerprint(el) {
196
+ if (!el) return null;
197
+ return {
198
+ tag: el.tagName.toLowerCase(),
199
+ id: el.id || null,
200
+ classes: Array.from(el.classList),
201
+ text: (el.textContent || '').trim().slice(0, 100),
202
+ ariaLabel: el.getAttribute('aria-label') || null,
203
+ name: el.getAttribute('name') || null,
204
+ type: el.getAttribute('type') || null,
205
+ role: el.getAttribute('role') || null,
206
+ dataTestId: el.getAttribute('data-testid') || null,
207
+ dataWabId: el.getAttribute('data-wab-id') || null,
208
+ href: el.tagName === 'A' ? el.getAttribute('href') : null,
209
+ placeholder: el.getAttribute('placeholder') || null,
210
+ parentTag: el.parentElement ? el.parentElement.tagName.toLowerCase() : null,
211
+ index: el.parentElement ? Array.from(el.parentElement.children).indexOf(el) : -1
212
+ };
213
+ }
214
+
215
+ store(actionName, selector) {
216
+ const el = safeQuerySelector(selector);
217
+ if (el) {
218
+ this._fingerprints.set(actionName, {
219
+ selector,
220
+ fp: this.fingerprint(el),
221
+ storedAt: Date.now()
222
+ });
223
+ }
224
+ }
225
+
226
+ resolve(actionName, originalSelector) {
227
+ // Try original selector first
228
+ const el = safeQuerySelector(originalSelector);
229
+ if (el) return { element: el, selector: originalSelector, healed: false };
230
+
231
+ // Try self-healing
232
+ const stored = this._fingerprints.get(actionName);
233
+ if (!stored || !stored.fp) {
234
+ this._healingStats.failed++;
235
+ return null;
236
+ }
237
+
238
+ const healed = this._heal(stored.fp);
239
+ if (healed) {
240
+ this._healingStats.healed++;
241
+ return healed;
242
+ }
243
+
244
+ this._healingStats.failed++;
245
+ return null;
246
+ }
247
+
248
+ _heal(fp) {
249
+ // Strategy 1: data-wab-id (most stable)
250
+ if (fp.dataWabId) {
251
+ const el = safeQuerySelector(`[data-wab-id="${fp.dataWabId}"]`);
252
+ if (el) return { element: el, selector: `[data-wab-id="${fp.dataWabId}"]`, healed: true, strategy: 'data-wab-id' };
253
+ }
254
+
255
+ // Strategy 2: data-testid
256
+ if (fp.dataTestId) {
257
+ const el = safeQuerySelector(`[data-testid="${fp.dataTestId}"]`);
258
+ if (el) return { element: el, selector: `[data-testid="${fp.dataTestId}"]`, healed: true, strategy: 'data-testid' };
259
+ }
260
+
261
+ // Strategy 3: id (may have changed)
262
+ if (fp.id) {
263
+ const el = safeQuerySelector(`#${CSS.escape(fp.id)}`);
264
+ if (el) return { element: el, selector: `#${CSS.escape(fp.id)}`, healed: true, strategy: 'id' };
265
+ }
266
+
267
+ // Strategy 4: aria-label (semantic, usually stable)
268
+ if (fp.ariaLabel) {
269
+ const sel = `${fp.tag}[aria-label="${CSS.escape(fp.ariaLabel)}"]`;
270
+ const el = safeQuerySelector(sel);
271
+ if (el) return { element: el, selector: sel, healed: true, strategy: 'aria-label' };
272
+ }
273
+
274
+ // Strategy 5: name attribute
275
+ if (fp.name) {
276
+ const sel = `${fp.tag}[name="${CSS.escape(fp.name)}"]`;
277
+ const el = safeQuerySelector(sel);
278
+ if (el) return { element: el, selector: sel, healed: true, strategy: 'name' };
279
+ }
280
+
281
+ // Strategy 6: text content matching (fuzzy)
282
+ if (fp.text && fp.text.length > 0) {
283
+ const candidates = safeQuerySelectorAll(fp.tag);
284
+ const target = fp.text.toLowerCase();
285
+ let bestMatch = null;
286
+ let bestScore = 0;
287
+
288
+ for (const candidate of candidates) {
289
+ const candidateText = (candidate.textContent || '').trim().toLowerCase();
290
+ if (!candidateText) continue;
291
+
292
+ const score = this._textSimilarity(target, candidateText);
293
+ if (score > 0.7 && score > bestScore) {
294
+ bestScore = score;
295
+ bestMatch = candidate;
296
+ }
297
+ }
298
+
299
+ if (bestMatch) {
300
+ const healedSel = this._buildSelectorFor(bestMatch);
301
+ return { element: bestMatch, selector: healedSel, healed: true, strategy: 'text-fuzzy', confidence: bestScore };
302
+ }
303
+ }
304
+
305
+ // Strategy 7: role + position heuristic
306
+ if (fp.role && fp.parentTag) {
307
+ const candidates = safeQuerySelectorAll(`${fp.parentTag} > ${fp.tag}[role="${fp.role}"]`);
308
+ if (candidates.length > 0 && fp.index >= 0 && fp.index < candidates.length) {
309
+ const el = candidates[fp.index];
310
+ const sel = this._buildSelectorFor(el);
311
+ return { element: el, selector: sel, healed: true, strategy: 'role-position' };
312
+ }
313
+ }
314
+
315
+ return null;
316
+ }
317
+
318
+ _textSimilarity(a, b) {
319
+ if (a === b) return 1;
320
+ const longer = a.length > b.length ? a : b;
321
+ const shorter = a.length > b.length ? b : a;
322
+ if (longer.length === 0) return 1;
323
+ if (longer.includes(shorter) || shorter.includes(longer)) {
324
+ return shorter.length / longer.length;
325
+ }
326
+ // Simple bigram similarity
327
+ const bigramsA = new Set();
328
+ for (let i = 0; i < a.length - 1; i++) bigramsA.add(a.slice(i, i + 2));
329
+ let matches = 0;
330
+ for (let i = 0; i < b.length - 1; i++) {
331
+ if (bigramsA.has(b.slice(i, i + 2))) matches++;
332
+ }
333
+ const total = Math.max(bigramsA.size, b.length - 1);
334
+ return total > 0 ? matches / total : 0;
335
+ }
336
+
337
+ _buildSelectorFor(el) {
338
+ if (el.id) return `#${CSS.escape(el.id)}`;
339
+ if (el.dataset.wabId) return `[data-wab-id="${el.dataset.wabId}"]`;
340
+ const tag = el.tagName.toLowerCase();
341
+ const attrs = ['data-testid', 'aria-label', 'name'];
342
+ for (const attr of attrs) {
343
+ const val = el.getAttribute(attr);
344
+ if (val && document.querySelectorAll(`${tag}[${attr}="${CSS.escape(val)}"]`).length === 1) {
345
+ return `${tag}[${attr}="${CSS.escape(val)}"]`;
346
+ }
347
+ }
348
+ return null;
349
+ }
350
+
351
+ getStats() {
352
+ return { ...this._healingStats, tracked: this._fingerprints.size };
353
+ }
354
+ }
355
+
356
+ // ─── Stealth / Human-like Interaction ─────────────────────────────────
357
+ // ⚠️ ETHICAL USE POLICY:
358
+ // Stealth mode simulates human-like interaction patterns for LEGITIMATE uses:
359
+ // - Accessibility testing - QA automation on YOUR OWN sites
360
+ // - UX research with consent - Authorized penetration testing
361
+ // DO NOT use to bypass anti-bot protections on sites you do not own or control.
362
+ // Misuse violates the MIT license terms and is the user's legal responsibility.
363
+ const Stealth = {
364
+ _enabled: false,
365
+ _consentGiven: false,
366
+
367
+ enable(consent) {
368
+ if (consent !== true) {
369
+ console.warn('[WAB] Stealth mode requires explicit consent: stealth.enable(true)');
370
+ return;
371
+ }
372
+ this._consentGiven = true;
373
+ this._enabled = true;
374
+ },
375
+ disable() { this._enabled = false; },
376
+ get isEnabled() { return this._enabled; },
377
+
378
+ // Random delay between min and max ms
379
+ delay(min = 50, max = 300) {
380
+ if (!this._enabled) return Promise.resolve();
381
+ const duration = min + Math.floor(Math.random() * (max - min));
382
+ return new Promise(resolve => setTimeout(resolve, duration));
383
+ },
384
+
385
+ // Simulate a full mouse event chain: mouseover → mouseenter → mousemove → mousedown → mouseup → click
386
+ async simulateClick(el) {
387
+ if (!el) return;
388
+ const rect = el.getBoundingClientRect();
389
+ const x = rect.left + rect.width * (0.3 + Math.random() * 0.4);
390
+ const y = rect.top + rect.height * (0.3 + Math.random() * 0.4);
391
+ const eventOpts = { bubbles: true, cancelable: true, clientX: x, clientY: y, view: window };
392
+
393
+ el.dispatchEvent(new MouseEvent('mouseover', eventOpts));
394
+ el.dispatchEvent(new MouseEvent('mouseenter', { ...eventOpts, bubbles: false }));
395
+ await this.delay(30, 80);
396
+ el.dispatchEvent(new MouseEvent('mousemove', eventOpts));
397
+ await this.delay(40, 120);
398
+ el.dispatchEvent(new MouseEvent('mousedown', eventOpts));
399
+ await this.delay(50, 150);
400
+ el.dispatchEvent(new MouseEvent('mouseup', eventOpts));
401
+ el.dispatchEvent(new MouseEvent('click', eventOpts));
402
+ },
403
+
404
+ // Simulate human-like typing with variable delays
405
+ async simulateTyping(el, text) {
406
+ if (!el) return;
407
+ el.focus();
408
+ el.dispatchEvent(new Event('focus', { bubbles: true }));
409
+ el.value = '';
410
+
411
+ for (let i = 0; i < text.length; i++) {
412
+ const char = text[i];
413
+ el.dispatchEvent(new KeyboardEvent('keydown', { key: char, bubbles: true }));
414
+ el.value += char;
415
+ el.dispatchEvent(new Event('input', { bubbles: true }));
416
+ el.dispatchEvent(new KeyboardEvent('keyup', { key: char, bubbles: true }));
417
+ await this.delay(30, 120); // Human typing speed: 30-120ms per key
418
+ }
419
+
420
+ el.dispatchEvent(new Event('change', { bubbles: true }));
421
+ },
422
+
423
+ // Simulate natural scrolling (variable speed, easing)
424
+ async simulateScroll(el, direction = 'down') {
425
+ const target = el || document.documentElement;
426
+ const distance = 300 + Math.floor(Math.random() * 400);
427
+ const steps = 5 + Math.floor(Math.random() * 5);
428
+ const stepSize = distance / steps;
429
+
430
+ for (let i = 0; i < steps; i++) {
431
+ const delta = direction === 'down' ? stepSize : -stepSize;
432
+ if (el && el !== document.documentElement) {
433
+ el.scrollTop += delta;
434
+ } else {
435
+ window.scrollBy(0, delta);
436
+ }
437
+ await this.delay(15, 50);
438
+ }
439
+ }
440
+ };
441
+
442
+ // ─── Element Utilities ────────────────────────────────────────────────
443
+ function isElementAllowed(selector, config) {
444
+ const { allowedSelectors, blockedSelectors } = config.restrictions;
445
+
446
+ if (blockedSelectors.length > 0) {
447
+ const el = document.querySelector(selector);
448
+ if (el) {
449
+ for (const blocked of blockedSelectors) {
450
+ if (el.matches(blocked) || el.closest(blocked)) return false;
451
+ }
452
+ }
453
+ }
454
+
455
+ if (allowedSelectors.length > 0) {
456
+ const el = document.querySelector(selector);
457
+ if (el) {
458
+ for (const allowed of allowedSelectors) {
459
+ if (el.matches(allowed) || el.closest(allowed)) return true;
460
+ }
461
+ return false;
462
+ }
463
+ }
464
+
465
+ return true;
466
+ }
467
+
468
+ function safeQuerySelector(selector) {
469
+ try {
470
+ return document.querySelector(selector);
471
+ } catch (e) {
472
+ return null;
473
+ }
474
+ }
475
+
476
+ function safeQuerySelectorAll(selector) {
477
+ try {
478
+ return Array.from(document.querySelectorAll(selector));
479
+ } catch (e) {
480
+ return [];
481
+ }
482
+ }
483
+
484
+ // ─── Action Registry ──────────────────────────────────────────────────
485
+ class ActionRegistry {
486
+ constructor() {
487
+ this.actions = new Map();
488
+ }
489
+
490
+ register(action) {
491
+ if (!action.name || !action.description) {
492
+ throw new Error('Action must have a name and description');
493
+ }
494
+ this.actions.set(action.name, {
495
+ name: action.name,
496
+ description: action.description,
497
+ trigger: action.trigger || 'click',
498
+ selector: action.selector || null,
499
+ fields: action.fields || null,
500
+ submitSelector: action.submitSelector || null,
501
+ endpoint: action.endpoint || null,
502
+ method: action.method || 'GET',
503
+ requiresAuth: action.requiresAuth || false,
504
+ category: action.category || 'general',
505
+ params: action.params || null,
506
+ handler: action.handler || null,
507
+ metadata: action.metadata || {}
508
+ });
509
+ }
510
+
511
+ unregister(name) {
512
+ return this.actions.delete(name);
513
+ }
514
+
515
+ get(name) {
516
+ return this.actions.get(name) || null;
517
+ }
518
+
519
+ list() {
520
+ return Array.from(this.actions.values()).map(a => ({
521
+ name: a.name,
522
+ description: a.description,
523
+ trigger: a.trigger,
524
+ category: a.category,
525
+ requiresAuth: a.requiresAuth,
526
+ params: a.params,
527
+ fields: a.fields ? a.fields.map(f => ({ name: f.name, type: f.type, required: f.required !== false })) : null
528
+ }));
529
+ }
530
+
531
+ getByCategory(category) {
532
+ return this.list().filter(a => a.category === category);
533
+ }
534
+
535
+ toJSON() {
536
+ return this.list();
537
+ }
538
+ }
539
+
540
+ // ─── Event Emitter ────────────────────────────────────────────────────
541
+ class BridgeEventEmitter {
542
+ constructor() {
543
+ this.listeners = {};
544
+ }
545
+
546
+ on(event, callback) {
547
+ if (!this.listeners[event]) this.listeners[event] = [];
548
+ this.listeners[event].push(callback);
549
+ return () => this.off(event, callback);
550
+ }
551
+
552
+ off(event, callback) {
553
+ if (!this.listeners[event]) return;
554
+ this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
555
+ }
556
+
557
+ emit(event, data) {
558
+ if (!this.listeners[event]) return;
559
+ this.listeners[event].forEach(cb => {
560
+ try { cb(data); } catch (e) { console.error(`[WAB] Event handler error for "${event}":`, e); }
561
+ });
562
+ }
563
+ }
564
+
565
+ // ─── Main Bridge Class ────────────────────────────────────────────────
566
+ class WebAgentBridge {
567
+ constructor(userConfig) {
568
+ this.config = this._mergeConfig(DEFAULT_CONFIG, userConfig || {});
569
+ this.registry = new ActionRegistry();
570
+ this.rateLimiter = new RateLimiter(this.config.restrictions.rateLimit.maxCallsPerMinute);
571
+ this.logger = new BridgeLogger(this.config.logging);
572
+ this.events = new BridgeEventEmitter();
573
+ this.security = new SecuritySandbox(this.config);
574
+ this.healer = new SelfHealingSelector();
575
+ this.stealth = Stealth;
576
+ this.authenticated = false;
577
+ this.agentInfo = null;
578
+ this._licenseVerified = null;
579
+ this._ready = false;
580
+ this._readyCallbacks = [];
581
+ this._mutationObserver = null;
582
+
583
+ // Enable stealth mode if configured (requires consent: true)
584
+ if (this.config.stealth?.enabled && this.config.stealth?.consent === true) {
585
+ this.stealth.enable(true);
586
+ }
587
+
588
+ this._init();
589
+ }
590
+
591
+ // ── Initialization ──────────────────────────────────────────────────
592
+ async _init() {
593
+ if (this.config.configEndpoint && (this.config.siteId || this.config._licenseKey || this.config.licenseKey)) {
594
+ await this._secureLicenseExchange();
595
+ } else if (this.config.licenseKey) {
596
+ await this._verifyLicense();
597
+ } else {
598
+ this._licenseVerified = { tier: 'free', valid: true };
599
+ }
600
+
601
+ this._autoDiscoverActions();
602
+ this._storeFingerprints();
603
+ this._setupSPAObserver();
604
+ this._ready = true;
605
+ this._readyCallbacks.forEach(cb => cb());
606
+ this._readyCallbacks = [];
607
+ this.events.emit('ready', { version: VERSION, tier: this.getEffectiveTier() });
608
+ this.logger.log('init', { version: VERSION, tier: this.getEffectiveTier(), security: 'sandbox-active' });
609
+ }
610
+
611
+ // Secure license exchange: POST license key to server, get session token back
612
+ // License key is transmitted once via POST (not visible in page source)
613
+ async _secureLicenseExchange() {
614
+ try {
615
+ const endpoint = this.config.configEndpoint;
616
+ const body = this.config.siteId
617
+ ? { siteId: this.config.siteId }
618
+ : { licenseKey: this.config._licenseKey || this.config.licenseKey };
619
+ const res = await fetch(endpoint, {
620
+ method: 'POST',
621
+ headers: { 'Content-Type': 'application/json' },
622
+ body: JSON.stringify(body)
623
+ });
624
+ if (res.ok) {
625
+ const data = await res.json();
626
+ this._licenseVerified = { tier: data.tier, valid: true, sessionToken: data.sessionToken };
627
+ delete this.config._licenseKey;
628
+ if (data.expiresIn) {
629
+ if (this._tokenRefreshTimer) clearTimeout(this._tokenRefreshTimer);
630
+ this._tokenRefreshTimer = setTimeout(() => {
631
+ this._secureLicenseExchange();
632
+ }, Math.floor(data.expiresIn * 800));
633
+ }
634
+ } else {
635
+ this._licenseVerified = { tier: 'free', valid: false, error: 'Token exchange failed' };
636
+ }
637
+ } catch (e) {
638
+ this._licenseVerified = { tier: this.config.subscriptionTier || 'free', valid: false, error: 'Offline' };
639
+ }
640
+ }
641
+
642
+ // Store fingerprints for all discovered actions (self-healing)
643
+ _storeFingerprints() {
644
+ for (const action of this.registry.actions.values()) {
645
+ if (action.selector) {
646
+ this.healer.store(action.name, action.selector);
647
+ }
648
+ }
649
+ }
650
+
651
+ // Watch for SPA DOM changes and re-discover actions
652
+ _setupSPAObserver() {
653
+ if (this._mutationObserver) this._mutationObserver.disconnect();
654
+
655
+ let debounceTimer = null;
656
+ this._mutationObserver = new MutationObserver(() => {
657
+ if (debounceTimer) clearTimeout(debounceTimer);
658
+ debounceTimer = setTimeout(() => {
659
+ this._autoDiscoverActions();
660
+ this._storeFingerprints();
661
+ this.events.emit('dom:changed', { actionsCount: this.registry.actions.size });
662
+ }, 500);
663
+ });
664
+
665
+ this._mutationObserver.observe(document.body, {
666
+ childList: true,
667
+ subtree: true,
668
+ attributes: false
669
+ });
670
+ }
671
+
672
+ onReady(callback) {
673
+ if (this._ready) {
674
+ callback();
675
+ } else {
676
+ this._readyCallbacks.push(callback);
677
+ }
678
+ }
679
+
680
+ _mergeConfig(defaults, overrides) {
681
+ const result = {};
682
+ for (const key of Object.keys(defaults)) {
683
+ if (typeof defaults[key] === 'object' && defaults[key] !== null && !Array.isArray(defaults[key])) {
684
+ result[key] = this._mergeConfig(defaults[key], overrides[key] || {});
685
+ } else {
686
+ result[key] = overrides[key] !== undefined ? overrides[key] : defaults[key];
687
+ }
688
+ }
689
+ for (const key of Object.keys(overrides)) {
690
+ if (!(key in defaults)) {
691
+ result[key] = overrides[key];
692
+ }
693
+ }
694
+ return result;
695
+ }
696
+
697
+ _getLicenseApiBase() {
698
+ const c = this.config;
699
+ if (c.apiBaseUrl) return String(c.apiBaseUrl).replace(/\/$/, '');
700
+ if (typeof global.location !== 'undefined' && location && location.origin) return location.origin;
701
+ return LICENSING_SERVER;
702
+ }
703
+
704
+ /**
705
+ * Report action execution to WAB (fills platform analytics in admin).
706
+ */
707
+ _maybeReportUsage(actionName, triggerType, success) {
708
+ try {
709
+ const sessionToken = this._licenseVerified && this._licenseVerified.sessionToken;
710
+ if (!sessionToken || (this.config.features && this.config.features.reportUsage === false)) return;
711
+ const base = this._getLicenseApiBase();
712
+ const payload = JSON.stringify({
713
+ sessionToken,
714
+ actionName,
715
+ triggerType: triggerType || 'unknown',
716
+ success: success !== false
717
+ });
718
+ fetch(`${base}/api/license/track`, {
719
+ method: 'POST',
720
+ headers: { 'Content-Type': 'application/json' },
721
+ body: payload,
722
+ keepalive: true,
723
+ mode: 'cors',
724
+ credentials: 'omit'
725
+ }).catch(function () {});
726
+ } catch (e) { /* ignore */ }
727
+ }
728
+
729
+ // ── License Verification ────────────────────────────────────────────
730
+ async _verifyLicense() {
731
+ try {
732
+ const res = await fetch(`${this._getLicenseApiBase()}/api/license/verify`, {
733
+ method: 'POST',
734
+ headers: { 'Content-Type': 'application/json' },
735
+ body: JSON.stringify({
736
+ domain: location.hostname,
737
+ licenseKey: this.config.licenseKey
738
+ })
739
+ });
740
+ if (res.ok) {
741
+ this._licenseVerified = await res.json();
742
+ } else {
743
+ this._licenseVerified = { tier: 'free', valid: false, error: 'License verification failed' };
744
+ }
745
+ } catch (e) {
746
+ this._licenseVerified = { tier: this.config.subscriptionTier || 'free', valid: false, error: 'Offline' };
747
+ }
748
+ }
749
+
750
+ getEffectiveTier() {
751
+ if (this._licenseVerified && this._licenseVerified.valid) {
752
+ return this._licenseVerified.tier;
753
+ }
754
+ return 'free';
755
+ }
756
+
757
+ // ── Auto-discover Page Actions ──────────────────────────────────────
758
+ _autoDiscoverActions() {
759
+ const buttons = safeQuerySelectorAll('button, [role="button"], input[type="submit"], a.btn, a.button');
760
+ buttons.forEach((el, i) => {
761
+ const text = (el.textContent || el.value || '').trim();
762
+ if (!text) return;
763
+
764
+ const selector = this._generateSelector(el);
765
+ if (!selector || !isElementAllowed(selector, this.config)) return;
766
+
767
+ const name = this._slugify(text) || `action_${i}`;
768
+ if (!this.registry.get(name)) {
769
+ this.registry.register({
770
+ name,
771
+ description: `Click: ${text}`,
772
+ trigger: 'click',
773
+ selector,
774
+ category: 'auto-discovered'
775
+ });
776
+ }
777
+ });
778
+
779
+ const forms = safeQuerySelectorAll('form');
780
+ forms.forEach((form, i) => {
781
+ const formSelector = this._generateSelector(form);
782
+ if (!formSelector || !isElementAllowed(formSelector, this.config)) return;
783
+
784
+ const fields = Array.from(form.querySelectorAll('input, textarea, select'))
785
+ .filter(f => f.type !== 'hidden' && f.type !== 'submit')
786
+ .map(f => ({
787
+ name: f.name || f.id || f.placeholder || `field_${Math.random().toString(36).slice(2, 6)}`,
788
+ selector: this._generateSelector(f),
789
+ type: f.type || 'text',
790
+ required: f.required,
791
+ placeholder: f.placeholder || ''
792
+ }));
793
+
794
+ const submitBtn = form.querySelector('[type="submit"], button:not([type])');
795
+ const formName = form.id || form.name || `form_${i}`;
796
+
797
+ this.registry.register({
798
+ name: `fill_${this._slugify(formName)}`,
799
+ description: `Fill and submit form: ${formName}`,
800
+ trigger: 'fill_and_submit',
801
+ selector: formSelector,
802
+ fields,
803
+ submitSelector: submitBtn ? this._generateSelector(submitBtn) : null,
804
+ category: 'auto-discovered'
805
+ });
806
+ });
807
+
808
+ const links = safeQuerySelectorAll('nav a, [role="navigation"] a');
809
+ links.forEach((el, i) => {
810
+ const text = (el.textContent || '').trim();
811
+ if (!text) return;
812
+
813
+ const selector = this._generateSelector(el);
814
+ if (!selector || !isElementAllowed(selector, this.config)) return;
815
+
816
+ const name = `nav_${this._slugify(text)}` || `nav_${i}`;
817
+ if (!this.registry.get(name)) {
818
+ this.registry.register({
819
+ name,
820
+ description: `Navigate: ${text}`,
821
+ trigger: 'click',
822
+ selector,
823
+ category: 'navigation'
824
+ });
825
+ }
826
+ });
827
+
828
+ this._autoDiscoverCommerceAndBookingActions();
829
+ this._autoDiscoverStructuredActions();
830
+ }
831
+
832
+ _autoDiscoverCommerceAndBookingActions() {
833
+ const clickable = safeQuerySelectorAll('button, [role="button"], a, input[type="submit"], input[type="button"]');
834
+ clickable.forEach((el, i) => {
835
+ const text = (el.textContent || el.value || el.getAttribute('aria-label') || '').trim();
836
+ if (!text) return;
837
+
838
+ const intent = this._inferActionIntent(text);
839
+ if (!intent) return;
840
+
841
+ const selector = this._generateSelector(el);
842
+ if (!selector || !isElementAllowed(selector, this.config)) return;
843
+
844
+ const name = `${intent}_${this._slugify(text) || i}`;
845
+ if (this.registry.get(name)) return;
846
+
847
+ this.registry.register({
848
+ name,
849
+ description: `${intent.replace(/_/g, ' ')}: ${text}`,
850
+ trigger: 'click',
851
+ selector,
852
+ category: intent.startsWith('book') ? 'booking' : 'commerce',
853
+ metadata: { discoveredBy: 'intent' }
854
+ });
855
+ });
856
+
857
+ const forms = safeQuerySelectorAll('form');
858
+ forms.forEach((form, i) => {
859
+ const formText = `${form.id || ''} ${form.name || ''} ${form.className || ''} ${(form.getAttribute('aria-label') || '')}`.toLowerCase();
860
+ const hasBookingSignals = /(book|reservation|reserve|appointment|schedule)/.test(formText)
861
+ || !!form.querySelector('input[type="date"], input[type="time"], input[name*="date" i], input[name*="time" i]');
862
+ if (!hasBookingSignals) return;
863
+
864
+ const formSelector = this._generateSelector(form);
865
+ if (!formSelector || !isElementAllowed(formSelector, this.config)) return;
866
+
867
+ const fields = Array.from(form.querySelectorAll('input, textarea, select'))
868
+ .filter(f => f.type !== 'hidden' && f.type !== 'submit')
869
+ .map(f => ({
870
+ name: f.name || f.id || f.placeholder || `field_${Math.random().toString(36).slice(2, 6)}`,
871
+ selector: this._generateSelector(f),
872
+ type: f.type || 'text',
873
+ required: f.required,
874
+ placeholder: f.placeholder || ''
875
+ }));
876
+
877
+ const submitBtn = form.querySelector('[type="submit"], button:not([type])');
878
+ const actionName = `book_${this._slugify(form.id || form.name || `form_${i}`)}`;
879
+ if (this.registry.get(actionName)) return;
880
+
881
+ this.registry.register({
882
+ name: actionName,
883
+ description: `Book or reserve via form: ${form.id || form.name || `form_${i}`}`,
884
+ trigger: 'fill_and_submit',
885
+ selector: formSelector,
886
+ fields,
887
+ submitSelector: submitBtn ? this._generateSelector(submitBtn) : null,
888
+ category: 'booking',
889
+ metadata: { discoveredBy: 'booking-form-signals' }
890
+ });
891
+ });
892
+ }
893
+
894
+ _autoDiscoverStructuredActions() {
895
+ const products = this._scanStructuredProducts();
896
+ if (!products.length) return;
897
+
898
+ if (!this.registry.get('get_structured_products')) {
899
+ this.registry.register({
900
+ name: 'get_structured_products',
901
+ description: 'Return product entities discovered from schema.org and microdata',
902
+ trigger: 'read',
903
+ category: 'structured-data',
904
+ params: [
905
+ { name: 'limit', type: 'number', required: false }
906
+ ],
907
+ handler: (params) => {
908
+ const limit = Number(params?.limit || products.length);
909
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, products.length) : products.length;
910
+ return { success: true, count: products.length, products: products.slice(0, safeLimit) };
911
+ },
912
+ metadata: { discoveredBy: 'schema' }
913
+ });
914
+ }
915
+
916
+ if (!this.registry.get('get_structured_prices')) {
917
+ this.registry.register({
918
+ name: 'get_structured_prices',
919
+ description: 'Return normalized offer prices discovered from structured product data',
920
+ trigger: 'read',
921
+ category: 'structured-data',
922
+ handler: () => {
923
+ const prices = products
924
+ .map(p => ({
925
+ name: p.name || null,
926
+ sku: p.sku || null,
927
+ price: p.price || null,
928
+ priceCurrency: p.priceCurrency || null,
929
+ availability: p.availability || null,
930
+ source: p.source
931
+ }))
932
+ .filter(p => p.price != null || p.availability != null);
933
+ return { success: true, count: prices.length, offers: prices };
934
+ },
935
+ metadata: { discoveredBy: 'schema' }
936
+ });
937
+ }
938
+ }
939
+
940
+ _scanStructuredProducts() {
941
+ const products = [];
942
+
943
+ const jsonLdScripts = safeQuerySelectorAll('script[type="application/ld+json"]');
944
+ jsonLdScripts.forEach((script, idx) => {
945
+ const text = script.textContent;
946
+ if (!text || !text.trim()) return;
947
+ try {
948
+ const parsed = JSON.parse(text);
949
+ const graph = Array.isArray(parsed) ? parsed : (Array.isArray(parsed?.['@graph']) ? parsed['@graph'] : [parsed]);
950
+ graph.forEach(node => {
951
+ if (!node || typeof node !== 'object') return;
952
+ const types = Array.isArray(node['@type']) ? node['@type'] : [node['@type']];
953
+ if (!types.includes('Product')) return;
954
+ const offer = Array.isArray(node.offers) ? node.offers[0] : node.offers;
955
+ products.push({
956
+ name: node.name || null,
957
+ sku: node.sku || null,
958
+ price: offer?.price || null,
959
+ priceCurrency: offer?.priceCurrency || null,
960
+ availability: offer?.availability || null,
961
+ source: `jsonld:${idx}`
962
+ });
963
+ });
964
+ } catch (e) {
965
+ // Ignore invalid JSON-LD blocks.
966
+ }
967
+ });
968
+
969
+ const microProducts = safeQuerySelectorAll('[itemtype*="schema.org/Product"]');
970
+ microProducts.forEach((el, idx) => {
971
+ const readProp = (prop) => {
972
+ const propEl = el.querySelector(`[itemprop="${prop}"]`);
973
+ if (!propEl) return null;
974
+ if (propEl.content) return propEl.content;
975
+ if (propEl.getAttribute('content')) return propEl.getAttribute('content');
976
+ if (propEl.value) return propEl.value;
977
+ return (propEl.textContent || '').trim() || null;
978
+ };
979
+
980
+ const offerRoot = el.querySelector('[itemprop="offers"]');
981
+ const readOfferProp = (prop) => {
982
+ if (!offerRoot) return null;
983
+ const node = offerRoot.querySelector(`[itemprop="${prop}"]`);
984
+ if (!node) return null;
985
+ return node.getAttribute('content') || node.content || (node.textContent || '').trim() || null;
986
+ };
987
+
988
+ products.push({
989
+ name: readProp('name'),
990
+ sku: readProp('sku'),
991
+ price: readOfferProp('price') || readProp('price'),
992
+ priceCurrency: readOfferProp('priceCurrency'),
993
+ availability: readOfferProp('availability'),
994
+ source: `microdata:${idx}`
995
+ });
996
+ });
997
+
998
+ const unique = [];
999
+ const seen = new Set();
1000
+ for (const item of products) {
1001
+ const key = `${item.name || ''}|${item.sku || ''}|${item.price || ''}|${item.source}`;
1002
+ if (seen.has(key)) continue;
1003
+ seen.add(key);
1004
+ unique.push(item);
1005
+ }
1006
+ return unique;
1007
+ }
1008
+
1009
+ _inferActionIntent(text) {
1010
+ const t = String(text || '').toLowerCase();
1011
+ if (!t) return null;
1012
+ if (/(add\s*to\s*cart|buy\s*now|add\s*bag|add\s*basket)/.test(t)) return 'add_to_cart';
1013
+ if (/(checkout|proceed\s*to\s*checkout|pay\s*now)/.test(t)) return 'checkout';
1014
+ if (/(book\s*now|reserve|reservation|schedule|appointment)/.test(t)) return 'book_now';
1015
+ if (/(get\s*price|check\s*price|view\s*price)/.test(t)) return 'get_price';
1016
+ return null;
1017
+ }
1018
+
1019
+ _generateSelector(el) {
1020
+ if (el.id) return `#${CSS.escape(el.id)}`;
1021
+ if (el.dataset.wabId) return `[data-wab-id="${el.dataset.wabId}"]`;
1022
+
1023
+ const classes = Array.from(el.classList).filter(c => !c.match(/^(js-|is-|has-)/));
1024
+ if (classes.length > 0) {
1025
+ const sel = `${el.tagName.toLowerCase()}.${classes.map(c => CSS.escape(c)).join('.')}`;
1026
+ if (document.querySelectorAll(sel).length === 1) return sel;
1027
+ }
1028
+
1029
+ const attrs = ['name', 'data-testid', 'aria-label', 'title', 'role'];
1030
+ for (const attr of attrs) {
1031
+ const val = el.getAttribute(attr);
1032
+ if (val) {
1033
+ const sel = `${el.tagName.toLowerCase()}[${attr}="${CSS.escape(val)}"]`;
1034
+ if (document.querySelectorAll(sel).length === 1) return sel;
1035
+ }
1036
+ }
1037
+
1038
+ const path = [];
1039
+ let current = el;
1040
+ while (current && current !== document.body) {
1041
+ let tag = current.tagName.toLowerCase();
1042
+ if (current.id) {
1043
+ path.unshift(`#${CSS.escape(current.id)}`);
1044
+ break;
1045
+ }
1046
+ const parent = current.parentElement;
1047
+ if (parent) {
1048
+ const siblings = Array.from(parent.children).filter(c => c.tagName === current.tagName);
1049
+ if (siblings.length > 1) {
1050
+ tag += `:nth-of-type(${siblings.indexOf(current) + 1})`;
1051
+ }
1052
+ }
1053
+ path.unshift(tag);
1054
+ current = parent;
1055
+ }
1056
+ return path.join(' > ');
1057
+ }
1058
+
1059
+ _slugify(text) {
1060
+ return text
1061
+ .toLowerCase()
1062
+ .replace(/[^\w\s-]/g, '')
1063
+ .replace(/[\s_]+/g, '_')
1064
+ .replace(/^-+|-+$/g, '')
1065
+ .slice(0, 40);
1066
+ }
1067
+
1068
+ // ── Agent Authentication ────────────────────────────────────────────
1069
+ authenticate(agentKey, agentMeta = {}) {
1070
+ this.logger.log('authenticate', { agentKey: agentKey ? '***' : null });
1071
+ this.events.emit('agent:authenticate', { agentMeta });
1072
+ this.authenticated = true;
1073
+ this.agentInfo = { key: agentKey, ...agentMeta, authenticatedAt: new Date().toISOString() };
1074
+ return { success: true, permissions: this._getEffectivePermissions() };
1075
+ }
1076
+
1077
+ // ── Permission Checks ───────────────────────────────────────────────
1078
+ _getEffectivePermissions() {
1079
+ const perms = { ...this.config.agentPermissions };
1080
+ const tier = this.getEffectiveTier();
1081
+
1082
+ if (tier === 'free') {
1083
+ perms.apiAccess = false;
1084
+ perms.automatedLogin = false;
1085
+ perms.extractData = false;
1086
+ }
1087
+
1088
+ return perms;
1089
+ }
1090
+
1091
+ _checkPermission(action) {
1092
+ const perms = this._getEffectivePermissions();
1093
+
1094
+ switch (action) {
1095
+ case 'click': return perms.click;
1096
+ case 'fill_and_submit': return perms.fillForms;
1097
+ case 'scroll': return perms.scroll;
1098
+ case 'navigate': return perms.navigate;
1099
+ case 'api': return perms.apiAccess;
1100
+ case 'read': return perms.readContent;
1101
+ case 'extract': return perms.extractData;
1102
+ default: return perms.click;
1103
+ }
1104
+ }
1105
+
1106
+ // ── Core: Execute Action ────────────────────────────────────────────
1107
+ async execute(actionName, params = {}) {
1108
+ // Security: check if bridge is locked
1109
+ if (this.security.isLocked) {
1110
+ return { success: false, error: 'Bridge locked due to security violations' };
1111
+ }
1112
+
1113
+ if (!this.rateLimiter.check()) {
1114
+ const error = { success: false, error: 'Rate limit exceeded', retryAfter: 60 };
1115
+ this.logger.log('rate_limit', { action: actionName });
1116
+ this.security.audit('rate_limit', { action: actionName }, 'blocked');
1117
+ this.events.emit('error', error);
1118
+ return error;
1119
+ }
1120
+
1121
+ const action = this.registry.get(actionName);
1122
+ if (!action) {
1123
+ return { success: false, error: `Action "${actionName}" not found`, available: this.registry.list().map(a => a.name) };
1124
+ }
1125
+
1126
+ if (!this._checkPermission(action.trigger)) {
1127
+ this.security.audit('permission_denied', { action: actionName, trigger: action.trigger }, 'blocked');
1128
+ return { success: false, error: `Permission denied for trigger type: ${action.trigger}`, tier: this.getEffectiveTier() };
1129
+ }
1130
+
1131
+ if (action.requiresAuth && !this.authenticated) {
1132
+ return { success: false, error: 'Authentication required for this action' };
1133
+ }
1134
+
1135
+ const loginRequired = this.config.restrictions.requireLoginForActions || [];
1136
+ if (loginRequired.includes(action.trigger) && !this.authenticated) {
1137
+ return { success: false, error: 'Login required for this action type' };
1138
+ }
1139
+
1140
+ // Self-healing: resolve selector if original is broken
1141
+ if (action.selector) {
1142
+ const resolved = this.healer.resolve(actionName, action.selector);
1143
+ if (resolved && resolved.healed) {
1144
+ action.selector = resolved.selector;
1145
+ this.logger.log('self_heal', { action: actionName, strategy: resolved.strategy, confidence: resolved.confidence }, 'detailed');
1146
+ this.events.emit('selector:healed', { action: actionName, strategy: resolved.strategy });
1147
+ }
1148
+ }
1149
+
1150
+ this.logger.log('execute', { action: actionName, params }, 'basic');
1151
+ this.security.audit('execute', { action: actionName, agentId: this.agentInfo?.key });
1152
+ this.events.emit('action:before', { action: actionName, params });
1153
+
1154
+ try {
1155
+ let result;
1156
+
1157
+ if (action.handler) {
1158
+ result = await action.handler(params);
1159
+ } else {
1160
+ switch (action.trigger) {
1161
+ case 'click':
1162
+ result = await this._executeClick(action);
1163
+ break;
1164
+ case 'fill_and_submit':
1165
+ result = await this._executeFillAndSubmit(action, params);
1166
+ break;
1167
+ case 'scroll':
1168
+ result = await this._executeScroll(action);
1169
+ break;
1170
+ case 'api':
1171
+ result = await this._executeApi(action, params);
1172
+ break;
1173
+ default:
1174
+ result = { success: false, error: `Unknown trigger: ${action.trigger}` };
1175
+ }
1176
+ }
1177
+
1178
+ this.events.emit('action:after', { action: actionName, result });
1179
+ this.logger.log('execute_result', { action: actionName, success: result.success }, 'detailed');
1180
+ this._maybeReportUsage(actionName, action.trigger, !!(result && result.success !== false));
1181
+ return result;
1182
+
1183
+ } catch (err) {
1184
+ const error = { success: false, error: err.message };
1185
+ this.events.emit('error', { action: actionName, error: err.message });
1186
+ this.logger.log('execute_error', { action: actionName, error: err.message });
1187
+ this._maybeReportUsage(actionName, action.trigger, false);
1188
+ return error;
1189
+ }
1190
+ }
1191
+
1192
+ async _executeClick(action) {
1193
+ if (!action.selector) return { success: false, error: 'No selector defined' };
1194
+ if (!isElementAllowed(action.selector, this.config)) {
1195
+ return { success: false, error: 'Element is blocked by restrictions' };
1196
+ }
1197
+
1198
+ // Self-healing: try to find element even if selector is stale
1199
+ let el = safeQuerySelector(action.selector);
1200
+ if (!el) {
1201
+ const resolved = this.healer.resolve(action.name, action.selector);
1202
+ if (resolved) {
1203
+ el = resolved.element;
1204
+ } else {
1205
+ return { success: false, error: `Element not found: ${action.selector}` };
1206
+ }
1207
+ }
1208
+
1209
+ // Stealth: use human-like click simulation
1210
+ if (this.stealth.isEnabled) {
1211
+ await this.stealth.delay(100, 400);
1212
+ await this.stealth.simulateClick(el);
1213
+ } else {
1214
+ el.click();
1215
+ }
1216
+
1217
+ return { success: true, action: 'click', selector: action.selector };
1218
+ }
1219
+
1220
+ async _executeFillAndSubmit(action, params) {
1221
+ if (!action.fields) return { success: false, error: 'No fields defined' };
1222
+
1223
+ const results = [];
1224
+ for (const field of action.fields) {
1225
+ if (!isElementAllowed(field.selector, this.config)) {
1226
+ results.push({ field: field.name, success: false, error: 'Element blocked' });
1227
+ continue;
1228
+ }
1229
+
1230
+ let el = safeQuerySelector(field.selector);
1231
+ if (!el) {
1232
+ // Self-healing: try to find field by name/placeholder
1233
+ const healedField = this.healer.resolve(`field_${field.name}`, field.selector);
1234
+ if (healedField) {
1235
+ el = healedField.element;
1236
+ } else {
1237
+ results.push({ field: field.name, success: false, error: 'Element not found' });
1238
+ continue;
1239
+ }
1240
+ }
1241
+
1242
+ const value = params[field.name];
1243
+ if (value !== undefined) {
1244
+ if (this.stealth.isEnabled) {
1245
+ await this.stealth.delay(200, 600);
1246
+ await this.stealth.simulateTyping(el, String(value));
1247
+ } else {
1248
+ el.focus();
1249
+ el.value = value;
1250
+ el.dispatchEvent(new Event('input', { bubbles: true }));
1251
+ el.dispatchEvent(new Event('change', { bubbles: true }));
1252
+ }
1253
+ results.push({ field: field.name, success: true });
1254
+ } else if (field.required !== false) {
1255
+ results.push({ field: field.name, success: false, error: 'Value required but not provided' });
1256
+ }
1257
+ }
1258
+
1259
+ if (action.submitSelector) {
1260
+ let submitEl = safeQuerySelector(action.submitSelector);
1261
+ if (submitEl) {
1262
+ if (this.stealth.isEnabled) {
1263
+ await this.stealth.delay(300, 800);
1264
+ await this.stealth.simulateClick(submitEl);
1265
+ } else {
1266
+ submitEl.click();
1267
+ }
1268
+ results.push({ field: '_submit', success: true });
1269
+ }
1270
+ }
1271
+
1272
+ return { success: results.every(r => r.success), results };
1273
+ }
1274
+
1275
+ async _executeScroll(action) {
1276
+ if (action.selector) {
1277
+ const el = safeQuerySelector(action.selector);
1278
+ if (el) {
1279
+ if (this.stealth.isEnabled) {
1280
+ await this.stealth.simulateScroll(el);
1281
+ } else {
1282
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
1283
+ }
1284
+ return { success: true, action: 'scroll', selector: action.selector };
1285
+ }
1286
+ return { success: false, error: `Element not found: ${action.selector}` };
1287
+ }
1288
+ if (this.stealth.isEnabled) {
1289
+ await this.stealth.simulateScroll(null, 'down');
1290
+ } else {
1291
+ window.scrollBy({ top: 500, behavior: 'smooth' });
1292
+ }
1293
+ return { success: true, action: 'scroll', direction: 'down' };
1294
+ }
1295
+
1296
+ async _executeApi(action, params) {
1297
+ if (this.getEffectiveTier() === 'free') {
1298
+ return { success: false, error: 'API access requires a premium subscription' };
1299
+ }
1300
+
1301
+ const url = new URL(action.endpoint, location.origin);
1302
+ const options = { method: action.method || 'GET', headers: { 'Content-Type': 'application/json' } };
1303
+
1304
+ if (action.method !== 'GET' && params) {
1305
+ options.body = JSON.stringify(params);
1306
+ }
1307
+
1308
+ const res = await fetch(url.toString(), options);
1309
+ const data = await res.json().catch(() => null);
1310
+ return { success: res.ok, status: res.status, data };
1311
+ }
1312
+
1313
+ // ── Content Reading ─────────────────────────────────────────────────
1314
+ readContent(selector) {
1315
+ if (!this._checkPermission('read')) {
1316
+ return { success: false, error: 'readContent permission denied' };
1317
+ }
1318
+
1319
+ if (!isElementAllowed(selector, this.config)) {
1320
+ return { success: false, error: 'Element is blocked by restrictions' };
1321
+ }
1322
+
1323
+ const el = safeQuerySelector(selector);
1324
+ if (!el) return { success: false, error: 'Element not found' };
1325
+
1326
+ return {
1327
+ success: true,
1328
+ text: el.textContent.trim(),
1329
+ html: el.innerHTML,
1330
+ attributes: Object.fromEntries(
1331
+ Array.from(el.attributes).map(a => [a.name, a.value])
1332
+ )
1333
+ };
1334
+ }
1335
+
1336
+ getPageInfo() {
1337
+ return {
1338
+ title: document.title,
1339
+ url: location.href,
1340
+ domain: location.hostname,
1341
+ lang: document.documentElement.lang || 'unknown',
1342
+ bridgeVersion: VERSION,
1343
+ tier: this.getEffectiveTier(),
1344
+ permissions: this._getEffectivePermissions(),
1345
+ actionsCount: this.registry.actions.size,
1346
+ rateLimitRemaining: this.rateLimiter.remaining,
1347
+ security: {
1348
+ sandboxActive: true,
1349
+ locked: this.security.isLocked,
1350
+ sessionToken: this.security.sessionToken
1351
+ },
1352
+ selfHealing: this.healer.getStats(),
1353
+ stealthMode: this.stealth.isEnabled
1354
+ };
1355
+ }
1356
+
1357
+ // ── Custom Action Registration ──────────────────────────────────────
1358
+ registerAction(actionDef) {
1359
+ this.registry.register(actionDef);
1360
+ this.events.emit('action:registered', { name: actionDef.name });
1361
+ this.logger.log('register_action', { name: actionDef.name });
1362
+ }
1363
+
1364
+ unregisterAction(name) {
1365
+ this.registry.unregister(name);
1366
+ this.events.emit('action:unregistered', { name });
1367
+ }
1368
+
1369
+ // ── Discovery / Info ────────────────────────────────────────────────
1370
+ getActions(category) {
1371
+ if (category) return this.registry.getByCategory(category);
1372
+ return this.registry.list();
1373
+ }
1374
+
1375
+ getAction(name) {
1376
+ return this.registry.get(name);
1377
+ }
1378
+
1379
+ // ── Waiting Utilities for Agents ────────────────────────────────────
1380
+ waitForElement(selector, timeout = 10000) {
1381
+ return new Promise((resolve, reject) => {
1382
+ const el = safeQuerySelector(selector);
1383
+ if (el) return resolve(el);
1384
+
1385
+ const observer = new MutationObserver(() => {
1386
+ const found = safeQuerySelector(selector);
1387
+ if (found) {
1388
+ observer.disconnect();
1389
+ clearTimeout(timer);
1390
+ resolve(found);
1391
+ }
1392
+ });
1393
+
1394
+ observer.observe(document.body, { childList: true, subtree: true });
1395
+
1396
+ const timer = setTimeout(() => {
1397
+ observer.disconnect();
1398
+ reject(new Error(`Timeout: element "${selector}" not found within ${timeout}ms`));
1399
+ }, timeout);
1400
+ });
1401
+ }
1402
+
1403
+ waitForNavigation(timeout = 15000) {
1404
+ return new Promise((resolve) => {
1405
+ const start = location.href;
1406
+ const check = setInterval(() => {
1407
+ if (location.href !== start) {
1408
+ clearInterval(check);
1409
+ clearTimeout(timer);
1410
+ resolve({ from: start, to: location.href });
1411
+ }
1412
+ }, 200);
1413
+ const timer = setTimeout(() => {
1414
+ clearInterval(check);
1415
+ resolve({ from: start, to: start, timedOut: true });
1416
+ }, timeout);
1417
+ });
1418
+ }
1419
+
1420
+ // ── Lifecycle ───────────────────────────────────────────────────────
1421
+ refresh() {
1422
+ this.registry = new ActionRegistry();
1423
+ this._autoDiscoverActions();
1424
+ this._storeFingerprints();
1425
+ this.events.emit('refresh');
1426
+ this.logger.log('refresh', {});
1427
+ }
1428
+
1429
+ // ── Agent Mesh Protocol (Client-Side) ───────────────────────────────
1430
+
1431
+ async _meshPost(path, body) {
1432
+ const base = this._resolveApiBase();
1433
+ const res = await fetch(`${base}/api/mesh${path}`, {
1434
+ method: 'POST',
1435
+ headers: { 'Content-Type': 'application/json' },
1436
+ body: body ? JSON.stringify(body) : undefined
1437
+ });
1438
+ if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.error || res.statusText); }
1439
+ return res.json();
1440
+ }
1441
+
1442
+ async _meshGet(path) {
1443
+ const base = this._resolveApiBase();
1444
+ const res = await fetch(`${base}/api/mesh${path}`);
1445
+ if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.error || res.statusText); }
1446
+ return res.json();
1447
+ }
1448
+
1449
+ async _meshDelete(path) {
1450
+ const base = this._resolveApiBase();
1451
+ const res = await fetch(`${base}/api/mesh${path}`, { method: 'DELETE' });
1452
+ if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.error || res.statusText); }
1453
+ return res.json();
1454
+ }
1455
+
1456
+ async meshJoin(role, displayName, capabilities) {
1457
+ const data = await this._meshPost('/agents', { siteId: this.config.siteId, role, displayName, capabilities });
1458
+ this._meshAgentId = data.agent.id;
1459
+ this._meshHeartbeat = setInterval(() => {
1460
+ this._meshPost(`/agents/${this._meshAgentId}/heartbeat`).catch(() => {});
1461
+ }, 30000);
1462
+ this.events.emit('mesh:joined', data.agent);
1463
+ return data.agent;
1464
+ }
1465
+
1466
+ async meshLeave() {
1467
+ if (this._meshHeartbeat) { clearInterval(this._meshHeartbeat); this._meshHeartbeat = null; }
1468
+ if (this._meshAgentId) {
1469
+ await this._meshDelete(`/agents/${this._meshAgentId}`).catch(() => {});
1470
+ this._meshAgentId = null;
1471
+ }
1472
+ }
1473
+
1474
+ async meshPublish(channel, messageType, subject, payload, opts) {
1475
+ if (!this._meshAgentId) throw new Error('Must call meshJoin() first');
1476
+ return (await this._meshPost('/messages', {
1477
+ channelName: channel || 'general', senderId: this._meshAgentId,
1478
+ targetId: opts?.targetId, type: messageType, subject, payload,
1479
+ priority: opts?.priority, ttl: opts?.ttl
1480
+ })).message;
1481
+ }
1482
+
1483
+ async meshReceive(limit) {
1484
+ if (!this._meshAgentId) throw new Error('Must call meshJoin() first');
1485
+ return (await this._meshGet(`/messages?agentId=${this._meshAgentId}&limit=${limit || 50}`)).messages;
1486
+ }
1487
+
1488
+ async meshAcknowledge(messageId) {
1489
+ return this._meshPost(`/messages/${encodeURIComponent(messageId)}/acknowledge`);
1490
+ }
1491
+
1492
+ async meshUnread() {
1493
+ if (!this._meshAgentId) throw new Error('Must call meshJoin() first');
1494
+ return this._meshGet(`/agents/${this._meshAgentId}/unread`);
1495
+ }
1496
+
1497
+ async meshShareKnowledge(type, key, value, opts) {
1498
+ if (!this._meshAgentId) throw new Error('Must call meshJoin() first');
1499
+ return (await this._meshPost('/knowledge', {
1500
+ agentId: this._meshAgentId, type, domain: opts?.domain,
1501
+ key, value, confidence: opts?.confidence, source: opts?.source
1502
+ })).knowledge;
1503
+ }
1504
+
1505
+ async meshQueryKnowledge(params) {
1506
+ const qs = new URLSearchParams(params || {}).toString();
1507
+ return (await this._meshGet(`/knowledge?${qs}`)).knowledge;
1508
+ }
1509
+
1510
+ async meshSearchKnowledge(query, limit) {
1511
+ return (await this._meshGet(`/knowledge/search/${encodeURIComponent(query)}?limit=${limit || 20}`)).knowledge;
1512
+ }
1513
+
1514
+ async meshAlert(subject, details, priority) {
1515
+ if (!this._meshAgentId) throw new Error('Must call meshJoin() first');
1516
+ return (await this._meshPost('/alert', { senderId: this._meshAgentId, subject, details, priority })).message;
1517
+ }
1518
+
1519
+ async meshCreateVote(subject, options, deadlineSeconds) {
1520
+ if (!this._meshAgentId) throw new Error('Must call meshJoin() first');
1521
+ return (await this._meshPost('/votes', { senderId: this._meshAgentId, subject, options, deadlineSeconds })).vote;
1522
+ }
1523
+
1524
+ async meshCastVote(voteMessageId, choice, weight, reason) {
1525
+ if (!this._meshAgentId) throw new Error('Must call meshJoin() first');
1526
+ return (await this._meshPost(`/votes/${encodeURIComponent(voteMessageId)}/cast`, {
1527
+ voterId: this._meshAgentId, choice, weight, reason
1528
+ })).result;
1529
+ }
1530
+
1531
+ async meshTallyVote(voteMessageId) {
1532
+ return (await this._meshGet(`/votes/${encodeURIComponent(voteMessageId)}/tally`)).tally;
1533
+ }
1534
+
1535
+ async symphonyPerform(template, inputData, schema) {
1536
+ const data = await this._meshPost('/symphony/compose', {
1537
+ siteId: this.config.siteId, template, inputData, schema
1538
+ });
1539
+ this.events.emit('symphony:completed', data);
1540
+ return data;
1541
+ }
1542
+
1543
+ async learnRecord(domain, action, context, features) {
1544
+ if (!this._meshAgentId) throw new Error('Must call meshJoin() first');
1545
+ return this._meshPost('/learning/decisions', {
1546
+ siteId: this.config.siteId, agentId: this._meshAgentId, domain, action, context, features
1547
+ });
1548
+ }
1549
+
1550
+ async learnFeedback(decisionId, outcome, reward) {
1551
+ return this._meshPost('/learning/feedback', { decisionId, outcome, reward });
1552
+ }
1553
+
1554
+ async learnRecommend(domain, actions, context) {
1555
+ if (!this._meshAgentId) throw new Error('Must call meshJoin() first');
1556
+ return this._meshPost('/learning/recommend', {
1557
+ siteId: this.config.siteId, agentId: this._meshAgentId, domain, actions, context
1558
+ });
1559
+ }
1560
+
1561
+ // ── Commander Agent Protocol ────────────────────────────────────────
1562
+
1563
+ async _cmdPost(path, body) {
1564
+ const base = this.config.serverUrl || '';
1565
+ const res = await fetch(`${base}/api/commander${path}`, {
1566
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1567
+ body: JSON.stringify(body)
1568
+ });
1569
+ if (!res.ok) throw new Error(`Commander POST ${path} failed: ${res.status}`);
1570
+ return res.json();
1571
+ }
1572
+
1573
+ async _cmdGet(path) {
1574
+ const base = this.config.serverUrl || '';
1575
+ const res = await fetch(`${base}/api/commander${path}`);
1576
+ if (!res.ok) throw new Error(`Commander GET ${path} failed: ${res.status}`);
1577
+ return res.json();
1578
+ }
1579
+
1580
+ /** Launch a mission — decompose a goal and execute it. */
1581
+ async commanderLaunch(goal, options) {
1582
+ const data = await this._cmdPost('/missions/launch', {
1583
+ siteId: this.config.siteId, goal,
1584
+ title: options?.title || goal.substring(0, 80),
1585
+ strategy: options?.strategy,
1586
+ priority: options?.priority, context: options?.context
1587
+ });
1588
+ this.events.emit('commander:mission', data.mission);
1589
+ return data.mission;
1590
+ }
1591
+
1592
+ /** Get commander + edge + local AI stats. */
1593
+ async commanderStats() {
1594
+ return this._cmdGet(`/stats?siteId=${encodeURIComponent(this.config.siteId || 'default')}`);
1595
+ }
1596
+
1597
+ /** Register an edge computing node. */
1598
+ async edgeRegisterNode(hostname, hardware, capabilities) {
1599
+ return this._cmdPost('/edge/nodes', {
1600
+ siteId: this.config.siteId, hostname, hardware, capabilities
1601
+ });
1602
+ }
1603
+
1604
+ /** Submit a task to the edge computing queue. */
1605
+ async edgeSubmitTask(taskType, payload, options) {
1606
+ return this._cmdPost('/edge/tasks', { taskType, payload, ...options });
1607
+ }
1608
+
1609
+ /** Discover local AI models (Ollama, llama.cpp, etc.). */
1610
+ async localAIDiscover(customEndpoints) {
1611
+ return this._cmdPost('/local-ai/discover', {
1612
+ siteId: this.config.siteId, customEndpoints
1613
+ });
1614
+ }
1615
+
1616
+ /** Run inference on a local AI model. */
1617
+ async localAIInfer(prompt, options) {
1618
+ return this._cmdPost('/local-ai/infer', {
1619
+ siteId: this.config.siteId, prompt, ...options
1620
+ });
1621
+ }
1622
+
1623
+ destroy() {
1624
+ this.events.emit('destroy');
1625
+ if (this._meshHeartbeat) { clearInterval(this._meshHeartbeat); this._meshHeartbeat = null; }
1626
+ this._meshAgentId = null;
1627
+ if (this._mutationObserver) {
1628
+ this._mutationObserver.disconnect();
1629
+ this._mutationObserver = null;
1630
+ }
1631
+ this.registry = new ActionRegistry();
1632
+ this.logger.clear();
1633
+ delete global.AICommands;
1634
+ delete global.WebAgentBridge;
1635
+ delete global.__wab_bidi;
1636
+ }
1637
+
1638
+ // ── Serialization ───────────────────────────────────────────────────
1639
+ toJSON() {
1640
+ return {
1641
+ version: VERSION,
1642
+ page: this.getPageInfo(),
1643
+ actions: this.getActions()
1644
+ };
1645
+ }
1646
+
1647
+ // ── WebDriver BiDi Compatibility ────────────────────────────────────
1648
+ // Exposes a standardized protocol for agents using WebDriver BiDi
1649
+ toBiDi() {
1650
+ return {
1651
+ type: 'wab:context',
1652
+ version: VERSION,
1653
+ context: {
1654
+ url: location.href,
1655
+ title: document.title,
1656
+ browsingContext: typeof window !== 'undefined' ? window.name || 'default' : 'default'
1657
+ },
1658
+ capabilities: {
1659
+ actions: this.getActions().map(a => ({
1660
+ id: a.name,
1661
+ type: a.trigger === 'click' ? 'pointerDown' : a.trigger === 'fill_and_submit' ? 'key' : a.trigger,
1662
+ description: a.description,
1663
+ parameters: a.fields ? a.fields.map(f => ({ name: f.name, type: f.type, required: f.required })) : undefined
1664
+ })),
1665
+ permissions: this._getEffectivePermissions(),
1666
+ tier: this.getEffectiveTier()
1667
+ }
1668
+ };
1669
+ }
1670
+
1671
+ // Execute via BiDi-style command
1672
+ async executeBiDi(command) {
1673
+ // Security: validate command
1674
+ const validation = this.security.validateCommand(command || {});
1675
+ if (!validation.valid) {
1676
+ return { id: command?.id, error: { code: 'security error', message: validation.error } };
1677
+ }
1678
+
1679
+ if (!command || !command.method) {
1680
+ return { id: command?.id, error: { code: 'invalid argument', message: 'Missing method' } };
1681
+ }
1682
+
1683
+ this.security.audit('bidi_command', { method: command.method });
1684
+ const responseBase = { id: command.id || null, type: 'success' };
1685
+
1686
+ switch (command.method) {
1687
+ case 'wab.getContext':
1688
+ return { ...responseBase, result: this.toBiDi() };
1689
+
1690
+ case 'wab.getActions':
1691
+ return { ...responseBase, result: this.getActions(command.params?.category) };
1692
+
1693
+ case 'wab.executeAction':
1694
+ if (!command.params?.name) {
1695
+ return { id: command.id, error: { code: 'invalid argument', message: 'Action name required' } };
1696
+ }
1697
+ const result = await this.execute(command.params.name, command.params.data || {});
1698
+ return { ...responseBase, result };
1699
+
1700
+ case 'wab.readContent':
1701
+ if (!command.params?.selector) {
1702
+ return { id: command.id, error: { code: 'invalid argument', message: 'Selector required' } };
1703
+ }
1704
+ return { ...responseBase, result: this.readContent(command.params.selector) };
1705
+
1706
+ case 'wab.getPageInfo':
1707
+ return { ...responseBase, result: this.getPageInfo() };
1708
+
1709
+ default:
1710
+ return { id: command.id, error: { code: 'unknown command', message: `Unknown method: ${command.method}` } };
1711
+ }
1712
+ }
1713
+ }
1714
+
1715
+ // ─── Auto-initialize ──────────────────────────────────────────────────
1716
+ function autoInit() {
1717
+ const config = global.AIBridgeConfig || {};
1718
+
1719
+ const scriptTag = document.currentScript || document.querySelector('script[data-wab-config]');
1720
+ if (scriptTag) {
1721
+ const dataConfig = scriptTag.getAttribute('data-config') || scriptTag.getAttribute('data-wab-config');
1722
+ if (dataConfig) {
1723
+ try {
1724
+ Object.assign(config, JSON.parse(dataConfig));
1725
+ } catch (e) {
1726
+ console.error('[WAB] Invalid data-config JSON:', e);
1727
+ }
1728
+ }
1729
+ }
1730
+
1731
+ const bridge = new WebAgentBridge(config);
1732
+
1733
+ global.AICommands = bridge;
1734
+ global.WebAgentBridge = WebAgentBridge;
1735
+
1736
+ // WebDriver BiDi compatibility: expose via __wab_bidi channel
1737
+ global.__wab_bidi = {
1738
+ version: VERSION,
1739
+ send: async (command) => bridge.executeBiDi(command),
1740
+ getContext: () => bridge.toBiDi()
1741
+ };
1742
+
1743
+ if (typeof CustomEvent !== 'undefined') {
1744
+ document.dispatchEvent(new CustomEvent('wab:ready', { detail: { version: VERSION } }));
1745
+ }
1746
+ }
1747
+
1748
+ if (document.readyState === 'loading') {
1749
+ document.addEventListener('DOMContentLoaded', autoInit);
1750
+ } else {
1751
+ autoInit();
1752
+ }
1753
+
1754
+ })(typeof window !== 'undefined' ? window : globalThis);