web-agent-bridge 3.0.0 → 3.3.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 (202) hide show
  1. package/LICENSE +72 -21
  2. package/README.ar.md +1286 -1073
  3. package/README.md +1764 -1535
  4. package/bin/agent-runner.js +474 -474
  5. package/bin/cli.js +237 -138
  6. package/bin/wab.js +80 -80
  7. package/examples/bidi-agent.js +119 -119
  8. package/examples/cross-site-agent.js +91 -91
  9. package/examples/mcp-agent.js +94 -94
  10. package/examples/next-app-router/README.md +44 -44
  11. package/examples/puppeteer-agent.js +108 -108
  12. package/examples/saas-dashboard/README.md +55 -55
  13. package/examples/shopify-hydrogen/README.md +74 -74
  14. package/examples/vision-agent.js +171 -171
  15. package/examples/wordpress-elementor/README.md +77 -77
  16. package/package.json +17 -3
  17. package/public/.well-known/agent-tools.json +180 -180
  18. package/public/.well-known/ai-assets.json +59 -59
  19. package/public/.well-known/ai-plugin.json +28 -0
  20. package/public/.well-known/security.txt +8 -0
  21. package/public/agent-workspace.html +349 -347
  22. package/public/ai.html +198 -196
  23. package/public/api.html +413 -0
  24. package/public/browser.html +486 -484
  25. package/public/commander-dashboard.html +243 -243
  26. package/public/cookies.html +210 -208
  27. package/public/css/agent-workspace.css +1713 -1713
  28. package/public/css/premium.css +317 -317
  29. package/public/css/styles.css +1235 -1235
  30. package/public/dashboard.html +706 -704
  31. package/public/demo.html +1770 -1
  32. package/public/dns.html +507 -0
  33. package/public/docs.html +587 -585
  34. package/public/feed.xml +89 -89
  35. package/public/growth.html +463 -0
  36. package/public/index.html +341 -9
  37. package/public/integrations.html +556 -0
  38. package/public/js/agent-workspace.js +1740 -1740
  39. package/public/js/auth-nav.js +31 -31
  40. package/public/js/auth-redirect.js +12 -12
  41. package/public/js/cookie-consent.js +56 -56
  42. package/public/js/wab-demo-page.js +721 -721
  43. package/public/js/ws-client.js +74 -74
  44. package/public/llms-full.txt +360 -309
  45. package/public/llms.txt +125 -86
  46. package/public/login.html +85 -83
  47. package/public/mesh-dashboard.html +328 -328
  48. package/public/openapi.json +580 -580
  49. package/public/phone-shield.html +281 -0
  50. package/public/premium-dashboard.html +2489 -2487
  51. package/public/premium.html +793 -791
  52. package/public/privacy.html +297 -295
  53. package/public/register.html +105 -103
  54. package/public/robots.txt +87 -87
  55. package/public/script/wab-consent.d.ts +36 -36
  56. package/public/script/wab-consent.js +104 -104
  57. package/public/script/wab-schema.js +131 -131
  58. package/public/script/wab.d.ts +108 -108
  59. package/public/script/wab.min.js +580 -580
  60. package/public/security.txt +8 -0
  61. package/public/terms.html +256 -254
  62. package/script/ai-agent-bridge.js +1754 -1754
  63. package/sdk/README.md +99 -99
  64. package/sdk/agent-mesh.js +449 -449
  65. package/sdk/commander.js +262 -262
  66. package/sdk/index.d.ts +464 -464
  67. package/sdk/index.js +18 -1
  68. package/sdk/multi-agent.js +318 -318
  69. package/sdk/package.json +12 -1
  70. package/sdk/safety-shield.js +219 -0
  71. package/sdk/schema-discovery.js +83 -83
  72. package/server/adapters/index.js +520 -520
  73. package/server/config/plans.js +367 -367
  74. package/server/config/secrets.js +102 -102
  75. package/server/control-plane/index.js +301 -301
  76. package/server/data-plane/index.js +354 -354
  77. package/server/index.js +175 -19
  78. package/server/llm/index.js +404 -404
  79. package/server/middleware/adminAuth.js +35 -35
  80. package/server/middleware/auth.js +50 -50
  81. package/server/middleware/featureGate.js +88 -88
  82. package/server/middleware/rateLimits.js +100 -100
  83. package/server/middleware/sensitiveAction.js +157 -0
  84. package/server/migrations/001_add_analytics_indexes.sql +7 -7
  85. package/server/migrations/002_premium_features.sql +418 -418
  86. package/server/migrations/003_ads_integer_cents.sql +33 -33
  87. package/server/migrations/004_agent_os.sql +158 -158
  88. package/server/migrations/005_marketplace_metering.sql +126 -126
  89. package/server/models/adapters/index.js +33 -33
  90. package/server/models/adapters/mysql.js +183 -183
  91. package/server/models/adapters/postgresql.js +172 -172
  92. package/server/models/adapters/sqlite.js +7 -7
  93. package/server/models/db.js +681 -681
  94. package/server/observability/failure-analysis.js +337 -337
  95. package/server/observability/index.js +394 -394
  96. package/server/protocol/capabilities.js +223 -223
  97. package/server/protocol/index.js +243 -243
  98. package/server/protocol/schema.js +584 -584
  99. package/server/registry/certification.js +271 -271
  100. package/server/registry/index.js +326 -326
  101. package/server/routes/admin-premium.js +671 -671
  102. package/server/routes/admin.js +261 -261
  103. package/server/routes/ads.js +130 -130
  104. package/server/routes/agent-workspace.js +540 -378
  105. package/server/routes/api.js +150 -150
  106. package/server/routes/auth.js +71 -71
  107. package/server/routes/billing.js +45 -45
  108. package/server/routes/commander.js +316 -316
  109. package/server/routes/demo-showcase.js +332 -0
  110. package/server/routes/demo-store.js +154 -0
  111. package/server/routes/discovery.js +417 -406
  112. package/server/routes/gateway.js +173 -0
  113. package/server/routes/license.js +251 -240
  114. package/server/routes/mesh.js +469 -469
  115. package/server/routes/noscript.js +543 -543
  116. package/server/routes/premium-v2.js +686 -686
  117. package/server/routes/premium.js +724 -724
  118. package/server/routes/runtime.js +2148 -2147
  119. package/server/routes/sovereign.js +465 -385
  120. package/server/routes/universal.js +200 -177
  121. package/server/routes/wab-api.js +850 -491
  122. package/server/runtime/container-worker.js +111 -111
  123. package/server/runtime/container.js +448 -448
  124. package/server/runtime/distributed-worker.js +362 -362
  125. package/server/runtime/event-bus.js +210 -210
  126. package/server/runtime/index.js +253 -253
  127. package/server/runtime/queue.js +599 -599
  128. package/server/runtime/replay.js +666 -666
  129. package/server/runtime/sandbox.js +266 -266
  130. package/server/runtime/scheduler.js +534 -534
  131. package/server/runtime/session-engine.js +293 -293
  132. package/server/runtime/state-manager.js +188 -188
  133. package/server/security/cross-site-redactor.js +196 -0
  134. package/server/security/dry-run.js +180 -0
  135. package/server/security/human-gate-rate-limit.js +147 -0
  136. package/server/security/human-gate-transports.js +178 -0
  137. package/server/security/human-gate.js +281 -0
  138. package/server/security/index.js +368 -368
  139. package/server/security/intent-engine.js +245 -0
  140. package/server/security/reward-guard.js +171 -0
  141. package/server/security/rollback-store.js +239 -0
  142. package/server/security/token-scope.js +404 -0
  143. package/server/security/url-policy.js +139 -0
  144. package/server/services/agent-chat.js +506 -506
  145. package/server/services/agent-learning.js +601 -575
  146. package/server/services/agent-memory.js +625 -625
  147. package/server/services/agent-mesh.js +555 -539
  148. package/server/services/agent-symphony.js +717 -717
  149. package/server/services/agent-tasks.js +1807 -1807
  150. package/server/services/api-key-engine.js +292 -0
  151. package/server/services/cluster.js +894 -894
  152. package/server/services/commander.js +738 -738
  153. package/server/services/edge-compute.js +440 -440
  154. package/server/services/email.js +204 -204
  155. package/server/services/hosted-runtime.js +205 -205
  156. package/server/services/lfd.js +635 -616
  157. package/server/services/local-ai.js +389 -389
  158. package/server/services/marketplace.js +270 -270
  159. package/server/services/metering.js +182 -182
  160. package/server/services/modules/affiliate-intelligence.js +93 -0
  161. package/server/services/modules/agent-firewall.js +90 -0
  162. package/server/services/modules/bounty.js +89 -0
  163. package/server/services/modules/collective-bargaining.js +92 -0
  164. package/server/services/modules/dark-pattern.js +66 -0
  165. package/server/services/modules/gov-intelligence.js +45 -0
  166. package/server/services/modules/neural.js +55 -0
  167. package/server/services/modules/notary.js +49 -0
  168. package/server/services/modules/price-time-machine.js +86 -0
  169. package/server/services/modules/protocol.js +104 -0
  170. package/server/services/negotiation.js +439 -439
  171. package/server/services/plugins.js +771 -771
  172. package/server/services/premium.js +1 -1
  173. package/server/services/price-intelligence.js +566 -565
  174. package/server/services/price-shield.js +1137 -1137
  175. package/server/services/reputation.js +465 -465
  176. package/server/services/search-engine.js +357 -357
  177. package/server/services/security.js +513 -513
  178. package/server/services/self-healing.js +843 -843
  179. package/server/services/sovereign-shield.js +542 -0
  180. package/server/services/stripe.js +192 -192
  181. package/server/services/swarm.js +788 -788
  182. package/server/services/universal-scraper.js +662 -661
  183. package/server/services/verification.js +481 -481
  184. package/server/services/vision.js +1163 -1163
  185. package/server/utils/cache.js +125 -125
  186. package/server/utils/migrate.js +81 -81
  187. package/server/utils/safe-fetch.js +228 -0
  188. package/server/utils/secureFields.js +50 -50
  189. package/server/ws.js +161 -161
  190. package/templates/artisan-marketplace.yaml +104 -104
  191. package/templates/book-price-scout.yaml +98 -98
  192. package/templates/electronics-price-tracker.yaml +108 -108
  193. package/templates/flight-deal-hunter.yaml +113 -113
  194. package/templates/freelancer-direct.yaml +116 -116
  195. package/templates/grocery-price-compare.yaml +93 -93
  196. package/templates/hotel-direct-booking.yaml +113 -113
  197. package/templates/local-services.yaml +98 -98
  198. package/templates/olive-oil-tunisia.yaml +88 -88
  199. package/templates/organic-farm-fresh.yaml +101 -101
  200. package/templates/restaurant-direct.yaml +97 -97
  201. package/server/services/fairness-engine.js +0 -409
  202. package/server/services/fairness.js +0 -420
