@networkdiagnostics/sdk 0.1.0-pre.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.
@@ -0,0 +1,481 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+
5
+ // src/react/useUdpTest.ts
6
+
7
+ // src/errors.ts
8
+ var NetworkTestsError = class extends Error {
9
+ constructor(code, message) {
10
+ super(message);
11
+ this.name = "NetworkTestsError";
12
+ this.code = code;
13
+ }
14
+ };
15
+ var AbortError = class extends NetworkTestsError {
16
+ constructor(message = "Operation aborted") {
17
+ super("ABORTED", message);
18
+ this.name = "AbortError";
19
+ }
20
+ };
21
+ var TimeoutError = class extends NetworkTestsError {
22
+ constructor(message = "Operation timed out") {
23
+ super("TIMEOUT", message);
24
+ this.name = "TimeoutError";
25
+ }
26
+ };
27
+
28
+ // src/udp/webrtc.ts
29
+ function createPeerConnection(iceServers) {
30
+ return new RTCPeerConnection({ iceServers });
31
+ }
32
+ function setupDataChannel(pc) {
33
+ const channel = pc.createDataChannel("test", { ordered: false });
34
+ const ready = new Promise((resolve, reject) => {
35
+ const onOpen = () => {
36
+ cleanup();
37
+ resolve();
38
+ };
39
+ const onError = () => {
40
+ cleanup();
41
+ reject(new NetworkTestsError("DC_ERROR", "DataChannel error before open"));
42
+ };
43
+ const onClose = () => {
44
+ cleanup();
45
+ reject(new NetworkTestsError("DC_CLOSED", "DataChannel closed before open"));
46
+ };
47
+ function cleanup() {
48
+ channel.removeEventListener("open", onOpen);
49
+ channel.removeEventListener("error", onError);
50
+ channel.removeEventListener("close", onClose);
51
+ }
52
+ channel.addEventListener("open", onOpen);
53
+ channel.addEventListener("error", onError);
54
+ channel.addEventListener("close", onClose);
55
+ });
56
+ return { channel, ready };
57
+ }
58
+ async function createOffer(pc) {
59
+ const offer = await pc.createOffer();
60
+ await pc.setLocalDescription(offer);
61
+ return offer;
62
+ }
63
+ async function applyAnswer(pc, sdp) {
64
+ await pc.setRemoteDescription({ type: "answer", sdp });
65
+ }
66
+
67
+ // src/udp/packetLoop.ts
68
+ function startPacketLoop(opts) {
69
+ const { channel, onProgress } = opts;
70
+ const progressMs = opts.progressIntervalMs ?? 100;
71
+ const startTs = Date.now();
72
+ let received = 0;
73
+ let highestSeq = -1;
74
+ let stopped = false;
75
+ const onMsg = (ev) => {
76
+ if (stopped) return;
77
+ try {
78
+ const msg = JSON.parse(ev.data);
79
+ if (msg && msg.type === "packet" && typeof msg.seq === "number") {
80
+ received++;
81
+ if (msg.seq > highestSeq) highestSeq = msg.seq;
82
+ try {
83
+ channel.send(JSON.stringify({ type: "ack", seq: msg.seq }));
84
+ } catch {
85
+ }
86
+ }
87
+ } catch {
88
+ }
89
+ };
90
+ channel.addEventListener("message", onMsg);
91
+ let progressTimer = null;
92
+ if (onProgress) {
93
+ progressTimer = setInterval(() => {
94
+ if (stopped) return;
95
+ const sent = highestSeq + 1;
96
+ const loss = sent > 0 ? (sent - received) / sent * 100 : 0;
97
+ onProgress({
98
+ packetsSent: sent,
99
+ packetsReceived: received,
100
+ lossPercent: Math.round(loss * 100) / 100,
101
+ elapsedMs: Date.now() - startTs
102
+ });
103
+ }, progressMs);
104
+ }
105
+ return {
106
+ stop() {
107
+ if (stopped) return;
108
+ stopped = true;
109
+ channel.removeEventListener("message", onMsg);
110
+ if (progressTimer) clearInterval(progressTimer);
111
+ }
112
+ };
113
+ }
114
+
115
+ // src/udp/signaling.ts
116
+ function isServerMsg(value) {
117
+ return typeof value === "object" && value !== null && typeof value.type === "string";
118
+ }
119
+
120
+ // src/udp/index.ts
121
+ var DEFAULT_ICE_SERVERS = [
122
+ { urls: "stun:stun.l.google.com:19302" }
123
+ ];
124
+ async function runUdpTest(session, options = {}) {
125
+ const startedAt = Date.now();
126
+ const { signal, onProgress } = options;
127
+ const timeoutMs = options.timeoutMs ?? ((session.duration ?? 30) + 10) * 1e3;
128
+ if (signal?.aborted) throw new AbortError();
129
+ const ws = new WebSocket(session.wsUrl);
130
+ ws.binaryType = "arraybuffer";
131
+ let pc = null;
132
+ let dataChannel = null;
133
+ let packetLoop = null;
134
+ let timeoutHandle = null;
135
+ let abortHandler = null;
136
+ const cleanup = () => {
137
+ if (timeoutHandle) clearTimeout(timeoutHandle);
138
+ if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
139
+ packetLoop?.stop();
140
+ try {
141
+ dataChannel?.close();
142
+ } catch {
143
+ }
144
+ try {
145
+ pc?.close();
146
+ } catch {
147
+ }
148
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
149
+ try {
150
+ ws.close();
151
+ } catch {
152
+ }
153
+ }
154
+ };
155
+ return new Promise((resolve, reject) => {
156
+ const fail = (err) => {
157
+ cleanup();
158
+ reject(err);
159
+ };
160
+ const succeed = (result) => {
161
+ cleanup();
162
+ resolve(result);
163
+ };
164
+ timeoutHandle = setTimeout(
165
+ () => fail(new TimeoutError(`UDP test exceeded ${timeoutMs}ms`)),
166
+ timeoutMs
167
+ );
168
+ if (signal) {
169
+ abortHandler = () => fail(new AbortError());
170
+ signal.addEventListener("abort", abortHandler, { once: true });
171
+ }
172
+ const send = (msg) => {
173
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg));
174
+ };
175
+ ws.addEventListener(
176
+ "error",
177
+ () => fail(new NetworkTestsError("WS_ERROR", "WebSocket error"))
178
+ );
179
+ ws.addEventListener("close", (ev) => {
180
+ if (ws.readyState === WebSocket.CLOSED) {
181
+ const reason = ev.reason || `WebSocket closed (code ${ev.code})`;
182
+ fail(new NetworkTestsError("WS_CLOSED", reason));
183
+ }
184
+ });
185
+ ws.addEventListener("open", async () => {
186
+ });
187
+ ws.addEventListener("message", async (ev) => {
188
+ let msg;
189
+ try {
190
+ msg = JSON.parse(String(ev.data));
191
+ } catch {
192
+ return;
193
+ }
194
+ if (!isServerMsg(msg)) return;
195
+ const m = msg;
196
+ try {
197
+ switch (m.type) {
198
+ case "welcome": {
199
+ const iceServers = [
200
+ ...session.iceServers ?? DEFAULT_ICE_SERVERS
201
+ ];
202
+ if (m.turn) {
203
+ iceServers.push({
204
+ urls: m.turn.urls,
205
+ username: m.turn.username,
206
+ credential: m.turn.credential
207
+ });
208
+ }
209
+ pc = createPeerConnection(iceServers);
210
+ pc.addEventListener("icecandidate", (e) => {
211
+ if (e.candidate) {
212
+ send({ type: "ice-candidate", candidate: e.candidate.toJSON() });
213
+ }
214
+ });
215
+ const dcSetup = setupDataChannel(pc);
216
+ dataChannel = dcSetup.channel;
217
+ const offer = await createOffer(pc);
218
+ send({ type: "offer", sdp: offer.sdp ?? "" });
219
+ dcSetup.ready.then(() => send({ type: "datachannel-ready" })).catch((err) => fail(toError(err)));
220
+ break;
221
+ }
222
+ case "answer": {
223
+ if (!pc) throw new NetworkTestsError("BAD_STATE", "answer before welcome");
224
+ await applyAnswer(pc, m.sdp.sdp);
225
+ break;
226
+ }
227
+ case "ice-candidate": {
228
+ if (!pc) return;
229
+ try {
230
+ await pc.addIceCandidate(m.candidate);
231
+ } catch {
232
+ }
233
+ break;
234
+ }
235
+ case "datachannel-open": {
236
+ if (!dataChannel) throw new NetworkTestsError("BAD_STATE", "no DC");
237
+ packetLoop = startPacketLoop({ channel: dataChannel, onProgress });
238
+ send({
239
+ type: "start-packet-test",
240
+ duration: session.duration,
241
+ rate: session.rate,
242
+ packetSize: session.packetSize
243
+ });
244
+ break;
245
+ }
246
+ case "test-started":
247
+ break;
248
+ case "test-complete": {
249
+ const r = m.results;
250
+ succeed({
251
+ sessionId: session.sessionId,
252
+ packetsSent: r.packetsSent,
253
+ packetsReceived: r.packetsReceived,
254
+ packetsLost: r.packetsLost,
255
+ lossPercent: r.lossPercent,
256
+ jitter: r.jitter,
257
+ latency: {
258
+ avg: r.latency.avg,
259
+ min: r.latency.min,
260
+ max: r.latency.max,
261
+ p50: r.latency.p50,
262
+ p95: r.latency.p95,
263
+ samples: r.latency.samples
264
+ },
265
+ durationMs: Date.now() - startedAt,
266
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
267
+ });
268
+ break;
269
+ }
270
+ case "test-stopped":
271
+ fail(new NetworkTestsError("TEST_STOPPED", "Server stopped the test"));
272
+ break;
273
+ case "error":
274
+ fail(new NetworkTestsError("SERVER_ERROR", m.message));
275
+ break;
276
+ }
277
+ } catch (err) {
278
+ fail(toError(err));
279
+ }
280
+ });
281
+ });
282
+ }
283
+ function toError(value) {
284
+ if (value instanceof Error) return value;
285
+ return new NetworkTestsError("UNKNOWN", String(value));
286
+ }
287
+
288
+ // src/react/useUdpTest.ts
289
+ var INITIAL = {
290
+ result: null,
291
+ error: null,
292
+ isRunning: false,
293
+ progress: null
294
+ };
295
+ function useUdpTest() {
296
+ const [state, setState] = react.useState(INITIAL);
297
+ const abortRef = react.useRef(null);
298
+ const reset = react.useCallback(() => {
299
+ abortRef.current?.abort();
300
+ abortRef.current = null;
301
+ setState(INITIAL);
302
+ }, []);
303
+ const cancel = react.useCallback(() => {
304
+ abortRef.current?.abort();
305
+ }, []);
306
+ const run = react.useCallback(async (session, options) => {
307
+ abortRef.current?.abort();
308
+ const ctrl = new AbortController();
309
+ abortRef.current = ctrl;
310
+ setState({ ...INITIAL, isRunning: true });
311
+ try {
312
+ const result = await runUdpTest(session, {
313
+ ...options,
314
+ signal: ctrl.signal,
315
+ onProgress: (progress) => {
316
+ setState((s) => s.isRunning ? { ...s, progress } : s);
317
+ }
318
+ });
319
+ setState({ result, error: null, isRunning: false, progress: null });
320
+ return result;
321
+ } catch (err) {
322
+ const error = err instanceof Error ? err : new Error(String(err));
323
+ setState({ result: null, error, isRunning: false, progress: null });
324
+ throw error;
325
+ } finally {
326
+ if (abortRef.current === ctrl) abortRef.current = null;
327
+ }
328
+ }, []);
329
+ return { ...state, run, cancel, reset };
330
+ }
331
+
332
+ // src/dnsleak/trigger.ts
333
+ var ABORT_AFTER_MS = 5e3;
334
+ function triggerDnsResolution(fqdn, signal) {
335
+ if (signal?.aborted) return Promise.resolve();
336
+ if (typeof Image !== "undefined") {
337
+ return triggerViaImage(fqdn, signal);
338
+ }
339
+ return triggerViaFetch(fqdn, signal);
340
+ }
341
+ function triggerViaImage(fqdn, signal) {
342
+ return new Promise((resolve) => {
343
+ const img = new Image();
344
+ let settled = false;
345
+ const finish = () => {
346
+ if (settled) return;
347
+ settled = true;
348
+ img.src = "";
349
+ resolve();
350
+ };
351
+ const onAbort = () => finish();
352
+ signal?.addEventListener("abort", onAbort, { once: true });
353
+ const timer = setTimeout(finish, ABORT_AFTER_MS);
354
+ img.onload = () => {
355
+ clearTimeout(timer);
356
+ finish();
357
+ };
358
+ img.onerror = () => {
359
+ clearTimeout(timer);
360
+ finish();
361
+ };
362
+ img.src = `https://${fqdn}/_/leak.gif?t=${Date.now()}-${Math.random().toString(36).slice(2)}`;
363
+ });
364
+ }
365
+ function triggerViaFetch(fqdn, signal) {
366
+ const controller = new AbortController();
367
+ const abortSignal = signal ? mergeSignals(signal, controller.signal) : controller.signal;
368
+ const timer = setTimeout(() => controller.abort(), ABORT_AFTER_MS);
369
+ return fetch(`https://${fqdn}/_/leak?t=${Date.now()}`, {
370
+ method: "GET",
371
+ mode: "no-cors",
372
+ cache: "no-store",
373
+ signal: abortSignal
374
+ }).catch(() => void 0).finally(() => clearTimeout(timer)).then(() => void 0);
375
+ }
376
+ function mergeSignals(a, b) {
377
+ const c = new AbortController();
378
+ const onAbort = () => c.abort();
379
+ if (a.aborted || b.aborted) c.abort();
380
+ else {
381
+ a.addEventListener("abort", onAbort, { once: true });
382
+ b.addEventListener("abort", onAbort, { once: true });
383
+ }
384
+ return c.signal;
385
+ }
386
+
387
+ // src/dnsleak/index.ts
388
+ async function runDnsLeakTest(session, options = {}) {
389
+ if (options.signal?.aborted) throw new AbortError();
390
+ const timeoutMs = options.timeoutMs ?? 15e3;
391
+ let timeoutHandle = null;
392
+ const timeoutController = new AbortController();
393
+ const combinedSignal = mergeSignals2(options.signal, timeoutController.signal);
394
+ const timeoutPromise = new Promise((_, reject) => {
395
+ timeoutHandle = setTimeout(() => {
396
+ timeoutController.abort();
397
+ reject(new TimeoutError(`DNS-leak test exceeded ${timeoutMs}ms`));
398
+ }, timeoutMs);
399
+ });
400
+ const abortPromise = new Promise((_, reject) => {
401
+ options.signal?.addEventListener(
402
+ "abort",
403
+ () => reject(new AbortError()),
404
+ { once: true }
405
+ );
406
+ });
407
+ const triggers = session.fqdns.map(
408
+ (fqdn) => triggerDnsResolution(fqdn, combinedSignal).then(() => {
409
+ options.onResolved?.(fqdn);
410
+ })
411
+ );
412
+ try {
413
+ await Promise.race([
414
+ Promise.all(triggers),
415
+ timeoutPromise,
416
+ abortPromise
417
+ ]);
418
+ } finally {
419
+ if (timeoutHandle) clearTimeout(timeoutHandle);
420
+ }
421
+ }
422
+ function mergeSignals2(a, b) {
423
+ if (!a) return b;
424
+ const c = new AbortController();
425
+ if (a.aborted || b.aborted) c.abort();
426
+ else {
427
+ a.addEventListener("abort", () => c.abort(), { once: true });
428
+ b.addEventListener("abort", () => c.abort(), { once: true });
429
+ }
430
+ return c.signal;
431
+ }
432
+
433
+ // src/react/useDnsLeakTest.ts
434
+ var INITIAL2 = {
435
+ isRunning: false,
436
+ error: null,
437
+ resolved: [],
438
+ complete: false
439
+ };
440
+ function useDnsLeakTest() {
441
+ const [state, setState] = react.useState(INITIAL2);
442
+ const abortRef = react.useRef(null);
443
+ const reset = react.useCallback(() => {
444
+ abortRef.current?.abort();
445
+ abortRef.current = null;
446
+ setState(INITIAL2);
447
+ }, []);
448
+ const cancel = react.useCallback(() => {
449
+ abortRef.current?.abort();
450
+ }, []);
451
+ const run = react.useCallback(async (session, options) => {
452
+ abortRef.current?.abort();
453
+ const ctrl = new AbortController();
454
+ abortRef.current = ctrl;
455
+ setState({ ...INITIAL2, isRunning: true });
456
+ try {
457
+ await runDnsLeakTest(session, {
458
+ ...options,
459
+ signal: ctrl.signal,
460
+ onResolved: (fqdn) => {
461
+ setState(
462
+ (s) => s.isRunning ? { ...s, resolved: [...s.resolved, fqdn] } : s
463
+ );
464
+ }
465
+ });
466
+ setState((s) => ({ ...s, isRunning: false, complete: true }));
467
+ } catch (err) {
468
+ const error = err instanceof Error ? err : new Error(String(err));
469
+ setState((s) => ({ ...s, isRunning: false, error }));
470
+ throw error;
471
+ } finally {
472
+ if (abortRef.current === ctrl) abortRef.current = null;
473
+ }
474
+ }, []);
475
+ return { ...state, run, cancel, reset };
476
+ }
477
+
478
+ exports.useDnsLeakTest = useDnsLeakTest;
479
+ exports.useUdpTest = useUdpTest;
480
+ //# sourceMappingURL=index.cjs.map
481
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/errors.ts","../../src/udp/webrtc.ts","../../src/udp/packetLoop.ts","../../src/udp/signaling.ts","../../src/udp/index.ts","../../src/react/useUdpTest.ts","../../src/dnsleak/trigger.ts","../../src/dnsleak/index.ts","../../src/react/useDnsLeakTest.ts"],"names":["useState","useRef","useCallback","mergeSignals","INITIAL"],"mappings":";;;;;;;AAIO,IAAM,iBAAA,GAAN,cAAgC,KAAA,CAAM;AAAA,EAG3C,WAAA,CAAY,MAAc,OAAA,EAAiB;AACzC,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,mBAAA;AACZ,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AAAA,EACd;AACF,CAAA;AAGO,IAAM,UAAA,GAAN,cAAyB,iBAAA,CAAkB;AAAA,EAChD,WAAA,CAAY,UAAU,mBAAA,EAAqB;AACzC,IAAA,KAAA,CAAM,WAAW,OAAO,CAAA;AACxB,IAAA,IAAA,CAAK,IAAA,GAAO,YAAA;AAAA,EACd;AACF,CAAA;AAGO,IAAM,YAAA,GAAN,cAA2B,iBAAA,CAAkB;AAAA,EAClD,WAAA,CAAY,UAAU,qBAAA,EAAuB;AAC3C,IAAA,KAAA,CAAM,WAAW,OAAO,CAAA;AACxB,IAAA,IAAA,CAAK,IAAA,GAAO,cAAA;AAAA,EACd;AACF,CAAA;;;ACbO,SAAS,qBAAqB,UAAA,EAA+C;AAClF,EAAA,OAAO,IAAI,iBAAA,CAAkB,EAAE,UAAA,EAAY,CAAA;AAC7C;AAEO,SAAS,iBAAiB,EAAA,EAG/B;AAIA,EAAA,MAAM,UAAU,EAAA,CAAG,iBAAA,CAAkB,QAAQ,EAAE,OAAA,EAAS,OAAO,CAAA;AAE/D,EAAA,MAAM,KAAA,GAAQ,IAAI,OAAA,CAAc,CAAC,SAAS,MAAA,KAAW;AACnD,IAAA,MAAM,SAAS,MAAM;AACnB,MAAA,OAAA,EAAQ;AACR,MAAA,OAAA,EAAQ;AAAA,IACV,CAAA;AACA,IAAA,MAAM,UAAU,MAAM;AACpB,MAAA,OAAA,EAAQ;AACR,MAAA,MAAA,CAAO,IAAI,iBAAA,CAAkB,UAAA,EAAY,+BAA+B,CAAC,CAAA;AAAA,IAC3E,CAAA;AACA,IAAA,MAAM,UAAU,MAAM;AACpB,MAAA,OAAA,EAAQ;AACR,MAAA,MAAA,CAAO,IAAI,iBAAA,CAAkB,WAAA,EAAa,gCAAgC,CAAC,CAAA;AAAA,IAC7E,CAAA;AACA,IAAA,SAAS,OAAA,GAAU;AACjB,MAAA,OAAA,CAAQ,mBAAA,CAAoB,QAAQ,MAAM,CAAA;AAC1C,MAAA,OAAA,CAAQ,mBAAA,CAAoB,SAAS,OAAO,CAAA;AAC5C,MAAA,OAAA,CAAQ,mBAAA,CAAoB,SAAS,OAAO,CAAA;AAAA,IAC9C;AACA,IAAA,OAAA,CAAQ,gBAAA,CAAiB,QAAQ,MAAM,CAAA;AACvC,IAAA,OAAA,CAAQ,gBAAA,CAAiB,SAAS,OAAO,CAAA;AACzC,IAAA,OAAA,CAAQ,gBAAA,CAAiB,SAAS,OAAO,CAAA;AAAA,EAC3C,CAAC,CAAA;AAED,EAAA,OAAO,EAAE,SAAS,KAAA,EAAM;AAC1B;AAEA,eAAsB,YAAY,EAAA,EAA2D;AAC3F,EAAA,MAAM,KAAA,GAAQ,MAAM,EAAA,CAAG,WAAA,EAAY;AACnC,EAAA,MAAM,EAAA,CAAG,oBAAoB,KAAK,CAAA;AAClC,EAAA,OAAO,KAAA;AACT;AAEA,eAAsB,WAAA,CACpB,IACA,GAAA,EACe;AACf,EAAA,MAAM,GAAG,oBAAA,CAAqB,EAAE,IAAA,EAAM,QAAA,EAAU,KAAK,CAAA;AACvD;;;ACvCO,SAAS,gBAAgB,IAAA,EAA2C;AACzE,EAAA,MAAM,EAAE,OAAA,EAAS,UAAA,EAAW,GAAI,IAAA;AAChC,EAAA,MAAM,UAAA,GAAa,KAAK,kBAAA,IAAsB,GAAA;AAC9C,EAAA,MAAM,OAAA,GAAU,KAAK,GAAA,EAAI;AAIzB,EAAA,IAAI,QAAA,GAAW,CAAA;AAKf,EAAA,IAAI,UAAA,GAAa,EAAA;AACjB,EAAA,IAAI,OAAA,GAAU,KAAA;AAEd,EAAA,MAAM,KAAA,GAAQ,CAAC,EAAA,KAA6B;AAC1C,IAAA,IAAI,OAAA,EAAS;AACb,IAAA,IAAI;AACF,MAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,EAAA,CAAG,IAAI,CAAA;AAC9B,MAAA,IAAI,OAAO,GAAA,CAAI,IAAA,KAAS,YAAY,OAAO,GAAA,CAAI,QAAQ,QAAA,EAAU;AAC/D,QAAA,QAAA,EAAA;AACA,QAAA,IAAI,GAAA,CAAI,GAAA,GAAM,UAAA,EAAY,UAAA,GAAa,GAAA,CAAI,GAAA;AAI3C,QAAA,IAAI;AACF,UAAA,OAAA,CAAQ,IAAA,CAAK,IAAA,CAAK,SAAA,CAAU,EAAE,IAAA,EAAM,OAAO,GAAA,EAAK,GAAA,CAAI,GAAA,EAAK,CAAC,CAAA;AAAA,QAC5D,CAAA,CAAA,MAAQ;AAAA,QAAiD;AAAA,MAC3D;AAAA,IACF,CAAA,CAAA,MAAQ;AAAA,IAAkC;AAAA,EAC5C,CAAA;AAEA,EAAA,OAAA,CAAQ,gBAAA,CAAiB,WAAW,KAAK,CAAA;AAEzC,EAAA,IAAI,aAAA,GAAuD,IAAA;AAC3D,EAAA,IAAI,UAAA,EAAY;AACd,IAAA,aAAA,GAAgB,YAAY,MAAM;AAChC,MAAA,IAAI,OAAA,EAAS;AAIb,MAAA,MAAM,OAAO,UAAA,GAAa,CAAA;AAC1B,MAAA,MAAM,OAAO,IAAA,GAAO,CAAA,GAAA,CAAM,IAAA,GAAO,QAAA,IAAY,OAAQ,GAAA,GAAM,CAAA;AAC3D,MAAA,UAAA,CAAW;AAAA,QACT,WAAA,EAAa,IAAA;AAAA,QACb,eAAA,EAAiB,QAAA;AAAA,QACjB,WAAA,EAAa,IAAA,CAAK,KAAA,CAAM,IAAA,GAAO,GAAG,CAAA,GAAI,GAAA;AAAA,QACtC,SAAA,EAAW,IAAA,CAAK,GAAA,EAAI,GAAI;AAAA,OACzB,CAAA;AAAA,IACH,GAAG,UAAU,CAAA;AAAA,EACf;AAEA,EAAA,OAAO;AAAA,IACL,IAAA,GAAO;AACL,MAAA,IAAI,OAAA,EAAS;AACb,MAAA,OAAA,GAAU,IAAA;AACV,MAAA,OAAA,CAAQ,mBAAA,CAAoB,WAAW,KAAK,CAAA;AAC5C,MAAA,IAAI,aAAA,gBAA6B,aAAa,CAAA;AAAA,IAChD;AAAA,GACF;AACF;;;AChBO,SAAS,YAAY,KAAA,EAAoC;AAC9D,EAAA,OACE,OAAO,KAAA,KAAU,QAAA,IACjB,UAAU,IAAA,IACV,OAAQ,MAA6B,IAAA,KAAS,QAAA;AAElD;;;AC3CA,IAAM,mBAAA,GAAsC;AAAA,EAC1C,EAAE,MAAM,8BAAA;AACV,CAAA;AAEA,eAAsB,UAAA,CACpB,OAAA,EACA,OAAA,GAAyB,EAAC,EACN;AACpB,EAAA,MAAM,SAAA,GAAY,KAAK,GAAA,EAAI;AAC3B,EAAA,MAAM,EAAE,MAAA,EAAQ,UAAA,EAAW,GAAI,OAAA;AAC/B,EAAA,MAAM,YAAY,OAAA,CAAQ,SAAA,IAAA,CAAA,CAAe,OAAA,CAAQ,QAAA,IAAY,MAAM,EAAA,IAAM,GAAA;AAEzE,EAAA,IAAI,MAAA,EAAQ,OAAA,EAAS,MAAM,IAAI,UAAA,EAAW;AAE1C,EAAA,MAAM,EAAA,GAAK,IAAI,SAAA,CAAU,OAAA,CAAQ,KAAK,CAAA;AACtC,EAAA,EAAA,CAAG,UAAA,GAAa,aAAA;AAEhB,EAAA,IAAI,EAAA,GAA+B,IAAA;AACnC,EAAA,IAAI,WAAA,GAAqC,IAAA;AACzC,EAAA,IAAI,UAAA,GAAwD,IAAA;AAC5D,EAAA,IAAI,aAAA,GAAsD,IAAA;AAC1D,EAAA,IAAI,YAAA,GAAoC,IAAA;AAExC,EAAA,MAAM,UAAU,MAAM;AACpB,IAAA,IAAI,aAAA,eAA4B,aAAa,CAAA;AAC7C,IAAA,IAAI,YAAA,IAAgB,MAAA,EAAQ,MAAA,CAAO,mBAAA,CAAoB,SAAS,YAAY,CAAA;AAC5E,IAAA,UAAA,EAAY,IAAA,EAAK;AACjB,IAAA,IAAI;AAAE,MAAA,WAAA,EAAa,KAAA,EAAM;AAAA,IAAG,CAAA,CAAA,MAAQ;AAAA,IAAe;AACnD,IAAA,IAAI;AAAE,MAAA,EAAA,EAAI,KAAA,EAAM;AAAA,IAAG,CAAA,CAAA,MAAQ;AAAA,IAAe;AAC1C,IAAA,IAAI,GAAG,UAAA,KAAe,SAAA,CAAU,QAAQ,EAAA,CAAG,UAAA,KAAe,UAAU,UAAA,EAAY;AAC9E,MAAA,IAAI;AAAE,QAAA,EAAA,CAAG,KAAA,EAAM;AAAA,MAAG,CAAA,CAAA,MAAQ;AAAA,MAAe;AAAA,IAC3C;AAAA,EACF,CAAA;AAEA,EAAA,OAAO,IAAI,OAAA,CAAmB,CAAC,OAAA,EAAS,MAAA,KAAW;AACjD,IAAA,MAAM,IAAA,GAAO,CAAC,GAAA,KAAe;AAAE,MAAA,OAAA,EAAQ;AAAG,MAAA,MAAA,CAAO,GAAG,CAAA;AAAA,IAAG,CAAA;AACvD,IAAA,MAAM,OAAA,GAAU,CAAC,MAAA,KAAsB;AAAE,MAAA,OAAA,EAAQ;AAAG,MAAA,OAAA,CAAQ,MAAM,CAAA;AAAA,IAAG,CAAA;AAErE,IAAA,aAAA,GAAgB,UAAA;AAAA,MACd,MAAM,IAAA,CAAK,IAAI,aAAa,CAAA,kBAAA,EAAqB,SAAS,IAAI,CAAC,CAAA;AAAA,MAC/D;AAAA,KACF;AACA,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,YAAA,GAAe,MAAM,IAAA,CAAK,IAAI,UAAA,EAAY,CAAA;AAC1C,MAAA,MAAA,CAAO,iBAAiB,OAAA,EAAS,YAAA,EAAc,EAAE,IAAA,EAAM,MAAM,CAAA;AAAA,IAC/D;AAEA,IAAA,MAAM,IAAA,GAAO,CAAC,GAAA,KAAmB;AAC/B,MAAA,IAAI,EAAA,CAAG,eAAe,SAAA,CAAU,IAAA,KAAS,IAAA,CAAK,IAAA,CAAK,SAAA,CAAU,GAAG,CAAC,CAAA;AAAA,IACnE,CAAA;AAEA,IAAA,EAAA,CAAG,gBAAA;AAAA,MAAiB,OAAA;AAAA,MAAS,MAC3B,IAAA,CAAK,IAAI,iBAAA,CAAkB,UAAA,EAAY,iBAAiB,CAAC;AAAA,KAC3D;AAEA,IAAA,EAAA,CAAG,gBAAA,CAAiB,OAAA,EAAS,CAAC,EAAA,KAAO;AAGnC,MAAA,IAAI,EAAA,CAAG,UAAA,KAAe,SAAA,CAAU,MAAA,EAAQ;AACtC,QAAA,MAAM,MAAA,GAAS,EAAA,CAAG,MAAA,IAAU,CAAA,uBAAA,EAA0B,GAAG,IAAI,CAAA,CAAA,CAAA;AAC7D,QAAA,IAAA,CAAK,IAAI,iBAAA,CAAkB,WAAA,EAAa,MAAM,CAAC,CAAA;AAAA,MACjD;AAAA,IACF,CAAC,CAAA;AAED,IAAA,EAAA,CAAG,gBAAA,CAAiB,QAAQ,YAAY;AAMtC,IACF,CAAC,CAAA;AAED,IAAA,EAAA,CAAG,gBAAA,CAAiB,SAAA,EAAW,OAAO,EAAA,KAAO;AAC3C,MAAA,IAAI,GAAA;AACJ,MAAA,IAAI;AAAE,QAAA,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,MAAA,CAAO,EAAA,CAAG,IAAI,CAAC,CAAA;AAAA,MAAG,CAAA,CAAA,MAAQ;AAAE,QAAA;AAAA,MAAQ;AAC3D,MAAA,IAAI,CAAC,WAAA,CAAY,GAAG,CAAA,EAAG;AACvB,MAAA,MAAM,CAAA,GAAI,GAAA;AAEV,MAAA,IAAI;AACF,QAAA,QAAQ,EAAE,IAAA;AAAM,UACd,KAAK,SAAA,EAAW;AACd,YAAA,MAAM,UAAA,GAA6B;AAAA,cACjC,GAAI,QAAQ,UAAA,IAAc;AAAA,aAC5B;AACA,YAAA,IAAI,EAAE,IAAA,EAAM;AACV,cAAA,UAAA,CAAW,IAAA,CAAK;AAAA,gBACd,IAAA,EAAM,EAAE,IAAA,CAAK,IAAA;AAAA,gBACb,QAAA,EAAU,EAAE,IAAA,CAAK,QAAA;AAAA,gBACjB,UAAA,EAAY,EAAE,IAAA,CAAK;AAAA,eACpB,CAAA;AAAA,YACH;AACA,YAAA,EAAA,GAAK,qBAAqB,UAAU,CAAA;AACpC,YAAA,EAAA,CAAG,gBAAA,CAAiB,cAAA,EAAgB,CAAC,CAAA,KAAM;AACzC,cAAA,IAAI,EAAE,SAAA,EAAW;AACf,gBAAA,IAAA,CAAK,EAAE,MAAM,eAAA,EAAiB,SAAA,EAAW,EAAE,SAAA,CAAU,MAAA,IAAU,CAAA;AAAA,cACjE;AAAA,YACF,CAAC,CAAA;AACD,YAAA,MAAM,OAAA,GAAU,iBAAiB,EAAE,CAAA;AACnC,YAAA,WAAA,GAAc,OAAA,CAAQ,OAAA;AACtB,YAAA,MAAM,KAAA,GAAQ,MAAM,WAAA,CAAY,EAAE,CAAA;AAClC,YAAA,IAAA,CAAK,EAAE,IAAA,EAAM,OAAA,EAAS,KAAK,KAAA,CAAM,GAAA,IAAO,IAAI,CAAA;AAE5C,YAAA,OAAA,CAAQ,MACL,IAAA,CAAK,MAAM,IAAA,CAAK,EAAE,MAAM,mBAAA,EAAqB,CAAC,CAAA,CAC9C,MAAM,CAAC,GAAA,KAAQ,KAAK,OAAA,CAAQ,GAAG,CAAC,CAAC,CAAA;AACpC,YAAA;AAAA,UACF;AAAA,UAEA,KAAK,QAAA,EAAU;AACb,YAAA,IAAI,CAAC,EAAA,EAAI,MAAM,IAAI,iBAAA,CAAkB,aAAa,uBAAuB,CAAA;AACzE,YAAA,MAAM,WAAA,CAAY,EAAA,EAAI,CAAA,CAAE,GAAA,CAAI,GAAG,CAAA;AAC/B,YAAA;AAAA,UACF;AAAA,UAEA,KAAK,eAAA,EAAiB;AACpB,YAAA,IAAI,CAAC,EAAA,EAAI;AACT,YAAA,IAAI;AAAE,cAAA,MAAM,EAAA,CAAG,eAAA,CAAgB,CAAA,CAAE,SAAS,CAAA;AAAA,YAAG,CAAA,CAAA,MAAQ;AAAA,YAAqB;AAC1E,YAAA;AAAA,UACF;AAAA,UAEA,KAAK,kBAAA,EAAoB;AACvB,YAAA,IAAI,CAAC,WAAA,EAAa,MAAM,IAAI,iBAAA,CAAkB,aAAa,OAAO,CAAA;AAClE,YAAA,UAAA,GAAa,eAAA,CAAgB,EAAE,OAAA,EAAS,WAAA,EAAa,YAAY,CAAA;AACjE,YAAA,IAAA,CAAK;AAAA,cACH,IAAA,EAAM,mBAAA;AAAA,cACN,UAAU,OAAA,CAAQ,QAAA;AAAA,cAClB,MAAM,OAAA,CAAQ,IAAA;AAAA,cACd,YAAY,OAAA,CAAQ;AAAA,aACrB,CAAA;AACD,YAAA;AAAA,UACF;AAAA,UAEA,KAAK,cAAA;AAGH,YAAA;AAAA,UAEF,KAAK,eAAA,EAAiB;AACpB,YAAA,MAAM,IAAI,CAAA,CAAE,OAAA;AACZ,YAAA,OAAA,CAAQ;AAAA,cACN,WAAW,OAAA,CAAQ,SAAA;AAAA,cACnB,aAAa,CAAA,CAAE,WAAA;AAAA,cACf,iBAAiB,CAAA,CAAE,eAAA;AAAA,cACnB,aAAa,CAAA,CAAE,WAAA;AAAA,cACf,aAAa,CAAA,CAAE,WAAA;AAAA,cACf,QAAQ,CAAA,CAAE,MAAA;AAAA,cACV,OAAA,EAAS;AAAA,gBACP,GAAA,EAAK,EAAE,OAAA,CAAQ,GAAA;AAAA,gBACf,GAAA,EAAK,EAAE,OAAA,CAAQ,GAAA;AAAA,gBACf,GAAA,EAAK,EAAE,OAAA,CAAQ,GAAA;AAAA,gBACf,GAAA,EAAK,EAAE,OAAA,CAAQ,GAAA;AAAA,gBACf,GAAA,EAAK,EAAE,OAAA,CAAQ,GAAA;AAAA,gBACf,OAAA,EAAS,EAAE,OAAA,CAAQ;AAAA,eACrB;AAAA,cACA,UAAA,EAAY,IAAA,CAAK,GAAA,EAAI,GAAI,SAAA;AAAA,cACzB,WAAA,EAAA,iBAAa,IAAI,IAAA,EAAK,EAAE,WAAA;AAAY,aACrC,CAAA;AACD,YAAA;AAAA,UACF;AAAA,UAEA,KAAK,cAAA;AACH,YAAA,IAAA,CAAK,IAAI,iBAAA,CAAkB,cAAA,EAAgB,yBAAyB,CAAC,CAAA;AACrE,YAAA;AAAA,UAEF,KAAK,OAAA;AACH,YAAA,IAAA,CAAK,IAAI,iBAAA,CAAkB,cAAA,EAAgB,CAAA,CAAE,OAAO,CAAC,CAAA;AACrD,YAAA;AAAA;AACJ,MACF,SAAS,GAAA,EAAK;AACZ,QAAA,IAAA,CAAK,OAAA,CAAQ,GAAG,CAAC,CAAA;AAAA,MACnB;AAAA,IACF,CAAC,CAAA;AAAA,EACH,CAAC,CAAA;AACH;AAEA,SAAS,QAAQ,KAAA,EAAuB;AACtC,EAAA,IAAI,KAAA,YAAiB,OAAO,OAAO,KAAA;AACnC,EAAA,OAAO,IAAI,iBAAA,CAAkB,SAAA,EAAW,MAAA,CAAO,KAAK,CAAC,CAAA;AACvD;;;AC3LA,IAAM,OAAA,GAA2B;AAAA,EAC/B,MAAA,EAAQ,IAAA;AAAA,EACR,KAAA,EAAO,IAAA;AAAA,EACP,SAAA,EAAW,KAAA;AAAA,EACX,QAAA,EAAU;AACZ,CAAA;AAEO,SAAS,UAAA,GAA4B;AAC1C,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAIA,eAA0B,OAAO,CAAA;AAC3D,EAAA,MAAM,QAAA,GAAWC,aAA+B,IAAI,CAAA;AAEpD,EAAA,MAAM,KAAA,GAAQC,kBAAY,MAAM;AAC9B,IAAA,QAAA,CAAS,SAAS,KAAA,EAAM;AACxB,IAAA,QAAA,CAAS,OAAA,GAAU,IAAA;AACnB,IAAA,QAAA,CAAS,OAAO,CAAA;AAAA,EAClB,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,MAAA,GAASA,kBAAY,MAAM;AAC/B,IAAA,QAAA,CAAS,SAAS,KAAA,EAAM;AAAA,EAC1B,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,GAAA,GAAMA,iBAAA,CAAY,OACtB,OAAA,EACA,OAAA,KACuB;AACvB,IAAA,QAAA,CAAS,SAAS,KAAA,EAAM;AACxB,IAAA,MAAM,IAAA,GAAO,IAAI,eAAA,EAAgB;AACjC,IAAA,QAAA,CAAS,OAAA,GAAU,IAAA;AAEnB,IAAA,QAAA,CAAS,EAAE,GAAG,OAAA,EAAS,SAAA,EAAW,MAAM,CAAA;AACxC,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,MAAM,UAAA,CAAW,OAAA,EAAS;AAAA,QACvC,GAAG,OAAA;AAAA,QACH,QAAQ,IAAA,CAAK,MAAA;AAAA,QACb,UAAA,EAAY,CAAC,QAAA,KAAa;AACxB,UAAA,QAAA,CAAS,CAAC,MAAO,CAAA,CAAE,SAAA,GAAY,EAAE,GAAG,CAAA,EAAG,QAAA,EAAS,GAAI,CAAE,CAAA;AAAA,QACxD;AAAA,OACD,CAAA;AACD,MAAA,QAAA,CAAS,EAAE,QAAQ,KAAA,EAAO,IAAA,EAAM,WAAW,KAAA,EAAO,QAAA,EAAU,MAAM,CAAA;AAClE,MAAA,OAAO,MAAA;AAAA,IACT,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,KAAA,GAAQ,eAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,KAAA,CAAM,MAAA,CAAO,GAAG,CAAC,CAAA;AAChE,MAAA,QAAA,CAAS,EAAE,QAAQ,IAAA,EAAM,KAAA,EAAO,WAAW,KAAA,EAAO,QAAA,EAAU,MAAM,CAAA;AAClE,MAAA,MAAM,KAAA;AAAA,IACR,CAAA,SAAE;AACA,MAAA,IAAI,QAAA,CAAS,OAAA,KAAY,IAAA,EAAM,QAAA,CAAS,OAAA,GAAU,IAAA;AAAA,IACpD;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,GAAA,EAAK,QAAQ,KAAA,EAAM;AACxC;;;AC3DA,IAAM,cAAA,GAAiB,GAAA;AAEhB,SAAS,oBAAA,CAAqB,MAAc,MAAA,EAAqC;AACtF,EAAA,IAAI,MAAA,EAAQ,OAAA,EAAS,OAAO,OAAA,CAAQ,OAAA,EAAQ;AAG5C,EAAA,IAAI,OAAO,UAAU,WAAA,EAAa;AAChC,IAAA,OAAO,eAAA,CAAgB,MAAM,MAAM,CAAA;AAAA,EACrC;AACA,EAAA,OAAO,eAAA,CAAgB,MAAM,MAAM,CAAA;AACrC;AAEA,SAAS,eAAA,CAAgB,MAAc,MAAA,EAAqC;AAC1E,EAAA,OAAO,IAAI,OAAA,CAAc,CAAC,OAAA,KAAY;AACpC,IAAA,MAAM,GAAA,GAAM,IAAI,KAAA,EAAM;AACtB,IAAA,IAAI,OAAA,GAAU,KAAA;AACd,IAAA,MAAM,SAAS,MAAM;AACnB,MAAA,IAAI,OAAA,EAAS;AACb,MAAA,OAAA,GAAU,IAAA;AACV,MAAA,GAAA,CAAI,GAAA,GAAM,EAAA;AACV,MAAA,OAAA,EAAQ;AAAA,IACV,CAAA;AAEA,IAAA,MAAM,OAAA,GAAU,MAAM,MAAA,EAAO;AAC7B,IAAA,MAAA,EAAQ,iBAAiB,OAAA,EAAS,OAAA,EAAS,EAAE,IAAA,EAAM,MAAM,CAAA;AAEzD,IAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,MAAA,EAAQ,cAAc,CAAA;AAE/C,IAAA,GAAA,CAAI,SAAS,MAAM;AAAE,MAAA,YAAA,CAAa,KAAK,CAAA;AAAG,MAAA,MAAA,EAAO;AAAA,IAAG,CAAA;AACpD,IAAA,GAAA,CAAI,UAAU,MAAM;AAAE,MAAA,YAAA,CAAa,KAAK,CAAA;AAAG,MAAA,MAAA,EAAO;AAAA,IAAG,CAAA;AAKrD,IAAA,GAAA,CAAI,MAAM,CAAA,QAAA,EAAW,IAAI,CAAA,cAAA,EAAiB,IAAA,CAAK,KAAK,CAAA,CAAA,EAAI,IAAA,CAAK,MAAA,GAAS,QAAA,CAAS,EAAE,CAAA,CAAE,KAAA,CAAM,CAAC,CAAC,CAAA,CAAA;AAAA,EAC7F,CAAC,CAAA;AACH;AAEA,SAAS,eAAA,CAAgB,MAAc,MAAA,EAAqC;AAC1E,EAAA,MAAM,UAAA,GAAa,IAAI,eAAA,EAAgB;AACvC,EAAA,MAAM,cAAc,MAAA,GAChB,YAAA,CAAa,QAAQ,UAAA,CAAW,MAAM,IACtC,UAAA,CAAW,MAAA;AACf,EAAA,MAAM,QAAQ,UAAA,CAAW,MAAM,UAAA,CAAW,KAAA,IAAS,cAAc,CAAA;AAEjE,EAAA,OAAO,MAAM,CAAA,QAAA,EAAW,IAAI,aAAa,IAAA,CAAK,GAAA,EAAK,CAAA,CAAA,EAAI;AAAA,IACrD,MAAA,EAAQ,KAAA;AAAA,IACR,IAAA,EAAM,SAAA;AAAA,IACN,KAAA,EAAO,UAAA;AAAA,IACP,MAAA,EAAQ;AAAA,GACT,CAAA,CACE,KAAA,CAAM,MAAM,MAAS,CAAA,CACrB,OAAA,CAAQ,MAAM,YAAA,CAAa,KAAK,CAAC,CAAA,CACjC,IAAA,CAAK,MAAM,MAAS,CAAA;AACzB;AAEA,SAAS,YAAA,CAAa,GAAgB,CAAA,EAA6B;AACjE,EAAA,MAAM,CAAA,GAAI,IAAI,eAAA,EAAgB;AAC9B,EAAA,MAAM,OAAA,GAAU,MAAM,CAAA,CAAE,KAAA,EAAM;AAC9B,EAAA,IAAI,CAAA,CAAE,OAAA,IAAW,CAAA,CAAE,OAAA,IAAW,KAAA,EAAM;AAAA,OAC/B;AACH,IAAA,CAAA,CAAE,iBAAiB,OAAA,EAAS,OAAA,EAAS,EAAE,IAAA,EAAM,MAAM,CAAA;AACnD,IAAA,CAAA,CAAE,iBAAiB,OAAA,EAAS,OAAA,EAAS,EAAE,IAAA,EAAM,MAAM,CAAA;AAAA,EACrD;AACA,EAAA,OAAO,CAAA,CAAE,MAAA;AACX;;;ACzEA,eAAsB,cAAA,CACpB,OAAA,EACA,OAAA,GAA6B,EAAC,EACf;AACf,EAAA,IAAI,OAAA,CAAQ,MAAA,EAAQ,OAAA,EAAS,MAAM,IAAI,UAAA,EAAW;AAClD,EAAA,MAAM,SAAA,GAAY,QAAQ,SAAA,IAAa,IAAA;AAEvC,EAAA,IAAI,aAAA,GAAsD,IAAA;AAC1D,EAAA,MAAM,iBAAA,GAAoB,IAAI,eAAA,EAAgB;AAC9C,EAAA,MAAM,cAAA,GAAiBC,aAAAA,CAAa,OAAA,CAAQ,MAAA,EAAQ,kBAAkB,MAAM,CAAA;AAE5E,EAAA,MAAM,cAAA,GAAiB,IAAI,OAAA,CAAe,CAAC,GAAG,MAAA,KAAW;AACvD,IAAA,aAAA,GAAgB,WAAW,MAAM;AAC/B,MAAA,iBAAA,CAAkB,KAAA,EAAM;AACxB,MAAA,MAAA,CAAO,IAAI,YAAA,CAAa,CAAA,uBAAA,EAA0B,SAAS,IAAI,CAAC,CAAA;AAAA,IAClE,GAAG,SAAS,CAAA;AAAA,EACd,CAAC,CAAA;AAED,EAAA,MAAM,YAAA,GAAe,IAAI,OAAA,CAAe,CAAC,GAAG,MAAA,KAAW;AACrD,IAAA,OAAA,CAAQ,MAAA,EAAQ,gBAAA;AAAA,MACd,OAAA;AAAA,MACA,MAAM,MAAA,CAAO,IAAI,UAAA,EAAY,CAAA;AAAA,MAC7B,EAAE,MAAM,IAAA;AAAK,KACf;AAAA,EACF,CAAC,CAAA;AAED,EAAA,MAAM,QAAA,GAAW,QAAQ,KAAA,CAAM,GAAA;AAAA,IAAI,CAAC,IAAA,KAClC,oBAAA,CAAqB,MAAM,cAAc,CAAA,CAAE,KAAK,MAAM;AACpD,MAAA,OAAA,CAAQ,aAAa,IAAI,CAAA;AAAA,IAC3B,CAAC;AAAA,GACH;AAEA,EAAA,IAAI;AACF,IAAA,MAAM,QAAQ,IAAA,CAAK;AAAA,MACjB,OAAA,CAAQ,IAAI,QAAQ,CAAA;AAAA,MACpB,cAAA;AAAA,MACA;AAAA,KACD,CAAA;AAAA,EACH,CAAA,SAAE;AACA,IAAA,IAAI,aAAA,eAA4B,aAAa,CAAA;AAAA,EAC/C;AACF;AAEA,SAASA,aAAAA,CAAa,GAA4B,CAAA,EAA6B;AAC7E,EAAA,IAAI,CAAC,GAAG,OAAO,CAAA;AACf,EAAA,MAAM,CAAA,GAAI,IAAI,eAAA,EAAgB;AAC9B,EAAA,IAAI,CAAA,CAAE,OAAA,IAAW,CAAA,CAAE,OAAA,IAAW,KAAA,EAAM;AAAA,OAC/B;AACH,IAAA,CAAA,CAAE,gBAAA,CAAiB,SAAS,MAAM,CAAA,CAAE,OAAM,EAAG,EAAE,IAAA,EAAM,IAAA,EAAM,CAAA;AAC3D,IAAA,CAAA,CAAE,gBAAA,CAAiB,SAAS,MAAM,CAAA,CAAE,OAAM,EAAG,EAAE,IAAA,EAAM,IAAA,EAAM,CAAA;AAAA,EAC7D;AACA,EAAA,OAAO,CAAA,CAAE,MAAA;AACX;;;ACtCA,IAAMC,QAAAA,GAA+B;AAAA,EACnC,SAAA,EAAW,KAAA;AAAA,EACX,KAAA,EAAO,IAAA;AAAA,EACP,UAAU,EAAC;AAAA,EACX,QAAA,EAAU;AACZ,CAAA;AAEO,SAAS,cAAA,GAAoC;AAClD,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAIJ,eAA8BI,QAAO,CAAA;AAC/D,EAAA,MAAM,QAAA,GAAWH,aAA+B,IAAI,CAAA;AAEpD,EAAA,MAAM,KAAA,GAAQC,kBAAY,MAAM;AAC9B,IAAA,QAAA,CAAS,SAAS,KAAA,EAAM;AACxB,IAAA,QAAA,CAAS,OAAA,GAAU,IAAA;AACnB,IAAA,QAAA,CAASE,QAAO,CAAA;AAAA,EAClB,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,MAAA,GAASF,kBAAY,MAAM;AAC/B,IAAA,QAAA,CAAS,SAAS,KAAA,EAAM;AAAA,EAC1B,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,GAAA,GAAMA,iBAAAA,CAAY,OACtB,OAAA,EACA,OAAA,KACkB;AAClB,IAAA,QAAA,CAAS,SAAS,KAAA,EAAM;AACxB,IAAA,MAAM,IAAA,GAAO,IAAI,eAAA,EAAgB;AACjC,IAAA,QAAA,CAAS,OAAA,GAAU,IAAA;AAEnB,IAAA,QAAA,CAAS,EAAE,GAAGE,QAAAA,EAAS,SAAA,EAAW,MAAM,CAAA;AACxC,IAAA,IAAI;AACF,MAAA,MAAM,eAAe,OAAA,EAAS;AAAA,QAC5B,GAAG,OAAA;AAAA,QACH,QAAQ,IAAA,CAAK,MAAA;AAAA,QACb,UAAA,EAAY,CAAC,IAAA,KAAS;AACpB,UAAA,QAAA;AAAA,YAAS,CAAC,CAAA,KACR,CAAA,CAAE,SAAA,GAAY,EAAE,GAAG,CAAA,EAAG,QAAA,EAAU,CAAC,GAAG,CAAA,CAAE,QAAA,EAAU,IAAI,GAAE,GAAI;AAAA,WAC5D;AAAA,QACF;AAAA,OACD,CAAA;AACD,MAAA,QAAA,CAAS,CAAC,OAAO,EAAE,GAAG,GAAG,SAAA,EAAW,KAAA,EAAO,QAAA,EAAU,IAAA,EAAK,CAAE,CAAA;AAAA,IAC9D,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,KAAA,GAAQ,eAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,KAAA,CAAM,MAAA,CAAO,GAAG,CAAC,CAAA;AAChE,MAAA,QAAA,CAAS,CAAC,OAAO,EAAE,GAAG,GAAG,SAAA,EAAW,KAAA,EAAO,OAAM,CAAE,CAAA;AACnD,MAAA,MAAM,KAAA;AAAA,IACR,CAAA,SAAE;AACA,MAAA,IAAI,QAAA,CAAS,OAAA,KAAY,IAAA,EAAM,QAAA,CAAS,OAAA,GAAU,IAAA;AAAA,IACpD;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,GAAA,EAAK,QAAQ,KAAA,EAAM;AACxC","file":"index.cjs","sourcesContent":["// Error hierarchy. Keep this small — three classes cover every failure\n// mode we care about for v0.1. Customers can `instanceof` to branch on\n// recoverable vs not.\n\nexport class NetworkTestsError extends Error {\n public readonly code: string;\n\n constructor(code: string, message: string) {\n super(message);\n this.name = \"NetworkTestsError\";\n this.code = code;\n }\n}\n\n/** Raised when the caller's AbortSignal fires. */\nexport class AbortError extends NetworkTestsError {\n constructor(message = \"Operation aborted\") {\n super(\"ABORTED\", message);\n this.name = \"AbortError\";\n }\n}\n\n/** Raised when a hard timeout fires before the test completes. */\nexport class TimeoutError extends NetworkTestsError {\n constructor(message = \"Operation timed out\") {\n super(\"TIMEOUT\", message);\n this.name = \"TimeoutError\";\n }\n}\n","// RTCPeerConnection wrapper. Encapsulates the offer/answer/ICE dance.\n//\n// Browser-native WebRTC only — no shims. The server uses `werift` and\n// speaks vanilla SDP, so any modern browser RTC stack interoperates.\n\nimport { NetworkTestsError } from \"../errors.js\";\n\nexport interface WebrtcSession {\n pc: RTCPeerConnection;\n dataChannel: RTCDataChannel;\n /** Resolves once the DataChannel is open. */\n ready: Promise<void>;\n close: () => void;\n}\n\nexport function createPeerConnection(iceServers: RTCIceServer[]): RTCPeerConnection {\n return new RTCPeerConnection({ iceServers });\n}\n\nexport function setupDataChannel(pc: RTCPeerConnection): {\n channel: RTCDataChannel;\n ready: Promise<void>;\n} {\n // Server expects a client-initiated DataChannel. Label \"test\" matches\n // what the consumer site has always used; server doesn't filter on it\n // but keeping it stable simplifies the wire trace.\n const channel = pc.createDataChannel(\"test\", { ordered: false });\n\n const ready = new Promise<void>((resolve, reject) => {\n const onOpen = () => {\n cleanup();\n resolve();\n };\n const onError = () => {\n cleanup();\n reject(new NetworkTestsError(\"DC_ERROR\", \"DataChannel error before open\"));\n };\n const onClose = () => {\n cleanup();\n reject(new NetworkTestsError(\"DC_CLOSED\", \"DataChannel closed before open\"));\n };\n function cleanup() {\n channel.removeEventListener(\"open\", onOpen);\n channel.removeEventListener(\"error\", onError);\n channel.removeEventListener(\"close\", onClose);\n }\n channel.addEventListener(\"open\", onOpen);\n channel.addEventListener(\"error\", onError);\n channel.addEventListener(\"close\", onClose);\n });\n\n return { channel, ready };\n}\n\nexport async function createOffer(pc: RTCPeerConnection): Promise<RTCSessionDescriptionInit> {\n const offer = await pc.createOffer();\n await pc.setLocalDescription(offer);\n return offer;\n}\n\nexport async function applyAnswer(\n pc: RTCPeerConnection,\n sdp: string,\n): Promise<void> {\n await pc.setRemoteDescription({ type: \"answer\", sdp });\n}\n","// DataChannel side of the packet-loss test.\n//\n// Server drives the test: it pushes packets at the configured rate, we\n// ack each one back. Server tracks send/ack timestamps and computes\n// loss + latency, then ships the final result via the WebSocket as\n// `test-complete`.\n//\n// Local job here is:\n// 1. Decode incoming server packets ({ type: \"packet\", seq, p? })\n// 2. Send ack back over DC ({ type: \"ack\", seq })\n// 3. Surface progress to the caller (sent/received/loss%/elapsed)\n\nimport type { UdpProgress } from \"../types.js\";\n\nexport interface PacketLoopHandle {\n /** Stop ack'ing future packets. Safe to call multiple times. */\n stop(): void;\n}\n\nexport interface PacketLoopOptions {\n channel: RTCDataChannel;\n onProgress?: (progress: UdpProgress) => void;\n /** Throttle progress callbacks to once per this many ms (default 100). */\n progressIntervalMs?: number;\n}\n\nexport function startPacketLoop(opts: PacketLoopOptions): PacketLoopHandle {\n const { channel, onProgress } = opts;\n const progressMs = opts.progressIntervalMs ?? 100;\n const startTs = Date.now();\n\n // Server sends them; we count locally for the onProgress callback.\n // The server's count is the source of truth in the final result.\n let received = 0;\n // We don't know what the server has sent (it tracks that side), but\n // we surface \"received\" as the progress signal. lossPercent below is\n // a coarse local estimate based on expected packets per second; we\n // intentionally don't try to be exact — the server's final number is.\n let highestSeq = -1;\n let stopped = false;\n\n const onMsg = (ev: MessageEvent<string>) => {\n if (stopped) return;\n try {\n const msg = JSON.parse(ev.data);\n if (msg && msg.type === \"packet\" && typeof msg.seq === \"number\") {\n received++;\n if (msg.seq > highestSeq) highestSeq = msg.seq;\n // Fire-and-forget ack. Send may throw if DC closed mid-flight;\n // silently drop — the server handles the missing ack as a lost\n // packet which is the same as a real network loss.\n try {\n channel.send(JSON.stringify({ type: \"ack\", seq: msg.seq }));\n } catch { /* DC closed; loop will exit when WS closes */ }\n }\n } catch { /* malformed packet — ignore */ }\n };\n\n channel.addEventListener(\"message\", onMsg);\n\n let progressTimer: ReturnType<typeof setInterval> | null = null;\n if (onProgress) {\n progressTimer = setInterval(() => {\n if (stopped) return;\n // highestSeq is 0-indexed; +1 gives count of packets the server\n // has sent us. lossPercent is locally estimated and approximate;\n // server emits the authoritative number in test-complete.\n const sent = highestSeq + 1;\n const loss = sent > 0 ? ((sent - received) / sent) * 100 : 0;\n onProgress({\n packetsSent: sent,\n packetsReceived: received,\n lossPercent: Math.round(loss * 100) / 100,\n elapsedMs: Date.now() - startTs,\n });\n }, progressMs);\n }\n\n return {\n stop() {\n if (stopped) return;\n stopped = true;\n channel.removeEventListener(\"message\", onMsg);\n if (progressTimer) clearInterval(progressTimer);\n },\n };\n}\n","// WebSocket protocol contract with server/websocket/signaling.js on the\n// networktests.com API server. Keep these in lockstep with the server.\n\nexport interface WelcomeMsg {\n type: \"welcome\";\n clientId: string;\n turn?: { urls: string[]; username: string; credential: string };\n}\n\nexport interface AnswerMsg {\n type: \"answer\";\n /** Server wraps the SDP in a nested object. */\n sdp: { type: \"answer\"; sdp: string };\n}\n\nexport interface IceCandidateMsg {\n type: \"ice-candidate\";\n candidate: RTCIceCandidateInit;\n}\n\nexport interface DataChannelOpenMsg { type: \"datachannel-open\"; }\nexport interface TestStartedMsg {\n type: \"test-started\";\n test: \"packet-loss\";\n duration: number;\n rate: number;\n packetSize: number;\n}\nexport interface TestStoppedMsg { type: \"test-stopped\"; }\nexport interface ErrorMsg { type: \"error\"; message: string; }\n\nexport interface TestCompleteMsg {\n type: \"test-complete\";\n test: \"packet-loss\";\n results: {\n packetsSent: number;\n packetsReceived: number;\n packetsLost: number;\n lossPercent: number;\n uploadReceived: number;\n latency: {\n avg: number;\n min: number;\n max: number;\n p50: number;\n p95: number;\n samples?: number[];\n };\n jitter: number;\n };\n}\n\nexport type ServerMsg =\n | WelcomeMsg\n | AnswerMsg\n | IceCandidateMsg\n | DataChannelOpenMsg\n | TestStartedMsg\n | TestStoppedMsg\n | TestCompleteMsg\n | ErrorMsg;\n\nexport type ClientMsg =\n | { type: \"offer\"; sdp: string }\n | { type: \"ice-candidate\"; candidate: RTCIceCandidateInit }\n | { type: \"datachannel-ready\" }\n | { type: \"start-packet-test\"; duration?: number; rate?: number; packetSize?: number }\n | { type: \"stop-test\" };\n\n/** Type guard for runtime ServerMsg validation. */\nexport function isServerMsg(value: unknown): value is ServerMsg {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as { type?: unknown }).type === \"string\"\n );\n}\n","// Public entry: runUdpTest(session, options).\n//\n// Sequence (must match server/websocket/signaling.js):\n// 1. Open WS → server sends `welcome` with optional TURN credentials\n// 2. Create RTCPeerConnection (iceServers from session ∪ welcome.turn)\n// 3. Create DataChannel (client-initiated, label \"test\")\n// 4. Create offer → send `{ type: \"offer\", sdp }`\n// 5. Receive `answer` → setRemoteDescription\n// 6. Trickle ICE in both directions\n// 7. Wait for DataChannel to open + server's `datachannel-open` ack\n// 8. Send `{ type: \"start-packet-test\", duration, rate, packetSize }`\n// 9. Receive `test-started` → start packet loop (ack incoming packets)\n// 10. Receive `test-complete` → close WS, resolve with results\n//\n// Any \"error\" message from the server or any unhandled WS/PC close\n// rejects the promise with NetworkTestsError.\n\nimport { NetworkTestsError, AbortError, TimeoutError } from \"../errors.js\";\nimport type {\n UdpSession,\n UdpResult,\n RunUdpOptions,\n} from \"../types.js\";\nimport {\n createPeerConnection,\n setupDataChannel,\n createOffer,\n applyAnswer,\n} from \"./webrtc.js\";\nimport { startPacketLoop } from \"./packetLoop.js\";\nimport type { ServerMsg, ClientMsg } from \"./signaling.js\";\nimport { isServerMsg } from \"./signaling.js\";\n\nconst DEFAULT_ICE_SERVERS: RTCIceServer[] = [\n { urls: \"stun:stun.l.google.com:19302\" },\n];\n\nexport async function runUdpTest(\n session: UdpSession,\n options: RunUdpOptions = {},\n): Promise<UdpResult> {\n const startedAt = Date.now();\n const { signal, onProgress } = options;\n const timeoutMs = options.timeoutMs ?? ((session.duration ?? 30) + 10) * 1000;\n\n if (signal?.aborted) throw new AbortError();\n\n const ws = new WebSocket(session.wsUrl);\n ws.binaryType = \"arraybuffer\";\n\n let pc: RTCPeerConnection | null = null;\n let dataChannel: RTCDataChannel | null = null;\n let packetLoop: ReturnType<typeof startPacketLoop> | null = null;\n let timeoutHandle: ReturnType<typeof setTimeout> | null = null;\n let abortHandler: (() => void) | null = null;\n\n const cleanup = () => {\n if (timeoutHandle) clearTimeout(timeoutHandle);\n if (abortHandler && signal) signal.removeEventListener(\"abort\", abortHandler);\n packetLoop?.stop();\n try { dataChannel?.close(); } catch { /* ignore */ }\n try { pc?.close(); } catch { /* ignore */ }\n if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {\n try { ws.close(); } catch { /* ignore */ }\n }\n };\n\n return new Promise<UdpResult>((resolve, reject) => {\n const fail = (err: Error) => { cleanup(); reject(err); };\n const succeed = (result: UdpResult) => { cleanup(); resolve(result); };\n\n timeoutHandle = setTimeout(\n () => fail(new TimeoutError(`UDP test exceeded ${timeoutMs}ms`)),\n timeoutMs,\n );\n if (signal) {\n abortHandler = () => fail(new AbortError());\n signal.addEventListener(\"abort\", abortHandler, { once: true });\n }\n\n const send = (msg: ClientMsg) => {\n if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg));\n };\n\n ws.addEventListener(\"error\", () =>\n fail(new NetworkTestsError(\"WS_ERROR\", \"WebSocket error\")),\n );\n\n ws.addEventListener(\"close\", (ev) => {\n // Close before result = unexpected. After resolve we've already\n // cleaned up so this handler is a no-op.\n if (ws.readyState === WebSocket.CLOSED) {\n const reason = ev.reason || `WebSocket closed (code ${ev.code})`;\n fail(new NetworkTestsError(\"WS_CLOSED\", reason));\n }\n });\n\n ws.addEventListener(\"open\", async () => {\n try {\n // The PC creation waits until the welcome arrives so we can\n // merge server-provided TURN creds into the iceServers list.\n } catch (err) {\n fail(toError(err));\n }\n });\n\n ws.addEventListener(\"message\", async (ev) => {\n let msg: unknown;\n try { msg = JSON.parse(String(ev.data)); } catch { return; }\n if (!isServerMsg(msg)) return;\n const m = msg as ServerMsg;\n\n try {\n switch (m.type) {\n case \"welcome\": {\n const iceServers: RTCIceServer[] = [\n ...(session.iceServers ?? DEFAULT_ICE_SERVERS),\n ];\n if (m.turn) {\n iceServers.push({\n urls: m.turn.urls,\n username: m.turn.username,\n credential: m.turn.credential,\n });\n }\n pc = createPeerConnection(iceServers);\n pc.addEventListener(\"icecandidate\", (e) => {\n if (e.candidate) {\n send({ type: \"ice-candidate\", candidate: e.candidate.toJSON() });\n }\n });\n const dcSetup = setupDataChannel(pc);\n dataChannel = dcSetup.channel;\n const offer = await createOffer(pc);\n send({ type: \"offer\", sdp: offer.sdp ?? \"\" });\n // Wait for DC to open then notify server.\n dcSetup.ready\n .then(() => send({ type: \"datachannel-ready\" }))\n .catch((err) => fail(toError(err)));\n break;\n }\n\n case \"answer\": {\n if (!pc) throw new NetworkTestsError(\"BAD_STATE\", \"answer before welcome\");\n await applyAnswer(pc, m.sdp.sdp);\n break;\n }\n\n case \"ice-candidate\": {\n if (!pc) return;\n try { await pc.addIceCandidate(m.candidate); } catch { /* end-of-cands */ }\n break;\n }\n\n case \"datachannel-open\": {\n if (!dataChannel) throw new NetworkTestsError(\"BAD_STATE\", \"no DC\");\n packetLoop = startPacketLoop({ channel: dataChannel, onProgress });\n send({\n type: \"start-packet-test\",\n duration: session.duration,\n rate: session.rate,\n packetSize: session.packetSize,\n });\n break;\n }\n\n case \"test-started\":\n // Server confirmed test parameters; nothing further to do\n // until packets arrive on the DC.\n break;\n\n case \"test-complete\": {\n const r = m.results;\n succeed({\n sessionId: session.sessionId,\n packetsSent: r.packetsSent,\n packetsReceived: r.packetsReceived,\n packetsLost: r.packetsLost,\n lossPercent: r.lossPercent,\n jitter: r.jitter,\n latency: {\n avg: r.latency.avg,\n min: r.latency.min,\n max: r.latency.max,\n p50: r.latency.p50,\n p95: r.latency.p95,\n samples: r.latency.samples,\n },\n durationMs: Date.now() - startedAt,\n completedAt: new Date().toISOString(),\n });\n break;\n }\n\n case \"test-stopped\":\n fail(new NetworkTestsError(\"TEST_STOPPED\", \"Server stopped the test\"));\n break;\n\n case \"error\":\n fail(new NetworkTestsError(\"SERVER_ERROR\", m.message));\n break;\n }\n } catch (err) {\n fail(toError(err));\n }\n });\n });\n}\n\nfunction toError(value: unknown): Error {\n if (value instanceof Error) return value;\n return new NetworkTestsError(\"UNKNOWN\", String(value));\n}\n","import { useCallback, useRef, useState } from \"react\";\nimport { runUdpTest } from \"../udp/index.js\";\nimport type {\n UdpSession,\n UdpResult,\n UdpProgress,\n RunUdpOptions,\n} from \"../types.js\";\n\nexport interface UseUdpTestState {\n result: UdpResult | null;\n error: Error | null;\n isRunning: boolean;\n progress: UdpProgress | null;\n}\n\nexport interface UseUdpTestApi extends UseUdpTestState {\n /** Start the test. Throws if already running. */\n run: (session: UdpSession, options?: Omit<RunUdpOptions, \"onProgress\" | \"signal\">) => Promise<UdpResult>;\n /** Abort an in-flight test. No-op if idle. */\n cancel: () => void;\n /** Reset state to initial. */\n reset: () => void;\n}\n\nconst INITIAL: UseUdpTestState = {\n result: null,\n error: null,\n isRunning: false,\n progress: null,\n};\n\nexport function useUdpTest(): UseUdpTestApi {\n const [state, setState] = useState<UseUdpTestState>(INITIAL);\n const abortRef = useRef<AbortController | null>(null);\n\n const reset = useCallback(() => {\n abortRef.current?.abort();\n abortRef.current = null;\n setState(INITIAL);\n }, []);\n\n const cancel = useCallback(() => {\n abortRef.current?.abort();\n }, []);\n\n const run = useCallback(async (\n session: UdpSession,\n options?: Omit<RunUdpOptions, \"onProgress\" | \"signal\">,\n ): Promise<UdpResult> => {\n abortRef.current?.abort();\n const ctrl = new AbortController();\n abortRef.current = ctrl;\n\n setState({ ...INITIAL, isRunning: true });\n try {\n const result = await runUdpTest(session, {\n ...options,\n signal: ctrl.signal,\n onProgress: (progress) => {\n setState((s) => (s.isRunning ? { ...s, progress } : s));\n },\n });\n setState({ result, error: null, isRunning: false, progress: null });\n return result;\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n setState({ result: null, error, isRunning: false, progress: null });\n throw error;\n } finally {\n if (abortRef.current === ctrl) abortRef.current = null;\n }\n }, []);\n\n return { ...state, run, cancel, reset };\n}\n","// Triggers a DNS resolution of a single FQDN from the browser.\n//\n// Our authoritative DNS server responds to every query with 192.0.2.1\n// (TEST-NET-1, guaranteed unreachable per RFC 5737) — the HTTP request\n// is *expected* to fail. We only care that the recursive resolver had\n// to look up the name, so its IP gets recorded server-side.\n//\n// Strategy (most-reliable first):\n// 1. new Image() — no CORS, no fetch policy. Triggers DNS even on\n// CSP-restricted pages. Errors are silent (image fails to decode).\n// 2. fetch(no-cors) — fallback. Some environments (workers, certain\n// WebView shells) lack DOM image support.\n//\n// Both error paths are normal. The function resolves on any terminal\n// state (success or error), never throws.\n\nconst ABORT_AFTER_MS = 5000;\n\nexport function triggerDnsResolution(fqdn: string, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) return Promise.resolve();\n\n // Prefer Image — broader browser support, no CORS.\n if (typeof Image !== \"undefined\") {\n return triggerViaImage(fqdn, signal);\n }\n return triggerViaFetch(fqdn, signal);\n}\n\nfunction triggerViaImage(fqdn: string, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve) => {\n const img = new Image();\n let settled = false;\n const finish = () => {\n if (settled) return;\n settled = true;\n img.src = \"\"; // cancel any pending request\n resolve();\n };\n\n const onAbort = () => finish();\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n const timer = setTimeout(finish, ABORT_AFTER_MS);\n\n img.onload = () => { clearTimeout(timer); finish(); };\n img.onerror = () => { clearTimeout(timer); finish(); };\n\n // Cache-bust so subsequent runs against the same FQDN actually\n // re-resolve. Path doesn't matter — server returns 192.0.2.1\n // regardless.\n img.src = `https://${fqdn}/_/leak.gif?t=${Date.now()}-${Math.random().toString(36).slice(2)}`;\n });\n}\n\nfunction triggerViaFetch(fqdn: string, signal?: AbortSignal): Promise<void> {\n const controller = new AbortController();\n const abortSignal = signal\n ? mergeSignals(signal, controller.signal)\n : controller.signal;\n const timer = setTimeout(() => controller.abort(), ABORT_AFTER_MS);\n\n return fetch(`https://${fqdn}/_/leak?t=${Date.now()}`, {\n method: \"GET\",\n mode: \"no-cors\",\n cache: \"no-store\",\n signal: abortSignal,\n })\n .catch(() => undefined)\n .finally(() => clearTimeout(timer))\n .then(() => undefined);\n}\n\nfunction mergeSignals(a: AbortSignal, b: AbortSignal): AbortSignal {\n const c = new AbortController();\n const onAbort = () => c.abort();\n if (a.aborted || b.aborted) c.abort();\n else {\n a.addEventListener(\"abort\", onAbort, { once: true });\n b.addEventListener(\"abort\", onAbort, { once: true });\n }\n return c.signal;\n}\n","// runDnsLeakTest(session, options) — triggers DNS resolution of every\n// FQDN in the session in parallel and resolves once all triggers have\n// fired (or errored — errors are expected).\n\nimport { AbortError, TimeoutError } from \"../errors.js\";\nimport { triggerDnsResolution } from \"./trigger.js\";\nimport type { DnsLeakSession, RunDnsLeakOptions } from \"../types.js\";\n\nexport async function runDnsLeakTest(\n session: DnsLeakSession,\n options: RunDnsLeakOptions = {},\n): Promise<void> {\n if (options.signal?.aborted) throw new AbortError();\n const timeoutMs = options.timeoutMs ?? 15000;\n\n let timeoutHandle: ReturnType<typeof setTimeout> | null = null;\n const timeoutController = new AbortController();\n const combinedSignal = mergeSignals(options.signal, timeoutController.signal);\n\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutHandle = setTimeout(() => {\n timeoutController.abort();\n reject(new TimeoutError(`DNS-leak test exceeded ${timeoutMs}ms`));\n }, timeoutMs);\n });\n\n const abortPromise = new Promise<never>((_, reject) => {\n options.signal?.addEventListener(\n \"abort\",\n () => reject(new AbortError()),\n { once: true },\n );\n });\n\n const triggers = session.fqdns.map((fqdn) =>\n triggerDnsResolution(fqdn, combinedSignal).then(() => {\n options.onResolved?.(fqdn);\n }),\n );\n\n try {\n await Promise.race([\n Promise.all(triggers),\n timeoutPromise,\n abortPromise,\n ]);\n } finally {\n if (timeoutHandle) clearTimeout(timeoutHandle);\n }\n}\n\nfunction mergeSignals(a: AbortSignal | undefined, b: AbortSignal): AbortSignal {\n if (!a) return b;\n const c = new AbortController();\n if (a.aborted || b.aborted) c.abort();\n else {\n a.addEventListener(\"abort\", () => c.abort(), { once: true });\n b.addEventListener(\"abort\", () => c.abort(), { once: true });\n }\n return c.signal;\n}\n","import { useCallback, useRef, useState } from \"react\";\nimport { runDnsLeakTest } from \"../dnsleak/index.js\";\nimport type { DnsLeakSession, RunDnsLeakOptions } from \"../types.js\";\n\nexport interface UseDnsLeakTestState {\n isRunning: boolean;\n error: Error | null;\n /** FQDNs that have been triggered (server-side resolver list is separate). */\n resolved: string[];\n /** True once all FQDNs have been triggered. */\n complete: boolean;\n}\n\nexport interface UseDnsLeakTestApi extends UseDnsLeakTestState {\n run: (\n session: DnsLeakSession,\n options?: Omit<RunDnsLeakOptions, \"onResolved\" | \"signal\">,\n ) => Promise<void>;\n cancel: () => void;\n reset: () => void;\n}\n\nconst INITIAL: UseDnsLeakTestState = {\n isRunning: false,\n error: null,\n resolved: [],\n complete: false,\n};\n\nexport function useDnsLeakTest(): UseDnsLeakTestApi {\n const [state, setState] = useState<UseDnsLeakTestState>(INITIAL);\n const abortRef = useRef<AbortController | null>(null);\n\n const reset = useCallback(() => {\n abortRef.current?.abort();\n abortRef.current = null;\n setState(INITIAL);\n }, []);\n\n const cancel = useCallback(() => {\n abortRef.current?.abort();\n }, []);\n\n const run = useCallback(async (\n session: DnsLeakSession,\n options?: Omit<RunDnsLeakOptions, \"onResolved\" | \"signal\">,\n ): Promise<void> => {\n abortRef.current?.abort();\n const ctrl = new AbortController();\n abortRef.current = ctrl;\n\n setState({ ...INITIAL, isRunning: true });\n try {\n await runDnsLeakTest(session, {\n ...options,\n signal: ctrl.signal,\n onResolved: (fqdn) => {\n setState((s) =>\n s.isRunning ? { ...s, resolved: [...s.resolved, fqdn] } : s,\n );\n },\n });\n setState((s) => ({ ...s, isRunning: false, complete: true }));\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n setState((s) => ({ ...s, isRunning: false, error }));\n throw error;\n } finally {\n if (abortRef.current === ctrl) abortRef.current = null;\n }\n }, []);\n\n return { ...state, run, cancel, reset };\n}\n"]}