@takodotid/circuit 0.34.2 → 0.35.0

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