@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,684 @@
1
+ // What a project's files call, as globals: `circuit`, `part`, `literal`, `merge`, `secret`, `secretFile`, `bgpNeighbors`. A network's `device()`.
2
+
3
+ import type { IP, Secret } from "./common";
4
+ import type { AnyDevice, AnyPort, Device, DevicePart, Names, Part } from "./device";
5
+ import type { FilterRule, NatRule } from "./firewall";
6
+ import type { Port } from "./interfaces";
7
+ import type { Model, Platform, PortName } from "./models";
8
+ import type { CommunitiesExport, CommunitiesImport, GroupsOf, RequestsOf } from "./communities";
9
+ import type { NetworkFacts, NetworkSettings, Prefixes, Site } from "./network";
10
+ import { ACL_RULE, ACTIONS, closest, FILTER_RULE, NAT_RULE, ROUTING_RULE, type Shape, unknownKeyHelp, unknownKeys } from "./keys";
11
+ import { type Before, type Clash, caller, innerCaller, made, mark, type Merged, mergeAll, resolve, ruleList, writtenAt } from "./merge";
12
+ import { type CallRule, type PolicyRule, policyRuleProblem, type RouteRule, type RoutingRule } from "./routing";
13
+ import type { AclRule } from "./switching";
14
+
15
+ /** A secret by name, read when a device is applied from the network's secrets file or the environment, and through 1Password when that value is an `op://` reference. Never the value itself. */
16
+ export const secret = (name: string): Secret => ({ secret: name });
17
+
18
+ /** A value read from a file when a device is applied, such as a private key in PEM. The path is from the project root; keep the file out of git. */
19
+ export const secretFile = (path: string): Secret => ({ secret_file: path });
20
+
21
+ /** One network `circuit()` made, the line that made it, and each device registered with it: the device, the line that made it, and the file Circuit was reading. */
22
+ export type Registered = { network: object; at: string; devices: { device: AnyDevice; at: string; file: string }[] };
23
+
24
+ /**
25
+ * What Circuit checks once it has read every device: each network and its devices, that each source's hostnames are devices of the
26
+ * network, and that each rule list `part` made is placed. `reading` is the file under `sites/` Circuit is importing.
27
+ */
28
+ export const registry = {
29
+ networks: [] as Registered[],
30
+ sources: [] as { source: object; device: string; file: string; network: object }[],
31
+ rules: [] as { rules: readonly object[]; file: string }[],
32
+ reading: "",
33
+ };
34
+
35
+ /** Ports by name. A name the model does not have is an error that says so. */
36
+ type CheckedPorts<Ports extends string, ModelPorts extends string, M extends string, P> = {
37
+ [K in Ports]?: K extends ModelPorts ? P : `${K} is not a port of ${M}`;
38
+ };
39
+
40
+ /** The names a device declares, by kind, from what `device()` finds in it. */
41
+ type Declared<
42
+ Pt extends string,
43
+ L extends string,
44
+ VI extends string,
45
+ I extends string,
46
+ V extends string,
47
+ A extends string,
48
+ P extends string,
49
+ S extends string,
50
+ G extends string,
51
+ T extends string,
52
+ F extends string,
53
+ AS extends string,
54
+ IS extends string,
55
+ U extends string,
56
+ C extends string,
57
+ > = {
58
+ port: Pt;
59
+ lag: L;
60
+ vlanInterface: VI;
61
+ interface: I;
62
+ vlan: V;
63
+ acl: A;
64
+ policy: P;
65
+ prefixSet: S;
66
+ neighborGroup: G;
67
+ table: T;
68
+ vrf: F;
69
+ addressSet: AS;
70
+ interfaceSet: IS;
71
+ user: U;
72
+ certificate: C;
73
+ anyInterface: Pt | L | VI | I;
74
+ };
75
+
76
+ /** The keys of the map at `D[G][K]`, such as the VLANs at `switching.vlans`. */
77
+ type KeysAt<D, G extends string, K extends string> = D extends { readonly [g in G]?: infer Group }
78
+ ? K extends keyof NonNullable<Group>
79
+ ? keyof NonNullable<NonNullable<Group>[K]> & string
80
+ : never
81
+ : never;
82
+
83
+ /** The names a device declares, by kind: the keys of its maps, from the device and every part merged. */
84
+ type NamesOf<D> = {
85
+ port: KeysAt<D, "interfaces", "ethernet">;
86
+ lag: KeysAt<D, "interfaces", "lag">;
87
+ vlanInterface: KeysAt<D, "interfaces", "vlan_interface">;
88
+ interface: KeysAt<D, "interfaces", "bridge" | "loopback" | "gre" | "vxlan" | "wireguard">;
89
+ vlan: KeysAt<D, "switching", "vlans">;
90
+ acl: KeysAt<D, "switching", "acls">;
91
+ policy: KeysAt<D, "routing", "policies">;
92
+ prefixSet: KeysAt<D, "routing", "prefix_sets">;
93
+ neighborGroup: D extends { readonly routing?: { readonly bgp?: { readonly neighbor_groups?: infer G } } } ? keyof G & string : never;
94
+ table: KeysAt<D, "routing", "tables">;
95
+ vrf: KeysAt<D, "routing", "vrfs">;
96
+ addressSet: KeysAt<D, "firewall", "address_sets">;
97
+ interfaceSet: KeysAt<D, "firewall", "interface_sets">;
98
+ user: KeysAt<D, "system", "users">;
99
+ certificate: KeysAt<D, "system", "certificates">;
100
+ anyInterface: KeysAt<D, "interfaces", "ethernet" | "lag" | "vlan_interface" | "bridge" | "loopback" | "gre" | "vxlan" | "wireguard">;
101
+ };
102
+
103
+ /** The ports a device declares, each checked against its model. */
104
+ type EthernetOf<P extends Platform, M extends Model<P>, N extends Names> = CheckedPorts<
105
+ N["port"],
106
+ PortName<P, M>,
107
+ M,
108
+ Port<NoInfer<N["vlan"]>, NoInfer<N["acl"]>, NoInfer<N["lag"]>, NoInfer<N["vrf"]>>
109
+ >;
110
+
111
+ /** The names the device itself declares, which `device()` infers, and those its parts declare. */
112
+ type All<Own extends Names, Parts extends readonly object[]> = {
113
+ [K in keyof Names]: Own[K] | NamesOf<Merged<Parts>>[K];
114
+ };
115
+
116
+ /** The part a source gives the device `Name`: its entry under that hostname, or nothing. */
117
+ type PartOf<S, Name extends string> = S extends { readonly [K in Name]: infer P } ? P : {};
118
+
119
+ /** The part each source gives the device `Name`, in the order of the sources. */
120
+ type PartsOf<Sources extends readonly object[], Name extends string> = { [I in keyof Sources]: PartOf<Sources[I], Name> };
121
+
122
+ /**
123
+ * Declare one device of this network, in its own file under `sites/`. Circuit finds the file and imports it; the call registers the
124
+ * device with this network:
125
+ *
126
+ * ```ts
127
+ * export default as64500.device({ name: "switch-02", site: "site-b", ... });
128
+ * export default as64500.device({ name: "edge-01", include: [tenants.parts], site: "site-a", ... });
129
+ * ```
130
+ *
131
+ * `site` is one of the network's `sites`, and the editor suggests them. `untrusted: true` on a VLAN is an error unless the network turns
132
+ * on the check `untrusted_vlans`. A neighbor's `communities` and a policy's `{ communities }` rule are errors unless the network writes
133
+ * `communities`, and take its groups and requests.
134
+ *
135
+ * `include` lists the device's sources, each an object keyed by hostname, each value a part, such as the tenants'. The device takes
136
+ * from each the part under its own `name`, merged in after it as `merge` does: maps key by key, lists joined. A value two of them
137
+ * write, or an ordered list from two places, is an error, in the editor where it can tell and always when Circuit reads the project.
138
+ * So is a hostname in a source that is no device of the network.
139
+ *
140
+ * The names the device and its parts declare (VLANs, interfaces, sets, ACLs, policies and the rest) are the only names their fields
141
+ * accept, and the editor suggests them. The port names come from the model.
142
+ */
143
+ export type DeviceOf<Sites extends string, Net extends NetworkFacts> = <
144
+ const Name extends string,
145
+ const P extends Platform,
146
+ const M extends Model<P>,
147
+ const Sources extends readonly object[] = [],
148
+ const Ports extends string = never,
149
+ const Lags extends string = never,
150
+ const VlanInterfaces extends string = never,
151
+ const OtherInterfaces extends string = never,
152
+ const Vlans extends string = never,
153
+ const Acls extends string = never,
154
+ const Policies extends string = never,
155
+ const PrefixSets extends string = never,
156
+ const NeighborGroups extends string = never,
157
+ const Tables extends string = never,
158
+ const Vrfs extends string = never,
159
+ const AddressSets extends string = never,
160
+ const InterfaceSets extends string = never,
161
+ const Users extends string = never,
162
+ const Certificates extends string = never,
163
+ >(
164
+ // `site` is checked on its own: inside the device, a network of one site made a wrong site a second error at each port.
165
+ device: {
166
+ /** The site it is at, one of the network's `sites`. The NetBox site and LibreNMS group of that name. */
167
+ site: Sites;
168
+ /**
169
+ * Where the device takes parts from, written right after `name`: `include: [tenants.parts]`. Each source is keyed by hostname; the
170
+ * device takes the part under its own `name`, merged in after its own object, in this order.
171
+ */
172
+ include?: {
173
+ [I in keyof Sources]: Sources[I] & {
174
+ readonly [K in Name]?: Clash<PartOf<Sources[I], Name>, Before<PartsOf<Sources, Name>, I>> &
175
+ Part<
176
+ All<
177
+ Declared<
178
+ Ports,
179
+ Lags,
180
+ VlanInterfaces,
181
+ OtherInterfaces,
182
+ Vlans,
183
+ Acls,
184
+ Policies,
185
+ PrefixSets,
186
+ NeighborGroups,
187
+ Tables,
188
+ Vrfs,
189
+ AddressSets,
190
+ InterfaceSets,
191
+ Users,
192
+ Certificates
193
+ >,
194
+ PartsOf<Sources, Name>
195
+ >,
196
+ EthernetOf<
197
+ P,
198
+ M,
199
+ All<
200
+ Declared<
201
+ Ports,
202
+ Lags,
203
+ VlanInterfaces,
204
+ OtherInterfaces,
205
+ Vlans,
206
+ Acls,
207
+ Policies,
208
+ PrefixSets,
209
+ NeighborGroups,
210
+ Tables,
211
+ Vrfs,
212
+ AddressSets,
213
+ InterfaceSets,
214
+ Users,
215
+ Certificates
216
+ >,
217
+ PartsOf<Sources, Name>
218
+ >
219
+ >,
220
+ Net
221
+ >;
222
+ };
223
+ };
224
+ } & Device<
225
+ P,
226
+ M,
227
+ string,
228
+ All<
229
+ Declared<
230
+ Ports,
231
+ Lags,
232
+ VlanInterfaces,
233
+ OtherInterfaces,
234
+ Vlans,
235
+ Acls,
236
+ Policies,
237
+ PrefixSets,
238
+ NeighborGroups,
239
+ Tables,
240
+ Vrfs,
241
+ AddressSets,
242
+ InterfaceSets,
243
+ Users,
244
+ Certificates
245
+ >,
246
+ PartsOf<Sources, Name>
247
+ >,
248
+ EthernetOf<
249
+ P,
250
+ M,
251
+ All<
252
+ Declared<
253
+ Ports,
254
+ Lags,
255
+ VlanInterfaces,
256
+ OtherInterfaces,
257
+ Vlans,
258
+ Acls,
259
+ Policies,
260
+ PrefixSets,
261
+ NeighborGroups,
262
+ Tables,
263
+ Vrfs,
264
+ AddressSets,
265
+ InterfaceSets,
266
+ Users,
267
+ Certificates
268
+ >,
269
+ PartsOf<Sources, Name>
270
+ >
271
+ >,
272
+ Name,
273
+ Net
274
+ >
275
+ ) => Device<
276
+ P,
277
+ M,
278
+ Sites,
279
+ All<
280
+ Declared<
281
+ Ports,
282
+ Lags,
283
+ VlanInterfaces,
284
+ OtherInterfaces,
285
+ Vlans,
286
+ Acls,
287
+ Policies,
288
+ PrefixSets,
289
+ NeighborGroups,
290
+ Tables,
291
+ Vrfs,
292
+ AddressSets,
293
+ InterfaceSets,
294
+ Users,
295
+ Certificates
296
+ >,
297
+ PartsOf<Sources, Name>
298
+ >,
299
+ Record<Ports | NamesOf<Merged<PartsOf<Sources, Name>>>["port"], AnyPort>,
300
+ Name
301
+ >;
302
+
303
+ /** One device, merged with the parts its `include` gives it and registered with its network, under the file Circuit is reading. */
304
+ function addDevice(network: Registered, written: AnyDevice & { include?: readonly object[] }): AnyDevice {
305
+ const at = caller();
306
+ // Taken off the device's own object, not copied, so the object stays the one the file wrote.
307
+ const include = written.include;
308
+ delete written.include;
309
+ const device: AnyDevice = written;
310
+ resolve(device, "");
311
+ const given = (include ?? []).map((source) => resolve(source, "")) as readonly Record<string, object>[];
312
+ given.forEach((source, i) => {
313
+ if (!source || typeof source !== "object" || Array.isArray(source))
314
+ throw new Error(
315
+ `${at}: ${device.name}: include[${i}] is ${Array.isArray(source) ? "a list" : JSON.stringify(source)}, not a source keyed by hostname, such as tenants.parts.`
316
+ );
317
+ });
318
+ registry.sources.push(...given.map((source) => ({ source, device: device.name, file: at, network: network.network })));
319
+ const merged = mergeAll([device, ...given.map((source) => source[device.name])], at) as AnyDevice;
320
+ network.devices.push({ device: merged, at, file: registry.reading });
321
+ return merged;
322
+ }
323
+
324
+ /** The single items of a list given to `merge`, its nested lists opened. */
325
+ type Flat<X> = X extends readonly (infer Y)[] ? Flat<Y> : X;
326
+
327
+ /** Every key of any member of a union. */
328
+ type AllKeys<U> = U extends unknown ? keyof U : never;
329
+
330
+ // ---------- what is wrong, said at the key, in one line ----------
331
+
332
+ /** `S` with one letter left out, each way. */
333
+ type Drop1<S extends string> = S extends `${infer A}${infer B}` ? B | `${A}${Drop1<B>}` : never;
334
+
335
+ /** Whether `A` and `B` are one letter apart: one left out, one too many, one changed, or two side by side swapped. */
336
+ type Near<A extends string, B extends string> =
337
+ A extends Drop1<B> ? true : B extends Drop1<A> ? true : [Drop1<A> & Drop1<B>] extends [never] ? false : true;
338
+
339
+ /** ` Did you mean x?` for the key of `Known` one letter from `K`, or nothing. */
340
+ type DidYouMean<K extends string, Known extends string> = {
341
+ [P in Known]: Near<K, P> extends true ? P : never;
342
+ }[Known] extends infer M extends string
343
+ ? [M] extends [never]
344
+ ? ""
345
+ : ` Did you mean ${M}?`
346
+ : "";
347
+
348
+ /** What a key Circuit doesn't take says, as its type: `fast_pth is no field of a filter rule. Did you mean fast_path?` */
349
+ type NoField<K extends string, Known, Of extends string> = `${K} is no field of ${Of}.${DidYouMean<K, Known & string>}`;
350
+
351
+ type ValueAt<U, K> = U extends unknown ? (K extends keyof U ? U[K] : never) : never;
352
+ /** The item of the lists among `R`, or nothing when `R` holds no list. */
353
+ type ListItemOf<R> =
354
+ Extract<NonNullable<R>, readonly unknown[]> extends infer A
355
+ ? [A] extends [never]
356
+ ? never
357
+ : A extends readonly (infer E)[]
358
+ ? E
359
+ : never
360
+ : never;
361
+
362
+ /** The maps among `R`, without plain values or lists: what a map written there is checked against. */
363
+ type MapsOf<R> = Exclude<NonNullable<R>, string | number | boolean | bigint | symbol | readonly unknown[]>;
364
+
365
+ /** Whether `R` holds maps, alone or in a list: only there is a value looked into. */
366
+ type HoldsMaps<R> = [MapsOf<R> | MapsOf<ListItemOf<R>>] extends [never] ? false : true;
367
+
368
+ /**
369
+ * `X` as it should be, by `R`: a key `R` doesn't take becomes a sentence that says so, and a `then` `R` doesn't take becomes what it
370
+ * takes. So the editor says what is wrong at that key, in one line. Maps and lists inside are checked the same way. `Of` names where.
371
+ *
372
+ * What `R` decides alone is looked into, never a value by its own type: in a function in `lib/` a value may still be a type parameter,
373
+ * which the editor can't look into. A map keyed by name, such as VLANs, is left to the device that takes it.
374
+ */
375
+ type Fix<X, R, Of extends string> =
376
+ IsNameMap<R> extends true
377
+ ? X
378
+ : X extends readonly unknown[]
379
+ ? { [I in keyof X]: Fix<X[I], ListItemOf<R>, Of> }
380
+ : X extends object
381
+ ? {
382
+ [K in keyof X]: K extends AllKeys<MapsOf<R>>
383
+ ? K extends "then"
384
+ ? [X[K]] extends [ValueAt<MapsOf<R>, K>]
385
+ ? X[K]
386
+ : ValueAt<MapsOf<R>, K>
387
+ : HoldsMaps<ValueAt<MapsOf<R>, K>> extends true
388
+ ? Fix<X[K], ValueAt<MapsOf<R>, K>, K & string>
389
+ : X[K]
390
+ : NoField<K & string, AllKeys<MapsOf<R>>, Of>;
391
+ }
392
+ : X;
393
+
394
+ /** Whether `R` is a map keyed by name only, such as VLANs: any key, and no list beside it. */
395
+ type IsNameMap<R> = [MapsOf<R>] extends [never]
396
+ ? false
397
+ : string extends AllKeys<MapsOf<R>>
398
+ ? [ListItemOf<R>] extends [never]
399
+ ? true
400
+ : false
401
+ : false;
402
+
403
+ /** `X` when it is right by `R`, else `X` as it should be: the editor's error is then at the wrong key, and short. */
404
+ type Checked<X, R, Of extends string> = X extends Fix<X, R, Of> ? X : Fix<X, R, Of>;
405
+
406
+ /** A route policy rule's kind, by what it holds, and its words. */
407
+ type PolicyRuleOf<X> = X extends { readonly call: unknown }
408
+ ? [CallRule<string>, "a call rule"]
409
+ : X extends { readonly communities: "import" }
410
+ ? [CommunitiesImport<string>, "a communities import rule"]
411
+ : X extends { readonly communities: "export" }
412
+ ? [CommunitiesExport, "a communities export rule"]
413
+ : [RouteRule<string>, "a route policy rule"];
414
+
415
+ /**
416
+ * What item `X` of a list of `E` is checked as, and its words: a route policy rule as its own kind (`PolicyRuleOf`), a rule as the kind
417
+ * the list holds, anything else as `E`.
418
+ */
419
+ type RuleOf<X, E> =
420
+ "call" extends AllKeys<E>
421
+ ? PolicyRuleOf<X>
422
+ : "fast_path" extends AllKeys<E>
423
+ ? [E, "a filter rule"]
424
+ : "to_address" extends AllKeys<E>
425
+ ? [E, "a nat rule"]
426
+ : "rate" extends AllKeys<E>
427
+ ? [E, "an acl rule"]
428
+ : "routing_table" extends AllKeys<E>
429
+ ? [E, "a policy-routing rule"]
430
+ : [E, "an item of this list"];
431
+
432
+ /** One item of a list of `E`, checked as `RuleOf` says; a list in it, each of its items. */
433
+ type ItemChecked<X, E> = X extends readonly unknown[]
434
+ ? { [J in keyof X]: ItemChecked<X[J], E> }
435
+ : X extends object
436
+ ? Checked<X, RuleOf<X, E>[0], RuleOf<X, E>[1]>
437
+ : X;
438
+
439
+ /** Whether item `X` of `merge`, at any depth, has a key or a `then` its list's items don't take. */
440
+ type IsBadItem<X, E> = true extends (Flat<X> extends infer Y ? (Y extends ItemChecked<Y, E> ? false : true) : never) ? true : false;
441
+
442
+ /** The list a field holds, from its type `F`: never where it holds no list. A field that takes one value or a list (`OneOrMore`) holds a list. */
443
+ type ListOf<F> = Extract<NonNullable<F>, readonly unknown[]>;
444
+
445
+ /**
446
+ * An item of a list field: one of its items, or a list of them, nested as deep as you like (a part's rules, a preset). The list has
447
+ * each key of an item as `never`, so a rule's own keys still tell its kind apart (`call` a call rule), and an error names what it takes.
448
+ */
449
+ type ItemOrList<E> = E | (readonly ItemOrList<E>[] & { readonly [K in AllKeys<Extract<E, object>>]?: never });
450
+
451
+ /** Maps merged: each item checked against those before it, so a value written twice, or an ordered list from two items, is an error at the item. */
452
+ type MapItems<T extends readonly unknown[]> = { [I in keyof T]: T[I] & {} & Clash<T[I], Before<T, I>> };
453
+
454
+ /** Every item is a map: no list, no single value. */
455
+ type AllMaps<T extends readonly unknown[]> = true extends (
456
+ T[number] extends infer X ? (X extends readonly unknown[] ? true : X extends object ? false : true) : never
457
+ )
458
+ ? false
459
+ : true;
460
+
461
+ /**
462
+ * Items made into one, in order: `merge([rule_a, tenants.rules.router_input, rule_b])`. Each item is one value or a list of them, nested
463
+ * as deep as you like; you never write `...`.
464
+ *
465
+ * What it makes is decided by the field it goes in, `F`, which the editor reads where `merge` lands, as Circuit does from the model:
466
+ *
467
+ * - **a list** (a chain's rules, NAT, an ACL, a route policy, policy routing, trunk VLANs, addresses, static routes): one flat list. Each
468
+ * item is checked as one of the field's own items, the device's names included, and completes as one: a rule's keys, its `if`,
469
+ * `then`, and the names of address sets, interfaces, VLANs and policies. Two single rules, `merge([rule_a, rule_b])`, are two rules.
470
+ * An ordered list keeps every item in order; any other list keeps the same item once.
471
+ * - **a map** (a part, VLANs, interfaces, address sets, a source keyed by hostname, a tenant): merged key by key, by MODEL.md's Merge
472
+ * rules. A list inside joins, and two rule lists `part` made join in order, every rule kept; an ordered list from two items, or
473
+ * another value at one path, is an error naming both files. So `merge([tenant_a, tenant_b])` gives each part by hostname, and each rule
474
+ * list with tenant_a's rules first.
475
+ * - **no field** (a value of its own, `export const tenants = merge([tenant_a])`): maps merged when every item is a map, else a list.
476
+ *
477
+ * The same map twice in one `merge` is an error.
478
+ *
479
+ * The field is known only once the device's names are: the second signature below makes TypeScript read `merge` after the rest of the
480
+ * device, so it has them.
481
+ */
482
+ export function merge<const T extends readonly unknown[], F = unknown>(
483
+ items: unknown extends F
484
+ ? AllMaps<T> extends true
485
+ ? MapItems<T>
486
+ : T
487
+ : [ListOf<F>] extends [never]
488
+ ? MapItems<T>
489
+ : // Each item is checked and completes as the field's own; one with a wrong key or `then` has it said there, in one line
490
+ // (`ItemChecked`). Item by item: a rule that holds a `merge` of its own, such as `dst_port: merge([...])`, is read later, and
491
+ // would otherwise leave every item unchecked this way.
492
+ {
493
+ [I in keyof T]: IsBadItem<T[I], ListOf<F>[number]> extends true
494
+ ? ItemChecked<T[I] & {}, ListOf<F>[number]>
495
+ : ItemOrList<ListOf<F>[number]>;
496
+ }
497
+ ): [T] extends [never]
498
+ ? F
499
+ : unknown extends F
500
+ ? AllMaps<T> extends true
501
+ ? Merged<T>
502
+ : readonly Flat<T[number]>[]
503
+ : [ListOf<F>] extends [never]
504
+ ? Merged<T>
505
+ : ListOf<F>;
506
+ /**
507
+ * Never called: no call has two arguments. A generic signature that returns a function makes TypeScript skip `merge` in its first
508
+ * pass over the device, and read it in a second, with the names the first pass found (`SkipGenericFunctions` in its checker).
509
+ */
510
+ export function merge<X>(deferred: never, _: never): () => X;
511
+ export function merge(items: readonly unknown[]): unknown {
512
+ return made(items, caller());
513
+ }
514
+
515
+ /** The rule each kind of rule list holds, by the ordered list it goes in. */
516
+ type Rules = {
517
+ /** A firewall chain's rules: `firewall.filter.<chain>.rules`. */
518
+ filter: FilterRule<string, string, string>;
519
+ /** Address translation: `firewall.nat.source` or `.destination`. */
520
+ nat: NatRule<string, string, string>;
521
+ /** A hardware ACL: `switching.acls.<name>`. */
522
+ acl: AclRule<string>;
523
+ /** A route policy: `routing.policies.<name>`. */
524
+ policy: PolicyRule<string, string>;
525
+ /** Policy routing rules: `routing.policy_routing`. */
526
+ "policy-routing": RoutingRule<string, string>;
527
+ };
528
+
529
+ /** What `part` takes, by kind: a device's shape, or a list of one kind of rule. */
530
+ type Kinds = {
531
+ device: DevicePart;
532
+ filter: readonly Rules["filter"][];
533
+ nat: readonly Rules["nat"][];
534
+ acl: readonly Rules["acl"][];
535
+ policy: readonly Rules["policy"][];
536
+ "policy-routing": readonly Rules["policy-routing"][];
537
+ };
538
+
539
+ /**
540
+ * What `part` checks, by kind, at the wrong key: a device part has only a device's keys, at every depth; each rule only its kind's keys,
541
+ * its `if` only its kind's conditions, and its `then` one its kind takes. A route policy rule as its own kind (`PolicyRuleOf`).
542
+ */
543
+ type PartChecked<T, K> = K extends "device"
544
+ ? Checked<T, DevicePart, "a device part">
545
+ : K extends "policy"
546
+ ? T extends ItemChecked<T, Rules["policy"]>
547
+ ? T
548
+ : ItemChecked<T, Rules["policy"]>
549
+ : K extends Exclude<keyof Rules, "policy">
550
+ ? Checked<T, readonly Rules[K][], `a ${K} rule`>
551
+ : T;
552
+
553
+ /** Each kind's keys at run time, from keys.ts, which checks them against the types; and what `then` may be. */
554
+ const RULE_SHAPES: Record<string, Shape> = { filter: FILTER_RULE, nat: NAT_RULE, acl: ACL_RULE, "policy-routing": ROUTING_RULE };
555
+ const THEN: Record<string, readonly string[]> = ACTIONS;
556
+
557
+ /** The map at `path` in `value`, its list positions included: `[2].if` is the `if` of the third rule. */
558
+ const mapAt = (value: unknown, path: string): unknown =>
559
+ (path.match(/[^.[\]]+/g) ?? []).reduce<unknown>(
560
+ (v, step) => (v && typeof v === "object" ? (v as Record<string, unknown>)[step] : undefined),
561
+ value
562
+ );
563
+
564
+ /**
565
+ * A part written in one file and used in another. The kind says its shape, and the editor completes and checks it as that:
566
+ *
567
+ * - `part("device", { ... })`: a part of a device, in the device's shape, given to each device under its hostname in a source:
568
+ * `{ "edge-01": part("device", { ... }) }`. The names it refers to are checked at the device.
569
+ * - `part("filter", [...])`, `"nat"`, `"acl"`, `"policy"`, `"policy-routing"`: rules for an ordered list another file writes, such as
570
+ * each tenant's rules for the router's input chain. That file places them with `merge`: `rules: merge([own_rule, tenants.rules.router_input])`.
571
+ * Each rule has its kind's fields only. Rules made here that no device holds stop `circuit validate`, so none is forgotten.
572
+ */
573
+ export function part<const K extends "device" | "filter" | "nat" | "acl" | "policy" | "policy-routing", const T>(
574
+ kind: K,
575
+ value: Kinds[K] & PartChecked<T, K>
576
+ ): // Rules with a mistake give no rules, so the mistake is said once, at the rule, and not again where the list is placed.
577
+ K extends "device" ? T : T extends PartChecked<T, K> ? T : readonly never[] {
578
+ const file = caller();
579
+ // The line that called `part`, such as one in lib/: where a wrong key of a rule is looked for first.
580
+ const inner = innerCaller();
581
+ if (!(kind in RULE_SHAPES) && kind !== "device" && kind !== "policy")
582
+ throw new Error(`${file}: part has no kind "${kind}". The kinds: device, policy, ${Object.keys(RULE_SHAPES).join(", ")}.`);
583
+ if (kind === "policy") {
584
+ if (!Array.isArray(value)) throw new Error(`${file}: a policy part is a list of rules.`);
585
+ value.forEach((rule: unknown, i: number) => {
586
+ const problem = policyRuleProblem(rule);
587
+ if (problem) throw new Error(`${file}: rule ${i + 1} of this policy part: ${problem}`);
588
+ });
589
+ registry.rules.push({ rules: value, file });
590
+ }
591
+ const shape = RULE_SHAPES[kind];
592
+ if (shape) {
593
+ if (!Array.isArray(value)) throw new Error(`${file}: a ${kind} part is a list of rules.`);
594
+ mark(value, file, inner);
595
+ const takes = THEN[kind];
596
+ const wrong = [
597
+ ...unknownKeys(value, [shape]).map((u) => {
598
+ // `[2].if` and `protocl`: rule 3, `if.protocl`.
599
+ const [, i = "0", within = ""] = /^\[(\d+)\]\.?(.*)$/.exec(u.path) ?? [];
600
+ const key = within ? `${within}.${u.key}` : u.key;
601
+ return `${writtenAt(mapAt(value, u.path), u.key)}rule ${Number(i) + 1} of this ${kind} part: ${key}: no such field. ${unknownKeyHelp(u)}`;
602
+ }),
603
+ ...(takes
604
+ ? value.flatMap((rule: Record<string, unknown>, i: number) => {
605
+ if (takes.includes(rule.then as string)) return [];
606
+ const hint = typeof rule.then === "string" ? closest(rule.then, takes) : undefined;
607
+ return [
608
+ `${writtenAt(rule, "then")}rule ${i + 1} of this ${kind} part: then: ${JSON.stringify(rule.then)} is no then of a ${kind} rule. ${hint ? `Did you mean ${hint}?` : `It takes: ${takes.join(", ")}.`}`,
609
+ ];
610
+ })
611
+ : []),
612
+ ];
613
+ if (wrong.length) throw new Error(wrong.join("\n"));
614
+ registry.rules.push({ rules: value, file });
615
+ }
616
+ // A rule list joins another one in `merge`, every rule kept, in order: each tenant's, one after another.
617
+ return (kind === "device" ? mark(value, file, inner) : ruleList(mark(value as object, file, inner))) as never;
618
+ }
619
+
620
+ /** Marks the list `bgpNeighbors()` makes, for Circuit to fill. */
621
+ export const BGP_NEIGHBORS = Symbol("every BGP neighbor");
622
+
623
+ /**
624
+ * Every BGP neighbor address of this device, as an address set: `address_sets: { "bgp-neighbors": bgpNeighbors() }`. Circuit fills
625
+ * it when it reads the project, from `routing.bgp.neighbors`, so a new neighbor needs no second line. Write it alone as the set.
626
+ * To the editor it is a list of addresses. Nothing refers to its items by name: rules name the set (`src_set: "bgp-neighbors"`).
627
+ */
628
+ export function bgpNeighbors(): readonly IP[] {
629
+ return Object.defineProperty([], BGP_NEIGHBORS, { value: true });
630
+ }
631
+
632
+ /**
633
+ * A value written once and used as it is written, in this file or another: `literal(["exchange-a", "exchange-b"])` stays those two VLAN names,
634
+ * so a device takes it where it takes VLANs, and the editor checks each name there. Without it, the editor would see only "some text".
635
+ */
636
+ export function literal<const T>(values: T): T {
637
+ return mark(values, caller());
638
+ }
639
+
640
+ /** What a device takes from a network that writes `checks` as `Own` and `communities` as `C`. */
641
+ type FactsOf<Own, C> = {
642
+ untrusted: Own extends { readonly untrusted_vlans: object } ? true : false;
643
+ communities: C extends object ? true : false;
644
+ groups: GroupsOf<C>;
645
+ requests: RequestsOf<C>;
646
+ };
647
+
648
+ /** The network `circuit()` returns: what it was given, as written, and `device()`, typed from it. */
649
+ export type Network<Sites extends Readonly<Record<string, Site>>, Asn extends number, Own, C, P extends Prefixes> = Readonly<
650
+ NetworkSettings<Sites, Asn, Own, C, P>
651
+ > & {
652
+ /** The AS the network operates, as written. */
653
+ readonly asn: Asn;
654
+ /** The network's own prefixes, as written, for a device to use by name: `as64500.prefixes.id01.v4_1`. Empty when `circuit()` writes none. */
655
+ readonly prefixes: P;
656
+ /** Declare one device of this network, in its own file under `sites/`: `export default as64500.device({ name: "edge-01", include: [tenants.parts], ... })`. */
657
+ readonly device: DeviceOf<keyof Sites & string, FactsOf<Own, C>>;
658
+ };
659
+
660
+ /**
661
+ * Declare a network, in `circuit.ts` at the project's root: `export const as64500 = circuit({ asn: 64500, sites: { ... }, checks: { ... } })`,
662
+ * named after its AS, as we suggest. Its devices are `as64500.device({ ... })`, each in its own file under `sites/`, typed from what the network writes here: its sites,
663
+ * whether `untrusted_vlans` is on, and its `communities`. Circuit imports `circuit.ts`, then every device file, so `circuit.ts` imports no device.
664
+ *
665
+ * A project may hold several networks, such as two ASes: one `circuit()` each, each exported from `circuit.ts` under its own name. The
666
+ * export writes one JSON file per network, named after it.
667
+ *
668
+ * A check of your own is any other name in `checks`, with a function: `checks: { exports_end_in_reject: true, prefixes_stay_in_region: prefixesStayInRegion }`.
669
+ * It reads the network resolved, as its devices will run it (`ResolvedNetwork`).
670
+ */
671
+ export function circuit<
672
+ const Sites extends Readonly<Record<string, Site>>,
673
+ const Asn extends number,
674
+ const Own = {},
675
+ const C = undefined,
676
+ const P extends Prefixes = {},
677
+ >(settings: NetworkSettings<Sites, Asn, Own, C, P>): Network<Sites, Asn, Own, C, P> {
678
+ const registered: Registered = { network: {}, at: caller(), devices: [] };
679
+ registered.network = { ...settings, prefixes: settings.prefixes ?? {}, device: (device: AnyDevice) => addDevice(registered, device) };
680
+ registry.networks.push(registered);
681
+ return registered.network as never;
682
+ }
683
+
684
+ export type { Names };