@takodotid/circuit 0.34.3 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/README.md +14 -15
  2. package/dist/chunks/index-20kcjxjg.js +174 -0
  3. package/dist/chunks/index-2r4bbhac.js +977 -0
  4. package/dist/chunks/index-n7w3tg5y.js +149 -0
  5. package/dist/chunks/new-w3zys5y4.js +172 -0
  6. package/dist/chunks/server-gzsf5me5.js +259 -0
  7. package/dist/cli/index.js +11345 -9024
  8. package/dist/helpers/index.js +594 -0
  9. package/dist/index.js +9 -28
  10. package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
  11. package/dist/types/core/address.d.ts +44 -0
  12. package/dist/types/core/cabling.d.ts +41 -0
  13. package/dist/types/core/cross-checks.d.ts +28 -0
  14. package/dist/types/core/define.d.ts +25 -3
  15. package/dist/types/core/project-files.d.ts +12 -0
  16. package/dist/types/core/schema-docs.d.ts +13 -4
  17. package/dist/types/core/secrets.d.ts +62 -21
  18. package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
  19. package/dist/types/core/utils.d.ts +8 -0
  20. package/dist/types/core/validator.d.ts +19 -0
  21. package/dist/types/helpers/bgp.d.ts +97 -0
  22. package/dist/types/helpers/checks.d.ts +32 -0
  23. package/dist/types/helpers/exchange.d.ts +49 -0
  24. package/dist/types/helpers/filters.d.ts +51 -0
  25. package/dist/types/helpers/index.d.ts +10 -0
  26. package/dist/types/helpers/route-server.d.ts +52 -0
  27. package/dist/types/index.d.ts +5 -3
  28. package/dist/types/integrations/librenms.d.ts +60 -0
  29. package/dist/types/integrations/netbox.d.ts +89 -0
  30. package/dist/types/integrations/peeringdb.d.ts +24 -0
  31. package/dist/types/integrations/prometheus.d.ts +24 -0
  32. package/dist/types/integrations/registries.d.ts +8 -0
  33. package/dist/types/platforms/bgp-wait.d.ts +18 -0
  34. package/dist/types/platforms/catalog.d.ts +109 -0
  35. package/dist/types/platforms/derive.d.ts +17 -0
  36. package/dist/types/platforms/fortios/firewall.d.ts +45 -0
  37. package/dist/types/platforms/fortios/index.d.ts +134 -0
  38. package/dist/types/platforms/fortios/names.d.ts +52 -0
  39. package/dist/types/platforms/fortios/plan.d.ts +104 -0
  40. package/dist/types/platforms/fortios/render.d.ts +75 -0
  41. package/dist/types/platforms/fortios/routing.d.ts +56 -0
  42. package/dist/types/platforms/fortios/tree.d.ts +37 -0
  43. package/dist/types/platforms/index.d.ts +13 -0
  44. package/dist/types/platforms/junos/bgp.d.ts +44 -0
  45. package/dist/types/platforms/junos/index.d.ts +114 -0
  46. package/dist/types/platforms/junos/render.d.ts +154 -0
  47. package/dist/types/platforms/line-diff.d.ts +67 -0
  48. package/dist/types/platforms/platform.d.ts +120 -0
  49. package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
  50. package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
  51. package/dist/types/platforms/routeros/export.d.ts +59 -0
  52. package/dist/types/platforms/routeros/index.d.ts +96 -0
  53. package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
  54. package/dist/types/platforms/routeros/plan.d.ts +54 -0
  55. package/dist/types/platforms/routeros/render.d.ts +240 -0
  56. package/dist/types/platforms/vrp/index.d.ts +83 -0
  57. package/dist/types/platforms/vrp/render.d.ts +96 -0
  58. package/dist/types/schema/common.d.ts +12 -6
  59. package/dist/types/schema/index.d.ts +1 -0
  60. package/dist/types/schema/interfaces.d.ts +14 -6
  61. package/dist/types/schema/policy.d.ts +18 -10
  62. package/dist/types/schema/routing.d.ts +14 -1
  63. package/dist/types/schema/system.d.ts +16 -6
  64. package/dist/types/schema/time-zones.d.ts +2 -0
  65. package/dist/types/transport/console.d.ts +79 -23
  66. package/dist/types/transport/path-check.d.ts +32 -0
  67. package/dist/types/transport/ssh.d.ts +17 -15
  68. package/package.json +18 -12
  69. package/src/cli/graph-format.ts +91 -0
  70. package/src/cli/index.ts +1074 -484
  71. package/src/cli/new.ts +208 -175
  72. package/src/cli/rename.ts +97 -91
  73. package/src/core/address-space.ts +320 -0
  74. package/src/core/address.ts +202 -0
  75. package/src/core/cabling.ts +154 -0
  76. package/src/core/cross-checks.ts +364 -0
  77. package/src/core/define.ts +26 -3
  78. package/src/core/project-files.ts +35 -0
  79. package/src/core/schema-docs.ts +138 -130
  80. package/src/core/secrets.ts +170 -76
  81. package/src/core/topology.ts +656 -0
  82. package/src/core/utils.ts +37 -0
  83. package/src/core/validator.ts +407 -0
  84. package/src/helpers/bgp.ts +301 -0
  85. package/src/helpers/checks.ts +178 -0
  86. package/src/helpers/exchange.ts +117 -0
  87. package/src/helpers/filters.ts +107 -0
  88. package/src/helpers/index.ts +12 -0
  89. package/src/helpers/route-server.ts +212 -0
  90. package/src/index.ts +6 -3
  91. package/src/integrations/librenms.ts +291 -0
  92. package/src/integrations/netbox.ts +620 -0
  93. package/src/integrations/peeringdb.ts +173 -0
  94. package/src/integrations/prometheus.ts +90 -0
  95. package/src/integrations/registries.ts +25 -0
  96. package/src/platforms/bgp-wait.ts +41 -0
  97. package/src/platforms/catalog.ts +69 -0
  98. package/src/platforms/derive.ts +117 -0
  99. package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
  100. package/src/platforms/fortios/firewall.ts +377 -0
  101. package/src/platforms/fortios/index.ts +1060 -0
  102. package/src/platforms/fortios/names.ts +130 -0
  103. package/src/platforms/fortios/plan.ts +929 -0
  104. package/src/platforms/fortios/render.ts +779 -0
  105. package/src/platforms/fortios/routing.ts +476 -0
  106. package/src/platforms/fortios/tree.ts +129 -0
  107. package/src/platforms/index.ts +36 -0
  108. package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
  109. package/src/platforms/junos/bgp.ts +293 -0
  110. package/src/platforms/junos/index.ts +1006 -0
  111. package/src/platforms/junos/render.ts +967 -0
  112. package/src/platforms/line-diff.ts +244 -0
  113. package/src/platforms/platform.ts +122 -0
  114. package/src/platforms/raisecom-ros/TRAPS.md +40 -0
  115. package/src/platforms/raisecom-ros/index.ts +731 -0
  116. package/src/platforms/raisecom-ros/render.ts +328 -0
  117. package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
  118. package/src/platforms/routeros/export.ts +342 -0
  119. package/src/platforms/routeros/index.ts +636 -0
  120. package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
  121. package/src/platforms/routeros/plan.ts +444 -0
  122. package/src/platforms/routeros/render.ts +1483 -0
  123. package/src/platforms/vrp/TRAPS.md +45 -0
  124. package/src/platforms/vrp/index.ts +652 -0
  125. package/src/platforms/vrp/render.ts +615 -0
  126. package/src/schema/common.ts +15 -6
  127. package/src/schema/index.ts +1 -0
  128. package/src/schema/interfaces.ts +15 -5
  129. package/src/schema/policy.ts +22 -10
  130. package/src/schema/routing.ts +16 -1
  131. package/src/schema/system.ts +16 -6
  132. package/src/schema/time-zones.ts +482 -0
  133. package/src/studio/data.ts +172 -168
  134. package/src/studio/git.ts +95 -90
  135. package/src/studio/public/app.js +161 -150
  136. package/src/studio/public/compare.js +219 -212
  137. package/src/studio/public/diff.js +77 -75
  138. package/src/studio/public/dom.js +118 -108
  139. package/src/studio/public/evaluate.js +584 -545
  140. package/src/studio/public/export.js +120 -118
  141. package/src/studio/public/graph.js +1343 -1266
  142. package/src/studio/public/net.js +115 -109
  143. package/src/studio/public/tables.js +1135 -1083
  144. package/src/studio/public/tools.js +561 -529
  145. package/src/studio/server.ts +202 -196
  146. package/src/transport/console.ts +363 -271
  147. package/src/transport/path-check.ts +91 -0
  148. package/src/transport/ssh.ts +98 -94
  149. package/templates/colocation/checks.ts +11 -6
  150. package/templates/colocation/edge-01.ts +9 -9
  151. package/templates/colocation/routing.ts +2 -2
  152. package/templates/colocation/tenancy.ts +3 -3
  153. package/templates/colocation/tor-01.ts +3 -3
  154. package/templates/edge-router/checks.ts +4 -4
  155. package/templates/edge-router/edge-01.ts +5 -5
  156. package/templates/edge-router/routing.ts +2 -2
  157. package/templates/project/AGENTS.md +5 -1
  158. package/templates/single-site/checks.ts +2 -2
  159. package/dist/chunks/index-47debt16.js +0 -956
  160. package/dist/chunks/index-v8zgr7wm.js +0 -128
  161. package/dist/chunks/new-ny89s7y7.js +0 -166
  162. package/dist/chunks/server-tjbb3rqc.js +0 -251
  163. package/dist/presets/index.js +0 -394
  164. package/dist/types/adapters/devices/catalog.d.ts +0 -86
  165. package/dist/types/adapters/devices/derive.d.ts +0 -7
  166. package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
  167. package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
  168. package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
  169. package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
  170. package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
  171. package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
  172. package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
  173. package/dist/types/adapters/devices/index.d.ts +0 -3
  174. package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
  175. package/dist/types/adapters/devices/junos/index.d.ts +0 -42
  176. package/dist/types/adapters/devices/junos/render.d.ts +0 -52
  177. package/dist/types/adapters/devices/lines.d.ts +0 -56
  178. package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
  179. package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
  180. package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
  181. package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
  182. package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
  183. package/dist/types/adapters/devices/types.d.ts +0 -40
  184. package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
  185. package/dist/types/adapters/registries/index.d.ts +0 -3
  186. package/dist/types/adapters/registries/librenms.d.ts +0 -15
  187. package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
  188. package/dist/types/adapters/registries/prometheus.d.ts +0 -16
  189. package/dist/types/core/across.d.ts +0 -4
  190. package/dist/types/core/addr.d.ts +0 -34
  191. package/dist/types/core/files.d.ts +0 -6
  192. package/dist/types/core/merge.d.ts +0 -4
  193. package/dist/types/core/time.d.ts +0 -2
  194. package/dist/types/core/validate.d.ts +0 -2
  195. package/dist/types/presets/index.d.ts +0 -183
  196. package/src/adapters/devices/catalog.ts +0 -41
  197. package/src/adapters/devices/derive.ts +0 -59
  198. package/src/adapters/devices/fortios/firewall.ts +0 -362
  199. package/src/adapters/devices/fortios/index.ts +0 -923
  200. package/src/adapters/devices/fortios/names.ts +0 -115
  201. package/src/adapters/devices/fortios/plan.ts +0 -888
  202. package/src/adapters/devices/fortios/render.ts +0 -768
  203. package/src/adapters/devices/fortios/routing.ts +0 -444
  204. package/src/adapters/devices/fortios/tree.ts +0 -121
  205. package/src/adapters/devices/index.ts +0 -9
  206. package/src/adapters/devices/junos/bgp.ts +0 -269
  207. package/src/adapters/devices/junos/index.ts +0 -862
  208. package/src/adapters/devices/junos/render.ts +0 -872
  209. package/src/adapters/devices/lines.ts +0 -212
  210. package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -26
  211. package/src/adapters/devices/raisecom-ros/index.ts +0 -700
  212. package/src/adapters/devices/routeros/export.ts +0 -321
  213. package/src/adapters/devices/routeros/index.ts +0 -346
  214. package/src/adapters/devices/routeros/plan.ts +0 -398
  215. package/src/adapters/devices/routeros/render.ts +0 -1490
  216. package/src/adapters/devices/types.ts +0 -38
  217. package/src/adapters/devices/vrp/TRAPS.md +0 -26
  218. package/src/adapters/devices/vrp/index.ts +0 -865
  219. package/src/adapters/registries/index.ts +0 -19
  220. package/src/adapters/registries/librenms.ts +0 -92
  221. package/src/adapters/registries/peeringdb.ts +0 -169
  222. package/src/adapters/registries/prometheus.ts +0 -71
  223. package/src/cli/graph.ts +0 -89
  224. package/src/core/across.ts +0 -350
  225. package/src/core/addr.ts +0 -174
  226. package/src/core/addresses.ts +0 -316
  227. package/src/core/files.ts +0 -32
  228. package/src/core/graph.ts +0 -640
  229. package/src/core/merge.ts +0 -19
  230. package/src/core/time.ts +0 -15
  231. package/src/core/validate.ts +0 -348
  232. package/src/presets/index.ts +0 -579
  233. /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
  234. /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
  235. /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
  236. /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
  237. /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
  238. /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
  239. /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
  240. /package/src/{adapters/devices → platforms}/vrp/models.ts +0 -0
