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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wu-framework",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Universal Microfrontends Framework - 13 frameworks, zero config, Shadow DOM isolation",
5
5
  "main": "dist/wu-framework.cjs.js",
6
6
  "module": "src/index.js",
@@ -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
+ })();