web-agent-bridge 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ar.md +506 -31
- package/README.md +574 -47
- package/bin/agent-runner.js +10 -1
- package/package.json +12 -4
- package/public/agent-workspace.html +347 -0
- package/public/browser.html +484 -0
- package/public/commander-dashboard.html +243 -0
- package/public/css/agent-workspace.css +1713 -0
- package/public/css/premium.css +317 -317
- package/public/demo.html +259 -259
- package/public/index.html +738 -644
- package/public/js/agent-workspace.js +1740 -0
- package/public/mesh-dashboard.html +309 -382
- package/public/premium-dashboard.html +2487 -2487
- package/public/premium.html +791 -791
- package/public/script/wab.min.js +124 -87
- package/script/ai-agent-bridge.js +154 -84
- package/sdk/agent-mesh.js +287 -171
- package/sdk/commander.js +262 -0
- package/sdk/index.d.ts +83 -0
- package/sdk/index.js +374 -260
- package/sdk/package.json +1 -1
- package/server/config/secrets.js +13 -5
- package/server/index.js +191 -5
- package/server/middleware/adminAuth.js +6 -1
- package/server/middleware/auth.js +11 -2
- package/server/middleware/rateLimits.js +78 -2
- package/server/migrations/002_premium_features.sql +418 -418
- package/server/migrations/003_ads_integer_cents.sql +33 -0
- package/server/models/db.js +121 -1
- package/server/routes/admin-premium.js +671 -671
- package/server/routes/admin.js +16 -2
- package/server/routes/ads.js +130 -0
- package/server/routes/agent-workspace.js +378 -0
- package/server/routes/api.js +21 -2
- package/server/routes/auth.js +26 -6
- package/server/routes/commander.js +316 -0
- package/server/routes/mesh.js +370 -201
- package/server/routes/premium-v2.js +686 -686
- package/server/routes/premium.js +724 -724
- package/server/routes/sovereign.js +78 -0
- package/server/routes/universal.js +177 -0
- package/server/routes/wab-api.js +20 -5
- package/server/services/agent-chat.js +506 -0
- package/server/services/agent-learning.js +230 -77
- package/server/services/agent-memory.js +625 -625
- package/server/services/agent-mesh.js +260 -67
- package/server/services/agent-symphony.js +553 -517
- package/server/services/agent-tasks.js +1807 -0
- package/server/services/commander.js +738 -0
- package/server/services/edge-compute.js +440 -0
- package/server/services/fairness-engine.js +409 -0
- package/server/services/local-ai.js +389 -0
- package/server/services/plugins.js +771 -747
- package/server/services/price-intelligence.js +565 -0
- package/server/services/price-shield.js +1137 -0
- package/server/services/search-engine.js +357 -0
- package/server/services/security.js +513 -0
- package/server/services/self-healing.js +843 -843
- package/server/services/swarm.js +788 -788
- package/server/services/universal-scraper.js +661 -0
- package/server/services/vision.js +871 -871
- package/server/ws.js +61 -1
- package/public/admin/dashboard.html +0 -848
- package/public/admin/login.html +0 -84
- package/public/video/tutorial.mp4 +0 -0
|
@@ -0,0 +1,409 @@
|
|
|
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
|
+
};
|