@mark1russell7/netdiag 0.1.0 → 0.1.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.
Files changed (45) hide show
  1. package/README.md +153 -1
  2. package/build/address.d.ts +56 -0
  3. package/build/address.d.ts.map +1 -0
  4. package/build/address.js +172 -0
  5. package/build/address.js.map +1 -0
  6. package/build/{diagnosis-BTa-Dfwl.js → diagnosis-a4hRzFxp.js} +113 -11
  7. package/build/diagnosis-a4hRzFxp.js.map +1 -0
  8. package/build/diagnosis.d.ts +3 -2
  9. package/build/diagnosis.js +1 -1
  10. package/build/{index-CP1hzELI.d.ts → index-K2dGiYxW.d.ts} +83 -22
  11. package/build/index-K2dGiYxW.d.ts.map +1 -0
  12. package/build/index.d.ts +253 -148
  13. package/build/index.d.ts.map +1 -1
  14. package/build/index.js +1084 -161
  15. package/build/index.js.map +1 -1
  16. package/build/kinds-CQv769lK.js +341 -0
  17. package/build/kinds-CQv769lK.js.map +1 -0
  18. package/build/kinds-_NShfZpU.d.ts +377 -0
  19. package/build/kinds-_NShfZpU.d.ts.map +1 -0
  20. package/build/layer-BNg0VYuK.d.ts +22 -0
  21. package/build/layer-BNg0VYuK.d.ts.map +1 -0
  22. package/build/tools.d.ts +206 -0
  23. package/build/tools.d.ts.map +1 -0
  24. package/build/tools.js +896 -0
  25. package/build/tools.js.map +1 -0
  26. package/package.json +17 -5
  27. package/src/address/index.ts +155 -0
  28. package/src/diagnosis/catalog.ts +8 -8
  29. package/src/diagnosis/index.ts +13 -0
  30. package/src/diagnosis/score.ts +160 -0
  31. package/src/index.ts +35 -0
  32. package/src/kinds.ts +460 -0
  33. package/src/probes/native.ts +354 -0
  34. package/src/probes/socket.ts +520 -0
  35. package/src/readers.ts +367 -0
  36. package/src/scope.ts +5 -24
  37. package/src/tools/common.ts +47 -0
  38. package/src/tools/host.ts +91 -0
  39. package/src/tools/index.ts +38 -0
  40. package/src/tools/licenses.ts +37 -0
  41. package/src/tools/mtr.ts +156 -0
  42. package/src/tools/ping.ts +231 -0
  43. package/src/tools/traceroute.ts +313 -0
  44. package/build/diagnosis-BTa-Dfwl.js.map +0 -1
  45. package/build/index-CP1hzELI.d.ts.map +0 -1
