@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/dist/index.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "./chunks/index-
|
|
10
|
-
import"./chunks/index-fq57hya4.js";
|
|
2
|
+
familyOf2,
|
|
3
|
+
isAddress2,
|
|
4
|
+
isCidr2,
|
|
5
|
+
network,
|
|
6
|
+
contains2,
|
|
7
|
+
host2,
|
|
8
|
+
isGlobal2
|
|
9
|
+
} from "./chunks/index-v8zgr7wm.js";
|
|
11
10
|
|
|
12
11
|
// src/core/define.ts
|
|
13
12
|
var secret = (name) => ({ secret: name });
|
|
@@ -19,7 +18,7 @@ var secretFile = (path) => {
|
|
|
19
18
|
function defineDevice(device) {
|
|
20
19
|
return device;
|
|
21
20
|
}
|
|
22
|
-
var defineNetwork = (
|
|
21
|
+
var defineNetwork = (network) => network;
|
|
23
22
|
// src/core/merge.ts
|
|
24
23
|
function merge(...records) {
|
|
25
24
|
const result = {};
|
|
@@ -33,16 +32,16 @@ function merge(...records) {
|
|
|
33
32
|
return result;
|
|
34
33
|
}
|
|
35
34
|
export {
|
|
36
|
-
|
|
37
|
-
secret,
|
|
38
|
-
network as prefixOf,
|
|
39
|
-
merge,
|
|
40
|
-
isGlobal,
|
|
41
|
-
isCidr,
|
|
42
|
-
isAddress,
|
|
43
|
-
host,
|
|
44
|
-
familyOf,
|
|
45
|
-
defineNetwork,
|
|
35
|
+
contains2 as contains,
|
|
46
36
|
defineDevice,
|
|
47
|
-
|
|
37
|
+
defineNetwork,
|
|
38
|
+
familyOf2 as familyOf,
|
|
39
|
+
host2 as host,
|
|
40
|
+
isAddress2 as isAddress,
|
|
41
|
+
isCidr2 as isCidr,
|
|
42
|
+
isGlobal2 as isGlobal,
|
|
43
|
+
merge,
|
|
44
|
+
network as prefixOf,
|
|
45
|
+
secret,
|
|
46
|
+
secretFile
|
|
48
47
|
};
|
package/dist/presets/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
} from "../chunks/index-
|
|
4
|
-
import"../chunks/index-fq57hya4.js";
|
|
2
|
+
contains2
|
|
3
|
+
} from "../chunks/index-v8zgr7wm.js";
|
|
5
4
|
|
|
6
5
|
// src/presets/index.ts
|
|
7
6
|
var MARTIANS = ["127.0.0.0/8", "169.254.0.0/16", "0.0.0.0/8"];
|
|
@@ -39,7 +38,7 @@ function antiAmplification(destinations, options = {}) {
|
|
|
39
38
|
{ min: port + 1, max: 1023 }
|
|
40
39
|
];
|
|
41
40
|
return destinations.flatMap((dst) => [
|
|
42
|
-
...(options.keep_fragments ?? []).filter((prefix) =>
|
|
41
|
+
...(options.keep_fragments ?? []).filter((prefix) => contains2(dst, prefix)).map((prefix) => ({
|
|
43
42
|
description: `UDP fragments to ${prefix} kept`,
|
|
44
43
|
match: { protocol: "udp", dst: prefix, src_port: 0 },
|
|
45
44
|
action: "accept"
|
|
@@ -174,10 +173,10 @@ var tunnelsOutsideOffered = (devices) => {
|
|
|
174
173
|
continue;
|
|
175
174
|
const subnets = (iface.addresses ?? []).map((entry) => typeof entry === "string" ? entry : entry.address);
|
|
176
175
|
for (const neighbor of neighborPolicies(device)) {
|
|
177
|
-
if (!subnets.some((subnet) =>
|
|
176
|
+
if (!subnets.some((subnet) => contains2(subnet, neighbor.address)))
|
|
178
177
|
continue;
|
|
179
178
|
for (const prefix of offered(device, neighbor.export)) {
|
|
180
|
-
if (
|
|
179
|
+
if (contains2(prefix, iface.local)) {
|
|
181
180
|
findings.push({
|
|
182
181
|
level: "error",
|
|
183
182
|
device: device.name,
|
|
@@ -346,7 +345,7 @@ function internetExchange(exchange, members) {
|
|
|
346
345
|
]) {
|
|
347
346
|
if (!address)
|
|
348
347
|
continue;
|
|
349
|
-
if (!lan || !
|
|
348
|
+
if (!lan || !contains2(lan, address))
|
|
350
349
|
error(`${address} of ${member.name} is outside the peering LAN`);
|
|
351
350
|
const holder = taken.get(address);
|
|
352
351
|
if (holder)
|
|
@@ -367,19 +366,19 @@ function internetExchange(exchange, members) {
|
|
|
367
366
|
return { ports, check };
|
|
368
367
|
}
|
|
369
368
|
export {
|
|
370
|
-
|
|
371
|
-
tunnelsOutsideOffered,
|
|
372
|
-
trustBoundary,
|
|
373
|
-
offered,
|
|
374
|
-
neighborPolicies,
|
|
375
|
-
internetExchange,
|
|
376
|
-
exportsEndInReject,
|
|
377
|
-
communityScheme,
|
|
378
|
-
bgpSanity,
|
|
379
|
-
badTcpFlags,
|
|
380
|
-
antiSpoofing,
|
|
381
|
-
antiAmplification,
|
|
382
|
-
PRIVATE_RANGES,
|
|
369
|
+
AMPLIFIERS,
|
|
383
370
|
MARTIANS,
|
|
384
|
-
|
|
371
|
+
PRIVATE_RANGES,
|
|
372
|
+
antiAmplification,
|
|
373
|
+
antiSpoofing,
|
|
374
|
+
badTcpFlags,
|
|
375
|
+
bgpSanity,
|
|
376
|
+
communityScheme,
|
|
377
|
+
exportsEndInReject,
|
|
378
|
+
internetExchange,
|
|
379
|
+
neighborPolicies,
|
|
380
|
+
offered,
|
|
381
|
+
trustBoundary,
|
|
382
|
+
tunnelsOutsideOffered,
|
|
383
|
+
vlansOf
|
|
385
384
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@takodotid/circuit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Declarative, vendor-neutral network configuration: describe each device in TypeScript, and Circuit makes the device match. Alpha.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"network",
|
|
@@ -86,14 +86,14 @@
|
|
|
86
86
|
"ssh2": "^1.17.0"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
|
-
"@commitlint/cli": "^19.
|
|
90
|
-
"@commitlint/config-conventional": "^19.
|
|
89
|
+
"@commitlint/cli": "^19.8.1",
|
|
90
|
+
"@commitlint/config-conventional": "^19.8.1",
|
|
91
91
|
"@types/bun": "^1.4.2",
|
|
92
|
-
"@types/node": "^24",
|
|
93
|
-
"husky": "^9.1.
|
|
94
|
-
"prettier": "^3.9.
|
|
92
|
+
"@types/node": "^24.19.2",
|
|
93
|
+
"husky": "^9.1.7",
|
|
94
|
+
"prettier": "^3.9.10",
|
|
95
95
|
"typescript": "^7.0.2",
|
|
96
96
|
"vitepress": "^1.6.4"
|
|
97
97
|
},
|
|
98
|
-
"packageManager": "bun@1.3
|
|
98
|
+
"packageManager": "bun@1.4.3"
|
|
99
99
|
}
|
package/src/core/files.ts
CHANGED
|
@@ -16,12 +16,14 @@ export function projectSources(root: string, directory = root): string[] {
|
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
/** The first name written out after a call to defineDevice: the device's own, not that of another device a link, a peer or a tunnel names further on. */
|
|
20
|
+
const FIRST_NAME = /^[\s\S]*?\bname\s*:\s*(["'`])(.*?)\1/;
|
|
20
21
|
|
|
21
22
|
/** Whether a file's text declares the device: it calls defineDevice and gives it the name, written out whole. */
|
|
22
23
|
export function declaresDevice(text: string, name: string): boolean {
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
return [...text.matchAll(/\bdefineDevice\s*\(/g)].some(
|
|
25
|
+
(call) => FIRST_NAME.exec(text.slice(call.index + call[0].length))?.[2] === name
|
|
26
|
+
);
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/** The project files that declare the device, relative to the root. */
|
package/src/studio/public/app.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/// <reference lib="dom" />
|
|
3
3
|
// Studio's page: loads what the server gathered from the config, shows the place the address names, and loads again whenever the server says the config changed.
|
|
4
4
|
|
|
5
|
-
import { byId, go } from "./dom.js";
|
|
5
|
+
import { byId, enter, go } from "./dom.js";
|
|
6
6
|
import { currentLayer, isLayer, setGraph, showLayer } from "./graph.js";
|
|
7
7
|
import { problemCount, showTables } from "./tables.js";
|
|
8
8
|
import { showTools } from "./tools.js";
|
|
@@ -42,12 +42,21 @@ function showSummary() {
|
|
|
42
42
|
const count = problemCount(studio);
|
|
43
43
|
problems.textContent = count ? String(count) : "";
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
]
|
|
45
|
+
const items = [
|
|
46
|
+
{ value: devices, label: devices === 1 ? "device" : "devices", state: "neutral" },
|
|
47
|
+
{ value: errors, label: errors === 1 ? "error" : "errors", state: errors ? "error" : "quiet" },
|
|
48
|
+
{ value: warnings, label: warnings === 1 ? "warning" : "warnings", state: warnings ? "warning" : "quiet" },
|
|
49
|
+
{ value: drifted, label: "unlike the snapshot", state: drifted ? "changed" : "quiet" },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
summary.replaceChildren(
|
|
53
|
+
...items.map(({ value, label, state }) => {
|
|
54
|
+
const item = document.createElement("span");
|
|
55
|
+
item.className = `summary-item ${state}`;
|
|
56
|
+
item.append(String(value), ` ${label}`);
|
|
57
|
+
return item;
|
|
58
|
+
})
|
|
59
|
+
);
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
/** Show the place the address names, such as `#graph/bgp` or `#devices/edge-01/ports`. */
|
|
@@ -90,10 +99,22 @@ function route() {
|
|
|
90
99
|
else if (view === "compare") showCompare(parts, tables, studio);
|
|
91
100
|
else showTables(parts, tables, studio);
|
|
92
101
|
tables.scrollTop = moved ? 0 : scrolled;
|
|
102
|
+
|
|
103
|
+
// A new place comes in; the same place drawn again, because the config changed, stays still.
|
|
104
|
+
if (moved) enter(tables);
|
|
93
105
|
}
|
|
94
106
|
|
|
95
107
|
window.addEventListener("hashchange", route);
|
|
96
108
|
|
|
109
|
+
// A menu closes once one of its items is chosen, or when the reader clicks anywhere else.
|
|
110
|
+
document.addEventListener("click", (event) => {
|
|
111
|
+
const target = event.target instanceof Element ? event.target : undefined;
|
|
112
|
+
|
|
113
|
+
for (const menu of document.querySelectorAll("details.menu[open]")) {
|
|
114
|
+
if (!target || !menu.contains(target) || target.closest(".menu-items button")) menu.removeAttribute("open");
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
97
118
|
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
98
119
|
let searching;
|
|
99
120
|
|
|
@@ -141,7 +162,14 @@ events.addEventListener("error", () => {
|
|
|
141
162
|
live.classList.remove("on");
|
|
142
163
|
});
|
|
143
164
|
|
|
144
|
-
// The config changed: load it again. The page itself changed, while working on Circuit: load the page again.
|
|
145
|
-
events.addEventListener("message", (event) =>
|
|
165
|
+
// The config changed: load it again, and the live mark beats once to say so. The page itself changed, while working on Circuit: load the page again.
|
|
166
|
+
events.addEventListener("message", (event) => {
|
|
167
|
+
if (event.data === "page") return location.reload();
|
|
168
|
+
|
|
169
|
+
live.classList.remove("beat");
|
|
170
|
+
void live.offsetWidth;
|
|
171
|
+
live.classList.add("beat");
|
|
172
|
+
void load();
|
|
173
|
+
});
|
|
146
174
|
|
|
147
175
|
void load();
|
package/src/studio/public/dom.js
CHANGED
|
@@ -71,6 +71,26 @@ export function svgById(id) {
|
|
|
71
71
|
return element;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/** The reader's system asks for less motion: nothing moves for show, and changes happen at once. */
|
|
75
|
+
export const still = matchMedia("(prefers-reduced-motion: reduce)");
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Bring a part of the page in as it changes: it fades in and rises a little.
|
|
79
|
+
*
|
|
80
|
+
* @param {Element} element
|
|
81
|
+
*/
|
|
82
|
+
export function enter(element) {
|
|
83
|
+
if (still.matches) return;
|
|
84
|
+
|
|
85
|
+
element.animate(
|
|
86
|
+
[
|
|
87
|
+
{ opacity: 0, transform: "translateY(6px)" },
|
|
88
|
+
{ opacity: 1, transform: "none" },
|
|
89
|
+
],
|
|
90
|
+
{ duration: 240, easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
74
94
|
/** Browser storage can be missing or refuse, in a private window for example. The page works without it. */
|
|
75
95
|
export const store = {
|
|
76
96
|
/**
|
|
@@ -64,15 +64,21 @@ export function svgOf(svg, viewport) {
|
|
|
64
64
|
const originals = [svg, ...svg.querySelectorAll("*")];
|
|
65
65
|
const copies = [copy, ...copy.querySelectorAll("*")];
|
|
66
66
|
|
|
67
|
+
// The dots travelling the cables are left out: a still image would show each one stopped at the drawing's corner.
|
|
68
|
+
const moving = [];
|
|
69
|
+
|
|
67
70
|
for (const [index, original] of originals.entries()) {
|
|
68
71
|
const target = copies[index];
|
|
69
72
|
if (!(target instanceof SVGElement)) continue;
|
|
73
|
+
if (original.classList.contains("signal")) moving.push(target);
|
|
70
74
|
|
|
71
75
|
const look = getComputedStyle(original);
|
|
72
76
|
target.setAttribute("style", DRAWN.map((name) => `${name}:${look.getPropertyValue(name)}`).join(";"));
|
|
73
77
|
target.removeAttribute("class");
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
for (const dot of moving) dot.remove();
|
|
81
|
+
|
|
76
82
|
// The drawing's own place on screen is left out: the file is framed around what is drawn.
|
|
77
83
|
copy.querySelector("#viewport")?.removeAttribute("transform");
|
|
78
84
|
copy.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
2
|
-
<rect width="32" height="32" rx="
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
|
|
2
|
+
<rect width="32" height="32" rx="6" fill="#f8e347" />
|
|
3
|
+
<rect x="4" y="8" width="24" height="16" rx="3" fill="#0a47b8" />
|
|
4
|
+
<g fill="#ffffff">
|
|
5
|
+
<rect x="7.5" y="11.5" width="4" height="3.5" rx="0.6" />
|
|
6
|
+
<rect x="14" y="11.5" width="4" height="3.5" rx="0.6" />
|
|
7
|
+
<rect x="20.5" y="11.5" width="4" height="3.5" rx="0.6" />
|
|
8
|
+
<rect x="7.5" y="17" width="4" height="3.5" rx="0.6" />
|
|
9
|
+
<rect x="14" y="17" width="4" height="3.5" rx="0.6" />
|
|
10
|
+
</g>
|
|
11
|
+
<rect x="20.5" y="17" width="4" height="3.5" rx="0.6" fill="#f8e347" />
|
|
6
12
|
</svg>
|