@takodotid/circuit 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/chunks/{index-jtzfw679.js → index-n4zpd25n.js} +92 -92
- package/dist/chunks/{index-xm9nma2t.js → index-v8zgr7wm.js} +14 -14
- package/dist/chunks/{new-7n8qvtjd.js → new-ny89s7y7.js} +4 -6
- package/dist/chunks/{server-5shmsmse.js → server-ckdy25w2.js} +16 -11
- package/dist/cli/index.js +406 -411
- package/dist/index.js +20 -21
- package/dist/presets/index.js +20 -21
- package/package.json +7 -7
- package/src/core/files.ts +5 -3
- package/src/studio/public/app.js +37 -9
- package/src/studio/public/dom.js +20 -0
- package/src/studio/public/export.js +6 -0
- package/src/studio/public/favicon.svg +10 -4
- package/src/studio/public/graph.js +396 -26
- package/src/studio/public/index.html +26 -12
- package/src/studio/public/special-gothic-latin-ext.woff2 +0 -0
- package/src/studio/public/special-gothic-latin.woff2 +0 -0
- package/src/studio/public/special-gothic-license.txt +93 -0
- package/src/studio/public/style.css +1125 -356
- package/src/studio/server.ts +10 -1
- package/dist/chunks/index-fq57hya4.js +0 -20
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Documentation: [circuit.tako.id](https://circuit.tako.id)
|
|
|
22
22
|
|
|
23
23
|
<picture>
|
|
24
24
|
<source media="(prefers-color-scheme: dark)" srcset="docs/public/screenshots/studio-dark.png">
|
|
25
|
-
<img src="docs/public/screenshots/studio-light.png" alt="Studio's graph
|
|
25
|
+
<img src="docs/public/screenshots/studio-light.png" alt="Studio's VLAN graph with devices that differ from their snapshots marked by orange dashed borders, beside the layer details and legend.">
|
|
26
26
|
</picture>
|
|
27
27
|
|
|
28
28
|
## Start
|
|
@@ -1,24 +1,62 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
familyOf,
|
|
4
|
-
formatAddress,
|
|
5
|
-
host,
|
|
6
|
-
isAddress,
|
|
7
|
-
isCidr,
|
|
8
|
-
network,
|
|
2
|
+
familyOf2,
|
|
9
3
|
parse,
|
|
10
|
-
|
|
4
|
+
split,
|
|
5
|
+
isAddress2,
|
|
6
|
+
isCidr2,
|
|
7
|
+
network,
|
|
8
|
+
contains2,
|
|
9
|
+
formatAddress,
|
|
11
10
|
rangeOf,
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
prefixesBetween,
|
|
12
|
+
host2
|
|
13
|
+
} from "./index-v8zgr7wm.js";
|
|
14
|
+
|
|
15
|
+
// src/adapters/devices/raisecom-ros/models.ts
|
|
16
|
+
var models = {
|
|
17
|
+
"RAX721-C-6C48": {
|
|
18
|
+
ports: { "100g": 6, "25g": 24, "10g": 24 },
|
|
19
|
+
spelling: { "100g": "hundredgige 1/1/{n}", "25g": "twenty-fivegige 1/2/{n}", "10g": "tengigabitethernet 1/3/{n}" }
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/adapters/devices/routeros/models.ts
|
|
24
|
+
var models2 = {
|
|
25
|
+
"CCR2216-1G-12XS-2XQ": {
|
|
26
|
+
ports: { "100g": 2, "25g": 12, "1g": 1 },
|
|
27
|
+
spelling: { "100g": "qsfp28-{n}-1", "25g": "sfp28-{n}", "1g": "ether{n}" }
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/adapters/devices/vrp/models.ts
|
|
32
|
+
var models3 = {
|
|
33
|
+
"CE6855-48S6Q-HI": { ports: { "10g": 48, "40g": 6 }, spelling: { "10g": "10GE1/0/{n}", "40g": "40GE1/0/{n}" } }
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// src/adapters/devices/catalog.ts
|
|
37
|
+
var catalog = { routeros: models2, vrp: models3, "raisecom-ros": models };
|
|
38
|
+
function portsOf(platform, model) {
|
|
39
|
+
const spec = catalog[platform]?.[model];
|
|
40
|
+
if (!spec)
|
|
41
|
+
throw new Error(`${platform} has no model ${model}`);
|
|
42
|
+
return Object.entries(spec.ports).flatMap(([speed, count]) => Array.from({ length: count }, (_, i) => `${speed}-${i + 1}`));
|
|
43
|
+
}
|
|
44
|
+
function spell(platform, model, port) {
|
|
45
|
+
const spec = catalog[platform]?.[model];
|
|
46
|
+
const parts = /^(\d+[gm])-(\d+)$/.exec(port);
|
|
47
|
+
const template = parts && spec?.spelling[parts[1]];
|
|
48
|
+
if (!template)
|
|
49
|
+
throw new Error(`${model} has no port ${port}`);
|
|
50
|
+
return template.replace("{n}", parts[2]);
|
|
51
|
+
}
|
|
14
52
|
|
|
15
53
|
// src/core/graph.ts
|
|
16
54
|
var LAYERS = ["l1", "l2", "l3", "bgp", "ospf", "tunnels"];
|
|
17
55
|
var deviceId = (name) => `device:${name}`;
|
|
18
56
|
function keyOf(address) {
|
|
19
|
-
if (!address || !
|
|
57
|
+
if (!address || !isAddress2(address))
|
|
20
58
|
return;
|
|
21
|
-
return `${
|
|
59
|
+
return `${familyOf2(address)}:${parse(address)}`;
|
|
22
60
|
}
|
|
23
61
|
var isHostRoute = (cidr) => {
|
|
24
62
|
const { len, family } = split(cidr);
|
|
@@ -51,7 +89,7 @@ function routedOf(device) {
|
|
|
51
89
|
function holdersOf(devices) {
|
|
52
90
|
return devices.flatMap((device) => routedOf(device).flatMap(([name, settings]) => (settings.addresses ?? []).flatMap((entry) => {
|
|
53
91
|
const address = typeof entry === "string" ? entry : entry.address;
|
|
54
|
-
if (!
|
|
92
|
+
if (!isCidr2(address))
|
|
55
93
|
return [];
|
|
56
94
|
const peer = typeof entry === "string" ? undefined : entry.peer;
|
|
57
95
|
return [{ device, name, address, peer, vrrp: settings.vrrp ?? [] }];
|
|
@@ -156,7 +194,7 @@ function segments(holders, owners, graph) {
|
|
|
156
194
|
layer: "l3",
|
|
157
195
|
from: deviceId(holder.device.name),
|
|
158
196
|
to: deviceId(far.device.name),
|
|
159
|
-
label: `${
|
|
197
|
+
label: `${host2(holder.address)} to ${host2(far.address)}`,
|
|
160
198
|
detail: [`${holder.device.name} ${holder.name} ${holder.address}`, `${far.device.name} ${far.name} ${far.address}`]
|
|
161
199
|
});
|
|
162
200
|
continue;
|
|
@@ -188,7 +226,7 @@ function segments(holders, owners, graph) {
|
|
|
188
226
|
layer: "l3",
|
|
189
227
|
from: deviceId(member.device.name),
|
|
190
228
|
to: id,
|
|
191
|
-
label: `${member.name} ${
|
|
229
|
+
label: `${member.name} ${host2(member.address)}`,
|
|
192
230
|
detail: [`${member.device.name} ${member.name} ${member.address}`]
|
|
193
231
|
});
|
|
194
232
|
}
|
|
@@ -229,7 +267,7 @@ function sessions(devices, owners, graph) {
|
|
|
229
267
|
continue;
|
|
230
268
|
const far = ownerOf(session);
|
|
231
269
|
if (far && far !== session.device) {
|
|
232
|
-
const back = all.find((other) => other.device === far && !paired.has(other) && ownerOf(other) === session.device &&
|
|
270
|
+
const back = all.find((other) => other.device === far && !paired.has(other) && ownerOf(other) === session.device && familyOf2(other.address) === familyOf2(session.address));
|
|
233
271
|
if (back)
|
|
234
272
|
paired.add(back);
|
|
235
273
|
graph.edge({
|
|
@@ -265,7 +303,7 @@ function adjacencies(devices, graph) {
|
|
|
265
303
|
const cost = settings?.cost ? `, cost ${settings.cost}` : "";
|
|
266
304
|
for (const entry of routed?.addresses ?? []) {
|
|
267
305
|
const address = typeof entry === "string" ? entry : entry.address;
|
|
268
|
-
if (!
|
|
306
|
+
if (!isCidr2(address) || isHostRoute(address))
|
|
269
307
|
continue;
|
|
270
308
|
const key = `${area} ${network(address)}`;
|
|
271
309
|
const member = { device, name, line: `${device.name} ${name} ${address}${cost}` };
|
|
@@ -345,8 +383,8 @@ function wireguard(devices, owners, graph) {
|
|
|
345
383
|
const farEndOf = (device, peer) => interfaces.find(({ device: other, iface }) => {
|
|
346
384
|
if (other === device)
|
|
347
385
|
return false;
|
|
348
|
-
const tunnelAddresses = (iface.addresses ?? []).map((entry) =>
|
|
349
|
-
const sendsFrom = tunnelAddresses.some((address) =>
|
|
386
|
+
const tunnelAddresses = (iface.addresses ?? []).map((entry) => host2(typeof entry === "string" ? entry : entry.address));
|
|
387
|
+
const sendsFrom = tunnelAddresses.some((address) => isAddress2(address) && peer.allowed_addresses.some((allowed) => isCidr2(allowed) && contains2(allowed, address)));
|
|
350
388
|
const endpoint = peer.endpoint ? endpointHost(peer.endpoint) : undefined;
|
|
351
389
|
const isEndpoint = endpoint !== undefined && owners.get(keyOf(endpoint) ?? "")?.device === other;
|
|
352
390
|
return sendsFrom || isEndpoint;
|
|
@@ -375,11 +413,11 @@ function wireguard(devices, owners, graph) {
|
|
|
375
413
|
continue;
|
|
376
414
|
}
|
|
377
415
|
const endpoint = endpointHost(peer.endpoint);
|
|
378
|
-
const
|
|
416
|
+
const outside = graph.node(`address:${endpoint}`, "outside", peer.name, [`WireGuard endpoint ${peer.endpoint}`]);
|
|
379
417
|
graph.edge({
|
|
380
418
|
layer: "tunnels",
|
|
381
419
|
from: deviceId(device.name),
|
|
382
|
-
to:
|
|
420
|
+
to: outside,
|
|
383
421
|
label: `WireGuard ${name}`,
|
|
384
422
|
detail: [`${device.name} ${name} peer ${peer.name}, allowed ${peer.allowed_addresses.join(", ")}`]
|
|
385
423
|
});
|
|
@@ -403,7 +441,7 @@ function graphOf(devices) {
|
|
|
403
441
|
const holders = holdersOf(devices);
|
|
404
442
|
const owners = new Map;
|
|
405
443
|
for (const holder of holders) {
|
|
406
|
-
const key = keyOf(
|
|
444
|
+
const key = keyOf(host2(holder.address));
|
|
407
445
|
if (!owners.has(key))
|
|
408
446
|
owners.set(key, holder);
|
|
409
447
|
}
|
|
@@ -482,24 +520,24 @@ function comparePrefixes(a, b) {
|
|
|
482
520
|
}
|
|
483
521
|
var byPrefix = (a, b) => comparePrefixes(a.prefix, b.prefix);
|
|
484
522
|
function addressesOf(devices, within) {
|
|
485
|
-
const
|
|
523
|
+
const segments = new Map;
|
|
486
524
|
const blocks = [];
|
|
487
|
-
const segmentOf = (address) => [...
|
|
525
|
+
const segmentOf = (address) => [...segments.keys()].find((prefix) => contains2(prefix, address));
|
|
488
526
|
for (const holder of holdersOf(devices)) {
|
|
489
527
|
const vrf = holder.device.ports?.[holder.name]?.vrf ?? holder.device.interfaces?.[holder.name]?.vrf;
|
|
490
528
|
const what = `${holder.device.name} ${holder.name}${vrf ? `, VRF ${vrf}` : ""}`;
|
|
491
529
|
if (isHostRoute2(holder.address) || holder.peer) {
|
|
492
|
-
blocks.push({ prefix: `${
|
|
530
|
+
blocks.push({ prefix: `${host2(holder.address)}/${bits(familyOf2(holder.address))}`, what });
|
|
493
531
|
continue;
|
|
494
532
|
}
|
|
495
533
|
const prefix = network(holder.address);
|
|
496
|
-
const segment =
|
|
497
|
-
const value = numberOf(
|
|
498
|
-
segment.uses.push({ address:
|
|
534
|
+
const segment = segments.get(prefix) ?? { uses: [], taken: [] };
|
|
535
|
+
const value = numberOf(host2(holder.address));
|
|
536
|
+
segment.uses.push({ address: host2(holder.address), what });
|
|
499
537
|
segment.taken.push({ first: value, last: value });
|
|
500
|
-
|
|
538
|
+
segments.set(prefix, segment);
|
|
501
539
|
for (const group of holder.vrrp) {
|
|
502
|
-
if (!
|
|
540
|
+
if (!isAddress2(group.address) || !contains2(prefix, group.address))
|
|
503
541
|
continue;
|
|
504
542
|
if (segment.uses.some((use) => use.address === group.address))
|
|
505
543
|
continue;
|
|
@@ -512,7 +550,7 @@ function addressesOf(devices, within) {
|
|
|
512
550
|
const prefix = segmentOf(first);
|
|
513
551
|
if (!prefix)
|
|
514
552
|
return false;
|
|
515
|
-
const segment =
|
|
553
|
+
const segment = segments.get(prefix);
|
|
516
554
|
if (segment.uses.some((use) => use.address === address))
|
|
517
555
|
return true;
|
|
518
556
|
segment.uses.push({ address, what });
|
|
@@ -522,19 +560,19 @@ function addressesOf(devices, within) {
|
|
|
522
560
|
for (const device of devices) {
|
|
523
561
|
for (const [name, server] of Object.entries(device.dhcp ?? {})) {
|
|
524
562
|
const [start, end] = server.pool;
|
|
525
|
-
if (
|
|
563
|
+
if (isAddress2(start) && isAddress2(end))
|
|
526
564
|
take(`${start}-${end}`, `DHCP pool ${name} on ${device.name}`, start, end);
|
|
527
565
|
for (const reservation of server.reservations ?? []) {
|
|
528
|
-
if (
|
|
566
|
+
if (isAddress2(reservation.address))
|
|
529
567
|
take(reservation.address, `reserved for ${reservation.description ?? reservation.mac} by ${device.name}`);
|
|
530
568
|
}
|
|
531
569
|
}
|
|
532
570
|
for (const route of device.routing?.static ?? []) {
|
|
533
|
-
if (route.via &&
|
|
571
|
+
if (route.via && isAddress2(route.via))
|
|
534
572
|
take(route.via, `next hop for ${route.prefix} on ${device.name}`);
|
|
535
573
|
}
|
|
536
574
|
for (const [name, neighbor] of Object.entries(device.routing?.bgp?.neighbors ?? {})) {
|
|
537
|
-
if (
|
|
575
|
+
if (isAddress2(neighbor.address))
|
|
538
576
|
take(neighbor.address, `BGP neighbor ${name} of ${device.name}`);
|
|
539
577
|
}
|
|
540
578
|
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
@@ -542,12 +580,12 @@ function addressesOf(devices, within) {
|
|
|
542
580
|
continue;
|
|
543
581
|
for (const peer of iface.peers ?? []) {
|
|
544
582
|
for (const allowed of peer.allowed_addresses) {
|
|
545
|
-
if (!
|
|
583
|
+
if (!isCidr2(allowed))
|
|
546
584
|
continue;
|
|
547
585
|
const what = `WireGuard peer ${peer.name} on ${device.name} ${name}`;
|
|
548
|
-
if (isHostRoute2(allowed) && take(
|
|
586
|
+
if (isHostRoute2(allowed) && take(host2(allowed), what))
|
|
549
587
|
continue;
|
|
550
|
-
if (!segmentOf(
|
|
588
|
+
if (!segmentOf(host2(allowed)))
|
|
551
589
|
blocks.push({ prefix: network(allowed), what });
|
|
552
590
|
}
|
|
553
591
|
}
|
|
@@ -556,17 +594,17 @@ function addressesOf(devices, within) {
|
|
|
556
594
|
const owners = new Map;
|
|
557
595
|
for (const device of devices) {
|
|
558
596
|
for (const prefix of device.routing?.bgp?.networks ?? []) {
|
|
559
|
-
if (!
|
|
597
|
+
if (!isCidr2(prefix))
|
|
560
598
|
continue;
|
|
561
599
|
owners.set(network(prefix), [...owners.get(network(prefix)) ?? [], device.name]);
|
|
562
600
|
}
|
|
563
601
|
}
|
|
564
602
|
for (const device of devices) {
|
|
565
603
|
for (const route of device.routing?.static ?? []) {
|
|
566
|
-
if (!
|
|
604
|
+
if (!isCidr2(route.prefix) || split(route.prefix).len === 0)
|
|
567
605
|
continue;
|
|
568
|
-
const overlapsSegment = [...
|
|
569
|
-
const coversOwned = [...owners.keys()].some((owned) =>
|
|
606
|
+
const overlapsSegment = [...segments.keys()].some((prefix) => contains2(route.prefix, prefix) || contains2(prefix, route.prefix));
|
|
607
|
+
const coversOwned = [...owners.keys()].some((owned) => contains2(route.prefix, owned));
|
|
570
608
|
if (overlapsSegment || coversOwned)
|
|
571
609
|
continue;
|
|
572
610
|
const how = route.blackhole ? "discarded" : route.via ? `routed via ${route.via}` : `routed out ${route.interface}`;
|
|
@@ -575,12 +613,12 @@ function addressesOf(devices, within) {
|
|
|
575
613
|
}
|
|
576
614
|
const asked = within ? network(within) : undefined;
|
|
577
615
|
const spacePrefixes = asked ? [asked] : [...owners.keys()];
|
|
578
|
-
const spaceOf = (prefix) => spacePrefixes.filter((space) =>
|
|
616
|
+
const spaceOf = (prefix) => spacePrefixes.filter((space) => contains2(space, prefix)).sort((a, b) => split(b).len - split(a).len)[0];
|
|
579
617
|
const toSegment = (prefix) => {
|
|
580
|
-
const { uses, taken } =
|
|
618
|
+
const { uses, taken } = segments.get(prefix);
|
|
581
619
|
const free = gaps(usable(prefix), taken);
|
|
582
620
|
const total = free.reduce((sum, range) => sum + range.last - range.first + 1n, 0n);
|
|
583
|
-
const family =
|
|
621
|
+
const family = familyOf2(prefix);
|
|
584
622
|
return {
|
|
585
623
|
prefix,
|
|
586
624
|
uses: uses.sort(byAddress),
|
|
@@ -591,11 +629,11 @@ function addressesOf(devices, within) {
|
|
|
591
629
|
const uniqueBlocks = blocks.filter((block, index) => blocks.findIndex((other) => other.prefix === block.prefix && other.what === block.what) === index);
|
|
592
630
|
const spaces = spacePrefixes.sort(comparePrefixes).map((space) => {
|
|
593
631
|
const inside = (prefix) => spaceOf(prefix) === space;
|
|
594
|
-
const spaceSegments = [...
|
|
632
|
+
const spaceSegments = [...segments.keys()].filter(inside).sort(comparePrefixes);
|
|
595
633
|
const spaceBlocks = uniqueBlocks.filter((block) => inside(block.prefix)).sort(byPrefix);
|
|
596
|
-
const nested = spacePrefixes.filter((other) => other !== space &&
|
|
634
|
+
const nested = spacePrefixes.filter((other) => other !== space && contains2(space, other));
|
|
597
635
|
const taken = [...spaceSegments, ...spaceBlocks.map((block) => block.prefix), ...nested].map((prefix) => rangeOf(prefix));
|
|
598
|
-
const family =
|
|
636
|
+
const family = familyOf2(space);
|
|
599
637
|
return {
|
|
600
638
|
prefix: space,
|
|
601
639
|
owner: asked && !owners.has(space) ? "asked for" : `originated by ${(owners.get(space) ?? []).join(", ")}`,
|
|
@@ -608,7 +646,7 @@ function addressesOf(devices, within) {
|
|
|
608
646
|
return {
|
|
609
647
|
spaces,
|
|
610
648
|
elsewhere: asked ? { segments: [], blocks: [] } : {
|
|
611
|
-
segments: [...
|
|
649
|
+
segments: [...segments.keys()].filter(outside).sort(comparePrefixes).map(toSegment),
|
|
612
650
|
blocks: uniqueBlocks.filter((block) => outside(block.prefix)).sort(byPrefix)
|
|
613
651
|
}
|
|
614
652
|
};
|
|
@@ -636,44 +674,6 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
636
674
|
import { dirname, join } from "node:path";
|
|
637
675
|
import { fileURLToPath } from "node:url";
|
|
638
676
|
|
|
639
|
-
// src/adapters/devices/raisecom-ros/models.ts
|
|
640
|
-
var models = {
|
|
641
|
-
"RAX721-C-6C48": {
|
|
642
|
-
ports: { "100g": 6, "25g": 24, "10g": 24 },
|
|
643
|
-
spelling: { "100g": "hundredgige 1/1/{n}", "25g": "twenty-fivegige 1/2/{n}", "10g": "tengigabitethernet 1/3/{n}" }
|
|
644
|
-
}
|
|
645
|
-
};
|
|
646
|
-
|
|
647
|
-
// src/adapters/devices/routeros/models.ts
|
|
648
|
-
var models2 = {
|
|
649
|
-
"CCR2216-1G-12XS-2XQ": {
|
|
650
|
-
ports: { "100g": 2, "25g": 12, "1g": 1 },
|
|
651
|
-
spelling: { "100g": "qsfp28-{n}-1", "25g": "sfp28-{n}", "1g": "ether{n}" }
|
|
652
|
-
}
|
|
653
|
-
};
|
|
654
|
-
|
|
655
|
-
// src/adapters/devices/vrp/models.ts
|
|
656
|
-
var models3 = {
|
|
657
|
-
"CE6855-48S6Q-HI": { ports: { "10g": 48, "40g": 6 }, spelling: { "10g": "10GE1/0/{n}", "40g": "40GE1/0/{n}" } }
|
|
658
|
-
};
|
|
659
|
-
|
|
660
|
-
// src/adapters/devices/catalog.ts
|
|
661
|
-
var catalog = { routeros: models2, vrp: models3, "raisecom-ros": models };
|
|
662
|
-
function portsOf(platform, model) {
|
|
663
|
-
const spec = catalog[platform]?.[model];
|
|
664
|
-
if (!spec)
|
|
665
|
-
throw new Error(`${platform} has no model ${model}`);
|
|
666
|
-
return Object.entries(spec.ports).flatMap(([speed, count2]) => Array.from({ length: count2 }, (_, i) => `${speed}-${i + 1}`));
|
|
667
|
-
}
|
|
668
|
-
function spell(platform, model, port) {
|
|
669
|
-
const spec = catalog[platform]?.[model];
|
|
670
|
-
const parts = /^(\d+[gm])-(\d+)$/.exec(port);
|
|
671
|
-
const template = parts && spec?.spelling[parts[1]];
|
|
672
|
-
if (!template)
|
|
673
|
-
throw new Error(`${model} has no port ${port}`);
|
|
674
|
-
return template.replace("{n}", parts[2]);
|
|
675
|
-
}
|
|
676
|
-
|
|
677
677
|
// src/core/schema-docs.ts
|
|
678
678
|
var SCHEMA_FILES = [
|
|
679
679
|
["device.ts", "Device"],
|
|
@@ -825,7 +825,7 @@ function sessionsOf(devices, owners) {
|
|
|
825
825
|
return all.map(({ deviceObject, ...session }) => {
|
|
826
826
|
const far = ownerOf(session.address);
|
|
827
827
|
if (far && far !== deviceObject) {
|
|
828
|
-
const back = all.some((other) => other.deviceObject === far && ownerOf(other.address) === deviceObject &&
|
|
828
|
+
const back = all.some((other) => other.deviceObject === far && ownerOf(other.address) === deviceObject && familyOf2(other.address) === familyOf2(session.address));
|
|
829
829
|
return { ...session, far: far.name, ...back ? {} : { oneSided: true } };
|
|
830
830
|
}
|
|
831
831
|
return { ...session, ...session.remoteAs ? { far: `AS${session.remoteAs}` } : {} };
|
|
@@ -834,7 +834,7 @@ function sessionsOf(devices, owners) {
|
|
|
834
834
|
function cablesOf(devices) {
|
|
835
835
|
const byName = new Map(devices.map((device) => [device.name, device]));
|
|
836
836
|
const drawn = new Set;
|
|
837
|
-
const
|
|
837
|
+
const cables = [];
|
|
838
838
|
const vlansOf = (device, portName) => {
|
|
839
839
|
const port = device.ports?.[portName];
|
|
840
840
|
const lag = port?.lag ? device.interfaces?.[port.lag] : undefined;
|
|
@@ -854,7 +854,7 @@ function cablesOf(devices) {
|
|
|
854
854
|
drawn.add(`${here}|${there}`);
|
|
855
855
|
const farPort = byName.get(link.device)?.ports?.[link.port];
|
|
856
856
|
const linksBack = farPort?.link?.device === device.name && farPort.link.port === portName;
|
|
857
|
-
|
|
857
|
+
cables.push({
|
|
858
858
|
device: device.name,
|
|
859
859
|
port: portName,
|
|
860
860
|
farDevice: link.device,
|
|
@@ -867,7 +867,7 @@ function cablesOf(devices) {
|
|
|
867
867
|
});
|
|
868
868
|
}
|
|
869
869
|
}
|
|
870
|
-
return
|
|
870
|
+
return cables;
|
|
871
871
|
}
|
|
872
872
|
function packageRoot() {
|
|
873
873
|
let directory = dirname(fileURLToPath(import.meta.url));
|
|
@@ -899,7 +899,7 @@ function schemaDocs() {
|
|
|
899
899
|
function studioData(devices, graph, texts) {
|
|
900
900
|
const owners = new Map;
|
|
901
901
|
for (const holder of holdersOf(devices)) {
|
|
902
|
-
const key = keyOf(
|
|
902
|
+
const key = keyOf(host2(holder.address));
|
|
903
903
|
if (!owners.has(key))
|
|
904
904
|
owners.set(key, holder);
|
|
905
905
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// src/core/addr.ts
|
|
2
|
-
var
|
|
2
|
+
var familyOf2 = (address) => address.includes(":") ? "ipv6" : "ipv4";
|
|
3
3
|
var bitsOf = (family) => family === "ipv4" ? 32 : 128;
|
|
4
4
|
function parse(address) {
|
|
5
5
|
if (!address.includes(":")) {
|
|
6
6
|
const octets = address.split(".");
|
|
7
|
-
const
|
|
8
|
-
if (!
|
|
7
|
+
const valid = octets.length === 4 && octets.every((octet) => /^\d{1,3}$/.test(octet) && Number(octet) <= 255);
|
|
8
|
+
if (!valid)
|
|
9
9
|
return null;
|
|
10
10
|
return octets.reduce((value, octet) => value << 8n | BigInt(octet), 0n);
|
|
11
11
|
}
|
|
@@ -24,15 +24,15 @@ function parse(address) {
|
|
|
24
24
|
}
|
|
25
25
|
function split(cidr) {
|
|
26
26
|
const [address, lengthText] = cidr.split("/");
|
|
27
|
-
const family =
|
|
27
|
+
const family = familyOf2(address);
|
|
28
28
|
const length = Number(lengthText);
|
|
29
29
|
const valid = parse(address) !== null && lengthText !== undefined && Number.isInteger(length) && length >= 0 && length <= bitsOf(family);
|
|
30
30
|
if (!valid)
|
|
31
31
|
throw new Error(`'${cidr}' is not an address with a length`);
|
|
32
32
|
return { addr: address, len: length, family };
|
|
33
33
|
}
|
|
34
|
-
var
|
|
35
|
-
function
|
|
34
|
+
var isAddress2 = (text) => parse(text) !== null;
|
|
35
|
+
function isCidr2(text) {
|
|
36
36
|
try {
|
|
37
37
|
split(text);
|
|
38
38
|
return true;
|
|
@@ -73,10 +73,10 @@ function network(cidr) {
|
|
|
73
73
|
const { addr, len, family } = split(cidr);
|
|
74
74
|
return `${format(parse(addr) & mask(len, family), family)}/${len}`;
|
|
75
75
|
}
|
|
76
|
-
function
|
|
76
|
+
function contains2(outer, inner) {
|
|
77
77
|
const outerParts = split(outer);
|
|
78
78
|
const [innerAddress, innerLengthText] = inner.split("/");
|
|
79
|
-
if (
|
|
79
|
+
if (familyOf2(innerAddress) !== outerParts.family)
|
|
80
80
|
return false;
|
|
81
81
|
const innerLength = innerLengthText === undefined ? bitsOf(outerParts.family) : Number(innerLengthText);
|
|
82
82
|
if (innerLength < outerParts.len)
|
|
@@ -105,7 +105,7 @@ function prefixesBetween(first, last, family) {
|
|
|
105
105
|
}
|
|
106
106
|
var netmask = (length) => format(mask(length, "ipv4"), "ipv4");
|
|
107
107
|
var wildcard = (length) => format(mask(length, "ipv4") ^ 0xffffffffn, "ipv4");
|
|
108
|
-
var
|
|
108
|
+
var host2 = (cidr) => cidr.split("/")[0];
|
|
109
109
|
var NOT_GLOBAL_V4 = [
|
|
110
110
|
"0.0.0.0/8",
|
|
111
111
|
"10.0.0.0/8",
|
|
@@ -119,10 +119,10 @@ var NOT_GLOBAL_V4 = [
|
|
|
119
119
|
"198.51.100.0/24",
|
|
120
120
|
"203.0.113.0/24"
|
|
121
121
|
];
|
|
122
|
-
function
|
|
123
|
-
if (
|
|
124
|
-
return
|
|
125
|
-
return !NOT_GLOBAL_V4.some((prefix) =>
|
|
122
|
+
function isGlobal2(address) {
|
|
123
|
+
if (familyOf2(address) === "ipv6")
|
|
124
|
+
return contains2("2000::/3", address) && !contains2("2001:db8::/32", address);
|
|
125
|
+
return !NOT_GLOBAL_V4.some((prefix) => contains2(prefix, address));
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
export {
|
|
128
|
+
export { familyOf2, parse, split, isAddress2, isCidr2, network, contains2, formatAddress, rangeOf, prefixesBetween, netmask, wildcard, host2, isGlobal2 };
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import"./index-fq57hya4.js";
|
|
2
|
-
|
|
3
1
|
// src/cli/new.ts
|
|
4
2
|
import { spawnSync } from "node:child_process";
|
|
5
3
|
import { cpSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
@@ -87,7 +85,7 @@ async function create(args) {
|
|
|
87
85
|
if (terminal) {
|
|
88
86
|
console.log(`
|
|
89
87
|
Which is closest to your network?`);
|
|
90
|
-
names.forEach((
|
|
88
|
+
names.forEach((name, index) => console.log(` ${index + 1}. ${PATTERNS[name].title}: ${PATTERNS[name].fits}`));
|
|
91
89
|
}
|
|
92
90
|
pattern = names[Number(await ask("Number", "1")) - 1];
|
|
93
91
|
}
|
|
@@ -120,14 +118,14 @@ Which is closest to your network?`);
|
|
|
120
118
|
let text = readFileSync(path, "utf8");
|
|
121
119
|
if (asn && asn !== EXAMPLE_ASN)
|
|
122
120
|
text = text.replaceAll(new RegExp(`\\b${EXAMPLE_ASN}\\b`, "g"), asn);
|
|
123
|
-
for (const [,
|
|
124
|
-
secrets.add(
|
|
121
|
+
for (const [, name] of text.matchAll(/secret\("([A-Z0-9_]+)"\)/g))
|
|
122
|
+
secrets.add(name);
|
|
125
123
|
writeFileSync(path, text);
|
|
126
124
|
}
|
|
127
125
|
const env = [
|
|
128
126
|
"# Secrets for Circuit. Never commit this file.",
|
|
129
127
|
"# A value is the secret itself, or a 1Password reference, op://vault/item/field, read with the 1Password CLI.",
|
|
130
|
-
...[...secrets].sort().map((
|
|
128
|
+
...[...secrets].sort().map((name) => onePassword ? `${name}=op://Network/${name}/password` : `${name}=`),
|
|
131
129
|
""
|
|
132
130
|
];
|
|
133
131
|
writeFileSync(join(target, ".env"), env.join(`
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
packageRoot
|
|
3
|
-
} from "./index-
|
|
4
|
-
import"./index-xm9nma2t.js";
|
|
5
|
-
import"./index-fq57hya4.js";
|
|
3
|
+
} from "./index-n4zpd25n.js";
|
|
6
4
|
|
|
7
5
|
// src/studio/server.ts
|
|
8
6
|
import { execFile as execFile2, spawn } from "node:child_process";
|
|
@@ -106,7 +104,8 @@ var CONTENT_TYPES = {
|
|
|
106
104
|
".html": "text/html; charset=utf-8",
|
|
107
105
|
".css": "text/css; charset=utf-8",
|
|
108
106
|
".js": "text/javascript; charset=utf-8",
|
|
109
|
-
".svg": "image/svg+xml"
|
|
107
|
+
".svg": "image/svg+xml",
|
|
108
|
+
".woff2": "font/woff2"
|
|
110
109
|
};
|
|
111
110
|
function gather(configPath) {
|
|
112
111
|
const cli = process.argv[1];
|
|
@@ -152,6 +151,7 @@ async function studio(options) {
|
|
|
152
151
|
`);
|
|
153
152
|
};
|
|
154
153
|
const server = createServer((request, response) => {
|
|
154
|
+
response.setHeader("x-robots-tag", "noindex, nofollow, noarchive");
|
|
155
155
|
if (!hostAllowed(request.headers.host, port, options.allowHosts)) {
|
|
156
156
|
response.writeHead(403).end();
|
|
157
157
|
return;
|
|
@@ -160,11 +160,11 @@ async function studio(options) {
|
|
|
160
160
|
const path = url.pathname;
|
|
161
161
|
if (path === "/history") {
|
|
162
162
|
answer(response, async () => {
|
|
163
|
-
const
|
|
164
|
-
const files = JSON.parse(data).files?.[
|
|
163
|
+
const name = url.searchParams.get("device") ?? "";
|
|
164
|
+
const files = JSON.parse(data).files?.[name];
|
|
165
165
|
if (!files)
|
|
166
|
-
throw new Error(`no device ${
|
|
167
|
-
return history(root, [files.source, files.snapshot].filter((
|
|
166
|
+
throw new Error(`no device ${name}`);
|
|
167
|
+
return history(root, [files.source, files.snapshot].filter((file) => Boolean(file)));
|
|
168
168
|
});
|
|
169
169
|
return;
|
|
170
170
|
}
|
|
@@ -187,6 +187,12 @@ async function studio(options) {
|
|
|
187
187
|
});
|
|
188
188
|
return;
|
|
189
189
|
}
|
|
190
|
+
if (path === "/robots.txt") {
|
|
191
|
+
response.writeHead(200, { "content-type": "text/plain; charset=utf-8" }).end(`User-agent: *
|
|
192
|
+
Disallow: /
|
|
193
|
+
`);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
190
196
|
if (path === "/data") {
|
|
191
197
|
response.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" }).end(data);
|
|
192
198
|
return;
|
|
@@ -208,7 +214,7 @@ async function studio(options) {
|
|
|
208
214
|
const name = path === "/" ? "index.html" : path.slice(1);
|
|
209
215
|
const type = CONTENT_TYPES[extname(name)];
|
|
210
216
|
const file = join2(pages, name);
|
|
211
|
-
if (!/^[a-z-]+\.[a-
|
|
217
|
+
if (!/^[a-z-]+\.[a-z0-9]+$/.test(name) || !type || !existsSync(file)) {
|
|
212
218
|
response.writeHead(404).end();
|
|
213
219
|
return;
|
|
214
220
|
}
|
|
@@ -240,7 +246,6 @@ async function studio(options) {
|
|
|
240
246
|
});
|
|
241
247
|
}
|
|
242
248
|
export {
|
|
243
|
-
studio,
|
|
244
249
|
isLoopback,
|
|
245
|
-
|
|
250
|
+
studio
|
|
246
251
|
};
|