@@ -1,97 +1,97 @@
1
- # WAB Agent Template — Local Restaurant Finder
2
- # Finds restaurants directly without Uber Eats / DoorDash
3
- # Run: npx wab-agent run restaurant-direct.yaml --cuisine "italian" --city "Rome"
4
-
5
- name: restaurant-direct
6
- version: 1.0.0
7
- description: Discover and order from local restaurants directly, saving delivery app commissions
8
- author: WAB Community
9
- tags: [food, restaurant, delivery, local, anti-monopoly]
10
-
11
- goal: >
12
- Find WAB-enabled restaurants, browse menus with verified prices,
13
- negotiate group/regular customer discounts, and order directly.
14
-
15
- target_sites:
16
- discovery_method: wab-registry
17
- category: restaurants
18
- region: auto
19
- fallback_urls: []
20
-
21
- parameters:
22
- - name: cuisine
23
- type: string
24
- description: Cuisine type (e.g. italian, japanese, moroccan)
25
- - name: city
26
- type: string
27
- required: true
28
- - name: delivery
29
- type: boolean
30
- default: true
31
- - name: max_price
32
- type: number
33
- default: 50
34
-
35
- actions:
36
- - name: discover
37
- description: Find restaurants in the area
38
- wab_action: discover
39
-
40
- - name: get_menu
41
- description: Get restaurant menu
42
- wab_action: getMenu
43
- params:
44
- cuisine: "{{cuisine}}"
45
- collect: true
46
-
47
- - name: verify_prices
48
- description: Cross-check menu prices
49
- wab_action: verifyPrice
50
- require_pass: true
51
-
52
- - name: check_reputation
53
- description: Check restaurant WAB reputation
54
- wab_action: getReputation
55
-
56
- - name: negotiate
57
- description: Negotiate regular customer or group order discount
58
- wab_action: negotiate
59
- strategy: repeat_customer
60
- conditions:
61
- proposed_discount: 10
62
- fallback_strategy: first_time
63
-
64
- - name: order
65
- description: Place order directly
66
- wab_action: placeOrder
67
- requires: [verify_prices]
68
-
69
- fairness_rules:
70
- prefer_local: true
71
- prefer_small_business: true
72
- max_price: "{{max_price}}"
73
- currency: auto
74
- min_reputation_score: 30
75
- avoid_monopolies: true
76
-
77
- negotiation:
78
- enabled: true
79
- max_rounds: 2
80
- strategies:
81
- - repeat_customer
82
- - first_time
83
- - bulk_order
84
- walk_away_threshold: 0
85
- pitch: "No Uber Eats commission — pass some savings to me"
86
-
87
- verification:
88
- anti_hallucination: true
89
- cross_check_prices: true
90
- require_vision_match: false
91
- max_price_variance: 0.10
92
-
93
- output:
94
- format: table
95
- include: [restaurant, cuisine, dish, price, rating, reputation, delivery_available]
96
- sort_by: price
97
- limit: 15
1
+ # WAB Agent Template — Local Restaurant Finder
2
+ # Finds restaurants directly without Uber Eats / DoorDash
3
+ # Run: npx wab-agent run restaurant-direct.yaml --cuisine "italian" --city "Rome"
4
+
5
+ name: restaurant-direct
6
+ version: 1.0.0
7
+ description: Discover and order from local restaurants directly, saving delivery app commissions
8
+ author: WAB Community
9
+ tags: [food, restaurant, delivery, local, anti-monopoly]
10
+
11
+ goal: >
12
+ Find WAB-enabled restaurants, browse menus with verified prices,
13
+ negotiate group/regular customer discounts, and order directly.
14
+
15
+ target_sites:
16
+ discovery_method: wab-registry
17
+ category: restaurants
18
+ region: auto
19
+ fallback_urls: []
20
+
21
+ parameters:
22
+ - name: cuisine
23
+ type: string
24
+ description: Cuisine type (e.g. italian, japanese, moroccan)
25
+ - name: city
26
+ type: string
27
+ required: true
28
+ - name: delivery
29
+ type: boolean
30
+ default: true
31
+ - name: max_price
32
+ type: number
33
+ default: 50
34
+
35
+ actions:
36
+ - name: discover
37
+ description: Find restaurants in the area
38
+ wab_action: discover
39
+
40
+ - name: get_menu
41
+ description: Get restaurant menu
42
+ wab_action: getMenu
43
+ params:
44
+ cuisine: "{{cuisine}}"
45
+ collect: true
46
+
47
+ - name: verify_prices
48
+ description: Cross-check menu prices
49
+ wab_action: verifyPrice
50
+ require_pass: true
51
+
52
+ - name: check_reputation
53
+ description: Check restaurant WAB reputation
54
+ wab_action: getReputation
55
+
56
+ - name: negotiate
57
+ description: Negotiate regular customer or group order discount
58
+ wab_action: negotiate
59
+ strategy: repeat_customer
60
+ conditions:
61
+ proposed_discount: 10
62
+ fallback_strategy: first_time
63
+
64
+ - name: order
65
+ description: Place order directly
66
+ wab_action: placeOrder
67
+ requires: [verify_prices]
68
+
69
+ fairness_rules:
70
+ prefer_local: true
71
+ prefer_small_business: true
72
+ max_price: "{{max_price}}"
73
+ currency: auto
74
+ min_reputation_score: 30
75
+ avoid_monopolies: true
76
+
77
+ negotiation:
78
+ enabled: true
79
+ max_rounds: 2
80
+ strategies:
81
+ - repeat_customer
82
+ - first_time
83
+ - bulk_order
84
+ walk_away_threshold: 0
85
+ pitch: "No Uber Eats commission — pass some savings to me"
86
+
87
+ verification:
88
+ anti_hallucination: true
89
+ cross_check_prices: true
90
+ require_vision_match: false
91
+ max_price_variance: 0.10
92
+
93
+ output:
94
+ format: table
95
+ include: [restaurant, cuisine, dish, price, rating, reputation, delivery_available]
96
+ sort_by: price
97
+ limit: 15
@@ -1,409 +0,0 @@
1
- /**
2
- * WAB Fairness Engine
3
- * ═══════════════════════════════════════════════════════════════════
4
- * نظام العدالة — Prevents AI bias toward large platforms.
5
- *
6
- * Core principle: Small trustworthy businesses deserve equal visibility.
7
- * Big platforms have marketing budgets; small businesses have better deals.
8
- *
9
- * Scoring dimensions:
10
- * 1. Size Penalty — big-tech platforms get penalized
11
- * 2. Direct Booking Bonus — booking directly = no commissions = better price
12
- * 3. Trust Attestations — verified by other WAB agents
13
- * 4. Price Honesty — sites that don't use dark patterns score higher
14
- * 5. Local/Independent Bonus — community businesses
15
- * 6. Transparency Score — clear pricing, no hidden fees
16
- */
17
-
18
- const crypto = require('crypto');
19
- const { db, findSiteByDomain } = require('../models/db');
20
-
21
- // ─── WAB Bridge Detection ────────────────────────────────────────────
22
- // Check if a domain has installed the WAB bridge script (cooperative site)
23
-
24
- const _stmtNegotiationRules = db.prepare(
25
- `SELECT COUNT(*) AS cnt FROM negotiation_rules WHERE site_id = ?`
26
- );
27
- const _stmtDirectoryEntry = db.prepare(
28
- `SELECT * FROM wab_directory WHERE site_id = ?`
29
- );
30
-
31
- /**
32
- * Returns bridge details for a domain, or null if not a WAB-enabled site.
33
- * { siteId, tier, hasNegotiation, directoryEntry }
34
- */
35
- function getWabBridgeInfo(domain) {
36
- const d = domain.replace(/^www\./, '').toLowerCase();
37
- const site = findSiteByDomain.get(d);
38
- if (!site) return null;
39
-
40
- let hasNegotiation = false;
41
- try {
42
- const nr = _stmtNegotiationRules.get(site.id);
43
- hasNegotiation = nr && nr.cnt > 0;
44
- } catch (_) {}
45
-
46
- let directoryEntry = null;
47
- try {
48
- directoryEntry = _stmtDirectoryEntry.get(site.id);
49
- } catch (_) {}
50
-
51
- return {
52
- siteId: site.id,
53
- tier: site.tier || 'free',
54
- hasNegotiation,
55
- directoryEntry,
56
- isListed: directoryEntry ? directoryEntry.listed === 1 : false,
57
- neutralityScore: directoryEntry ? directoryEntry.neutrality_score : null,
58
- };
59
- }
60
-
61
- // ─── Schema ──────────────────────────────────────────────────────────
62
-
63
- db.exec(`
64
- CREATE TABLE IF NOT EXISTS fairness_scores (
65
- domain TEXT PRIMARY KEY,
66
- category TEXT DEFAULT 'neutral',
67
- size_score INTEGER DEFAULT 50,
68
- trust_score INTEGER DEFAULT 50,
69
- price_honesty INTEGER DEFAULT 50,
70
- transparency INTEGER DEFAULT 50,
71
- direct_booking INTEGER DEFAULT 0,
72
- total_score INTEGER DEFAULT 50,
73
- attestation_count INTEGER DEFAULT 0,
74
- fraud_count INTEGER DEFAULT 0,
75
- last_checked TEXT DEFAULT (datetime('now')),
76
- updated_at TEXT DEFAULT (datetime('now'))
77
- );
78
-
79
- CREATE TABLE IF NOT EXISTS fairness_overrides (
80
- domain TEXT PRIMARY KEY,
81
- boost INTEGER DEFAULT 0,
82
- reason TEXT,
83
- created_by TEXT DEFAULT 'system',
84
- created_at TEXT DEFAULT (datetime('now'))
85
- );
86
- `);
87
-
88
- const stmts = {
89
- upsertScore: db.prepare(`INSERT OR REPLACE INTO fairness_scores
90
- (domain, category, size_score, trust_score, price_honesty, transparency,
91
- direct_booking, total_score, attestation_count, fraud_count, last_checked, updated_at)
92
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`),
93
- getScore: db.prepare('SELECT * FROM fairness_scores WHERE domain = ?'),
94
- getTopFair: db.prepare('SELECT * FROM fairness_scores ORDER BY total_score DESC LIMIT ?'),
95
- upsertOverride: db.prepare(`INSERT OR REPLACE INTO fairness_overrides
96
- (domain, boost, reason, created_by) VALUES (?, ?, ?, ?)`),
97
- getOverride: db.prepare('SELECT * FROM fairness_overrides WHERE domain = ?'),
98
- };
99
-
100
- // ─── Big Tech / Platform Registry ────────────────────────────────────
101
-
102
- const PLATFORM_REGISTRY = {
103
- // Mega platforms — high commission, opaque pricing, aggressive marketing
104
- 'amazon.com': { size: 'mega', commission: 15, darkPatterns: true, category: 'marketplace' },
105
- 'ebay.com': { size: 'mega', commission: 13, darkPatterns: false, category: 'marketplace' },
106
- 'alibaba.com': { size: 'mega', commission: 8, darkPatterns: true, category: 'marketplace' },
107
- 'aliexpress.com': { size: 'mega', commission: 8, darkPatterns: true, category: 'marketplace' },
108
- 'walmart.com': { size: 'mega', commission: 15, darkPatterns: true, category: 'marketplace' },
109
-
110
- // Large travel platforms — high commission to hotels/airlines
111
- 'booking.com': { size: 'large', commission: 18, darkPatterns: true, category: 'travel' },
112
- 'expedia.com': { size: 'large', commission: 20, darkPatterns: true, category: 'travel' },
113
- 'hotels.com': { size: 'large', commission: 20, darkPatterns: true, category: 'travel' },
114
- 'agoda.com': { size: 'large', commission: 18, darkPatterns: true, category: 'travel' },
115
- 'tripadvisor.com': { size: 'large', commission: 15, darkPatterns: false, category: 'travel' },
116
-
117
- // Medium aggregators — useful but still take commission
118
- 'kayak.com': { size: 'medium', commission: 5, darkPatterns: false, category: 'aggregator' },
119
- 'skyscanner.com': { size: 'medium', commission: 5, darkPatterns: false, category: 'aggregator' },
120
- 'trivago.com': { size: 'medium', commission: 5, darkPatterns: false, category: 'aggregator' },
121
- 'momondo.com': { size: 'medium', commission: 5, darkPatterns: false, category: 'aggregator' },
122
- 'google.com': { size: 'mega', commission: 0, darkPatterns: false, category: 'search' },
123
-
124
- // Small/Independent — zero or low commission, direct relationships
125
- 'hostelworld.com': { size: 'small', commission: 12, darkPatterns: false, category: 'travel' },
126
- 'kiwi.com': { size: 'small', commission: 5, darkPatterns: false, category: 'travel' },
127
- 'almosafer.com': { size: 'small', commission: 8, darkPatterns: false, category: 'travel' },
128
- 'wego.com': { size: 'small', commission: 3, darkPatterns: false, category: 'aggregator' },
129
- 'flyin.com': { size: 'small', commission: 5, darkPatterns: false, category: 'travel' },
130
- 'etsy.com': { size: 'medium', commission: 6.5, darkPatterns: false, category: 'marketplace' },
131
- };
132
-
133
- // Known dark patterns used by big platforms
134
- const DARK_PATTERNS = {
135
- urgencyScarcity: {
136
- name: 'Urgency/Scarcity',
137
- name_ar: 'استعجال/ندرة وهمية',
138
- indicators: ['only X left', 'book now', 'limited time', 'selling fast', 'hurry',
139
- 'عرض محدود', 'آخر فرصة', 'تبقى فقط', 'احجز الآن'],
140
- },
141
- confirmShaming: {
142
- name: 'Confirm Shaming',
143
- name_ar: 'إذلال للرفض',
144
- indicators: ['no thanks, i don\'t want to save', 'i\'ll pay full price',
145
- 'لا أريد التوفير', 'سأدفع السعر الكامل'],
146
- },
147
- hiddenCosts: {
148
- name: 'Hidden Costs',
149
- name_ar: 'تكاليف مخفية',
150
- indicators: ['resort fee', 'cleaning fee', 'service charge', 'processing fee',
151
- 'رسوم خدمة', 'رسوم تنظيف', 'رسوم منتجع'],
152
- },
153
- misdirection: {
154
- name: 'Misdirection',
155
- name_ar: 'تضليل',
156
- indicators: ['recommended', 'most popular', 'best value', 'top pick',
157
- 'موصى به', 'الأكثر شعبية', 'أفضل قيمة'],
158
- },
159
- };
160
-
161
- // ─── Score Calculator ────────────────────────────────────────────────
162
-
163
- function calculateFairnessScore(domain, context = {}) {
164
- const d = domain.replace(/^www\./, '').toLowerCase();
165
- const platform = PLATFORM_REGISTRY[d];
166
- const override = stmts.getOverride.get(d);
167
-
168
- let sizeScore = 50;
169
- let trustScore = 50;
170
- let priceHonesty = 50;
171
- let transparency = 50;
172
- let directBooking = 0;
173
-
174
- if (platform) {
175
- // Size scoring — smaller = higher
176
- switch (platform.size) {
177
- case 'mega': sizeScore = 15; break;
178
- case 'large': sizeScore = 30; break;
179
- case 'medium': sizeScore = 60; break;
180
- case 'small': sizeScore = 85; break;
181
- }
182
-
183
- // Commission impacts price honesty
184
- priceHonesty = Math.max(10, 100 - platform.commission * 3);
185
-
186
- // Dark patterns reduce transparency
187
- transparency = platform.darkPatterns ? 30 : 75;
188
-
189
- // Direct booking capability
190
- if (platform.category === 'travel' && platform.size === 'small') directBooking = 1;
191
- } else {
192
- // Unknown domain — likely independent/small
193
- sizeScore = 75;
194
- priceHonesty = 60;
195
- transparency = 60;
196
-
197
- // Check TLD indicators
198
- const tld = '.' + d.split('.').pop();
199
- const smallTlds = ['.shop', '.store', '.boutique', '.local', '.direct'];
200
- if (smallTlds.some(t => d.endsWith(t))) sizeScore += 10;
201
-
202
- // Long domain = likely niche/specific
203
- if (d.length > 15) sizeScore += 5;
204
-
205
- directBooking = 1; // Unknown = assume direct
206
- }
207
-
208
- // Apply context-based adjustments
209
- if (context.fraudAlerts && context.fraudAlerts > 0) {
210
- priceHonesty -= context.fraudAlerts * 15;
211
- trustScore -= context.fraudAlerts * 10;
212
- }
213
- if (context.attestations && context.attestations > 0) {
214
- trustScore += Math.min(30, context.attestations * 5);
215
- }
216
- if (context.priceIncreased) {
217
- priceHonesty -= 20;
218
- }
219
-
220
- // ── WAB Bridge Priority ───────────────────────────────────────────
221
- // Sites that installed the WAB script get significant bonuses:
222
- // +15 trust (cooperative = trustworthy)
223
- // +10 transparency (open to agent interaction)
224
- // +10 bonus if negotiation rules exist (willing to negotiate)
225
- // +5 bonus if listed in WAB directory
226
- let wabBridgeBonus = 0;
227
- let wabBridge = null;
228
- try {
229
- wabBridge = context.wabBridge || getWabBridgeInfo(d);
230
- } catch (_) {}
231
-
232
- if (wabBridge) {
233
- trustScore += 15;
234
- transparency += 10;
235
- wabBridgeBonus += 5; // Base bonus for installing bridge
236
-
237
- if (wabBridge.hasNegotiation) {
238
- wabBridgeBonus += 10; // Negotiation-ready sites get extra priority
239
- }
240
- if (wabBridge.isListed) {
241
- wabBridgeBonus += 5; // Listed in directory = transparent
242
- }
243
- // Higher tiers show greater commitment
244
- if (wabBridge.tier === 'pro' || wabBridge.tier === 'enterprise') {
245
- wabBridgeBonus += 5;
246
- }
247
- }
248
-
249
- // Apply admin override
250
- const boost = (override ? override.boost : 0) + wabBridgeBonus;
251
-
252
- // Calculate total
253
- const total = Math.max(0, Math.min(100,
254
- Math.round(sizeScore * 0.25 + trustScore * 0.25 + priceHonesty * 0.25 + transparency * 0.25) + boost
255
- ));
256
-
257
- const category = total >= 70 ? 'recommended' : total >= 45 ? 'neutral' : 'caution';
258
-
259
- // Upsert to DB
260
- try {
261
- stmts.upsertScore.run(d, category, sizeScore, trustScore, priceHonesty, transparency,
262
- directBooking, total, context.attestations || 0, context.fraudAlerts || 0);
263
- } catch (_) {}
264
-
265
- return {
266
- domain: d,
267
- category,
268
- total,
269
- breakdown: { sizeScore, trustScore, priceHonesty, transparency, directBooking },
270
- wabBridge: wabBridge ? {
271
- installed: true,
272
- hasNegotiation: wabBridge.hasNegotiation,
273
- isListed: wabBridge.isListed,
274
- tier: wabBridge.tier,
275
- bonus: wabBridgeBonus,
276
- } : { installed: false },
277
- platform: platform ? { size: platform.size, commission: platform.commission } : null,
278
- override: boost !== wabBridgeBonus ? { boost: boost - wabBridgeBonus, reason: override?.reason } : null,
279
- };
280
- }
281
-
282
- // ─── Rank results with fairness ──────────────────────────────────────
283
-
284
- function rankWithFairness(results, options = {}) {
285
- if (!results || results.length === 0) return [];
286
-
287
- // Calculate fairness for each result
288
- const scored = results.map(r => {
289
- const domain = r.domain || _extractDomain(r.url || '');
290
- const fairness = calculateFairnessScore(domain, {
291
- fraudAlerts: r.fraudAlerts?.length || 0,
292
- attestations: r.attestations || 0,
293
- });
294
-
295
- // Composite score: price (45%) + fairness (25%) + quality (15%) + WAB bridge (15%)
296
- const priceWeight = options.priceWeight || 0.45;
297
- const fairnessWeight = options.fairnessWeight || 0.25;
298
- const qualityWeight = options.qualityWeight || 0.15;
299
- const bridgeWeight = options.bridgeWeight || 0.15;
300
-
301
- // Normalize price score (lower price = higher score, 0-100)
302
- let priceScore = 50;
303
- if (r.priceUsd && options.avgPrice) {
304
- priceScore = Math.max(0, Math.min(100, 100 - ((r.priceUsd / options.avgPrice) * 50)));
305
- }
306
-
307
- // Quality score from rating
308
- const qualityScore = r.rating ? Math.min(100, r.rating * 20) : 50;
309
-
310
- // WAB Bridge score — sites with the bridge installed get priority
311
- // 100 = bridge + negotiation + listed, 60 = bridge only, 0 = no bridge
312
- let bridgeScore = 0;
313
- if (fairness.wabBridge && fairness.wabBridge.installed) {
314
- bridgeScore = 60; // Base: bridge installed
315
- if (fairness.wabBridge.hasNegotiation) bridgeScore += 25; // Can negotiate prices
316
- if (fairness.wabBridge.isListed) bridgeScore += 15; // Listed in directory
317
- }
318
-
319
- const compositeScore = Math.round(
320
- priceScore * priceWeight +
321
- fairness.total * fairnessWeight +
322
- qualityScore * qualityWeight +
323
- bridgeScore * bridgeWeight
324
- );
325
-
326
- return {
327
- ...r,
328
- fairness,
329
- priceScore,
330
- qualityScore,
331
- bridgeScore,
332
- compositeScore,
333
- };
334
- });
335
-
336
- // Sort by composite score
337
- scored.sort((a, b) => b.compositeScore - a.compositeScore);
338
-
339
- // Add rank labels
340
- scored.forEach((r, i) => {
341
- r.rank = i + 1;
342
- if (i === 0) r.badge = '🥇';
343
- else if (i === 1) r.badge = '🥈';
344
- else if (i === 2) r.badge = '🥉';
345
-
346
- // Add fairness badges
347
- if (r.fairness.category === 'recommended') r.fairnessBadge = '✅';
348
- else if (r.fairness.category === 'caution') r.fairnessBadge = '⚠️';
349
-
350
- if (r.fairness.breakdown.directBooking) r.directBadge = '🔗 Direct';
351
- if (r.fairness.platform?.size === 'small') r.sizeBadge = '🏪 Independent';
352
-
353
- // WAB Bridge badges
354
- if (r.fairness.wabBridge?.installed) {
355
- r.wabBridgeBadge = '🌉 WAB';
356
- if (r.fairness.wabBridge.hasNegotiation) r.negotiationBadge = '🤝 Negotiable';
357
- }
358
- });
359
-
360
- return scored;
361
- }
362
-
363
- // ─── Dark Pattern Detector ───────────────────────────────────────────
364
-
365
- function detectDarkPatterns(text, lang = 'en') {
366
- const detected = [];
367
- const lowerText = (text || '').toLowerCase();
368
-
369
- for (const [key, pattern] of Object.entries(DARK_PATTERNS)) {
370
- const found = pattern.indicators.filter(ind => lowerText.includes(ind.toLowerCase()));
371
- if (found.length > 0) {
372
- detected.push({
373
- type: key,
374
- name: lang === 'ar' ? pattern.name_ar : pattern.name,
375
- matches: found,
376
- severity: found.length >= 3 ? 'high' : found.length >= 2 ? 'medium' : 'low',
377
- });
378
- }
379
- }
380
-
381
- return detected;
382
- }
383
-
384
- // ─── Helpers ─────────────────────────────────────────────────────────
385
-
386
- function _extractDomain(url) {
387
- try { return new URL(url).hostname.replace(/^www\./, ''); } catch (_) { return ''; }
388
- }
389
-
390
- function getTopFairSites(limit = 20) {
391
- return stmts.getTopFair.all(limit);
392
- }
393
-
394
- function setOverride(domain, boost, reason, createdBy = 'admin') {
395
- stmts.upsertOverride.run(domain.replace(/^www\./, ''), boost, reason, createdBy);
396
- }
397
-
398
- // ─── Exports ─────────────────────────────────────────────────────────
399
-
400
- module.exports = {
401
- calculateFairnessScore,
402
- rankWithFairness,
403
- detectDarkPatterns,
404
- getTopFairSites,
405
- setOverride,
406
- getWabBridgeInfo,
407
- PLATFORM_REGISTRY,
408
- DARK_PATTERNS,
409
- };