@takodotid/circuit 0.34.2 → 0.35.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.
Files changed (240) hide show
  1. package/README.md +13 -14
  2. package/dist/chunks/index-20kcjxjg.js +174 -0
  3. package/dist/chunks/index-2r4bbhac.js +977 -0
  4. package/dist/chunks/index-n7w3tg5y.js +149 -0
  5. package/dist/chunks/new-w3zys5y4.js +172 -0
  6. package/dist/chunks/server-gzsf5me5.js +259 -0
  7. package/dist/cli/index.js +11347 -9018
  8. package/dist/helpers/index.js +594 -0
  9. package/dist/index.js +9 -28
  10. package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
  11. package/dist/types/core/address.d.ts +44 -0
  12. package/dist/types/core/cabling.d.ts +41 -0
  13. package/dist/types/core/cross-checks.d.ts +28 -0
  14. package/dist/types/core/define.d.ts +25 -3
  15. package/dist/types/core/project-files.d.ts +12 -0
  16. package/dist/types/core/schema-docs.d.ts +13 -4
  17. package/dist/types/core/secrets.d.ts +62 -21
  18. package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
  19. package/dist/types/core/utils.d.ts +8 -0
  20. package/dist/types/core/validator.d.ts +19 -0
  21. package/dist/types/helpers/bgp.d.ts +97 -0
  22. package/dist/types/helpers/checks.d.ts +32 -0
  23. package/dist/types/helpers/exchange.d.ts +49 -0
  24. package/dist/types/helpers/filters.d.ts +51 -0
  25. package/dist/types/helpers/index.d.ts +10 -0
  26. package/dist/types/helpers/route-server.d.ts +52 -0
  27. package/dist/types/index.d.ts +5 -3
  28. package/dist/types/integrations/librenms.d.ts +60 -0
  29. package/dist/types/integrations/netbox.d.ts +89 -0
  30. package/dist/types/integrations/peeringdb.d.ts +24 -0
  31. package/dist/types/integrations/prometheus.d.ts +24 -0
  32. package/dist/types/integrations/registries.d.ts +8 -0
  33. package/dist/types/platforms/bgp-wait.d.ts +18 -0
  34. package/dist/types/platforms/catalog.d.ts +109 -0
  35. package/dist/types/platforms/derive.d.ts +17 -0
  36. package/dist/types/platforms/fortios/firewall.d.ts +45 -0
  37. package/dist/types/platforms/fortios/index.d.ts +134 -0
  38. package/dist/types/platforms/fortios/names.d.ts +52 -0
  39. package/dist/types/platforms/fortios/plan.d.ts +104 -0
  40. package/dist/types/platforms/fortios/render.d.ts +75 -0
  41. package/dist/types/platforms/fortios/routing.d.ts +56 -0
  42. package/dist/types/platforms/fortios/tree.d.ts +37 -0
  43. package/dist/types/platforms/index.d.ts +13 -0
  44. package/dist/types/platforms/junos/bgp.d.ts +44 -0
  45. package/dist/types/platforms/junos/index.d.ts +114 -0
  46. package/dist/types/platforms/junos/render.d.ts +154 -0
  47. package/dist/types/platforms/line-diff.d.ts +67 -0
  48. package/dist/types/platforms/platform.d.ts +120 -0
  49. package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
  50. package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
  51. package/dist/types/platforms/routeros/export.d.ts +59 -0
  52. package/dist/types/platforms/routeros/index.d.ts +96 -0
  53. package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
  54. package/dist/types/platforms/routeros/plan.d.ts +54 -0
  55. package/dist/types/platforms/routeros/render.d.ts +240 -0
  56. package/dist/types/platforms/vrp/index.d.ts +83 -0
  57. package/dist/types/platforms/vrp/render.d.ts +96 -0
  58. package/dist/types/schema/common.d.ts +12 -6
  59. package/dist/types/schema/index.d.ts +1 -0
  60. package/dist/types/schema/interfaces.d.ts +14 -6
  61. package/dist/types/schema/policy.d.ts +18 -10
  62. package/dist/types/schema/routing.d.ts +14 -1
  63. package/dist/types/schema/system.d.ts +16 -6
  64. package/dist/types/schema/time-zones.d.ts +2 -0
  65. package/dist/types/transport/console.d.ts +79 -23
  66. package/dist/types/transport/path-check.d.ts +32 -0
  67. package/dist/types/transport/ssh.d.ts +17 -15
  68. package/package.json +17 -11
  69. package/src/cli/graph-format.ts +91 -0
  70. package/src/cli/index.ts +1074 -484
  71. package/src/cli/new.ts +208 -175
  72. package/src/cli/rename.ts +97 -91
  73. package/src/core/address-space.ts +320 -0
  74. package/src/core/address.ts +202 -0
  75. package/src/core/cabling.ts +154 -0
  76. package/src/core/cross-checks.ts +364 -0
  77. package/src/core/define.ts +26 -3
  78. package/src/core/project-files.ts +35 -0
  79. package/src/core/schema-docs.ts +138 -130
  80. package/src/core/secrets.ts +170 -76
  81. package/src/core/topology.ts +656 -0
  82. package/src/core/utils.ts +37 -0
  83. package/src/core/validator.ts +407 -0
  84. package/src/helpers/bgp.ts +301 -0
  85. package/src/helpers/checks.ts +178 -0
  86. package/src/helpers/exchange.ts +117 -0
  87. package/src/helpers/filters.ts +107 -0
  88. package/src/helpers/index.ts +12 -0
  89. package/src/helpers/route-server.ts +212 -0
  90. package/src/index.ts +6 -3
  91. package/src/integrations/librenms.ts +291 -0
  92. package/src/integrations/netbox.ts +620 -0
  93. package/src/integrations/peeringdb.ts +173 -0
  94. package/src/integrations/prometheus.ts +90 -0
  95. package/src/integrations/registries.ts +25 -0
  96. package/src/platforms/bgp-wait.ts +41 -0
  97. package/src/platforms/catalog.ts +69 -0
  98. package/src/platforms/derive.ts +117 -0
  99. package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
  100. package/src/platforms/fortios/firewall.ts +377 -0
  101. package/src/platforms/fortios/index.ts +1060 -0
  102. package/src/platforms/fortios/names.ts +130 -0
  103. package/src/platforms/fortios/plan.ts +929 -0
  104. package/src/platforms/fortios/render.ts +779 -0
  105. package/src/platforms/fortios/routing.ts +476 -0
  106. package/src/platforms/fortios/tree.ts +129 -0
  107. package/src/platforms/index.ts +36 -0
  108. package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
  109. package/src/platforms/junos/bgp.ts +293 -0
  110. package/src/platforms/junos/index.ts +1006 -0
  111. package/src/platforms/junos/render.ts +967 -0
  112. package/src/platforms/line-diff.ts +244 -0
  113. package/src/platforms/platform.ts +122 -0
  114. package/src/platforms/raisecom-ros/TRAPS.md +40 -0
  115. package/src/platforms/raisecom-ros/index.ts +731 -0
  116. package/src/platforms/raisecom-ros/render.ts +328 -0
  117. package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
  118. package/src/platforms/routeros/export.ts +342 -0
  119. package/src/platforms/routeros/index.ts +636 -0
  120. package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
  121. package/src/platforms/routeros/plan.ts +444 -0
  122. package/src/platforms/routeros/render.ts +1483 -0
  123. package/src/platforms/vrp/TRAPS.md +45 -0
  124. package/src/platforms/vrp/index.ts +652 -0
  125. package/src/platforms/vrp/render.ts +615 -0
  126. package/src/schema/common.ts +15 -6
  127. package/src/schema/index.ts +1 -0
  128. package/src/schema/interfaces.ts +15 -5
  129. package/src/schema/policy.ts +22 -10
  130. package/src/schema/routing.ts +16 -1
  131. package/src/schema/system.ts +16 -6
  132. package/src/schema/time-zones.ts +482 -0
  133. package/src/studio/data.ts +172 -168
  134. package/src/studio/git.ts +95 -90
  135. package/src/studio/public/app.js +161 -150
  136. package/src/studio/public/compare.js +219 -212
  137. package/src/studio/public/diff.js +77 -75
  138. package/src/studio/public/dom.js +118 -108
  139. package/src/studio/public/evaluate.js +584 -545
  140. package/src/studio/public/export.js +120 -118
  141. package/src/studio/public/graph.js +1343 -1266
  142. package/src/studio/public/net.js +115 -109
  143. package/src/studio/public/tables.js +1135 -1083
  144. package/src/studio/public/tools.js +561 -529
  145. package/src/studio/server.ts +202 -196
  146. package/src/transport/console.ts +363 -271
  147. package/src/transport/path-check.ts +91 -0
  148. package/src/transport/ssh.ts +98 -94
  149. package/templates/colocation/checks.ts +11 -6
  150. package/templates/colocation/edge-01.ts +9 -9
  151. package/templates/colocation/routing.ts +2 -2
  152. package/templates/colocation/tenancy.ts +3 -3
  153. package/templates/colocation/tor-01.ts +3 -3
  154. package/templates/edge-router/checks.ts +4 -4
  155. package/templates/edge-router/edge-01.ts +5 -5
  156. package/templates/edge-router/routing.ts +2 -2
  157. package/templates/project/AGENTS.md +5 -1
  158. package/templates/single-site/checks.ts +2 -2
  159. package/dist/chunks/index-47debt16.js +0 -956
  160. package/dist/chunks/index-v8zgr7wm.js +0 -128
  161. package/dist/chunks/new-ny89s7y7.js +0 -166
  162. package/dist/chunks/server-tjbb3rqc.js +0 -251
  163. package/dist/presets/index.js +0 -394
  164. package/dist/types/adapters/devices/catalog.d.ts +0 -86
  165. package/dist/types/adapters/devices/derive.d.ts +0 -7
  166. package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
  167. package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
  168. package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
  169. package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
  170. package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
  171. package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
  172. package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
  173. package/dist/types/adapters/devices/index.d.ts +0 -3
  174. package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
  175. package/dist/types/adapters/devices/junos/index.d.ts +0 -42
  176. package/dist/types/adapters/devices/junos/render.d.ts +0 -52
  177. package/dist/types/adapters/devices/lines.d.ts +0 -56
  178. package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
  179. package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
  180. package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
  181. package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
  182. package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
  183. package/dist/types/adapters/devices/types.d.ts +0 -40
  184. package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
  185. package/dist/types/adapters/registries/index.d.ts +0 -3
  186. package/dist/types/adapters/registries/librenms.d.ts +0 -15
  187. package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
  188. package/dist/types/adapters/registries/prometheus.d.ts +0 -16
  189. package/dist/types/core/across.d.ts +0 -4
  190. package/dist/types/core/addr.d.ts +0 -34
  191. package/dist/types/core/files.d.ts +0 -6
  192. package/dist/types/core/merge.d.ts +0 -4
  193. package/dist/types/core/time.d.ts +0 -2
  194. package/dist/types/core/validate.d.ts +0 -2
  195. package/dist/types/presets/index.d.ts +0 -183
  196. package/src/adapters/devices/catalog.ts +0 -41
  197. package/src/adapters/devices/derive.ts +0 -59
  198. package/src/adapters/devices/fortios/firewall.ts +0 -362
  199. package/src/adapters/devices/fortios/index.ts +0 -923
  200. package/src/adapters/devices/fortios/names.ts +0 -115
  201. package/src/adapters/devices/fortios/plan.ts +0 -888
  202. package/src/adapters/devices/fortios/render.ts +0 -768
  203. package/src/adapters/devices/fortios/routing.ts +0 -444
  204. package/src/adapters/devices/fortios/tree.ts +0 -121
  205. package/src/adapters/devices/index.ts +0 -9
  206. package/src/adapters/devices/junos/bgp.ts +0 -269
  207. package/src/adapters/devices/junos/index.ts +0 -862
  208. package/src/adapters/devices/junos/render.ts +0 -872
  209. package/src/adapters/devices/lines.ts +0 -212
  210. package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -25
  211. package/src/adapters/devices/raisecom-ros/index.ts +0 -693
  212. package/src/adapters/devices/routeros/export.ts +0 -321
  213. package/src/adapters/devices/routeros/index.ts +0 -346
  214. package/src/adapters/devices/routeros/plan.ts +0 -398
  215. package/src/adapters/devices/routeros/render.ts +0 -1490
  216. package/src/adapters/devices/types.ts +0 -38
  217. package/src/adapters/devices/vrp/TRAPS.md +0 -26
  218. package/src/adapters/devices/vrp/index.ts +0 -865
  219. package/src/adapters/registries/index.ts +0 -19
  220. package/src/adapters/registries/librenms.ts +0 -92
  221. package/src/adapters/registries/peeringdb.ts +0 -169
  222. package/src/adapters/registries/prometheus.ts +0 -71
  223. package/src/cli/graph.ts +0 -89
  224. package/src/core/across.ts +0 -350
  225. package/src/core/addr.ts +0 -174
  226. package/src/core/addresses.ts +0 -316
  227. package/src/core/files.ts +0 -32
  228. package/src/core/graph.ts +0 -640
  229. package/src/core/merge.ts +0 -19
  230. package/src/core/time.ts +0 -15
  231. package/src/core/validate.ts +0 -348
  232. package/src/presets/index.ts +0 -579
  233. /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
  234. /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
  235. /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
  236. /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
  237. /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
  238. /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
  239. /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
  240. /package/src/{adapters/devices → platforms}/vrp/models.ts +0 -0
