@takodotid/circuit 0.34.3 → 0.36.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 +14 -15
- package/dist/chunks/index-20kcjxjg.js +174 -0
- package/dist/chunks/index-2r4bbhac.js +977 -0
- package/dist/chunks/index-n7w3tg5y.js +149 -0
- package/dist/chunks/new-w3zys5y4.js +172 -0
- package/dist/chunks/server-gzsf5me5.js +259 -0
- package/dist/cli/index.js +11345 -9024
- package/dist/helpers/index.js +594 -0
- package/dist/index.js +9 -28
- package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
- package/dist/types/core/address.d.ts +44 -0
- package/dist/types/core/cabling.d.ts +41 -0
- package/dist/types/core/cross-checks.d.ts +28 -0
- package/dist/types/core/define.d.ts +25 -3
- package/dist/types/core/project-files.d.ts +12 -0
- package/dist/types/core/schema-docs.d.ts +13 -4
- package/dist/types/core/secrets.d.ts +62 -21
- package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
- package/dist/types/core/utils.d.ts +8 -0
- package/dist/types/core/validator.d.ts +19 -0
- package/dist/types/helpers/bgp.d.ts +97 -0
- package/dist/types/helpers/checks.d.ts +32 -0
- package/dist/types/helpers/exchange.d.ts +49 -0
- package/dist/types/helpers/filters.d.ts +51 -0
- package/dist/types/helpers/index.d.ts +10 -0
- package/dist/types/helpers/route-server.d.ts +52 -0
- package/dist/types/index.d.ts +5 -3
- package/dist/types/integrations/librenms.d.ts +60 -0
- package/dist/types/integrations/netbox.d.ts +89 -0
- package/dist/types/integrations/peeringdb.d.ts +24 -0
- package/dist/types/integrations/prometheus.d.ts +24 -0
- package/dist/types/integrations/registries.d.ts +8 -0
- package/dist/types/platforms/bgp-wait.d.ts +18 -0
- package/dist/types/platforms/catalog.d.ts +109 -0
- package/dist/types/platforms/derive.d.ts +17 -0
- package/dist/types/platforms/fortios/firewall.d.ts +45 -0
- package/dist/types/platforms/fortios/index.d.ts +134 -0
- package/dist/types/platforms/fortios/names.d.ts +52 -0
- package/dist/types/platforms/fortios/plan.d.ts +104 -0
- package/dist/types/platforms/fortios/render.d.ts +75 -0
- package/dist/types/platforms/fortios/routing.d.ts +56 -0
- package/dist/types/platforms/fortios/tree.d.ts +37 -0
- package/dist/types/platforms/index.d.ts +13 -0
- package/dist/types/platforms/junos/bgp.d.ts +44 -0
- package/dist/types/platforms/junos/index.d.ts +114 -0
- package/dist/types/platforms/junos/render.d.ts +154 -0
- package/dist/types/platforms/line-diff.d.ts +67 -0
- package/dist/types/platforms/platform.d.ts +120 -0
- package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
- package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
- package/dist/types/platforms/routeros/export.d.ts +59 -0
- package/dist/types/platforms/routeros/index.d.ts +96 -0
- package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
- package/dist/types/platforms/routeros/plan.d.ts +54 -0
- package/dist/types/platforms/routeros/render.d.ts +240 -0
- package/dist/types/platforms/vrp/index.d.ts +83 -0
- package/dist/types/platforms/vrp/render.d.ts +96 -0
- package/dist/types/schema/common.d.ts +12 -6
- package/dist/types/schema/index.d.ts +1 -0
- package/dist/types/schema/interfaces.d.ts +14 -6
- package/dist/types/schema/policy.d.ts +18 -10
- package/dist/types/schema/routing.d.ts +14 -1
- package/dist/types/schema/system.d.ts +16 -6
- package/dist/types/schema/time-zones.d.ts +2 -0
- package/dist/types/transport/console.d.ts +79 -23
- package/dist/types/transport/path-check.d.ts +32 -0
- package/dist/types/transport/ssh.d.ts +17 -15
- package/package.json +18 -12
- package/src/cli/graph-format.ts +91 -0
- package/src/cli/index.ts +1074 -484
- package/src/cli/new.ts +208 -175
- package/src/cli/rename.ts +97 -91
- package/src/core/address-space.ts +320 -0
- package/src/core/address.ts +202 -0
- package/src/core/cabling.ts +154 -0
- package/src/core/cross-checks.ts +364 -0
- package/src/core/define.ts +26 -3
- package/src/core/project-files.ts +35 -0
- package/src/core/schema-docs.ts +138 -130
- package/src/core/secrets.ts +170 -76
- package/src/core/topology.ts +656 -0
- package/src/core/utils.ts +37 -0
- package/src/core/validator.ts +407 -0
- package/src/helpers/bgp.ts +301 -0
- package/src/helpers/checks.ts +178 -0
- package/src/helpers/exchange.ts +117 -0
- package/src/helpers/filters.ts +107 -0
- package/src/helpers/index.ts +12 -0
- package/src/helpers/route-server.ts +212 -0
- package/src/index.ts +6 -3
- package/src/integrations/librenms.ts +291 -0
- package/src/integrations/netbox.ts +620 -0
- package/src/integrations/peeringdb.ts +173 -0
- package/src/integrations/prometheus.ts +90 -0
- package/src/integrations/registries.ts +25 -0
- package/src/platforms/bgp-wait.ts +41 -0
- package/src/platforms/catalog.ts +69 -0
- package/src/platforms/derive.ts +117 -0
- package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
- package/src/platforms/fortios/firewall.ts +377 -0
- package/src/platforms/fortios/index.ts +1060 -0
- package/src/platforms/fortios/names.ts +130 -0
- package/src/platforms/fortios/plan.ts +929 -0
- package/src/platforms/fortios/render.ts +779 -0
- package/src/platforms/fortios/routing.ts +476 -0
- package/src/platforms/fortios/tree.ts +129 -0
- package/src/platforms/index.ts +36 -0
- package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
- package/src/platforms/junos/bgp.ts +293 -0
- package/src/platforms/junos/index.ts +1006 -0
- package/src/platforms/junos/render.ts +967 -0
- package/src/platforms/line-diff.ts +244 -0
- package/src/platforms/platform.ts +122 -0
- package/src/platforms/raisecom-ros/TRAPS.md +40 -0
- package/src/platforms/raisecom-ros/index.ts +731 -0
- package/src/platforms/raisecom-ros/render.ts +328 -0
- package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
- package/src/platforms/routeros/export.ts +342 -0
- package/src/platforms/routeros/index.ts +636 -0
- package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
- package/src/platforms/routeros/plan.ts +444 -0
- package/src/platforms/routeros/render.ts +1483 -0
- package/src/platforms/vrp/TRAPS.md +45 -0
- package/src/platforms/vrp/index.ts +652 -0
- package/src/platforms/vrp/render.ts +615 -0
- package/src/schema/common.ts +15 -6
- package/src/schema/index.ts +1 -0
- package/src/schema/interfaces.ts +15 -5
- package/src/schema/policy.ts +22 -10
- package/src/schema/routing.ts +16 -1
- package/src/schema/system.ts +16 -6
- package/src/schema/time-zones.ts +482 -0
- package/src/studio/data.ts +172 -168
- package/src/studio/git.ts +95 -90
- package/src/studio/public/app.js +161 -150
- package/src/studio/public/compare.js +219 -212
- package/src/studio/public/diff.js +77 -75
- package/src/studio/public/dom.js +118 -108
- package/src/studio/public/evaluate.js +584 -545
- package/src/studio/public/export.js +120 -118
- package/src/studio/public/graph.js +1343 -1266
- package/src/studio/public/net.js +115 -109
- package/src/studio/public/tables.js +1135 -1083
- package/src/studio/public/tools.js +561 -529
- package/src/studio/server.ts +202 -196
- package/src/transport/console.ts +363 -271
- package/src/transport/path-check.ts +91 -0
- package/src/transport/ssh.ts +98 -94
- package/templates/colocation/checks.ts +11 -6
- package/templates/colocation/edge-01.ts +9 -9
- package/templates/colocation/routing.ts +2 -2
- package/templates/colocation/tenancy.ts +3 -3
- package/templates/colocation/tor-01.ts +3 -3
- package/templates/edge-router/checks.ts +4 -4
- package/templates/edge-router/edge-01.ts +5 -5
- package/templates/edge-router/routing.ts +2 -2
- package/templates/project/AGENTS.md +5 -1
- package/templates/single-site/checks.ts +2 -2
- package/dist/chunks/index-47debt16.js +0 -956
- package/dist/chunks/index-v8zgr7wm.js +0 -128
- package/dist/chunks/new-ny89s7y7.js +0 -166
- package/dist/chunks/server-tjbb3rqc.js +0 -251
- package/dist/presets/index.js +0 -394
- package/dist/types/adapters/devices/catalog.d.ts +0 -86
- package/dist/types/adapters/devices/derive.d.ts +0 -7
- package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
- package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
- package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
- package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
- package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
- package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
- package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
- package/dist/types/adapters/devices/index.d.ts +0 -3
- package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
- package/dist/types/adapters/devices/junos/index.d.ts +0 -42
- package/dist/types/adapters/devices/junos/render.d.ts +0 -52
- package/dist/types/adapters/devices/lines.d.ts +0 -56
- package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
- package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
- package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
- package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
- package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
- package/dist/types/adapters/devices/types.d.ts +0 -40
- package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
- package/dist/types/adapters/registries/index.d.ts +0 -3
- package/dist/types/adapters/registries/librenms.d.ts +0 -15
- package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
- package/dist/types/adapters/registries/prometheus.d.ts +0 -16
- package/dist/types/core/across.d.ts +0 -4
- package/dist/types/core/addr.d.ts +0 -34
- package/dist/types/core/files.d.ts +0 -6
- package/dist/types/core/merge.d.ts +0 -4
- package/dist/types/core/time.d.ts +0 -2
- package/dist/types/core/validate.d.ts +0 -2
- package/dist/types/presets/index.d.ts +0 -183
- package/src/adapters/devices/catalog.ts +0 -41
- package/src/adapters/devices/derive.ts +0 -59
- package/src/adapters/devices/fortios/firewall.ts +0 -362
- package/src/adapters/devices/fortios/index.ts +0 -923
- package/src/adapters/devices/fortios/names.ts +0 -115
- package/src/adapters/devices/fortios/plan.ts +0 -888
- package/src/adapters/devices/fortios/render.ts +0 -768
- package/src/adapters/devices/fortios/routing.ts +0 -444
- package/src/adapters/devices/fortios/tree.ts +0 -121
- package/src/adapters/devices/index.ts +0 -9
- package/src/adapters/devices/junos/bgp.ts +0 -269
- package/src/adapters/devices/junos/index.ts +0 -862
- package/src/adapters/devices/junos/render.ts +0 -872
- package/src/adapters/devices/lines.ts +0 -212
- package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -26
- package/src/adapters/devices/raisecom-ros/index.ts +0 -700
- package/src/adapters/devices/routeros/export.ts +0 -321
- package/src/adapters/devices/routeros/index.ts +0 -346
- package/src/adapters/devices/routeros/plan.ts +0 -398
- package/src/adapters/devices/routeros/render.ts +0 -1490
- package/src/adapters/devices/types.ts +0 -38
- package/src/adapters/devices/vrp/TRAPS.md +0 -26
- package/src/adapters/devices/vrp/index.ts +0 -865
- package/src/adapters/registries/index.ts +0 -19
- package/src/adapters/registries/librenms.ts +0 -92
- package/src/adapters/registries/peeringdb.ts +0 -169
- package/src/adapters/registries/prometheus.ts +0 -71
- package/src/cli/graph.ts +0 -89
- package/src/core/across.ts +0 -350
- package/src/core/addr.ts +0 -174
- package/src/core/addresses.ts +0 -316
- package/src/core/files.ts +0 -32
- package/src/core/graph.ts +0 -640
- package/src/core/merge.ts +0 -19
- package/src/core/time.ts +0 -15
- package/src/core/validate.ts +0 -348
- package/src/presets/index.ts +0 -579
- /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
- /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/vrp/models.ts +0 -0
package/src/core/schema-docs.ts
CHANGED
|
@@ -1,166 +1,174 @@
|
|
|
1
1
|
// The schema's own comments, read as data: each type, what it is, and each field with its type and description. The schema reference page and Studio both show them.
|
|
2
2
|
|
|
3
|
-
/** The schema files in reading order, with the title each gets. */
|
|
4
|
-
export const SCHEMA_FILES = [
|
|
5
|
-
["device.ts", "Device"],
|
|
6
|
-
["interfaces.ts", "Ports and interfaces"],
|
|
7
|
-
["routing.ts", "Routing"],
|
|
8
|
-
["policy.ts", "Route policies"],
|
|
9
|
-
["firewall.ts", "Firewall and ACLs"],
|
|
10
|
-
["system.ts", "System and management"],
|
|
11
|
-
["common.ts", "Common values"],
|
|
12
|
-
] as const;
|
|
13
|
-
|
|
14
3
|
export type Field = { path: string; type: string; description: string };
|
|
15
4
|
export type TypeDoc = { name: string; description: string; definition?: string; includes?: string[]; fields: Field[] };
|
|
16
5
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
.
|
|
21
|
-
.
|
|
22
|
-
.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
6
|
+
export class SchemaDocs {
|
|
7
|
+
/** The schema files in reading order, with the title each gets. */
|
|
8
|
+
static readonly FILES = [
|
|
9
|
+
["device.ts", "Device"],
|
|
10
|
+
["interfaces.ts", "Ports and interfaces"],
|
|
11
|
+
["routing.ts", "Routing"],
|
|
12
|
+
["policy.ts", "Route policies"],
|
|
13
|
+
["firewall.ts", "Firewall and ACLs"],
|
|
14
|
+
["system.ts", "System and management"],
|
|
15
|
+
["common.ts", "Common values"],
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
/** The text of a JSDoc comment, without its markers, as one paragraph. */
|
|
19
|
+
private static commentText(lines: string[]): string {
|
|
20
|
+
return lines
|
|
21
|
+
.map((line) => line.replace(/^\s*\/\*\*\s?|\s*\*\/\s*$|^\s*\*\s?/g, "").trim())
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.join(" ");
|
|
33
24
|
}
|
|
34
25
|
|
|
35
|
-
for
|
|
36
|
-
|
|
37
|
-
}
|
|
26
|
+
/** What a type parameter stands for, where it is not one of the device's names. */
|
|
27
|
+
private static readonly PARAMETER_MEANING: Record<string, string> = { Pl: "platform", M: "model" };
|
|
38
28
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
29
|
+
/** A type as a reader needs it: "name" for each of the device's names, and no parameter list on the schema's own types. */
|
|
30
|
+
private static withoutGenerics(type: string, generics: Set<string>): string {
|
|
31
|
+
let result = type;
|
|
32
|
+
while (/NoInfer<([^<>]*)>/.test(result)) {
|
|
33
|
+
result = result.replace(/NoInfer<([^<>]*)>/g, (_, inner: string) => (inner.includes("|") ? `(${inner})` : inner));
|
|
34
|
+
}
|
|
44
35
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const types: TypeDoc[] = [];
|
|
48
|
-
|
|
49
|
-
let comment: string[] = [];
|
|
50
|
-
let inComment = false;
|
|
51
|
-
let current: TypeDoc | undefined;
|
|
52
|
-
let depth = 0;
|
|
53
|
-
const parents: string[] = [];
|
|
54
|
-
let header: string | undefined;
|
|
55
|
-
|
|
56
|
-
/** Reads a whole declaration: a type it aliases, the types whose fields it also has, and whether a body opens. */
|
|
57
|
-
function finishHeader(): void {
|
|
58
|
-
const text = header!;
|
|
59
|
-
header = undefined;
|
|
60
|
-
|
|
61
|
-
// The part after `=`, with the generic parameter list before it left out.
|
|
62
|
-
const body = text.slice(text.indexOf("> =") >= 0 ? text.indexOf("> =") + 3 : text.indexOf("=") + 1).trim();
|
|
63
|
-
|
|
64
|
-
if (!body.includes("{")) {
|
|
65
|
-
current!.definition = body.replace(/;$/, "").replace(/^\|\s*/, "");
|
|
66
|
-
} else {
|
|
67
|
-
const before = body.slice(0, body.indexOf("{"));
|
|
68
|
-
const included = before
|
|
69
|
-
.split("&")
|
|
70
|
-
.map((part) => part.trim())
|
|
71
|
-
.filter((part) => /^\w/.test(part));
|
|
72
|
-
if (included.length) current!.includes = included;
|
|
36
|
+
for (const name of generics) {
|
|
37
|
+
result = result.replace(new RegExp(`\\b${name}<[^<>]*(?:<[^<>]*>[^<>]*)*>`, "g"), name);
|
|
73
38
|
}
|
|
74
39
|
|
|
75
|
-
|
|
40
|
+
return (
|
|
41
|
+
result
|
|
42
|
+
// A quoted value, such as the "M" of a rate, is left as it is.
|
|
43
|
+
.replace(/"[^"]*"|\b(Pl|M|N|V|I|P|S|G|A|AS|U|F|C|L|K)\b/g, (found, parameter?: string) =>
|
|
44
|
+
parameter ? (SchemaDocs.PARAMETER_MEANING[parameter] ?? "name") : found
|
|
45
|
+
)
|
|
46
|
+
.replace(/\bname(?: \| name)+\b/g, "name")
|
|
47
|
+
.replace(/\(name\)/g, "name")
|
|
48
|
+
);
|
|
76
49
|
}
|
|
77
50
|
|
|
78
|
-
|
|
79
|
-
const
|
|
51
|
+
private static parse(source: string, generics: Set<string>): TypeDoc[] {
|
|
52
|
+
const lines = source.split("\n");
|
|
53
|
+
const types: TypeDoc[] = [];
|
|
54
|
+
|
|
55
|
+
let comment: string[] = [];
|
|
56
|
+
let inComment = false;
|
|
57
|
+
let current: TypeDoc | undefined;
|
|
58
|
+
let depth = 0;
|
|
59
|
+
const parents: string[] = [];
|
|
60
|
+
let header: string | undefined;
|
|
61
|
+
|
|
62
|
+
/** Reads a whole declaration: a type it aliases, the types whose fields it also has, and whether a body opens. */
|
|
63
|
+
function finishHeader(): void {
|
|
64
|
+
const text = header!;
|
|
65
|
+
header = undefined;
|
|
66
|
+
|
|
67
|
+
// The part after `=`, with the generic parameter list before it left out.
|
|
68
|
+
const body = text.slice(text.indexOf("> =") >= 0 ? text.indexOf("> =") + 3 : text.indexOf("=") + 1).trim();
|
|
69
|
+
|
|
70
|
+
// A template literal type, such as `${number}:${number}`, holds braces but opens no body.
|
|
71
|
+
if (!body.includes("{") || body.startsWith("`")) {
|
|
72
|
+
current!.definition = body.replace(/;$/, "").replace(/^\|\s*/, "");
|
|
73
|
+
} else {
|
|
74
|
+
const before = body.slice(0, body.indexOf("{"));
|
|
75
|
+
const included = before
|
|
76
|
+
.split("&")
|
|
77
|
+
.map((part) => part.trim())
|
|
78
|
+
.filter((part) => /^\w/.test(part));
|
|
79
|
+
if (included.length) current!.includes = included;
|
|
80
|
+
}
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
comment = [trimmed];
|
|
83
|
-
inComment = !trimmed.endsWith("*/");
|
|
84
|
-
continue;
|
|
82
|
+
depth = (text.match(/{/g) ?? []).length - (text.match(/}/g) ?? []).length;
|
|
85
83
|
}
|
|
86
84
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (trimmed.endsWith("*/")) inComment = false;
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
const trimmed = line.trim();
|
|
92
87
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
88
|
+
if (trimmed.startsWith("/**")) {
|
|
89
|
+
comment = [trimmed];
|
|
90
|
+
inComment = !trimmed.endsWith("*/");
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
99
93
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
94
|
+
if (inComment) {
|
|
95
|
+
comment.push(trimmed);
|
|
96
|
+
if (trimmed.endsWith("*/")) inComment = false;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
104
99
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
100
|
+
// A declaration can span lines before its body opens or it ends.
|
|
101
|
+
if (header !== undefined) {
|
|
102
|
+
header += ` ${trimmed}`;
|
|
103
|
+
if (trimmed.endsWith("{") || trimmed.endsWith(";")) finishHeader();
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
108
106
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
// A type the schema does not export is documented too, since exported types take its fields.
|
|
108
|
+
const declaration = /^(?:export )?type (\w+)(<)?/.exec(trimmed);
|
|
109
|
+
if (declaration && depth === 0) {
|
|
110
|
+
if (declaration[2]) generics.add(declaration[1]!);
|
|
113
111
|
|
|
114
|
-
|
|
112
|
+
current = { name: declaration[1]!, description: SchemaDocs.commentText(comment), fields: [] };
|
|
113
|
+
types.push(current);
|
|
114
|
+
comment = [];
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const opensObject = rest!.trim().endsWith("{");
|
|
121
|
-
const type = opensObject ? "object" : rest!.replace(/;$/, "").trim();
|
|
116
|
+
header = trimmed;
|
|
117
|
+
if (trimmed.endsWith("{") || trimmed.endsWith(";")) finishHeader();
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
122
120
|
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
if (!current) continue;
|
|
122
|
+
|
|
123
|
+
const field = /^(?:readonly\s+)?("?[\w-]+"?)(\?)?:\s*(.*)$/.exec(trimmed);
|
|
124
|
+
if (field && depth >= 1) {
|
|
125
|
+
const [, name, optional, rest] = field;
|
|
126
|
+
const path = [...parents, name!.replace(/"/g, "")].join(".");
|
|
127
|
+
const opensObject = rest!.trim().endsWith("{");
|
|
128
|
+
const type = opensObject ? "object" : rest!.replace(/;$/, "").trim();
|
|
129
|
+
|
|
130
|
+
current.fields.push({ path: `${path}${optional ? "?" : ""}`, type, description: SchemaDocs.commentText(comment) });
|
|
131
|
+
comment = [];
|
|
125
132
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
133
|
+
if (opensObject) {
|
|
134
|
+
parents.push(name!.replace(/"/g, ""));
|
|
135
|
+
depth++;
|
|
136
|
+
}
|
|
137
|
+
continue;
|
|
129
138
|
}
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
139
|
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
const opens = (trimmed.match(/{/g) ?? []).length;
|
|
141
|
+
const closes = (trimmed.match(/}/g) ?? []).length;
|
|
135
142
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
143
|
+
for (let index = 0; index < closes - opens; index++) {
|
|
144
|
+
if (parents.length >= depth - 1 && parents.length > 0) parents.pop();
|
|
145
|
+
}
|
|
139
146
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
depth += opens - closes;
|
|
148
|
+
if (depth <= 0) {
|
|
149
|
+
depth = 0;
|
|
150
|
+
parents.length = 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
comment = [];
|
|
144
154
|
}
|
|
145
155
|
|
|
146
|
-
|
|
156
|
+
return types;
|
|
147
157
|
}
|
|
148
158
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
export function schemaTypes(sources: readonly string[]): TypeDoc[][] {
|
|
154
|
-
const generics = new Set<string>();
|
|
155
|
-
const parsed = sources.map((source) => parse(source, generics));
|
|
159
|
+
/** Every type in each schema file, in the order given. Every file is read first, so a type is known to take parameters wherever it is used. */
|
|
160
|
+
static read(sources: readonly string[]): TypeDoc[][] {
|
|
161
|
+
const generics = new Set<string>();
|
|
162
|
+
const parsed = sources.map((source) => SchemaDocs.parse(source, generics));
|
|
156
163
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
for (const types of parsed) {
|
|
165
|
+
for (const type of types) {
|
|
166
|
+
if (type.definition) type.definition = SchemaDocs.withoutGenerics(type.definition, generics);
|
|
167
|
+
if (type.includes) type.includes = type.includes.map((name) => SchemaDocs.withoutGenerics(name, generics));
|
|
168
|
+
for (const field of type.fields) field.type = SchemaDocs.withoutGenerics(field.type, generics);
|
|
169
|
+
}
|
|
162
170
|
}
|
|
163
|
-
}
|
|
164
171
|
|
|
165
|
-
|
|
172
|
+
return parsed;
|
|
173
|
+
}
|
|
166
174
|
}
|
package/src/core/secrets.ts
CHANGED
|
@@ -3,111 +3,205 @@
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import
|
|
6
|
+
import { parseEnv } from "node:util";
|
|
7
|
+
import type { Network } from "./define";
|
|
8
|
+
import type { Console, Device, IP, Secret } from "../schema";
|
|
9
|
+
|
|
10
|
+
/** How a device is logged in to: its management address and SSH port, its connection user and that user's password, and its console. */
|
|
11
|
+
export type DeviceLogin = {
|
|
12
|
+
/** Absent for a device reached only through its console. */
|
|
13
|
+
host?: IP;
|
|
14
|
+
port: number;
|
|
15
|
+
user: string;
|
|
16
|
+
/** The value, not a reference. Never print it. */
|
|
17
|
+
password: string;
|
|
18
|
+
console?: Console;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class Secrets {
|
|
22
|
+
/** The directory of the config file. `.env` and secret files are found from here. */
|
|
23
|
+
private static root = process.cwd();
|
|
24
|
+
|
|
25
|
+
private static envFile: Record<string, string> | undefined;
|
|
26
|
+
private static readonly fromOnePassword = new Map<string, string>();
|
|
27
|
+
/** Every value read in this run, so `scrub` can hide it. */
|
|
28
|
+
private static readonly revealed = new Set<string>();
|
|
29
|
+
|
|
30
|
+
/** What `scrub` puts where a secret was. */
|
|
31
|
+
static readonly REDACTED = "<redacted>";
|
|
32
|
+
|
|
33
|
+
/** A value in `.env` shorter than this is not hidden: `.env` can hold settings that are not secrets, and hiding a short one would garble the text. */
|
|
34
|
+
private static readonly SHORTEST_DOTENV_VALUE = 6;
|
|
35
|
+
|
|
36
|
+
/** A marker anywhere in a command. Its group is the reference. */
|
|
37
|
+
static readonly MARKER = /<secret:([A-Za-z0-9_./:-]+)>/g;
|
|
38
|
+
|
|
39
|
+
/** Find `.env` and secret files from this directory, the config file's, instead of the working directory. */
|
|
40
|
+
static useRoot(directory: string): void {
|
|
41
|
+
Secrets.root = directory;
|
|
42
|
+
Secrets.envFile = undefined;
|
|
43
|
+
}
|
|
7
44
|
|
|
8
|
-
/**
|
|
9
|
-
|
|
45
|
+
/** `.env` beside the config, read once. Node and Bun read it the same way they load `.env` themselves: a `#` starts a comment unless the value is quoted. */
|
|
46
|
+
private static dotenv(): Record<string, string> {
|
|
47
|
+
if (Secrets.envFile) return Secrets.envFile;
|
|
10
48
|
|
|
11
|
-
|
|
12
|
-
const
|
|
49
|
+
const path = join(Secrets.root, ".env");
|
|
50
|
+
const text = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
13
51
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
52
|
+
Secrets.refuseCutValues(text);
|
|
53
|
+
Secrets.envFile = parseEnv(text) as Record<string, string>;
|
|
54
|
+
return Secrets.envFile;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** An unquoted value is cut at a `#`. A `#` after a space is a comment, but one inside the value, as in `abc#def`, would cut a password short and send that to a device, so it stops the run and asks for quotes. */
|
|
58
|
+
private static refuseCutValues(text: string): void {
|
|
59
|
+
for (const line of text.split(/\r?\n/)) {
|
|
60
|
+
const [, name, value] = /^\s*(?:export\s+)?([\w.-]+)\s*=\s*(.*)$/.exec(line) ?? [];
|
|
61
|
+
if (!name || !value || /^['"`]/.test(value) || !/\S#/.test(value)) continue;
|
|
62
|
+
|
|
63
|
+
throw new Error(`.env: the value of ${name} has a # in it, which starts a comment there; put the value in quotes`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Read a 1Password reference with the 1Password CLI, `op`, once per run. */
|
|
68
|
+
private static readOnePassword(name: string, reference: string): string {
|
|
69
|
+
const cached = Secrets.fromOnePassword.get(reference);
|
|
70
|
+
if (cached !== undefined) return cached;
|
|
19
71
|
|
|
20
|
-
|
|
21
|
-
function dotenv(): Record<string, string> {
|
|
22
|
-
if (envFile) return envFile;
|
|
72
|
+
const result = spawnSync("op", ["read", "--no-newline", reference], { encoding: "utf8" });
|
|
23
73
|
|
|
24
|
-
|
|
25
|
-
const path = join(root, ".env");
|
|
26
|
-
if (!existsSync(path)) return envFile;
|
|
74
|
+
if (result.error) throw new Error(`secret ${name} is a 1Password reference, and the 1Password CLI, op, is not installed`);
|
|
27
75
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
76
|
+
if (result.status !== 0) {
|
|
77
|
+
const reason = result.stderr.trim() || `op exited with ${result.status}`;
|
|
78
|
+
throw new Error(`secret ${name}: 1Password could not read ${reference}: ${reason}`);
|
|
79
|
+
}
|
|
31
80
|
|
|
32
|
-
const
|
|
33
|
-
|
|
81
|
+
const value = result.stdout;
|
|
82
|
+
Secrets.fromOnePassword.set(reference, value);
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
34
85
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
envFile[trimmed.slice(0, separator).trim()] = unquoted;
|
|
86
|
+
static isSecret(value: unknown): value is Secret {
|
|
87
|
+
return typeof value === "object" && value !== null && ("secret" in value || "secret_file" in value);
|
|
38
88
|
}
|
|
39
89
|
|
|
40
|
-
|
|
41
|
-
|
|
90
|
+
/** How a reference is written inside a marker: a variable's name, or `file:` and a path. */
|
|
91
|
+
static referenceOf(secret: Secret): string {
|
|
92
|
+
return "secret" in secret ? secret.secret : `file:${secret.secret_file}`;
|
|
93
|
+
}
|
|
42
94
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (cached !== undefined) return cached;
|
|
95
|
+
/** Where a reference's value comes from, such as `.env` or `1Password, through .env`, without reading it. Undefined when it is set nowhere. */
|
|
96
|
+
static sourceOf(reference: string): string | undefined {
|
|
97
|
+
if (reference.startsWith("file:")) return existsSync(join(Secrets.root, reference.slice("file:".length))) ? "a file" : undefined;
|
|
47
98
|
|
|
48
|
-
|
|
99
|
+
const fromFile = Secrets.dotenv()[reference];
|
|
100
|
+
const value = process.env[reference] || fromFile;
|
|
101
|
+
if (!value) return undefined;
|
|
49
102
|
|
|
50
|
-
|
|
103
|
+
// Bun loads `.env` into the environment when it starts in the same directory, so the same value in both came from `.env`.
|
|
104
|
+
const where = process.env[reference] && process.env[reference] !== fromFile ? "the environment" : ".env";
|
|
51
105
|
|
|
52
|
-
|
|
53
|
-
const reason = result.stderr.trim() || `op exited with ${result.status}`;
|
|
54
|
-
throw new Error(`secret ${name}: 1Password could not read ${reference}: ${reason}`);
|
|
106
|
+
return value.startsWith("op://") ? `1Password, through ${where}` : where;
|
|
55
107
|
}
|
|
56
108
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
109
|
+
/** The value behind a reference, as `referenceOf` writes it: one marker's reference, for a platform that takes a secret interactively. */
|
|
110
|
+
static valueOf(reference: string): string {
|
|
111
|
+
if (reference.startsWith("file:")) {
|
|
112
|
+
const path = reference.slice("file:".length);
|
|
113
|
+
const fullPath = join(Secrets.root, path);
|
|
114
|
+
if (!existsSync(fullPath)) throw new Error(`secret file ${path} does not exist`);
|
|
61
115
|
|
|
62
|
-
|
|
63
|
-
|
|
116
|
+
const value = readFileSync(fullPath, "utf8").trimEnd();
|
|
117
|
+
if (!value) throw new Error(`secret file ${path} is empty`);
|
|
64
118
|
|
|
65
|
-
|
|
66
|
-
|
|
119
|
+
Secrets.revealed.add(value);
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
67
122
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
123
|
+
// `.env` is read first even when the environment has the value, so a value cut at a `#` is caught under Bun too, which loads `.env` itself.
|
|
124
|
+
const fromFile = Secrets.dotenv()[reference];
|
|
125
|
+
const value = process.env[reference] || fromFile;
|
|
126
|
+
if (!value) throw new Error(`secret ${reference} is not set in the environment or in .env`);
|
|
71
127
|
|
|
72
|
-
|
|
73
|
-
|
|
128
|
+
const revealed = value.startsWith("op://") ? Secrets.readOnePassword(reference, value) : value;
|
|
129
|
+
Secrets.revealed.add(revealed);
|
|
130
|
+
return revealed;
|
|
131
|
+
}
|
|
74
132
|
|
|
75
|
-
|
|
76
|
-
|
|
133
|
+
/** The value of a secret, or of a variable named directly: from the environment, `.env`, 1Password or a file. */
|
|
134
|
+
static reveal(secret: Secret | string): string {
|
|
135
|
+
return Secrets.valueOf(typeof secret === "string" ? secret : Secrets.referenceOf(secret));
|
|
136
|
+
}
|
|
77
137
|
|
|
78
|
-
|
|
79
|
-
|
|
138
|
+
/** Every secret reference in a value, however deep, such as a network or one device, as `referenceOf` writes it. */
|
|
139
|
+
static referencesIn(value: unknown, found = new Set<string>()): Set<string> {
|
|
140
|
+
if (Secrets.isSecret(value)) found.add(Secrets.referenceOf(value));
|
|
141
|
+
else if (Array.isArray(value)) for (const item of value) Secrets.referencesIn(item, found);
|
|
142
|
+
else if (typeof value === "object" && value !== null) for (const item of Object.values(value)) Secrets.referencesIn(item, found);
|
|
80
143
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (reference.startsWith("file:")) {
|
|
84
|
-
const path = reference.slice("file:".length);
|
|
85
|
-
const fullPath = join(root, path);
|
|
86
|
-
if (!existsSync(fullPath)) throw new Error(`secret file ${path} does not exist`);
|
|
144
|
+
return found;
|
|
145
|
+
}
|
|
87
146
|
|
|
88
|
-
|
|
89
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Text with every secret value Circuit knows replaced by `<redacted>`, before it is printed. Devices echo what they are sent, passwords too.
|
|
149
|
+
*
|
|
150
|
+
* Hidden are: each value in `.env` of at least six characters, each value read in this run (1Password reads and secret files among them), each secret of `network` that the environment, `.env` or a file holds, and `extra`. A 1Password reference not read in this run is not read for this.
|
|
151
|
+
*/
|
|
152
|
+
static scrub(text: string, options: { network?: Network; extra?: readonly string[] } = {}): string {
|
|
153
|
+
const values = new Set<string>(options.extra ?? []);
|
|
90
154
|
|
|
91
|
-
|
|
92
|
-
|
|
155
|
+
for (const value of Object.values(Secrets.dotenv())) {
|
|
156
|
+
if (value.length >= Secrets.SHORTEST_DOTENV_VALUE && !value.startsWith("op://")) values.add(value);
|
|
157
|
+
}
|
|
93
158
|
|
|
94
|
-
|
|
95
|
-
if (!value) throw new Error(`secret ${reference} is not set in the environment or in .env`);
|
|
159
|
+
for (const value of Secrets.revealed) values.add(value);
|
|
96
160
|
|
|
97
|
-
|
|
98
|
-
|
|
161
|
+
for (const reference of Secrets.referencesIn(options.network)) {
|
|
162
|
+
const value = Secrets.storedValueOf(reference);
|
|
163
|
+
if (value) values.add(value);
|
|
164
|
+
}
|
|
99
165
|
|
|
100
|
-
|
|
101
|
-
|
|
166
|
+
// Longest first, so a value that holds another is hidden whole.
|
|
167
|
+
const ordered = [...values].filter(Boolean).sort((a, b) => b.length - a.length);
|
|
168
|
+
return ordered.reduce((out, value) => out.split(value).join(Secrets.REDACTED), text);
|
|
169
|
+
}
|
|
102
170
|
|
|
103
|
-
/**
|
|
104
|
-
|
|
171
|
+
/** The value behind a reference when it can be had without asking anyone: a 1Password reference only once read in this run. Undefined otherwise. */
|
|
172
|
+
private static storedValueOf(reference: string): string | undefined {
|
|
173
|
+
if (reference.startsWith("file:")) {
|
|
174
|
+
const path = join(Secrets.root, reference.slice("file:".length));
|
|
175
|
+
return existsSync(path) ? readFileSync(path, "utf8").trimEnd() : undefined;
|
|
176
|
+
}
|
|
105
177
|
|
|
106
|
-
|
|
107
|
-
|
|
178
|
+
const value = process.env[reference] || Secrets.dotenv()[reference];
|
|
179
|
+
if (!value?.startsWith("op://")) return value;
|
|
108
180
|
|
|
109
|
-
|
|
110
|
-
|
|
181
|
+
return Secrets.fromOnePassword.get(value);
|
|
182
|
+
}
|
|
111
183
|
|
|
112
|
-
/**
|
|
113
|
-
|
|
184
|
+
/** How Circuit logs in to a device: its address, port, connection user, that user's password revealed, and its console. */
|
|
185
|
+
static loginOf(device: Device): DeviceLogin {
|
|
186
|
+
const user = device.users?.[device.connection.user];
|
|
187
|
+
if (!user) throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
...(device.connection.host ? { host: device.connection.host } : {}),
|
|
191
|
+
port: device.connection.port ?? 22,
|
|
192
|
+
user: device.connection.user,
|
|
193
|
+
password: Secrets.reveal(user.password),
|
|
194
|
+
...(device.connection.console ? { console: device.connection.console } : {}),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** How a secret appears in rendered config. Replaced with its value only when sent to a device. */
|
|
199
|
+
static marker(secret: Secret): string {
|
|
200
|
+
return `<secret:${Secrets.referenceOf(secret)}>`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Replace every marker in a command with its value. */
|
|
204
|
+
static substitute(text: string): string {
|
|
205
|
+
return text.replace(Secrets.MARKER, (_, reference: string) => Secrets.valueOf(reference));
|
|
206
|
+
}
|
|
207
|
+
}
|