@@ -0,0 +1,977 @@
1
+ import {
2
+ Address2
3
+ } from "./index-n7w3tg5y.js";
4
+
5
+ // src/platforms/fortios/models.ts
6
+ var models = {
7
+ "FG-3700D": {
8
+ ports: { "40g": 4, "10g": 28, "1g": 2 },
9
+ spelling: { "40g": "port{n}", "10g": "port{n+4}", "1g": "mgmt{n}" }
10
+ }
11
+ };
12
+
13
+ // src/platforms/junos/models.ts
14
+ var models2 = {
15
+ "QFX5100-48S-6Q": {
16
+ ports: { "40g": 6, "10g": 48, "1g": 2 },
17
+ spelling: { "40g": "et-0/0/{n+47}", "10g": "xe-0/0/{n-1}", "1g": "em{n-1}" }
18
+ }
19
+ };
20
+
21
+ // src/platforms/raisecom-ros/models.ts
22
+ var models3 = {
23
+ "RAX721-C-6C48": {
24
+ ports: { "100g": 6, "25g": 24, "10g": 24 },
25
+ spelling: { "100g": "hundredgige 1/1/{n}", "25g": "twenty-fivegige 1/2/{n}", "10g": "tengigabitethernet 1/3/{n}" }
26
+ }
27
+ };
28
+
29
+ // src/platforms/routeros/models.ts
30
+ var models4 = {
31
+ "CCR2216-1G-12XS-2XQ": {
32
+ ports: { "100g": 2, "25g": 12, "1g": 1 },
33
+ spelling: { "100g": "qsfp28-{n}-1", "25g": "sfp28-{n}", "1g": "ether{n}" },
34
+ limits: { lag_members: 8, switch_port_ranges: 5 }
35
+ }
36
+ };
37
+
38
+ // src/platforms/vrp/models.ts
39
+ var models5 = {
40
+ "CE6855-48S6Q-HI": { ports: { "10g": 48, "40g": 6 }, spelling: { "10g": "10GE1/0/{n}", "40g": "40GE1/0/{n}" } }
41
+ };
42
+
43
+ // src/platforms/catalog.ts
44
+ class Catalog {
45
+ static MODELS = { routeros: models4, vrp: models5, "raisecom-ros": models3, junos: models2, fortios: models };
46
+ static ports(platform, model) {
47
+ const spec = Catalog.MODELS[platform]?.[model];
48
+ if (!spec)
49
+ throw new Error(`${platform} has no model ${model}`);
50
+ return Object.entries(spec.ports).flatMap(([speed, count]) => Array.from({ length: count }, (_, i) => `${speed}-${i + 1}`));
51
+ }
52
+ static limits(platform, model) {
53
+ const spec = Catalog.MODELS[platform]?.[model];
54
+ return spec?.limits ?? {};
55
+ }
56
+ static spell(platform, model, port) {
57
+ const spec = Catalog.MODELS[platform]?.[model];
58
+ const parts = /^(\d+[gm])-(\d+)$/.exec(port);
59
+ const template = parts && spec?.spelling[parts[1]];
60
+ if (!template)
61
+ throw new Error(`${model} has no port ${port}`);
62
+ return template.replace(/\{n([+-]\d+)?\}/, (_, shift) => String(Number(parts[2]) + Number(shift ?? 0)));
63
+ }
64
+ }
65
+
66
+ // src/core/topology.ts
67
+ class Builder {
68
+ nodes = new Map;
69
+ edges = [];
70
+ node(id, kind, label, detail = []) {
71
+ const existing = this.nodes.get(id);
72
+ if (!existing)
73
+ this.nodes.set(id, { id, kind, label, detail: [...detail] });
74
+ else
75
+ for (const line of detail)
76
+ if (!existing.detail.includes(line))
77
+ existing.detail.push(line);
78
+ return id;
79
+ }
80
+ edge(edge) {
81
+ this.edges.push(edge);
82
+ return edge;
83
+ }
84
+ }
85
+
86
+ class Topology {
87
+ static LAYERS = ["l1", "l2", "l3", "bgp", "ospf", "tunnels"];
88
+ static deviceId(name) {
89
+ return `device:${name}`;
90
+ }
91
+ static keyOf(address) {
92
+ if (!address || !Address2.isValid(address))
93
+ return;
94
+ return `${Address2.family(address)}:${Address2.toNumber(address)}`;
95
+ }
96
+ static isHostRoute(cidr) {
97
+ const { len, family } = Address2.split(cidr);
98
+ return len === Address2.bits(family);
99
+ }
100
+ static routedOf(device) {
101
+ const ports = Object.entries(device.ports ?? {}).flatMap(([name, port]) => port ? [[name, port]] : []);
102
+ const interfaces = Object.entries(device.interfaces ?? {});
103
+ return [...ports, ...interfaces];
104
+ }
105
+ static holders(devices) {
106
+ return devices.flatMap((device) => Topology.routedOf(device).flatMap(([name, settings]) => (settings.addresses ?? []).flatMap((entry) => {
107
+ const address = typeof entry === "string" ? entry : entry.address;
108
+ if (!Address2.isCidr(address))
109
+ return [];
110
+ const peer = typeof entry === "string" ? undefined : entry.peer;
111
+ return [{ device, name, address, peer, vrrp: settings.vrrp ?? [], vrf: settings.vrf }];
112
+ })));
113
+ }
114
+ static describeDevice(device) {
115
+ const addresses = Topology.routedOf(device).filter(([, settings]) => settings.addresses?.length).map(([name, settings]) => {
116
+ const list = (settings.addresses ?? []).map((entry) => typeof entry === "string" ? entry : `${entry.address} to ${entry.peer}`);
117
+ const vrf = settings.vrf ? `, VRF ${settings.vrf}` : "";
118
+ return `${name}: ${list.join(", ")}${vrf}`;
119
+ });
120
+ return [
121
+ `${device.platform} ${device.model}`,
122
+ device.connection.host ? `reached at ${device.connection.host}` : "reached through its console",
123
+ ...addresses
124
+ ];
125
+ }
126
+ static carriedBy(device, settings) {
127
+ const vlan = (name, untagged) => {
128
+ const id = name ? device.vlans?.[name]?.id : undefined;
129
+ return name && id !== undefined ? [{ name, id, untagged }] : [];
130
+ };
131
+ const trunk = (settings?.trunk_vlans ?? []).filter((name) => name !== settings?.native_vlan);
132
+ return [
133
+ ...vlan(settings?.access_vlan, true),
134
+ ...vlan(settings?.native_vlan, true),
135
+ ...trunk.flatMap((name) => vlan(name, false))
136
+ ].sort((a, b) => a.id - b.id);
137
+ }
138
+ static vlanLabel(vlans) {
139
+ if (vlans.length === 1)
140
+ return `${vlans[0].name}${vlans[0].untagged ? ", untagged" : ""}`;
141
+ if (vlans.length <= 3)
142
+ return vlans.map((vlan) => vlan.name).join(", ");
143
+ return `${vlans.length} VLANs`;
144
+ }
145
+ static cables(devices, graph) {
146
+ const byName = new Map(devices.map((device) => [device.name, device]));
147
+ const drawn = new Set;
148
+ const bundles = new Map;
149
+ for (const device of devices) {
150
+ for (const [portName, port] of Object.entries(device.ports ?? {})) {
151
+ const link = port?.link;
152
+ if (!port || !link)
153
+ continue;
154
+ const here = `${device.name} ${portName}`;
155
+ const there = `${link.device} ${link.port}`;
156
+ if (drawn.has(`${there}|${here}`))
157
+ continue;
158
+ drawn.add(`${here}|${there}`);
159
+ const far = byName.get(link.device);
160
+ const farPort = far?.ports?.[link.port];
161
+ const linksBack = farPort?.link?.device === device.name && farPort.link.port === portName;
162
+ const to = far ? Topology.deviceId(far.name) : graph.node(`missing:${link.device}`, "outside", link.device, ["named by a link, and not in the network"]);
163
+ const cableLine = `${here} to ${there}`;
164
+ const lag = port.lag;
165
+ const farLag = farPort?.lag;
166
+ const bundleKey = `${device.name} ${lag}|${link.device} ${farLag}`;
167
+ const bundle = lag && farLag && linksBack ? bundles.get(bundleKey) : undefined;
168
+ if (bundle) {
169
+ bundle.members++;
170
+ bundle.cable.detail.push(cableLine);
171
+ bundle.cable.label = `${lag} to ${farLag}, ${bundle.members} cables`;
172
+ continue;
173
+ }
174
+ const isBundle = Boolean(lag && farLag && linksBack);
175
+ const cable = graph.edge({
176
+ layer: "l1",
177
+ from: Topology.deviceId(device.name),
178
+ to,
179
+ label: isBundle ? `${lag} to ${farLag}` : `${portName} to ${link.port}`,
180
+ detail: [cableLine],
181
+ ...linksBack ? {} : { oneSided: true }
182
+ });
183
+ if (isBundle)
184
+ bundles.set(bundleKey, { cable, members: 1 });
185
+ const lagSettings = lag ? device.interfaces?.[lag] : undefined;
186
+ const carrier = lagSettings?.type === "lag" ? lagSettings : lag ? undefined : port;
187
+ const vlans = Topology.carriedBy(device, carrier);
188
+ if (!vlans.length)
189
+ continue;
190
+ graph.edge({
191
+ layer: "l2",
192
+ from: Topology.deviceId(device.name),
193
+ to,
194
+ label: Topology.vlanLabel(vlans),
195
+ detail: [
196
+ `${device.name} ${lag ?? portName}`,
197
+ ...vlans.map((vlan) => `${vlan.name}, VLAN ${vlan.id}${vlan.untagged ? ", untagged" : ""}`)
198
+ ],
199
+ vlans,
200
+ ...linksBack ? {} : { oneSided: true }
201
+ });
202
+ }
203
+ }
204
+ }
205
+ static segments(holders, owners, graph) {
206
+ const bySegment = new Map;
207
+ const pairs = new Set;
208
+ for (const holder of holders) {
209
+ if (holder.peer) {
210
+ const far = owners.get(Topology.keyOf(holder.peer) ?? "");
211
+ if (!far || far.device === holder.device)
212
+ continue;
213
+ const ends = [`${holder.device.name} ${holder.name}`, `${far.device.name} ${far.name}`].sort().join("|");
214
+ if (pairs.has(ends))
215
+ continue;
216
+ pairs.add(ends);
217
+ graph.edge({
218
+ layer: "l3",
219
+ from: Topology.deviceId(holder.device.name),
220
+ to: Topology.deviceId(far.device.name),
221
+ label: `${Address2.host(holder.address)} to ${Address2.host(far.address)}`,
222
+ detail: [`${holder.device.name} ${holder.name} ${holder.address}`, `${far.device.name} ${far.name} ${far.address}`]
223
+ });
224
+ continue;
225
+ }
226
+ if (Topology.isHostRoute(holder.address))
227
+ continue;
228
+ const segment = Address2.prefixOf(holder.address);
229
+ bySegment.set(segment, [...bySegment.get(segment) ?? [], holder]);
230
+ }
231
+ for (const [segment, members] of bySegment) {
232
+ if (new Set(members.map((member) => member.device.name)).size < 2)
233
+ continue;
234
+ const memberLines = members.map((member) => `${member.device.name} ${member.name} ${member.address}`);
235
+ const vrrpLines = members.flatMap((member) => member.vrrp.map((group) => `VRRP ${group.id} ${group.address} on ${member.device.name}, priority ${group.priority ?? 100}`));
236
+ if (members.length === 2) {
237
+ const [a, b] = members;
238
+ graph.edge({
239
+ layer: "l3",
240
+ from: Topology.deviceId(a.device.name),
241
+ to: Topology.deviceId(b.device.name),
242
+ label: segment,
243
+ detail: [...memberLines, ...vrrpLines]
244
+ });
245
+ continue;
246
+ }
247
+ const id = graph.node(`segment:${segment}`, "segment", segment, [...memberLines, ...vrrpLines]);
248
+ for (const member of members) {
249
+ graph.edge({
250
+ layer: "l3",
251
+ from: Topology.deviceId(member.device.name),
252
+ to: id,
253
+ label: `${member.name} ${Address2.host(member.address)}`,
254
+ detail: [`${member.device.name} ${member.name} ${member.address}`]
255
+ });
256
+ }
257
+ }
258
+ }
259
+ static sessions(devices, owners, graph) {
260
+ const all = devices.flatMap((device) => {
261
+ const bgp = device.routing?.bgp;
262
+ return Object.entries(bgp?.neighbors ?? {}).map(([name, neighbor]) => {
263
+ const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
264
+ return {
265
+ device,
266
+ name,
267
+ address: neighbor.address,
268
+ ownAs: bgp.asn,
269
+ remoteAs: neighbor.remote_as ?? group?.remote_as,
270
+ role: neighbor.role ?? group?.role,
271
+ description: neighbor.description ?? group?.description,
272
+ import: neighbor.import ?? group?.import,
273
+ export: neighbor.export ?? group?.export
274
+ };
275
+ });
276
+ });
277
+ const describe = (session) => {
278
+ const parts = [
279
+ session.address,
280
+ session.remoteAs ? `AS${session.remoteAs}` : "no remote AS",
281
+ session.role,
282
+ session.import ? `import ${session.import}` : "imports nothing",
283
+ session.export ? `export ${session.export}` : "exports nothing"
284
+ ];
285
+ return `${session.device.name} ${session.name}: ${parts.filter(Boolean).join(", ")}`;
286
+ };
287
+ const ownerOf = (session) => owners.get(Topology.keyOf(session.address) ?? "")?.device;
288
+ const paired = new Set;
289
+ for (const session of all) {
290
+ if (paired.has(session))
291
+ continue;
292
+ const far = ownerOf(session);
293
+ if (far && far !== session.device) {
294
+ const back = all.find((other) => other.device === far && !paired.has(other) && ownerOf(other) === session.device && Address2.family(other.address) === Address2.family(session.address));
295
+ if (back)
296
+ paired.add(back);
297
+ graph.edge({
298
+ layer: "bgp",
299
+ from: Topology.deviceId(session.device.name),
300
+ to: Topology.deviceId(far.name),
301
+ label: session.remoteAs === session.ownAs ? "iBGP" : "eBGP",
302
+ detail: [describe(session), ...back ? [describe(back)] : [`${far.name} has no session back`]],
303
+ ...back ? {} : { oneSided: true }
304
+ });
305
+ continue;
306
+ }
307
+ const outside = session.remoteAs ? graph.node(`as:${session.remoteAs}`, "outside", `AS${session.remoteAs}`, session.description ? [session.description] : []) : graph.node(`address:${session.address}`, "outside", session.address, ["a BGP neighbor without a remote AS"]);
308
+ const internal = session.remoteAs === session.ownAs;
309
+ graph.edge({
310
+ layer: "bgp",
311
+ from: Topology.deviceId(session.device.name),
312
+ to: outside,
313
+ label: [session.name, session.role].filter(Boolean).join(", "),
314
+ detail: [describe(session), ...internal ? ["internal, and no device in the network holds the address"] : []],
315
+ ...internal ? { oneSided: true } : {}
316
+ });
317
+ }
318
+ }
319
+ static adjacencies(devices, graph) {
320
+ const byKey = new Map;
321
+ for (const device of devices) {
322
+ for (const [area, { interfaces }] of Object.entries(device.routing?.ospf?.areas ?? {})) {
323
+ for (const [name, settings] of Object.entries(interfaces)) {
324
+ if (settings?.passive)
325
+ continue;
326
+ const routed = device.ports?.[name] ?? device.interfaces?.[name];
327
+ const cost = settings?.cost ? `, cost ${settings.cost}` : "";
328
+ for (const entry of routed?.addresses ?? []) {
329
+ const address = typeof entry === "string" ? entry : entry.address;
330
+ if (!Address2.isCidr(address) || Topology.isHostRoute(address))
331
+ continue;
332
+ const key = `${area} ${Address2.prefixOf(address)}`;
333
+ const member = { device, name, line: `${device.name} ${name} ${address}${cost}` };
334
+ byKey.set(key, [...byKey.get(key) ?? [], member]);
335
+ }
336
+ }
337
+ }
338
+ }
339
+ for (const [key, members] of byKey) {
340
+ const [area] = key.split(" ");
341
+ for (const [index, a] of members.entries()) {
342
+ for (const b of members.slice(index + 1)) {
343
+ if (a.device === b.device)
344
+ continue;
345
+ graph.edge({
346
+ layer: "ospf",
347
+ from: Topology.deviceId(a.device.name),
348
+ to: Topology.deviceId(b.device.name),
349
+ label: `area ${area}`,
350
+ detail: [a.line, b.line]
351
+ });
352
+ }
353
+ }
354
+ }
355
+ }
356
+ static endpointHost(endpoint) {
357
+ return endpoint.replace(/:\d+$/, "").replace(/^\[(.*)\]$/, "$1");
358
+ }
359
+ static tunnels(devices, owners, graph) {
360
+ const ends = devices.flatMap((device) => Object.entries(device.interfaces ?? {}).flatMap(([name, iface]) => iface.type === "gre" || iface.type === "vxlan" ? [{ device, name, iface }] : []));
361
+ const same = (a, b) => Topology.keyOf(a) !== undefined && Topology.keyOf(a) === Topology.keyOf(b);
362
+ const mirrors = (a, b) => a.device !== b.device && a.iface.type === b.iface.type && same(a.iface.local, b.iface.remote) && same(a.iface.remote, b.iface.local) && (a.iface.type !== "vxlan" || b.iface.type === "vxlan" && a.iface.vni === b.iface.vni);
363
+ const describe = (end) => {
364
+ const vni = end.iface.type === "vxlan" ? `, VNI ${end.iface.vni}` : "";
365
+ return `${end.device.name} ${end.name}: ${end.iface.local} to ${end.iface.remote}${vni}`;
366
+ };
367
+ const paired = new Set;
368
+ for (const end of ends) {
369
+ if (paired.has(end))
370
+ continue;
371
+ const kind = end.iface.type === "gre" ? "GRE" : "VXLAN";
372
+ const mirror = ends.find((other) => !paired.has(other) && mirrors(end, other));
373
+ if (mirror) {
374
+ paired.add(mirror);
375
+ graph.edge({
376
+ layer: "tunnels",
377
+ from: Topology.deviceId(end.device.name),
378
+ to: Topology.deviceId(mirror.device.name),
379
+ label: `${kind} ${end.name} to ${mirror.name}`,
380
+ detail: [describe(end), describe(mirror)]
381
+ });
382
+ continue;
383
+ }
384
+ const owner = owners.get(Topology.keyOf(end.iface.remote) ?? "")?.device;
385
+ if (owner && owner !== end.device) {
386
+ graph.edge({
387
+ layer: "tunnels",
388
+ from: Topology.deviceId(end.device.name),
389
+ to: Topology.deviceId(owner.name),
390
+ label: `${kind} ${end.name}`,
391
+ detail: [describe(end), `${owner.name} declares no tunnel back`],
392
+ oneSided: true
393
+ });
394
+ continue;
395
+ }
396
+ const outside = graph.node(`address:${end.iface.remote}`, "outside", end.iface.remote, end.iface.description ? [end.iface.description] : []);
397
+ graph.edge({
398
+ layer: "tunnels",
399
+ from: Topology.deviceId(end.device.name),
400
+ to: outside,
401
+ label: `${kind} ${end.name}`,
402
+ detail: [describe(end)]
403
+ });
404
+ }
405
+ Topology.wireguard(devices, owners, graph);
406
+ }
407
+ static wireguard(devices, owners, graph) {
408
+ const interfaces = devices.flatMap((device) => Object.entries(device.interfaces ?? {}).flatMap(([name, iface]) => iface.type === "wireguard" ? [{ device, name, iface }] : []));
409
+ const farEndOf = (device, peer) => interfaces.find(({ device: other, iface }) => {
410
+ if (other === device)
411
+ return false;
412
+ const tunnelAddresses = (iface.addresses ?? []).map((entry) => Address2.host(typeof entry === "string" ? entry : entry.address));
413
+ const sendsFrom = tunnelAddresses.some((address) => Address2.isValid(address) && peer.allowed_addresses.some((allowed) => Address2.isCidr(allowed) && Address2.contains(allowed, address)));
414
+ const endpoint = peer.endpoint ? Topology.endpointHost(peer.endpoint) : undefined;
415
+ const isEndpoint = endpoint !== undefined && owners.get(Topology.keyOf(endpoint) ?? "")?.device === other;
416
+ return sendsFrom || isEndpoint;
417
+ });
418
+ const drawn = new Set;
419
+ for (const { device, name, iface } of interfaces) {
420
+ const clients = [];
421
+ for (const peer of iface.peers ?? []) {
422
+ const far = farEndOf(device, peer);
423
+ if (far) {
424
+ const ends = [`${device.name} ${name}`, `${far.device.name} ${far.name}`].sort().join("|");
425
+ if (drawn.has(ends))
426
+ continue;
427
+ drawn.add(ends);
428
+ graph.edge({
429
+ layer: "tunnels",
430
+ from: Topology.deviceId(device.name),
431
+ to: Topology.deviceId(far.device.name),
432
+ label: `WireGuard ${name} to ${far.name}`,
433
+ detail: [`${device.name} ${name} peer ${peer.name}`]
434
+ });
435
+ continue;
436
+ }
437
+ if (!peer.endpoint) {
438
+ clients.push(peer);
439
+ continue;
440
+ }
441
+ const endpoint = Topology.endpointHost(peer.endpoint);
442
+ const outside = graph.node(`address:${endpoint}`, "outside", peer.name, [`WireGuard endpoint ${peer.endpoint}`]);
443
+ graph.edge({
444
+ layer: "tunnels",
445
+ from: Topology.deviceId(device.name),
446
+ to: outside,
447
+ label: `WireGuard ${name}`,
448
+ detail: [`${device.name} ${name} peer ${peer.name}, allowed ${peer.allowed_addresses.join(", ")}`]
449
+ });
450
+ }
451
+ if (!clients.length)
452
+ continue;
453
+ const outside = graph.node(`clients:${device.name}:${name}`, "outside", `${clients.length} ${clients.length === 1 ? "client" : "clients"}`, clients.map((peer) => `${peer.name}: ${peer.allowed_addresses.join(", ")}`));
454
+ graph.edge({
455
+ layer: "tunnels",
456
+ from: Topology.deviceId(device.name),
457
+ to: outside,
458
+ label: `WireGuard ${name}`,
459
+ detail: [`peers that dial in to ${device.name} ${name}`]
460
+ });
461
+ }
462
+ }
463
+ static of(devices) {
464
+ const graph = new Builder;
465
+ for (const device of devices)
466
+ graph.node(Topology.deviceId(device.name), "device", device.name, Topology.describeDevice(device));
467
+ const holders = Topology.holders(devices);
468
+ const owners = new Map;
469
+ for (const holder of holders) {
470
+ const key = Topology.keyOf(Address2.host(holder.address));
471
+ if (!owners.has(key))
472
+ owners.set(key, holder);
473
+ }
474
+ Topology.cables(devices, graph);
475
+ Topology.segments(holders, owners, graph);
476
+ Topology.sessions(devices, owners, graph);
477
+ Topology.adjacencies(devices, graph);
478
+ Topology.tunnels(devices, owners, graph);
479
+ return { nodes: [...graph.nodes.values()], edges: graph.edges };
480
+ }
481
+ static only(graph, layers, deviceNames = []) {
482
+ const named = new Set(deviceNames.map(Topology.deviceId));
483
+ const touches = (edge) => !named.size || named.has(edge.from) || named.has(edge.to);
484
+ const edges = graph.edges.filter((edge) => layers.includes(edge.layer) && touches(edge));
485
+ const ends = new Set(edges.flatMap((edge) => [edge.from, edge.to]));
486
+ const nodes = graph.nodes.filter((node) => (named.size ? named.has(node.id) : node.kind === "device") || ends.has(node.id));
487
+ return { nodes, edges };
488
+ }
489
+ }
490
+
491
+ // src/core/address-space.ts
492
+ class AddressSpace {
493
+ static isHostRoute(cidr) {
494
+ return Address2.split(cidr).len === Address2.bits(Address2.split(cidr).family);
495
+ }
496
+ static numberOf(address) {
497
+ return Address2.toNumber(address);
498
+ }
499
+ static usable(prefix) {
500
+ const { first, last, family } = Address2.range(prefix);
501
+ const { len } = Address2.split(prefix);
502
+ if (family === "ipv4" && len <= 30)
503
+ return { first: first + 1n, last: last - 1n };
504
+ if (family === "ipv6" && len < 127)
505
+ return { first: first + 1n, last };
506
+ return { first, last };
507
+ }
508
+ static merged(ranges) {
509
+ const sorted = [...ranges].sort((a, b) => a.first < b.first ? -1 : a.first > b.first ? 1 : 0);
510
+ const result = [];
511
+ for (const range of sorted) {
512
+ const previous = result.at(-1);
513
+ if (previous && range.first <= previous.last + 1n)
514
+ previous.last = range.last > previous.last ? range.last : previous.last;
515
+ else
516
+ result.push({ ...range });
517
+ }
518
+ return result;
519
+ }
520
+ static gaps(within, taken) {
521
+ const result = [];
522
+ let cursor = within.first;
523
+ for (const range of AddressSpace.merged(taken)) {
524
+ if (range.last < within.first || range.first > within.last)
525
+ continue;
526
+ if (range.first > cursor)
527
+ result.push({ first: cursor, last: range.first - 1n });
528
+ if (range.last + 1n > cursor)
529
+ cursor = range.last + 1n;
530
+ }
531
+ if (cursor <= within.last)
532
+ result.push({ first: cursor, last: within.last });
533
+ return result;
534
+ }
535
+ static count(value) {
536
+ if (value <= 1n << 20n)
537
+ return String(value);
538
+ const power = value.toString(2).length - 1;
539
+ const nearer = value - (1n << BigInt(power)) >= 1n << BigInt(power - 1) ? power + 1 : power;
540
+ return `about 2^${nearer}`;
541
+ }
542
+ static byAddress(a, b) {
543
+ const first = (use) => AddressSpace.numberOf(use.address.split("-")[0]);
544
+ return first(a) < first(b) ? -1 : first(a) > first(b) ? 1 : 0;
545
+ }
546
+ static comparePrefixes(a, b) {
547
+ const [ra, rb] = [Address2.range(a), Address2.range(b)];
548
+ if (ra.family !== rb.family)
549
+ return ra.family === "ipv4" ? -1 : 1;
550
+ return ra.first < rb.first ? -1 : ra.first > rb.first ? 1 : 0;
551
+ }
552
+ static byPrefix(a, b) {
553
+ return AddressSpace.comparePrefixes(a.prefix, b.prefix);
554
+ }
555
+ static report(devices, within) {
556
+ const segments = new Map;
557
+ const blocks = [];
558
+ const segmentOf = (address) => [...segments.keys()].find((prefix) => Address2.contains(prefix, address));
559
+ for (const holder of Topology.holders(devices)) {
560
+ const vrf = holder.device.ports?.[holder.name]?.vrf ?? holder.device.interfaces?.[holder.name]?.vrf;
561
+ const what = `${holder.device.name} ${holder.name}${vrf ? `, VRF ${vrf}` : ""}`;
562
+ if (AddressSpace.isHostRoute(holder.address) || holder.peer) {
563
+ blocks.push({ prefix: `${Address2.host(holder.address)}/${Address2.bits(Address2.family(holder.address))}`, what });
564
+ continue;
565
+ }
566
+ const prefix = Address2.prefixOf(holder.address);
567
+ const segment = segments.get(prefix) ?? { uses: [], taken: [] };
568
+ const value = AddressSpace.numberOf(Address2.host(holder.address));
569
+ segment.uses.push({ address: Address2.host(holder.address), what });
570
+ segment.taken.push({ first: value, last: value });
571
+ segments.set(prefix, segment);
572
+ for (const group of holder.vrrp) {
573
+ if (!Address2.isValid(group.address) || !Address2.contains(prefix, group.address))
574
+ continue;
575
+ if (segment.uses.some((use) => use.address === group.address))
576
+ continue;
577
+ const holders = devices.filter((device) => Object.values({ ...device.ports, ...device.interfaces }).some((settings) => settings?.vrrp?.some((other) => other.id === group.id && other.address === group.address)));
578
+ segment.uses.push({
579
+ address: group.address,
580
+ what: `VRRP ${group.id} on ${holders.map((device) => device.name).join(", ")}`
581
+ });
582
+ segment.taken.push({ first: AddressSpace.numberOf(group.address), last: AddressSpace.numberOf(group.address) });
583
+ }
584
+ }
585
+ const take = (address, what, first = address, last = address) => {
586
+ const prefix = segmentOf(first);
587
+ if (!prefix)
588
+ return false;
589
+ const segment = segments.get(prefix);
590
+ if (segment.uses.some((use) => use.address === address))
591
+ return true;
592
+ segment.uses.push({ address, what });
593
+ segment.taken.push({ first: AddressSpace.numberOf(first), last: AddressSpace.numberOf(last) });
594
+ return true;
595
+ };
596
+ for (const device of devices) {
597
+ for (const [name, server] of Object.entries(device.dhcp ?? {})) {
598
+ const [start, end] = server.pool;
599
+ if (Address2.isValid(start) && Address2.isValid(end))
600
+ take(`${start}-${end}`, `DHCP pool ${name} on ${device.name}`, start, end);
601
+ for (const reservation of server.reservations ?? []) {
602
+ if (Address2.isValid(reservation.address))
603
+ take(reservation.address, `reserved for ${reservation.description ?? reservation.mac} by ${device.name}`);
604
+ }
605
+ }
606
+ for (const route of device.routing?.static ?? []) {
607
+ if (route.via && Address2.isValid(route.via))
608
+ take(route.via, `next hop for ${route.prefix} on ${device.name}`);
609
+ }
610
+ for (const [name, neighbor] of Object.entries(device.routing?.bgp?.neighbors ?? {})) {
611
+ if (Address2.isValid(neighbor.address))
612
+ take(neighbor.address, `BGP neighbor ${name} of ${device.name}`);
613
+ }
614
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
615
+ if (iface.type !== "wireguard")
616
+ continue;
617
+ for (const peer of iface.peers ?? []) {
618
+ for (const allowed of peer.allowed_addresses) {
619
+ if (!Address2.isCidr(allowed))
620
+ continue;
621
+ const what = `WireGuard peer ${peer.name} on ${device.name} ${name}`;
622
+ if (AddressSpace.isHostRoute(allowed) && take(Address2.host(allowed), what))
623
+ continue;
624
+ if (!segmentOf(Address2.host(allowed)))
625
+ blocks.push({ prefix: Address2.prefixOf(allowed), what });
626
+ }
627
+ }
628
+ }
629
+ }
630
+ const owners = new Map;
631
+ for (const device of devices) {
632
+ for (const prefix of device.routing?.bgp?.networks ?? []) {
633
+ if (!Address2.isCidr(prefix))
634
+ continue;
635
+ owners.set(Address2.prefixOf(prefix), [...owners.get(Address2.prefixOf(prefix)) ?? [], device.name]);
636
+ }
637
+ }
638
+ for (const device of devices) {
639
+ for (const route of device.routing?.static ?? []) {
640
+ if (!Address2.isCidr(route.prefix) || Address2.split(route.prefix).len === 0)
641
+ continue;
642
+ const overlapsSegment = [...segments.keys()].some((prefix) => Address2.contains(route.prefix, prefix) || Address2.contains(prefix, route.prefix));
643
+ const coversOwned = [...owners.keys()].some((owned) => Address2.contains(route.prefix, owned));
644
+ if (overlapsSegment || coversOwned)
645
+ continue;
646
+ const how = route.blackhole ? "discarded" : route.via ? `routed via ${route.via}` : `routed out ${route.interface}`;
647
+ blocks.push({ prefix: Address2.prefixOf(route.prefix), what: `${how} by ${device.name}` });
648
+ }
649
+ }
650
+ const asked = within ? Address2.prefixOf(within) : undefined;
651
+ const spacePrefixes = asked ? [asked] : [...owners.keys()];
652
+ const spaceOf = (prefix) => spacePrefixes.filter((space) => Address2.contains(space, prefix)).sort((a, b) => Address2.split(b).len - Address2.split(a).len)[0];
653
+ const toSegment = (prefix) => {
654
+ const { uses, taken } = segments.get(prefix);
655
+ const free = AddressSpace.gaps(AddressSpace.usable(prefix), taken);
656
+ const total = free.reduce((sum, range) => sum + range.last - range.first + 1n, 0n);
657
+ const family = Address2.family(prefix);
658
+ return {
659
+ prefix,
660
+ uses: uses.sort(AddressSpace.byAddress),
661
+ free: AddressSpace.count(total),
662
+ ...free[0] ? { next: Address2.fromNumber(free[0].first, family) } : {}
663
+ };
664
+ };
665
+ const uniqueBlocks = blocks.filter((block, index) => blocks.findIndex((other) => other.prefix === block.prefix && other.what === block.what) === index);
666
+ const spaces = spacePrefixes.sort(AddressSpace.comparePrefixes).map((space) => {
667
+ const inside = (prefix) => spaceOf(prefix) === space;
668
+ const spaceSegments = [...segments.keys()].filter(inside).sort(AddressSpace.comparePrefixes);
669
+ const spaceBlocks = uniqueBlocks.filter((block) => inside(block.prefix)).sort(AddressSpace.byPrefix);
670
+ const nested = spacePrefixes.filter((other) => other !== space && Address2.contains(space, other));
671
+ const taken = [...spaceSegments, ...spaceBlocks.map((block) => block.prefix), ...nested].map((prefix) => Address2.range(prefix));
672
+ const family = Address2.family(space);
673
+ return {
674
+ prefix: space,
675
+ owner: asked && !owners.has(space) ? "asked for" : `originated by ${(owners.get(space) ?? []).join(", ")}`,
676
+ segments: spaceSegments.map(toSegment),
677
+ blocks: spaceBlocks,
678
+ free: AddressSpace.gaps(Address2.range(space), taken).flatMap((range) => Address2.prefixesBetween(range.first, range.last, family))
679
+ };
680
+ });
681
+ const outside = (prefix) => !spaceOf(prefix);
682
+ return {
683
+ spaces,
684
+ elsewhere: asked ? { segments: [], blocks: [] } : {
685
+ segments: [...segments.keys()].filter(outside).sort(AddressSpace.comparePrefixes).map(toSegment),
686
+ blocks: uniqueBlocks.filter((block) => outside(block.prefix)).sort(AddressSpace.byPrefix)
687
+ }
688
+ };
689
+ }
690
+ static freeOfLength(free, length, limit) {
691
+ const prefixes = [];
692
+ let total = 0n;
693
+ for (const prefix of free) {
694
+ const { len, family } = Address2.split(prefix);
695
+ if (len > length || length > Address2.bits(family))
696
+ continue;
697
+ const { first } = Address2.range(prefix);
698
+ const step = 1n << BigInt(Address2.bits(family) - length);
699
+ const fits = 1n << BigInt(length - len);
700
+ total += fits;
701
+ for (let index = 0n;index < fits && prefixes.length < limit; index++) {
702
+ prefixes.push(`${Address2.fromNumber(first + index * step, family)}/${length}`);
703
+ }
704
+ }
705
+ return { prefixes, total };
706
+ }
707
+ }
708
+
709
+ // src/studio/data.ts
710
+ import { existsSync, readFileSync } from "node:fs";
711
+ import { dirname, join } from "node:path";
712
+ import { fileURLToPath } from "node:url";
713
+
714
+ // src/core/schema-docs.ts
715
+ class SchemaDocs {
716
+ static FILES = [
717
+ ["device.ts", "Device"],
718
+ ["interfaces.ts", "Ports and interfaces"],
719
+ ["routing.ts", "Routing"],
720
+ ["policy.ts", "Route policies"],
721
+ ["firewall.ts", "Firewall and ACLs"],
722
+ ["system.ts", "System and management"],
723
+ ["common.ts", "Common values"]
724
+ ];
725
+ static commentText(lines) {
726
+ return lines.map((line) => line.replace(/^\s*\/\*\*\s?|\s*\*\/\s*$|^\s*\*\s?/g, "").trim()).filter(Boolean).join(" ");
727
+ }
728
+ static PARAMETER_MEANING = { Pl: "platform", M: "model" };
729
+ static withoutGenerics(type, generics) {
730
+ let result = type;
731
+ while (/NoInfer<([^<>]*)>/.test(result)) {
732
+ result = result.replace(/NoInfer<([^<>]*)>/g, (_, inner) => inner.includes("|") ? `(${inner})` : inner);
733
+ }
734
+ for (const name of generics) {
735
+ result = result.replace(new RegExp(`\\b${name}<[^<>]*(?:<[^<>]*>[^<>]*)*>`, "g"), name);
736
+ }
737
+ return result.replace(/"[^"]*"|\b(Pl|M|N|V|I|P|S|G|A|AS|U|F|C|L|K)\b/g, (found, parameter) => parameter ? SchemaDocs.PARAMETER_MEANING[parameter] ?? "name" : found).replace(/\bname(?: \| name)+\b/g, "name").replace(/\(name\)/g, "name");
738
+ }
739
+ static parse(source, generics) {
740
+ const lines = source.split(`
741
+ `);
742
+ const types = [];
743
+ let comment = [];
744
+ let inComment = false;
745
+ let current;
746
+ let depth = 0;
747
+ const parents = [];
748
+ let header;
749
+ function finishHeader() {
750
+ const text = header;
751
+ header = undefined;
752
+ const body = text.slice(text.indexOf("> =") >= 0 ? text.indexOf("> =") + 3 : text.indexOf("=") + 1).trim();
753
+ if (!body.includes("{") || body.startsWith("`")) {
754
+ current.definition = body.replace(/;$/, "").replace(/^\|\s*/, "");
755
+ } else {
756
+ const before = body.slice(0, body.indexOf("{"));
757
+ const included = before.split("&").map((part) => part.trim()).filter((part) => /^\w/.test(part));
758
+ if (included.length)
759
+ current.includes = included;
760
+ }
761
+ depth = (text.match(/{/g) ?? []).length - (text.match(/}/g) ?? []).length;
762
+ }
763
+ for (const line of lines) {
764
+ const trimmed = line.trim();
765
+ if (trimmed.startsWith("/**")) {
766
+ comment = [trimmed];
767
+ inComment = !trimmed.endsWith("*/");
768
+ continue;
769
+ }
770
+ if (inComment) {
771
+ comment.push(trimmed);
772
+ if (trimmed.endsWith("*/"))
773
+ inComment = false;
774
+ continue;
775
+ }
776
+ if (header !== undefined) {
777
+ header += ` ${trimmed}`;
778
+ if (trimmed.endsWith("{") || trimmed.endsWith(";"))
779
+ finishHeader();
780
+ continue;
781
+ }
782
+ const declaration = /^(?:export )?type (\w+)(<)?/.exec(trimmed);
783
+ if (declaration && depth === 0) {
784
+ if (declaration[2])
785
+ generics.add(declaration[1]);
786
+ current = { name: declaration[1], description: SchemaDocs.commentText(comment), fields: [] };
787
+ types.push(current);
788
+ comment = [];
789
+ header = trimmed;
790
+ if (trimmed.endsWith("{") || trimmed.endsWith(";"))
791
+ finishHeader();
792
+ continue;
793
+ }
794
+ if (!current)
795
+ continue;
796
+ const field = /^(?:readonly\s+)?("?[\w-]+"?)(\?)?:\s*(.*)$/.exec(trimmed);
797
+ if (field && depth >= 1) {
798
+ const [, name, optional, rest] = field;
799
+ const path = [...parents, name.replace(/"/g, "")].join(".");
800
+ const opensObject = rest.trim().endsWith("{");
801
+ const type = opensObject ? "object" : rest.replace(/;$/, "").trim();
802
+ current.fields.push({ path: `${path}${optional ? "?" : ""}`, type, description: SchemaDocs.commentText(comment) });
803
+ comment = [];
804
+ if (opensObject) {
805
+ parents.push(name.replace(/"/g, ""));
806
+ depth++;
807
+ }
808
+ continue;
809
+ }
810
+ const opens = (trimmed.match(/{/g) ?? []).length;
811
+ const closes = (trimmed.match(/}/g) ?? []).length;
812
+ for (let index = 0;index < closes - opens; index++) {
813
+ if (parents.length >= depth - 1 && parents.length > 0)
814
+ parents.pop();
815
+ }
816
+ depth += opens - closes;
817
+ if (depth <= 0) {
818
+ depth = 0;
819
+ parents.length = 0;
820
+ }
821
+ comment = [];
822
+ }
823
+ return types;
824
+ }
825
+ static read(sources) {
826
+ const generics = new Set;
827
+ const parsed = sources.map((source) => SchemaDocs.parse(source, generics));
828
+ for (const types of parsed) {
829
+ for (const type of types) {
830
+ if (type.definition)
831
+ type.definition = SchemaDocs.withoutGenerics(type.definition, generics);
832
+ if (type.includes)
833
+ type.includes = type.includes.map((name) => SchemaDocs.withoutGenerics(name, generics));
834
+ for (const field of type.fields)
835
+ field.type = SchemaDocs.withoutGenerics(field.type, generics);
836
+ }
837
+ }
838
+ return parsed;
839
+ }
840
+ }
841
+
842
+ // src/studio/data.ts
843
+ class StudioModel {
844
+ static build(devices, graph, texts) {
845
+ const owners = new Map;
846
+ for (const holder of Topology.holders(devices)) {
847
+ const key = Topology.keyOf(Address2.host(holder.address));
848
+ if (!owners.has(key))
849
+ owners.set(key, holder);
850
+ }
851
+ const attempt = (work, otherwise) => {
852
+ try {
853
+ return work();
854
+ } catch (error) {
855
+ return otherwise(error);
856
+ }
857
+ };
858
+ return {
859
+ graph,
860
+ devices,
861
+ ports: Object.fromEntries(devices.map((device) => [
862
+ device.name,
863
+ attempt(() => Catalog.ports(device.platform, device.model), () => [])
864
+ ])),
865
+ rendered: Object.fromEntries(devices.map((device) => [
866
+ device.name,
867
+ attempt(() => texts.render(device), (error) => `Cannot be rendered: ${error.message}`)
868
+ ])),
869
+ snapshots: Object.fromEntries(devices.flatMap((device) => {
870
+ const text = texts.snapshot(device);
871
+ return text === undefined ? [] : [[device.name, text]];
872
+ })),
873
+ files: Object.fromEntries(devices.map((device) => [device.name, texts.files(device)])),
874
+ addresses: AddressSpace.report(devices),
875
+ sessions: StudioModel.sessionsOf(devices, owners),
876
+ cables: StudioModel.cablesOf(devices),
877
+ docs: StudioModel.schemaDocs()
878
+ };
879
+ }
880
+ static packageRoot() {
881
+ let directory = dirname(fileURLToPath(import.meta.url));
882
+ while (true) {
883
+ const manifest = join(directory, "package.json");
884
+ if (existsSync(manifest) && JSON.parse(readFileSync(manifest, "utf8")).name === "@takodotid/circuit")
885
+ return directory;
886
+ const parent = dirname(directory);
887
+ if (parent === directory)
888
+ return;
889
+ directory = parent;
890
+ }
891
+ }
892
+ static sessionsOf(devices, owners) {
893
+ const all = devices.flatMap((device) => {
894
+ const bgp = device.routing?.bgp;
895
+ return Object.entries(bgp?.neighbors ?? {}).map(([name, neighbor]) => {
896
+ const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
897
+ const settings = { ...group, ...neighbor };
898
+ return {
899
+ deviceObject: device,
900
+ device: device.name,
901
+ name,
902
+ address: neighbor.address,
903
+ group: neighbor.group,
904
+ remoteAs: settings.remote_as,
905
+ role: settings.role,
906
+ import: settings.import,
907
+ export: settings.export,
908
+ description: settings.description
909
+ };
910
+ });
911
+ });
912
+ const ownerOf = (address) => owners.get(Topology.keyOf(address) ?? "")?.device;
913
+ return all.map(({ deviceObject, ...session }) => {
914
+ const far = ownerOf(session.address);
915
+ if (far && far !== deviceObject) {
916
+ const back = all.some((other) => other.deviceObject === far && ownerOf(other.address) === deviceObject && Address2.family(other.address) === Address2.family(session.address));
917
+ return { ...session, far: far.name, ...back ? {} : { oneSided: true } };
918
+ }
919
+ return { ...session, ...session.remoteAs ? { far: `AS${session.remoteAs}` } : {} };
920
+ });
921
+ }
922
+ static cablesOf(devices) {
923
+ const byName = new Map(devices.map((device) => [device.name, device]));
924
+ const drawn = new Set;
925
+ const cables = [];
926
+ const vlansOf = (device, portName) => {
927
+ const port = device.ports?.[portName];
928
+ const lag = port?.lag ? device.interfaces?.[port.lag] : undefined;
929
+ const carrier = lag?.type === "lag" ? lag : port;
930
+ const names = [carrier?.access_vlan, carrier?.native_vlan, ...carrier?.trunk_vlans ?? []].filter((name) => name !== undefined);
931
+ return [...new Set(names)].map((name) => `${name} (${device.vlans?.[name]?.id ?? "?"})`);
932
+ };
933
+ for (const device of devices) {
934
+ for (const [portName, port] of Object.entries(device.ports ?? {})) {
935
+ const link = port?.link;
936
+ if (!port || !link)
937
+ continue;
938
+ const here = `${device.name} ${portName}`;
939
+ const there = `${link.device} ${link.port}`;
940
+ if (drawn.has(`${there}|${here}`))
941
+ continue;
942
+ drawn.add(`${here}|${there}`);
943
+ const farPort = byName.get(link.device)?.ports?.[link.port];
944
+ const linksBack = farPort?.link?.device === device.name && farPort.link.port === portName;
945
+ cables.push({
946
+ device: device.name,
947
+ port: portName,
948
+ farDevice: link.device,
949
+ farPort: link.port,
950
+ ...port.lag ? { lag: port.lag } : {},
951
+ ...farPort?.lag ? { farLag: farPort.lag } : {},
952
+ ...port.description ? { description: port.description } : {},
953
+ vlans: vlansOf(device, portName),
954
+ ...linksBack ? {} : { oneSided: true }
955
+ });
956
+ }
957
+ }
958
+ return cables;
959
+ }
960
+ static schemaDocs() {
961
+ const root = StudioModel.packageRoot();
962
+ if (!root)
963
+ return {};
964
+ const sources = SchemaDocs.FILES.map(([file]) => readFileSync(join(root, "src", "schema", file), "utf8"));
965
+ const docs = {};
966
+ for (const type of SchemaDocs.read(sources).flat()) {
967
+ docs[type.name] = {
968
+ description: type.description,
969
+ includes: (type.includes ?? []).map((name) => name.replace(/<.*$/, "")),
970
+ fields: Object.fromEntries(type.fields.map((field) => [field.path.replace(/\?$/, ""), { type: field.type, description: field.description }]))
971
+ };
972
+ }
973
+ return docs;
974
+ }
975
+ }
976
+
977
+ export { Catalog, Topology, AddressSpace, StudioModel };