@takodotid/circuit 0.43.1 → 1.0.0-rc.2

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 (256) hide show
  1. package/README.md +115 -37
  2. package/package.json +21 -58
  3. package/src/checks/across.ts +402 -0
  4. package/src/checks/built-in.ts +256 -0
  5. package/src/checks/device.ts +436 -0
  6. package/src/checks/index.ts +53 -0
  7. package/src/cli/advisories.ts +108 -0
  8. package/src/cli/app.ts +71 -0
  9. package/src/cli/editor-types.generated.ts +30 -0
  10. package/src/cli/editor-types.ts +69 -0
  11. package/src/cli/format.ts +124 -0
  12. package/src/cli/git-hooks.ts +79 -0
  13. package/src/cli/index.ts +350 -1170
  14. package/src/cli/migrate.ts +554 -0
  15. package/src/cli/project-commands.ts +656 -0
  16. package/src/cli/releases.ts +374 -0
  17. package/src/cli/remember.ts +339 -0
  18. package/src/cli/rename.ts +27 -33
  19. package/src/cli/secrets-password.ts +60 -0
  20. package/src/cli/secrets.ts +258 -0
  21. package/src/cli/setup.ts +300 -0
  22. package/src/cli/signing.ts +23 -0
  23. package/src/cli/text-editor.ts +268 -0
  24. package/src/cli/ui.ts +298 -0
  25. package/src/cli/version.ts +2 -0
  26. package/src/cli/versions.ts +158 -0
  27. package/src/config/address.ts +102 -0
  28. package/src/config/circuit.d.ts +30 -0
  29. package/src/config/common.ts +65 -0
  30. package/src/config/communities.ts +479 -0
  31. package/src/config/define.ts +684 -0
  32. package/src/config/device.ts +164 -0
  33. package/src/config/firewall.ts +150 -0
  34. package/src/config/index.ts +22 -0
  35. package/src/{schema → config}/interfaces.ts +121 -85
  36. package/src/config/keys.ts +434 -0
  37. package/src/config/management.ts +61 -0
  38. package/src/config/merge.ts +335 -0
  39. package/src/config/models.ts +70 -0
  40. package/src/config/monitoring.ts +64 -0
  41. package/src/config/network.ts +180 -0
  42. package/src/config/presets.ts +149 -0
  43. package/src/config/routing.ts +401 -0
  44. package/src/config/services.ts +51 -0
  45. package/src/config/switching.ts +84 -0
  46. package/src/config/system.ts +103 -0
  47. package/src/{schema → config}/time-zones.ts +1 -1
  48. package/src/core/address.ts +1 -1
  49. package/src/core/cabling.ts +8 -43
  50. package/src/core/graph.ts +40 -0
  51. package/src/{integrations → core}/registries.ts +5 -3
  52. package/src/core/secrets-file.ts +539 -0
  53. package/src/core/secrets.ts +83 -64
  54. package/src/core/topology.ts +103 -146
  55. package/src/core/utils.ts +12 -1
  56. package/src/model.ts +83 -0
  57. package/src/platforms/derive.ts +64 -37
  58. package/src/platforms/finding.ts +9 -0
  59. package/src/platforms/fortios/firewall.ts +76 -64
  60. package/src/platforms/fortios/index.ts +189 -125
  61. package/src/platforms/fortios/names.ts +51 -18
  62. package/src/platforms/fortios/render.ts +69 -62
  63. package/src/platforms/fortios/routing.ts +89 -58
  64. package/src/platforms/fortios/vdoms.ts +47 -16
  65. package/src/platforms/junos/TRAPS.md +2 -2
  66. package/src/platforms/junos/bgp.ts +63 -42
  67. package/src/platforms/junos/index.ts +122 -89
  68. package/src/platforms/junos/render.ts +139 -104
  69. package/src/platforms/platform.ts +10 -8
  70. package/src/platforms/raisecom-ros/TRAPS.md +1 -1
  71. package/src/platforms/raisecom-ros/index.ts +89 -70
  72. package/src/platforms/raisecom-ros/render.ts +49 -45
  73. package/src/platforms/routeros/TRAPS.md +3 -3
  74. package/src/platforms/routeros/index.ts +121 -60
  75. package/src/platforms/routeros/render.ts +214 -195
  76. package/src/platforms/vrp/TRAPS.md +1 -1
  77. package/src/platforms/vrp/index.ts +74 -50
  78. package/src/platforms/vrp/render.ts +67 -56
  79. package/src/project/project.ts +725 -0
  80. package/src/studio/app/env.d.ts +9 -0
  81. package/src/studio/app/index.html +18 -0
  82. package/src/studio/app/src/App.vue +95 -0
  83. package/src/studio/app/src/components/AddressesView.vue +38 -0
  84. package/src/studio/app/src/components/CablesView.vue +29 -0
  85. package/src/studio/app/src/components/CompareView.vue +160 -0
  86. package/src/studio/app/src/components/DataTable.vue +49 -0
  87. package/src/studio/app/src/components/DeviceView.vue +237 -0
  88. package/src/studio/app/src/components/DevicesView.vue +32 -0
  89. package/src/studio/app/src/components/Faceplate.vue +75 -0
  90. package/src/studio/app/src/components/GraphView.vue +282 -0
  91. package/src/studio/app/src/components/LineDiff.vue +24 -0
  92. package/src/studio/app/src/components/ProblemsView.vue +42 -0
  93. package/src/studio/app/src/components/SearchView.vue +72 -0
  94. package/src/studio/app/src/components/SecretsView.vue +112 -0
  95. package/src/studio/app/src/components/SessionsView.vue +35 -0
  96. package/src/studio/app/src/components/Settings.vue +95 -0
  97. package/src/studio/app/src/components/ToolsView.vue +118 -0
  98. package/src/studio/app/src/components/VlansView.vue +50 -0
  99. package/src/studio/app/src/lib/csv.ts +19 -0
  100. package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
  101. package/src/studio/app/src/lib/format.ts +38 -0
  102. package/src/studio/app/src/lib/net.ts +68 -0
  103. package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
  104. package/src/studio/app/src/lib/store.ts +71 -0
  105. package/src/studio/app/src/lib/tools.ts +217 -0
  106. package/src/studio/app/src/main.ts +9 -0
  107. package/src/studio/{public → app/src}/style.css +14 -4
  108. package/src/studio/app/tsconfig.json +17 -0
  109. package/src/studio/app/vite.config.ts +16 -0
  110. package/src/studio/assets.generated.ts +3 -0
  111. package/src/studio/data.ts +96 -188
  112. package/src/studio/git.ts +27 -43
  113. package/src/studio/server.ts +196 -107
  114. package/src/studio/types.ts +141 -0
  115. package/src/transport/console.ts +3 -3
  116. package/src/transport/ssh.ts +1 -1
  117. package/dist/chunks/index-3b343xva.js +0 -177
  118. package/dist/chunks/index-6wb41q67.js +0 -62
  119. package/dist/chunks/index-n7w3tg5y.js +0 -149
  120. package/dist/chunks/index-yzw86rc8.js +0 -921
  121. package/dist/chunks/new-w3zys5y4.js +0 -172
  122. package/dist/chunks/server-cxr9ynxc.js +0 -259
  123. package/dist/cli/index.js +0 -12922
  124. package/dist/helpers/index.js +0 -881
  125. package/dist/index.js +0 -28
  126. package/dist/types/core/address-space.d.ts +0 -62
  127. package/dist/types/core/address.d.ts +0 -44
  128. package/dist/types/core/cabling.d.ts +0 -41
  129. package/dist/types/core/cross-checks.d.ts +0 -28
  130. package/dist/types/core/define.d.ts +0 -79
  131. package/dist/types/core/project-files.d.ts +0 -12
  132. package/dist/types/core/schema-docs.d.ts +0 -25
  133. package/dist/types/core/secrets.d.ts +0 -62
  134. package/dist/types/core/topology.d.ts +0 -96
  135. package/dist/types/core/utils.d.ts +0 -10
  136. package/dist/types/core/validator.d.ts +0 -23
  137. package/dist/types/helpers/bgp.d.ts +0 -195
  138. package/dist/types/helpers/cables.d.ts +0 -69
  139. package/dist/types/helpers/checks.d.ts +0 -36
  140. package/dist/types/helpers/exchange.d.ts +0 -55
  141. package/dist/types/helpers/filters.d.ts +0 -51
  142. package/dist/types/helpers/index.d.ts +0 -14
  143. package/dist/types/helpers/route-server.d.ts +0 -52
  144. package/dist/types/helpers/upstreams.d.ts +0 -192
  145. package/dist/types/index.d.ts +0 -10
  146. package/dist/types/integrations/librenms.d.ts +0 -60
  147. package/dist/types/integrations/netbox.d.ts +0 -89
  148. package/dist/types/integrations/peeringdb.d.ts +0 -24
  149. package/dist/types/integrations/prometheus.d.ts +0 -24
  150. package/dist/types/integrations/registries.d.ts +0 -8
  151. package/dist/types/platforms/bgp-wait.d.ts +0 -24
  152. package/dist/types/platforms/catalog.d.ts +0 -109
  153. package/dist/types/platforms/derive.d.ts +0 -17
  154. package/dist/types/platforms/fortios/firewall.d.ts +0 -47
  155. package/dist/types/platforms/fortios/index.d.ts +0 -140
  156. package/dist/types/platforms/fortios/models.d.ts +0 -14
  157. package/dist/types/platforms/fortios/names.d.ts +0 -52
  158. package/dist/types/platforms/fortios/plan.d.ts +0 -114
  159. package/dist/types/platforms/fortios/render.d.ts +0 -77
  160. package/dist/types/platforms/fortios/routing.d.ts +0 -56
  161. package/dist/types/platforms/fortios/tree.d.ts +0 -37
  162. package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
  163. package/dist/types/platforms/index.d.ts +0 -13
  164. package/dist/types/platforms/junos/bgp.d.ts +0 -44
  165. package/dist/types/platforms/junos/index.d.ts +0 -114
  166. package/dist/types/platforms/junos/models.d.ts +0 -14
  167. package/dist/types/platforms/junos/render.d.ts +0 -154
  168. package/dist/types/platforms/line-diff.d.ts +0 -67
  169. package/dist/types/platforms/platform.d.ts +0 -135
  170. package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
  171. package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
  172. package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
  173. package/dist/types/platforms/routeros/export.d.ts +0 -59
  174. package/dist/types/platforms/routeros/index.d.ts +0 -116
  175. package/dist/types/platforms/routeros/models.d.ts +0 -18
  176. package/dist/types/platforms/routeros/plan.d.ts +0 -58
  177. package/dist/types/platforms/routeros/render.d.ts +0 -250
  178. package/dist/types/platforms/vrp/index.d.ts +0 -83
  179. package/dist/types/platforms/vrp/models.d.ts +0 -12
  180. package/dist/types/platforms/vrp/render.d.ts +0 -96
  181. package/dist/types/schema/common.d.ts +0 -30
  182. package/dist/types/schema/device.d.ts +0 -126
  183. package/dist/types/schema/firewall.d.ts +0 -131
  184. package/dist/types/schema/index.d.ts +0 -8
  185. package/dist/types/schema/interfaces.d.ts +0 -205
  186. package/dist/types/schema/policy.d.ts +0 -107
  187. package/dist/types/schema/routing.d.ts +0 -161
  188. package/dist/types/schema/system.d.ts +0 -198
  189. package/dist/types/schema/time-zones.d.ts +0 -2
  190. package/dist/types/transport/console.d.ts +0 -115
  191. package/dist/types/transport/path-check.d.ts +0 -32
  192. package/dist/types/transport/ssh.d.ts +0 -27
  193. package/src/cli/graph-format.ts +0 -91
  194. package/src/cli/new.ts +0 -234
  195. package/src/core/address-space.ts +0 -320
  196. package/src/core/cross-checks.ts +0 -369
  197. package/src/core/define.ts +0 -124
  198. package/src/core/project-files.ts +0 -35
  199. package/src/core/schema-docs.ts +0 -174
  200. package/src/core/validator.ts +0 -508
  201. package/src/helpers/bgp.ts +0 -473
  202. package/src/helpers/cables.ts +0 -74
  203. package/src/helpers/checks.ts +0 -201
  204. package/src/helpers/exchange.ts +0 -123
  205. package/src/helpers/filters.ts +0 -107
  206. package/src/helpers/index.ts +0 -25
  207. package/src/helpers/route-server.ts +0 -212
  208. package/src/helpers/upstreams.ts +0 -369
  209. package/src/index.ts +0 -13
  210. package/src/integrations/librenms.ts +0 -291
  211. package/src/integrations/netbox.ts +0 -620
  212. package/src/integrations/peeringdb.ts +0 -173
  213. package/src/integrations/prometheus.ts +0 -90
  214. package/src/schema/common.ts +0 -41
  215. package/src/schema/device.ts +0 -145
  216. package/src/schema/firewall.ts +0 -140
  217. package/src/schema/index.ts +0 -8
  218. package/src/schema/policy.ts +0 -115
  219. package/src/schema/routing.ts +0 -173
  220. package/src/schema/system.ts +0 -203
  221. package/src/studio/public/app.js +0 -186
  222. package/src/studio/public/compare.js +0 -250
  223. package/src/studio/public/dom.js +0 -138
  224. package/src/studio/public/evaluate.js +0 -687
  225. package/src/studio/public/export.js +0 -130
  226. package/src/studio/public/graph.js +0 -1541
  227. package/src/studio/public/index.html +0 -83
  228. package/src/studio/public/net.js +0 -134
  229. package/src/studio/public/tables.js +0 -1289
  230. package/src/studio/public/tools.js +0 -610
  231. package/templates/colocation/checks.ts +0 -53
  232. package/templates/colocation/circuit.config.ts +0 -9
  233. package/templates/colocation/edge-01.ts +0 -133
  234. package/templates/colocation/routing.ts +0 -13
  235. package/templates/colocation/tenancy.ts +0 -76
  236. package/templates/colocation/tenants/acme.ts +0 -19
  237. package/templates/colocation/tenants/index.ts +0 -6
  238. package/templates/colocation/tor-01.ts +0 -33
  239. package/templates/edge-router/checks.ts +0 -20
  240. package/templates/edge-router/circuit.config.ts +0 -8
  241. package/templates/edge-router/edge-01.ts +0 -110
  242. package/templates/edge-router/routing.ts +0 -13
  243. package/templates/project/AGENTS.md +0 -51
  244. package/templates/project/CLAUDE.md +0 -1
  245. package/templates/project/README.md +0 -16
  246. package/templates/project/gitignore +0 -5
  247. package/templates/project/tsconfig.json +0 -13
  248. package/templates/single-site/checks.ts +0 -26
  249. package/templates/single-site/circuit.config.ts +0 -8
  250. package/templates/single-site/router.ts +0 -86
  251. package/templates/single-site/site.ts +0 -18
  252. package/templates/single-site/switch.ts +0 -34
  253. /package/src/studio/{public → app/public}/favicon.svg +0 -0
  254. /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
  255. /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
  256. /package/src/studio/{public → app/public}/special-gothic-license.txt +0 -0
