@jjlmoya/utils-streaming 1.1.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 (46) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +134 -0
  3. package/src/category/i18n/es.ts +134 -0
  4. package/src/category/i18n/fr.ts +134 -0
  5. package/src/category/index.ts +12 -0
  6. package/src/category/seo.astro +14 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +24 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +148 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/locale_completeness.test.ts +42 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/schemas_fulfillment.test.ts +23 -0
  21. package/src/tests/seo_length.test.ts +22 -0
  22. package/src/tests/title_quality.test.ts +55 -0
  23. package/src/tests/tool_validation.test.ts +17 -0
  24. package/src/tool/sorteo/bibliography.astro +14 -0
  25. package/src/tool/sorteo/component.astro +1319 -0
  26. package/src/tool/sorteo/engine.ts +143 -0
  27. package/src/tool/sorteo/i18n/en.ts +253 -0
  28. package/src/tool/sorteo/i18n/es.ts +253 -0
  29. package/src/tool/sorteo/i18n/fr.ts +253 -0
  30. package/src/tool/sorteo/index.ts +29 -0
  31. package/src/tool/sorteo/seo.astro +14 -0
  32. package/src/tool/sorteo/ui-manager.ts +81 -0
  33. package/src/tool/sorteo/ui.ts +30 -0
  34. package/src/tool/tebasCheck/bibliography.astro +14 -0
  35. package/src/tool/tebasCheck/component.astro +587 -0
  36. package/src/tool/tebasCheck/engine.ts +116 -0
  37. package/src/tool/tebasCheck/i18n/en.ts +254 -0
  38. package/src/tool/tebasCheck/i18n/es.ts +254 -0
  39. package/src/tool/tebasCheck/i18n/fr.ts +254 -0
  40. package/src/tool/tebasCheck/index.ts +29 -0
  41. package/src/tool/tebasCheck/ips.json +31 -0
  42. package/src/tool/tebasCheck/seo.astro +14 -0
  43. package/src/tool/tebasCheck/types.ts +45 -0
  44. package/src/tool/tebasCheck/ui-manager.ts +31 -0
  45. package/src/tools.ts +8 -0
  46. package/src/types.ts +71 -0
