wu-framework 1.2.0 → 1.2.1
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/dist/wu-framework.cjs.js +3 -0
- package/dist/wu-framework.cjs.js.map +1 -0
- package/dist/wu-framework.dev.js +15732 -0
- package/dist/wu-framework.dev.js.map +1 -0
- package/dist/wu-framework.esm.js +3 -0
- package/dist/wu-framework.esm.js.map +1 -0
- package/dist/wu-framework.umd.js +3 -0
- package/dist/wu-framework.umd.js.map +1 -0
- package/package.json +1 -1
- package/src/core/wu-loader.js +65 -0
- package/src/core/wu-sentinel-client.js +311 -0
package/package.json
CHANGED
package/src/core/wu-loader.js
CHANGED
|
@@ -128,6 +128,9 @@ export class WuLoader {
|
|
|
128
128
|
* @returns {string} Codigo JavaScript de la aplicacion
|
|
129
129
|
*/
|
|
130
130
|
async loadApp(appUrl, manifest) {
|
|
131
|
+
// Sentinel gate: block code loading for unverified clients
|
|
132
|
+
await this._ensureSentinelVerified();
|
|
133
|
+
|
|
131
134
|
const entryFile = manifest?.entry || 'index.js';
|
|
132
135
|
const fullUrl = `${appUrl}/${entryFile}`;
|
|
133
136
|
|
|
@@ -174,6 +177,9 @@ export class WuLoader {
|
|
|
174
177
|
* @returns {Function} Funcion del componente
|
|
175
178
|
*/
|
|
176
179
|
async loadComponent(appUrl, componentPath) {
|
|
180
|
+
// Sentinel gate
|
|
181
|
+
await this._ensureSentinelVerified();
|
|
182
|
+
|
|
177
183
|
// Normalizar ruta del componente
|
|
178
184
|
let normalizedPath = componentPath;
|
|
179
185
|
if (normalizedPath.startsWith('./')) {
|
|
@@ -382,4 +388,63 @@ export class WuLoader {
|
|
|
382
388
|
cacheKeys: Array.from(this.cache.keys())
|
|
383
389
|
};
|
|
384
390
|
}
|
|
391
|
+
|
|
392
|
+
// ── Sentinel Gate ─────────────────────────────────────────────────
|
|
393
|
+
// Blocks code loading until the client is verified as human.
|
|
394
|
+
// Scrapers that execute JS but don't pass proof-of-work get nothing.
|
|
395
|
+
|
|
396
|
+
async _ensureSentinelVerified() {
|
|
397
|
+
// Skip in development / localhost (sentinel is for production)
|
|
398
|
+
if (typeof window === 'undefined') return;
|
|
399
|
+
const host = window.location?.hostname || '';
|
|
400
|
+
if (host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0') return;
|
|
401
|
+
|
|
402
|
+
// If sentinel is not loaded, allow (backwards compat — site doesn't use sentinel)
|
|
403
|
+
if (!window.__wu_sentinel) return;
|
|
404
|
+
|
|
405
|
+
// Already verified? Proceed.
|
|
406
|
+
if (window.__wu_sentinel.isVerified()) return;
|
|
407
|
+
|
|
408
|
+
// Wait for sentinel verification (max 10 seconds)
|
|
409
|
+
await new Promise((resolve, reject) => {
|
|
410
|
+
// Check if verified during wait
|
|
411
|
+
if (window.__wu_sentinel.isVerified()) {
|
|
412
|
+
resolve();
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const timeout = setTimeout(() => {
|
|
417
|
+
cleanup();
|
|
418
|
+
reject(new Error('[WuLoader] Sentinel verification timeout — content blocked'));
|
|
419
|
+
}, 10000);
|
|
420
|
+
|
|
421
|
+
const onVerified = () => {
|
|
422
|
+
cleanup();
|
|
423
|
+
resolve();
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
function cleanup() {
|
|
427
|
+
clearTimeout(timeout);
|
|
428
|
+
window.removeEventListener('wu:sentinel:verified', onVerified);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
window.addEventListener('wu:sentinel:verified', onVerified);
|
|
432
|
+
|
|
433
|
+
// Check periodically in case event was missed
|
|
434
|
+
const check = setInterval(() => {
|
|
435
|
+
if (window.__wu_sentinel.isVerified()) {
|
|
436
|
+
clearInterval(check);
|
|
437
|
+
cleanup();
|
|
438
|
+
resolve();
|
|
439
|
+
}
|
|
440
|
+
}, 100);
|
|
441
|
+
|
|
442
|
+
// Clean up interval on timeout too
|
|
443
|
+
const origCleanup = cleanup;
|
|
444
|
+
cleanup = function() {
|
|
445
|
+
clearInterval(check);
|
|
446
|
+
origCleanup();
|
|
447
|
+
};
|
|
448
|
+
});
|
|
449
|
+
}
|
|
385
450
|
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WU SENTINEL — Client-Side Anti-Scraping Protection
|
|
3
|
+
*
|
|
4
|
+
* Works on ANY hosting (nginx, S3, CloudFront, etc.)
|
|
5
|
+
* because it runs in the browser, not the server.
|
|
6
|
+
*
|
|
7
|
+
* Defense layers:
|
|
8
|
+
* 1. Headless browser detection (navigator.webdriver, etc.)
|
|
9
|
+
* 2. Honeypot links (invisible traps for crawlers)
|
|
10
|
+
* 3. Content hydration gate (real content loads after JS proof)
|
|
11
|
+
* 4. Behavioral analysis (mouse/touch/scroll patterns)
|
|
12
|
+
* 5. Canvas fingerprint for bot detection
|
|
13
|
+
*
|
|
14
|
+
* Usage: automatically injected by `wu build` into production HTML.
|
|
15
|
+
* Or manually: <script src="wu-sentinel-client.js"></script>
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
(function() {
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const SENTINEL_VERSION = '1.0.0';
|
|
22
|
+
const COOKIE_NAME = 'wu_sentinel';
|
|
23
|
+
const COOKIE_MAX_AGE = 86400; // 24h
|
|
24
|
+
|
|
25
|
+
// ── Layer 1: Headless Browser Detection ─────────────────────────────
|
|
26
|
+
|
|
27
|
+
function detectHeadless() {
|
|
28
|
+
const signals = [];
|
|
29
|
+
|
|
30
|
+
// navigator.webdriver (set by Puppeteer, Playwright, Selenium)
|
|
31
|
+
if (navigator.webdriver === true) {
|
|
32
|
+
signals.push('webdriver');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Missing languages (headless often has empty array)
|
|
36
|
+
if (!navigator.languages || navigator.languages.length === 0) {
|
|
37
|
+
signals.push('no-languages');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Chrome without chrome runtime object
|
|
41
|
+
if (window.chrome === undefined && /Chrome/.test(navigator.userAgent)) {
|
|
42
|
+
signals.push('chrome-no-runtime');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Permissions API inconsistency
|
|
46
|
+
if (navigator.permissions) {
|
|
47
|
+
navigator.permissions.query({ name: 'notifications' }).then(function(result) {
|
|
48
|
+
if (Notification.permission === 'denied' && result.state === 'prompt') {
|
|
49
|
+
signals.push('permissions-mismatch');
|
|
50
|
+
}
|
|
51
|
+
}).catch(function() {});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Screen dimensions (headless often has 0x0 or 800x600 exactly)
|
|
55
|
+
if (screen.width === 0 && screen.height === 0) {
|
|
56
|
+
signals.push('zero-screen');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Broken WebGL (headless often fails or returns generic renderer)
|
|
60
|
+
try {
|
|
61
|
+
var canvas = document.createElement('canvas');
|
|
62
|
+
var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
63
|
+
if (gl) {
|
|
64
|
+
var debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
|
|
65
|
+
if (debugInfo) {
|
|
66
|
+
var renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
67
|
+
if (/SwiftShader|llvmpipe|Software/i.test(renderer)) {
|
|
68
|
+
signals.push('software-renderer');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
signals.push('no-webgl');
|
|
73
|
+
}
|
|
74
|
+
} catch(e) {
|
|
75
|
+
signals.push('webgl-error');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Plugin count (headless typically has 0 plugins)
|
|
79
|
+
if (navigator.plugins && navigator.plugins.length === 0 &&
|
|
80
|
+
!/Firefox/.test(navigator.userAgent)) {
|
|
81
|
+
signals.push('no-plugins');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return signals;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Layer 2: Honeypot Links ─────────────────────────────────────────
|
|
88
|
+
// Invisible links that real users never click. Bots follow all links.
|
|
89
|
+
|
|
90
|
+
function injectHoneypots() {
|
|
91
|
+
var styles = [
|
|
92
|
+
'position:absolute;left:-9999px;top:-9999px',
|
|
93
|
+
'opacity:0;height:0;width:0;overflow:hidden',
|
|
94
|
+
'display:block;height:1px;width:1px;overflow:hidden;clip:rect(0,0,0,0)',
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
var paths = [
|
|
98
|
+
'/admin/login', '/wp-admin', '/api/users', '/sitemap-index.xml',
|
|
99
|
+
'/.env', '/config.json', '/graphql', '/api/v2/data',
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
var container = document.createElement('div');
|
|
103
|
+
container.setAttribute('aria-hidden', 'true');
|
|
104
|
+
container.style.cssText = 'position:absolute;overflow:hidden;height:0;width:0';
|
|
105
|
+
|
|
106
|
+
for (var i = 0; i < paths.length; i++) {
|
|
107
|
+
var a = document.createElement('a');
|
|
108
|
+
a.href = paths[i];
|
|
109
|
+
a.style.cssText = styles[i % styles.length];
|
|
110
|
+
a.tabIndex = -1;
|
|
111
|
+
a.textContent = 'Click here';
|
|
112
|
+
a.addEventListener('click', function(e) {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
// Bot clicked a honeypot — flag it
|
|
115
|
+
markAsBot('honeypot-click');
|
|
116
|
+
});
|
|
117
|
+
container.appendChild(a);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Insert after DOM ready
|
|
121
|
+
if (document.body) {
|
|
122
|
+
document.body.appendChild(container);
|
|
123
|
+
} else {
|
|
124
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
125
|
+
document.body.appendChild(container);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Layer 3: Content Hydration Gate ─────────────────────────────────
|
|
131
|
+
// Real content is hidden until JS proves the client is a browser.
|
|
132
|
+
// Static HTML scrapers only see the skeleton, not the data.
|
|
133
|
+
|
|
134
|
+
function hydrateContent() {
|
|
135
|
+
// If already verified (cookie exists), hydrate immediately
|
|
136
|
+
if (isVerified()) {
|
|
137
|
+
revealContent();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Simple proof-of-work: compute a hash that starts with "00"
|
|
142
|
+
// Takes ~50ms on a real browser, proves JS execution
|
|
143
|
+
var nonce = 0;
|
|
144
|
+
var challenge = document.title + navigator.userAgent + screen.width;
|
|
145
|
+
|
|
146
|
+
function simpleHash(str) {
|
|
147
|
+
var hash = 0;
|
|
148
|
+
for (var i = 0; i < str.length; i++) {
|
|
149
|
+
var char = str.charCodeAt(i);
|
|
150
|
+
hash = ((hash << 5) - hash) + char;
|
|
151
|
+
hash = hash & hash; // Convert to 32-bit
|
|
152
|
+
}
|
|
153
|
+
return Math.abs(hash).toString(16).padStart(8, '0');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Find a nonce where hash starts with "00" (proof of computation)
|
|
157
|
+
while (nonce < 100000) {
|
|
158
|
+
var result = simpleHash(challenge + nonce);
|
|
159
|
+
if (result.substring(0, 2) === '00') {
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
nonce++;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Set verified cookie
|
|
166
|
+
setVerified(nonce);
|
|
167
|
+
revealContent();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function revealContent() {
|
|
171
|
+
// Remove any wu-sentinel-gate class (content was hidden)
|
|
172
|
+
var gated = document.querySelectorAll('[data-wu-gated]');
|
|
173
|
+
for (var i = 0; i < gated.length; i++) {
|
|
174
|
+
gated[i].style.opacity = '1';
|
|
175
|
+
gated[i].style.visibility = 'visible';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Dispatch event for wu-framework to know content is ready
|
|
179
|
+
window.dispatchEvent(new CustomEvent('wu:sentinel:verified'));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── Layer 4: Behavioral Analysis ────────────────────────────────────
|
|
183
|
+
// Track mouse/touch/scroll to build a human confidence score.
|
|
184
|
+
|
|
185
|
+
function startBehavioralAnalysis() {
|
|
186
|
+
var interactions = { mouse: 0, scroll: 0, touch: 0, keys: 0 };
|
|
187
|
+
var isHuman = false;
|
|
188
|
+
|
|
189
|
+
function onInteraction(type) {
|
|
190
|
+
interactions[type]++;
|
|
191
|
+
var total = interactions.mouse + interactions.scroll +
|
|
192
|
+
interactions.touch + interactions.keys;
|
|
193
|
+
// After 3+ different interaction types, mark as human
|
|
194
|
+
var types = 0;
|
|
195
|
+
if (interactions.mouse > 0) types++;
|
|
196
|
+
if (interactions.scroll > 0) types++;
|
|
197
|
+
if (interactions.touch > 0) types++;
|
|
198
|
+
if (interactions.keys > 0) types++;
|
|
199
|
+
|
|
200
|
+
if (total > 5 && types >= 2 && !isHuman) {
|
|
201
|
+
isHuman = true;
|
|
202
|
+
setVerified('behavioral');
|
|
203
|
+
window.dispatchEvent(new CustomEvent('wu:sentinel:human', {
|
|
204
|
+
detail: { interactions: interactions }
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
document.addEventListener('mousemove', function() { onInteraction('mouse'); }, { passive: true });
|
|
210
|
+
document.addEventListener('scroll', function() { onInteraction('scroll'); }, { passive: true });
|
|
211
|
+
document.addEventListener('touchstart', function() { onInteraction('touch'); }, { passive: true });
|
|
212
|
+
document.addEventListener('keydown', function() { onInteraction('keys'); }, { passive: true });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Layer 5: Canvas Fingerprint ─────────────────────────────────────
|
|
216
|
+
// Generates a unique browser fingerprint. Headless browsers produce
|
|
217
|
+
// identical or empty fingerprints.
|
|
218
|
+
|
|
219
|
+
function getCanvasFingerprint() {
|
|
220
|
+
try {
|
|
221
|
+
var canvas = document.createElement('canvas');
|
|
222
|
+
canvas.width = 200;
|
|
223
|
+
canvas.height = 50;
|
|
224
|
+
var ctx = canvas.getContext('2d');
|
|
225
|
+
if (!ctx) return 'no-canvas';
|
|
226
|
+
|
|
227
|
+
ctx.textBaseline = 'top';
|
|
228
|
+
ctx.font = '14px Arial';
|
|
229
|
+
ctx.fillStyle = '#f60';
|
|
230
|
+
ctx.fillRect(125, 1, 62, 20);
|
|
231
|
+
ctx.fillStyle = '#069';
|
|
232
|
+
ctx.fillText('Wu Sentinel', 2, 15);
|
|
233
|
+
ctx.fillStyle = 'rgba(102,204,0,0.7)';
|
|
234
|
+
ctx.fillText('Wu Sentinel', 4, 17);
|
|
235
|
+
|
|
236
|
+
var dataUrl = canvas.toDataURL();
|
|
237
|
+
// Simple hash of the data URL
|
|
238
|
+
var hash = 0;
|
|
239
|
+
for (var i = 0; i < dataUrl.length; i++) {
|
|
240
|
+
hash = ((hash << 5) - hash) + dataUrl.charCodeAt(i);
|
|
241
|
+
hash = hash & hash;
|
|
242
|
+
}
|
|
243
|
+
return Math.abs(hash).toString(36);
|
|
244
|
+
} catch(e) {
|
|
245
|
+
return 'canvas-error';
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Utilities ───────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
function isVerified() {
|
|
252
|
+
return document.cookie.indexOf(COOKIE_NAME + '=') !== -1;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function setVerified(proof) {
|
|
256
|
+
var fp = getCanvasFingerprint();
|
|
257
|
+
var value = btoa(JSON.stringify({
|
|
258
|
+
v: SENTINEL_VERSION,
|
|
259
|
+
t: Date.now(),
|
|
260
|
+
p: String(proof),
|
|
261
|
+
f: fp
|
|
262
|
+
}));
|
|
263
|
+
document.cookie = COOKIE_NAME + '=' + value +
|
|
264
|
+
';path=/;max-age=' + COOKIE_MAX_AGE +
|
|
265
|
+
';SameSite=Lax';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function markAsBot(reason) {
|
|
269
|
+
console.warn('[Wu Sentinel] Bot detected:', reason);
|
|
270
|
+
window.dispatchEvent(new CustomEvent('wu:sentinel:bot', {
|
|
271
|
+
detail: { reason: reason }
|
|
272
|
+
}));
|
|
273
|
+
// Could send to analytics endpoint if configured
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── Initialize ──────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
function init() {
|
|
279
|
+
// 1. Check for headless signals
|
|
280
|
+
var headlessSignals = detectHeadless();
|
|
281
|
+
if (headlessSignals.length >= 2) {
|
|
282
|
+
markAsBot('headless:' + headlessSignals.join(','));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 2. Inject honeypot links
|
|
286
|
+
injectHoneypots();
|
|
287
|
+
|
|
288
|
+
// 3. Hydrate gated content (proof-of-work)
|
|
289
|
+
hydrateContent();
|
|
290
|
+
|
|
291
|
+
// 4. Start behavioral tracking
|
|
292
|
+
startBehavioralAnalysis();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Run after DOM is ready
|
|
296
|
+
if (document.readyState === 'loading') {
|
|
297
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
298
|
+
} else {
|
|
299
|
+
init();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Expose for wu-framework integration
|
|
303
|
+
if (typeof window !== 'undefined') {
|
|
304
|
+
window.__wu_sentinel = {
|
|
305
|
+
version: SENTINEL_VERSION,
|
|
306
|
+
isVerified: isVerified,
|
|
307
|
+
getFingerprint: getCanvasFingerprint,
|
|
308
|
+
detectHeadless: detectHeadless,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
})();
|