@@ -0,0 +1,335 @@
1
+ // `merge`, by MODEL.md's Merge rules for a language with import, and the types that check a merge in the editor. Each map remembers the file that wrote each of its keys, so an error names both.
2
+
3
+ /** Where each key of a map was written, `file:line`. Not enumerable, so the JSON never shows it. */
4
+ const WRITTEN = Symbol("written in");
5
+
6
+ /** For a key written through a function in `lib/`, the line in `lib/` that called Circuit, `file:line`. Not enumerable either. */
7
+ const INNER = Symbol("written through");
8
+
9
+ type Obj = Record<string, unknown>;
10
+ const isMap = (v: unknown): v is Obj => !!v && typeof v === "object" && !Array.isArray(v);
11
+ const same = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b);
12
+
13
+ /** Circuit's own folder, the one above this file's: its files are left out when looking for the user's file on the stack. */
14
+ const CIRCUIT = decodeURIComponent(new URL("..", import.meta.url).pathname)
15
+ .replace(/^\/([A-Za-z]:)/, "$1")
16
+ .toLowerCase();
17
+
18
+ /** Whether a file on the stack is one of Circuit's own. */
19
+ const isCircuit = (file: string) => file.replaceAll("\\", "/").toLowerCase().startsWith(CIRCUIT);
20
+
21
+ /** The user's frames on the stack of `error`, innermost first, each `file:line`. */
22
+ export function userFrames(error: Error = new Error()): string[] {
23
+ return (error.stack ?? "")
24
+ .split("\n")
25
+ .map((line) => /\(?((?:[A-Za-z]:)?[^():]+\.ts):(\d+)/.exec(line))
26
+ .filter((m): m is RegExpExecArray => !!m && !isCircuit(m[1]!))
27
+ .map((m) => `${m[1]!.trim()}:${m[2]}`);
28
+ }
29
+
30
+ /**
31
+ * The user's file and line that called into Circuit: the outermost frame outside Circuit. A part a function in `lib/` builds is
32
+ * named after the file that called that function, such as the tenant's file, which is the one to change.
33
+ */
34
+ export const caller = (): string => userFrames().at(-1) ?? "?";
35
+
36
+ /** The innermost frame outside Circuit, such as the line in `lib/` that called `part`. */
37
+ export const innerCaller = (): string => userFrames()[0] ?? "?";
38
+
39
+ /** Marks every map in a value as written in `file`, and through `inner` when that is another line, where nothing marked it before. */
40
+ export function mark<T>(value: T, file: string, inner = file): T {
41
+ if (Array.isArray(value)) value.forEach((v) => mark(v, file, inner));
42
+ else if (isMap(value)) {
43
+ if (!Object.hasOwn(value, WRITTEN)) {
44
+ Object.defineProperty(value, WRITTEN, { value: Object.fromEntries(Object.keys(value).map((k) => [k, file])) });
45
+ if (inner !== file)
46
+ Object.defineProperty(value, INNER, { value: Object.fromEntries(Object.keys(value).map((k) => [k, inner])) });
47
+ }
48
+ for (const v of Object.values(value)) mark(v, file, inner);
49
+ }
50
+ return value;
51
+ }
52
+
53
+ /** Where each key of a map was written, for an error that names the file. */
54
+ export const writtenIn = (map: object, key: string): string => (map as { [WRITTEN]?: Record<string, string> })[WRITTEN]?.[key] ?? "?";
55
+
56
+ /** The line in `lib/` a key was written through, or nothing. */
57
+ const innerIn = (map: object, key: string): string | undefined => (map as { [INNER]?: Record<string, string> })[INNER]?.[key];
58
+
59
+ /** Reads a file of the project as its lines. Circuit gives it when it reads a project (src/project/project.ts): the project's editor has no Node types, so this file reads none itself. */
60
+ export const source = { read: (_file: string): string => "" };
61
+
62
+ /** Each file's lines, read once. */
63
+ const lines = new Map<string, string[]>();
64
+ const escape = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
65
+
66
+ /**
67
+ * From `written`, `file:line`, the first line at or after it that writes each of `path` in turn, as `file:line`, or undefined when the last
68
+ * of `path` is not found. A key not found before the last is skipped.
69
+ */
70
+ function find(written: string, path: string[]): string | undefined {
71
+ const colon = written.lastIndexOf(":");
72
+ if (colon < 0) return undefined;
73
+ const file = written.slice(0, colon);
74
+ let line = Number(written.slice(colon + 1)) - 1;
75
+ if (!lines.has(file))
76
+ try {
77
+ lines.set(file, source.read(file).split("\n"));
78
+ } catch {
79
+ lines.set(file, []);
80
+ }
81
+ const text = lines.get(file)!;
82
+ let found = -1;
83
+ path.forEach((name, p) => {
84
+ const writes = new RegExp(`(^|[\\s{,])(["']?)${escape(name)}\\2\\s*:`);
85
+ const i = text.findIndex((t, n) => n >= line && !t.trim().startsWith("//") && writes.test(t));
86
+ if (i >= 0) line = found = i;
87
+ else if (p === path.length - 1) found = -1;
88
+ });
89
+ return found >= 0 ? `${file}:${found + 1}` : undefined;
90
+ }
91
+
92
+ /**
93
+ * Where `key` of `map` is written, as `file:line: `, or nothing when no file is known. `writtenIn` knows the line that made the map (a
94
+ * network's `device()`, a `literal`, a `part`); from there, the file is read for the first line that writes each of `path` in turn,
95
+ * `path` ending in the key: `["vlans", "exchange-a"]`. A key written through `lib/` is looked for there first. When the key is not found,
96
+ * the line that made the map stays.
97
+ */
98
+ export function writtenAt(map: unknown, key: string, path: string[] = [key]): string {
99
+ const outer = writtenIn(map as object, key);
100
+ const inner = innerIn(map as object, key);
101
+ const at = (inner && find(inner, path)) ?? find(outer, path) ?? (outer === "?" ? undefined : outer);
102
+ return at ? `${at}: ` : "";
103
+ }
104
+
105
+ /** From `written`, `file:line`, the line that writes the last of `path`, as `file:line: `; `written` itself when it is not found. */
106
+ export const lineOf = (written: string, path: string[]): string => `${find(written, path) ?? written}: `;
107
+
108
+ /**
109
+ * An ordered list, by its path: a chain's rules, NAT, an ACL, a route policy, policy routing. Under a source, the hostname comes
110
+ * first (`edge-01.firewall.nat.source`). It is written whole in one place, never joined.
111
+ */
112
+ const ORDERED =
113
+ /(^|\.)(firewall\.filter\.[^.]+\.rules|firewall\.nat\.(source|destination)|switching\.acls\.[^.]+|routing\.policies\.[^.]+|routing\.policy_routing)$/;
114
+ export const isOrdered = (path: string) => ORDERED.test(path);
115
+
116
+ /** A value that is one value, compared whole and never merged key by key: a secret, a link, a threshold, a port range. */
117
+ const isOneValue = (key: string, v: Obj) =>
118
+ key === "link" ||
119
+ "secret" in v ||
120
+ "secret_file" in v ||
121
+ "pps" in v ||
122
+ "percent" in v ||
123
+ ("min" in v && "max" in v && Object.keys(v).length === 2);
124
+
125
+ /** Marks a rule list `part` made. Not enumerable, so nothing else sees it. */
126
+ const RULE_LIST = Symbol("rule list");
127
+ export const ruleList = <T extends object>(list: T): T => Object.defineProperty(list, RULE_LIST, { value: true });
128
+ const isRuleList = (v: unknown) => Array.isArray(v) && Object.hasOwn(v, RULE_LIST);
129
+
130
+ /**
131
+ * Merges `b` into a copy of `a`. Two rule lists `part` made, at one path that is no ordered list, join in order with every rule kept:
132
+ * each tenant's router input rules, one tenant after another, for the router to place.
133
+ */
134
+ function mergeTwo(a: Obj, b: Obj, path: string): Obj {
135
+ const out: Obj = { ...a };
136
+ const written: Record<string, string> = {};
137
+ const inner: Record<string, string> = {};
138
+ const note = (from: Obj, key: string) => {
139
+ written[key] = writtenIn(from, key);
140
+ const through = innerIn(from, key);
141
+ if (through) inner[key] = through;
142
+ else delete inner[key];
143
+ };
144
+ for (const key of Object.keys(a)) note(a, key);
145
+
146
+ for (const [key, value] of Object.entries(b)) {
147
+ const here = path ? `${path}.${key}` : key;
148
+ if (!(key in out)) {
149
+ out[key] = value;
150
+ note(b, key);
151
+ continue;
152
+ }
153
+ const old = out[key];
154
+ const both = `in ${written[key]} and in ${writtenIn(b, key)}`;
155
+ if (Array.isArray(old) && Array.isArray(value) && isOrdered(here))
156
+ throw new Error(
157
+ `${here} is an ordered list, written ${both}. Write it whole in one place, and place the other's rules in it with merge.`
158
+ );
159
+ if (isRuleList(old) && isRuleList(value)) out[key] = ruleList([...(old as unknown[]), ...(value as unknown[])]);
160
+ else if (Array.isArray(old) && Array.isArray(value)) out[key] = [...old, ...value.filter((v) => !old.some((o) => same(o, v)))];
161
+ else if (isMap(old) && isMap(value) && !isOneValue(key, old) && !isOneValue(key, value)) out[key] = mergeTwo(old, value, here);
162
+ else if (!same(old, value))
163
+ throw new Error(
164
+ `${here} is written twice with two values: ${JSON.stringify(old)} in ${written[key]}, and ${JSON.stringify(value)} in ${writtenIn(b, key)}. One name holds one value: rename one, or make both the same.`
165
+ );
166
+ }
167
+
168
+ Object.defineProperty(out, WRITTEN, { value: written });
169
+ if (Object.keys(inner).length) Object.defineProperty(out, INNER, { value: inner });
170
+ return out;
171
+ }
172
+
173
+ /** Merges parts in order. The file of a part not marked yet is `file`. */
174
+ export function mergeAll(parts: readonly unknown[], file: string, path = ""): Obj {
175
+ let out: Obj = {};
176
+ for (const part of parts) if (part !== undefined) out = mergeTwo(out, mark(part as Obj, file), path);
177
+ return out;
178
+ }
179
+
180
+ // ---------- `merge([...])`: one list or one map, by where it lands ----------
181
+
182
+ /** What a `merge` call was given, flat, and the file that called it. Not enumerable, so nothing else sees it. */
183
+ const MADE = Symbol("merge items");
184
+ type Made = { [MADE]: { values: readonly unknown[]; file: string } };
185
+ const madeBy = (v: unknown) => (v && typeof v === "object" && Object.hasOwn(v, MADE) ? (v as Made)[MADE] : undefined);
186
+
187
+ /** The items, flat: a list in the items gives its own items in its place, in order. */
188
+ const flat = (items: readonly unknown[]): unknown[] => items.flatMap((v) => (Array.isArray(v) ? flat(v) : [v]));
189
+
190
+ /**
191
+ * What `merge` returns before it lands: a list when an item is a list or no map, else the maps merged. The same rule the editor uses
192
+ * for a `merge` that lands in no field. Where it lands decides (`resolve`).
193
+ */
194
+ export function made(items: readonly unknown[], file: string): object {
195
+ const values = flat(items);
196
+ // One map given twice, such as a tenant: its data would be kept once and its rules twice, and nothing would say so.
197
+ if (values.some((v, i) => isMap(v) && values.indexOf(v) !== i))
198
+ throw new Error(`${file}: merge is given the same value twice. Give each once.`);
199
+ const isList = items.some(Array.isArray) || values.some((v) => !isMap(v));
200
+ let out: object = [...values];
201
+ if (!isList)
202
+ try {
203
+ out = mergeAll(values, file);
204
+ } catch (error) {
205
+ // Maps that clash may be single rules, which a list field takes as they are: they stay a list until they land (`resolve`).
206
+ // A field of them read before, such as `tenants.parts` of two tenants with one VLAN name, says what clashed, naming both
207
+ // values and where each is written, not that the field is missing. Never `then`, which a promise reads.
208
+ const clash = `${file}: merge: ${error instanceof Error ? error.message : String(error)}`;
209
+ for (const key of new Set(values.flatMap((v) => Object.keys(v as object))))
210
+ if (key !== "then")
211
+ Object.defineProperty(out, key, {
212
+ get: () => {
213
+ throw new Error(clash);
214
+ },
215
+ });
216
+ }
217
+ return Object.defineProperty(out, MADE, { value: { values, file } });
218
+ }
219
+
220
+ /**
221
+ * Lists of the model whose items are maps, besides the ordered ones: there, a merge of single maps is a list of them, never one map.
222
+ * Taken from Circuit's schema (every array of objects under a device).
223
+ */
224
+ const MAP_LISTS =
225
+ /(^|\.)(routing\.(static_routes|rpki)|monitoring\.(syslog|flow_export\.collectors)|system\.vdoms\.[^.]+\.static_routes|interfaces\.[^.]+\.[^.]+\.(addresses|peers|vrrp)|services\.dhcp_server\.[^.]+\.reservations)$/;
226
+
227
+ /**
228
+ * Every `merge` in a value, made again by the field it lands in, `path`, which Circuit knows from the model:
229
+ *
230
+ * - in an ordered list (`isOrdered`), its items flat, in order, each kept as it is;
231
+ * - in a list of maps (`MAP_LISTS`), or when an item is no map (a VLAN name, a port), its items flat, the same item twice kept once;
232
+ * - anywhere else, its items merged as maps, by `mergeAll`.
233
+ *
234
+ * So two single rules, `merge([rule_a, rule_b])`, are two rules in a chain and never one map: the chain's path says so. A source in a
235
+ * device's `include` lands at the top, so it is a map, keyed by hostname. Changes maps and lists in place, so a rule keeps its identity.
236
+ */
237
+ export function resolve(value: unknown, path: string): unknown {
238
+ const by = madeBy(value);
239
+ if (by) {
240
+ const { values, file } = by;
241
+ if (isOrdered(path)) value = [...values];
242
+ else if (MAP_LISTS.test(path) || values.some((v) => !isMap(v)))
243
+ value = values.filter((v, i, all) => all.findIndex((o) => same(o, v)) === i);
244
+ else if (Array.isArray(value))
245
+ try {
246
+ value = mergeAll(values, file, path);
247
+ } catch (error) {
248
+ throw new Error(`${file}: merge: ${error instanceof Error ? error.message : String(error)}`);
249
+ }
250
+ }
251
+ if (Array.isArray(value)) value.forEach((v, i) => (value[i] = resolve(v, `${path}[]`)));
252
+ else if (isMap(value))
253
+ for (const [k, v] of Object.entries(value)) if (v && typeof v === "object") value[k] = resolve(v, path ? `${path}.${k}` : k);
254
+ return value;
255
+ }
256
+
257
+ // ---------- the types: what the editor checks ----------
258
+
259
+ /**
260
+ * Two parts merged as `merge` merges them: maps key by key, lists joined, anything else the later one. A key one side writes out keeps
261
+ * its own type, even when the other side has an index signature.
262
+ */
263
+ export type Merge2<A, B> = [A] extends [readonly (infer X)[]]
264
+ ? [B] extends [readonly (infer Y)[]]
265
+ ? readonly (X | Y)[]
266
+ : B
267
+ : [A] extends [object]
268
+ ? [B] extends [object]
269
+ ? {
270
+ [K in keyof A | keyof B]: K extends WrittenKeys<A>
271
+ ? K extends WrittenKeys<B>
272
+ ? Merge2<A[K], B[K]>
273
+ : A[K]
274
+ : K extends WrittenKeys<B>
275
+ ? B[K]
276
+ : (K extends keyof A ? A[K] : never) | (K extends keyof B ? B[K] : never);
277
+ }
278
+ : B
279
+ : B;
280
+
281
+ /** Parts merged in order. */
282
+ export type Merged<T extends readonly unknown[], Acc = {}> = T extends readonly [infer H, ...infer R] ? Merged<R, Merge2<Acc, H>> : Acc;
283
+
284
+ /** The first `N` parts. */
285
+ type Take<T extends readonly unknown[], N extends number, Acc extends unknown[] = []> = Acc["length"] extends N
286
+ ? Acc
287
+ : T extends readonly [infer H, ...infer R]
288
+ ? Take<R, N, [...Acc, H]>
289
+ : Acc;
290
+
291
+ /** The parts before position `I` of `T`, merged, with `First` before them. */
292
+ export type Before<T extends readonly unknown[], I, First = {}> = I extends `${infer N extends number}`
293
+ ? Merged<[First, ...Take<T, N>]>
294
+ : First;
295
+
296
+ /** The keys written out, not those of an index signature. */
297
+ type WrittenKeys<A> = keyof { [K in keyof A as number extends K ? never : string extends K ? never : K]: 0 };
298
+
299
+ /** Keys to look into: a map's, without symbols. */
300
+ type Keys<B> = keyof B & (string | number);
301
+
302
+ /** The path of an ordered list, as `isOrdered` reads it. */
303
+ type OrderedPath =
304
+ | `${string}firewall.filter.${string}.rules`
305
+ | `${string}firewall.nat.${"source" | "destination"}`
306
+ | `${string}switching.acls.${string}`
307
+ | `${string}routing.policies.${string}`
308
+ | `${string}routing.policy_routing`;
309
+
310
+ /**
311
+ * Every place where part `B` clashes with what came before it, `A`, said in words: an ordered list both write, or another value at
312
+ * one path. Any other list never clashes: it joins.
313
+ */
314
+ type ClashPaths<B, A, Path extends string = ""> = {
315
+ [K in Keys<B>]: K extends WrittenKeys<A>
316
+ ? [B[K]] extends [readonly unknown[]]
317
+ ? `${Path}${K}` extends OrderedPath
318
+ ? `${Path}${K} is an ordered list written in two places`
319
+ : never
320
+ : [B[K]] extends [object]
321
+ ? ClashPaths<B[K], A[K & keyof A], `${Path}${K}.`>
322
+ : [B[K]] extends [A[K & keyof A]]
323
+ ? never
324
+ : [A[K & keyof A]] extends [B[K]]
325
+ ? never
326
+ : `${Path}${K} is written twice`
327
+ : never;
328
+ }[Keys<B>];
329
+
330
+ /**
331
+ * What part `B` must also be, after `A`: nothing more when nothing clashes, and else a property named after each clash that it lacks,
332
+ * so the editor says `Property '"edge-01.firewall.nat.source is an ordered list written in two places"' is missing`. An
333
+ * ordered list is written whole in one place. Any other value written twice must be the same.
334
+ */
335
+ export type Clash<B, A> = [ClashPaths<B, A>] extends [never] ? unknown : { [P in ClashPaths<B, A>]: true };
@@ -0,0 +1,70 @@
1
+ // Every platform Circuit runs on, every model it knows, each model's ports and how it prints them. The types read from here, so a model or port that does not exist fails to compile.
2
+
3
+ import { models as fortios } from "../platforms/fortios/models";
4
+ import { models as junos } from "../platforms/junos/models";
5
+ import { models as raisecom } from "../platforms/raisecom-ros/models";
6
+ import { models as routeros } from "../platforms/routeros/models";
7
+ import { models as vrp } from "../platforms/vrp/models";
8
+
9
+ /** What a model's hardware holds, where a real device held less than its datasheet lists. `circuit validate` checks a config against it. */
10
+ export type ModelLimits = {
11
+ /** The most member ports one LAG may have. More is an error. */
12
+ readonly lag_members?: number;
13
+ /** About how many switch rules the switch chip held. More is a warning: a rule the chip cannot hold is invalid, and so is every rule after it, without a log line. */
14
+ readonly switch_rules?: number;
15
+ /** How many distinct port ranges the switch rules held. More is a warning, for the same reason. */
16
+ readonly switch_port_ranges?: number;
17
+ };
18
+
19
+ /** One hardware model: its ports per speed class, how the platform spells a port, and what its hardware holds. */
20
+ export type ModelSpec = {
21
+ /** How many ports of each speed class, such as `{ "10g": 48 }`. A port is `<class>-<position>`, such as `10g-4`. */
22
+ readonly ports: Readonly<Record<string, number>>;
23
+ /** How the platform spells a port of each class. `{n}` is the position within the class; `{n+4}` and `{n-1}` shift it, because some platforms number every port in one run or count from 0. */
24
+ readonly spelling: Readonly<Record<string, string>>;
25
+ /** What the hardware held, where a real device held less than its datasheet. */
26
+ readonly limits?: ModelLimits;
27
+ };
28
+
29
+ /** Every model of every platform: each platform's own list, in its folder under `src/platforms/`. */
30
+ export const MODELS = { routeros, vrp, "raisecom-ros": raisecom, junos, fortios } as const satisfies Record<
31
+ string,
32
+ Record<string, ModelSpec>
33
+ >;
34
+
35
+ type Models = typeof MODELS;
36
+
37
+ /** The operating system of a device. It decides which adapter renders and applies the device. */
38
+ export type Platform = keyof Models;
39
+
40
+ /** A hardware model the platform runs on. It decides which port names exist. */
41
+ export type Model<P extends Platform> = P extends Platform ? keyof Models[P] & string : never;
42
+
43
+ type Upto<N extends number, A extends unknown[] = [], R = never> = A["length"] extends N
44
+ ? R
45
+ : Upto<N, [...A, 0], R | `${[...A, 0]["length"]}`>;
46
+ type SpecOf<P extends Platform, M extends string> = P extends Platform ? (M extends keyof Models[P] ? Models[P][M] : never) : never;
47
+ type PortsOf<P extends Platform, M extends string> = SpecOf<P, M> extends { ports: infer Ports } ? Ports : never;
48
+
49
+ /** A port of the model, `<speed class>-<position>`: `10g-1` to `10g-48` on a model with 48 10G ports. */
50
+ export type PortName<P extends Platform, M extends string> = {
51
+ [S in keyof PortsOf<P, M>]: `${S & string}-${Upto<PortsOf<P, M>[S] & number>}`;
52
+ }[keyof PortsOf<P, M>];
53
+
54
+ /** Every port a model has, in order. */
55
+ export function portsOf(platform: string, model: string): string[] {
56
+ const spec = (MODELS as Record<string, Record<string, ModelSpec>>)[platform]?.[model];
57
+ if (!spec) return [];
58
+ return Object.entries(spec.ports).flatMap(([speed, count]) => Array.from({ length: count }, (_, i) => `${speed}-${i + 1}`));
59
+ }
60
+
61
+ /** A port as the platform prints it, from the model's `spelling`: `10g-2` on a FG-3700D is `port6`. The port as written when the model has no spelling for it. */
62
+ export function spell(platform: string, model: string, port: string): string {
63
+ const spec = (MODELS as Record<string, Record<string, ModelSpec>>)[platform]?.[model];
64
+ const [speed = "", n = ""] = port.split("-");
65
+ const pattern = spec?.spelling[speed];
66
+ return pattern ? pattern.replace(/\{n([+-]\d+)?\}/g, (_, shift: string | undefined) => String(Number(n) + Number(shift ?? 0))) : port;
67
+ }
68
+
69
+ /** The platforms where a VLAN is only its number: no name, no description. A VLAN interface holds its own. Circuit drops a VLAN's `description` there, as their renderers do. */
70
+ export const NUMBERED_VLANS: readonly Platform[] = ["raisecom-ros", "fortios"];
@@ -0,0 +1,64 @@
1
+ // What the box reports to collectors: `monitoring.snmp`, `.syslog`, `.flow_export`, `.transceivers`.
2
+
3
+ import type { IP, Secret, Severity } from "./common";
4
+ import type { Service } from "./management";
5
+
6
+ /** One SNMPv3 user. */
7
+ export type SnmpUser = {
8
+ /** How messages are authenticated. */
9
+ auth: "sha1" | "sha256";
10
+ /** The authentication passphrase. */
11
+ auth_password: Secret;
12
+ /** How messages are encrypted. */
13
+ privacy: "aes128" | "des";
14
+ /** The encryption passphrase. */
15
+ privacy_password: Secret;
16
+ };
17
+
18
+ /** One syslog collector. */
19
+ export type SyslogServer = {
20
+ /** Where the collector listens. */
21
+ address: IP;
22
+ /** UDP port. 514 when absent. */
23
+ port?: number;
24
+ /** The least severe message sent. */
25
+ level: Severity;
26
+ };
27
+
28
+ /** Export flow records or packet samples to collectors. */
29
+ export type FlowExport<I extends string> = {
30
+ /** The record format. */
31
+ protocol: "sflow" | "netflow-v9" | "ipfix";
32
+ /** Where records are sent. */
33
+ collectors: readonly {
34
+ /** Where the collector listens. */
35
+ address: IP;
36
+ /** UDP port. When absent: 6343 for sFlow and 4739 for IPFIX (IANA), 2055 for NetFlow v9 (the usual one). */
37
+ port?: number;
38
+ }[];
39
+ /** Sample one packet in this many. Every packet when absent, on a platform that can export every packet. */
40
+ sampling?: number;
41
+ /** Some of the device's ports and interfaces: where traffic is observed. Every interface when absent. */
42
+ interfaces?: readonly I[];
43
+ };
44
+
45
+ /** What the device reports to collectors. `I` is the device's ports and interfaces. */
46
+ export type Monitoring<I extends string> = {
47
+ /** Polling by SNMP. */
48
+ snmp?: Service & {
49
+ /** Read-only community for version 2c. Version 2c is off when absent. */
50
+ community?: Secret;
51
+ /** Version 3 users, keyed by name. */
52
+ users?: Record<string, SnmpUser>;
53
+ /** Who to contact about the device. */
54
+ contact?: string;
55
+ /** Where the device is. */
56
+ location?: string;
57
+ };
58
+ /** Log messages sent to syslog collectors, each from its own `level` up. */
59
+ syslog?: readonly SyslogServer[];
60
+ /** Flow records or packet samples sent to collectors. */
61
+ flow_export?: FlowExport<I>;
62
+ /** Read optic temperature, power and voltage. */
63
+ transceivers?: boolean;
64
+ };
@@ -0,0 +1,180 @@
1
+ // The whole network, what `circuit()` in `circuit.ts` at a project's root takes: `asn`, `prefixes`, `communities`, `sites`, `checks`, `integrations`. Its devices register with it.
2
+
3
+ import type { Prefix, Secret } from "./common";
4
+ import type { CommunityRequest, CommunityScheme, SchemeCheck } from "./communities";
5
+ import type { AnyDevice } from "./device";
6
+ import type { RouteRule } from "./routing";
7
+
8
+ /**
9
+ * The network's own prefixes, in groups of your choosing, such as a location (`id01`), each a map of named prefixes:
10
+ * `{ region_a: { v4_1: "192.0.2.0/24", v6_1: "2001:db8::/48" } }`.
11
+ */
12
+ export type Prefixes = Readonly<Record<string, Readonly<Record<string, Prefix>>>>;
13
+
14
+ /**
15
+ * One BGP community the network defines, in the community description format of the NLNOG Ring looking glass, which bgp.tools also reads.
16
+ */
17
+ export type Community = {
18
+ /** The community. `nnn` stands for any number, `x` for one digit and `a-b` for a range, such as `65535:0:nnn`. */
19
+ community: string;
20
+ /** What it means. `$0`, `$1` and so on stand for what each wildcard matched, such as `do not announce to AS$0`. */
21
+ description: string;
22
+ };
23
+
24
+ /** A site: a data centre or an office. */
25
+ export type Site = {
26
+ /** What and where the site is. */
27
+ description?: string;
28
+ /**
29
+ * The site's number, the last part of the community `learned_at_site` tags a route with, such as `64500:2:0`. Only Circuit reads
30
+ * it: the resolved network leaves it out.
31
+ */
32
+ number?: number;
33
+ };
34
+
35
+ /** Circuit's built-in checks, by name. A check is on when it is here. `D` is the network's device names: `circuit validate` checks each is a device of the network. */
36
+ export type Checks<D extends string> = {
37
+ /**
38
+ * Where traffic from outside, before anything filters it, may go. The untrusted VLANs are those written `untrusted: true` in
39
+ * `switching.vlans`; every other VLAN is trusted, so a VLAN added later is protected without being listed. `untrusted: true` is
40
+ * allowed only while this check is on.
41
+ */
42
+ untrusted_vlans?: {
43
+ /** The devices that filter: the only ones that may give an untrusted VLAN an address, and the only ones a port carrying both kinds may face. */
44
+ filtering_devices: readonly D[];
45
+ /** Links between two switches that may carry both kinds, each named by its two devices: a site's switch extended to another building, with an exchange's VLAN and its own management on one cable. Neither end gives an untrusted VLAN an address. */
46
+ mixed_links?: readonly (readonly [D, D])[];
47
+ };
48
+ /** Every BGP export ends in a reject that matches everything, so nothing leaves by accident. An export to a neighbor whose `role` is `customer` is left out. */
49
+ exports_end_in_reject?: true;
50
+ /** Every BGP import ends in a rule that decides for every route, an accept or a reject without a match. */
51
+ imports_end_in_accept_or_reject?: true;
52
+ /** A native VLAN is a dead end: native on one trunk (a port or a LAG) of a device at most, and never an access VLAN or a tagged VLAN anywhere on that device. */
53
+ native_vlans_are_dead_ends?: true;
54
+ /** A GRE or VXLAN tunnel's local address is outside what the neighbors over it are offered, or their replies route back into the tunnel. */
55
+ tunnels_outside_offered?: true;
56
+ /**
57
+ * One VLAN name has one ID on every device of the network. Circuit runs it when it reads the project, and `circuit validate` stops on a VLAN
58
+ * with two IDs, naming both devices and where each is written. So the resolved network always passes it, and leaves it out.
59
+ */
60
+ same_vlan_ids?: true;
61
+ };
62
+
63
+ /**
64
+ * What one of your own checks found: a sentence, and the device and path it is about, when it knows them. Circuit prints it with the
65
+ * file and line that write that path, as for its own errors: `sites/site-a/switch-01.ts:21: tenants_dont_clash: switch-01: interfaces.ethernet.10g-2: ...`.
66
+ */
67
+ export type Finding = {
68
+ /** What is wrong, in a sentence. */
69
+ message: string;
70
+ /** The device it is about, by name. */
71
+ device?: string;
72
+ /** Where in that device, dot by dot, as Circuit names a field: `interfaces.ethernet.10g-2`, `routing.policies.transit-a-export[2]`. */
73
+ path?: string;
74
+ };
75
+
76
+ /**
77
+ * A device as your own checks read it: as it will run. Each BGP neighbor holds what it takes from its group. Each policy is route rules
78
+ * only: a `call` is the called policy's rules, and a `{ communities }` rule the rules it means there. `management.allow` written as an
79
+ * address set's name is that set's addresses, and a rule's interface set is its interfaces. A copy: a check changes nothing.
80
+ */
81
+ export type ResolvedDevice = Omit<AnyDevice, "routing" | "management"> & {
82
+ readonly routing?: Omit<NonNullable<AnyDevice["routing"]>, "policies"> & {
83
+ readonly policies?: Readonly<Record<string, readonly RouteRule<string>[]>>;
84
+ };
85
+ readonly management?: Omit<NonNullable<AnyDevice["management"]>, "allow"> & { readonly allow?: readonly Prefix[] };
86
+ };
87
+
88
+ /** The network as your own checks read it: its AS, its prefixes, and each device as it will run, its parts merged in. */
89
+ export type ResolvedNetwork = { readonly asn?: number; readonly prefixes: Prefixes; readonly devices: readonly ResolvedDevice[] };
90
+
91
+ /**
92
+ * A check of your own, beside Circuit's: it reads the network resolved, as its devices will run it, and returns what is wrong. Nothing
93
+ * found is an empty list. `circuit validate` runs it; the resolved network never shows it.
94
+ */
95
+ export type Check = (network: ResolvedNetwork) => readonly Finding[];
96
+
97
+ /** Where a network's secrets are: a file of `NAME="value"` lines, each value encrypted, changed with `circuit secrets`. */
98
+ export type NetworkSecrets = {
99
+ /** The file, relative to the project, such as `.circuit/lab-secrets` for a lab network with its own password. Commit it. */
100
+ file: string;
101
+ };
102
+
103
+ /** Systems Circuit keeps in step with the config. */
104
+ export type Integrations = {
105
+ /** Keeps the network's PeeringDB record in step with the config: `circuit peeringdb`. Needs `asn`. */
106
+ peeringdb?: {
107
+ /** An API key with write access to the network's record. */
108
+ api_key: Secret;
109
+ };
110
+ /** Keeps LibreNMS monitoring every device, under its name, with its SNMP credentials and location, and in a group for its site: `circuit librenms`. */
111
+ librenms?: {
112
+ /** Where LibreNMS is, such as `https://librenms.example.com`. */
113
+ url: `http://${string}` | `https://${string}`;
114
+ /** An API token that can add and change devices and device groups. */
115
+ api_token: Secret;
116
+ };
117
+ /** Keeps NetBox in step with the config: `circuit netbox`. Each device's `site` and `role` place it there. */
118
+ netbox?: {
119
+ /** Where NetBox is, such as `https://netbox.example.com`. */
120
+ url: `http://${string}` | `https://${string}`;
121
+ /** An API token that can write. */
122
+ api_token: Secret;
123
+ };
124
+ };
125
+
126
+ /**
127
+ * What a device takes from its network, for the editor: whether `untrusted_vlans` is on, whether the network writes `communities`, and
128
+ * the scheme's groups and requests.
129
+ */
130
+ export type NetworkFacts = { untrusted: boolean; communities: boolean; groups: string; requests: string };
131
+
132
+ /** Any network's facts: what a part, or a device seen from the network, takes. */
133
+ export type AnyFacts = { untrusted: true; communities: true; groups: string; requests: CommunityRequest };
134
+
135
+ /** Whether a site has a number, which `learned_at_site` needs. */
136
+ export type Numbered<Sites> = true extends (
137
+ Sites[keyof Sites] extends infer X ? (X extends { readonly number: number } ? true : false) : never
138
+ )
139
+ ? true
140
+ : false;
141
+
142
+ /**
143
+ * What `circuit()` takes: the whole network but its devices, which register with it from their own files. `Sites` is its sites as
144
+ * written. `Own` is the `checks` as written: a key that is not one of Circuit's is a check of your own. `C` is the `communities` as
145
+ * written. `P` is the `prefixes` as written.
146
+ */
147
+ export type NetworkSettings<
148
+ Sites extends Readonly<Record<string, Site>>,
149
+ Asn extends number,
150
+ Own = {},
151
+ C = undefined,
152
+ P extends Prefixes = {},
153
+ > = {
154
+ /** The AS the network operates. A device's BGP runs it, unless the device writes `routing.bgp.asn`. */
155
+ asn?: Asn;
156
+ /**
157
+ * The network's own prefixes, in groups of your choosing, each a map of named prefixes: `{ region_a: { v4_1: "192.0.2.0/24" } }`.
158
+ * Devices use them by name, `as64500.prefixes.id01.v4_1`, and your own checks read them. The resolved network keeps them, for the
159
+ * commands that would use them: PeeringDB's prefix counts, and RPKI and IRR checks.
160
+ */
161
+ prefixes?: P;
162
+ /**
163
+ * The network's BGP large communities, `asn:code:value` (RFC 8092), used as RFC 8195 describes. Written once here: a neighbor group
164
+ * says which `groups` it is in, and a policy places `{ communities: "import" }` or `{ communities: "export" }`. Circuit writes the
165
+ * catalogue from it, for a looking glass or bgp.tools.
166
+ */
167
+ communities?: C & CommunityScheme & SchemeCheck<C, Numbered<Sites>>;
168
+ /** Each site, by name: a data centre or an office. The NetBox site and LibreNMS group of that name. A device's `site` is one of these, and each has a device. */
169
+ sites: Sites;
170
+ /**
171
+ * Circuit's built-in checks, turned on by name, with their settings, and your own: any other name takes a function that reads the
172
+ * network and returns what it found (`Check`). `circuit validate` runs them all. `untrusted: true` on a VLAN is allowed only while
173
+ * `untrusted_vlans` is here.
174
+ */
175
+ checks?: Own & Checks<string> & { [K in Exclude<keyof Own, keyof Checks<string>>]: Check };
176
+ /** Systems Circuit keeps in step with the config. */
177
+ integrations?: Integrations;
178
+ /** Where the network's secrets are. `.circuit/secrets` when absent. */
179
+ secrets?: NetworkSecrets;
180
+ };