@@ -0,0 +1,341 @@
1
+ import { isIP } from "node:net";
2
+ import { err, ok } from "@mark1russell7/optional";
3
+ // src/target.ts
4
+ /** The ports of the URL schemes that a target can have. */
5
+ const SCHEME_PORTS = /* @__PURE__ */ new Map([
6
+ ["http:", 80],
7
+ ["https:", 443],
8
+ ["ws:", 80],
9
+ ["wss:", 443],
10
+ ["dns:", 53],
11
+ ["tls:", 443]
12
+ ]);
13
+ /** This function gives the kind of a host in its canonical form. */
14
+ function hostKind(host) {
15
+ const family = isIP(host);
16
+ if (family === 4) return "ipv4";
17
+ if (family === 6) return "ipv6";
18
+ return "name";
19
+ }
20
+ /** This function gives the canonical form of a host: lower case, with no final dot and no brackets. */
21
+ function canonicalHost(host) {
22
+ let text = host.trim().toLowerCase();
23
+ if (text.startsWith("[") && text.endsWith("]")) text = text.slice(1, -1);
24
+ if (text.endsWith(".")) text = text.slice(0, -1);
25
+ return text;
26
+ }
27
+ /** This function makes a target from a host and an optional port. */
28
+ function target(host, port) {
29
+ const canonical = canonicalHost(host);
30
+ return port === void 0 ? {
31
+ host: canonical,
32
+ kind: hostKind(canonical)
33
+ } : {
34
+ host: canonical,
35
+ kind: hostKind(canonical),
36
+ port
37
+ };
38
+ }
39
+ /** This function gives the text of a target: `host`, `host:port`, or `[address]:port` for an IPv6 address. */
40
+ function formatTarget(t) {
41
+ const host = t.kind === "ipv6" && t.port !== void 0 ? `[${t.host}]` : t.host;
42
+ return t.port === void 0 ? host : `${host}:${t.port}`;
43
+ }
44
+ /** This function tells if two targets have the same host and the same port. */
45
+ function sameTarget(a, b) {
46
+ return a.host === b.host && a.port === b.port;
47
+ }
48
+ const LABEL = /^(?!-)[a-z0-9-]{1,63}(?<!-)$/;
49
+ function validName(host) {
50
+ if (host.length === 0 || host.length > 253) return false;
51
+ return host.split(".").every((label) => LABEL.test(label) || label === "_" || /^_[a-z0-9-]{1,62}$/.test(label));
52
+ }
53
+ function portOf(text) {
54
+ if (!/^\d{1,5}$/.test(text)) return void 0;
55
+ const port = Number(text);
56
+ return port >= 1 && port <= 65535 ? port : void 0;
57
+ }
58
+ /**
59
+ * This function reads a target from the text that a user gives.
60
+ *
61
+ * It accepts a URL (`https://example.com/a`), a host (`example.com`) and a host with a port (`example.com:8443`).
62
+ * It also accepts an IPv4 address, and an IPv6 address in brackets with a port (`[2001:db8::1]:443`).
63
+ * A URL with a known scheme and no port gets the port of its scheme.
64
+ */
65
+ function parseTarget(text) {
66
+ const input = text.trim();
67
+ if (input.length === 0) return err("The target is empty.");
68
+ if (input.includes("://")) {
69
+ let url;
70
+ try {
71
+ url = new URL(input);
72
+ } catch {
73
+ return err(`"${input}" is not a valid URL.`);
74
+ }
75
+ const host = canonicalHost(url.hostname);
76
+ if (host.length === 0) return err(`The URL "${input}" has no host.`);
77
+ return checked(host, url.port === "" ? SCHEME_PORTS.get(url.protocol) : Number(url.port), input);
78
+ }
79
+ if (isIP(input) === 6) return checked(canonicalHost(input), void 0, input);
80
+ const bracket = /^\[([^\]]+)\](?::(.*))?$/.exec(input);
81
+ if (bracket !== null) {
82
+ const host = bracket[1] ?? "";
83
+ if (isIP(host) !== 6) return err(`"${host}" is not an IPv6 address.`);
84
+ if (bracket[2] === void 0) return checked(canonicalHost(host), void 0, input);
85
+ const port = portOf(bracket[2]);
86
+ return port === void 0 ? err(`"${bracket[2]}" is not a port from 1 to 65535.`) : checked(canonicalHost(host), port, input);
87
+ }
88
+ const colon = input.lastIndexOf(":");
89
+ if (colon === -1) return checked(canonicalHost(input), void 0, input);
90
+ const port = portOf(input.slice(colon + 1));
91
+ if (port === void 0) return err(`"${input.slice(colon + 1)}" is not a port from 1 to 65535.`);
92
+ return checked(canonicalHost(input.slice(0, colon)), port, input);
93
+ }
94
+ function checked(host, port, input) {
95
+ const kind = hostKind(host);
96
+ if (kind === "name" && !validName(host)) return err(`"${input}" has no valid host name.`);
97
+ return ok(port === void 0 ? {
98
+ host,
99
+ kind
100
+ } : {
101
+ host,
102
+ kind,
103
+ port
104
+ });
105
+ }
106
+ // src/fact.ts
107
+ const KIND_NAME = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;
108
+ /** This function makes a fact kind. The name has lower-case words with dots, for example `tls.handshake`. */
109
+ function defineFactKind(spec) {
110
+ if (!KIND_NAME.test(spec.kind)) throw new RangeError(`"${spec.kind}" is not a valid name of a fact kind`);
111
+ return Object.freeze({ ...spec });
112
+ }
113
+ /** This function tells if a fact is of a kind, and if its value passes the check of the kind. */
114
+ function isFactOf(kind, fact) {
115
+ return fact.kind === kind.kind && fact.layer === kind.layer && kind.check(fact.value);
116
+ }
117
+ /** This function gives the facts of one kind, in their order. */
118
+ function factsOf(kind, facts) {
119
+ return facts.filter((fact) => isFactOf(kind, fact));
120
+ }
121
+ /** This function makes an observation with a type check of its value against its kind. */
122
+ function observe(kind, subject, value, time) {
123
+ return time === void 0 ? {
124
+ kind,
125
+ subject,
126
+ value
127
+ } : {
128
+ kind,
129
+ subject,
130
+ value,
131
+ time
132
+ };
133
+ }
134
+ // src/probe.ts
135
+ /** This function makes a probe, and examines its fields. */
136
+ function defineProbe(probe) {
137
+ if (!/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/.test(probe.id)) throw new RangeError(`"${probe.id}" is not a valid probe id`);
138
+ if (probe.kinds.length === 0) throw new RangeError(`The probe "${probe.id}" gives no kind of fact`);
139
+ return probe;
140
+ }
141
+ // src/kinds.ts
142
+ function isRecord(value) {
143
+ return typeof value === "object" && value !== null && !Array.isArray(value);
144
+ }
145
+ function isText(value) {
146
+ return typeof value === "string" && value.length > 0;
147
+ }
148
+ function isDuration(value) {
149
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
150
+ }
151
+ function isPort(value) {
152
+ return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65535;
153
+ }
154
+ function isCount(value) {
155
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
156
+ }
157
+ function isOneOf(values, value) {
158
+ return typeof value === "string" && values.includes(value);
159
+ }
160
+ function optional(value, check) {
161
+ return value === void 0 || check(value);
162
+ }
163
+ function isTextList(value) {
164
+ return Array.isArray(value) && value.every(isText);
165
+ }
166
+ /** The outcomes of a DNS query. `nodata` tells that the name exists, but it has no record of the type. */
167
+ const DNS_OUTCOMES = [
168
+ "ok",
169
+ "nodata",
170
+ "nxdomain",
171
+ "servfail",
172
+ "refused",
173
+ "timeout",
174
+ "unreachable",
175
+ "error"
176
+ ];
177
+ /** The record types of a DNS query for addresses. */
178
+ const RECORD_TYPES = ["A", "AAAA"];
179
+ /** The fact kind `dns.answer`. */
180
+ const DNS_ANSWER = defineFactKind({
181
+ kind: "dns.answer",
182
+ layer: "dns",
183
+ summary: "The answer of a DNS resolver to one query for the addresses of a name.",
184
+ check: (value) => isRecord(value) && isText(value["name"]) && isOneOf(RECORD_TYPES, value["record"]) && isText(value["resolver"]) && isOneOf(DNS_OUTCOMES, value["outcome"]) && isTextList(value["addresses"]) && (value["outcome"] !== "ok" || value["addresses"].length > 0) && optional(value["ttl"], isDuration) && isDuration(value["ms"]) && optional(value["code"], isText)
185
+ });
186
+ /** The outcomes of a TCP connect. */
187
+ const CONNECT_OUTCOMES = [
188
+ "connected",
189
+ "refused",
190
+ "timeout",
191
+ "unreachable",
192
+ "reset",
193
+ "error"
194
+ ];
195
+ /** The fact kind `tcp.connect`. */
196
+ const TCP_CONNECT = defineFactKind({
197
+ kind: "tcp.connect",
198
+ layer: "tcp",
199
+ summary: "The outcome and the time of one TCP connect to an address and a port.",
200
+ check: (value) => isRecord(value) && isText(value["address"]) && isPort(value["port"]) && (value["family"] === 4 || value["family"] === 6) && isOneOf(CONNECT_OUTCOMES, value["outcome"]) && isDuration(value["ms"]) && optional(value["code"], isText)
201
+ });
202
+ /**
203
+ * The outcomes of a TLS handshake.
204
+ *
205
+ * - `cert-error`: the handshake ended, but the certificate is not valid for the name.
206
+ * - `alert`: the client and the server stopped the handshake, for example because they have no version in common.
207
+ * - `not-tls`: the server does not speak TLS on the port.
208
+ * - `closed`: the server closed the connection before the end of the handshake.
209
+ */
210
+ const HANDSHAKE_OUTCOMES = [
211
+ "ok",
212
+ "cert-error",
213
+ "alert",
214
+ "not-tls",
215
+ "timeout",
216
+ "reset",
217
+ "closed",
218
+ "error"
219
+ ];
220
+ /** The roots of trust that a certificate chain ends at: a public root, a root of the computer or the user, or none. */
221
+ const TRUSTS = [
222
+ "public",
223
+ "private",
224
+ "none"
225
+ ];
226
+ /** The problems of a certificate. */
227
+ const CERT_PROBLEMS = [
228
+ "expired",
229
+ "not-yet-valid",
230
+ "name",
231
+ "untrusted",
232
+ "revoked",
233
+ "other"
234
+ ];
235
+ function isCertificate(value) {
236
+ return isRecord(value) && typeof value["subject"] === "string" && typeof value["issuer"] === "string" && isTextList(value["names"]) && isText(value["validFrom"]) && isText(value["validTo"]) && isText(value["fingerprint256"]) && typeof value["selfSigned"] === "boolean" && typeof value["chain"] === "number" && Number.isInteger(value["chain"]) && value["chain"] >= 1 && isOneOf(TRUSTS, value["trust"]) && optional(value["problem"], (problem) => isOneOf(CERT_PROBLEMS, problem));
237
+ }
238
+ /** The fact kind `tls.handshake`. */
239
+ const TLS_HANDSHAKE = defineFactKind({
240
+ kind: "tls.handshake",
241
+ layer: "tls",
242
+ summary: "The outcome and the time of one TLS handshake, and the certificate of the server.",
243
+ check: (value) => isRecord(value) && isText(value["address"]) && isPort(value["port"]) && optional(value["servername"], isText) && isOneOf(HANDSHAKE_OUTCOMES, value["outcome"]) && isDuration(value["ms"]) && optional(value["protocol"], isText) && optional(value["cipher"], isText) && optional(value["alpn"], isText) && optional(value["code"], isText) && optional(value["certificate"], isCertificate)
244
+ });
245
+ /**
246
+ * The outcomes of an HTTP request.
247
+ *
248
+ * - `response`: the server sent a response. Its status can be an error.
249
+ * - `protocol`: the server sent bytes that are not HTTP.
250
+ * - `closed`: the server closed the connection before the response.
251
+ */
252
+ const HTTP_OUTCOMES = [
253
+ "response",
254
+ "protocol",
255
+ "timeout",
256
+ "reset",
257
+ "closed",
258
+ "error"
259
+ ];
260
+ /** The fact kind `http.response`. */
261
+ const HTTP_RESPONSE = defineFactKind({
262
+ kind: "http.response",
263
+ layer: "http",
264
+ summary: "The outcome and the times of one HTTP request: the status, some fields of the head and the time to the first byte.",
265
+ check: (value) => isRecord(value) && isText(value["address"]) && isPort(value["port"]) && (value["scheme"] === "http" || value["scheme"] === "https") && (value["method"] === "GET" || value["method"] === "HEAD") && isText(value["host"]) && isText(value["path"]) && isOneOf(HTTP_OUTCOMES, value["outcome"]) && (value["outcome"] !== "response" || typeof value["status"] === "number" && Number.isInteger(value["status"])) && optional(value["status"], (status) => typeof status === "number" && status >= 100 && status <= 999) && optional(value["version"], isText) && isRecord(value["fields"]) && Object.values(value["fields"]).every((field) => typeof field === "string") && typeof value["bytes"] === "number" && Number.isInteger(value["bytes"]) && value["bytes"] >= 0 && typeof value["truncated"] === "boolean" && optional(value["ttfbMs"], isDuration) && isDuration(value["ms"]) && optional(value["code"], isText)
266
+ });
267
+ /** The fact kinds of the native probes. */
268
+ const NATIVE_KINDS = [
269
+ DNS_ANSWER,
270
+ TCP_CONNECT,
271
+ TLS_HANDSHAKE,
272
+ HTTP_RESPONSE
273
+ ];
274
+ /**
275
+ * The outcomes of the ICMP echo requests to one address.
276
+ *
277
+ * - `reply`: one or more echo replies came back.
278
+ * - `none`: no reply came back in the time limit.
279
+ * - `unreachable`: no echo reply came back, and a router or this computer told that the address is unreachable.
280
+ * - `error`: the tool did not start, or netdiag cannot read its output.
281
+ */
282
+ const ECHO_OUTCOMES = [
283
+ "reply",
284
+ "none",
285
+ "unreachable",
286
+ "error"
287
+ ];
288
+ /** The fact kind `icmp.echo`. */
289
+ const ICMP_ECHO = defineFactKind({
290
+ kind: "icmp.echo",
291
+ layer: "ip",
292
+ summary: "The replies to the ICMP echo requests to one address, their round-trip times and the messages of an unreachable address.",
293
+ check: (value) => isRecord(value) && isText(value["address"]) && isOneOf(ECHO_OUTCOMES, value["outcome"]) && isCount(value["sent"]) && isCount(value["received"]) && value["received"] <= value["sent"] && Array.isArray(value["rttMs"]) && value["rttMs"].every(isDuration) && value["rttMs"].length <= value["received"] && value["outcome"] === "reply" === value["received"] > 0 && optional(value["ttl"], isCount) && optional(value["from"], isText) && optional(value["detail"], isText)
294
+ });
295
+ /**
296
+ * The outcomes of a trace of the path to an address.
297
+ *
298
+ * - `reached`: the address answered at the last hop.
299
+ * - `stopped`: the last hop is not the address, and no router told that the address is unreachable.
300
+ * - `unreachable`: a router or this computer told that the address is unreachable.
301
+ * - `error`: the tool failed, or netdiag cannot read its output.
302
+ */
303
+ const TRACE_OUTCOMES = [
304
+ "reached",
305
+ "stopped",
306
+ "unreachable",
307
+ "error"
308
+ ];
309
+ function isTraceHop(value) {
310
+ return isRecord(value) && isCount(value["ttl"]) && value["ttl"] >= 1 && isTextList(value["addresses"]) && isCount(value["sent"]) && isCount(value["received"]) && value["received"] <= value["sent"] && value["received"] > 0 === value["addresses"].length > 0 && Array.isArray(value["rttMs"]) && value["rttMs"].every(isDuration) && value["rttMs"].length <= value["received"] && optional(value["unreachable"], isText);
311
+ }
312
+ /** This function tells if the TTL of each hop is larger than the TTL of the hop before it, and not larger than `maxHops`. */
313
+ function inOrder(hops, maxHops) {
314
+ return hops.every((hop, index) => hop.ttl <= maxHops && (index === 0 || hop.ttl > (hops[index - 1]?.ttl ?? 0)));
315
+ }
316
+ /** The fact kind `path.trace`. */
317
+ const PATH_TRACE = defineFactKind({
318
+ kind: "path.trace",
319
+ layer: "path",
320
+ summary: "The hops of the path to one address: the routers that answered each TTL, their round-trip times and the messages of an unreachable address.",
321
+ check: (value) => isRecord(value) && isText(value["address"]) && isOneOf(TRACE_OUTCOMES, value["outcome"]) && isCount(value["maxHops"]) && Array.isArray(value["hops"]) && value["hops"].every(isTraceHop) && inOrder(value["hops"], value["maxHops"]) && (value["outcome"] !== "reached" || value["hops"].length > 0) && optional(value["detail"], isText)
322
+ });
323
+ function isReportHop(value) {
324
+ return isRecord(value) && isCount(value["ttl"]) && value["ttl"] >= 1 && optional(value["address"], isText) && isCount(value["sent"]) && isCount(value["received"]) && value["received"] <= value["sent"] && (value["received"] === 0 || value["address"] !== void 0) && optional(value["bestMs"], isDuration) && optional(value["meanMs"], isDuration) && optional(value["worstMs"], isDuration);
325
+ }
326
+ /** The fact kind `path.report`. */
327
+ const PATH_REPORT = defineFactKind({
328
+ kind: "path.report",
329
+ layer: "path",
330
+ summary: "The hops of the path to one address over many cycles of probes: the address, the loss and the round-trip times of each hop.",
331
+ check: (value) => isRecord(value) && isText(value["address"]) && isOneOf(TRACE_OUTCOMES, value["outcome"]) && value["outcome"] !== "unreachable" && isCount(value["cycles"]) && Array.isArray(value["hops"]) && value["hops"].every(isReportHop) && inOrder(value["hops"], Number.MAX_SAFE_INTEGER) && (value["outcome"] !== "reached" || value["hops"].length > 0) && optional(value["detail"], isText)
332
+ });
333
+ /** The fact kinds of the adapters of outside tools (`@mark1russell7/netdiag/tools`). */
334
+ const TOOL_KINDS = [
335
+ ICMP_ECHO,
336
+ PATH_TRACE,
337
+ PATH_REPORT
338
+ ];
339
+ export { observe as C, parseTarget as D, hostKind as E, sameTarget as O, isFactOf as S, formatTarget as T, TRACE_OUTCOMES as _, ECHO_OUTCOMES as a, defineFactKind as b, HTTP_RESPONSE as c, PATH_REPORT as d, PATH_TRACE as f, TOOL_KINDS as g, TLS_HANDSHAKE as h, DNS_OUTCOMES as i, target as k, ICMP_ECHO as l, TCP_CONNECT as m, CONNECT_OUTCOMES as n, HANDSHAKE_OUTCOMES as o, RECORD_TYPES as p, DNS_ANSWER as r, HTTP_OUTCOMES as s, CERT_PROBLEMS as t, NATIVE_KINDS as u, TRUSTS as v, canonicalHost as w, factsOf as x, defineProbe as y };
340
+
341
+ //# sourceMappingURL=kinds-CQv769lK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kinds-CQv769lK.js","names":[],"sources":["../src/target.ts","../src/fact.ts","../src/probe.ts","../src/kinds.ts"],"sourcesContent":["import { isIP } from \"node:net\";\nimport { err, ok, type Result } from \"@mark1russell7/optional\";\n\n/** The kind of the host of a target: a DNS name, an IPv4 address or an IPv6 address. */\nexport type HostKind = \"name\" | \"ipv4\" | \"ipv6\";\n\n/**\n * A host that a probe examines, and an optional port.\n *\n * The host is in its canonical form: lower case, with no final dot and no brackets around an IPv6 address.\n */\nexport interface Target {\n readonly host: string;\n readonly kind: HostKind;\n readonly port?: number | undefined;\n}\n\n/** The ports of the URL schemes that a target can have. */\nconst SCHEME_PORTS: ReadonlyMap<string, number> = new Map([\n [\"http:\", 80],\n [\"https:\", 443],\n [\"ws:\", 80],\n [\"wss:\", 443],\n [\"dns:\", 53],\n [\"tls:\", 443],\n]);\n\n/** This function gives the kind of a host in its canonical form. */\nexport function hostKind(host: string): HostKind {\n const family = isIP(host);\n if (family === 4) return \"ipv4\";\n if (family === 6) return \"ipv6\";\n return \"name\";\n}\n\n/** This function gives the canonical form of a host: lower case, with no final dot and no brackets. */\nexport function canonicalHost(host: string): string {\n let text = host.trim().toLowerCase();\n if (text.startsWith(\"[\") && text.endsWith(\"]\")) text = text.slice(1, -1);\n if (text.endsWith(\".\")) text = text.slice(0, -1);\n return text;\n}\n\n/** This function makes a target from a host and an optional port. */\nexport function target(host: string, port?: number): Target {\n const canonical = canonicalHost(host);\n return port === undefined ? { host: canonical, kind: hostKind(canonical) } : { host: canonical, kind: hostKind(canonical), port };\n}\n\n/** This function gives the text of a target: `host`, `host:port`, or `[address]:port` for an IPv6 address. */\nexport function formatTarget(t: Target): string {\n const host = t.kind === \"ipv6\" && t.port !== undefined ? `[${t.host}]` : t.host;\n return t.port === undefined ? host : `${host}:${t.port}`;\n}\n\n/** This function tells if two targets have the same host and the same port. */\nexport function sameTarget(a: Target, b: Target): boolean {\n return a.host === b.host && a.port === b.port;\n}\n\nconst LABEL = /^(?!-)[a-z0-9-]{1,63}(?<!-)$/;\n\nfunction validName(host: string): boolean {\n if (host.length === 0 || host.length > 253) return false;\n return host.split(\".\").every((label) => LABEL.test(label) || label === \"_\" || /^_[a-z0-9-]{1,62}$/.test(label));\n}\n\nfunction portOf(text: string): number | undefined {\n if (!/^\\d{1,5}$/.test(text)) return undefined;\n const port = Number(text);\n return port >= 1 && port <= 65535 ? port : undefined;\n}\n\n/**\n * This function reads a target from the text that a user gives.\n *\n * It accepts a URL (`https://example.com/a`), a host (`example.com`) and a host with a port (`example.com:8443`).\n * It also accepts an IPv4 address, and an IPv6 address in brackets with a port (`[2001:db8::1]:443`).\n * A URL with a known scheme and no port gets the port of its scheme.\n */\nexport function parseTarget(text: string): Result<Target, string> {\n const input = text.trim();\n if (input.length === 0) return err(\"The target is empty.\");\n if (input.includes(\"://\")) {\n let url: URL;\n try {\n url = new URL(input);\n } catch {\n return err(`\"${input}\" is not a valid URL.`);\n }\n const host = canonicalHost(url.hostname);\n if (host.length === 0) return err(`The URL \"${input}\" has no host.`);\n const port = url.port === \"\" ? SCHEME_PORTS.get(url.protocol) : Number(url.port);\n return checked(host, port, input);\n }\n if (isIP(input) === 6) return checked(canonicalHost(input), undefined, input);\n const bracket = /^\\[([^\\]]+)\\](?::(.*))?$/.exec(input);\n if (bracket !== null) {\n const host = bracket[1] ?? \"\";\n if (isIP(host) !== 6) return err(`\"${host}\" is not an IPv6 address.`);\n if (bracket[2] === undefined) return checked(canonicalHost(host), undefined, input);\n const port = portOf(bracket[2]);\n return port === undefined ? err(`\"${bracket[2]}\" is not a port from 1 to 65535.`) : checked(canonicalHost(host), port, input);\n }\n const colon = input.lastIndexOf(\":\");\n if (colon === -1) return checked(canonicalHost(input), undefined, input);\n const port = portOf(input.slice(colon + 1));\n if (port === undefined) return err(`\"${input.slice(colon + 1)}\" is not a port from 1 to 65535.`);\n return checked(canonicalHost(input.slice(0, colon)), port, input);\n}\n\nfunction checked(host: string, port: number | undefined, input: string): Result<Target, string> {\n const kind = hostKind(host);\n if (kind === \"name\" && !validName(host)) return err(`\"${input}\" has no valid host name.`);\n return ok(port === undefined ? { host, kind } : { host, kind, port });\n}\n","import type { Layer } from \"./layer.ts\";\nimport type { Target } from \"./target.ts\";\n\n/**\n * The time of a fact, in milliseconds on the monotonic clock of the runner.\n *\n * The field `origin` of the runner gives the wall-clock time of one monotonic time. Thus each fact time has a wall-clock time.\n */\nexport interface FactTime {\n readonly start: number;\n readonly end: number;\n}\n\n/** The source of a fact: the probe and its version, and the outside tool when the probe starts one. */\nexport interface FactSource {\n readonly probe: string;\n readonly version: string;\n readonly tool?: string | undefined;\n}\n\n/**\n * A typed fact of one layer of the network stack.\n *\n * A fact has a layer, a time, a source and a value. Its kind names the type of its value, for example `tcp.connect`.\n * The subject is the target of the fact. A fact is plain data, so a record of facts is JSON.\n */\nexport interface Fact<V = unknown> {\n /** The id is unique in the facts of one runner, for example `f12`. */\n readonly id: string;\n readonly kind: string;\n readonly layer: Layer;\n readonly subject: Target;\n readonly time: FactTime;\n readonly source: FactSource;\n readonly value: V;\n}\n\n/**\n * The definition of a kind of fact: its name, its layer and the check of its value.\n *\n * The check is the contract of the value. The runner refuses a value that does not pass it, thus each fact of a kind has a\n * value of the type `V`.\n */\nexport interface FactKind<V> {\n readonly kind: string;\n readonly layer: Layer;\n readonly summary: string;\n readonly check: (value: unknown) => value is V;\n}\n\n/** The value type of a fact kind. */\nexport type ValueOf<K> = K extends FactKind<infer V> ? V : never;\n\nconst KIND_NAME = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;\n\n/** This function makes a fact kind. The name has lower-case words with dots, for example `tls.handshake`. */\nexport function defineFactKind<V>(spec: FactKind<V>): FactKind<V> {\n if (!KIND_NAME.test(spec.kind)) throw new RangeError(`\"${spec.kind}\" is not a valid name of a fact kind`);\n return Object.freeze({ ...spec });\n}\n\n/** This function tells if a fact is of a kind, and if its value passes the check of the kind. */\nexport function isFactOf<V>(kind: FactKind<V>, fact: Fact): fact is Fact<V> {\n return fact.kind === kind.kind && fact.layer === kind.layer && kind.check(fact.value);\n}\n\n/** This function gives the facts of one kind, in their order. */\nexport function factsOf<V>(kind: FactKind<V>, facts: readonly Fact[]): Fact<V>[] {\n return facts.filter((fact): fact is Fact<V> => isFactOf(kind, fact));\n}\n\n/**\n * An observation is a fact that a probe parsed, before the runner gives it an id, a layer and a source.\n *\n * The time is optional. Without a time, the fact gets the time of the run of the probe.\n */\nexport interface Observation<V = unknown> {\n readonly kind: FactKind<V>;\n readonly subject: Target;\n readonly value: V;\n readonly time?: FactTime | undefined;\n}\n\n/** This function makes an observation with a type check of its value against its kind. */\nexport function observe<V>(kind: FactKind<V>, subject: Target, value: V, time?: FactTime): Observation<V> {\n return time === undefined ? { kind, subject, value } : { kind, subject, value, time };\n}\n","import type { Clock } from \"@mark1russell7/algebra/clock\";\nimport type { FactKind, Observation } from \"./fact.ts\";\nimport type { Layer } from \"./layer.ts\";\nimport type { Target } from \"./target.ts\";\n\n/**\n * The mode of a probe.\n *\n * - `active`: the probe sends packets to its peers, for example a TCP connect.\n * - `passive`: the probe reads only the state of this computer, for example its routes. It has no peers.\n */\nexport type ProbeMode = \"active\" | \"passive\";\n\n/** The context of a run: the clock of the runner, and the signal that stops the run. */\nexport interface ProbeContext {\n readonly clock: Clock;\n readonly signal: AbortSignal;\n}\n\n/** The availability of a probe on this computer. An unavailable probe tells why, for example a tool that is not installed. */\nexport type Availability =\n | { readonly available: true; readonly version?: string | undefined }\n | { readonly available: false; readonly reason: string };\n\n/**\n * The contract of a probe. A probe is an adapter: it tells if it is available, it does its work, and it parses its raw output into\n * observations.\n *\n * - `peers` gives each host that `run` sends packets to. A passive probe gives no peers. The runner examines each peer\n * against the scope before `run` starts.\n * - `run` does the effects and gives the raw output. A network failure is an observation, not an error. For example, a\n * TCP connect that times out gives the outcome `timeout`. `run` throws only for a fault of the probe.\n * - `parse` is a pure function of the raw output and the input. Thus a record of a raw output gives the same facts again,\n * also after a correction of the parser.\n *\n * The input and the raw output are plain JSON data, so a record can keep them.\n */\nexport interface Probe<I, R> {\n /** The id has lower-case words with dots, for example `tcp.connect`. */\n readonly id: string;\n /** The version of the probe changes when its facts change. */\n readonly version: string;\n readonly summary: string;\n readonly layer: Layer;\n readonly mode: ProbeMode;\n /** The kinds of the facts that the probe gives. The runner refuses an observation of a different kind. */\n readonly kinds: readonly FactKind<unknown>[];\n /** The outside tool that the probe starts, for example `ping`. A native probe has no tool. */\n readonly tool?: string | undefined;\n peers(input: I): readonly Target[];\n available(context: ProbeContext): Promise<Availability>;\n run(input: I, context: ProbeContext): Promise<R>;\n parse(raw: R, input: I): readonly Observation[];\n}\n\n/** This function makes a probe, and examines its fields. */\nexport function defineProbe<I, R>(probe: Probe<I, R>): Probe<I, R> {\n if (!/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/.test(probe.id)) throw new RangeError(`\"${probe.id}\" is not a valid probe id`);\n if (probe.kinds.length === 0) throw new RangeError(`The probe \"${probe.id}\" gives no kind of fact`);\n return probe;\n}\n","import { defineFactKind, type FactKind } from \"./fact.ts\";\n\n// The fact kinds of the native probes. This module imports no `node:` module, so a browser can read a record of facts.\n\nfunction isRecord(value: unknown): value is Readonly<Record<string, unknown>> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isText(value: unknown): value is string {\n return typeof value === \"string\" && value.length > 0;\n}\n\nfunction isDuration(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value) && value >= 0;\n}\n\nfunction isPort(value: unknown): value is number {\n return typeof value === \"number\" && Number.isInteger(value) && value >= 1 && value <= 65535;\n}\n\nfunction isCount(value: unknown): value is number {\n return typeof value === \"number\" && Number.isInteger(value) && value >= 0;\n}\n\nfunction isOneOf<T extends string>(values: readonly T[], value: unknown): value is T {\n return typeof value === \"string\" && (values as readonly string[]).includes(value);\n}\n\nfunction optional(value: unknown, check: (value: unknown) => boolean): boolean {\n return value === undefined || check(value);\n}\n\nfunction isTextList(value: unknown): value is readonly string[] {\n return Array.isArray(value) && value.every(isText);\n}\n\n/** The outcomes of a DNS query. `nodata` tells that the name exists, but it has no record of the type. */\nexport const DNS_OUTCOMES = [\"ok\", \"nodata\", \"nxdomain\", \"servfail\", \"refused\", \"timeout\", \"unreachable\", \"error\"] as const;\nexport type DnsOutcome = (typeof DNS_OUTCOMES)[number];\n\n/** The record types of a DNS query for addresses. */\nexport const RECORD_TYPES = [\"A\", \"AAAA\"] as const;\nexport type RecordType = (typeof RECORD_TYPES)[number];\n\n/** The answer of a DNS resolver to one query. */\nexport interface DnsAnswer {\n readonly name: string;\n readonly record: RecordType;\n /** The resolver, as `address:port`. */\n readonly resolver: string;\n readonly outcome: DnsOutcome;\n readonly addresses: readonly string[];\n /** The lowest TTL of the records, in seconds. */\n readonly ttl?: number | undefined;\n /** The time from the query to the answer or to the failure, in milliseconds. */\n readonly ms: number;\n /** The error code of the platform for a failure, for example `ENOTFOUND`. */\n readonly code?: string | undefined;\n}\n\n/** The fact kind `dns.answer`. */\nexport const DNS_ANSWER: FactKind<DnsAnswer> = defineFactKind<DnsAnswer>({\n kind: \"dns.answer\",\n layer: \"dns\",\n summary: \"The answer of a DNS resolver to one query for the addresses of a name.\",\n check: (value): value is DnsAnswer =>\n isRecord(value) &&\n isText(value[\"name\"]) &&\n isOneOf(RECORD_TYPES, value[\"record\"]) &&\n isText(value[\"resolver\"]) &&\n isOneOf(DNS_OUTCOMES, value[\"outcome\"]) &&\n isTextList(value[\"addresses\"]) &&\n (value[\"outcome\"] !== \"ok\" || value[\"addresses\"].length > 0) &&\n optional(value[\"ttl\"], isDuration) &&\n isDuration(value[\"ms\"]) &&\n optional(value[\"code\"], isText),\n});\n\n/** The outcomes of a TCP connect. */\nexport const CONNECT_OUTCOMES = [\"connected\", \"refused\", \"timeout\", \"unreachable\", \"reset\", \"error\"] as const;\nexport type ConnectOutcome = (typeof CONNECT_OUTCOMES)[number];\n\n/** The outcome of one TCP connect. */\nexport interface TcpConnect {\n readonly address: string;\n readonly port: number;\n readonly family: 4 | 6;\n readonly outcome: ConnectOutcome;\n /** The time from the start of the connect to the connection or to the failure, in milliseconds. */\n readonly ms: number;\n readonly code?: string | undefined;\n}\n\n/** The fact kind `tcp.connect`. */\nexport const TCP_CONNECT: FactKind<TcpConnect> = defineFactKind<TcpConnect>({\n kind: \"tcp.connect\",\n layer: \"tcp\",\n summary: \"The outcome and the time of one TCP connect to an address and a port.\",\n check: (value): value is TcpConnect =>\n isRecord(value) &&\n isText(value[\"address\"]) &&\n isPort(value[\"port\"]) &&\n (value[\"family\"] === 4 || value[\"family\"] === 6) &&\n isOneOf(CONNECT_OUTCOMES, value[\"outcome\"]) &&\n isDuration(value[\"ms\"]) &&\n optional(value[\"code\"], isText),\n});\n\n/**\n * The outcomes of a TLS handshake.\n *\n * - `cert-error`: the handshake ended, but the certificate is not valid for the name.\n * - `alert`: the client and the server stopped the handshake, for example because they have no version in common.\n * - `not-tls`: the server does not speak TLS on the port.\n * - `closed`: the server closed the connection before the end of the handshake.\n */\nexport const HANDSHAKE_OUTCOMES = [\"ok\", \"cert-error\", \"alert\", \"not-tls\", \"timeout\", \"reset\", \"closed\", \"error\"] as const;\nexport type HandshakeOutcome = (typeof HANDSHAKE_OUTCOMES)[number];\n\n/** The roots of trust that a certificate chain ends at: a public root, a root of the computer or the user, or none. */\nexport const TRUSTS = [\"public\", \"private\", \"none\"] as const;\nexport type Trust = (typeof TRUSTS)[number];\n\n/** The problems of a certificate. */\nexport const CERT_PROBLEMS = [\"expired\", \"not-yet-valid\", \"name\", \"untrusted\", \"revoked\", \"other\"] as const;\nexport type CertProblem = (typeof CERT_PROBLEMS)[number];\n\n/** The certificate of a server. */\nexport interface TlsCertificate {\n readonly subject: string;\n readonly issuer: string;\n /** The DNS names and the IP addresses of the certificate. */\n readonly names: readonly string[];\n /** The start of the validity, as an ISO 8601 time. */\n readonly validFrom: string;\n /** The end of the validity, as an ISO 8601 time. */\n readonly validTo: string;\n readonly fingerprint256: string;\n readonly selfSigned: boolean;\n /** The number of certificates from the certificate of the server to the root, with both. */\n readonly chain: number;\n /** The root that the chain ends at. A failed check of the certificate does not change it. */\n readonly trust: Trust;\n readonly problem?: CertProblem | undefined;\n}\n\n/** The outcome of one TLS handshake. */\nexport interface TlsHandshake {\n readonly address: string;\n readonly port: number;\n readonly servername?: string | undefined;\n readonly outcome: HandshakeOutcome;\n /** The time from the TCP connection to the end of the handshake or to the failure, in milliseconds. */\n readonly ms: number;\n /** The TLS version, for example `TLSv1.3`. */\n readonly protocol?: string | undefined;\n readonly cipher?: string | undefined;\n /** The application protocol that the server chose with ALPN, for example `http/1.1`. */\n readonly alpn?: string | undefined;\n readonly code?: string | undefined;\n readonly certificate?: TlsCertificate | undefined;\n}\n\nfunction isCertificate(value: unknown): value is TlsCertificate {\n return (\n isRecord(value) &&\n typeof value[\"subject\"] === \"string\" &&\n typeof value[\"issuer\"] === \"string\" &&\n isTextList(value[\"names\"]) &&\n isText(value[\"validFrom\"]) &&\n isText(value[\"validTo\"]) &&\n isText(value[\"fingerprint256\"]) &&\n typeof value[\"selfSigned\"] === \"boolean\" &&\n typeof value[\"chain\"] === \"number\" &&\n Number.isInteger(value[\"chain\"]) &&\n value[\"chain\"] >= 1 &&\n isOneOf(TRUSTS, value[\"trust\"]) &&\n optional(value[\"problem\"], (problem) => isOneOf(CERT_PROBLEMS, problem))\n );\n}\n\n/** The fact kind `tls.handshake`. */\nexport const TLS_HANDSHAKE: FactKind<TlsHandshake> = defineFactKind<TlsHandshake>({\n kind: \"tls.handshake\",\n layer: \"tls\",\n summary: \"The outcome and the time of one TLS handshake, and the certificate of the server.\",\n check: (value): value is TlsHandshake =>\n isRecord(value) &&\n isText(value[\"address\"]) &&\n isPort(value[\"port\"]) &&\n optional(value[\"servername\"], isText) &&\n isOneOf(HANDSHAKE_OUTCOMES, value[\"outcome\"]) &&\n isDuration(value[\"ms\"]) &&\n optional(value[\"protocol\"], isText) &&\n optional(value[\"cipher\"], isText) &&\n optional(value[\"alpn\"], isText) &&\n optional(value[\"code\"], isText) &&\n optional(value[\"certificate\"], isCertificate),\n});\n\n/**\n * The outcomes of an HTTP request.\n *\n * - `response`: the server sent a response. Its status can be an error.\n * - `protocol`: the server sent bytes that are not HTTP.\n * - `closed`: the server closed the connection before the response.\n */\nexport const HTTP_OUTCOMES = [\"response\", \"protocol\", \"timeout\", \"reset\", \"closed\", \"error\"] as const;\nexport type HttpOutcome = (typeof HTTP_OUTCOMES)[number];\n\n/** The outcome of one HTTP request. */\nexport interface HttpResponse {\n readonly address: string;\n readonly port: number;\n readonly scheme: \"http\" | \"https\";\n readonly method: \"GET\" | \"HEAD\";\n /** The value of the `Host` field of the request. */\n readonly host: string;\n readonly path: string;\n readonly outcome: HttpOutcome;\n readonly status?: number | undefined;\n /** The HTTP version of the response, for example `1.1`. */\n readonly version?: string | undefined;\n /** Some fields of the response head, with lower-case names: `server`, `via`, `location` and others. */\n readonly fields: Readonly<Record<string, string>>;\n /** The bytes of the body that the probe read. */\n readonly bytes: number;\n /** The probe stopped the read of the body at its limit. */\n readonly truncated: boolean;\n /** The time from the request to the head of the response, in milliseconds. */\n readonly ttfbMs?: number | undefined;\n /** The time from the request to the end of the response or to the failure, in milliseconds. */\n readonly ms: number;\n readonly code?: string | undefined;\n}\n\n/** The fact kind `http.response`. */\nexport const HTTP_RESPONSE: FactKind<HttpResponse> = defineFactKind<HttpResponse>({\n kind: \"http.response\",\n layer: \"http\",\n summary: \"The outcome and the times of one HTTP request: the status, some fields of the head and the time to the first byte.\",\n check: (value): value is HttpResponse =>\n isRecord(value) &&\n isText(value[\"address\"]) &&\n isPort(value[\"port\"]) &&\n (value[\"scheme\"] === \"http\" || value[\"scheme\"] === \"https\") &&\n (value[\"method\"] === \"GET\" || value[\"method\"] === \"HEAD\") &&\n isText(value[\"host\"]) &&\n isText(value[\"path\"]) &&\n isOneOf(HTTP_OUTCOMES, value[\"outcome\"]) &&\n (value[\"outcome\"] !== \"response\" || (typeof value[\"status\"] === \"number\" && Number.isInteger(value[\"status\"]))) &&\n optional(value[\"status\"], (status) => typeof status === \"number\" && status >= 100 && status <= 999) &&\n optional(value[\"version\"], isText) &&\n isRecord(value[\"fields\"]) &&\n Object.values(value[\"fields\"]).every((field) => typeof field === \"string\") &&\n typeof value[\"bytes\"] === \"number\" &&\n Number.isInteger(value[\"bytes\"]) &&\n value[\"bytes\"] >= 0 &&\n typeof value[\"truncated\"] === \"boolean\" &&\n optional(value[\"ttfbMs\"], isDuration) &&\n isDuration(value[\"ms\"]) &&\n optional(value[\"code\"], isText),\n});\n\n/** The fact kinds of the native probes. */\nexport const NATIVE_KINDS: readonly FactKind<unknown>[] = [DNS_ANSWER, TCP_CONNECT, TLS_HANDSHAKE, HTTP_RESPONSE];\n\n/**\n * The outcomes of the ICMP echo requests to one address.\n *\n * - `reply`: one or more echo replies came back.\n * - `none`: no reply came back in the time limit.\n * - `unreachable`: no echo reply came back, and a router or this computer told that the address is unreachable.\n * - `error`: the tool did not start, or netdiag cannot read its output.\n */\nexport const ECHO_OUTCOMES = [\"reply\", \"none\", \"unreachable\", \"error\"] as const;\nexport type EchoOutcome = (typeof ECHO_OUTCOMES)[number];\n\n/** The result of the ICMP echo requests to one address. */\nexport interface IcmpEcho {\n readonly address: string;\n readonly outcome: EchoOutcome;\n /** The number of echo requests that the tool sent. */\n readonly sent: number;\n /** The number of echo replies. A message of an unreachable address is not a reply. */\n readonly received: number;\n /** The round-trip time of each reply, in milliseconds. */\n readonly rttMs: readonly number[];\n /** The TTL (IPv4) or the hop limit (IPv6) of the first reply. */\n readonly ttl?: number | undefined;\n /** The router that told that the address is unreachable. */\n readonly from?: string | undefined;\n /** The text of the tool for the outcomes `unreachable` and `error`. */\n readonly detail?: string | undefined;\n}\n\n/** The fact kind `icmp.echo`. */\nexport const ICMP_ECHO: FactKind<IcmpEcho> = defineFactKind<IcmpEcho>({\n kind: \"icmp.echo\",\n layer: \"ip\",\n summary: \"The replies to the ICMP echo requests to one address, their round-trip times and the messages of an unreachable address.\",\n check: (value): value is IcmpEcho =>\n isRecord(value) &&\n isText(value[\"address\"]) &&\n isOneOf(ECHO_OUTCOMES, value[\"outcome\"]) &&\n isCount(value[\"sent\"]) &&\n isCount(value[\"received\"]) &&\n value[\"received\"] <= value[\"sent\"] &&\n Array.isArray(value[\"rttMs\"]) &&\n value[\"rttMs\"].every(isDuration) &&\n value[\"rttMs\"].length <= value[\"received\"] &&\n (value[\"outcome\"] === \"reply\") === value[\"received\"] > 0 &&\n optional(value[\"ttl\"], isCount) &&\n optional(value[\"from\"], isText) &&\n optional(value[\"detail\"], isText),\n});\n\n/**\n * The outcomes of a trace of the path to an address.\n *\n * - `reached`: the address answered at the last hop.\n * - `stopped`: the last hop is not the address, and no router told that the address is unreachable.\n * - `unreachable`: a router or this computer told that the address is unreachable.\n * - `error`: the tool failed, or netdiag cannot read its output.\n */\nexport const TRACE_OUTCOMES = [\"reached\", \"stopped\", \"unreachable\", \"error\"] as const;\nexport type TraceOutcome = (typeof TRACE_OUTCOMES)[number];\n\n/** One hop of a trace: the answers to the probes with one TTL. */\nexport interface TraceHop {\n /** The TTL (IPv4) or the hop limit (IPv6) of the probes, from 1. */\n readonly ttl: number;\n /** The addresses that answered, in the order of their first answer. */\n readonly addresses: readonly string[];\n /** The number of probes that the tool sent with this TTL. */\n readonly sent: number;\n /** The number of answers. */\n readonly received: number;\n /** The round-trip time of each answer that has a time, in milliseconds. */\n readonly rttMs: readonly number[];\n /** The message of an unreachable address at this hop, for example `host unreachable (!H)`. */\n readonly unreachable?: string | undefined;\n}\n\n/** The trace of the path to one address: the hops of the tool `traceroute`, or `tracert` on Windows. */\nexport interface PathTrace {\n readonly address: string;\n readonly outcome: TraceOutcome;\n /** The largest TTL of the probes. */\n readonly maxHops: number;\n /** The hops in the order of their TTL. An `error` keeps the hops before the failure. */\n readonly hops: readonly TraceHop[];\n /** The text of the tool for the outcomes `unreachable` and `error`. */\n readonly detail?: string | undefined;\n}\n\nfunction isTraceHop(value: unknown): value is TraceHop {\n return (\n isRecord(value) &&\n isCount(value[\"ttl\"]) &&\n value[\"ttl\"] >= 1 &&\n isTextList(value[\"addresses\"]) &&\n isCount(value[\"sent\"]) &&\n isCount(value[\"received\"]) &&\n value[\"received\"] <= value[\"sent\"] &&\n (value[\"received\"] > 0) === (value[\"addresses\"].length > 0) &&\n Array.isArray(value[\"rttMs\"]) &&\n value[\"rttMs\"].every(isDuration) &&\n value[\"rttMs\"].length <= value[\"received\"] &&\n optional(value[\"unreachable\"], isText)\n );\n}\n\n/** This function tells if the TTL of each hop is larger than the TTL of the hop before it, and not larger than `maxHops`. */\nfunction inOrder(hops: readonly { readonly ttl: number }[], maxHops: number): boolean {\n return hops.every((hop, index) => hop.ttl <= maxHops && (index === 0 || hop.ttl > (hops[index - 1]?.ttl ?? 0)));\n}\n\n/** The fact kind `path.trace`. */\nexport const PATH_TRACE: FactKind<PathTrace> = defineFactKind<PathTrace>({\n kind: \"path.trace\",\n layer: \"path\",\n summary: \"The hops of the path to one address: the routers that answered each TTL, their round-trip times and the messages of an unreachable address.\",\n check: (value): value is PathTrace =>\n isRecord(value) &&\n isText(value[\"address\"]) &&\n isOneOf(TRACE_OUTCOMES, value[\"outcome\"]) &&\n isCount(value[\"maxHops\"]) &&\n Array.isArray(value[\"hops\"]) &&\n value[\"hops\"].every(isTraceHop) &&\n inOrder(value[\"hops\"], value[\"maxHops\"]) &&\n (value[\"outcome\"] !== \"reached\" || value[\"hops\"].length > 0) &&\n optional(value[\"detail\"], isText),\n});\n\n/** One hop of a report of `mtr`: the counts and the times of many cycles of probes with one TTL. */\nexport interface ReportHop {\n readonly ttl: number;\n /** The address that answered. A hop with no answer has no address. */\n readonly address?: string | undefined;\n /** The number of probes that the tool sent with this TTL. */\n readonly sent: number;\n /** The number of answers. The adapter calculates it from the loss and the number of probes of the report. */\n readonly received: number;\n /** The best round-trip time, in milliseconds. Only a hop with answers has the times. */\n readonly bestMs?: number | undefined;\n /** The mean round-trip time, in milliseconds. */\n readonly meanMs?: number | undefined;\n /** The worst round-trip time, in milliseconds. */\n readonly worstMs?: number | undefined;\n}\n\n/** The report of the tool `mtr` for the path to one address. */\nexport interface PathReport {\n readonly address: string;\n /** The outcome. A report of `mtr` has no messages of an unreachable address, so it does not have the outcome `unreachable`. */\n readonly outcome: TraceOutcome;\n /** The number of cycles of probes. */\n readonly cycles: number;\n readonly hops: readonly ReportHop[];\n /** The text of the tool for the outcome `error`. */\n readonly detail?: string | undefined;\n}\n\nfunction isReportHop(value: unknown): value is ReportHop {\n return (\n isRecord(value) &&\n isCount(value[\"ttl\"]) &&\n value[\"ttl\"] >= 1 &&\n optional(value[\"address\"], isText) &&\n isCount(value[\"sent\"]) &&\n isCount(value[\"received\"]) &&\n value[\"received\"] <= value[\"sent\"] &&\n (value[\"received\"] === 0 || value[\"address\"] !== undefined) &&\n optional(value[\"bestMs\"], isDuration) &&\n optional(value[\"meanMs\"], isDuration) &&\n optional(value[\"worstMs\"], isDuration)\n );\n}\n\n/** The fact kind `path.report`. */\nexport const PATH_REPORT: FactKind<PathReport> = defineFactKind<PathReport>({\n kind: \"path.report\",\n layer: \"path\",\n summary: \"The hops of the path to one address over many cycles of probes: the address, the loss and the round-trip times of each hop.\",\n check: (value): value is PathReport =>\n isRecord(value) &&\n isText(value[\"address\"]) &&\n isOneOf(TRACE_OUTCOMES, value[\"outcome\"]) &&\n value[\"outcome\"] !== \"unreachable\" &&\n isCount(value[\"cycles\"]) &&\n Array.isArray(value[\"hops\"]) &&\n value[\"hops\"].every(isReportHop) &&\n inOrder(value[\"hops\"], Number.MAX_SAFE_INTEGER) &&\n (value[\"outcome\"] !== \"reached\" || value[\"hops\"].length > 0) &&\n optional(value[\"detail\"], isText),\n});\n\n/** The fact kinds of the adapters of outside tools (`@mark1russell7/netdiag/tools`). */\nexport const TOOL_KINDS: readonly FactKind<unknown>[] = [ICMP_ECHO, PATH_TRACE, PATH_REPORT];\n"],"mappings":"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"}