@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.
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/dist/index.cjs +415 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +127 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.mjs +410 -0
- package/dist/index.mjs.map +1 -0
- package/dist/networktests.umd.js +2 -0
- package/dist/networktests.umd.js.map +1 -0
- package/dist/react/index.cjs +481 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +113 -0
- package/dist/react/index.d.ts +113 -0
- package/dist/react/index.mjs +478 -0
- package/dist/react/index.mjs.map +1 -0
- package/package.json +82 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var NetworkTestsError = class extends Error {
|
|
3
|
+
constructor(code, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "NetworkTestsError";
|
|
6
|
+
this.code = code;
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
var AbortError = class extends NetworkTestsError {
|
|
10
|
+
constructor(message = "Operation aborted") {
|
|
11
|
+
super("ABORTED", message);
|
|
12
|
+
this.name = "AbortError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var TimeoutError = class extends NetworkTestsError {
|
|
16
|
+
constructor(message = "Operation timed out") {
|
|
17
|
+
super("TIMEOUT", message);
|
|
18
|
+
this.name = "TimeoutError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/udp/webrtc.ts
|
|
23
|
+
function createPeerConnection(iceServers) {
|
|
24
|
+
return new RTCPeerConnection({ iceServers });
|
|
25
|
+
}
|
|
26
|
+
function setupDataChannel(pc) {
|
|
27
|
+
const channel = pc.createDataChannel("test", { ordered: false });
|
|
28
|
+
const ready = new Promise((resolve, reject) => {
|
|
29
|
+
const onOpen = () => {
|
|
30
|
+
cleanup();
|
|
31
|
+
resolve();
|
|
32
|
+
};
|
|
33
|
+
const onError = () => {
|
|
34
|
+
cleanup();
|
|
35
|
+
reject(new NetworkTestsError("DC_ERROR", "DataChannel error before open"));
|
|
36
|
+
};
|
|
37
|
+
const onClose = () => {
|
|
38
|
+
cleanup();
|
|
39
|
+
reject(new NetworkTestsError("DC_CLOSED", "DataChannel closed before open"));
|
|
40
|
+
};
|
|
41
|
+
function cleanup() {
|
|
42
|
+
channel.removeEventListener("open", onOpen);
|
|
43
|
+
channel.removeEventListener("error", onError);
|
|
44
|
+
channel.removeEventListener("close", onClose);
|
|
45
|
+
}
|
|
46
|
+
channel.addEventListener("open", onOpen);
|
|
47
|
+
channel.addEventListener("error", onError);
|
|
48
|
+
channel.addEventListener("close", onClose);
|
|
49
|
+
});
|
|
50
|
+
return { channel, ready };
|
|
51
|
+
}
|
|
52
|
+
async function createOffer(pc) {
|
|
53
|
+
const offer = await pc.createOffer();
|
|
54
|
+
await pc.setLocalDescription(offer);
|
|
55
|
+
return offer;
|
|
56
|
+
}
|
|
57
|
+
async function applyAnswer(pc, sdp) {
|
|
58
|
+
await pc.setRemoteDescription({ type: "answer", sdp });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/udp/packetLoop.ts
|
|
62
|
+
function startPacketLoop(opts) {
|
|
63
|
+
const { channel, onProgress } = opts;
|
|
64
|
+
const progressMs = opts.progressIntervalMs ?? 100;
|
|
65
|
+
const startTs = Date.now();
|
|
66
|
+
let received = 0;
|
|
67
|
+
let highestSeq = -1;
|
|
68
|
+
let stopped = false;
|
|
69
|
+
const onMsg = (ev) => {
|
|
70
|
+
if (stopped) return;
|
|
71
|
+
try {
|
|
72
|
+
const msg = JSON.parse(ev.data);
|
|
73
|
+
if (msg && msg.type === "packet" && typeof msg.seq === "number") {
|
|
74
|
+
received++;
|
|
75
|
+
if (msg.seq > highestSeq) highestSeq = msg.seq;
|
|
76
|
+
try {
|
|
77
|
+
channel.send(JSON.stringify({ type: "ack", seq: msg.seq }));
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
channel.addEventListener("message", onMsg);
|
|
85
|
+
let progressTimer = null;
|
|
86
|
+
if (onProgress) {
|
|
87
|
+
progressTimer = setInterval(() => {
|
|
88
|
+
if (stopped) return;
|
|
89
|
+
const sent = highestSeq + 1;
|
|
90
|
+
const loss = sent > 0 ? (sent - received) / sent * 100 : 0;
|
|
91
|
+
onProgress({
|
|
92
|
+
packetsSent: sent,
|
|
93
|
+
packetsReceived: received,
|
|
94
|
+
lossPercent: Math.round(loss * 100) / 100,
|
|
95
|
+
elapsedMs: Date.now() - startTs
|
|
96
|
+
});
|
|
97
|
+
}, progressMs);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
stop() {
|
|
101
|
+
if (stopped) return;
|
|
102
|
+
stopped = true;
|
|
103
|
+
channel.removeEventListener("message", onMsg);
|
|
104
|
+
if (progressTimer) clearInterval(progressTimer);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/udp/signaling.ts
|
|
110
|
+
function isServerMsg(value) {
|
|
111
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/udp/index.ts
|
|
115
|
+
var DEFAULT_ICE_SERVERS = [
|
|
116
|
+
{ urls: "stun:stun.l.google.com:19302" }
|
|
117
|
+
];
|
|
118
|
+
async function runUdpTest(session, options = {}) {
|
|
119
|
+
const startedAt = Date.now();
|
|
120
|
+
const { signal, onProgress } = options;
|
|
121
|
+
const timeoutMs = options.timeoutMs ?? ((session.duration ?? 30) + 10) * 1e3;
|
|
122
|
+
if (signal?.aborted) throw new AbortError();
|
|
123
|
+
const ws = new WebSocket(session.wsUrl);
|
|
124
|
+
ws.binaryType = "arraybuffer";
|
|
125
|
+
let pc = null;
|
|
126
|
+
let dataChannel = null;
|
|
127
|
+
let packetLoop = null;
|
|
128
|
+
let timeoutHandle = null;
|
|
129
|
+
let abortHandler = null;
|
|
130
|
+
const cleanup = () => {
|
|
131
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
132
|
+
if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
|
|
133
|
+
packetLoop?.stop();
|
|
134
|
+
try {
|
|
135
|
+
dataChannel?.close();
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
pc?.close();
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
143
|
+
try {
|
|
144
|
+
ws.close();
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
const fail = (err) => {
|
|
151
|
+
cleanup();
|
|
152
|
+
reject(err);
|
|
153
|
+
};
|
|
154
|
+
const succeed = (result) => {
|
|
155
|
+
cleanup();
|
|
156
|
+
resolve(result);
|
|
157
|
+
};
|
|
158
|
+
timeoutHandle = setTimeout(
|
|
159
|
+
() => fail(new TimeoutError(`UDP test exceeded ${timeoutMs}ms`)),
|
|
160
|
+
timeoutMs
|
|
161
|
+
);
|
|
162
|
+
if (signal) {
|
|
163
|
+
abortHandler = () => fail(new AbortError());
|
|
164
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
165
|
+
}
|
|
166
|
+
const send = (msg) => {
|
|
167
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg));
|
|
168
|
+
};
|
|
169
|
+
ws.addEventListener(
|
|
170
|
+
"error",
|
|
171
|
+
() => fail(new NetworkTestsError("WS_ERROR", "WebSocket error"))
|
|
172
|
+
);
|
|
173
|
+
ws.addEventListener("close", (ev) => {
|
|
174
|
+
if (ws.readyState === WebSocket.CLOSED) {
|
|
175
|
+
const reason = ev.reason || `WebSocket closed (code ${ev.code})`;
|
|
176
|
+
fail(new NetworkTestsError("WS_CLOSED", reason));
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
ws.addEventListener("open", async () => {
|
|
180
|
+
});
|
|
181
|
+
ws.addEventListener("message", async (ev) => {
|
|
182
|
+
let msg;
|
|
183
|
+
try {
|
|
184
|
+
msg = JSON.parse(String(ev.data));
|
|
185
|
+
} catch {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (!isServerMsg(msg)) return;
|
|
189
|
+
const m = msg;
|
|
190
|
+
try {
|
|
191
|
+
switch (m.type) {
|
|
192
|
+
case "welcome": {
|
|
193
|
+
const iceServers = [
|
|
194
|
+
...session.iceServers ?? DEFAULT_ICE_SERVERS
|
|
195
|
+
];
|
|
196
|
+
if (m.turn) {
|
|
197
|
+
iceServers.push({
|
|
198
|
+
urls: m.turn.urls,
|
|
199
|
+
username: m.turn.username,
|
|
200
|
+
credential: m.turn.credential
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
pc = createPeerConnection(iceServers);
|
|
204
|
+
pc.addEventListener("icecandidate", (e) => {
|
|
205
|
+
if (e.candidate) {
|
|
206
|
+
send({ type: "ice-candidate", candidate: e.candidate.toJSON() });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
const dcSetup = setupDataChannel(pc);
|
|
210
|
+
dataChannel = dcSetup.channel;
|
|
211
|
+
const offer = await createOffer(pc);
|
|
212
|
+
send({ type: "offer", sdp: offer.sdp ?? "" });
|
|
213
|
+
dcSetup.ready.then(() => send({ type: "datachannel-ready" })).catch((err) => fail(toError(err)));
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
case "answer": {
|
|
217
|
+
if (!pc) throw new NetworkTestsError("BAD_STATE", "answer before welcome");
|
|
218
|
+
await applyAnswer(pc, m.sdp.sdp);
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
case "ice-candidate": {
|
|
222
|
+
if (!pc) return;
|
|
223
|
+
try {
|
|
224
|
+
await pc.addIceCandidate(m.candidate);
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case "datachannel-open": {
|
|
230
|
+
if (!dataChannel) throw new NetworkTestsError("BAD_STATE", "no DC");
|
|
231
|
+
packetLoop = startPacketLoop({ channel: dataChannel, onProgress });
|
|
232
|
+
send({
|
|
233
|
+
type: "start-packet-test",
|
|
234
|
+
duration: session.duration,
|
|
235
|
+
rate: session.rate,
|
|
236
|
+
packetSize: session.packetSize
|
|
237
|
+
});
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case "test-started":
|
|
241
|
+
break;
|
|
242
|
+
case "test-complete": {
|
|
243
|
+
const r = m.results;
|
|
244
|
+
succeed({
|
|
245
|
+
sessionId: session.sessionId,
|
|
246
|
+
packetsSent: r.packetsSent,
|
|
247
|
+
packetsReceived: r.packetsReceived,
|
|
248
|
+
packetsLost: r.packetsLost,
|
|
249
|
+
lossPercent: r.lossPercent,
|
|
250
|
+
jitter: r.jitter,
|
|
251
|
+
latency: {
|
|
252
|
+
avg: r.latency.avg,
|
|
253
|
+
min: r.latency.min,
|
|
254
|
+
max: r.latency.max,
|
|
255
|
+
p50: r.latency.p50,
|
|
256
|
+
p95: r.latency.p95,
|
|
257
|
+
samples: r.latency.samples
|
|
258
|
+
},
|
|
259
|
+
durationMs: Date.now() - startedAt,
|
|
260
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
261
|
+
});
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
case "test-stopped":
|
|
265
|
+
fail(new NetworkTestsError("TEST_STOPPED", "Server stopped the test"));
|
|
266
|
+
break;
|
|
267
|
+
case "error":
|
|
268
|
+
fail(new NetworkTestsError("SERVER_ERROR", m.message));
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
} catch (err) {
|
|
272
|
+
fail(toError(err));
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
function toError(value) {
|
|
278
|
+
if (value instanceof Error) return value;
|
|
279
|
+
return new NetworkTestsError("UNKNOWN", String(value));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/dnsleak/trigger.ts
|
|
283
|
+
var ABORT_AFTER_MS = 5e3;
|
|
284
|
+
function triggerDnsResolution(fqdn, signal) {
|
|
285
|
+
if (signal?.aborted) return Promise.resolve();
|
|
286
|
+
if (typeof Image !== "undefined") {
|
|
287
|
+
return triggerViaImage(fqdn, signal);
|
|
288
|
+
}
|
|
289
|
+
return triggerViaFetch(fqdn, signal);
|
|
290
|
+
}
|
|
291
|
+
function triggerViaImage(fqdn, signal) {
|
|
292
|
+
return new Promise((resolve) => {
|
|
293
|
+
const img = new Image();
|
|
294
|
+
let settled = false;
|
|
295
|
+
const finish = () => {
|
|
296
|
+
if (settled) return;
|
|
297
|
+
settled = true;
|
|
298
|
+
img.src = "";
|
|
299
|
+
resolve();
|
|
300
|
+
};
|
|
301
|
+
const onAbort = () => finish();
|
|
302
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
303
|
+
const timer = setTimeout(finish, ABORT_AFTER_MS);
|
|
304
|
+
img.onload = () => {
|
|
305
|
+
clearTimeout(timer);
|
|
306
|
+
finish();
|
|
307
|
+
};
|
|
308
|
+
img.onerror = () => {
|
|
309
|
+
clearTimeout(timer);
|
|
310
|
+
finish();
|
|
311
|
+
};
|
|
312
|
+
img.src = `https://${fqdn}/_/leak.gif?t=${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
function triggerViaFetch(fqdn, signal) {
|
|
316
|
+
const controller = new AbortController();
|
|
317
|
+
const abortSignal = signal ? mergeSignals(signal, controller.signal) : controller.signal;
|
|
318
|
+
const timer = setTimeout(() => controller.abort(), ABORT_AFTER_MS);
|
|
319
|
+
return fetch(`https://${fqdn}/_/leak?t=${Date.now()}`, {
|
|
320
|
+
method: "GET",
|
|
321
|
+
mode: "no-cors",
|
|
322
|
+
cache: "no-store",
|
|
323
|
+
signal: abortSignal
|
|
324
|
+
}).catch(() => void 0).finally(() => clearTimeout(timer)).then(() => void 0);
|
|
325
|
+
}
|
|
326
|
+
function mergeSignals(a, b) {
|
|
327
|
+
const c = new AbortController();
|
|
328
|
+
const onAbort = () => c.abort();
|
|
329
|
+
if (a.aborted || b.aborted) c.abort();
|
|
330
|
+
else {
|
|
331
|
+
a.addEventListener("abort", onAbort, { once: true });
|
|
332
|
+
b.addEventListener("abort", onAbort, { once: true });
|
|
333
|
+
}
|
|
334
|
+
return c.signal;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/dnsleak/index.ts
|
|
338
|
+
async function runDnsLeakTest(session, options = {}) {
|
|
339
|
+
if (options.signal?.aborted) throw new AbortError();
|
|
340
|
+
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
341
|
+
let timeoutHandle = null;
|
|
342
|
+
const timeoutController = new AbortController();
|
|
343
|
+
const combinedSignal = mergeSignals2(options.signal, timeoutController.signal);
|
|
344
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
345
|
+
timeoutHandle = setTimeout(() => {
|
|
346
|
+
timeoutController.abort();
|
|
347
|
+
reject(new TimeoutError(`DNS-leak test exceeded ${timeoutMs}ms`));
|
|
348
|
+
}, timeoutMs);
|
|
349
|
+
});
|
|
350
|
+
const abortPromise = new Promise((_, reject) => {
|
|
351
|
+
options.signal?.addEventListener(
|
|
352
|
+
"abort",
|
|
353
|
+
() => reject(new AbortError()),
|
|
354
|
+
{ once: true }
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
const triggers = session.fqdns.map(
|
|
358
|
+
(fqdn) => triggerDnsResolution(fqdn, combinedSignal).then(() => {
|
|
359
|
+
options.onResolved?.(fqdn);
|
|
360
|
+
})
|
|
361
|
+
);
|
|
362
|
+
try {
|
|
363
|
+
await Promise.race([
|
|
364
|
+
Promise.all(triggers),
|
|
365
|
+
timeoutPromise,
|
|
366
|
+
abortPromise
|
|
367
|
+
]);
|
|
368
|
+
} finally {
|
|
369
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function mergeSignals2(a, b) {
|
|
373
|
+
if (!a) return b;
|
|
374
|
+
const c = new AbortController();
|
|
375
|
+
if (a.aborted || b.aborted) c.abort();
|
|
376
|
+
else {
|
|
377
|
+
a.addEventListener("abort", () => c.abort(), { once: true });
|
|
378
|
+
b.addEventListener("abort", () => c.abort(), { once: true });
|
|
379
|
+
}
|
|
380
|
+
return c.signal;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/client.ts
|
|
384
|
+
var NetworkTests = class {
|
|
385
|
+
/**
|
|
386
|
+
* Run the UDP throughput probe to completion.
|
|
387
|
+
*
|
|
388
|
+
* Opens a WebSocket to `session.wsUrl`, negotiates a WebRTC DataChannel,
|
|
389
|
+
* runs the packet-loss test, and resolves with the final stats once the
|
|
390
|
+
* server emits `test-complete`.
|
|
391
|
+
*/
|
|
392
|
+
runUdpTest(session, options) {
|
|
393
|
+
return runUdpTest(session, options);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Trigger DNS resolution of every FQDN in the session. Returns once all
|
|
397
|
+
* triggers have fired (or errored — errors are expected because the
|
|
398
|
+
* fake server returns 192.0.2.1).
|
|
399
|
+
*
|
|
400
|
+
* Actual resolver IPs are observed server-side. Customer's backend
|
|
401
|
+
* polls `GET /v1/probe/dns-leak/sessions/:id` to retrieve them.
|
|
402
|
+
*/
|
|
403
|
+
runDnsLeakTest(session, options) {
|
|
404
|
+
return runDnsLeakTest(session, options);
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
export { AbortError, NetworkTests, NetworkTestsError, TimeoutError };
|
|
409
|
+
//# sourceMappingURL=index.mjs.map
|
|
410
|
+
//# sourceMappingURL=index.mjs.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/dnsleak/trigger.ts","../src/dnsleak/index.ts","../src/client.ts"],"names":["mergeSignals"],"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;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;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;;;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;;;ACpMA,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,GAAiBA,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;;;ACtCO,IAAM,eAAN,MAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQxB,UAAA,CAAW,SAAqB,OAAA,EAA6C;AAC3E,IAAA,OAAO,UAAA,CAAW,SAAS,OAAO,CAAA;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,cAAA,CAAe,SAAyB,OAAA,EAA4C;AAClF,IAAA,OAAO,cAAA,CAAe,SAAS,OAAO,CAAA;AAAA,EACxC;AACF","file":"index.mjs","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","// 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 { runUdpTest } from \"./udp/index.js\";\nimport { runDnsLeakTest } from \"./dnsleak/index.js\";\nimport type {\n UdpSession,\n DnsLeakSession,\n UdpResult,\n RunUdpOptions,\n RunDnsLeakOptions,\n} from \"./types.js\";\n\n/**\n * Entry point for the networktests.com browser SDK.\n *\n * The constructor takes no arguments — the SDK is keyless on the wire.\n * Customers' backends mint session tickets via the authenticated\n * api.networktests.com API and ship them to the browser.\n *\n * @example\n * const nt = new NetworkTests();\n * const session = await fetch(\"/my-backend/start-udp\").then(r => r.json());\n * const result = await nt.runUdpTest(session);\n */\nexport class NetworkTests {\n /**\n * Run the UDP throughput probe to completion.\n *\n * Opens a WebSocket to `session.wsUrl`, negotiates a WebRTC DataChannel,\n * runs the packet-loss test, and resolves with the final stats once the\n * server emits `test-complete`.\n */\n runUdpTest(session: UdpSession, options?: RunUdpOptions): Promise<UdpResult> {\n return runUdpTest(session, options);\n }\n\n /**\n * Trigger DNS resolution of every FQDN in the session. Returns once all\n * triggers have fired (or errored — errors are expected because the\n * fake server returns 192.0.2.1).\n *\n * Actual resolver IPs are observed server-side. Customer's backend\n * polls `GET /v1/probe/dns-leak/sessions/:id` to retrieve them.\n */\n runDnsLeakTest(session: DnsLeakSession, options?: RunDnsLeakOptions): Promise<void> {\n return runDnsLeakTest(session, options);\n }\n}\n"]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var NetworkTests=(()=>{var R=Object.defineProperty;var $=Object.getOwnPropertyDescriptor;var B=Object.getOwnPropertyNames;var q=Object.prototype.hasOwnProperty;var F=(t,e)=>{for(var r in e)R(t,r,{get:e[r],enumerable:!0})},H=(t,e,r,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of B(e))!q.call(t,a)&&a!==r&&R(t,a,{get:()=>e[a],enumerable:!(n=$(e,a))||n.enumerable});return t};var J=t=>H(R({},"__esModule",{value:!0}),t);var X={};F(X,{AbortError:()=>g,NetworkTests:()=>w,NetworkTestsError:()=>i,TimeoutError:()=>b});var i=class extends Error{constructor(e,r){super(r),this.name="NetworkTestsError",this.code=e}},g=class extends i{constructor(e="Operation aborted"){super("ABORTED",e),this.name="AbortError"}},b=class extends i{constructor(e="Operation timed out"){super("TIMEOUT",e),this.name="TimeoutError"}};function h(t){return new RTCPeerConnection({iceServers:t})}function P(t){let e=t.createDataChannel("test",{ordered:!1}),r=new Promise((n,a)=>{let c=()=>{l(),n()},o=()=>{l(),a(new i("DC_ERROR","DataChannel error before open"))},s=()=>{l(),a(new i("DC_CLOSED","DataChannel closed before open"))};function l(){e.removeEventListener("open",c),e.removeEventListener("error",o),e.removeEventListener("close",s)}e.addEventListener("open",c),e.addEventListener("error",o),e.addEventListener("close",s)});return{channel:e,ready:r}}async function L(t){let e=await t.createOffer();return await t.setLocalDescription(e),e}async function D(t,e){await t.setRemoteDescription({type:"answer",sdp:e})}function M(t){let{channel:e,onProgress:r}=t,n=t.progressIntervalMs??100,a=Date.now(),c=0,o=-1,s=!1,l=d=>{if(!s)try{let u=JSON.parse(d.data);if(u&&u.type==="packet"&&typeof u.seq=="number"){c++,u.seq>o&&(o=u.seq);try{e.send(JSON.stringify({type:"ack",seq:u.seq}))}catch{}}}catch{}};e.addEventListener("message",l);let m=null;return r&&(m=setInterval(()=>{if(s)return;let d=o+1,u=d>0?(d-c)/d*100:0;r({packetsSent:d,packetsReceived:c,lossPercent:Math.round(u*100)/100,elapsedMs:Date.now()-a})},n)),{stop(){s||(s=!0,e.removeEventListener("message",l),m&&clearInterval(m))}}}function O(t){return typeof t=="object"&&t!==null&&typeof t.type=="string"}var z=[{urls:"stun:stun.l.google.com:19302"}];async function A(t,e={}){let r=Date.now(),{signal:n,onProgress:a}=e,c=e.timeoutMs??((t.duration??30)+10)*1e3;if(n?.aborted)throw new g;let o=new WebSocket(t.wsUrl);o.binaryType="arraybuffer";let s=null,l=null,m=null,d=null,u=null,E=()=>{d&&clearTimeout(d),u&&n&&n.removeEventListener("abort",u),m?.stop();try{l?.close()}catch{}try{s?.close()}catch{}if(o.readyState===WebSocket.OPEN||o.readyState===WebSocket.CONNECTING)try{o.close()}catch{}};return new Promise((_,N)=>{let S=f=>{E(),N(f)},W=f=>{E(),_(f)};d=setTimeout(()=>S(new b(`UDP test exceeded ${c}ms`)),c),n&&(u=()=>S(new g),n.addEventListener("abort",u,{once:!0}));let k=f=>{o.readyState===WebSocket.OPEN&&o.send(JSON.stringify(f))};o.addEventListener("error",()=>S(new i("WS_ERROR","WebSocket error"))),o.addEventListener("close",f=>{if(o.readyState===WebSocket.CLOSED){let T=f.reason||`WebSocket closed (code ${f.code})`;S(new i("WS_CLOSED",T))}}),o.addEventListener("open",async()=>{}),o.addEventListener("message",async f=>{let T;try{T=JSON.parse(String(f.data))}catch{return}if(!O(T))return;let y=T;try{switch(y.type){case"welcome":{let p=[...t.iceServers??z];y.turn&&p.push({urls:y.turn.urls,username:y.turn.username,credential:y.turn.credential}),s=h(p),s.addEventListener("icecandidate",v=>{v.candidate&&k({type:"ice-candidate",candidate:v.candidate.toJSON()})});let C=P(s);l=C.channel;let j=await L(s);k({type:"offer",sdp:j.sdp??""}),C.ready.then(()=>k({type:"datachannel-ready"})).catch(v=>S(x(v)));break}case"answer":{if(!s)throw new i("BAD_STATE","answer before welcome");await D(s,y.sdp.sdp);break}case"ice-candidate":{if(!s)return;try{await s.addIceCandidate(y.candidate)}catch{}break}case"datachannel-open":{if(!l)throw new i("BAD_STATE","no DC");m=M({channel:l,onProgress:a}),k({type:"start-packet-test",duration:t.duration,rate:t.rate,packetSize:t.packetSize});break}case"test-started":break;case"test-complete":{let p=y.results;W({sessionId:t.sessionId,packetsSent:p.packetsSent,packetsReceived:p.packetsReceived,packetsLost:p.packetsLost,lossPercent:p.lossPercent,jitter:p.jitter,latency:{avg:p.latency.avg,min:p.latency.min,max:p.latency.max,p50:p.latency.p50,p95:p.latency.p95,samples:p.latency.samples},durationMs:Date.now()-r,completedAt:new Date().toISOString()});break}case"test-stopped":S(new i("TEST_STOPPED","Server stopped the test"));break;case"error":S(new i("SERVER_ERROR",y.message));break}}catch(p){S(x(p))}})})}function x(t){return t instanceof Error?t:new i("UNKNOWN",String(t))}function U(t,e){return e?.aborted?Promise.resolve():typeof Image<"u"?V(t,e):G(t,e)}function V(t,e){return new Promise(r=>{let n=new Image,a=!1,c=()=>{a||(a=!0,n.src="",r())},o=()=>c();e?.addEventListener("abort",o,{once:!0});let s=setTimeout(c,5e3);n.onload=()=>{clearTimeout(s),c()},n.onerror=()=>{clearTimeout(s),c()},n.src=`https://${t}/_/leak.gif?t=${Date.now()}-${Math.random().toString(36).slice(2)}`})}function G(t,e){let r=new AbortController,n=e?K(e,r.signal):r.signal,a=setTimeout(()=>r.abort(),5e3);return fetch(`https://${t}/_/leak?t=${Date.now()}`,{method:"GET",mode:"no-cors",cache:"no-store",signal:n}).catch(()=>{}).finally(()=>clearTimeout(a)).then(()=>{})}function K(t,e){let r=new AbortController,n=()=>r.abort();return t.aborted||e.aborted?r.abort():(t.addEventListener("abort",n,{once:!0}),e.addEventListener("abort",n,{once:!0})),r.signal}async function I(t,e={}){if(e.signal?.aborted)throw new g;let r=e.timeoutMs??15e3,n=null,a=new AbortController,c=Q(e.signal,a.signal),o=new Promise((m,d)=>{n=setTimeout(()=>{a.abort(),d(new b(`DNS-leak test exceeded ${r}ms`))},r)}),s=new Promise((m,d)=>{e.signal?.addEventListener("abort",()=>d(new g),{once:!0})}),l=t.fqdns.map(m=>U(m,c).then(()=>{e.onResolved?.(m)}));try{await Promise.race([Promise.all(l),o,s])}finally{n&&clearTimeout(n)}}function Q(t,e){if(!t)return e;let r=new AbortController;return t.aborted||e.aborted?r.abort():(t.addEventListener("abort",()=>r.abort(),{once:!0}),e.addEventListener("abort",()=>r.abort(),{once:!0})),r.signal}var w=class{runUdpTest(e,r){return A(e,r)}runDnsLeakTest(e,r){return I(e,r)}};return J(X);})();
|
|
2
|
+
//# sourceMappingURL=networktests.umd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/udp/webrtc.ts","../src/udp/packetLoop.ts","../src/udp/signaling.ts","../src/udp/index.ts","../src/dnsleak/trigger.ts","../src/dnsleak/index.ts","../src/client.ts"],"sourcesContent":["export { NetworkTests } from \"./client.js\";\nexport { NetworkTestsError, AbortError, TimeoutError } from \"./errors.js\";\nexport type {\n UdpSession,\n DnsLeakSession,\n UdpResult,\n UdpProgress,\n LatencyStats,\n RunUdpOptions,\n RunDnsLeakOptions,\n} from \"./types.js\";\n","// 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","// 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 { runUdpTest } from \"./udp/index.js\";\nimport { runDnsLeakTest } from \"./dnsleak/index.js\";\nimport type {\n UdpSession,\n DnsLeakSession,\n UdpResult,\n RunUdpOptions,\n RunDnsLeakOptions,\n} from \"./types.js\";\n\n/**\n * Entry point for the networktests.com browser SDK.\n *\n * The constructor takes no arguments — the SDK is keyless on the wire.\n * Customers' backends mint session tickets via the authenticated\n * api.networktests.com API and ship them to the browser.\n *\n * @example\n * const nt = new NetworkTests();\n * const session = await fetch(\"/my-backend/start-udp\").then(r => r.json());\n * const result = await nt.runUdpTest(session);\n */\nexport class NetworkTests {\n /**\n * Run the UDP throughput probe to completion.\n *\n * Opens a WebSocket to `session.wsUrl`, negotiates a WebRTC DataChannel,\n * runs the packet-loss test, and resolves with the final stats once the\n * server emits `test-complete`.\n */\n runUdpTest(session: UdpSession, options?: RunUdpOptions): Promise<UdpResult> {\n return runUdpTest(session, options);\n }\n\n /**\n * Trigger DNS resolution of every FQDN in the session. Returns once all\n * triggers have fired (or errored — errors are expected because the\n * fake server returns 192.0.2.1).\n *\n * Actual resolver IPs are observed server-side. Customer's backend\n * polls `GET /v1/probe/dns-leak/sessions/:id` to retrieve them.\n */\n runDnsLeakTest(session: DnsLeakSession, options?: RunDnsLeakOptions): Promise<void> {\n return runDnsLeakTest(session, options);\n }\n}\n"],"mappings":"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","names":["src_exports","__export","AbortError","NetworkTests","NetworkTestsError","TimeoutError","NetworkTestsError","code","message","AbortError","TimeoutError","createPeerConnection","iceServers","setupDataChannel","pc","channel","ready","resolve","reject","onOpen","cleanup","onError","NetworkTestsError","onClose","createOffer","offer","applyAnswer","sdp","startPacketLoop","opts","channel","onProgress","progressMs","startTs","received","highestSeq","stopped","onMsg","ev","msg","progressTimer","sent","loss","isServerMsg","value","DEFAULT_ICE_SERVERS","runUdpTest","session","options","startedAt","signal","onProgress","timeoutMs","AbortError","ws","pc","dataChannel","packetLoop","timeoutHandle","abortHandler","cleanup","resolve","reject","fail","err","succeed","result","TimeoutError","send","msg","NetworkTestsError","ev","reason","isServerMsg","m","iceServers","createPeerConnection","e","dcSetup","setupDataChannel","offer","createOffer","toError","applyAnswer","startPacketLoop","r","value","triggerDnsResolution","fqdn","signal","triggerViaImage","triggerViaFetch","resolve","img","settled","finish","onAbort","timer","controller","abortSignal","mergeSignals","a","b","c","runDnsLeakTest","session","options","AbortError","timeoutMs","timeoutHandle","timeoutController","combinedSignal","mergeSignals","timeoutPromise","_","reject","TimeoutError","abortPromise","triggers","fqdn","triggerDnsResolution","a","b","c","NetworkTests","session","options","runUdpTest","runDnsLeakTest"]}
|