@@ -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
- /** The text of a JSDoc comment, without its markers, as one paragraph. */
18
- function commentText(lines: string[]): string {
19
- return lines
20
- .map((line) => line.replace(/^\s*\/\*\*\s?|\s*\*\/\s*$|^\s*\*\s?/g, "").trim())
21
- .filter(Boolean)
22
- .join(" ");
23
- }
24
-
25
- /** What a type parameter stands for, where it is not one of the device's names. */
26
- const PARAMETER_MEANING: Record<string, string> = { Pl: "platform", M: "model" };
27
-
28
- /** 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. */
29
- function withoutGenerics(type: string, generics: Set<string>): string {
30
- let result = type;
31
- while (/NoInfer<([^<>]*)>/.test(result)) {
32
- result = result.replace(/NoInfer<([^<>]*)>/g, (_, inner: string) => (inner.includes("|") ? `(${inner})` : inner));
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 (const name of generics) {
36
- result = result.replace(new RegExp(`\\b${name}<[^<>]*(?:<[^<>]*>[^<>]*)*>`, "g"), name);
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
- return result
40
- .replace(/\b(Pl|M|N|V|I|P|S|G|A|AS|U|F|C|L|K)\b/g, (parameter) => PARAMETER_MEANING[parameter] ?? "name")
41
- .replace(/\bname(?: \| name)+\b/g, "name")
42
- .replace(/\(name\)/g, "name");
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
- function parse(source: string, generics: Set<string>): TypeDoc[] {
46
- const lines = source.split("\n");
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
- depth = (text.match(/{/g) ?? []).length - (text.match(/}/g) ?? []).length;
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
- for (const line of lines) {
79
- const trimmed = line.trim();
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
- if (trimmed.startsWith("/**")) {
82
- comment = [trimmed];
83
- inComment = !trimmed.endsWith("*/");
84
- continue;
82
+ depth = (text.match(/{/g) ?? []).length - (text.match(/}/g) ?? []).length;
85
83
  }
86
84
 
87
- if (inComment) {
88
- comment.push(trimmed);
89
- if (trimmed.endsWith("*/")) inComment = false;
90
- continue;
91
- }
85
+ for (const line of lines) {
86
+ const trimmed = line.trim();
92
87
 
93
- // A declaration can span lines before its body opens or it ends.
94
- if (header !== undefined) {
95
- header += ` ${trimmed}`;
96
- if (trimmed.endsWith("{") || trimmed.endsWith(";")) finishHeader();
97
- continue;
98
- }
88
+ if (trimmed.startsWith("/**")) {
89
+ comment = [trimmed];
90
+ inComment = !trimmed.endsWith("*/");
91
+ continue;
92
+ }
99
93
 
100
- // A type the schema does not export is documented too, since exported types take its fields.
101
- const declaration = /^(?:export )?type (\w+)(<)?/.exec(trimmed);
102
- if (declaration && depth === 0) {
103
- if (declaration[2]) generics.add(declaration[1]!);
94
+ if (inComment) {
95
+ comment.push(trimmed);
96
+ if (trimmed.endsWith("*/")) inComment = false;
97
+ continue;
98
+ }
104
99
 
105
- current = { name: declaration[1]!, description: commentText(comment), fields: [] };
106
- types.push(current);
107
- comment = [];
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
- header = trimmed;
110
- if (trimmed.endsWith("{") || trimmed.endsWith(";")) finishHeader();
111
- continue;
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
- if (!current) continue;
112
+ current = { name: declaration[1]!, description: SchemaDocs.commentText(comment), fields: [] };
113
+ types.push(current);
114
+ comment = [];
115
115
 
116
- const field = /^(?:readonly\s+)?("?[\w-]+"?)(\?)?:\s*(.*)$/.exec(trimmed);
117
- if (field && depth >= 1) {
118
- const [, name, optional, rest] = field;
119
- const path = [...parents, name!.replace(/"/g, "")].join(".");
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
- current.fields.push({ path: `${path}${optional ? "?" : ""}`, type, description: commentText(comment) });
124
- comment = [];
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
- if (opensObject) {
127
- parents.push(name!.replace(/"/g, ""));
128
- depth++;
133
+ if (opensObject) {
134
+ parents.push(name!.replace(/"/g, ""));
135
+ depth++;
136
+ }
137
+ continue;
129
138
  }
130
- continue;
131
- }
132
139
 
133
- const opens = (trimmed.match(/{/g) ?? []).length;
134
- const closes = (trimmed.match(/}/g) ?? []).length;
140
+ const opens = (trimmed.match(/{/g) ?? []).length;
141
+ const closes = (trimmed.match(/}/g) ?? []).length;
135
142
 
136
- for (let index = 0; index < closes - opens; index++) {
137
- if (parents.length >= depth - 1 && parents.length > 0) parents.pop();
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
- depth += opens - closes;
141
- if (depth <= 0) {
142
- depth = 0;
143
- parents.length = 0;
147
+ depth += opens - closes;
148
+ if (depth <= 0) {
149
+ depth = 0;
150
+ parents.length = 0;
151
+ }
152
+
153
+ comment = [];
144
154
  }
145
155
 
146
- comment = [];
156
+ return types;
147
157
  }
148
158
 
149
- return types;
150
- }
151
-
152
- /** 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. */
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
- for (const types of parsed) {
158
- for (const type of types) {
159
- if (type.definition) type.definition = withoutGenerics(type.definition, generics);
160
- if (type.includes) type.includes = type.includes.map((name) => withoutGenerics(name, generics));
161
- for (const field of type.fields) field.type = withoutGenerics(field.type, generics);
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
- return parsed;
172
+ return parsed;
173
+ }
166
174
  }
@@ -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 type { Secret } from "../schema";
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
- /** The directory of the config file. `.env` and secret files are found from here. */
9
- let root = process.cwd();
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
- let envFile: Record<string, string> | undefined;
12
- const fromOnePassword = new Map<string, string>();
49
+ const path = join(Secrets.root, ".env");
50
+ const text = existsSync(path) ? readFileSync(path, "utf8") : "";
13
51
 
14
- /** Find `.env` and secret files from this directory, the config file's, instead of the working directory. */
15
- export function useRoot(directory: string): void {
16
- root = directory;
17
- envFile = undefined;
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
- /** `.env` beside the config, read once. */
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
- envFile = {};
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
- for (const line of readFileSync(path, "utf8").split("\n")) {
29
- const trimmed = line.trim();
30
- if (!trimmed || trimmed.startsWith("#")) continue;
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 separator = trimmed.indexOf("=");
33
- if (separator <= 0) continue;
81
+ const value = result.stdout;
82
+ Secrets.fromOnePassword.set(reference, value);
83
+ return value;
84
+ }
34
85
 
35
- const value = trimmed.slice(separator + 1).trim();
36
- const unquoted = /^(["']).*\1$/.test(value) ? value.slice(1, -1) : value;
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
- return envFile;
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
- /** Read a 1Password reference with the 1Password CLI, `op`, once per run. */
44
- function readOnePassword(name: string, reference: string): string {
45
- const cached = fromOnePassword.get(reference);
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
- const result = spawnSync("op", ["read", "--no-newline", reference], { encoding: "utf8" });
99
+ const fromFile = Secrets.dotenv()[reference];
100
+ const value = process.env[reference] || fromFile;
101
+ if (!value) return undefined;
49
102
 
50
- if (result.error) throw new Error(`secret ${name} is a 1Password reference, and the 1Password CLI, op, is not installed`);
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
- if (result.status !== 0) {
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
- const value = result.stdout;
58
- fromOnePassword.set(reference, value);
59
- return value;
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
- export const isSecret = (value: unknown): value is Secret =>
63
- typeof value === "object" && value !== null && ("secret" in value || "secret_file" in value);
116
+ const value = readFileSync(fullPath, "utf8").trimEnd();
117
+ if (!value) throw new Error(`secret file ${path} is empty`);
64
118
 
65
- /** How a reference is written inside a marker: a variable's name, or `file:` and a path. */
66
- export const referenceOf = (secret: Secret) => ("secret" in secret ? secret.secret : `file:${secret.secret_file}`);
119
+ Secrets.revealed.add(value);
120
+ return value;
121
+ }
67
122
 
68
- /** Where a reference's value comes from, such as `.env` or `1Password, through .env`, without reading it. Undefined when it is set nowhere. */
69
- export function sourceOf(reference: string): string | undefined {
70
- if (reference.startsWith("file:")) return existsSync(join(root, reference.slice("file:".length))) ? "a file" : undefined;
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
- const value = process.env[reference] || dotenv()[reference];
73
- if (!value) return undefined;
128
+ const revealed = value.startsWith("op://") ? Secrets.readOnePassword(reference, value) : value;
129
+ Secrets.revealed.add(revealed);
130
+ return revealed;
131
+ }
74
132
 
75
- // Bun loads `.env` into the environment when it starts in the same directory, so the same value in both came from `.env`.
76
- const where = process.env[reference] && process.env[reference] !== dotenv()[reference] ? "the environment" : ".env";
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
- return value.startsWith("op://") ? `1Password, through ${where}` : where;
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
- /** The value behind a reference, as `referenceOf` writes it. */
82
- function valueOf(reference: string): string {
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
- const value = readFileSync(fullPath, "utf8").trimEnd();
89
- if (!value) throw new Error(`secret file ${path} is empty`);
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
- return value;
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
- const value = process.env[reference] || dotenv()[reference];
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
- return value.startsWith("op://") ? readOnePassword(reference, value) : value;
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
- /** The value of a secret, or of a variable named directly. */
101
- export const resolve = (secret: Secret | string): string => valueOf(typeof secret === "string" ? secret : referenceOf(secret));
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
- /** How a secret appears in rendered config. Replaced with its value only when sent to a device. */
104
- export const marker = (secret: Secret) => `<secret:${referenceOf(secret)}>`;
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
- /** A marker anywhere in a command. Its group is the reference. */
107
- export const MARKER = /<secret:([A-Za-z0-9_./:-]+)>/g;
178
+ const value = process.env[reference] || Secrets.dotenv()[reference];
179
+ if (!value?.startsWith("op://")) return value;
108
180
 
109
- /** Replace every marker in a command with its value. */
110
- export const substitute = (text: string) => text.replace(MARKER, (_, reference: string) => valueOf(reference));
181
+ return Secrets.fromOnePassword.get(value);
182
+ }
111
183
 
112
- /** The value behind one marker's reference, for a platform that takes a secret interactively. */
113
- export const valueOfReference = valueOf;
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
+ }