@@ -0,0 +1,587 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import { TEBAS_CHECK_TOOL } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+ import targetIps from './ips.json';
6
+
7
+ interface Props {
8
+ locale?: KnownLocale;
9
+ }
10
+
11
+ const { locale = 'es' } = Astro.props;
12
+ const content = await TEBAS_CHECK_TOOL.entry.i18n[locale]?.();
13
+ const ui = content?.ui;
14
+ ---
15
+
16
+ <div
17
+ id="tebas-app"
18
+ class="tebas-container"
19
+ data-targets={JSON.stringify(targetIps)}
20
+ data-ui={JSON.stringify(ui)}
21
+ >
22
+ <div class="tebas-card">
23
+ <div class="tebas-card-bg-icon">
24
+ <Icon name="mdi:cctv" />
25
+ </div>
26
+
27
+ <div class="tebas-content">
28
+ <div id="status-arena" class="tebas-status-view">
29
+ <div id="loading-state" class="tebas-loading">
30
+ <div class="tebas-spinner-wrapper">
31
+ <div class="tebas-spinner">
32
+ <Icon name="mdi:eye-refresh-outline" class="animate-spin" />
33
+ </div>
34
+ <div class="tebas-ping-ring"></div>
35
+ </div>
36
+ <div class="tebas-loading-text">
37
+ <h2 class="tebas-title-main">{ui?.scanning}</h2>
38
+ <p id="loading-subtext" class="tebas-subtitle-pulse">{ui?.seekingBlocks}</p>
39
+ </div>
40
+ </div>
41
+
42
+ <div id="result-state" class="tebas-results hidden">
43
+ <div id="res-blocked" class="tebas-result-box blocked hidden">
44
+ <div class="tebas-result-icon-badge">
45
+ <Icon name="mdi:account-alert" class="tebas-result-main-icon" />
46
+ <div class="tebas-badge-overlay"><Icon name="mdi:gavel" /></div>
47
+ </div>
48
+ <div class="tebas-result-info">
49
+ <h3 class="tebas-result-status">{ui?.blockedTitle}</h3>
50
+ <div class="tebas-result-details">
51
+ <p class="tebas-font-bold">{ui?.blockedDiagnosis}</p>
52
+ <p id="blocked-reason" class="tebas-text-sm">{ui?.blockedReason}</p>
53
+ </div>
54
+ </div>
55
+ </div>
56
+
57
+ <div id="res-no-internet" class="tebas-result-box error hidden">
58
+ <div class="tebas-result-icon-badge">
59
+ <Icon name="mdi:router-wireless-off" class="tebas-result-main-icon" />
60
+ </div>
61
+ <div class="tebas-result-info">
62
+ <h3 class="tebas-result-status">{ui?.noInternetTitle}</h3>
63
+ <p class="tebas-text-sm">{ui?.noInternetReason}</p>
64
+ </div>
65
+ </div>
66
+
67
+ <div id="res-success" class="tebas-result-box success hidden">
68
+ <div class="tebas-result-icon-badge">
69
+ <Icon name="mdi:emoticon-happy-outline" class="tebas-result-main-icon" />
70
+ <div class="tebas-badge-overlay"><Icon name="mdi:shield-check" /></div>
71
+ </div>
72
+ <div class="tebas-result-info">
73
+ <h3 class="tebas-result-status">{ui?.successTitle}</h3>
74
+ <p class="tebas-text-sm">{ui?.successReason}</p>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </div>
79
+
80
+ <div class="tebas-actions">
81
+ <button id="retry-btn" class="tebas-btn hidden">
82
+ <Icon name="mdi:reload" />
83
+ <span>{ui?.retryBtn}</span>
84
+ </button>
85
+ </div>
86
+
87
+ <div class="tebas-note">
88
+ <Icon name="mdi:alert-decagram-outline" class="tebas-note-icon" />
89
+ <div class="tebas-note-content">
90
+ <span class="tebas-note-label">{ui?.authorNoteTitle}</span>
91
+ <p class="tebas-note-text">{ui?.authorNoteText}</p>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ <div class="tebas-console">
97
+ <div class="tebas-console-header">
98
+ <div class="tebas-console-dots">
99
+ <span></span><span></span><span></span>
100
+ </div>
101
+ <span class="tebas-console-version">{ui?.consoleHeader}</span>
102
+ </div>
103
+ <div id="console-output" class="tebas-console-body"></div>
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <script>
109
+ import { TebasEngine } from './engine';
110
+ import { TebasUIManager } from './ui-manager';
111
+ import type { ProbeResult, TebasCheckUI } from './types';
112
+
113
+ const app = document.getElementById('tebas-app');
114
+ const ips = JSON.parse(app?.dataset.targets || '[]');
115
+ const apiUi = JSON.parse(app?.dataset.ui || '{}') as TebasCheckUI;
116
+
117
+ const engine = new TebasEngine(ips);
118
+ const ui = new TebasUIManager('console-output');
119
+
120
+ const els = {
121
+ loading: document.getElementById('loading-state'),
122
+ results: document.getElementById('result-state'),
123
+ resBlocked: document.getElementById('res-blocked'),
124
+ resNoInternet: document.getElementById('res-no-internet'),
125
+ resSuccess: document.getElementById('res-success'),
126
+ loadingText: document.getElementById('loading-subtext'),
127
+ retryBtn: document.getElementById('retry-btn'),
128
+ blockedReason: document.getElementById('blocked-reason'),
129
+ };
130
+
131
+ const messages = [
132
+ apiUi.statusNegotiating,
133
+ apiUi.statusDodging,
134
+ apiUi.statusCheckingPirate,
135
+ apiUi.statusPinging,
136
+ apiUi.statusConsulting,
137
+ apiUi.statusCheckingFee,
138
+ apiUi.statusCalculating,
139
+ apiUi.statusDeciphering,
140
+ ];
141
+
142
+ const resetState = () => {
143
+ els.loading?.classList.remove('hidden');
144
+ els.results?.classList.add('hidden');
145
+ els.resBlocked?.classList.add('hidden');
146
+ els.resNoInternet?.classList.add('hidden');
147
+ els.resSuccess?.classList.add('hidden');
148
+ els.retryBtn?.classList.add('hidden');
149
+ ui.clearLog();
150
+ ui.log(apiUi.logStarted);
151
+ };
152
+
153
+ const handleNoInternet = (msgInterval: ReturnType<typeof setInterval>) => {
154
+ clearInterval(msgInterval);
155
+ els.loading?.classList.add('hidden');
156
+ els.results?.classList.remove('hidden');
157
+ els.resNoInternet?.classList.remove('hidden');
158
+ els.retryBtn?.classList.remove('hidden');
159
+ ui.log(apiUi.logNoInternet, 'error');
160
+ };
161
+
162
+ interface DiagSummary {
163
+ isp: string;
164
+ poisoned: boolean;
165
+ results: ProbeResult[];
166
+ isBlocked: boolean;
167
+ }
168
+
169
+ const renderDiagnosticResults = (summary: DiagSummary) => {
170
+ if (summary.isp) ui.log(apiUi.logIspFound + summary.isp);
171
+ if (summary.poisoned) ui.log(apiUi.logDnsPoisoned, 'error');
172
+
173
+ summary.results.forEach((res) => {
174
+ const type = res.status === 'blocked' ? 'error' : 'success';
175
+ const msg = res.status === 'blocked' ? apiUi.logIpBlocked : apiUi.logIpActive;
176
+ ui.log(msg.replace('{ip}', res.ip), type);
177
+ });
178
+ };
179
+
180
+ const showFinalState = (summary: DiagSummary) => {
181
+ els.loading?.classList.add('hidden');
182
+ els.results?.classList.remove('hidden');
183
+ els.retryBtn?.classList.remove('hidden');
184
+
185
+ if (summary.isBlocked) {
186
+ ui.log(apiUi.logAlertInterference, 'error');
187
+ els.resBlocked?.classList.remove('hidden');
188
+ } else {
189
+ ui.log(apiUi.logClean, 'success');
190
+ els.resSuccess?.classList.remove('hidden');
191
+ }
192
+ };
193
+
194
+ const runDiagnostic = async () => {
195
+ resetState();
196
+ const msgInterval = setInterval(() => {
197
+ if (els.loadingText) {
198
+ els.loadingText.innerText = messages[Math.floor(Math.random() * messages.length)] || '';
199
+ }
200
+ }, 800);
201
+
202
+ try {
203
+ const summary = await engine.runFullDiagnostic();
204
+ if (!summary.internet) {
205
+ handleNoInternet(msgInterval);
206
+ return;
207
+ }
208
+ renderDiagnosticResults(summary);
209
+ clearInterval(msgInterval);
210
+ showFinalState(summary);
211
+ } catch {
212
+ ui.log(apiUi.logDiagError, 'error');
213
+ clearInterval(msgInterval);
214
+ }
215
+ };
216
+
217
+ runDiagnostic();
218
+ els.retryBtn?.addEventListener('click', runDiagnostic);
219
+ </script>
220
+
221
+ <style is:global>
222
+ .tebas-container {
223
+ --tebas-primary: #f43f5e;
224
+ --tebas-secondary: #8b5cf6;
225
+ --tebas-success: #10b981;
226
+ --tebas-neutral: #64748b;
227
+ --tebas-bg: #f8fafc;
228
+ --tebas-card: #fff;
229
+ --tebas-border: #e2e8f0;
230
+ --tebas-text: #0f172a;
231
+ --tebas-text-muted: #64748b;
232
+ --tebas-console-bg: #0c0c0c;
233
+
234
+ width: 100%;
235
+ max-width: 42rem;
236
+ margin: 0 auto;
237
+ padding: 1rem;
238
+ }
239
+
240
+ .theme-dark .tebas-container {
241
+ --tebas-bg: #020617;
242
+ --tebas-card: #0f172a;
243
+ --tebas-border: #1e293b;
244
+ --tebas-text: #f1f5f9;
245
+ --tebas-text-muted: #94a3b8;
246
+ }
247
+
248
+ .tebas-card {
249
+ background: var(--tebas-card);
250
+ border-radius: 2rem;
251
+ border: 1px solid var(--tebas-border);
252
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.1);
253
+ position: relative;
254
+ overflow: hidden;
255
+ }
256
+
257
+ .tebas-card-bg-icon {
258
+ position: absolute;
259
+ top: 0;
260
+ right: 0;
261
+ padding: 1.5rem;
262
+ opacity: 0.05;
263
+ pointer-events: none;
264
+ }
265
+
266
+ .tebas-card-bg-icon [data-icon] {
267
+ width: 8rem;
268
+ height: 8rem;
269
+ }
270
+
271
+ .tebas-content {
272
+ padding: 2.5rem;
273
+ display: flex;
274
+ flex-direction: column;
275
+ gap: 2rem;
276
+ }
277
+
278
+ .tebas-status-view {
279
+ min-height: 14rem;
280
+ display: flex;
281
+ align-items: center;
282
+ justify-content: center;
283
+ text-align: center;
284
+ }
285
+
286
+ .tebas-loading {
287
+ display: flex;
288
+ flex-direction: column;
289
+ align-items: center;
290
+ gap: 1.5rem;
291
+ }
292
+
293
+ .tebas-spinner-wrapper {
294
+ position: relative;
295
+ }
296
+
297
+ .tebas-spinner {
298
+ width: 5rem;
299
+ height: 5rem;
300
+ background: var(--tebas-card);
301
+ border: 3px solid var(--tebas-border);
302
+ border-radius: 50%;
303
+ display: flex;
304
+ align-items: center;
305
+ justify-content: center;
306
+ z-index: 10;
307
+ position: relative;
308
+ }
309
+
310
+ .tebas-spinner [data-icon] {
311
+ width: 2.5rem;
312
+ height: 2.5rem;
313
+ color: var(--tebas-secondary);
314
+ }
315
+
316
+ .tebas-ping-ring {
317
+ position: absolute;
318
+ inset: 0;
319
+ background: var(--tebas-secondary);
320
+ border-radius: 50%;
321
+ opacity: 0.2;
322
+ animation: tebas-ping 1.5s cubic-bezier(0, 0, 0.2, 1) infinite;
323
+ }
324
+
325
+ @keyframes tebas-ping {
326
+ 75%,
327
+ 100% {
328
+ transform: scale(2);
329
+ opacity: 0;
330
+ }
331
+ }
332
+
333
+ .tebas-title-main {
334
+ margin: 0;
335
+ font-size: 1.5rem;
336
+ font-weight: 900;
337
+ color: var(--tebas-text);
338
+ text-transform: uppercase;
339
+ font-style: italic;
340
+ }
341
+
342
+ .tebas-subtitle-pulse {
343
+ margin: 0.5rem 0 0;
344
+ color: var(--tebas-text-muted);
345
+ font-weight: 500;
346
+ animation: tebas-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
347
+ }
348
+
349
+ @keyframes tebas-pulse {
350
+ 0%, 100% { opacity: 1; }
351
+ 50% { opacity: 0.5; }
352
+ }
353
+
354
+ .tebas-result-box {
355
+ width: 100%;
356
+ padding: 1.5rem;
357
+ border-radius: 1.25rem;
358
+ display: flex;
359
+ flex-direction: column;
360
+ gap: 1.5rem;
361
+ align-items: center;
362
+ border-width: 2px;
363
+ border-style: solid;
364
+ animation: tebas-slide-up 0.5s ease-out;
365
+ }
366
+
367
+ @media (min-width: 640px) {
368
+ .tebas-result-box {
369
+ flex-direction: row;
370
+ text-align: left;
371
+ }
372
+ }
373
+
374
+ @keyframes tebas-slide-up {
375
+ from {
376
+ opacity: 0;
377
+ transform: translateY(20px);
378
+ }
379
+ to {
380
+ opacity: 1;
381
+ transform: translateY(0);
382
+ }
383
+ }
384
+
385
+ .tebas-result-box.blocked {
386
+ background: rgba(244, 63, 94, 0.05);
387
+ border-color: var(--tebas-primary);
388
+ box-shadow: 0 0 30px rgba(244, 63, 94, 0.2);
389
+ }
390
+
391
+ .tebas-result-box.success {
392
+ background: rgba(16, 185, 129, 0.05);
393
+ border-color: var(--tebas-success);
394
+ }
395
+
396
+ .tebas-result-box.error {
397
+ background: rgba(100, 116, 139, 0.05);
398
+ border-color: var(--tebas-neutral);
399
+ }
400
+
401
+ .tebas-result-icon-badge {
402
+ position: relative;
403
+ flex-shrink: 0;
404
+ }
405
+
406
+ .tebas-result-main-icon {
407
+ width: 5rem;
408
+ height: 5rem;
409
+ border-radius: 1rem;
410
+ padding: 0.75rem;
411
+ background: var(--tebas-card);
412
+ border: 1px solid var(--tebas-border);
413
+ }
414
+
415
+ .blocked .tebas-result-main-icon { color: var(--tebas-primary); }
416
+ .success .tebas-result-main-icon { color: var(--tebas-success); }
417
+ .error .tebas-result-main-icon { color: var(--tebas-neutral); }
418
+
419
+ .tebas-badge-overlay {
420
+ position: absolute;
421
+ top: -0.5rem;
422
+ right: -0.5rem;
423
+ background: var(--tebas-primary);
424
+ color: white;
425
+ padding: 0.25rem;
426
+ border-radius: 50%;
427
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
428
+ }
429
+
430
+ .success .tebas-badge-overlay {
431
+ background: var(--tebas-success);
432
+ }
433
+
434
+ .tebas-result-status {
435
+ margin: 0;
436
+ font-size: 1.5rem;
437
+ font-weight: 900;
438
+ text-transform: uppercase;
439
+ font-style: italic;
440
+ transform: skew(-5deg);
441
+ }
442
+
443
+ .blocked .tebas-result-status { color: var(--tebas-primary); }
444
+ .success .tebas-result-status { color: var(--tebas-success); }
445
+
446
+ .tebas-result-details {
447
+ margin-top: 0.5rem;
448
+ display: flex;
449
+ flex-direction: column;
450
+ gap: 0.25rem;
451
+ }
452
+
453
+ .tebas-font-bold {
454
+ font-weight: 700;
455
+ }
456
+
457
+ .tebas-text-sm {
458
+ margin: 0;
459
+ font-size: 0.875rem;
460
+ opacity: 0.8;
461
+ }
462
+
463
+ .tebas-actions {
464
+ display: flex;
465
+ justify-content: center;
466
+ border-top: 1px solid var(--tebas-border);
467
+ padding-top: 1.5rem;
468
+ }
469
+
470
+ .tebas-btn {
471
+ background: var(--tebas-text);
472
+ color: var(--tebas-card);
473
+ border: none;
474
+ padding: 0.75rem 1.5rem;
475
+ border-radius: 0.75rem;
476
+ font-weight: 700;
477
+ display: flex;
478
+ align-items: center;
479
+ gap: 0.5rem;
480
+ cursor: pointer;
481
+ transition: transform 0.2s;
482
+ }
483
+
484
+ .theme-dark .tebas-btn {
485
+ background: #fff;
486
+ color: #000;
487
+ }
488
+
489
+ .tebas-btn:hover { transform: scale(1.05); }
490
+
491
+ .tebas-note {
492
+ background: rgba(245, 158, 11, 0.05);
493
+ border: 1px solid rgba(245, 158, 11, 0.2);
494
+ border-radius: 1.25rem;
495
+ padding: 1.25rem;
496
+ display: flex;
497
+ gap: 1rem;
498
+ align-items: flex-start;
499
+ }
500
+
501
+ .tebas-note-icon {
502
+ width: 1.5rem;
503
+ height: 1.5rem;
504
+ color: #f59e0b;
505
+ flex-shrink: 0;
506
+ }
507
+
508
+ .tebas-note-label {
509
+ display: block;
510
+ font-weight: 900;
511
+ text-transform: uppercase;
512
+ font-size: 0.75rem;
513
+ color: #b45309;
514
+ margin-bottom: 0.25rem;
515
+ }
516
+
517
+ .tebas-note-text {
518
+ margin: 0;
519
+ font-size: 0.8125rem;
520
+ line-height: 1.5;
521
+ color: var(--tebas-text-muted);
522
+ font-weight: 500;
523
+ }
524
+
525
+ .tebas-console {
526
+ background: var(--tebas-console-bg);
527
+ padding: 1.25rem;
528
+ font-size: 0.75rem;
529
+ color: #94a3b8;
530
+ }
531
+
532
+ .tebas-console-header {
533
+ display: flex;
534
+ justify-content: space-between;
535
+ align-items: center;
536
+ margin-bottom: 0.75rem;
537
+ opacity: 0.5;
538
+ }
539
+
540
+ .tebas-console-dots {
541
+ display: flex;
542
+ gap: 0.4rem;
543
+ }
544
+
545
+ .tebas-console-dots span {
546
+ width: 0.6rem;
547
+ height: 0.6rem;
548
+ border-radius: 50%;
549
+ background: #334155;
550
+ }
551
+
552
+ .tebas-console-body {
553
+ height: 8rem;
554
+ overflow-y: auto;
555
+ display: flex;
556
+ flex-direction: column;
557
+ gap: 0.25rem;
558
+ }
559
+
560
+ .tebas-log-line {
561
+ border-left: 2px solid transparent;
562
+ padding-left: 0.5rem;
563
+ }
564
+
565
+ .tebas-log-time {
566
+ opacity: 0.3;
567
+ margin-right: 0.5rem;
568
+ }
569
+
570
+ .tebas-log-error {
571
+ color: #f87171;
572
+ background: rgba(127, 29, 29, 0.2);
573
+ border-left-color: #7f1d1d;
574
+ }
575
+
576
+ .tebas-log-success {
577
+ color: #34d399;
578
+ background: rgba(6, 78, 59, 0.2);
579
+ border-left-color: #064e3b;
580
+ }
581
+
582
+ .tebas-log-neutral {
583
+ color: #94a3b8;
584
+ }
585
+
586
+ .hidden { display: none; }
587
+ </style>
@@ -0,0 +1,116 @@
1
+ import type { ProbeResult } from './types';
2
+
3
+ export class TebasEngine {
4
+ private targetIps: string[];
5
+
6
+ constructor(ips: string[]) {
7
+ this.targetIps = ips;
8
+ }
9
+
10
+ async checkGoogle(): Promise<boolean> {
11
+ try {
12
+ await fetch('https://www.google.es/favicon.ico', {
13
+ mode: 'no-cors',
14
+ cache: 'no-store',
15
+ });
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ async getIspInfo(): Promise<{ org?: string }> {
23
+ try {
24
+ const res = await fetch('https://ipapi.co/json/');
25
+ return await res.json();
26
+ } catch {
27
+ return {};
28
+ }
29
+ }
30
+
31
+ async checkDnsPoisoning(testDomain: string = 'www.cloudflare.com'): Promise<boolean> {
32
+ try {
33
+ const res = await fetch(`https://dns.google/resolve?name=${testDomain}&type=A`);
34
+ const data = (await res.json()) as { Answer?: Array<{ data: string }> };
35
+ const realIps = data.Answer?.map((a) => a.data) || [];
36
+
37
+ if (realIps.length === 0) return false;
38
+
39
+ const [domainWorks, ipWorks] = await Promise.all([
40
+ fetch(`https://${testDomain}/favicon.ico`, { mode: 'no-cors' })
41
+ .then(() => true)
42
+ .catch(() => false),
43
+ fetch(`https://${realIps[0]}/favicon.ico`, { mode: 'no-cors' })
44
+ .then(() => true)
45
+ .catch(() => false),
46
+ ]);
47
+
48
+ return !domainWorks && ipWorks;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ async probeIps(limit: number = 10): Promise<ProbeResult[]> {
55
+ const subset = [...this.targetIps].sort(() => 0.5 - Math.random()).slice(0, limit);
56
+ return Promise.all(subset.map(ip => this.probeOne(ip)));
57
+ }
58
+
59
+ private async probeOne(ip: string): Promise<ProbeResult> {
60
+ const start = performance.now();
61
+ const controller = new AbortController();
62
+ const timeoutId = setTimeout(() => controller.abort(), 2500);
63
+
64
+ try {
65
+ await fetch(`https://${ip}/favicon.ico`, {
66
+ mode: 'no-cors',
67
+ cache: 'no-store',
68
+ signal: controller.signal,
69
+ });
70
+ return { ip, status: 'ok', time: performance.now() - start };
71
+ } catch (e: unknown) {
72
+ const time = performance.now() - start;
73
+ if (e instanceof Error && e.name === 'AbortError') {
74
+ return { ip, status: 'blocked', time };
75
+ }
76
+ if (time > 2000) return { ip, status: 'blocked', time };
77
+
78
+ return { ip, status: 'ok', time };
79
+ } finally {
80
+ clearTimeout(timeoutId);
81
+ }
82
+ }
83
+
84
+ async runFullDiagnostic(): Promise<{
85
+ internet: boolean;
86
+ isp: string;
87
+ poisoned: boolean;
88
+ blocks: number;
89
+ results: ProbeResult[];
90
+ isBlocked: boolean;
91
+ }> {
92
+ const [google, ispInfo, poisoned] = await Promise.all([
93
+ this.checkGoogle(),
94
+ this.getIspInfo(),
95
+ this.checkDnsPoisoning()
96
+ ]);
97
+
98
+ if (!google) {
99
+ return {
100
+ internet: false, isp: 'Unknown', poisoned: false, blocks: 0, results: [], isBlocked: false
101
+ };
102
+ }
103
+
104
+ const results = await this.probeIps(10);
105
+ const blocks = results.filter(r => r.status === 'blocked').length;
106
+
107
+ return {
108
+ internet: true,
109
+ isp: ispInfo.org || 'Unknown',
110
+ poisoned,
111
+ blocks,
112
+ results,
113
+ isBlocked: poisoned || blocks >= 3,
114
+ };
115
+ }
116
+ }