@takodotid/circuit 0.43.1 → 1.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/README.md +115 -37
  2. package/package.json +21 -58
  3. package/src/checks/across.ts +402 -0
  4. package/src/checks/built-in.ts +256 -0
  5. package/src/checks/device.ts +436 -0
  6. package/src/checks/index.ts +53 -0
  7. package/src/cli/advisories.ts +108 -0
  8. package/src/cli/app.ts +71 -0
  9. package/src/cli/editor-types.generated.ts +30 -0
  10. package/src/cli/editor-types.ts +69 -0
  11. package/src/cli/format.ts +124 -0
  12. package/src/cli/git-hooks.ts +79 -0
  13. package/src/cli/index.ts +350 -1170
  14. package/src/cli/migrate.ts +554 -0
  15. package/src/cli/project-commands.ts +656 -0
  16. package/src/cli/releases.ts +374 -0
  17. package/src/cli/remember.ts +339 -0
  18. package/src/cli/rename.ts +27 -33
  19. package/src/cli/secrets-password.ts +60 -0
  20. package/src/cli/secrets.ts +258 -0
  21. package/src/cli/setup.ts +300 -0
  22. package/src/cli/signing.ts +23 -0
  23. package/src/cli/text-editor.ts +268 -0
  24. package/src/cli/ui.ts +298 -0
  25. package/src/cli/version.ts +2 -0
  26. package/src/cli/versions.ts +158 -0
  27. package/src/config/address.ts +102 -0
  28. package/src/config/circuit.d.ts +30 -0
  29. package/src/config/common.ts +65 -0
  30. package/src/config/communities.ts +479 -0
  31. package/src/config/define.ts +684 -0
  32. package/src/config/device.ts +164 -0
  33. package/src/config/firewall.ts +150 -0
  34. package/src/config/index.ts +22 -0
  35. package/src/{schema → config}/interfaces.ts +121 -85
  36. package/src/config/keys.ts +434 -0
  37. package/src/config/management.ts +61 -0
  38. package/src/config/merge.ts +335 -0
  39. package/src/config/models.ts +70 -0
  40. package/src/config/monitoring.ts +64 -0
  41. package/src/config/network.ts +180 -0
  42. package/src/config/presets.ts +149 -0
  43. package/src/config/routing.ts +401 -0
  44. package/src/config/services.ts +51 -0
  45. package/src/config/switching.ts +84 -0
  46. package/src/config/system.ts +103 -0
  47. package/src/{schema → config}/time-zones.ts +1 -1
  48. package/src/core/address.ts +1 -1
  49. package/src/core/cabling.ts +8 -43
  50. package/src/core/graph.ts +40 -0
  51. package/src/{integrations → core}/registries.ts +5 -3
  52. package/src/core/secrets-file.ts +539 -0
  53. package/src/core/secrets.ts +83 -64
  54. package/src/core/topology.ts +103 -146
  55. package/src/core/utils.ts +12 -1
  56. package/src/model.ts +83 -0
  57. package/src/platforms/derive.ts +64 -37
  58. package/src/platforms/finding.ts +9 -0
  59. package/src/platforms/fortios/firewall.ts +76 -64
  60. package/src/platforms/fortios/index.ts +189 -125
  61. package/src/platforms/fortios/names.ts +51 -18
  62. package/src/platforms/fortios/render.ts +69 -62
  63. package/src/platforms/fortios/routing.ts +89 -58
  64. package/src/platforms/fortios/vdoms.ts +47 -16
  65. package/src/platforms/junos/TRAPS.md +2 -2
  66. package/src/platforms/junos/bgp.ts +63 -42
  67. package/src/platforms/junos/index.ts +122 -89
  68. package/src/platforms/junos/render.ts +139 -104
  69. package/src/platforms/platform.ts +10 -8
  70. package/src/platforms/raisecom-ros/TRAPS.md +1 -1
  71. package/src/platforms/raisecom-ros/index.ts +89 -70
  72. package/src/platforms/raisecom-ros/render.ts +49 -45
  73. package/src/platforms/routeros/TRAPS.md +3 -3
  74. package/src/platforms/routeros/index.ts +121 -60
  75. package/src/platforms/routeros/render.ts +214 -195
  76. package/src/platforms/vrp/TRAPS.md +1 -1
  77. package/src/platforms/vrp/index.ts +74 -50
  78. package/src/platforms/vrp/render.ts +67 -56
  79. package/src/project/project.ts +725 -0
  80. package/src/studio/app/env.d.ts +9 -0
  81. package/src/studio/app/index.html +18 -0
  82. package/src/studio/app/src/App.vue +95 -0
  83. package/src/studio/app/src/components/AddressesView.vue +38 -0
  84. package/src/studio/app/src/components/CablesView.vue +29 -0
  85. package/src/studio/app/src/components/CompareView.vue +160 -0
  86. package/src/studio/app/src/components/DataTable.vue +49 -0
  87. package/src/studio/app/src/components/DeviceView.vue +237 -0
  88. package/src/studio/app/src/components/DevicesView.vue +32 -0
  89. package/src/studio/app/src/components/Faceplate.vue +75 -0
  90. package/src/studio/app/src/components/GraphView.vue +282 -0
  91. package/src/studio/app/src/components/LineDiff.vue +24 -0
  92. package/src/studio/app/src/components/ProblemsView.vue +42 -0
  93. package/src/studio/app/src/components/SearchView.vue +72 -0
  94. package/src/studio/app/src/components/SecretsView.vue +112 -0
  95. package/src/studio/app/src/components/SessionsView.vue +35 -0
  96. package/src/studio/app/src/components/Settings.vue +95 -0
  97. package/src/studio/app/src/components/ToolsView.vue +118 -0
  98. package/src/studio/app/src/components/VlansView.vue +50 -0
  99. package/src/studio/app/src/lib/csv.ts +19 -0
  100. package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
  101. package/src/studio/app/src/lib/format.ts +38 -0
  102. package/src/studio/app/src/lib/net.ts +68 -0
  103. package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
  104. package/src/studio/app/src/lib/store.ts +71 -0
  105. package/src/studio/app/src/lib/tools.ts +217 -0
  106. package/src/studio/app/src/main.ts +9 -0
  107. package/src/studio/{public → app/src}/style.css +14 -4
  108. package/src/studio/app/tsconfig.json +17 -0
  109. package/src/studio/app/vite.config.ts +16 -0
  110. package/src/studio/assets.generated.ts +3 -0
  111. package/src/studio/data.ts +96 -188
  112. package/src/studio/git.ts +27 -43
  113. package/src/studio/server.ts +196 -107
  114. package/src/studio/types.ts +141 -0
  115. package/src/transport/console.ts +3 -3
  116. package/src/transport/ssh.ts +1 -1
  117. package/dist/chunks/index-3b343xva.js +0 -177
  118. package/dist/chunks/index-6wb41q67.js +0 -62
  119. package/dist/chunks/index-n7w3tg5y.js +0 -149
  120. package/dist/chunks/index-yzw86rc8.js +0 -921
  121. package/dist/chunks/new-w3zys5y4.js +0 -172
  122. package/dist/chunks/server-cxr9ynxc.js +0 -259
  123. package/dist/cli/index.js +0 -12922
  124. package/dist/helpers/index.js +0 -881
  125. package/dist/index.js +0 -28
  126. package/dist/types/core/address-space.d.ts +0 -62
  127. package/dist/types/core/address.d.ts +0 -44
  128. package/dist/types/core/cabling.d.ts +0 -41
  129. package/dist/types/core/cross-checks.d.ts +0 -28
  130. package/dist/types/core/define.d.ts +0 -79
  131. package/dist/types/core/project-files.d.ts +0 -12
  132. package/dist/types/core/schema-docs.d.ts +0 -25
  133. package/dist/types/core/secrets.d.ts +0 -62
  134. package/dist/types/core/topology.d.ts +0 -96
  135. package/dist/types/core/utils.d.ts +0 -10
  136. package/dist/types/core/validator.d.ts +0 -23
  137. package/dist/types/helpers/bgp.d.ts +0 -195
  138. package/dist/types/helpers/cables.d.ts +0 -69
  139. package/dist/types/helpers/checks.d.ts +0 -36
  140. package/dist/types/helpers/exchange.d.ts +0 -55
  141. package/dist/types/helpers/filters.d.ts +0 -51
  142. package/dist/types/helpers/index.d.ts +0 -14
  143. package/dist/types/helpers/route-server.d.ts +0 -52
  144. package/dist/types/helpers/upstreams.d.ts +0 -192
  145. package/dist/types/index.d.ts +0 -10
  146. package/dist/types/integrations/librenms.d.ts +0 -60
  147. package/dist/types/integrations/netbox.d.ts +0 -89
  148. package/dist/types/integrations/peeringdb.d.ts +0 -24
  149. package/dist/types/integrations/prometheus.d.ts +0 -24
  150. package/dist/types/integrations/registries.d.ts +0 -8
  151. package/dist/types/platforms/bgp-wait.d.ts +0 -24
  152. package/dist/types/platforms/catalog.d.ts +0 -109
  153. package/dist/types/platforms/derive.d.ts +0 -17
  154. package/dist/types/platforms/fortios/firewall.d.ts +0 -47
  155. package/dist/types/platforms/fortios/index.d.ts +0 -140
  156. package/dist/types/platforms/fortios/models.d.ts +0 -14
  157. package/dist/types/platforms/fortios/names.d.ts +0 -52
  158. package/dist/types/platforms/fortios/plan.d.ts +0 -114
  159. package/dist/types/platforms/fortios/render.d.ts +0 -77
  160. package/dist/types/platforms/fortios/routing.d.ts +0 -56
  161. package/dist/types/platforms/fortios/tree.d.ts +0 -37
  162. package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
  163. package/dist/types/platforms/index.d.ts +0 -13
  164. package/dist/types/platforms/junos/bgp.d.ts +0 -44
  165. package/dist/types/platforms/junos/index.d.ts +0 -114
  166. package/dist/types/platforms/junos/models.d.ts +0 -14
  167. package/dist/types/platforms/junos/render.d.ts +0 -154
  168. package/dist/types/platforms/line-diff.d.ts +0 -67
  169. package/dist/types/platforms/platform.d.ts +0 -135
  170. package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
  171. package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
  172. package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
  173. package/dist/types/platforms/routeros/export.d.ts +0 -59
  174. package/dist/types/platforms/routeros/index.d.ts +0 -116
  175. package/dist/types/platforms/routeros/models.d.ts +0 -18
  176. package/dist/types/platforms/routeros/plan.d.ts +0 -58
  177. package/dist/types/platforms/routeros/render.d.ts +0 -250
  178. package/dist/types/platforms/vrp/index.d.ts +0 -83
  179. package/dist/types/platforms/vrp/models.d.ts +0 -12
  180. package/dist/types/platforms/vrp/render.d.ts +0 -96
  181. package/dist/types/schema/common.d.ts +0 -30
  182. package/dist/types/schema/device.d.ts +0 -126
  183. package/dist/types/schema/firewall.d.ts +0 -131
  184. package/dist/types/schema/index.d.ts +0 -8
  185. package/dist/types/schema/interfaces.d.ts +0 -205
  186. package/dist/types/schema/policy.d.ts +0 -107
  187. package/dist/types/schema/routing.d.ts +0 -161
  188. package/dist/types/schema/system.d.ts +0 -198
  189. package/dist/types/schema/time-zones.d.ts +0 -2
  190. package/dist/types/transport/console.d.ts +0 -115
  191. package/dist/types/transport/path-check.d.ts +0 -32
  192. package/dist/types/transport/ssh.d.ts +0 -27
  193. package/src/cli/graph-format.ts +0 -91
  194. package/src/cli/new.ts +0 -234
  195. package/src/core/address-space.ts +0 -320
  196. package/src/core/cross-checks.ts +0 -369
  197. package/src/core/define.ts +0 -124
  198. package/src/core/project-files.ts +0 -35
  199. package/src/core/schema-docs.ts +0 -174
  200. package/src/core/validator.ts +0 -508
  201. package/src/helpers/bgp.ts +0 -473
  202. package/src/helpers/cables.ts +0 -74
  203. package/src/helpers/checks.ts +0 -201
  204. package/src/helpers/exchange.ts +0 -123
  205. package/src/helpers/filters.ts +0 -107
  206. package/src/helpers/index.ts +0 -25
  207. package/src/helpers/route-server.ts +0 -212
  208. package/src/helpers/upstreams.ts +0 -369
  209. package/src/index.ts +0 -13
  210. package/src/integrations/librenms.ts +0 -291
  211. package/src/integrations/netbox.ts +0 -620
  212. package/src/integrations/peeringdb.ts +0 -173
  213. package/src/integrations/prometheus.ts +0 -90
  214. package/src/schema/common.ts +0 -41
  215. package/src/schema/device.ts +0 -145
  216. package/src/schema/firewall.ts +0 -140
  217. package/src/schema/index.ts +0 -8
  218. package/src/schema/policy.ts +0 -115
  219. package/src/schema/routing.ts +0 -173
  220. package/src/schema/system.ts +0 -203
  221. package/src/studio/public/app.js +0 -186
  222. package/src/studio/public/compare.js +0 -250
  223. package/src/studio/public/dom.js +0 -138
  224. package/src/studio/public/evaluate.js +0 -687
  225. package/src/studio/public/export.js +0 -130
  226. package/src/studio/public/graph.js +0 -1541
  227. package/src/studio/public/index.html +0 -83
  228. package/src/studio/public/net.js +0 -134
  229. package/src/studio/public/tables.js +0 -1289
  230. package/src/studio/public/tools.js +0 -610
  231. package/templates/colocation/checks.ts +0 -53
  232. package/templates/colocation/circuit.config.ts +0 -9
  233. package/templates/colocation/edge-01.ts +0 -133
  234. package/templates/colocation/routing.ts +0 -13
  235. package/templates/colocation/tenancy.ts +0 -76
  236. package/templates/colocation/tenants/acme.ts +0 -19
  237. package/templates/colocation/tenants/index.ts +0 -6
  238. package/templates/colocation/tor-01.ts +0 -33
  239. package/templates/edge-router/checks.ts +0 -20
  240. package/templates/edge-router/circuit.config.ts +0 -8
  241. package/templates/edge-router/edge-01.ts +0 -110
  242. package/templates/edge-router/routing.ts +0 -13
  243. package/templates/project/AGENTS.md +0 -51
  244. package/templates/project/CLAUDE.md +0 -1
  245. package/templates/project/README.md +0 -16
  246. package/templates/project/gitignore +0 -5
  247. package/templates/project/tsconfig.json +0 -13
  248. package/templates/single-site/checks.ts +0 -26
  249. package/templates/single-site/circuit.config.ts +0 -8
  250. package/templates/single-site/router.ts +0 -86
  251. package/templates/single-site/site.ts +0 -18
  252. package/templates/single-site/switch.ts +0 -34
  253. /package/src/studio/{public → app/public}/favicon.svg +0 -0
  254. /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
  255. /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
  256. /package/src/studio/{public → app/public}/special-gothic-license.txt +0 -0
@@ -0,0 +1,30 @@
1
+ // Written by scripts/editor-types.ts from src/config and each platform's models.ts: do not edit.
2
+
3
+ export const EDITOR_FILES: Record<string, string> = {
4
+ "config/address.ts": "// `address`, a global beside `presets`: standard IP math for a project's own checks, IPv4 prefixes by RFC 4632 and IPv6 by RFC 4291,\n// each address written back in its standard form (RFC 5952 for IPv6). Each helper takes an address alone, a /32 or a /128, or with its length.\n\nimport type { Family, IP, Prefix } from \"./common\";\n\n/** An address alone, such as `10.0.0.1`, or with its length, such as `10.0.0.0/24` or `10.0.0.1/24`. */\nexport type AddressOrPrefix = IP | Prefix;\n\n/** An address or prefix as numbers: the address written, and the first and last address of its range. */\nfunction parse(text: string) {\n const bad = () => new Error(`address: ${JSON.stringify(text)} is not an IPv4 or IPv6 address, alone or with its length.`);\n const [ip = \"\", length, extra] = text.split(\"/\");\n const v6 = ip.includes(\":\");\n const bits = v6 ? 128 : 32;\n const len = length === undefined ? bits : /^\\d{1,3}$/.test(length) ? Number(length) : -1;\n if (extra !== undefined || len < 0 || len > bits) throw bad();\n\n let value = 0n;\n if (v6) {\n // An IPv4 address may end an IPv6 one, `::ffff:192.0.2.1` (RFC 4291, 2.2): it is the last two groups.\n let text6 = ip;\n const tail = /(\\d+\\.\\d+\\.\\d+\\.\\d+)$/.exec(ip);\n if (tail) {\n const v4 = parse(tail[1]!).address;\n text6 = `${ip.slice(0, -tail[1]!.length)}${(v4 >> 16n).toString(16)}:${(v4 & 0xffffn).toString(16)}`;\n }\n const halves = text6.split(\"::\");\n const groups = (s: string) => (s === \"\" ? [] : s.split(\":\"));\n const head = groups(halves[0]!);\n const rest = halves.length === 2 ? groups(halves[1]!) : [];\n const zeros = 8 - head.length - rest.length;\n // `::` stands for one or more groups of zeros.\n if (halves.length > 2 || (halves.length === 2 ? zeros < 1 : zeros !== 0)) throw bad();\n const all = [...head, ...Array<string>(halves.length === 2 ? zeros : 0).fill(\"0\"), ...rest];\n if (all.some((g) => !/^[0-9a-f]{1,4}$/i.test(g))) throw bad();\n for (const g of all) value = (value << 16n) + BigInt(parseInt(g, 16));\n } else {\n const parts = ip.split(\".\");\n if (parts.length !== 4 || parts.some((p) => !/^\\d{1,3}$/.test(p) || Number(p) > 255)) throw bad();\n for (const p of parts) value = (value << 8n) + BigInt(p);\n }\n const size = 1n << BigInt(bits - len);\n const first = value - (value % size);\n return { family: (v6 ? \"ipv6\" : \"ipv4\") as Family, address: value, first, last: first + size - 1n };\n}\n\n/** A number written as an address of `family`: dotted for IPv4; for IPv6 lowercase, no leading zeros, the longest run of two or more zero groups as `::`, the first if two tie (RFC 5952). */\nfunction format(family: Family, n: bigint): IP {\n if (family === \"ipv4\") return [24n, 16n, 8n, 0n].map((s) => String((n >> s) & 255n)).join(\".\") as IP;\n const groups = [112n, 96n, 80n, 64n, 48n, 32n, 16n, 0n].map((s) => ((n >> s) & 0xffffn).toString(16));\n let best = { at: -1, length: 1 };\n for (let i = 0; i < 8; i++) {\n let j = i;\n while (j < 8 && groups[j] === \"0\") j++;\n if (j - i > best.length) best = { at: i, length: j - i };\n }\n if (best.at < 0) return groups.join(\":\") as IP;\n return `${groups.slice(0, best.at).join(\":\")}::${groups.slice(best.at + best.length).join(\":\")}` as IP;\n}\n\n/**\n * IP math for your own checks, by RFC 4632 (IPv4) and RFC 4291 (IPv6). Each takes an address alone (a /32 or a /128) or with its\n * length. An address it can't read is an error naming it.\n */\nexport const address = {\n /** Whether `inner` lies wholly inside `outer`, of the same family: `address.contains(\"10.0.0.0/8\", \"10.1.0.0/16\")` is true. */\n contains(outer: AddressOrPrefix, inner: AddressOrPrefix): boolean {\n const a = parse(outer);\n const b = parse(inner);\n return a.family === b.family && a.first <= b.first && b.last <= a.last;\n },\n\n /** Whether `a` and `b` share any address: one contains the other. */\n overlaps(a: AddressOrPrefix, b: AddressOrPrefix): boolean {\n const x = parse(a);\n const y = parse(b);\n return x.family === y.family && x.first <= y.last && y.first <= x.last;\n },\n\n /** `ipv4` or `ipv6`. */\n family(x: AddressOrPrefix): Family {\n return parse(x).family;\n },\n\n /** The address without its length, in its standard form: `address.host(\"2001:db8:0:0::33/64\")` is `2001:db8::33`. */\n host(x: AddressOrPrefix): IP {\n const p = parse(x);\n return format(p.family, p.address);\n },\n\n /** The first address of a prefix, its network address: `address.first(\"192.0.2.33/27\")` is `192.0.2.32`. */\n first(x: AddressOrPrefix): IP {\n const p = parse(x);\n return format(p.family, p.first);\n },\n\n /** The last address of a prefix: `address.last(\"192.0.2.32/27\")` is `192.0.2.63`. */\n last(x: AddressOrPrefix): IP {\n const p = parse(x);\n return format(p.family, p.last);\n },\n};\n",
5
+ "config/circuit.d.ts": "// Circuit's functions, `presets` and `address`, declared as globals, so a project's files use them with no import. The project's tsconfig.json\n// includes this file (Circuit writes both). Each is the same as index.ts gives, which Circuit puts on `globalThis` when it reads a project.\n\nimport type * as Circuit from \"./index\";\n\ndeclare global {\n /**\n * Declare a network, in `circuit.ts` at the project's root: `export const as64500 = circuit({ asn, prefixes, sites, checks, integrations })`, named after its AS. Each\n * device is `as64500.device({ name, include: [sources], ... })` in its own file under `sites/`, typed from the network. Several networks: one `circuit()` each.\n */\n const circuit: typeof Circuit.circuit;\n /** A part written in one file and used in another: `part(\"device\", {...})`, or a rule list such as `part(\"filter\", [...])`. */\n const part: typeof Circuit.part;\n /** Items made into one list or one map, by the field it lands in. */\n const merge: typeof Circuit.merge;\n /** A value written once and kept as written. */\n const literal: typeof Circuit.literal;\n /** A value read by name when a device is applied. */\n const secret: typeof Circuit.secret;\n /** A value read from a file when a device is applied. */\n const secretFile: typeof Circuit.secretFile;\n /** An address set Circuit fills with every BGP neighbor address of the device. */\n const bgpNeighbors: typeof Circuit.bgpNeighbors;\n /** Values every network writes the same way, each from a public standard. */\n const presets: typeof Circuit.presets;\n /** IP math for your own checks (RFC 4632, RFC 4291): `address.contains`, `.overlaps`, `.family`, `.host`, `.first`, `.last`. */\n const address: typeof Circuit.address;\n}\n\nexport {};\n",
6
+ "config/common.ts": "// Shapes every group shares: secrets, addresses, ordered rules, lists in a match.\n\n/**\n * A value kept out of the repository and resolved only when a device is applied.\n *\n * `secret(\"NAME\")` reads a secret from the network's secrets file, `.circuit/secrets`, set with `circuit secrets`, or from the environment; a value written as `op://vault/item/field` is read from 1Password. `secretFile(\"path\")` reads a file, relative to the project root, for a value that spans lines such as a private key in PEM. Keep that file out of git.\n */\nexport type Secret =\n | {\n /** Name of the secret, as `circuit secrets set NAME` sets it. */\n readonly secret: string;\n }\n | {\n /** Path of the file with the value, relative to the project root. */\n readonly secret_file: string;\n };\n\n/** An IPv4 address, four numbers with dots between them, for example `10.0.0.1`. The type checks the shape; `circuit validate` checks each number is 0 to 255. */\nexport type IPv4 = `${number}.${number}.${number}.${number}`;\n\n/** An IPv6 address, groups with colons between them, for example `2001:db8::1`. The type only checks there is a colon; `circuit validate` checks the rest. */\nexport type IPv6 = `${string}:${string}`;\n\n/** An address without a length, IPv4 or IPv6, for example `10.0.0.1` or `2001:db8::1`. */\nexport type IP = IPv4 | IPv6;\n\n/** A network with its length, for example `10.0.0.0/24` or `2001:db8::/32`. */\nexport type Prefix = `${IP}/${number}`;\n\n/** An interface address with its length, for example `10.0.0.1/24` or `2001:db8::1/64`. */\nexport type InterfaceAddress = `${IP}/${number}`;\n\n/**\n * One address, alone or with its length: `10.0.0.1` or `10.0.0.1/24`. A field that names one host takes either, so an address written\n * once with its length (on a site, for an interface) is used as it is. Circuit writes the address alone there.\n */\nexport type HostAddress = IP | InterfaceAddress;\n\n/** A MAC address, six pairs of hex digits with colons between them, for example `02:00:00:00:00:01`. */\nexport type MacAddress = `${string}:${string}:${string}:${string}:${string}:${string}`;\n\n/** An address family. */\nexport type Family = \"ipv4\" | \"ipv6\";\n\n/** Bits per second, for example `2G`, `500M` or `64k`. */\nexport type Rate = `${number}${\"k\" | \"M\" | \"G\"}`;\n\n/** A log severity, most severe first. A threshold includes everything above it. */\nexport type Severity = \"emergency\" | \"alert\" | \"critical\" | \"error\" | \"warning\" | \"notice\" | \"info\" | \"debug\";\n\n/** A TCP or UDP port number, 1 to 65535. */\nexport type PortNumber = number;\n\n/**\n * Rules where order decides: the first runs first. An ordered list is written whole in one place, the device or one part, and\n * places rules from other files where they go with `merge`: `merge([own_rule, tenants.rules.router_input, last_rule])`. The same list from two places\n * is an error naming both.\n */\nexport type Ordered<Rule> = readonly Rule[];\n\n/** One value, or a list of them. In a rule's match, a list matches any of them: Circuit writes one rule for each, where the platform has no list of its own. */\nexport type OneOrMore<T> = T | readonly T[];\n\n/** Entries keyed by name, such as VLANs or policies. A name written here is one the device declares: what its other fields may refer to. */\nexport type Named<K extends string, V> = { [N in K]?: V };\n",
7
+ "config/communities.ts": "// A network's BGP large communities, `asn:code:value` (RFC 8092), used as RFC 8195 describes. The scheme is written once, in\n// `circuit({ communities })`; a neighbor group says which groups it is in, and a policy places `{ communities: \"import\" }` or\n// `{ communities: \"export\" }`. Circuit writes the catalogue from the scheme, and each such rule as the rules it means for the neighbors\n// that use the policy.\n\nimport type { Community } from \"./network\";\nimport type { LargeCommunity, StandardCommunity } from \"./routing\";\n\n/** One option of the scheme. */\nexport type CommunityOption = {\n /** The middle number of `asn:code:value` (RFC 8195 calls it the function). */\n code: number;\n /** The catalogue's words for it, in place of Circuit's. Each option says which line they are. */\n description?: string;\n};\n\n/** A group of neighbors, such as `transit`. */\nexport type CommunityGroup = {\n /** The last number of a community about the group, such as the `2` of `64500:1:2`. Neighbors write it into their routers: keep it. */\n value: number;\n /** How the catalogue names the group, such as `an IP transit`. Its name with `a` or `an` when absent: `a transit`. */\n description?: string;\n};\n\n/**\n * The network's large communities, `asn:code:value`. Write the options the network offers, and leave out the rest. Each option is an\n * object with its `code`, and may have its own `description` for the catalogue.\n */\nexport type CommunityScheme = {\n /** The first number of every community. The network's `asn` when absent. */\n asn?: number;\n /** Groups of neighbors, by name, such as `{ transit: { value: 1 }, exchange: { value: 2 } }`. A neighbor group says which it is in. Needed by the `_group` options. */\n groups?: Readonly<Record<string, CommunityGroup>>;\n /** A tag: the groups of the neighbor a route was learned from, `asn:code:<group value>`. `description` is the words before the group: `Learned from` when absent. */\n learned_from_group?: CommunityOption;\n /** A tag: the site a route was learned at, `asn:code:<site number>`, from the site's `number` in `sites`. `description` is the `nnn` line: `Learned at site $0` when absent. */\n learned_at_site?: CommunityOption;\n /** A tag: the AS a route was learned from, `asn:code:<asn>`. Never on a customer's routes, so customers share one import policy. `description` is the `nnn` line: `Learned from AS$0` when absent. */\n learned_from_as?: CommunityOption;\n /** A request: do not announce to an AS, `asn:code:<asn>`, or to anyone, `asn:code:0`. `description` is the `nnn` line: `Do not announce to AS$0` when absent. */\n do_not_announce_to_as?: CommunityOption;\n /** A request: do not announce to the neighbors of a group, `asn:code:<group value>`. `description` is the words before the group: `Do not announce to` when absent. */\n do_not_announce_to_group?: CommunityOption;\n /** A request: prepend our AS toward an AS once, twice or three times, `asn:code:<asn>`. Each `description` is its `nnn` line: `Prepend once to AS$0` when absent. */\n prepend?: { once?: CommunityOption; twice?: CommunityOption; three_times?: CommunityOption };\n /** A request: drop all traffic to the route, `asn:code:0`. A neighbor group with its own `blackhole` community is passed the route with it, so it drops the traffic too; any other never gets the route. `description` is its line: `Blackhole: dropped in AS<asn>` when absent. */\n blackhole?: CommunityOption;\n /** Your own words for one community of the catalogue, by the community as the catalogue writes it: `{ \"64500:2:0\": \"Learned at site-a\" }`. */\n custom_descriptions?: Readonly<Record<string, string>>;\n};\n\n/** The options a neighbor sends to ask something of us, which an import may accept. */\nexport type CommunityRequest = \"do_not_announce_to_as\" | \"do_not_announce_to_group\" | \"prepend\" | \"blackhole\";\n\n/** The requests a scheme `C` offers. */\nexport type RequestsOf<C> = C extends object ? Extract<keyof C, CommunityRequest> : never;\n\n/** The group names a scheme `C` writes. */\nexport type GroupsOf<C> = C extends { readonly groups: infer G } ? keyof G & string : never;\n\n/**\n * The scheme as written, `C`, and an error at each option that needs what is not written: a `_group` option without `groups`, and\n * `learned_at_site` while no site has a `number` (`Numbered`). The editor still offers them. A key that is no option is an error too.\n */\nexport type SchemeCheck<C, Numbered extends boolean> = {\n [K in Exclude<keyof C, keyof CommunityScheme> & string]: `${K} is no option of communities`;\n} & (C extends {\n readonly groups: object;\n}\n ? unknown\n : {\n learned_from_group?: \"learned_from_group needs communities.groups\";\n do_not_announce_to_group?: \"do_not_announce_to_group needs communities.groups\";\n }) &\n (Numbered extends true ? unknown : { learned_at_site?: \"learned_at_site needs a number on a site, in sites\" });\n\n/** What a neighbor, or its group, says about communities. `G` is the scheme's groups. */\nexport type NeighborCommunities<G extends string> = {\n /** The groups it is in, from the network's `communities.groups`. Its routes are tagged with each, and a request about a group acts on it. */\n groups?: readonly ([G] extends [never] ? \"needs communities.groups in circuit.ts\" : G)[];\n /** Its own blackhole community, when it takes blackholes from us, such as `64501:666`. A route asked to be blackholed is passed to it with this. */\n blackhole?: StandardCommunity | LargeCommunity;\n};\n\n/**\n * A route policy rule that Circuit writes out, in an import: it takes off the requests a neighbor may not send, and the tags only we\n * set, then tags the route with where it was learned. `R` is the requests the scheme offers.\n */\nexport type CommunitiesImport<R extends string> = {\n /** The rules for an import. */\n communities: \"import\";\n /** The requests this neighbor may send, which stay on the route. None when absent: every request is taken off (RFC 7454). */\n accept_requests?: readonly R[];\n /** Tag the route with where it was learned: its groups, the site, its AS (not a customer's). `true` when absent. `false` leaves the tags as they came, for iBGP, where our own router set them. */\n add_tags?: boolean;\n /** A communities rule holds nothing else. */\n call?: never;\n};\n\n/**\n * A route policy rule that Circuit writes out, in an export, before the rules that accept: it does what routes ask (do not announce,\n * prepend, blackhole) toward the neighbor, then takes our communities off.\n */\nexport type CommunitiesExport = {\n /** The rules for an export. */\n communities: \"export\";\n /** Keep our communities on the route. `false` when absent. Toward iBGP, write it: `true` sends every tag and request on, as it is; without it, `circuit validate` stops. */\n keep_ours?: boolean;\n /** Keep our tags on the route, for a neighbor that wants to see where a route was learned; the requests are still taken off. `false` when absent. */\n send_tags?: boolean;\n /** A communities rule holds nothing else. */\n call?: never;\n};\n\n/** A route policy rule Circuit writes out from the scheme, when the network writes `communities`. */\nexport type CommunitiesRule<R extends string> = CommunitiesImport<R> | CommunitiesExport;\n\n// ---------- what Circuit does with them ----------\n\n// The network and a device's JSON, walked without their types.\ntype Json = any;\n\nconst PREPENDS = [\n [\"once\", 1],\n [\"twice\", 2],\n [\"three_times\", 3],\n] as const;\nconst REQUESTS: CommunityRequest[] = [\"do_not_announce_to_as\", \"do_not_announce_to_group\", \"prepend\", \"blackhole\"];\nconst TAGS = [\"learned_from_group\", \"learned_at_site\", \"learned_from_as\"];\n\nconst article = (word: string) => `${/^[aeiou]/i.test(word) ? \"an\" : \"a\"} ${word}`;\n\n/** The codes of the options named, a prepend's three included. */\nconst codesOf = (s: Json, names: string[]): number[] =>\n names.flatMap((n) =>\n n === \"prepend\" ? PREPENDS.flatMap(([p]) => (s.prepend?.[p] ? [s.prepend[p].code] : [])) : s[n] ? [s[n].code] : []\n );\n\n/** Every community of the scheme as the catalogue lists it, `nnn` for any number, with the scheme's own words where it has them. */\nexport function catalogue(network: Json): Community[] {\n const s = network.communities;\n if (!s) return [];\n const asn = s.asn ?? network.asn;\n const line = (option: Json, value: number | string, text: string) => ({\n community: `${asn}:${option.code}:${value}`,\n description: text,\n });\n const groups = Object.entries<Json>(s.groups ?? {});\n const named = (name: string, group: Json) => group.description ?? article(name);\n const sites = Object.entries<Json>(network.sites ?? {}).filter(([, site]) => site.number !== undefined);\n const o = s;\n const lines = [\n ...(o.learned_from_group\n ? groups.map(([n, g]) =>\n line(o.learned_from_group, g.value, `${o.learned_from_group.description ?? \"Learned from\"} ${named(n, g)}`)\n )\n : []),\n ...(o.learned_at_site\n ? [\n ...sites.map(([n, site]) => line(o.learned_at_site, site.number, `Learned at ${n}`)),\n line(o.learned_at_site, \"nnn\", o.learned_at_site.description ?? \"Learned at site $0\"),\n ]\n : []),\n ...(o.learned_from_as ? [line(o.learned_from_as, \"nnn\", o.learned_from_as.description ?? \"Learned from AS$0\")] : []),\n ...(o.do_not_announce_to_as\n ? [\n line(o.do_not_announce_to_as, 0, \"Do not announce to anyone\"),\n line(o.do_not_announce_to_as, \"nnn\", o.do_not_announce_to_as.description ?? \"Do not announce to AS$0\"),\n ]\n : []),\n ...(o.do_not_announce_to_group\n ? groups.map(([n, g]) =>\n line(\n o.do_not_announce_to_group,\n g.value,\n `${o.do_not_announce_to_group.description ?? \"Do not announce to\"} ${named(n, g)}`\n )\n )\n : []),\n ...PREPENDS.flatMap(([p]) =>\n o.prepend?.[p] ? [line(o.prepend[p], \"nnn\", o.prepend[p].description ?? `Prepend ${p.replace(\"_\", \" \")} to AS$0`)] : []\n ),\n ...(o.blackhole ? [line(o.blackhole, 0, o.blackhole.description ?? `Blackhole: dropped in AS${asn}`)] : []),\n ];\n return lines.map((l) => ({ ...l, description: s.custom_descriptions?.[l.community] ?? l.description }));\n}\n\n/** What is wrong with the scheme, which the editor may have missed. */\nexport function schemeProblems(network: Json): string[] {\n const s = network.communities;\n if (!s) return [];\n const found: string[] = [];\n for (const option of [\"learned_from_group\", \"do_not_announce_to_group\"])\n if (s[option] && !s.groups) found.push(`communities.${option} needs communities.groups.`);\n if (s.learned_at_site && !Object.values<Json>(network.sites ?? {}).some((site) => site.number !== undefined))\n found.push(\"communities.learned_at_site needs a number on a site, in sites.\");\n const listed = new Set(catalogue(network).map((l) => l.community));\n for (const community of Object.keys(s.custom_descriptions ?? {}))\n if (!listed.has(community))\n found.push(\n `communities.custom_descriptions: ${community} is no community of the scheme. The catalogue has ${[...listed].join(\", \")}.`\n );\n return found;\n}\n\n/** One neighbor as a communities rule sees it: its AS, its groups, its blackhole community, and whether it is a customer. */\ntype Peer = { asn: number; groups: string[]; blackhole?: string; customer: boolean; ibgp: boolean };\n\n/**\n * The rules one communities rule is written as, toward one neighbor: `all` in place, for a policy one AS uses; for a policy several\n * ASes use, `own`, the rules of this AS, which run first in a policy of their own, and `shared`, the same for every AS.\n */\ntype Written = { all: Json[]; own: Json[]; shared: Json[] };\n\n/**\n * An import: takes off the requests not accepted and the tags, adds the tags, and blackholes a route that asks, when accepted. A\n * customer's routes get no `learned_from_as` tag (decision 50), so the customers share one import policy.\n */\nfunction importRules(s: Json, asn: number, rule: Json, to: Peer, site: number | undefined, groupsShared: boolean): Written {\n const accepted: string[] = rule.accept_requests ?? [];\n const tags = rule.add_tags !== false;\n const off = codesOf(s, [...(tags ? TAGS : []), ...REQUESTS.filter((r) => !accepted.includes(r))]);\n const everything = tags && !REQUESTS.some((r) => accepted.includes(r) && (r === \"prepend\" ? s.prepend : s[r]));\n const asTag = !!s.learned_from_as && !to.customer;\n const add = tags\n ? [\n ...(s.learned_from_group ? to.groups.map((g) => `${asn}:${s.learned_from_group.code}:${s.groups[g].value}`) : []),\n ...(s.learned_at_site ? [`${asn}:${s.learned_at_site.code}:${site}`] : []),\n ...(asTag ? [`${asn}:${s.learned_from_as.code}:${to.asn}`] : []),\n ]\n : [];\n const set = {\n remove_standard_communities: [`${asn}:*`],\n ...(everything\n ? { remove_large_communities: [`${asn}:*:*`] }\n : off.length\n ? { remove_large_communities: off.map((c) => `${asn}:${c}:*`) }\n : {}),\n ...(add.length ? { add_large_communities: add } : {}),\n };\n const blackhole =\n accepted.includes(\"blackhole\") && s.blackhole\n ? [\n {\n description: \"Blackhole, asked by the neighbor\",\n if: { large_community: `${asn}:${s.blackhole.code}:0` },\n set: { blackhole: true },\n },\n ]\n : [];\n // The tag is the AS's own when it names the AS, or groups that differ between the ASes.\n const ownTag = tags && (asTag || (!!s.learned_from_group && to.groups.length > 0 && !groupsShared));\n const tag = {\n description: !tags ? \"Requests it may not send taken off\" : ownTag ? `Learned from AS${to.asn}` : \"Where it was learned\",\n set,\n };\n return {\n all: [{ ...tag, description: tags && !to.customer ? `Learned from AS${to.asn}` : tag.description }, ...blackhole],\n own: ownTag ? [tag] : [],\n shared: [...(ownTag ? [] : [tag]), ...blackhole],\n };\n}\n\n/**\n * An export, before the rules that accept: a blackholed route rejected, or passed on to a neighbor that takes it; a route asked not to\n * go anywhere, to this AS or to one of its groups, rejected; the prepends it asks for; our communities taken off. Toward a customer, no\n * request about one AS (decision 39). Rejects come first, so \"do not announce\" always wins over a prepend. Toward iBGP, where our\n * other routers act on the requests themselves, only our communities taken off, unless `keep_ours`.\n */\nfunction exportRules(s: Json, asn: number, rule: Json, to: Peer, groupsShared: boolean): Written {\n const requests = codesOf(s, REQUESTS);\n const ours = rule.keep_ours\n ? {}\n : {\n remove_standard_communities: [`${asn}:*`],\n remove_large_communities: rule.send_tags ? requests.map((c) => `${asn}:${c}:*`) : [`${asn}:*:*`],\n };\n const asks = (option: Json, value: number) => ({ large_community: `${asn}:${option.code}:${value}` });\n const dna = s.do_not_announce_to_as;\n const blackholed = s.blackhole ? [{ description: \"Blackholed here, never announced\", if: asks(s.blackhole, 0), then: \"reject\" }] : [];\n const anywhere = dna ? [{ description: \"Asked not to be announced anywhere\", if: asks(dna, 0), then: \"reject\" }] : [];\n const toAs =\n dna && !to.customer ? [{ description: `Asked not to be announced to AS${to.asn}`, if: asks(dna, to.asn), then: \"reject\" }] : [];\n const toGroups = s.do_not_announce_to_group\n ? to.groups.map((g) => ({\n description: `Asked not to be announced to ${g}`,\n if: asks(s.do_not_announce_to_group, s.groups[g].value),\n then: \"reject\",\n }))\n : [];\n const add =\n to.blackhole?.split(\":\").length === 3 ? { add_large_communities: [to.blackhole] } : { add_standard_communities: [to.blackhole] };\n const passOn =\n s.blackhole && to.blackhole\n ? [{ description: `Blackhole passed on to AS${to.asn}`, if: asks(s.blackhole, 0), set: { ...ours, ...add }, then: \"accept\" }]\n : [];\n const prepends = to.customer\n ? []\n : PREPENDS.flatMap(([p, times]) =>\n s.prepend?.[p]\n ? [\n {\n description: `Asked to prepend ${p.replace(\"_\", \" \")} to AS${to.asn}`,\n if: asks(s.prepend[p], to.asn),\n set: { prepend: times },\n },\n ]\n : []\n );\n const strip = rule.keep_ours\n ? []\n : [{ description: rule.send_tags ? \"Our requests stay inside our network\" : \"Our communities stay inside our network\", set: ours }];\n if (to.ibgp) return { all: strip, own: [], shared: strip };\n return {\n all: [...(passOn.length ? [] : blackholed), ...anywhere, ...toAs, ...toGroups, ...passOn, ...prepends, ...strip],\n // A route passed on as a blackhole is accepted in the AS's own policy, so what must reject it first comes with it there.\n own: passOn.length\n ? [...anywhere, ...toAs, ...toGroups, ...passOn, ...prepends]\n : [...toAs, ...(groupsShared ? [] : toGroups), ...prepends],\n shared: [...blackholed, ...anywhere, ...(groupsShared ? toGroups : []), ...strip],\n };\n}\n\n/** Each BGP neighbor of a device, with what it takes from its group. */\nfunction neighborsOf(d: Json) {\n const bgp = d.routing?.bgp ?? {};\n return Object.entries<Json>(bgp.neighbors ?? {}).map(([name, x]) => {\n const group = x.neighbor_group ? bgp.neighbor_groups?.[x.neighbor_group] : undefined;\n const c = x.communities ?? group?.communities ?? {};\n return {\n name,\n x,\n group,\n asn: x.remote_as ?? group?.remote_as,\n groups: (c.groups ?? []) as string[],\n blackhole: c.blackhole as string | undefined,\n customer: (x.role ?? group?.role) === \"customer\",\n import: x.import_policy ?? group?.import_policy,\n export: x.export_policy ?? group?.export_policy,\n };\n });\n}\n\nconst isCommunities = (rule: Json) => !!rule && typeof rule === \"object\" && \"communities\" in rule;\n\n/**\n * Writes out each `{ communities }` rule of a device, for the neighbors that use its policy: their AS, their groups, their blackhole\n * community, and the device's site. A policy one AS uses gets the rules in place. A policy several ASes use keeps what they share, and\n * each AS with rules of its own gets a policy `<policy>-as<asn>` with them, which then calls it; its neighbors use that one. Then\n * takes the `communities` block off each neighbor and group: only Circuit reads it. Returns what is wrong, each starting with `at`.\n */\nexport function writeCommunities(network: Json, d: Json, at: (map: object | undefined, kind: string, key: string) => string): string[] {\n const found: string[] = [];\n const policies = d.routing?.policies ?? {};\n const bgp = d.routing?.bgp ?? {};\n const neighbors = neighborsOf(d);\n const s = network.communities;\n const blocks = [\n ...Object.entries<Json>(bgp.neighbor_groups ?? {}).map(([n, g]) => [\"neighbor_groups\", n, g]),\n ...Object.entries<Json>(bgp.neighbors ?? {}).map(([n, x]) => [\"neighbors\", n, x]),\n ];\n for (const [kind, n, x] of blocks) {\n if (!x.communities) continue;\n const where = `${kind}.${n}`;\n const here = at(bgp[kind], kind, n);\n if (!s) found.push(`${here}routing.bgp.${where}.communities: needs communities in circuit.ts.`);\n else\n for (const g of x.communities.groups ?? [])\n if (!s.groups?.[g])\n found.push(\n `${here}routing.bgp.${where}.communities: no group ${g} in communities.groups. The groups: ${Object.keys(s.groups ?? {}).join(\", \") || \"none\"}.`\n );\n }\n // A policy is written out only from blocks that are right.\n if (found.length) {\n for (const [, , x] of blocks) delete x.communities;\n return found;\n }\n\n for (const [p, rules] of Object.entries<Json[]>(policies)) {\n const written = rules.filter(isCommunities);\n if (!written.length) continue;\n const here = `${at(policies, \"policies\", p)}routing.policies.${p}: `;\n if (!s) {\n found.push(`${here}a communities rule needs communities in circuit.ts.`);\n continue;\n }\n const start = found.length;\n const kinds = [...new Set(written.map((r) => r.communities))];\n if (kinds.length > 1) found.push(`${here}holds an import and an export communities rule. A policy is one or the other.`);\n const kind: \"import\" | \"export\" = kinds[0];\n const other = kind === \"import\" ? \"export\" : \"import\";\n const wrong = neighbors.filter((n) => n[other] === p);\n if (wrong.length)\n found.push(\n `${here}has a communities ${kind} rule, but ${wrong.map((n) => n.name).join(\", \")} use it as their ${other} policy.`\n );\n const users = neighbors.filter((n) => n[kind] === p);\n if (!users.length)\n found.push(\n `${here}no neighbor uses it as its ${kind} policy. Circuit writes a communities rule for the neighbors that use its policy, so it goes in a policy a neighbor names, not one another policy calls.`\n );\n for (const rule of written) {\n for (const r of rule.accept_requests ?? [])\n if (!REQUESTS.includes(r) || !(r === \"prepend\" ? s.prepend : s[r]))\n found.push(\n `${here}accept_requests: ${r} is no request of communities. It has: ${REQUESTS.filter((q) => (q === \"prepend\" ? s.prepend : s[q])).join(\", \") || \"none\"}.`\n );\n if (kind === \"import\" && rule.add_tags !== false && s.learned_at_site && network.sites?.[d.site]?.number === undefined)\n found.push(\n `${here}learned_at_site tags each route with the site's number, but ${d.site} has none. Write number on it, in sites.`\n );\n }\n const ours = bgp.asn ?? network.asn;\n if (kind === \"export\")\n for (const n of users)\n if (n.asn === ours && written.some((r) => r.keep_ours === undefined))\n found.push(\n `${here}${n.name} is iBGP (AS${ours}), and its communities export doesn't write keep_ours. Write keep_ours: true to send our tags and requests on inside the network, or false to take them off.`\n );\n for (const n of users) if (n.asn === undefined) found.push(`${here}${n.name} has no remote_as, which a communities rule needs.`);\n\n const byAs = new Map<number, typeof users>();\n for (const n of users) byAs.set(n.asn, [...(byAs.get(n.asn) ?? []), n]);\n const peer = (asn: number): Peer => {\n const [n] = byAs.get(asn)!;\n return { asn, groups: n!.groups, blackhole: n!.blackhole, customer: n!.customer, ibgp: asn === ours };\n };\n for (const [asn, ns] of byAs)\n if (new Set(ns.map((n) => JSON.stringify([n.groups, n.blackhole, n.customer]))).size > 1)\n found.push(\n `${here}the neighbors of AS${asn} that use it (${ns.map((n) => n.name).join(\", \")}) differ in their communities or role. Give them one neighbor group.`\n );\n const ases = [...byAs.keys()];\n if (ases.length > 1 && (written.length > 1 || !isCommunities(rules[0])))\n found.push(\n `${here}several ASes use it (${ases.map((a) => `AS${a}`).join(\", \")}), so each one's own rules run first, in a policy of its own that then calls this one. Write its communities rule first, and once.`\n );\n if (found.length > start) continue;\n\n const site = network.sites?.[d.site]?.number;\n const asn = s.asn ?? network.asn;\n const groupsShared = new Set(users.map((n) => JSON.stringify(n.groups))).size === 1;\n const expand = (rule: Json, to: Peer) =>\n kind === \"import\" ? importRules(s, asn, rule, to, site, groupsShared) : exportRules(s, asn, rule, to, groupsShared);\n\n // A policy one AS uses: the rules in place.\n if (ases.length === 1) {\n policies[p] = rules.flatMap((r) => (isCommunities(r) ? expand(r, peer(ases[0]!)).all : [r]));\n continue;\n }\n\n // Several ASes: what they share once, in the policy, and each AS's own rules in a policy that runs first, then calls it.\n const parts = ases.map((a) => ({ asn: a, ...expand(rules[0], peer(a)) }));\n if (new Set(parts.map((x) => JSON.stringify(x.shared))).size > 1) {\n found.push(`${here}its ASes share no common rules. Give each AS a policy of its own.`);\n continue;\n }\n policies[p] = [...parts[0]!.shared, ...rules.slice(1)];\n const field = kind === \"import\" ? \"import_policy\" : \"export_policy\";\n // The ASes of the neighbors that take the policy from each group, before any of them changes.\n const fromGroup = new Map<object, Set<number>>();\n for (const u of users) if (u.x[field] === undefined) fromGroup.set(u.group, (fromGroup.get(u.group) ?? new Set()).add(u.asn));\n for (const { asn: a, own } of parts) {\n if (!own.length) continue;\n const name = `${p}-as${a}`;\n if (policies[name]) found.push(`${here}AS${a}'s own policy would be ${name}, which is taken.`);\n policies[name] = [...own, { call: p }];\n // Its neighbors use it: on their group, when every neighbor that takes the policy from the group is of this AS.\n for (const n of byAs.get(a)!) {\n if (n.x[field] !== undefined || fromGroup.get(n.group)!.size > 1) n.x[field] = name;\n else n.group[field] = name;\n }\n }\n }\n\n for (const [, , x] of blocks) delete x.communities;\n return found;\n}\n",
8
+ "config/define.ts": "// What a project's files call, as globals: `circuit`, `part`, `literal`, `merge`, `secret`, `secretFile`, `bgpNeighbors`. A network's `device()`.\n\nimport type { IP, Secret } from \"./common\";\nimport type { AnyDevice, AnyPort, Device, DevicePart, Names, Part } from \"./device\";\nimport type { FilterRule, NatRule } from \"./firewall\";\nimport type { Port } from \"./interfaces\";\nimport type { Model, Platform, PortName } from \"./models\";\nimport type { CommunitiesExport, CommunitiesImport, GroupsOf, RequestsOf } from \"./communities\";\nimport type { NetworkFacts, NetworkSettings, Prefixes, Site } from \"./network\";\nimport { ACL_RULE, ACTIONS, closest, FILTER_RULE, NAT_RULE, ROUTING_RULE, type Shape, unknownKeyHelp, unknownKeys } from \"./keys\";\nimport { type Before, type Clash, caller, innerCaller, made, mark, type Merged, mergeAll, resolve, ruleList, writtenAt } from \"./merge\";\nimport { type CallRule, type PolicyRule, policyRuleProblem, type RouteRule, type RoutingRule } from \"./routing\";\nimport type { AclRule } from \"./switching\";\n\n/** A secret by name, read when a device is applied from the network's secrets file or the environment, and through 1Password when that value is an `op://` reference. Never the value itself. */\nexport const secret = (name: string): Secret => ({ secret: name });\n\n/** A value read from a file when a device is applied, such as a private key in PEM. The path is from the project root; keep the file out of git. */\nexport const secretFile = (path: string): Secret => ({ secret_file: path });\n\n/** One network `circuit()` made, the line that made it, and each device registered with it: the device, the line that made it, and the file Circuit was reading. */\nexport type Registered = { network: object; at: string; devices: { device: AnyDevice; at: string; file: string }[] };\n\n/**\n * What Circuit checks once it has read every device: each network and its devices, that each source's hostnames are devices of the\n * network, and that each rule list `part` made is placed. `reading` is the file under `sites/` Circuit is importing.\n */\nexport const registry = {\n networks: [] as Registered[],\n sources: [] as { source: object; device: string; file: string; network: object }[],\n rules: [] as { rules: readonly object[]; file: string }[],\n reading: \"\",\n};\n\n/** Ports by name. A name the model does not have is an error that says so. */\ntype CheckedPorts<Ports extends string, ModelPorts extends string, M extends string, P> = {\n [K in Ports]?: K extends ModelPorts ? P : `${K} is not a port of ${M}`;\n};\n\n/** The names a device declares, by kind, from what `device()` finds in it. */\ntype Declared<\n Pt extends string,\n L extends string,\n VI extends string,\n I extends string,\n V extends string,\n A extends string,\n P extends string,\n S extends string,\n G extends string,\n T extends string,\n F extends string,\n AS extends string,\n IS extends string,\n U extends string,\n C extends string,\n> = {\n port: Pt;\n lag: L;\n vlanInterface: VI;\n interface: I;\n vlan: V;\n acl: A;\n policy: P;\n prefixSet: S;\n neighborGroup: G;\n table: T;\n vrf: F;\n addressSet: AS;\n interfaceSet: IS;\n user: U;\n certificate: C;\n anyInterface: Pt | L | VI | I;\n};\n\n/** The keys of the map at `D[G][K]`, such as the VLANs at `switching.vlans`. */\ntype KeysAt<D, G extends string, K extends string> = D extends { readonly [g in G]?: infer Group }\n ? K extends keyof NonNullable<Group>\n ? keyof NonNullable<NonNullable<Group>[K]> & string\n : never\n : never;\n\n/** The names a device declares, by kind: the keys of its maps, from the device and every part merged. */\ntype NamesOf<D> = {\n port: KeysAt<D, \"interfaces\", \"ethernet\">;\n lag: KeysAt<D, \"interfaces\", \"lag\">;\n vlanInterface: KeysAt<D, \"interfaces\", \"vlan_interface\">;\n interface: KeysAt<D, \"interfaces\", \"bridge\" | \"loopback\" | \"gre\" | \"vxlan\" | \"wireguard\">;\n vlan: KeysAt<D, \"switching\", \"vlans\">;\n acl: KeysAt<D, \"switching\", \"acls\">;\n policy: KeysAt<D, \"routing\", \"policies\">;\n prefixSet: KeysAt<D, \"routing\", \"prefix_sets\">;\n neighborGroup: D extends { readonly routing?: { readonly bgp?: { readonly neighbor_groups?: infer G } } } ? keyof G & string : never;\n table: KeysAt<D, \"routing\", \"tables\">;\n vrf: KeysAt<D, \"routing\", \"vrfs\">;\n addressSet: KeysAt<D, \"firewall\", \"address_sets\">;\n interfaceSet: KeysAt<D, \"firewall\", \"interface_sets\">;\n user: KeysAt<D, \"system\", \"users\">;\n certificate: KeysAt<D, \"system\", \"certificates\">;\n anyInterface: KeysAt<D, \"interfaces\", \"ethernet\" | \"lag\" | \"vlan_interface\" | \"bridge\" | \"loopback\" | \"gre\" | \"vxlan\" | \"wireguard\">;\n};\n\n/** The ports a device declares, each checked against its model. */\ntype EthernetOf<P extends Platform, M extends Model<P>, N extends Names> = CheckedPorts<\n N[\"port\"],\n PortName<P, M>,\n M,\n Port<NoInfer<N[\"vlan\"]>, NoInfer<N[\"acl\"]>, NoInfer<N[\"lag\"]>, NoInfer<N[\"vrf\"]>>\n>;\n\n/** The names the device itself declares, which `device()` infers, and those its parts declare. */\ntype All<Own extends Names, Parts extends readonly object[]> = {\n [K in keyof Names]: Own[K] | NamesOf<Merged<Parts>>[K];\n};\n\n/** The part a source gives the device `Name`: its entry under that hostname, or nothing. */\ntype PartOf<S, Name extends string> = S extends { readonly [K in Name]: infer P } ? P : {};\n\n/** The part each source gives the device `Name`, in the order of the sources. */\ntype PartsOf<Sources extends readonly object[], Name extends string> = { [I in keyof Sources]: PartOf<Sources[I], Name> };\n\n/**\n * Declare one device of this network, in its own file under `sites/`. Circuit finds the file and imports it; the call registers the\n * device with this network:\n *\n * ```ts\n * export default as64500.device({ name: \"switch-02\", site: \"site-b\", ... });\n * export default as64500.device({ name: \"edge-01\", include: [tenants.parts], site: \"site-a\", ... });\n * ```\n *\n * `site` is one of the network's `sites`, and the editor suggests them. `untrusted: true` on a VLAN is an error unless the network turns\n * on the check `untrusted_vlans`. A neighbor's `communities` and a policy's `{ communities }` rule are errors unless the network writes\n * `communities`, and take its groups and requests.\n *\n * `include` lists the device's sources, each an object keyed by hostname, each value a part, such as the tenants'. The device takes\n * from each the part under its own `name`, merged in after it as `merge` does: maps key by key, lists joined. A value two of them\n * write, or an ordered list from two places, is an error, in the editor where it can tell and always when Circuit reads the project.\n * So is a hostname in a source that is no device of the network.\n *\n * The names the device and its parts declare (VLANs, interfaces, sets, ACLs, policies and the rest) are the only names their fields\n * accept, and the editor suggests them. The port names come from the model.\n */\nexport type DeviceOf<Sites extends string, Net extends NetworkFacts> = <\n const Name extends string,\n const P extends Platform,\n const M extends Model<P>,\n const Sources extends readonly object[] = [],\n const Ports extends string = never,\n const Lags extends string = never,\n const VlanInterfaces extends string = never,\n const OtherInterfaces extends string = never,\n const Vlans extends string = never,\n const Acls extends string = never,\n const Policies extends string = never,\n const PrefixSets extends string = never,\n const NeighborGroups extends string = never,\n const Tables extends string = never,\n const Vrfs extends string = never,\n const AddressSets extends string = never,\n const InterfaceSets extends string = never,\n const Users extends string = never,\n const Certificates extends string = never,\n>(\n // `site` is checked on its own: inside the device, a network of one site made a wrong site a second error at each port.\n device: {\n /** The site it is at, one of the network's `sites`. The NetBox site and LibreNMS group of that name. */\n site: Sites;\n /**\n * Where the device takes parts from, written right after `name`: `include: [tenants.parts]`. Each source is keyed by hostname; the\n * device takes the part under its own `name`, merged in after its own object, in this order.\n */\n include?: {\n [I in keyof Sources]: Sources[I] & {\n readonly [K in Name]?: Clash<PartOf<Sources[I], Name>, Before<PartsOf<Sources, Name>, I>> &\n Part<\n All<\n Declared<\n Ports,\n Lags,\n VlanInterfaces,\n OtherInterfaces,\n Vlans,\n Acls,\n Policies,\n PrefixSets,\n NeighborGroups,\n Tables,\n Vrfs,\n AddressSets,\n InterfaceSets,\n Users,\n Certificates\n >,\n PartsOf<Sources, Name>\n >,\n EthernetOf<\n P,\n M,\n All<\n Declared<\n Ports,\n Lags,\n VlanInterfaces,\n OtherInterfaces,\n Vlans,\n Acls,\n Policies,\n PrefixSets,\n NeighborGroups,\n Tables,\n Vrfs,\n AddressSets,\n InterfaceSets,\n Users,\n Certificates\n >,\n PartsOf<Sources, Name>\n >\n >,\n Net\n >;\n };\n };\n } & Device<\n P,\n M,\n string,\n All<\n Declared<\n Ports,\n Lags,\n VlanInterfaces,\n OtherInterfaces,\n Vlans,\n Acls,\n Policies,\n PrefixSets,\n NeighborGroups,\n Tables,\n Vrfs,\n AddressSets,\n InterfaceSets,\n Users,\n Certificates\n >,\n PartsOf<Sources, Name>\n >,\n EthernetOf<\n P,\n M,\n All<\n Declared<\n Ports,\n Lags,\n VlanInterfaces,\n OtherInterfaces,\n Vlans,\n Acls,\n Policies,\n PrefixSets,\n NeighborGroups,\n Tables,\n Vrfs,\n AddressSets,\n InterfaceSets,\n Users,\n Certificates\n >,\n PartsOf<Sources, Name>\n >\n >,\n Name,\n Net\n >\n) => Device<\n P,\n M,\n Sites,\n All<\n Declared<\n Ports,\n Lags,\n VlanInterfaces,\n OtherInterfaces,\n Vlans,\n Acls,\n Policies,\n PrefixSets,\n NeighborGroups,\n Tables,\n Vrfs,\n AddressSets,\n InterfaceSets,\n Users,\n Certificates\n >,\n PartsOf<Sources, Name>\n >,\n Record<Ports | NamesOf<Merged<PartsOf<Sources, Name>>>[\"port\"], AnyPort>,\n Name\n>;\n\n/** One device, merged with the parts its `include` gives it and registered with its network, under the file Circuit is reading. */\nfunction addDevice(network: Registered, written: AnyDevice & { include?: readonly object[] }): AnyDevice {\n const at = caller();\n // Taken off the device's own object, not copied, so the object stays the one the file wrote.\n const include = written.include;\n delete written.include;\n const device: AnyDevice = written;\n resolve(device, \"\");\n const given = (include ?? []).map((source) => resolve(source, \"\")) as readonly Record<string, object>[];\n given.forEach((source, i) => {\n if (!source || typeof source !== \"object\" || Array.isArray(source))\n throw new Error(\n `${at}: ${device.name}: include[${i}] is ${Array.isArray(source) ? \"a list\" : JSON.stringify(source)}, not a source keyed by hostname, such as tenants.parts.`\n );\n });\n registry.sources.push(...given.map((source) => ({ source, device: device.name, file: at, network: network.network })));\n const merged = mergeAll([device, ...given.map((source) => source[device.name])], at) as AnyDevice;\n network.devices.push({ device: merged, at, file: registry.reading });\n return merged;\n}\n\n/** The single items of a list given to `merge`, its nested lists opened. */\ntype Flat<X> = X extends readonly (infer Y)[] ? Flat<Y> : X;\n\n/** Every key of any member of a union. */\ntype AllKeys<U> = U extends unknown ? keyof U : never;\n\n// ---------- what is wrong, said at the key, in one line ----------\n\n/** `S` with one letter left out, each way. */\ntype Drop1<S extends string> = S extends `${infer A}${infer B}` ? B | `${A}${Drop1<B>}` : never;\n\n/** Whether `A` and `B` are one letter apart: one left out, one too many, one changed, or two side by side swapped. */\ntype Near<A extends string, B extends string> =\n A extends Drop1<B> ? true : B extends Drop1<A> ? true : [Drop1<A> & Drop1<B>] extends [never] ? false : true;\n\n/** ` Did you mean x?` for the key of `Known` one letter from `K`, or nothing. */\ntype DidYouMean<K extends string, Known extends string> = {\n [P in Known]: Near<K, P> extends true ? P : never;\n}[Known] extends infer M extends string\n ? [M] extends [never]\n ? \"\"\n : ` Did you mean ${M}?`\n : \"\";\n\n/** What a key Circuit doesn't take says, as its type: `fast_pth is no field of a filter rule. Did you mean fast_path?` */\ntype NoField<K extends string, Known, Of extends string> = `${K} is no field of ${Of}.${DidYouMean<K, Known & string>}`;\n\ntype ValueAt<U, K> = U extends unknown ? (K extends keyof U ? U[K] : never) : never;\n/** The item of the lists among `R`, or nothing when `R` holds no list. */\ntype ListItemOf<R> =\n Extract<NonNullable<R>, readonly unknown[]> extends infer A\n ? [A] extends [never]\n ? never\n : A extends readonly (infer E)[]\n ? E\n : never\n : never;\n\n/** The maps among `R`, without plain values or lists: what a map written there is checked against. */\ntype MapsOf<R> = Exclude<NonNullable<R>, string | number | boolean | bigint | symbol | readonly unknown[]>;\n\n/** Whether `R` holds maps, alone or in a list: only there is a value looked into. */\ntype HoldsMaps<R> = [MapsOf<R> | MapsOf<ListItemOf<R>>] extends [never] ? false : true;\n\n/**\n * `X` as it should be, by `R`: a key `R` doesn't take becomes a sentence that says so, and a `then` `R` doesn't take becomes what it\n * takes. So the editor says what is wrong at that key, in one line. Maps and lists inside are checked the same way. `Of` names where.\n *\n * What `R` decides alone is looked into, never a value by its own type: in a function in `lib/` a value may still be a type parameter,\n * which the editor can't look into. A map keyed by name, such as VLANs, is left to the device that takes it.\n */\ntype Fix<X, R, Of extends string> =\n IsNameMap<R> extends true\n ? X\n : X extends readonly unknown[]\n ? { [I in keyof X]: Fix<X[I], ListItemOf<R>, Of> }\n : X extends object\n ? {\n [K in keyof X]: K extends AllKeys<MapsOf<R>>\n ? K extends \"then\"\n ? [X[K]] extends [ValueAt<MapsOf<R>, K>]\n ? X[K]\n : ValueAt<MapsOf<R>, K>\n : HoldsMaps<ValueAt<MapsOf<R>, K>> extends true\n ? Fix<X[K], ValueAt<MapsOf<R>, K>, K & string>\n : X[K]\n : NoField<K & string, AllKeys<MapsOf<R>>, Of>;\n }\n : X;\n\n/** Whether `R` is a map keyed by name only, such as VLANs: any key, and no list beside it. */\ntype IsNameMap<R> = [MapsOf<R>] extends [never]\n ? false\n : string extends AllKeys<MapsOf<R>>\n ? [ListItemOf<R>] extends [never]\n ? true\n : false\n : false;\n\n/** `X` when it is right by `R`, else `X` as it should be: the editor's error is then at the wrong key, and short. */\ntype Checked<X, R, Of extends string> = X extends Fix<X, R, Of> ? X : Fix<X, R, Of>;\n\n/** A route policy rule's kind, by what it holds, and its words. */\ntype PolicyRuleOf<X> = X extends { readonly call: unknown }\n ? [CallRule<string>, \"a call rule\"]\n : X extends { readonly communities: \"import\" }\n ? [CommunitiesImport<string>, \"a communities import rule\"]\n : X extends { readonly communities: \"export\" }\n ? [CommunitiesExport, \"a communities export rule\"]\n : [RouteRule<string>, \"a route policy rule\"];\n\n/**\n * What item `X` of a list of `E` is checked as, and its words: a route policy rule as its own kind (`PolicyRuleOf`), a rule as the kind\n * the list holds, anything else as `E`.\n */\ntype RuleOf<X, E> =\n \"call\" extends AllKeys<E>\n ? PolicyRuleOf<X>\n : \"fast_path\" extends AllKeys<E>\n ? [E, \"a filter rule\"]\n : \"to_address\" extends AllKeys<E>\n ? [E, \"a nat rule\"]\n : \"rate\" extends AllKeys<E>\n ? [E, \"an acl rule\"]\n : \"routing_table\" extends AllKeys<E>\n ? [E, \"a policy-routing rule\"]\n : [E, \"an item of this list\"];\n\n/** One item of a list of `E`, checked as `RuleOf` says; a list in it, each of its items. */\ntype ItemChecked<X, E> = X extends readonly unknown[]\n ? { [J in keyof X]: ItemChecked<X[J], E> }\n : X extends object\n ? Checked<X, RuleOf<X, E>[0], RuleOf<X, E>[1]>\n : X;\n\n/** Whether item `X` of `merge`, at any depth, has a key or a `then` its list's items don't take. */\ntype IsBadItem<X, E> = true extends (Flat<X> extends infer Y ? (Y extends ItemChecked<Y, E> ? false : true) : never) ? true : false;\n\n/** The list a field holds, from its type `F`: never where it holds no list. A field that takes one value or a list (`OneOrMore`) holds a list. */\ntype ListOf<F> = Extract<NonNullable<F>, readonly unknown[]>;\n\n/**\n * An item of a list field: one of its items, or a list of them, nested as deep as you like (a part's rules, a preset). The list has\n * each key of an item as `never`, so a rule's own keys still tell its kind apart (`call` a call rule), and an error names what it takes.\n */\ntype ItemOrList<E> = E | (readonly ItemOrList<E>[] & { readonly [K in AllKeys<Extract<E, object>>]?: never });\n\n/** Maps merged: each item checked against those before it, so a value written twice, or an ordered list from two items, is an error at the item. */\ntype MapItems<T extends readonly unknown[]> = { [I in keyof T]: T[I] & {} & Clash<T[I], Before<T, I>> };\n\n/** Every item is a map: no list, no single value. */\ntype AllMaps<T extends readonly unknown[]> = true extends (\n T[number] extends infer X ? (X extends readonly unknown[] ? true : X extends object ? false : true) : never\n)\n ? false\n : true;\n\n/**\n * Items made into one, in order: `merge([rule_a, tenants.rules.router_input, rule_b])`. Each item is one value or a list of them, nested\n * as deep as you like; you never write `...`.\n *\n * What it makes is decided by the field it goes in, `F`, which the editor reads where `merge` lands, as Circuit does from the model:\n *\n * - **a list** (a chain's rules, NAT, an ACL, a route policy, policy routing, trunk VLANs, addresses, static routes): one flat list. Each\n * item is checked as one of the field's own items, the device's names included, and completes as one: a rule's keys, its `if`,\n * `then`, and the names of address sets, interfaces, VLANs and policies. Two single rules, `merge([rule_a, rule_b])`, are two rules.\n * An ordered list keeps every item in order; any other list keeps the same item once.\n * - **a map** (a part, VLANs, interfaces, address sets, a source keyed by hostname, a tenant): merged key by key, by MODEL.md's Merge\n * rules. A list inside joins, and two rule lists `part` made join in order, every rule kept; an ordered list from two items, or\n * another value at one path, is an error naming both files. So `merge([tenant_a, tenant_b])` gives each part by hostname, and each rule\n * list with tenant_a's rules first.\n * - **no field** (a value of its own, `export const tenants = merge([tenant_a])`): maps merged when every item is a map, else a list.\n *\n * The same map twice in one `merge` is an error.\n *\n * The field is known only once the device's names are: the second signature below makes TypeScript read `merge` after the rest of the\n * device, so it has them.\n */\nexport function merge<const T extends readonly unknown[], F = unknown>(\n items: unknown extends F\n ? AllMaps<T> extends true\n ? MapItems<T>\n : T\n : [ListOf<F>] extends [never]\n ? MapItems<T>\n : // Each item is checked and completes as the field's own; one with a wrong key or `then` has it said there, in one line\n // (`ItemChecked`). Item by item: a rule that holds a `merge` of its own, such as `dst_port: merge([...])`, is read later, and\n // would otherwise leave every item unchecked this way.\n {\n [I in keyof T]: IsBadItem<T[I], ListOf<F>[number]> extends true\n ? ItemChecked<T[I] & {}, ListOf<F>[number]>\n : ItemOrList<ListOf<F>[number]>;\n }\n): [T] extends [never]\n ? F\n : unknown extends F\n ? AllMaps<T> extends true\n ? Merged<T>\n : readonly Flat<T[number]>[]\n : [ListOf<F>] extends [never]\n ? Merged<T>\n : ListOf<F>;\n/**\n * Never called: no call has two arguments. A generic signature that returns a function makes TypeScript skip `merge` in its first\n * pass over the device, and read it in a second, with the names the first pass found (`SkipGenericFunctions` in its checker).\n */\nexport function merge<X>(deferred: never, _: never): () => X;\nexport function merge(items: readonly unknown[]): unknown {\n return made(items, caller());\n}\n\n/** The rule each kind of rule list holds, by the ordered list it goes in. */\ntype Rules = {\n /** A firewall chain's rules: `firewall.filter.<chain>.rules`. */\n filter: FilterRule<string, string, string>;\n /** Address translation: `firewall.nat.source` or `.destination`. */\n nat: NatRule<string, string, string>;\n /** A hardware ACL: `switching.acls.<name>`. */\n acl: AclRule<string>;\n /** A route policy: `routing.policies.<name>`. */\n policy: PolicyRule<string, string>;\n /** Policy routing rules: `routing.policy_routing`. */\n \"policy-routing\": RoutingRule<string, string>;\n};\n\n/** What `part` takes, by kind: a device's shape, or a list of one kind of rule. */\ntype Kinds = {\n device: DevicePart;\n filter: readonly Rules[\"filter\"][];\n nat: readonly Rules[\"nat\"][];\n acl: readonly Rules[\"acl\"][];\n policy: readonly Rules[\"policy\"][];\n \"policy-routing\": readonly Rules[\"policy-routing\"][];\n};\n\n/**\n * What `part` checks, by kind, at the wrong key: a device part has only a device's keys, at every depth; each rule only its kind's keys,\n * its `if` only its kind's conditions, and its `then` one its kind takes. A route policy rule as its own kind (`PolicyRuleOf`).\n */\ntype PartChecked<T, K> = K extends \"device\"\n ? Checked<T, DevicePart, \"a device part\">\n : K extends \"policy\"\n ? T extends ItemChecked<T, Rules[\"policy\"]>\n ? T\n : ItemChecked<T, Rules[\"policy\"]>\n : K extends Exclude<keyof Rules, \"policy\">\n ? Checked<T, readonly Rules[K][], `a ${K} rule`>\n : T;\n\n/** Each kind's keys at run time, from keys.ts, which checks them against the types; and what `then` may be. */\nconst RULE_SHAPES: Record<string, Shape> = { filter: FILTER_RULE, nat: NAT_RULE, acl: ACL_RULE, \"policy-routing\": ROUTING_RULE };\nconst THEN: Record<string, readonly string[]> = ACTIONS;\n\n/** The map at `path` in `value`, its list positions included: `[2].if` is the `if` of the third rule. */\nconst mapAt = (value: unknown, path: string): unknown =>\n (path.match(/[^.[\\]]+/g) ?? []).reduce<unknown>(\n (v, step) => (v && typeof v === \"object\" ? (v as Record<string, unknown>)[step] : undefined),\n value\n );\n\n/**\n * A part written in one file and used in another. The kind says its shape, and the editor completes and checks it as that:\n *\n * - `part(\"device\", { ... })`: a part of a device, in the device's shape, given to each device under its hostname in a source:\n * `{ \"edge-01\": part(\"device\", { ... }) }`. The names it refers to are checked at the device.\n * - `part(\"filter\", [...])`, `\"nat\"`, `\"acl\"`, `\"policy\"`, `\"policy-routing\"`: rules for an ordered list another file writes, such as\n * each tenant's rules for the router's input chain. That file places them with `merge`: `rules: merge([own_rule, tenants.rules.router_input])`.\n * Each rule has its kind's fields only. Rules made here that no device holds stop `circuit validate`, so none is forgotten.\n */\nexport function part<const K extends \"device\" | \"filter\" | \"nat\" | \"acl\" | \"policy\" | \"policy-routing\", const T>(\n kind: K,\n value: Kinds[K] & PartChecked<T, K>\n): // Rules with a mistake give no rules, so the mistake is said once, at the rule, and not again where the list is placed.\nK extends \"device\" ? T : T extends PartChecked<T, K> ? T : readonly never[] {\n const file = caller();\n // The line that called `part`, such as one in lib/: where a wrong key of a rule is looked for first.\n const inner = innerCaller();\n if (!(kind in RULE_SHAPES) && kind !== \"device\" && kind !== \"policy\")\n throw new Error(`${file}: part has no kind \"${kind}\". The kinds: device, policy, ${Object.keys(RULE_SHAPES).join(\", \")}.`);\n if (kind === \"policy\") {\n if (!Array.isArray(value)) throw new Error(`${file}: a policy part is a list of rules.`);\n value.forEach((rule: unknown, i: number) => {\n const problem = policyRuleProblem(rule);\n if (problem) throw new Error(`${file}: rule ${i + 1} of this policy part: ${problem}`);\n });\n registry.rules.push({ rules: value, file });\n }\n const shape = RULE_SHAPES[kind];\n if (shape) {\n if (!Array.isArray(value)) throw new Error(`${file}: a ${kind} part is a list of rules.`);\n mark(value, file, inner);\n const takes = THEN[kind];\n const wrong = [\n ...unknownKeys(value, [shape]).map((u) => {\n // `[2].if` and `protocl`: rule 3, `if.protocl`.\n const [, i = \"0\", within = \"\"] = /^\\[(\\d+)\\]\\.?(.*)$/.exec(u.path) ?? [];\n const key = within ? `${within}.${u.key}` : u.key;\n return `${writtenAt(mapAt(value, u.path), u.key)}rule ${Number(i) + 1} of this ${kind} part: ${key}: no such field. ${unknownKeyHelp(u)}`;\n }),\n ...(takes\n ? value.flatMap((rule: Record<string, unknown>, i: number) => {\n if (takes.includes(rule.then as string)) return [];\n const hint = typeof rule.then === \"string\" ? closest(rule.then, takes) : undefined;\n return [\n `${writtenAt(rule, \"then\")}rule ${i + 1} of this ${kind} part: then: ${JSON.stringify(rule.then)} is no then of a ${kind} rule. ${hint ? `Did you mean ${hint}?` : `It takes: ${takes.join(\", \")}.`}`,\n ];\n })\n : []),\n ];\n if (wrong.length) throw new Error(wrong.join(\"\\n\"));\n registry.rules.push({ rules: value, file });\n }\n // A rule list joins another one in `merge`, every rule kept, in order: each tenant's, one after another.\n return (kind === \"device\" ? mark(value, file, inner) : ruleList(mark(value as object, file, inner))) as never;\n}\n\n/** Marks the list `bgpNeighbors()` makes, for Circuit to fill. */\nexport const BGP_NEIGHBORS = Symbol(\"every BGP neighbor\");\n\n/**\n * Every BGP neighbor address of this device, as an address set: `address_sets: { \"bgp-neighbors\": bgpNeighbors() }`. Circuit fills\n * it when it reads the project, from `routing.bgp.neighbors`, so a new neighbor needs no second line. Write it alone as the set.\n * To the editor it is a list of addresses. Nothing refers to its items by name: rules name the set (`src_set: \"bgp-neighbors\"`).\n */\nexport function bgpNeighbors(): readonly IP[] {\n return Object.defineProperty([], BGP_NEIGHBORS, { value: true });\n}\n\n/**\n * A value written once and used as it is written, in this file or another: `literal([\"exchange-a\", \"exchange-b\"])` stays those two VLAN names,\n * so a device takes it where it takes VLANs, and the editor checks each name there. Without it, the editor would see only \"some text\".\n */\nexport function literal<const T>(values: T): T {\n return mark(values, caller());\n}\n\n/** What a device takes from a network that writes `checks` as `Own` and `communities` as `C`. */\ntype FactsOf<Own, C> = {\n untrusted: Own extends { readonly untrusted_vlans: object } ? true : false;\n communities: C extends object ? true : false;\n groups: GroupsOf<C>;\n requests: RequestsOf<C>;\n};\n\n/** The network `circuit()` returns: what it was given, as written, and `device()`, typed from it. */\nexport type Network<Sites extends Readonly<Record<string, Site>>, Asn extends number, Own, C, P extends Prefixes> = Readonly<\n NetworkSettings<Sites, Asn, Own, C, P>\n> & {\n /** The AS the network operates, as written. */\n readonly asn: Asn;\n /** The network's own prefixes, as written, for a device to use by name: `as64500.prefixes.id01.v4_1`. Empty when `circuit()` writes none. */\n readonly prefixes: P;\n /** Declare one device of this network, in its own file under `sites/`: `export default as64500.device({ name: \"edge-01\", include: [tenants.parts], ... })`. */\n readonly device: DeviceOf<keyof Sites & string, FactsOf<Own, C>>;\n};\n\n/**\n * Declare a network, in `circuit.ts` at the project's root: `export const as64500 = circuit({ asn: 64500, sites: { ... }, checks: { ... } })`,\n * named after its AS, as we suggest. Its devices are `as64500.device({ ... })`, each in its own file under `sites/`, typed from what the network writes here: its sites,\n * whether `untrusted_vlans` is on, and its `communities`. Circuit imports `circuit.ts`, then every device file, so `circuit.ts` imports no device.\n *\n * A project may hold several networks, such as two ASes: one `circuit()` each, each exported from `circuit.ts` under its own name. The\n * export writes one JSON file per network, named after it.\n *\n * A check of your own is any other name in `checks`, with a function: `checks: { exports_end_in_reject: true, prefixes_stay_in_region: prefixesStayInRegion }`.\n * It reads the network resolved, as its devices will run it (`ResolvedNetwork`).\n */\nexport function circuit<\n const Sites extends Readonly<Record<string, Site>>,\n const Asn extends number,\n const Own = {},\n const C = undefined,\n const P extends Prefixes = {},\n>(settings: NetworkSettings<Sites, Asn, Own, C, P>): Network<Sites, Asn, Own, C, P> {\n const registered: Registered = { network: {}, at: caller(), devices: [] };\n registered.network = { ...settings, prefixes: settings.prefixes ?? {}, device: (device: AnyDevice) => addDevice(registered, device) };\n registry.networks.push(registered);\n return registered.network as never;\n}\n\nexport type { Names };\n",
9
+ "config/device.ts": "// One device: its name, where it is, what it is, how Circuit logs in, and its groups. A part of a device, written in another file and given to it by a source.\n\nimport type { IP, Secret } from \"./common\";\nimport type { Firewall } from \"./firewall\";\nimport type { Interfaces, Port } from \"./interfaces\";\nimport type { Management } from \"./management\";\nimport type { Monitoring } from \"./monitoring\";\nimport type { AnyFacts, NetworkFacts } from \"./network\";\nimport type { Routing } from \"./routing\";\nimport type { Services } from \"./services\";\nimport type { Switching } from \"./switching\";\nimport type { System } from \"./system\";\n\n/** A computer with SSH that holds the serial consoles of devices, such as a laptop beside the rack. It runs Python 3. */\nexport type Jump = {\n /** Its address or name. */\n host: string;\n /** The user Circuit logs in as. It must be allowed to open the serial ports. */\n user: string;\n /** The user's password. */\n password: Secret;\n /** SSH port. 22 when absent. */\n port?: number;\n /** The jump host is reached through a Cloudflare Tunnel, so Circuit connects with `cloudflared access ssh`, which must be installed. */\n cloudflared?: true;\n};\n\n/** A serial console, on a jump host or on the computer Circuit runs on. */\nexport type Console = {\n /** The jump host the console cable is plugged into. When absent, the cable is plugged into the computer Circuit runs on. */\n jump?: Jump;\n /** The serial port: on Linux or macOS a path under `/dev`, where a path under `/dev/serial/by-path/` stays the same when two adapters swap names; on Windows a COM port, such as `COM3`. */\n port: string;\n /** Line speed in bits per second, such as 9600 or 115200. */\n baud: number;\n /** The console asks only for a password, with no name first. Circuit then types no name; `connection.user` still picks the password. */\n password_only?: true;\n};\n\n/** How Circuit itself logs in. `U` is the device's user names. */\nexport type Connection<U extends string> = {\n /** Management address. Absent for a device reached only through its console. Its own value, apart from any interface's address. */\n host?: IP;\n /** One of `system.users`. Its password is the login. */\n user: U;\n /** SSH port. 22 when absent. */\n port?: number;\n /** The device's serial console, for `--console`: the way in before the device has a management address, or when its management network is down. */\n console?: Console;\n};\n\n/** The names a device declares, by kind. A field that refers to a name takes only one of these. */\nexport type Names = {\n /** Keys of `interfaces.ethernet`. */\n port: string;\n /** Keys of `interfaces.lag`. */\n lag: string;\n /** Keys of `interfaces.vlan_interface`. */\n vlanInterface: string;\n /** Keys of `interfaces.bridge`, `.loopback`, `.gre`, `.vxlan` and `.wireguard`. */\n interface: string;\n /** Keys of `switching.vlans`. */\n vlan: string;\n /** Keys of `switching.acls`. */\n acl: string;\n /** Keys of `routing.policies`. */\n policy: string;\n /** Keys of `routing.prefix_sets`. */\n prefixSet: string;\n /** Keys of `routing.bgp.neighbor_groups`. */\n neighborGroup: string;\n /** Keys of `routing.tables`. */\n table: string;\n /** Keys of `routing.vrfs`. */\n vrf: string;\n /** Keys of `firewall.address_sets`. */\n addressSet: string;\n /** Keys of `firewall.interface_sets`. */\n interfaceSet: string;\n /** Keys of `system.users`. */\n user: string;\n /** Keys of `system.certificates`. */\n certificate: string;\n /** Every port and interface: `port`, `lag`, `vlanInterface` and `interface` together. */\n anyInterface: string;\n};\n\n/**\n * The groups of a device, what a part may add to as well. `N` holds the names the device declares, `Ethernet` its ports. `Part` is true in a part, where BGP holds only what the part adds.\n * `Net` is what the network writes: whether it turns on the check `untrusted_vlans`, and its `communities`.\n */\nexport type Groups<N extends Names, Ethernet, Part extends boolean = false, Net extends NetworkFacts = AnyFacts> = {\n /** The box itself: banner, time, DNS it uses, users, certificates, logs kept on it, software, hardware. */\n system?: System<N[\"user\"], N[\"certificate\"], NoInfer<N[\"anyInterface\"]>>;\n /** How people and tools log in: ssh, https, api, native protocol, console. */\n management?: Management<NoInfer<N[\"anyInterface\"]>, NoInfer<N[\"certificate\"]>, NoInfer<N[\"addressSet\"]>>;\n /** What the box reports to collectors: snmp, syslog, flow export, optics. */\n monitoring?: Monitoring<NoInfer<N[\"anyInterface\"]>>;\n /** Every port and interface, by kind: ethernet, lag, vlan_interface, bridge, loopback, gre, vxlan, wireguard. */\n interfaces?: Interfaces<Ethernet, N[\"lag\"], N[\"vlanInterface\"], N[\"interface\"], NoInfer<N[\"vlan\"]>, NoInfer<N[\"vrf\"]>>;\n /** Layer 2 shared by ports: vlans, stp, lldp, acls. */\n switching?: Switching<N[\"vlan\"], N[\"acl\"], NoInfer<N[\"anyInterface\"]>, Net[\"untrusted\"]>;\n /** What the box serves to hosts: dhcp server, dhcp relay, dns server, ntp server. */\n services?: Services<NoInfer<N[\"anyInterface\"]>>;\n /** Where packets go: tables, vrfs, policy routing, static routes, bgp, ospf, bfd, rpki, policies, prefix sets. */\n routing?: Routing<N[\"policy\"], N[\"prefixSet\"], N[\"neighborGroup\"], N[\"table\"], N[\"vrf\"], NoInfer<N[\"anyInterface\"]>, Part, Net>;\n /** Which packets pass and how they are rewritten: address sets, interface sets, filter, nat, untracked, conntrack helpers. */\n firewall?: Firewall<N[\"addressSet\"], NoInfer<N[\"anyInterface\"]>, N[\"interfaceSet\"]>;\n};\n\n/**\n * Everything one device runs. What is not written here is removed from the device or returned to its default.\n *\n * `N` holds the names it declares, so a reference to a name it does not declare is an error. `Ethernet` is its ports, checked against the model.\n * `Site` is the network's sites. `Net` is what the network writes: whether it turns on the check `untrusted_vlans`, and its `communities`.\n */\nexport type Device<\n Platform extends string,\n Model extends string,\n Site extends string,\n N extends Names,\n Ethernet,\n Name extends string = string,\n Net extends NetworkFacts = AnyFacts,\n> = {\n /** Its hostname: its name in the network, and the key under which each source gives it a part. */\n name: Name;\n /** The site it is at, one of the network's `sites`. The NetBox site and LibreNMS group of that name. */\n site: Site;\n /** Its role in NetBox, such as `Core switch`. Circuit makes the role when NetBox has none of that name. */\n role?: string;\n /** Operating system. It decides which adapter renders and applies the device. */\n platform: Platform;\n /** Hardware model, one of the platform's. It decides which port names exist. */\n model: Model;\n /** How Circuit itself logs in. */\n connection: Connection<NoInfer<N[\"user\"]>>;\n} & Groups<N, Ethernet, false, Net>;\n\n/**\n * A part of a device, which a source gives it under its hostname: what an upstream, a tenant or a monitoring host adds to it. The names it refers to must be declared by the device or by one of its parts.\n */\nexport type Part<N extends Names, Ethernet, Net extends NetworkFacts = AnyFacts> = {\n /** How Circuit itself logs in. In a part, only what the part adds, such as the console. */\n connection?: Partial<Connection<NoInfer<N[\"user\"]>>>;\n} & Groups<N, Ethernet, true, Net>;\n\n/** Any name: what a part, or a device seen from the network, holds. */\ntype AnyNames = { [K in keyof Names]: string };\n\n/** A port as a part writes it, before the device it goes to checks its names. */\nexport type AnyPort = Port<string, string, string, string>;\n\n/** Any device, as the network holds it. */\nexport type AnyDevice = Device<string, string, string, AnyNames, Record<string, AnyPort>>;\n\n/**\n * A part of one or more devices, written apart from them and given to each device that carries it, under its hostname in a source: an upstream's VLAN, sessions and policies on the router, a tenant's ports on a switch, what every device sends to the monitoring hosts.\n *\n * It has a device's shape and docs. What only a whole device has (its site, platform, model) is not here, and what a part writes alone stays optional: a part can add BGP neighbors without the AS, or a console without the user.\n *\n * Names a part refers to (a VLAN, an interface, a policy) are checked once the part reaches a device: there they must be declared.\n */\nexport type DevicePart = Part<AnyNames, Record<string, AnyPort>>;\n",
10
+ "config/firewall.ts": "// Which packets pass and how they are rewritten: `firewall.address_sets`, `.interface_sets`, `.filter`, `.nat`, `.untracked`, `.conntrack_helpers`.\n\nimport type { Family, HostAddress, IP, Named, OneOrMore, Ordered, Prefix } from \"./common\";\n\n/** An IP protocol. `icmp` means ICMPv6 in an IPv6 rule. */\nexport type Protocol = \"tcp\" | \"udp\" | \"icmp\" | \"gre\" | \"esp\" | \"ah\" | \"ospf\" | \"vrrp\";\n\n/** The state connection tracking gives a packet. */\nexport type ConnectionState = \"new\" | \"established\" | \"related\" | \"untracked\" | \"invalid\";\n\n/** Ports from `min` to `max`, both included. */\nexport type PortRange = {\n /** The first port, 0 to 65535. */\n min: number;\n /** The last port, 0 to 65535, not below `min`. */\n max: number;\n};\n\n/** A port, a range of ports, or any of several. */\nexport type Ports = number | PortRange | readonly (number | PortRange)[];\n\n/** A TCP header flag. */\nexport type TcpFlag = \"fin\" | \"syn\" | \"rst\" | \"psh\" | \"ack\" | \"urg\";\n\n/**\n * Conditions on a packet. All must be true. A rule with addresses of one family applies to that family only. `I` is the device's\n * ports and interfaces, `AS` its address sets, `IS` its interface sets.\n *\n * `src`, `dst`, `in_interface` and `out_interface` each take a list, and an interface set stands for its interfaces. Where the\n * platform has no list, Circuit writes one rule per combination: the interface outermost (`in` before `out`), then `src`, then `dst`.\n */\nexport type FilterMatch<I extends string, AS extends string, IS extends string> = {\n /** Only packets of this family. */\n family?: Family;\n /** IP protocol. */\n protocol?: Protocol;\n /** Source address or prefix, or any of several. */\n src?: OneOrMore<Prefix>;\n /** Destination address or prefix, or any of several. */\n dst?: OneOrMore<Prefix>;\n /** One of `firewall.address_sets`: the source is in it. */\n src_address_set?: AS;\n /** One of `firewall.address_sets`: the destination is in it. */\n dst_address_set?: AS;\n /** Source port, or any of several. TCP and UDP only. */\n src_port?: Ports;\n /** Destination port, or any of several. TCP and UDP only. */\n dst_port?: Ports;\n /** One of the device's ports or interfaces, or any of several: the packet arrived on it. */\n in_interface?: OneOrMore<I>;\n /** One of the device's ports or interfaces, or any of several: the packet leaves through it. */\n out_interface?: OneOrMore<I>;\n /** One of `firewall.interface_sets`: the packet arrived on one of its interfaces. Not with `in_interface`. */\n in_interface_set?: IS;\n /** One of `firewall.interface_sets`: the packet leaves through one of its interfaces. Not with `out_interface`. */\n out_interface_set?: IS;\n /** Connection tracking state, any of these. */\n connection_state?: readonly ConnectionState[];\n /** TCP flags that must be set, and flags that must not be. */\n tcp_flags?: {\n /** Flags that must be set. */\n set?: readonly TcpFlag[];\n /** Flags that must not be set. */\n unset?: readonly TcpFlag[];\n };\n};\n\n/**\n * One firewall rule, the condition first: `{ if: { protocol: \"icmp\" }, then: \"accept\" }`. No `if`: every packet.\n */\nexport type FilterRule<I extends string, AS extends string, IS extends string> = {\n /** Shown beside the rule on the device. */\n description?: string;\n /** Which packets the rule applies to. Every packet when absent. */\n if?: FilterMatch<I, AS, IS>;\n /** What happens to a matching packet. `reject` answers the sender, `drop` does not. */\n then: \"accept\" | \"drop\" | \"reject\";\n /** Send the rest of an accepted flow through the fast path, skipping the remaining rules. */\n fast_path?: true;\n};\n\n/** Rules in order, the first match wins, then `default_action`. */\nexport type Chain<I extends string, AS extends string, IS extends string> = {\n /** What happens to a packet no rule matched. Accept when absent. */\n default_action?: \"accept\" | \"drop\";\n /** Rules in order: the first runs first. Written whole in one place; `merge` places rules from other files in it. */\n rules?: Ordered<FilterRule<I, AS, IS>>;\n};\n\n/**\n * One address translation, the condition first: `{ if: { src: \"10.0.0.0/24\", out_interface_set: \"uplinks\" }, then: \"snat\", to_address: \"192.0.2.1\" }`.\n * No `if`: every packet.\n */\nexport type NatRule<I extends string, AS extends string, IS extends string> = {\n /** Shown beside the rule on the device. */\n description?: string;\n /** Which packets are translated. Every packet when absent. */\n if?: FilterMatch<I, AS, IS>;\n} & (\n | {\n /** `masquerade` uses the address of the outgoing interface. */\n then: \"masquerade\";\n }\n | {\n /** `snat` rewrites the source, `dnat` the destination. */\n then: \"snat\" | \"dnat\";\n /** The address to rewrite to, alone or with its length. */\n to_address: HostAddress;\n /** The port to rewrite to. Unchanged when absent. */\n to_port?: number;\n }\n);\n\n/** Connection-tracking helpers that rewrite application payloads. None run unless listed. */\nexport type Helper = \"ftp\" | \"tftp\" | \"sip\" | \"h323\" | \"pptp\" | \"rtsp\" | \"irc\";\n\n/** What the device filters and translates. `AS` is the device's address set names, `I` its ports and interfaces, `IS` its interface set names. */\nexport type Firewall<AS extends string, I extends string, IS extends string> = {\n /** Named groups of addresses and prefixes, of either family, that rules match on with `src_address_set` and `dst_address_set`. */\n address_sets?: Named<AS, readonly (IP | Prefix)[]>;\n /**\n * Named groups of the device's ports and interfaces, that rules match on with `in_interface_set` and `out_interface_set`, like a\n * RouterOS interface list or a FortiOS zone. A list: each part adds its own, such as each upstream its interface to `uplinks`.\n */\n interface_sets?: Named<IS, readonly I[]>;\n /** Traffic to the device (`input`), through it (`forward`), and from it (`output`). */\n filter?: {\n /** Traffic to the device itself. */\n input?: Chain<I, NoInfer<AS>, NoInfer<IS>>;\n /** Traffic the device routes. */\n forward?: Chain<I, NoInfer<AS>, NoInfer<IS>>;\n /** Traffic the device itself sends. */\n output?: Chain<I, NoInfer<AS>, NoInfer<IS>>;\n };\n /** Source translation on the way out, destination translation on the way in. Each is rules in order: the first runs first. */\n nat?: {\n /** Rewrites the source of traffic on the way out. */\n source?: Ordered<NatRule<I, NoInfer<AS>, NoInfer<IS>>>;\n /** Rewrites the destination of traffic on the way in. */\n destination?: Ordered<NatRule<I, NoInfer<AS>, NoInfer<IS>>>;\n };\n /** Connection-tracking helpers that run. None when absent. */\n conntrack_helpers?: readonly Helper[];\n /**\n * Traffic to and from these prefixes passes without connection tracking. A flood toward them cannot fill the connection table, and rules see its packets in the state `untracked`. Everything is tracked when absent.\n *\n * Each address NAT uses inside one of these prefixes (a source translation's `to_address`, a destination translation's `to_address` or matched `dst`) stays tracked: Circuit writes two accept rules for it, one for traffic to it and one for traffic from it, before the untracked rules. Without them, a reply to that address would skip tracking and never be translated back.\n */\n untracked?: readonly Prefix[];\n};\n",
11
+ "config/index.ts": "// Circuit v1 for a TypeScript project. Its values (functions, `presets` and `address`) are globals: circuit.d.ts declares them for the editor, and\n// Circuit puts them on `globalThis` before it reads the project. A project never imports them. `lib/` may import the types:\n// `import type { Prefix } from \"circuit\"`.\n\nexport { bgpNeighbors, circuit, literal, merge, part, secret, secretFile } from \"./define\";\nexport { presets } from \"./presets\";\nexport { address } from \"./address\";\nexport type * from \"./address\";\nexport type * from \"./common\";\nexport type * from \"./communities\";\nexport type * from \"./device\";\nexport type * from \"./firewall\";\nexport type * from \"./interfaces\";\nexport type * from \"./management\";\nexport type * from \"./models\";\nexport type * from \"./monitoring\";\nexport type * from \"./network\";\nexport type * from \"./routing\";\nexport type * from \"./services\";\nexport type * from \"./switching\";\nexport type * from \"./system\";\nexport type * from \"./time-zones\";\n",
12
+ "config/interfaces.ts": "// Every port and interface, by kind: `interfaces.ethernet`, `.lag`, `.vlan_interface`, `.bridge`, `.loopback`, `.gre`, `.vxlan`, `.wireguard`.\n\nimport type { HostAddress, IP, InterfaceAddress, MacAddress, Named, Prefix, Secret } from \"./common\";\n\n/** A storm-control threshold: packets per second, or percent of the link's speed. */\nexport type Threshold =\n | {\n /** Packets per second. */\n pps: number;\n }\n | {\n /** Percent of the link's speed, above 0 and up to 100. */\n percent: number;\n };\n\n/** A kind of frame, by the ethertype in its header: `ipv4` is 0x0800, `ipv6` 0x86DD, `arp` 0x0806. */\nexport type Ethertype = \"ipv4\" | \"ipv6\" | \"arp\";\n\n/** A virtual router address shared with other devices on the same segment, RFC 9568 (VRRPv3). */\nexport type Vrrp = {\n /** Virtual router number, 1 to 255. The same on every device sharing the address. */\n id: number;\n /** The shared address, without a length. */\n address: IP;\n /** Higher wins the master election, 1 to 254. 100 when absent. */\n priority?: number;\n /** Take mastership back from a lower priority device when this one returns. Off when absent. */\n preempt?: boolean;\n /** Seconds between advertisements. 1 when absent. */\n interval?: number;\n};\n\n/** Presence at an internet exchange, as a registry lists it. The addresses are the interface's own. */\nexport type Exchange = {\n /** The exchange's name, for output. */\n name: string;\n /** Capacity toward the exchange, in Mbit/s. */\n speed: number;\n /** The exchange LAN's id at PeeringDB, its `ixlan_id`. */\n peeringdb_ixlan?: number;\n};\n\n/** An address with its length, or `{ address, peer_address }` for a point-to-point address whose far end is outside its own prefix. */\nexport type Address =\n | InterfaceAddress\n | {\n /** Our address, with its length. */\n address: InterfaceAddress;\n /** The far end's address. */\n peer_address: IP;\n };\n\n/** Layer 3 settings shared by every interface that can have an address. `Vrf` is the device's VRF names. */\nexport type Routed<Vrf extends string> = {\n /** Addresses with their length. Use `{ address, peer_address }` for a point-to-point address whose far end is outside its own prefix. */\n addresses?: readonly Address[];\n /** Send IPv6 router advertisements on this interface. Off when absent. */\n ipv6_ra?: boolean;\n /** Overrides `system.hardware_offload` for traffic routed through this interface. Software forwarding is what makes the firewall apply. */\n hardware_offload?: boolean;\n /** Virtual router addresses on this interface. */\n vrrp?: readonly Vrrp[];\n /** One of `routing.vrfs`: the VRF this interface routes in. The main table when absent. */\n vrf?: Vrf;\n /** This interface is the network's presence at an internet exchange. Devices ignore it; `circuit peeringdb` publishes it. */\n exchange?: Exchange;\n};\n\n/** Spanning tree settings for one port. */\nexport type PortStp = {\n /** The port faces a host, never a switch, so it forwards at once. */\n edge?: boolean;\n /** Path cost. Lower is preferred. Derived from speed when absent. */\n cost?: number;\n /** Port priority, 0 to 240 in steps of 16. Lower is preferred. */\n priority?: number;\n};\n\n/** Drop flooded traffic above these thresholds. */\nexport type StormControl = {\n /** Frames to every host. */\n broadcast?: Threshold;\n /** Frames to a group. */\n multicast?: Threshold;\n /** Frames to a MAC address the switch has not learned. */\n unknown_unicast?: Threshold;\n};\n\n/** Layer 2 membership. A port or LAG with none of these is routed. `Vlan` is the device's VLAN names. */\nexport type Switched<Vlan extends string> = {\n /** One of `switching.vlans`: the one VLAN this port or LAG is in, untagged. */\n access_vlan?: Vlan;\n /** Some of `switching.vlans`: the VLANs this port or LAG carries, tagged. */\n trunk_vlans?: readonly Vlan[];\n /** One of `switching.vlans`: untagged frames on a trunk are in this VLAN. Only with `trunk_vlans`. */\n native_vlan?: Vlan;\n /** Overrides `switching.stp` for this port. `false` stops it sending or acting on BPDUs. */\n stp?: boolean | PortStp;\n /** Drop flooded traffic above these thresholds. */\n storm_control?: StormControl;\n /** The most MAC addresses this port or LAG learns, 1 or more. Frames from any other source address are dropped. An exchange sets 1 on a member port, so each member sends from one router only. No limit when absent. */\n mac_limit?: number;\n /** The only kinds of frame this port or LAG takes in; any other kind is dropped. An exchange takes `ipv4`, `ipv6` and `arp` on a member port. Every kind when absent. */\n ethertypes?: readonly Ethertype[];\n};\n\n/**\n * The other end of a cable: another of our devices and its port. Both ends must agree on how they are switched, and the far end must\n * link back. The port's own `description` names that end, `<far device> <far port as printed there>`, such as `fw-01 port6`:\n * Circuit warns when it doesn't, and never writes it.\n */\nexport type Link = {\n /** The device at the other end, by its key in `devices`. */\n device: string;\n /** The port on that device, such as `10g-2`. */\n port: string;\n /**\n * What is between the two ends when the cable is not direct, such as a carrier's local loop or L2 transport: `\"Carrier A local loop\"`. LLDP then sees the carrier's switches, not the far end, so `validate` does not compare LLDP on this port. Both ends name the same carrier.\n */\n through?: string;\n};\n\n/** A physical port, by its key `<speed>-<position>`, such as `10g-4`. Ports a device does not declare are shut down. */\nexport type Port<Vlan extends string, Acl extends string, Lag extends string, Vrf extends string> = Routed<Vrf> &\n Switched<Vlan> & {\n /** What is plugged in. On a port with a `link`, the far end: `<far device> <far port as printed there>`. */\n description?: string;\n /** Largest IP packet, in bytes. 1500 when absent. */\n mtu?: number;\n /** Fixed speed, for an optic slower than the cage. Negotiated when absent. */\n speed?: \"100m\" | \"1g\" | \"10g\" | \"25g\" | \"40g\" | \"50g\" | \"100g\";\n /** Overrides `switching.lldp` for this port. */\n lldp?: boolean;\n /** One of `switching.acls`: the hardware ACL applied to traffic arriving on this port. */\n ingress_acl?: Acl;\n /** One of `interfaces.lag`: the LAG this port is in. Every other setting goes on the LAG. */\n in_lag?: Lag;\n /** Another of our devices at the other end of the cable. Circuit checks that it links back, and warns when `description` doesn't name it. */\n link?: Link;\n };\n\n/** Settings every interface that is not a physical port shares. */\nexport type Base<Vrf extends string> = Routed<Vrf> & {\n /** What the interface is for. */\n description?: string;\n /** Largest IP packet, in bytes. */\n mtu?: number;\n};\n\n/** A layer 3 interface on a VLAN. */\nexport type VlanInterface<Vlan extends string, Vrf extends string> = Base<Vrf> & {\n /** One of `switching.vlans`: the VLAN it is on. */\n vlan: Vlan;\n /**\n * A MAC address of its own, instead of the one it shares with the device's other VLAN interfaces. A gateway in front of a transparent firewall needs one: frames to it come back on another VLAN, where the bridge would otherwise take a shared MAC for its own and keep them.\n */\n mac?: MacAddress;\n};\n\n/** An interface that is always up and is on no link, for addresses that must not depend on one. */\nexport type Loopback<Vrf extends string> = Base<Vrf>;\n\n/** A link aggregation. Each member port points to it with `in_lag`. */\nexport type Lag<Vlan extends string, Vrf extends string> = Base<Vrf> &\n Switched<Vlan> & {\n /** LACP negotiates membership with the far end; `static` joins the ports without asking. LACP when absent. */\n mode?: \"lacp\" | \"static\";\n /** Number, on platforms where a LAG has a number instead of a name. Its position among the device's LAGs when absent. */\n id?: number;\n };\n\n/** A GRE tunnel. */\nexport type Gre<Vrf extends string> = Base<Vrf> & {\n /** Our tunnel endpoint, alone or with its length. */\n local_address: HostAddress;\n /** The far tunnel endpoint. */\n remote_address: IP;\n};\n\n/** A point-to-point VXLAN tunnel. */\nexport type Vxlan<Vrf extends string> = Base<Vrf> & {\n /** Segment identifier, the same on both ends, 1 to 16777215. */\n vni: number;\n /** Our tunnel endpoint, alone or with its length. */\n local_address: HostAddress;\n /** The far tunnel endpoint. */\n remote_address: IP;\n /** UDP port. 4789 when absent. */\n port?: number;\n /** Fixed MAC address, so a rebuild keeps the one the far side learned. */\n mac?: MacAddress;\n};\n\n/** A host and a UDP port, `host:port`, for example `203.0.113.10:51820` or `vpn.example.net:51820`. */\nexport type Endpoint = `${string}:${number}`;\n\n/** One peer of a WireGuard interface. */\nexport type WireGuardPeer = {\n /** Who or what the peer is. */\n name: string;\n /** The peer's public key. Public keys are not secrets. */\n public_key: string;\n /** Addresses the peer may send from and that are routed to it. */\n allowed_addresses: readonly Prefix[];\n /** `host:port`, for a peer this side dials. The peer dials in when absent. */\n endpoint?: Endpoint;\n /** Seconds between keepalives, for a peer behind NAT. None when absent. */\n keepalive?: number;\n /** What a generated client config routes into the tunnel. Affects no forwarding here. */\n client_allowed_addresses?: readonly Prefix[];\n};\n\n/** A WireGuard interface. */\nexport type WireGuard<Vrf extends string> = Base<Vrf> & {\n /** UDP port peers connect to. */\n listen_port: number;\n /** The address clients connect to, for generated client configs, alone or with its length. */\n endpoint?: HostAddress;\n /** This interface's private key. Its public key is derived from it. */\n private_key: Secret;\n /** Who may connect. */\n peers?: readonly WireGuardPeer[];\n};\n\n/**\n * A layer 2 bridge between VLAN interfaces, each keeping its own VLAN: a frame that comes in on one member leaves another with that member's tag. A firewall filters what crosses it, by the rules of its forward chain between the members, without routing it.\n */\nexport type Bridge<VlanInterface extends string, Vrf extends string> = Base<Vrf> & {\n /** Some of `interfaces.vlan_interface`: the VLAN interfaces it joins. Each has no address of its own. */\n member_interfaces: readonly VlanInterface[];\n};\n\n/**\n * Every port and interface, by kind, each keyed by its name. A port's key is `<speed>-<position>` from the model.\n *\n * The type parameters are the names the device declares: `Ethernet` the ports, `L` LAGs, `VI` VLAN interfaces, `T` the other interfaces, `V` VLANs, `A` ACLs, `F` VRFs.\n */\nexport type Interfaces<Ethernet, L extends string, VI extends string, T extends string, V extends string, F extends string> = {\n /** Physical ports, keyed by `<speed>-<position>` from the model, such as `10g-4`. Ports not listed are shut down. */\n ethernet?: Ethernet;\n /** Link aggregations, keyed by name. Each member port names one with `in_lag`. */\n lag?: Named<L, Lag<V, F>>;\n /** Layer 3 interfaces on a VLAN, keyed by name. Each names its VLAN in `vlan`. */\n vlan_interface?: Named<VI, VlanInterface<V, F>>;\n /** Layer 2 bridges between VLAN interfaces, keyed by name. */\n bridge?: Named<T, Bridge<NoInfer<VI>, F>>;\n /** Interfaces on no link, keyed by name. */\n loopback?: Named<T, Loopback<F>>;\n /** GRE tunnels, keyed by name. */\n gre?: Named<T, Gre<F>>;\n /** Point-to-point VXLAN tunnels, keyed by name. */\n vxlan?: Named<T, Vxlan<F>>;\n /** WireGuard interfaces, keyed by name. */\n wireguard?: Named<T, WireGuard<F>>;\n};\n",
13
+ "config/keys.ts": "// Every key Circuit takes, at run time, so `circuit validate` stops on one it doesn't know. One source: the types. Each shape here is typed\n// `ShapeOf<the type>`, which needs every key of the type and allows no other, so a key added to, renamed in or removed from a type\n// fails `tsc` until its shape says the same. The two can't drift.\n\nimport type { CommunityScheme } from \"./communities\";\nimport type { AnyDevice } from \"./device\";\nimport type { FilterRule, NatRule } from \"./firewall\";\nimport type { Checks, Integrations, NetworkSecrets, NetworkSettings, Prefixes, Site } from \"./network\";\nimport type { CallRule, PolicyRule, RouteMatch, RouteRule, RouteSet, RoutingRule } from \"./routing\";\nimport type { CommunitiesExport, CommunitiesImport } from \"./communities\";\nimport type { AclRule } from \"./switching\";\n\n/**\n * Where keys may be: `0` holds none Circuit checks (a number, a name, a list of names); a map lists its keys, each with its own shape;\n * `{ \"*\": shape }` takes any key, such as a VLAN's name; `[shape]` is a list of that item.\n */\nexport type Shape = 0 | readonly [Shape] | { readonly [key: string]: Shape };\n\ntype Prim = string | number | boolean | bigint | symbol | null | undefined;\ntype AllKeys<U> = U extends unknown ? keyof U : never;\ntype ValueAt<U, K> = U extends unknown ? (K extends keyof U ? U[K] : never) : never;\ntype Item<T> = Extract<T, readonly unknown[]> extends readonly (infer E)[] ? E : never;\n\n/** A map with any key (an index signature, such as VLANs by name), or one with keys written out. A key typed `never` is no key. */\ntype ObjectShape<O> =\n {} extends Required<O>\n ? { readonly \"*\": ShapeOf<O[keyof O]> }\n : { readonly [K in AllKeys<O> as [NonNullable<ValueAt<O, K>>] extends [never] ? never : K]-?: ShapeOf<ValueAt<O, K>> };\n\n/** The shape of type `T`: what each shape here must be, exactly. A list of plain values is `0`, as a plain value. */\nexport type ShapeOf<T> = [Exclude<T, Prim>] extends [never]\n ? 0\n : [Exclude<T, Prim | readonly unknown[]>] extends [never]\n ? ShapeOf<Item<T>> extends 0\n ? 0\n : readonly [ShapeOf<Item<T>>]\n : ObjectShape<Exclude<T, Prim | readonly unknown[]>>;\n\nconst SECRET = { secret: 0, secret_file: 0 } as const;\nconst RANGE = { min: 0, max: 0 } as const;\n\n// ---------- rules, each kind on its own ----------\n\nexport const FILTER_MATCH = {\n family: 0,\n protocol: 0,\n src: 0,\n dst: 0,\n src_address_set: 0,\n dst_address_set: 0,\n src_port: RANGE,\n dst_port: RANGE,\n in_interface: 0,\n out_interface: 0,\n in_interface_set: 0,\n out_interface_set: 0,\n connection_state: 0,\n tcp_flags: { set: 0, unset: 0 },\n} as const;\n\nexport const FILTER_RULE = { description: 0, if: FILTER_MATCH, then: 0, fast_path: 0 } as const satisfies ShapeOf<\n FilterRule<string, string, string>\n>;\nexport const NAT_RULE = { description: 0, if: FILTER_MATCH, then: 0, to_address: 0, to_port: 0 } as const satisfies ShapeOf<\n NatRule<string, string, string>\n>;\nexport const ACL_RULE = {\n description: 0,\n if: { family: 0, protocol: 0, src: 0, dst: 0, src_port: RANGE, dst_port: RANGE, vlan: 0 },\n then: 0,\n rate: 0,\n} as const satisfies ShapeOf<AclRule<string>>;\nexport const ROUTING_RULE = {\n description: 0,\n match: { src: 0, dst: 0, in_interface: 0 },\n routing_table: 0,\n fallback: 0,\n} as const satisfies ShapeOf<RoutingRule<string, string>>;\n\nexport const ROUTE_MATCH = {\n family: 0,\n prefix: 0,\n prefix_set: 0,\n prefix_length: RANGE,\n as_path_length: RANGE,\n as_path_contains: 0,\n rpki: 0,\n standard_community: 0,\n large_community: 0,\n} as const satisfies ShapeOf<RouteMatch<string>>;\nexport const ROUTE_SET = {\n local_pref: 0,\n med: 0,\n prepend: 0,\n standard_communities: 0,\n add_standard_communities: 0,\n remove_standard_communities: 0,\n large_communities: 0,\n add_large_communities: 0,\n remove_large_communities: 0,\n next_hop: 0,\n preferred_source: 0,\n blackhole: 0,\n} as const satisfies ShapeOf<RouteSet>;\n/** A route policy rule of each kind: on routes, a call, a communities import, a communities export. */\nexport const ROUTE_RULE = { description: 0, if: ROUTE_MATCH, set: ROUTE_SET, then: 0 } as const satisfies ShapeOf<RouteRule<string>>;\nexport const CALL_RULE = { call: 0 } as const satisfies ShapeOf<CallRule<string>>;\nexport const COMMUNITIES_IMPORT = { communities: 0, accept_requests: 0, add_tags: 0 } as const satisfies ShapeOf<CommunitiesImport<string>>;\nexport const COMMUNITIES_EXPORT = { communities: 0, keep_ours: 0, send_tags: 0 } as const satisfies ShapeOf<CommunitiesExport>;\nexport const POLICY_RULE = { ...ROUTE_RULE, ...CALL_RULE, ...COMMUNITIES_IMPORT, ...COMMUNITIES_EXPORT } as const satisfies ShapeOf<\n PolicyRule<string, string>\n>;\n\n/** What `then` may be in each kind of rule. */\nexport const ACTIONS = {\n filter: [\"accept\", \"drop\", \"reject\"],\n nat: [\"masquerade\", \"snat\", \"dnat\"],\n acl: [\"accept\", \"drop\"],\n policy: [\"accept\", \"reject\"],\n} as const;\n\ntype Same<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;\n// Each list is exactly what its type's `then` takes: `tsc` fails here otherwise.\ntrue satisfies Same<(typeof ACTIONS.filter)[number], FilterRule<string, string, string>[\"then\"]> &\n Same<(typeof ACTIONS.nat)[number], NatRule<string, string, string>[\"then\"]> &\n Same<(typeof ACTIONS.acl)[number], AclRule<string>[\"then\"]> &\n Same<(typeof ACTIONS.policy)[number], NonNullable<RouteRule<string>[\"then\"]>>;\n\n// ---------- a device ----------\n\nconst SERVICE = { allow: 0, port: 0 } as const;\nconst STATIC_ROUTE = {\n prefix: 0,\n next_hop: 0,\n interface: 0,\n blackhole: 0,\n bfd: 0,\n distance: 0,\n vrf: 0,\n routing_table: 0,\n description: 0,\n} as const;\nconst ROUTED = {\n addresses: [{ address: 0, peer_address: 0 }],\n ipv6_ra: 0,\n hardware_offload: 0,\n vrrp: [{ id: 0, address: 0, priority: 0, preempt: 0, interval: 0 }],\n vrf: 0,\n exchange: { name: 0, speed: 0, peeringdb_ixlan: 0 },\n} as const;\nconst THRESHOLD = { pps: 0, percent: 0 } as const;\nconst SWITCHED = {\n access_vlan: 0,\n trunk_vlans: 0,\n native_vlan: 0,\n stp: { edge: 0, cost: 0, priority: 0 },\n storm_control: { broadcast: THRESHOLD, multicast: THRESHOLD, unknown_unicast: THRESHOLD },\n mac_limit: 0,\n ethertypes: 0,\n} as const;\nconst BASE = { ...ROUTED, description: 0, mtu: 0 } as const;\nconst NEIGHBOR_SETTINGS = {\n remote_as: 0,\n description: 0,\n local_address: 0,\n import_policy: 0,\n export_policy: 0,\n role: 0,\n multihop: 0,\n password: SECRET,\n max_prefixes: 0,\n hold_time: 0,\n keepalive: 0,\n address_families: 0,\n passive: 0,\n bfd: 0,\n routing_table: 0,\n communities: { groups: 0, blackhole: 0 },\n} as const;\nconst CHAIN = { default_action: 0, rules: [FILTER_RULE] } as const;\n\n/** Every key a device may have, its parts merged in. */\nexport const DEVICE = {\n name: 0,\n site: 0,\n role: 0,\n platform: 0,\n model: 0,\n connection: {\n host: 0,\n user: 0,\n port: 0,\n console: { jump: { host: 0, user: 0, password: SECRET, port: 0, cloudflared: 0 }, port: 0, baud: 0, password_only: 0 },\n },\n system: {\n timezone: 0,\n banner: 0,\n release_channel: 0,\n ip_stack: { icmp_redirects: 0, syn_cookies: 0 },\n alarms: { \"*\": { severity: 0 } },\n dns: { servers: 0 },\n ntp: { servers: 0 },\n logging: { local: { level: 0, files: 0, lines_per_file: 0 } },\n hardware_offload: 0,\n users: { \"*\": { role: 0, password: SECRET, description: 0, ssh_keys: 0 } },\n certificates: { \"*\": { certificate_file: 0, private_key: SECRET } },\n // A VDOM's routes have no VRF.\n vdoms: {\n management: {\n name: 0,\n interfaces: 0,\n static_routes: [\n { prefix: 0, next_hop: 0, interface: 0, blackhole: 0, bfd: 0, distance: 0, routing_table: 0, description: 0 },\n ],\n },\n },\n },\n management: {\n allow: 0,\n interfaces: 0,\n ssh: { ...SERVICE, weak_crypto: 0, auth_timeout: 0, auth_retries: 0 },\n telnet: SERVICE,\n http: SERVICE,\n https: { ...SERVICE, certificate: 0 },\n api: SERVICE,\n api_tls: { ...SERVICE, certificate: 0 },\n ftp: SERVICE,\n native_protocol: { ...SERVICE, interfaces: 0 },\n console: { password: SECRET },\n privilege_password: SECRET,\n },\n monitoring: {\n snmp: {\n ...SERVICE,\n community: SECRET,\n users: { \"*\": { auth: 0, auth_password: SECRET, privacy: 0, privacy_password: SECRET } },\n contact: 0,\n location: 0,\n },\n syslog: [{ address: 0, port: 0, level: 0 }],\n flow_export: { protocol: 0, collectors: [{ address: 0, port: 0 }], sampling: 0, interfaces: 0 },\n transceivers: 0,\n },\n interfaces: {\n ethernet: {\n \"*\": {\n ...ROUTED,\n ...SWITCHED,\n description: 0,\n mtu: 0,\n speed: 0,\n lldp: 0,\n ingress_acl: 0,\n in_lag: 0,\n link: { device: 0, port: 0, through: 0 },\n },\n },\n lag: { \"*\": { ...BASE, ...SWITCHED, mode: 0, id: 0 } },\n vlan_interface: { \"*\": { ...BASE, vlan: 0, mac: 0 } },\n bridge: { \"*\": { ...BASE, member_interfaces: 0 } },\n loopback: { \"*\": BASE },\n gre: { \"*\": { ...BASE, local_address: 0, remote_address: 0 } },\n vxlan: { \"*\": { ...BASE, vni: 0, local_address: 0, remote_address: 0, port: 0, mac: 0 } },\n wireguard: {\n \"*\": {\n ...BASE,\n listen_port: 0,\n endpoint: 0,\n private_key: SECRET,\n peers: [{ name: 0, public_key: 0, allowed_addresses: 0, endpoint: 0, keepalive: 0, client_allowed_addresses: 0 }],\n },\n },\n },\n switching: {\n vlans: { \"*\": { id: 0, description: 0, untrusted: 0 } },\n stp: { mode: 0, priority: 0 },\n lldp: { interfaces: 0 },\n acls: { \"*\": [ACL_RULE] },\n },\n services: {\n dhcp_server: {\n \"*\": {\n interface: 0,\n network: 0,\n gateway: 0,\n dns: 0,\n pool: 0,\n lease_time: 0,\n reservations: [{ address: 0, mac: 0, description: 0 }],\n },\n },\n dhcp_relay: { \"*\": { interface: 0, servers: 0 } },\n dns_server: 0,\n ntp_server: 0,\n },\n routing: {\n router_id: 0,\n tables: { \"*\": { description: 0 } },\n vrfs: { \"*\": { description: 0 } },\n policy_routing: [ROUTING_RULE],\n static_routes: [STATIC_ROUTE],\n bgp: {\n asn: 0,\n originated_prefixes: 0,\n neighbor_groups: { \"*\": NEIGHBOR_SETTINGS },\n neighbors: { \"*\": { ...NEIGHBOR_SETTINGS, address: 0, neighbor_group: 0 } },\n },\n ospf: {\n address_families: 0,\n areas: { \"*\": { interfaces: { \"*\": { cost: 0, passive: 0, network: 0, bfd: 0 } } } },\n redistribute: 0,\n },\n bfd: { interval: 0, multiplier: 0 },\n rpki: [{ address: 0, port: 0, description: 0 }],\n policies: { \"*\": [POLICY_RULE] },\n prefix_sets: { \"*\": { prefixes: { \"*\": 0 }, source: { registry: 0, query: 0, family: 0 } } },\n },\n firewall: {\n address_sets: { \"*\": 0 },\n interface_sets: { \"*\": 0 },\n filter: { input: CHAIN, forward: CHAIN, output: CHAIN },\n nat: { source: [NAT_RULE], destination: [NAT_RULE] },\n conntrack_helpers: 0,\n untracked: 0,\n },\n} as const satisfies ShapeOf<AnyDevice>;\n\n// ---------- the network ----------\n\nconst OPTION = { code: 0, description: 0 } as const;\n/** Every option of a community scheme. */\nexport const COMMUNITIES = {\n asn: 0,\n groups: { \"*\": { value: 0, description: 0 } },\n learned_from_group: OPTION,\n learned_at_site: OPTION,\n learned_from_as: OPTION,\n do_not_announce_to_as: OPTION,\n do_not_announce_to_group: OPTION,\n prepend: { once: OPTION, twice: OPTION, three_times: OPTION },\n blackhole: OPTION,\n custom_descriptions: { \"*\": 0 },\n} as const satisfies ShapeOf<CommunityScheme>;\n\n/** Circuit's built-in checks, each with its settings. Any other name in `checks` is one of the project's own. */\nexport const CHECKS = {\n untrusted_vlans: { filtering_devices: 0, mixed_links: 0 },\n exports_end_in_reject: 0,\n imports_end_in_accept_or_reject: 0,\n native_vlans_are_dead_ends: 0,\n tunnels_outside_offered: 0,\n same_vlan_ids: 0,\n} as const satisfies ShapeOf<Checks<string>>;\n\n/** Every key `circuit()` takes. `checks` is looked at apart: a project's own check has a name of its own. */\nexport const NETWORK = {\n asn: 0,\n prefixes: { \"*\": { \"*\": 0 } } satisfies ShapeOf<Prefixes>,\n communities: COMMUNITIES,\n sites: { \"*\": { description: 0, number: 0 } satisfies ShapeOf<Site> },\n checks: 0,\n integrations: {\n peeringdb: { api_key: SECRET },\n librenms: { url: 0, api_token: SECRET },\n netbox: { url: 0, api_token: SECRET },\n } satisfies ShapeOf<Integrations>,\n secrets: { file: 0 } satisfies ShapeOf<NetworkSecrets>,\n} as const satisfies { readonly [K in keyof NetworkSettings<{}, number>]-?: Shape };\n\n// `satisfies` misses a key too many that comes from a spread or another constant. Each side assignable to the other doesn't: every key\n// of a shape is required, so a shape with one more key, at any depth, is not the type's. `tsc` fails here then.\ntrue satisfies Same<typeof DEVICE, ShapeOf<AnyDevice>> &\n Same<typeof FILTER_RULE, ShapeOf<FilterRule<string, string, string>>> &\n Same<typeof NAT_RULE, ShapeOf<NatRule<string, string, string>>> &\n Same<typeof ACL_RULE, ShapeOf<AclRule<string>>> &\n Same<typeof ROUTING_RULE, ShapeOf<RoutingRule<string, string>>> &\n Same<typeof ROUTE_RULE, ShapeOf<RouteRule<string>>> &\n Same<typeof CALL_RULE, ShapeOf<CallRule<string>>> &\n Same<typeof COMMUNITIES_IMPORT, ShapeOf<CommunitiesImport<string>>> &\n Same<typeof COMMUNITIES_EXPORT, ShapeOf<CommunitiesExport>> &\n Same<typeof COMMUNITIES, ShapeOf<CommunityScheme>> &\n Same<typeof CHECKS, ShapeOf<Checks<string>>> &\n Same<typeof NETWORK.integrations, ShapeOf<Integrations>> &\n Same<typeof NETWORK.sites, ShapeOf<Readonly<Record<string, Site>>>> &\n Same<typeof NETWORK.prefixes, ShapeOf<Prefixes>> &\n Same<keyof typeof NETWORK, keyof NetworkSettings<{}, number>>;\n\n// ---------- the check ----------\n\n/** A key Circuit doesn't take: where it is, dot by dot (`firewall.filter.input.rules[3].if`), the key, and the keys taken there. */\nexport type UnknownKey = { path: string; key: string; known: string[] };\n\n/**\n * Every key in `value` that `shape` doesn't take. A list met where the shape is a map is a list of such maps (a port range, or several).\n * A plain value met where the shape is a map is one the type allows beside it, such as an address written alone.\n */\nexport function unknownKeys(value: unknown, shape: Shape, path = \"\"): UnknownKey[] {\n if (shape === 0 || !value || typeof value !== \"object\") return [];\n if (Array.isArray(value)) {\n const item: Shape = Array.isArray(shape) ? (shape as readonly [Shape])[0] : shape;\n return value.flatMap((v, i) => unknownKeys(v, item, `${path}[${i}]`));\n }\n if (Array.isArray(shape)) return [];\n const map = shape as { readonly [key: string]: Shape };\n const at = (key: string) => (path ? `${path}.${key}` : key);\n if (Object.hasOwn(map, \"*\")) return Object.entries(value).flatMap(([k, v]) => unknownKeys(v, map[\"*\"]!, at(k)));\n return Object.entries(value).flatMap(([k, v]) =>\n Object.hasOwn(map, k) ? unknownKeys(v, map[k]!, at(k)) : [{ path, key: k, known: Object.keys(map) }]\n );\n}\n\n/** The closest of `names`, for \"Did you mean\": at most two letters off, or a quarter of a long name. */\nexport function closest(name: string, names: Iterable<string>): string | undefined {\n const distance = (a: string, b: string) => {\n let row = Array.from({ length: b.length + 1 }, (_, i) => i);\n for (let i = 1; i <= a.length; i++) {\n const next = [i];\n for (let j = 1; j <= b.length; j++)\n next[j] = Math.min(row[j]! + 1, next[j - 1]! + 1, row[j - 1]! + (a[i - 1] === b[j - 1] ? 0 : 1));\n row = next;\n }\n return row[b.length]!;\n };\n let best: [string, number] | undefined;\n for (const n of names)\n if (distance(name, n) <= Math.max(2, name.length / 4) && (!best || distance(name, n) < best[1])) best = [n, distance(name, n)];\n return best?.[0];\n}\n\n/** What to say about an unknown key: \"Did you mean\" when one is close, else the keys taken there. */\nexport function unknownKeyHelp({ key, known }: UnknownKey): string {\n const hint = closest(key, known);\n return hint ? `Did you mean ${hint}?` : `It takes: ${known.join(\", \")}.`;\n}\n",
14
+ "config/management.ts": "// How people and tools log in: `management.ssh`, `.https`, `.api`, `.native_protocol`, `.console`, and the rest. SNMP polling is `monitoring.snmp`.\n\nimport type { Prefix, Secret } from \"./common\";\n\n/** A way into the device. Absent means turned off. */\nexport type Service = {\n /** Sources allowed. `management.allow` when absent. */\n allow?: readonly Prefix[];\n /** TCP or UDP port. The protocol's own when absent. */\n port?: number;\n};\n\n/** How the device is reached and managed. `I` is the device's ports and interfaces, `C` its certificates, `AS` its address sets. */\nexport type Management<I extends string, C extends string, AS extends string = string> = {\n /**\n * Sources allowed to every service that does not set its own: a list, or one of `firewall.address_sets` by name, so a device that\n * filters on those sources too writes them once. Circuit writes the set's addresses here.\n */\n allow?: readonly Prefix[] | AS;\n /** Some of the device's ports and interfaces: the ones the services answer on; every other one answers ping alone. Every one with an address when absent. */\n interfaces?: readonly I[];\n /** Secure shell. */\n ssh?: Service & {\n /** Also offer legacy ciphers, MACs and key exchanges. Off when absent. */\n weak_crypto?: boolean;\n /** Seconds a login may take. */\n auth_timeout?: number;\n /** Failed attempts allowed per connection. */\n auth_retries?: number;\n };\n /** Unencrypted remote login. */\n telnet?: Service;\n /** Web interface, unencrypted. */\n http?: Service;\n /** Web interface over TLS. */\n https?: Service & {\n /** One of `system.certificates`. */\n certificate?: C;\n };\n /** The platform's own API, unencrypted. */\n api?: Service;\n /** The platform's own API over TLS. */\n api_tls?: Service & {\n /** One of `system.certificates`. */\n certificate?: C;\n };\n /** File transfer, unencrypted. */\n ftp?: Service;\n /** The platform's own management protocol, such as Winbox. `interfaces` also allows it at layer 2 on those interfaces. */\n native_protocol?: Service & {\n /** Some of the device's ports and interfaces. */\n interfaces?: readonly I[];\n };\n /** Console login uses this password. The device's users when absent. */\n console?: {\n /** The console password. */\n password: Secret;\n };\n /** Password for privileged mode, on platforms that have one. */\n privilege_password?: Secret;\n};\n",
15
+ "config/merge.ts": "// `merge`, by MODEL.md's Merge rules for a language with import, and the types that check a merge in the editor. Each map remembers the file that wrote each of its keys, so an error names both.\n\n/** Where each key of a map was written, `file:line`. Not enumerable, so the JSON never shows it. */\nconst WRITTEN = Symbol(\"written in\");\n\n/** For a key written through a function in `lib/`, the line in `lib/` that called Circuit, `file:line`. Not enumerable either. */\nconst INNER = Symbol(\"written through\");\n\ntype Obj = Record<string, unknown>;\nconst isMap = (v: unknown): v is Obj => !!v && typeof v === \"object\" && !Array.isArray(v);\nconst same = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b);\n\n/** Circuit's own folder, the one above this file's: its files are left out when looking for the user's file on the stack. */\nconst CIRCUIT = decodeURIComponent(new URL(\"..\", import.meta.url).pathname)\n .replace(/^\\/([A-Za-z]:)/, \"$1\")\n .toLowerCase();\n\n/** Whether a file on the stack is one of Circuit's own. */\nconst isCircuit = (file: string) => file.replaceAll(\"\\\\\", \"/\").toLowerCase().startsWith(CIRCUIT);\n\n/** The user's frames on the stack of `error`, innermost first, each `file:line`. */\nexport function userFrames(error: Error = new Error()): string[] {\n return (error.stack ?? \"\")\n .split(\"\\n\")\n .map((line) => /\\(?((?:[A-Za-z]:)?[^():]+\\.ts):(\\d+)/.exec(line))\n .filter((m): m is RegExpExecArray => !!m && !isCircuit(m[1]!))\n .map((m) => `${m[1]!.trim()}:${m[2]}`);\n}\n\n/**\n * The user's file and line that called into Circuit: the outermost frame outside Circuit. A part a function in `lib/` builds is\n * named after the file that called that function, such as the tenant's file, which is the one to change.\n */\nexport const caller = (): string => userFrames().at(-1) ?? \"?\";\n\n/** The innermost frame outside Circuit, such as the line in `lib/` that called `part`. */\nexport const innerCaller = (): string => userFrames()[0] ?? \"?\";\n\n/** Marks every map in a value as written in `file`, and through `inner` when that is another line, where nothing marked it before. */\nexport function mark<T>(value: T, file: string, inner = file): T {\n if (Array.isArray(value)) value.forEach((v) => mark(v, file, inner));\n else if (isMap(value)) {\n if (!Object.hasOwn(value, WRITTEN)) {\n Object.defineProperty(value, WRITTEN, { value: Object.fromEntries(Object.keys(value).map((k) => [k, file])) });\n if (inner !== file)\n Object.defineProperty(value, INNER, { value: Object.fromEntries(Object.keys(value).map((k) => [k, inner])) });\n }\n for (const v of Object.values(value)) mark(v, file, inner);\n }\n return value;\n}\n\n/** Where each key of a map was written, for an error that names the file. */\nexport const writtenIn = (map: object, key: string): string => (map as { [WRITTEN]?: Record<string, string> })[WRITTEN]?.[key] ?? \"?\";\n\n/** The line in `lib/` a key was written through, or nothing. */\nconst innerIn = (map: object, key: string): string | undefined => (map as { [INNER]?: Record<string, string> })[INNER]?.[key];\n\n/** Reads a file of the project as its lines. Circuit gives it when it reads a project (src/project/project.ts): the project's editor has no Node types, so this file reads none itself. */\nexport const source = { read: (_file: string): string => \"\" };\n\n/** Each file's lines, read once. */\nconst lines = new Map<string, string[]>();\nconst escape = (text: string) => text.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\n/**\n * From `written`, `file:line`, the first line at or after it that writes each of `path` in turn, as `file:line`, or undefined when the last\n * of `path` is not found. A key not found before the last is skipped.\n */\nfunction find(written: string, path: string[]): string | undefined {\n const colon = written.lastIndexOf(\":\");\n if (colon < 0) return undefined;\n const file = written.slice(0, colon);\n let line = Number(written.slice(colon + 1)) - 1;\n if (!lines.has(file))\n try {\n lines.set(file, source.read(file).split(\"\\n\"));\n } catch {\n lines.set(file, []);\n }\n const text = lines.get(file)!;\n let found = -1;\n path.forEach((name, p) => {\n const writes = new RegExp(`(^|[\\\\s{,])([\"']?)${escape(name)}\\\\2\\\\s*:`);\n const i = text.findIndex((t, n) => n >= line && !t.trim().startsWith(\"//\") && writes.test(t));\n if (i >= 0) line = found = i;\n else if (p === path.length - 1) found = -1;\n });\n return found >= 0 ? `${file}:${found + 1}` : undefined;\n}\n\n/**\n * Where `key` of `map` is written, as `file:line: `, or nothing when no file is known. `writtenIn` knows the line that made the map (a\n * network's `device()`, a `literal`, a `part`); from there, the file is read for the first line that writes each of `path` in turn,\n * `path` ending in the key: `[\"vlans\", \"exchange-a\"]`. A key written through `lib/` is looked for there first. When the key is not found,\n * the line that made the map stays.\n */\nexport function writtenAt(map: unknown, key: string, path: string[] = [key]): string {\n const outer = writtenIn(map as object, key);\n const inner = innerIn(map as object, key);\n const at = (inner && find(inner, path)) ?? find(outer, path) ?? (outer === \"?\" ? undefined : outer);\n return at ? `${at}: ` : \"\";\n}\n\n/** From `written`, `file:line`, the line that writes the last of `path`, as `file:line: `; `written` itself when it is not found. */\nexport const lineOf = (written: string, path: string[]): string => `${find(written, path) ?? written}: `;\n\n/**\n * An ordered list, by its path: a chain's rules, NAT, an ACL, a route policy, policy routing. Under a source, the hostname comes\n * first (`edge-01.firewall.nat.source`). It is written whole in one place, never joined.\n */\nconst ORDERED =\n /(^|\\.)(firewall\\.filter\\.[^.]+\\.rules|firewall\\.nat\\.(source|destination)|switching\\.acls\\.[^.]+|routing\\.policies\\.[^.]+|routing\\.policy_routing)$/;\nexport const isOrdered = (path: string) => ORDERED.test(path);\n\n/** A value that is one value, compared whole and never merged key by key: a secret, a link, a threshold, a port range. */\nconst isOneValue = (key: string, v: Obj) =>\n key === \"link\" ||\n \"secret\" in v ||\n \"secret_file\" in v ||\n \"pps\" in v ||\n \"percent\" in v ||\n (\"min\" in v && \"max\" in v && Object.keys(v).length === 2);\n\n/** Marks a rule list `part` made. Not enumerable, so nothing else sees it. */\nconst RULE_LIST = Symbol(\"rule list\");\nexport const ruleList = <T extends object>(list: T): T => Object.defineProperty(list, RULE_LIST, { value: true });\nconst isRuleList = (v: unknown) => Array.isArray(v) && Object.hasOwn(v, RULE_LIST);\n\n/**\n * Merges `b` into a copy of `a`. Two rule lists `part` made, at one path that is no ordered list, join in order with every rule kept:\n * each tenant's router input rules, one tenant after another, for the router to place.\n */\nfunction mergeTwo(a: Obj, b: Obj, path: string): Obj {\n const out: Obj = { ...a };\n const written: Record<string, string> = {};\n const inner: Record<string, string> = {};\n const note = (from: Obj, key: string) => {\n written[key] = writtenIn(from, key);\n const through = innerIn(from, key);\n if (through) inner[key] = through;\n else delete inner[key];\n };\n for (const key of Object.keys(a)) note(a, key);\n\n for (const [key, value] of Object.entries(b)) {\n const here = path ? `${path}.${key}` : key;\n if (!(key in out)) {\n out[key] = value;\n note(b, key);\n continue;\n }\n const old = out[key];\n const both = `in ${written[key]} and in ${writtenIn(b, key)}`;\n if (Array.isArray(old) && Array.isArray(value) && isOrdered(here))\n throw new Error(\n `${here} is an ordered list, written ${both}. Write it whole in one place, and place the other's rules in it with merge.`\n );\n if (isRuleList(old) && isRuleList(value)) out[key] = ruleList([...(old as unknown[]), ...(value as unknown[])]);\n else if (Array.isArray(old) && Array.isArray(value)) out[key] = [...old, ...value.filter((v) => !old.some((o) => same(o, v)))];\n else if (isMap(old) && isMap(value) && !isOneValue(key, old) && !isOneValue(key, value)) out[key] = mergeTwo(old, value, here);\n else if (!same(old, value))\n throw new Error(\n `${here} is written twice with two values: ${JSON.stringify(old)} in ${written[key]}, and ${JSON.stringify(value)} in ${writtenIn(b, key)}. One name holds one value: rename one, or make both the same.`\n );\n }\n\n Object.defineProperty(out, WRITTEN, { value: written });\n if (Object.keys(inner).length) Object.defineProperty(out, INNER, { value: inner });\n return out;\n}\n\n/** Merges parts in order. The file of a part not marked yet is `file`. */\nexport function mergeAll(parts: readonly unknown[], file: string, path = \"\"): Obj {\n let out: Obj = {};\n for (const part of parts) if (part !== undefined) out = mergeTwo(out, mark(part as Obj, file), path);\n return out;\n}\n\n// ---------- `merge([...])`: one list or one map, by where it lands ----------\n\n/** What a `merge` call was given, flat, and the file that called it. Not enumerable, so nothing else sees it. */\nconst MADE = Symbol(\"merge items\");\ntype Made = { [MADE]: { values: readonly unknown[]; file: string } };\nconst madeBy = (v: unknown) => (v && typeof v === \"object\" && Object.hasOwn(v, MADE) ? (v as Made)[MADE] : undefined);\n\n/** The items, flat: a list in the items gives its own items in its place, in order. */\nconst flat = (items: readonly unknown[]): unknown[] => items.flatMap((v) => (Array.isArray(v) ? flat(v) : [v]));\n\n/**\n * What `merge` returns before it lands: a list when an item is a list or no map, else the maps merged. The same rule the editor uses\n * for a `merge` that lands in no field. Where it lands decides (`resolve`).\n */\nexport function made(items: readonly unknown[], file: string): object {\n const values = flat(items);\n // One map given twice, such as a tenant: its data would be kept once and its rules twice, and nothing would say so.\n if (values.some((v, i) => isMap(v) && values.indexOf(v) !== i))\n throw new Error(`${file}: merge is given the same value twice. Give each once.`);\n const isList = items.some(Array.isArray) || values.some((v) => !isMap(v));\n let out: object = [...values];\n if (!isList)\n try {\n out = mergeAll(values, file);\n } catch (error) {\n // Maps that clash may be single rules, which a list field takes as they are: they stay a list until they land (`resolve`).\n // A field of them read before, such as `tenants.parts` of two tenants with one VLAN name, says what clashed, naming both\n // values and where each is written, not that the field is missing. Never `then`, which a promise reads.\n const clash = `${file}: merge: ${error instanceof Error ? error.message : String(error)}`;\n for (const key of new Set(values.flatMap((v) => Object.keys(v as object))))\n if (key !== \"then\")\n Object.defineProperty(out, key, {\n get: () => {\n throw new Error(clash);\n },\n });\n }\n return Object.defineProperty(out, MADE, { value: { values, file } });\n}\n\n/**\n * Lists of the model whose items are maps, besides the ordered ones: there, a merge of single maps is a list of them, never one map.\n * Taken from Circuit's schema (every array of objects under a device).\n */\nconst MAP_LISTS =\n /(^|\\.)(routing\\.(static_routes|rpki)|monitoring\\.(syslog|flow_export\\.collectors)|system\\.vdoms\\.[^.]+\\.static_routes|interfaces\\.[^.]+\\.[^.]+\\.(addresses|peers|vrrp)|services\\.dhcp_server\\.[^.]+\\.reservations)$/;\n\n/**\n * Every `merge` in a value, made again by the field it lands in, `path`, which Circuit knows from the model:\n *\n * - in an ordered list (`isOrdered`), its items flat, in order, each kept as it is;\n * - in a list of maps (`MAP_LISTS`), or when an item is no map (a VLAN name, a port), its items flat, the same item twice kept once;\n * - anywhere else, its items merged as maps, by `mergeAll`.\n *\n * So two single rules, `merge([rule_a, rule_b])`, are two rules in a chain and never one map: the chain's path says so. A source in a\n * device's `include` lands at the top, so it is a map, keyed by hostname. Changes maps and lists in place, so a rule keeps its identity.\n */\nexport function resolve(value: unknown, path: string): unknown {\n const by = madeBy(value);\n if (by) {\n const { values, file } = by;\n if (isOrdered(path)) value = [...values];\n else if (MAP_LISTS.test(path) || values.some((v) => !isMap(v)))\n value = values.filter((v, i, all) => all.findIndex((o) => same(o, v)) === i);\n else if (Array.isArray(value))\n try {\n value = mergeAll(values, file, path);\n } catch (error) {\n throw new Error(`${file}: merge: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n if (Array.isArray(value)) value.forEach((v, i) => (value[i] = resolve(v, `${path}[]`)));\n else if (isMap(value))\n for (const [k, v] of Object.entries(value)) if (v && typeof v === \"object\") value[k] = resolve(v, path ? `${path}.${k}` : k);\n return value;\n}\n\n// ---------- the types: what the editor checks ----------\n\n/**\n * Two parts merged as `merge` merges them: maps key by key, lists joined, anything else the later one. A key one side writes out keeps\n * its own type, even when the other side has an index signature.\n */\nexport type Merge2<A, B> = [A] extends [readonly (infer X)[]]\n ? [B] extends [readonly (infer Y)[]]\n ? readonly (X | Y)[]\n : B\n : [A] extends [object]\n ? [B] extends [object]\n ? {\n [K in keyof A | keyof B]: K extends WrittenKeys<A>\n ? K extends WrittenKeys<B>\n ? Merge2<A[K], B[K]>\n : A[K]\n : K extends WrittenKeys<B>\n ? B[K]\n : (K extends keyof A ? A[K] : never) | (K extends keyof B ? B[K] : never);\n }\n : B\n : B;\n\n/** Parts merged in order. */\nexport type Merged<T extends readonly unknown[], Acc = {}> = T extends readonly [infer H, ...infer R] ? Merged<R, Merge2<Acc, H>> : Acc;\n\n/** The first `N` parts. */\ntype Take<T extends readonly unknown[], N extends number, Acc extends unknown[] = []> = Acc[\"length\"] extends N\n ? Acc\n : T extends readonly [infer H, ...infer R]\n ? Take<R, N, [...Acc, H]>\n : Acc;\n\n/** The parts before position `I` of `T`, merged, with `First` before them. */\nexport type Before<T extends readonly unknown[], I, First = {}> = I extends `${infer N extends number}`\n ? Merged<[First, ...Take<T, N>]>\n : First;\n\n/** The keys written out, not those of an index signature. */\ntype WrittenKeys<A> = keyof { [K in keyof A as number extends K ? never : string extends K ? never : K]: 0 };\n\n/** Keys to look into: a map's, without symbols. */\ntype Keys<B> = keyof B & (string | number);\n\n/** The path of an ordered list, as `isOrdered` reads it. */\ntype OrderedPath =\n | `${string}firewall.filter.${string}.rules`\n | `${string}firewall.nat.${\"source\" | \"destination\"}`\n | `${string}switching.acls.${string}`\n | `${string}routing.policies.${string}`\n | `${string}routing.policy_routing`;\n\n/**\n * Every place where part `B` clashes with what came before it, `A`, said in words: an ordered list both write, or another value at\n * one path. Any other list never clashes: it joins.\n */\ntype ClashPaths<B, A, Path extends string = \"\"> = {\n [K in Keys<B>]: K extends WrittenKeys<A>\n ? [B[K]] extends [readonly unknown[]]\n ? `${Path}${K}` extends OrderedPath\n ? `${Path}${K} is an ordered list written in two places`\n : never\n : [B[K]] extends [object]\n ? ClashPaths<B[K], A[K & keyof A], `${Path}${K}.`>\n : [B[K]] extends [A[K & keyof A]]\n ? never\n : [A[K & keyof A]] extends [B[K]]\n ? never\n : `${Path}${K} is written twice`\n : never;\n}[Keys<B>];\n\n/**\n * What part `B` must also be, after `A`: nothing more when nothing clashes, and else a property named after each clash that it lacks,\n * so the editor says `Property '\"edge-01.firewall.nat.source is an ordered list written in two places\"' is missing`. An\n * ordered list is written whole in one place. Any other value written twice must be the same.\n */\nexport type Clash<B, A> = [ClashPaths<B, A>] extends [never] ? unknown : { [P in ClashPaths<B, A>]: true };\n",
16
+ "config/models.ts": "// Every platform Circuit runs on, every model it knows, each model's ports and how it prints them. The types read from here, so a model or port that does not exist fails to compile.\n\nimport { models as fortios } from \"../platforms/fortios/models\";\nimport { models as junos } from \"../platforms/junos/models\";\nimport { models as raisecom } from \"../platforms/raisecom-ros/models\";\nimport { models as routeros } from \"../platforms/routeros/models\";\nimport { models as vrp } from \"../platforms/vrp/models\";\n\n/** What a model's hardware holds, where a real device held less than its datasheet lists. `circuit validate` checks a config against it. */\nexport type ModelLimits = {\n /** The most member ports one LAG may have. More is an error. */\n readonly lag_members?: number;\n /** About how many switch rules the switch chip held. More is a warning: a rule the chip cannot hold is invalid, and so is every rule after it, without a log line. */\n readonly switch_rules?: number;\n /** How many distinct port ranges the switch rules held. More is a warning, for the same reason. */\n readonly switch_port_ranges?: number;\n};\n\n/** One hardware model: its ports per speed class, how the platform spells a port, and what its hardware holds. */\nexport type ModelSpec = {\n /** How many ports of each speed class, such as `{ \"10g\": 48 }`. A port is `<class>-<position>`, such as `10g-4`. */\n readonly ports: Readonly<Record<string, number>>;\n /** How the platform spells a port of each class. `{n}` is the position within the class; `{n+4}` and `{n-1}` shift it, because some platforms number every port in one run or count from 0. */\n readonly spelling: Readonly<Record<string, string>>;\n /** What the hardware held, where a real device held less than its datasheet. */\n readonly limits?: ModelLimits;\n};\n\n/** Every model of every platform: each platform's own list, in its folder under `src/platforms/`. */\nexport const MODELS = { routeros, vrp, \"raisecom-ros\": raisecom, junos, fortios } as const satisfies Record<\n string,\n Record<string, ModelSpec>\n>;\n\ntype Models = typeof MODELS;\n\n/** The operating system of a device. It decides which adapter renders and applies the device. */\nexport type Platform = keyof Models;\n\n/** A hardware model the platform runs on. It decides which port names exist. */\nexport type Model<P extends Platform> = P extends Platform ? keyof Models[P] & string : never;\n\ntype Upto<N extends number, A extends unknown[] = [], R = never> = A[\"length\"] extends N\n ? R\n : Upto<N, [...A, 0], R | `${[...A, 0][\"length\"]}`>;\ntype SpecOf<P extends Platform, M extends string> = P extends Platform ? (M extends keyof Models[P] ? Models[P][M] : never) : never;\ntype PortsOf<P extends Platform, M extends string> = SpecOf<P, M> extends { ports: infer Ports } ? Ports : never;\n\n/** A port of the model, `<speed class>-<position>`: `10g-1` to `10g-48` on a model with 48 10G ports. */\nexport type PortName<P extends Platform, M extends string> = {\n [S in keyof PortsOf<P, M>]: `${S & string}-${Upto<PortsOf<P, M>[S] & number>}`;\n}[keyof PortsOf<P, M>];\n\n/** Every port a model has, in order. */\nexport function portsOf(platform: string, model: string): string[] {\n const spec = (MODELS as Record<string, Record<string, ModelSpec>>)[platform]?.[model];\n if (!spec) return [];\n return Object.entries(spec.ports).flatMap(([speed, count]) => Array.from({ length: count }, (_, i) => `${speed}-${i + 1}`));\n}\n\n/** A port as the platform prints it, from the model's `spelling`: `10g-2` on a FG-3700D is `port6`. The port as written when the model has no spelling for it. */\nexport function spell(platform: string, model: string, port: string): string {\n const spec = (MODELS as Record<string, Record<string, ModelSpec>>)[platform]?.[model];\n const [speed = \"\", n = \"\"] = port.split(\"-\");\n const pattern = spec?.spelling[speed];\n return pattern ? pattern.replace(/\\{n([+-]\\d+)?\\}/g, (_, shift: string | undefined) => String(Number(n) + Number(shift ?? 0))) : port;\n}\n\n/** The platforms where a VLAN is only its number: no name, no description. A VLAN interface holds its own. Circuit drops a VLAN's `description` there, as their renderers do. */\nexport const NUMBERED_VLANS: readonly Platform[] = [\"raisecom-ros\", \"fortios\"];\n",
17
+ "config/monitoring.ts": "// What the box reports to collectors: `monitoring.snmp`, `.syslog`, `.flow_export`, `.transceivers`.\n\nimport type { IP, Secret, Severity } from \"./common\";\nimport type { Service } from \"./management\";\n\n/** One SNMPv3 user. */\nexport type SnmpUser = {\n /** How messages are authenticated. */\n auth: \"sha1\" | \"sha256\";\n /** The authentication passphrase. */\n auth_password: Secret;\n /** How messages are encrypted. */\n privacy: \"aes128\" | \"des\";\n /** The encryption passphrase. */\n privacy_password: Secret;\n};\n\n/** One syslog collector. */\nexport type SyslogServer = {\n /** Where the collector listens. */\n address: IP;\n /** UDP port. 514 when absent. */\n port?: number;\n /** The least severe message sent. */\n level: Severity;\n};\n\n/** Export flow records or packet samples to collectors. */\nexport type FlowExport<I extends string> = {\n /** The record format. */\n protocol: \"sflow\" | \"netflow-v9\" | \"ipfix\";\n /** Where records are sent. */\n collectors: readonly {\n /** Where the collector listens. */\n address: IP;\n /** UDP port. When absent: 6343 for sFlow and 4739 for IPFIX (IANA), 2055 for NetFlow v9 (the usual one). */\n port?: number;\n }[];\n /** Sample one packet in this many. Every packet when absent, on a platform that can export every packet. */\n sampling?: number;\n /** Some of the device's ports and interfaces: where traffic is observed. Every interface when absent. */\n interfaces?: readonly I[];\n};\n\n/** What the device reports to collectors. `I` is the device's ports and interfaces. */\nexport type Monitoring<I extends string> = {\n /** Polling by SNMP. */\n snmp?: Service & {\n /** Read-only community for version 2c. Version 2c is off when absent. */\n community?: Secret;\n /** Version 3 users, keyed by name. */\n users?: Record<string, SnmpUser>;\n /** Who to contact about the device. */\n contact?: string;\n /** Where the device is. */\n location?: string;\n };\n /** Log messages sent to syslog collectors, each from its own `level` up. */\n syslog?: readonly SyslogServer[];\n /** Flow records or packet samples sent to collectors. */\n flow_export?: FlowExport<I>;\n /** Read optic temperature, power and voltage. */\n transceivers?: boolean;\n};\n",
18
+ "config/network.ts": "// The whole network, what `circuit()` in `circuit.ts` at a project's root takes: `asn`, `prefixes`, `communities`, `sites`, `checks`, `integrations`. Its devices register with it.\n\nimport type { Prefix, Secret } from \"./common\";\nimport type { CommunityRequest, CommunityScheme, SchemeCheck } from \"./communities\";\nimport type { AnyDevice } from \"./device\";\nimport type { RouteRule } from \"./routing\";\n\n/**\n * The network's own prefixes, in groups of your choosing, such as a location (`id01`), each a map of named prefixes:\n * `{ region_a: { v4_1: \"192.0.2.0/24\", v6_1: \"2001:db8::/48\" } }`.\n */\nexport type Prefixes = Readonly<Record<string, Readonly<Record<string, Prefix>>>>;\n\n/**\n * One BGP community the network defines, in the community description format of the NLNOG Ring looking glass, which bgp.tools also reads.\n */\nexport type Community = {\n /** The community. `nnn` stands for any number, `x` for one digit and `a-b` for a range, such as `65535:0:nnn`. */\n community: string;\n /** What it means. `$0`, `$1` and so on stand for what each wildcard matched, such as `do not announce to AS$0`. */\n description: string;\n};\n\n/** A site: a data centre or an office. */\nexport type Site = {\n /** What and where the site is. */\n description?: string;\n /**\n * The site's number, the last part of the community `learned_at_site` tags a route with, such as `64500:2:0`. Only Circuit reads\n * it: the resolved network leaves it out.\n */\n number?: number;\n};\n\n/** Circuit's built-in checks, by name. A check is on when it is here. `D` is the network's device names: `circuit validate` checks each is a device of the network. */\nexport type Checks<D extends string> = {\n /**\n * Where traffic from outside, before anything filters it, may go. The untrusted VLANs are those written `untrusted: true` in\n * `switching.vlans`; every other VLAN is trusted, so a VLAN added later is protected without being listed. `untrusted: true` is\n * allowed only while this check is on.\n */\n untrusted_vlans?: {\n /** The devices that filter: the only ones that may give an untrusted VLAN an address, and the only ones a port carrying both kinds may face. */\n filtering_devices: readonly D[];\n /** Links between two switches that may carry both kinds, each named by its two devices: a site's switch extended to another building, with an exchange's VLAN and its own management on one cable. Neither end gives an untrusted VLAN an address. */\n mixed_links?: readonly (readonly [D, D])[];\n };\n /** Every BGP export ends in a reject that matches everything, so nothing leaves by accident. An export to a neighbor whose `role` is `customer` is left out. */\n exports_end_in_reject?: true;\n /** Every BGP import ends in a rule that decides for every route, an accept or a reject without a match. */\n imports_end_in_accept_or_reject?: true;\n /** A native VLAN is a dead end: native on one trunk (a port or a LAG) of a device at most, and never an access VLAN or a tagged VLAN anywhere on that device. */\n native_vlans_are_dead_ends?: true;\n /** A GRE or VXLAN tunnel's local address is outside what the neighbors over it are offered, or their replies route back into the tunnel. */\n tunnels_outside_offered?: true;\n /**\n * One VLAN name has one ID on every device of the network. Circuit runs it when it reads the project, and `circuit validate` stops on a VLAN\n * with two IDs, naming both devices and where each is written. So the resolved network always passes it, and leaves it out.\n */\n same_vlan_ids?: true;\n};\n\n/**\n * What one of your own checks found: a sentence, and the device and path it is about, when it knows them. Circuit prints it with the\n * file and line that write that path, as for its own errors: `sites/site-a/switch-01.ts:21: tenants_dont_clash: switch-01: interfaces.ethernet.10g-2: ...`.\n */\nexport type Finding = {\n /** What is wrong, in a sentence. */\n message: string;\n /** The device it is about, by name. */\n device?: string;\n /** Where in that device, dot by dot, as Circuit names a field: `interfaces.ethernet.10g-2`, `routing.policies.transit-a-export[2]`. */\n path?: string;\n};\n\n/**\n * A device as your own checks read it: as it will run. Each BGP neighbor holds what it takes from its group. Each policy is route rules\n * only: a `call` is the called policy's rules, and a `{ communities }` rule the rules it means there. `management.allow` written as an\n * address set's name is that set's addresses, and a rule's interface set is its interfaces. A copy: a check changes nothing.\n */\nexport type ResolvedDevice = Omit<AnyDevice, \"routing\" | \"management\"> & {\n readonly routing?: Omit<NonNullable<AnyDevice[\"routing\"]>, \"policies\"> & {\n readonly policies?: Readonly<Record<string, readonly RouteRule<string>[]>>;\n };\n readonly management?: Omit<NonNullable<AnyDevice[\"management\"]>, \"allow\"> & { readonly allow?: readonly Prefix[] };\n};\n\n/** The network as your own checks read it: its AS, its prefixes, and each device as it will run, its parts merged in. */\nexport type ResolvedNetwork = { readonly asn?: number; readonly prefixes: Prefixes; readonly devices: readonly ResolvedDevice[] };\n\n/**\n * A check of your own, beside Circuit's: it reads the network resolved, as its devices will run it, and returns what is wrong. Nothing\n * found is an empty list. `circuit validate` runs it; the resolved network never shows it.\n */\nexport type Check = (network: ResolvedNetwork) => readonly Finding[];\n\n/** Where a network's secrets are: a file of `NAME=\"value\"` lines, each value encrypted, changed with `circuit secrets`. */\nexport type NetworkSecrets = {\n /** The file, relative to the project, such as `.circuit/lab-secrets` for a lab network with its own password. Commit it. */\n file: string;\n};\n\n/** Systems Circuit keeps in step with the config. */\nexport type Integrations = {\n /** Keeps the network's PeeringDB record in step with the config: `circuit peeringdb`. Needs `asn`. */\n peeringdb?: {\n /** An API key with write access to the network's record. */\n api_key: Secret;\n };\n /** Keeps LibreNMS monitoring every device, under its name, with its SNMP credentials and location, and in a group for its site: `circuit librenms`. */\n librenms?: {\n /** Where LibreNMS is, such as `https://librenms.example.com`. */\n url: `http://${string}` | `https://${string}`;\n /** An API token that can add and change devices and device groups. */\n api_token: Secret;\n };\n /** Keeps NetBox in step with the config: `circuit netbox`. Each device's `site` and `role` place it there. */\n netbox?: {\n /** Where NetBox is, such as `https://netbox.example.com`. */\n url: `http://${string}` | `https://${string}`;\n /** An API token that can write. */\n api_token: Secret;\n };\n};\n\n/**\n * What a device takes from its network, for the editor: whether `untrusted_vlans` is on, whether the network writes `communities`, and\n * the scheme's groups and requests.\n */\nexport type NetworkFacts = { untrusted: boolean; communities: boolean; groups: string; requests: string };\n\n/** Any network's facts: what a part, or a device seen from the network, takes. */\nexport type AnyFacts = { untrusted: true; communities: true; groups: string; requests: CommunityRequest };\n\n/** Whether a site has a number, which `learned_at_site` needs. */\nexport type Numbered<Sites> = true extends (\n Sites[keyof Sites] extends infer X ? (X extends { readonly number: number } ? true : false) : never\n)\n ? true\n : false;\n\n/**\n * What `circuit()` takes: the whole network but its devices, which register with it from their own files. `Sites` is its sites as\n * written. `Own` is the `checks` as written: a key that is not one of Circuit's is a check of your own. `C` is the `communities` as\n * written. `P` is the `prefixes` as written.\n */\nexport type NetworkSettings<\n Sites extends Readonly<Record<string, Site>>,\n Asn extends number,\n Own = {},\n C = undefined,\n P extends Prefixes = {},\n> = {\n /** The AS the network operates. A device's BGP runs it, unless the device writes `routing.bgp.asn`. */\n asn?: Asn;\n /**\n * The network's own prefixes, in groups of your choosing, each a map of named prefixes: `{ region_a: { v4_1: \"192.0.2.0/24\" } }`.\n * Devices use them by name, `as64500.prefixes.id01.v4_1`, and your own checks read them. The resolved network keeps them, for the\n * commands that would use them: PeeringDB's prefix counts, and RPKI and IRR checks.\n */\n prefixes?: P;\n /**\n * The network's BGP large communities, `asn:code:value` (RFC 8092), used as RFC 8195 describes. Written once here: a neighbor group\n * says which `groups` it is in, and a policy places `{ communities: \"import\" }` or `{ communities: \"export\" }`. Circuit writes the\n * catalogue from it, for a looking glass or bgp.tools.\n */\n communities?: C & CommunityScheme & SchemeCheck<C, Numbered<Sites>>;\n /** Each site, by name: a data centre or an office. The NetBox site and LibreNMS group of that name. A device's `site` is one of these, and each has a device. */\n sites: Sites;\n /**\n * Circuit's built-in checks, turned on by name, with their settings, and your own: any other name takes a function that reads the\n * network and returns what it found (`Check`). `circuit validate` runs them all. `untrusted: true` on a VLAN is allowed only while\n * `untrusted_vlans` is here.\n */\n checks?: Own & Checks<string> & { [K in Exclude<keyof Own, keyof Checks<string>>]: Check };\n /** Systems Circuit keeps in step with the config. */\n integrations?: Integrations;\n /** Where the network's secrets are. `.circuit/secrets` when absent. */\n secrets?: NetworkSecrets;\n};\n",
19
+ "config/presets.ts": "// Values every network writes the same way, each from a public standard it names. Plain data, in one `presets` object: pick from it, or\n// place its rules where they go in an ordered list with `merge`. Data is snake_case, like every field.\n\nimport type { Prefix } from \"./common\";\nimport type { FilterRule, Ports } from \"./firewall\";\nimport type { RouteRule } from \"./routing\";\n\nconst bogons = {\n ipv4: {\n /** \"This network\", RFC 791 and RFC 1122. */\n this_network: \"0.0.0.0/8\",\n /** Private use, RFC 1918. */\n private10: \"10.0.0.0/8\",\n /** Shared address space for carrier-grade NAT, RFC 6598. */\n shared_address_space: \"100.64.0.0/10\",\n /** Loopback, RFC 1122. */\n loopback: \"127.0.0.0/8\",\n /** Link-local, RFC 3927. */\n link_local: \"169.254.0.0/16\",\n /** Private use, RFC 1918. */\n private172: \"172.16.0.0/12\",\n /** IETF protocol assignments, RFC 6890. */\n ietf_protocols: \"192.0.0.0/24\",\n /** Documentation, TEST-NET-1, RFC 5737. */\n documentation1: \"192.0.2.0/24\",\n /** Private use, RFC 1918. */\n private192: \"192.168.0.0/16\",\n /** Benchmarking, RFC 2544. */\n benchmarking: \"198.18.0.0/15\",\n /** Documentation, TEST-NET-2, RFC 5737. */\n documentation2: \"198.51.100.0/24\",\n /** Documentation, TEST-NET-3, RFC 5737. */\n documentation3: \"203.0.113.0/24\",\n /** Multicast, RFC 5771. */\n multicast: \"224.0.0.0/4\",\n /** Reserved for future use, RFC 1112. */\n reserved: \"240.0.0.0/4\",\n },\n ipv6: {\n /** The unspecified address, RFC 4291. */\n unspecified: \"::/128\",\n /** Loopback, RFC 4291. */\n loopback: \"::1/128\",\n /** IPv4-mapped addresses, RFC 4291. */\n ipv4_mapped: \"::ffff:0:0/96\",\n /** Discard-only, RFC 6666. */\n discard_only: \"100::/64\",\n /** Documentation, RFC 3849. */\n documentation: \"2001:db8::/32\",\n /** Unique local, RFC 4193. */\n unique_local: \"fc00::/7\",\n /** Link-local, RFC 4291. */\n link_local: \"fe80::/10\",\n /** Multicast, RFC 4291. */\n multicast: \"ff00::/8\",\n },\n} as const satisfies Record<\"ipv4\" | \"ipv6\", Record<string, Prefix>>;\n\nconst bgp_sanity: readonly RouteRule<never>[] = [\n { description: \"IPv4 shorter than /8\", if: { family: \"ipv4\", prefix_length: { max: 7 } }, then: \"reject\" },\n { description: \"IPv4 longer than /24\", if: { family: \"ipv4\", prefix_length: { min: 25 } }, then: \"reject\" },\n { description: \"IPv6 shorter than /16\", if: { family: \"ipv6\", prefix_length: { max: 15 } }, then: \"reject\" },\n { description: \"IPv6 longer than /48\", if: { family: \"ipv6\", prefix_length: { min: 49 } }, then: \"reject\" },\n { description: \"RPKI invalid\", if: { rpki: \"invalid\" }, then: \"reject\" },\n];\n\nconst bad_tcp_flags: readonly FilterRule<never, never, never>[] = [\n { description: \"Bad flags FIN+SYN\", if: { protocol: \"tcp\", tcp_flags: { set: [\"fin\", \"syn\"], unset: [] } }, then: \"drop\" },\n { description: \"Bad flags SYN+RST\", if: { protocol: \"tcp\", tcp_flags: { set: [\"syn\", \"rst\"], unset: [] } }, then: \"drop\" },\n { description: \"Bad flags FIN+RST\", if: { protocol: \"tcp\", tcp_flags: { set: [\"fin\", \"rst\"], unset: [] } }, then: \"drop\" },\n { description: \"Bad flags FIN without ACK\", if: { protocol: \"tcp\", tcp_flags: { set: [\"fin\"], unset: [\"ack\"] } }, then: \"drop\" },\n { description: \"Bad flags FIN+URG\", if: { protocol: \"tcp\", tcp_flags: { set: [\"fin\", \"urg\"], unset: [] } }, then: \"drop\" },\n {\n description: \"Bad flags Xmas scan\",\n if: { protocol: \"tcp\", tcp_flags: { set: [\"fin\", \"syn\", \"psh\", \"urg\"], unset: [] } },\n then: \"drop\",\n },\n];\n\n/** Values every network writes the same way, each from a public standard. A global: write `presets.bogons`, with no import. */\nexport const presets = {\n /**\n * Address blocks that never appear as a source or destination on the internet: the IANA IPv4 and IPv6 Special-Purpose Address\n * Registries (RFC 6890), each block with the RFC that reserves it. Pick the ones your filters need: `presets.bogons.ipv4.loopback`.\n */\n bogons,\n\n /** The IPv4 private ranges of RFC 1918, in address order. */\n rfc1918: [bogons.ipv4.private10, bogons.ipv4.private172, bogons.ipv4.private192] as const,\n\n /** The default route of each family: every destination, `0.0.0.0/0` (RFC 4632) and `::/0` (RFC 4291). */\n default_routes: { ipv4: \"0.0.0.0/0\", ipv6: \"::/0\" } as const satisfies Record<\"ipv4\" | \"ipv6\", Prefix>,\n\n /** Well-known ports, by the service names of the IANA Service Name and Transport Protocol Port Number Registry (RFC 6335). */\n ports: {\n /** DNS, RFC 1035. */\n dns: 53,\n /** DHCP and BOOTP server, RFC 2131. */\n dhcp_server: 67,\n /** DHCP and BOOTP client, RFC 2131. */\n dhcp_client: 68,\n /** NTP, RFC 5905. */\n ntp: 123,\n /** SNMP, RFC 3411. */\n snmp: 161,\n /** BGP, RFC 4271. */\n bgp: 179,\n /** Windows networking: MS-RPC endpoint mapper (135), NetBIOS (137 to 139, RFC 1001 and RFC 1002) and SMB (445), as IANA lists them. */\n windows_networking: [135, { min: 137, max: 139 }, 445],\n } as const satisfies Record<string, Ports>,\n\n /**\n * Route policy rules rejecting what no neighbor should send, for an import, or as a policy every import calls (MANRS action 1,\n * filtering). In order: IPv4 shorter than /8 and longer than /24, IPv6 shorter than /16 and longer than /48: too specific for the\n * global table (RFC 7454, section 6.1.3), or wider than any block one network holds. Then routes RPKI finds invalid (RFC 6811).\n * A policy of its own (`\"common-sanity\": presets.bgp_sanity`), or placed with `merge`: `merge([presets.bgp_sanity, own_rule])`.\n */\n bgp_sanity,\n\n /** Firewall rules dropping TCP flag combinations no packet that follows TCP (RFC 9293) carries, for a forward chain. Place them with `merge`. */\n bad_tcp_flags,\n\n /**\n * UDP services that answer a small request with a much larger reply, which attackers use to reflect a flood at a victim: the ports\n * of CISA's alert TA14-017A, \"UDP-Based Amplification Attacks\", plus six that are widely abused the same way but not in it: SLP,\n * MS-SQL, ARD, CoAP, Ubiquiti and DVR. A reflected flood arrives from these. Pick the ports a rule needs by name: `src_port: [presets.udp_amplifiers.qotd, presets.udp_amplifiers.chargen]`.\n */\n udp_amplifiers: {\n qotd: 17,\n chargen: 19,\n dns: 53,\n portmap: 111,\n ntp: 123,\n netbios: 137,\n snmp: 161,\n cldap: 389,\n slp: 427,\n rip: 520,\n ms_sql: 1434,\n ssdp: 1900,\n ard: 3283,\n ws_discovery: 3702,\n mdns: 5353,\n coap: 5683,\n ubiquiti: 10001,\n memcached: 11211,\n dvr: 37810,\n } as const,\n};\n",
20
+ "config/routing.ts": "// Where packets go: `routing.tables`, `.vrfs`, `.policy_routing`, `.static_routes`, `.bgp`, `.ospf`, `.bfd`, `.rpki`, `.policies`, `.prefix_sets`.\n\nimport type { Family, HostAddress, IP, Named, Ordered, Prefix, Secret } from \"./common\";\nimport type { CommunitiesRule, NeighborCommunities } from \"./communities\";\nimport { ACTIONS, CALL_RULE, closest, COMMUNITIES_EXPORT, COMMUNITIES_IMPORT, ROUTE_MATCH, ROUTE_RULE, ROUTE_SET } from \"./keys\";\nimport type { AnyFacts, NetworkFacts } from \"./network\";\n\n/** A route the device has without learning it. `I` is the device's ports and interfaces, `F` its VRFs, `T` its tables. */\nexport type StaticRoute<I extends string, F extends string, T extends string> = {\n /** Destination. */\n prefix: Prefix;\n /** Next-hop address, alone or with its length. */\n next_hop?: HostAddress;\n /** One of the device's ports or interfaces: the next hop, for a link without a next-hop address. */\n interface?: I;\n /** Discard matching traffic. The usual way to originate an aggregate. */\n blackhole?: true;\n /** Withdraw the route while BFD finds `next_hop` dead, and put it back once `next_hop` answers again. Needs `next_hop`. */\n bfd?: true;\n /** Preference against other routes to the same prefix, 1 to 255. Lower wins. 1 when absent. */\n distance?: number;\n /** One of `routing.vrfs`: the VRF the route is in. A route has a `vrf` or a `routing_table`, not both. */\n vrf?: F;\n /** One of `routing.tables`: the table the route is in. The main table when absent. A route has a `vrf` or a `routing_table`, not both. */\n routing_table?: T | \"main\";\n /** Why the route exists. */\n description?: string;\n};\n\n/**\n * What a BGP neighbor is to us, with the roles RFC 9234 defines: `provider`, a transit we buy from; `customer`, one we sell transit to; `peer`, one we exchange our own routes with; `rs`, an exchange's route server; `rs-client`, a network using our route server.\n */\nexport type BgpRole = \"provider\" | \"customer\" | \"peer\" | \"rs\" | \"rs-client\";\n\n/** Settings a BGP neighbor can set itself or take from its group. `P` is the device's policies, `T` its tables, `Net` what the network writes. */\nexport type NeighborSettings<P extends string, T extends string, Net extends NetworkFacts = AnyFacts> = {\n /** The neighbor's AS. Our own AS makes the session internal. */\n remote_as?: number;\n /** Who the neighbor is. */\n description?: string;\n /** Source address for the session, alone or with its length. The outgoing interface's address when absent. */\n local_address?: HostAddress;\n /** One of `routing.policies`: the policy for routes learned. Nothing is accepted when absent. */\n import_policy?: P;\n /** One of `routing.policies`: the policy for routes announced. Nothing is announced when absent. */\n export_policy?: P;\n /** What the neighbor is to us, such as `provider` for a transit. Both sides then reject a route leak, as RFC 9234 describes. */\n role?: BgpRole;\n /** The neighbor is more than one hop away. */\n multihop?: boolean;\n /** TCP MD5 password shared with the neighbor. */\n password?: Secret;\n /** Close the session when the neighbor announces more prefixes than this. It stays closed until cleared by hand, on RouterOS with `/routing bgp session clear <session> flag=limit-exceeded`. */\n max_prefixes?: number;\n /** Seconds without a message before the session is declared down, 3 or more. */\n hold_time?: number;\n /** Seconds between keepalive messages. */\n keepalive?: number;\n /** Address families exchanged. The family of the neighbor's address when absent. */\n address_families?: readonly Family[];\n /** Wait for the neighbor to connect instead of connecting to it. */\n passive?: boolean;\n /** Detect a dead link in under a second with BFD. */\n bfd?: boolean;\n /** One of `routing.tables`: the table the routes learned are installed in. The main table when absent. On RouterOS the session also announces from that table, so each prefix of `originated_prefixes` it announces needs a route in the table too, and a running session takes a new table only when it restarts. */\n routing_table?: T | \"main\";\n /**\n * The groups of the network's `communities` it is in, and its own blackhole community, for the `{ communities }` rules of its\n * policies: `{ groups: [\"transit\"], blackhole: \"64501:666\" }`. Only Circuit reads it. Allowed only when the network writes `communities`.\n */\n communities?: Net[\"communities\"] extends true ? NeighborCommunities<Net[\"groups\"]> : \"allowed only when circuit.ts writes communities\";\n};\n\n/** One BGP session. Settings it does not set come from its neighbor group. */\nexport type Neighbor<P extends string, G extends string, T extends string, Net extends NetworkFacts = AnyFacts> = NeighborSettings<\n P,\n T,\n Net\n> & {\n /** The neighbor's address. */\n address: IP;\n /** One of `routing.bgp.neighbor_groups`: the group this neighbor takes a setting from when it does not set that setting itself. */\n neighbor_group?: G;\n};\n\n/** BGP on this device. `G` is the device's neighbor groups. */\nexport type Bgp<P extends string, G extends string, T extends string, Net extends NetworkFacts = AnyFacts> = {\n /** Our AS number. The network's `asn` when absent: the model needs one here, so Circuit writes the network's own. */\n asn?: number;\n /** Prefixes this device originates. Each needs a route, for example a static blackhole. Export policy still decides who receives them. */\n originated_prefixes?: readonly Prefix[];\n /** Settings shared by several neighbors, keyed by group name. */\n neighbor_groups?: Named<G, NeighborSettings<P, T, Net>>;\n /** Sessions, keyed by name. */\n neighbors?: Record<string, Neighbor<P, NoInfer<G>, T, Net>>;\n};\n\n/** How fast BFD notices a dead neighbor: after `multiplier` packets in a row are missed, so in `interval` times `multiplier` milliseconds. */\nexport type Bfd = {\n /** Milliseconds between the packets this device sends, and the shortest gap it asks of the neighbor. */\n interval: number;\n /** How many packets in a row may be missed before the neighbor is declared down. */\n multiplier: number;\n};\n\n/** A route origin validation server, RFC 8210. */\nexport type RpkiServer = {\n /** Where the validator listens. */\n address: IP;\n /** Its RTR port, often 323 or 8282. */\n port: number;\n /** Who runs it, or why it is here. */\n description?: string;\n};\n\n/** OSPF on one interface. */\nexport type OspfInterface = {\n /** Cost of sending through this interface, 1 to 65535. Lower is preferred. Derived from speed when absent. */\n cost?: number;\n /** Advertise the interface's network but form no adjacency on it. */\n passive?: boolean;\n /** How the link is treated. Broadcast when absent. */\n network?: \"broadcast\" | \"point-to-point\";\n /** Detect a dead neighbor in under a second with BFD. */\n bfd?: boolean;\n};\n\n/** An OSPF area ID, four numbers with dots between them, for example `0.0.0.0` for the backbone. */\nexport type OspfAreaId = `${number}.${number}.${number}.${number}`;\n\n/** OSPF, version 2 for IPv4 and version 3 for IPv6. */\nexport type Ospf<I extends string> = {\n /** Which versions run. IPv4 when absent. */\n address_families?: readonly Family[];\n /** Areas keyed by area ID, for example `0.0.0.0`, each with the interfaces in it. */\n areas: Record<\n OspfAreaId,\n {\n /** Some of the device's ports and interfaces, each with its OSPF settings. */\n interfaces: Named<I, OspfInterface>;\n }\n >;\n /** Routes from other sources announced into OSPF. */\n redistribute?: readonly (\"connected\" | \"static\")[];\n};\n\n/** A routing table beside the main one. It holds no interfaces: routes go in from static routes and BGP neighbors that name it in `routing_table`, and `routing.policy_routing` sends traffic to it. */\nexport type RoutingTable = {\n /** What the table is for. */\n description?: string;\n};\n\n/** A separate routing table, with the interfaces that route in it. */\nexport type Vrf = {\n /** What the VRF is for. */\n description?: string;\n};\n\n/** Traffic a routing rule matches: all of what is given. Every packet when absent. */\nexport type RoutingRuleMatch<I extends string> = {\n /** Source prefix. */\n src?: Prefix;\n /** Destination prefix. */\n dst?: Prefix;\n /** One of the device's ports or interfaces: the traffic arrives on it. */\n in_interface?: I;\n};\n\n/** One policy routing rule: traffic it matches is routed by `routing_table` instead of the main table. */\nexport type RoutingRule<I extends string, T extends string> = {\n /** Why the rule exists. */\n description?: string;\n /** What the traffic must match. Every packet when absent, so such a rule is the last one. */\n match?: RoutingRuleMatch<I>;\n /** One of `routing.tables`, or `main`: the table that routes the traffic. Always written: choosing it is what the rule is for. */\n routing_table: T | \"main\";\n /** When `routing_table` has no route for a packet: `none` drops it as unroutable, `main` routes it by the main table instead. `none` when absent. Not for a rule whose table is `main`. */\n fallback?: \"none\" | \"main\";\n};\n\n/** A standard BGP community, `asn:value`, for example `64500:100`. Each part is 0 to 65535. */\nexport type StandardCommunity = `${number}:${number}`;\n\n/** A large BGP community, `asn:function:parameter`, for example `64500:1:6939`. Each part is 0 to 4294967295. */\nexport type LargeCommunity = `${number}:${number}:${number}`;\n\n/** A standard community to remove. `*` stands for any value of a part, as in `64500:*`. */\nexport type StandardCommunityPattern = `${number | \"*\"}:${number | \"*\"}`;\n\n/** A large community to remove. `*` stands for any value of a part, as in `64500:*:*`. */\nexport type LargeCommunityPattern = `${number | \"*\"}:${number | \"*\"}:${number | \"*\"}`;\n\n/** Prefixes listed here, or fetched from a registry by `refresh` and never typed by hand. */\nexport type PrefixSet =\n | {\n /** The prefixes, of either family or both: a list, or a map of named prefixes, which gives every value. Where the platform keeps one list per family, Circuit writes one of each. */\n prefixes: readonly Prefix[] | Readonly<Record<string, Prefix>>;\n }\n | {\n /** Where the prefixes are fetched from. */\n source: {\n /** The registry they come from. */\n registry: \"ripe-stat\";\n /** What to ask for, for example `AS64501`. */\n query: string;\n /** Which family to keep. */\n family: Family;\n };\n };\n\n/** Conditions on a route. All must be true. An empty match is true for every route. `S` is the device's prefix sets. */\nexport type RouteMatch<S extends string> = {\n /** Only routes of this family. */\n family?: Family;\n /** Exactly this prefix. */\n prefix?: Prefix;\n /** One of `routing.prefix_sets`: any prefix in it. */\n prefix_set?: S;\n /** Prefix length within this range, inclusive, 0 to 128. */\n prefix_length?: {\n /** The shortest length. */\n min?: number;\n /** The longest length. */\n max?: number;\n };\n /** AS path length within this range, inclusive. Every AS counts, prepends included. */\n as_path_length?: {\n /** The shortest path. */\n min?: number;\n /** The longest path. */\n max?: number;\n };\n /** The AS path holds this AS anywhere: the route came from it or through it. */\n as_path_contains?: number;\n /** The route's origin validation state. */\n rpki?: \"valid\" | \"invalid\" | \"not-found\";\n /** Has this standard community, `asn:value`. */\n standard_community?: StandardCommunity;\n /** Has this large community, `asn:function:parameter`. */\n large_community?: LargeCommunity;\n};\n\n/** Changes to a route. In community lists to remove, `*` stands for any value of a part, as in `64500:*:*`. */\nexport type RouteSet = {\n /** Preference among routes to the same prefix within our AS. Higher wins. */\n local_pref?: number;\n /** Multi-exit discriminator offered to the neighbor. Lower wins. */\n med?: number;\n /** Prepend our own AS this many times, 1 to 16, to make the path look longer. */\n prepend?: number;\n /** Replace the route's standard communities with these. */\n standard_communities?: readonly StandardCommunity[];\n /** Add these standard communities. */\n add_standard_communities?: readonly StandardCommunity[];\n /** Remove standard communities matching these. A part may be `*`. */\n remove_standard_communities?: readonly StandardCommunityPattern[];\n /** Replace the route's large communities with these. */\n large_communities?: readonly LargeCommunity[];\n /** Add these large communities. */\n add_large_communities?: readonly LargeCommunity[];\n /** Remove large communities matching these. A part may be `*`. */\n remove_large_communities?: readonly LargeCommunityPattern[];\n /** Next hop for the route. */\n next_hop?: IP;\n /** Source address for traffic the device itself sends to the route, alone or with its length. Applies to routes of the same family as the address. */\n preferred_source?: HostAddress;\n /** Drop traffic to the route on this device, instead of forwarding it. */\n blackhole?: true;\n};\n\n/** A route policy rule that runs another policy, and holds nothing else: `{ call: \"exchange-import\" }`. */\nexport type CallRule<P extends string> = {\n /** One of `routing.policies`, run here. A route it accepts or rejects stops there; any other goes on, with what it set. To call it for some routes only, put the condition inside that policy. */\n call: P;\n /** A call rule holds nothing else. */\n communities?: never;\n};\n\n/** A route policy rule on routes: which ones (`if`), what to change (`set`), and what then (`then`). It has `then`, `set`, or both. */\nexport type RouteRule<S extends string> = {\n /** Shown beside the rule on the device. */\n description?: string;\n /** Which routes the rule applies to. Every route when absent. */\n if?: RouteMatch<S>;\n /** What to change on a route that matches. Without `then`, the route goes on to the next rule, changed. */\n set?: RouteSet;\n /** What happens to a route that matches, after `set`: accepted or rejected, and the policy ends for it. Without `then`, it goes on to the next rule. */\n then?: \"accept\" | \"reject\";\n /** A rule that calls another policy holds nothing else. */\n call?: never;\n /** A communities rule holds nothing else. */\n communities?: never;\n};\n\n/**\n * One step of a route policy, plain data, the condition first:\n *\n * - `{ if: { prefix_set: \"bogons\" }, then: \"reject\" }` rejects the routes that match. No `if`: every route, `{ then: \"accept\" }`.\n * - `{ if: { prefix: \"0.0.0.0/0\" }, set: { local_pref: 110 }, then: \"accept\" }` changes them, then accepts them.\n * - `{ if: { ... }, set: { local_pref: 300 } }` changes them and goes on to the next rule.\n * - `{ call: \"other-policy\" }` runs one of `routing.policies`, and holds nothing else.\n * - `{ communities: \"import\" }` or `{ communities: \"export\" }`: the rules the network's `communities` mean here, which Circuit writes\n * for the neighbors that use the policy. Allowed only when the network writes `communities`.\n *\n * The model keeps each as written.\n */\nexport type PolicyRule<P extends string, S extends string, Net extends NetworkFacts = AnyFacts> =\n | CallRule<P>\n | RouteRule<S>\n | (Net[\"communities\"] extends true\n ? CommunitiesRule<Net[\"requests\"]>\n : { communities: \"allowed only when circuit.ts writes communities\" });\n\n/** What a route policy rule, its `if` and its `set` may hold, at run time: from keys.ts, which the types above check. */\nconst RULE_KEYS = Object.keys(ROUTE_RULE);\nconst MATCH_KEYS = Object.keys(ROUTE_MATCH);\nconst SET_KEYS = Object.keys(ROUTE_SET);\nconst COMMUNITIES_RULE: Record<string, string[]> = {\n import: Object.keys(COMMUNITIES_IMPORT).filter((k) => k !== \"communities\"),\n export: Object.keys(COMMUNITIES_EXPORT).filter((k) => k !== \"communities\"),\n};\n\n/** Keys of `written` not in `takes`, each with \"did you mean\" where one is close: `protocl (did you mean protocol?)`. */\nconst wrongKeys = (written: string[], takes: string[]) =>\n written\n .filter((k) => !takes.includes(k))\n .map((k) => {\n const hint = closest(k, takes);\n return hint ? `${k} (did you mean ${hint}?)` : k;\n });\n\n/** What is wrong with a route policy rule as written, or nothing. */\nexport function policyRuleProblem(rule: unknown): string | undefined {\n if (!rule || typeof rule !== \"object\" || Array.isArray(rule))\n return `${JSON.stringify(rule)} is no rule. A rule is { if, set, then }, or { call }.`;\n const r = rule as Record<string, unknown>;\n const keys = Object.keys(r);\n if (\"communities\" in r) {\n const takes = COMMUNITIES_RULE[r.communities as string];\n if (!takes) return `communities is import or export, not ${JSON.stringify(r.communities)}.`;\n const more = wrongKeys(\n keys.filter((k) => k !== \"communities\"),\n takes\n );\n return more.length ? `a communities ${r.communities} rule has no ${more.join(\", \")}. It takes: ${takes.join(\", \")}.` : undefined;\n }\n if (\"call\" in r) {\n if (typeof r.call !== \"string\") return \"call takes the name of a policy.\";\n const more = keys.filter((k) => !Object.hasOwn(CALL_RULE, k));\n return more.length ? `a call rule holds nothing else, but this one has ${more.join(\", \")}.` : undefined;\n }\n const wrong = wrongKeys(keys, RULE_KEYS);\n if (wrong.length) return `a rule has no ${wrong.join(\", \")}. It takes: ${RULE_KEYS.join(\", \")}, or call alone.`;\n const then: readonly unknown[] = ACTIONS.policy;\n if (r.then !== undefined && !then.includes(r.then)) {\n const hint = typeof r.then === \"string\" ? closest(r.then, ACTIONS.policy) : undefined;\n return `then is accept or reject, not ${JSON.stringify(r.then)}.${hint ? ` Did you mean ${hint}?` : \"\"}`;\n }\n if (r.then === undefined && r.set === undefined) return \"the rule does nothing: it needs then, set, or both.\";\n const match = wrongKeys(Object.keys((r.if as object | undefined) ?? {}), MATCH_KEYS);\n if (match.length) return `if has no ${match.join(\", \")}. It takes: ${MATCH_KEYS.join(\", \")}.`;\n const set = wrongKeys(Object.keys((r.set as object | undefined) ?? {}), SET_KEYS);\n return set.length ? `set has no ${set.join(\", \")}. It takes: ${SET_KEYS.join(\", \")}.` : undefined;\n}\n\n/**\n * Where packets go. The type parameters are the names the device declares: `P` policies, `S` prefix sets, `G` neighbor groups, `T` tables, `F` VRFs; `I` is its ports and interfaces. `Part` is true in a part, where BGP holds only what the part adds. `Net` is what the network writes.\n */\nexport type Routing<\n P extends string,\n S extends string,\n G extends string,\n T extends string,\n F extends string,\n I extends string,\n Part extends boolean = false,\n Net extends NetworkFacts = AnyFacts,\n> = {\n /** Identifies this router to its neighbors. */\n router_id?: IP;\n /** Routing tables beside the main one, keyed by name. Not `main`, and not the name of a VRF. */\n tables?: Named<T, RoutingTable>;\n /** Separate routing tables with the interfaces that route in them, keyed by name. */\n vrfs?: Named<F, Vrf>;\n /** Rules in order that route matching traffic by another table, such as traffic from one block by its own transits' default. The first that matches decides; traffic no rule matches is routed by the main table. */\n policy_routing?: Ordered<RoutingRule<I, NoInfer<T>>>;\n /** Routes the device has without learning them. A set: order means nothing. */\n static_routes?: readonly StaticRoute<I, NoInfer<F>, NoInfer<T>>[];\n /** BGP sessions and what they exchange. In a part, only what the part adds, such as its neighbor groups and neighbors. */\n bgp?: Part extends true ? Partial<Bgp<NoInfer<P>, G, NoInfer<T>, Net>> : Bgp<NoInfer<P>, G, NoInfer<T>, Net>;\n /** OSPF areas and interfaces. */\n ospf?: Ospf<I>;\n /** BFD timers for every BFD session on the device: BGP, OSPF and static routes. The platform's own when absent; Junos has none, so Circuit sends every 300 ms there. Both ends of a link should agree. */\n bfd?: Bfd;\n /** Validators answering route origin validation. */\n rpki?: readonly RpkiServer[];\n /** Route policies, keyed by name. Each is rules in order, written whole in one place: the first runs first, and a route no rule accepts is rejected. */\n policies?: Named<P, Ordered<PolicyRule<NoInfer<P>, NoInfer<S>, Net>>>;\n /** Prefix sets that policies match on, keyed by name. */\n prefix_sets?: Named<S, PrefixSet>;\n};\n",
21
+ "config/services.ts": "// What the box serves to hosts: `services.dhcp_server`, `.dhcp_relay`, `.dns_server`, `.ntp_server`.\n\nimport type { IP, MacAddress, Prefix } from \"./common\";\n\n/** An address a DHCP server keeps for one client. */\nexport type Reservation = {\n /** The address it keeps. */\n address: IP;\n /** The client's MAC address. */\n mac: MacAddress;\n /** Whose address it is. */\n description?: string;\n};\n\n/** A DHCP server for one network. */\nexport type DhcpServer<I extends string> = {\n /** One of the device's ports or interfaces: where it answers. */\n interface: I;\n /** The network it serves. */\n network: Prefix;\n /** Default gateway given to clients. */\n gateway: IP;\n /** Resolvers given to clients. */\n dns?: readonly IP[];\n /** First and last address given to clients. */\n pool: readonly [IP, IP];\n /** Seconds a lease lasts. */\n lease_time?: number;\n /** Addresses fixed to a MAC. */\n reservations?: readonly Reservation[];\n};\n\n/** Forward DHCP requests heard on an interface to servers elsewhere. */\nexport type DhcpRelay<I extends string> = {\n /** One of the device's ports or interfaces: where requests are heard. */\n interface: I;\n /** Where they are forwarded, in order of preference. */\n servers: readonly IP[];\n};\n\n/** What the device serves to hosts. `I` is the device's ports and interfaces. */\nexport type Services<I extends string> = {\n /** DHCP servers, keyed by name, one per network. */\n dhcp_server?: Record<string, DhcpServer<I>>;\n /** DHCP relays, keyed by name. */\n dhcp_relay?: Record<string, DhcpRelay<I>>;\n /** Answer DNS queries from others, from `system.dns.servers`. Off when absent. */\n dns_server?: true;\n /** Answer time queries from others. Off when absent. */\n ntp_server?: true;\n};\n",
22
+ "config/switching.ts": "// Layer 2 shared by ports: `switching.vlans`, `.stp`, `.lldp`, `.acls`.\n\nimport type { Family, Named, OneOrMore, Ordered, Prefix, Rate } from \"./common\";\nimport type { Ports, Protocol } from \"./firewall\";\n\n/**\n * A VLAN. Its key in `switching.vlans` is its name. `Untrusted` is whether the network turns on the check `untrusted_vlans`, which\n * `untrusted` needs.\n */\nexport type Vlan<Untrusted extends boolean = true> = {\n /** The VLAN's number on the wire, 1 to 4094. Two VLANs on a device never share one. */\n id: number;\n /** What the VLAN is for. Dropped on a platform where a VLAN is only its number (Raisecom, FortiOS), so a device takes a VLAN whole wherever it runs. */\n description?: string;\n /**\n * The VLAN carries traffic straight from outside: a transit, an exchange, an ISP handoff, or such a one's native VLAN. Only Circuit\n * reads it, for the check `untrusted_vlans`; the device never sees it. Write it on every device that declares the VLAN. Allowed only\n * when the network's `circuit()` turns on `untrusted_vlans`: an error in the editor, and `circuit validate` stops.\n */\n untrusted?: Untrusted extends true ? true : \"allowed only when circuit.ts turns on the check untrusted_vlans\";\n};\n\n/**\n * Which packets an ACL rule applies to. All must be true.\n *\n * `vlan`, `src` and `dst` each take a list. Where the platform has no list, Circuit writes one rule per combination: the VLAN\n * outermost, then `src`, then `dst`.\n */\nexport type AclMatch<V extends string> = {\n /** Only packets of this family. */\n family?: Family;\n /** IP protocol. */\n protocol?: Protocol;\n /** Source prefix, or any of several. */\n src?: OneOrMore<Prefix>;\n /** Destination prefix, or any of several. */\n dst?: OneOrMore<Prefix>;\n /** Source port, or any of several. */\n src_port?: Ports;\n /** Destination port, or any of several. */\n dst_port?: Ports;\n /** One of `switching.vlans`, or any of several: only packets in this VLAN. */\n vlan?: OneOrMore<V>;\n};\n\n/**\n * A stateless rule evaluated in hardware where traffic arrives, before routing, the condition first:\n * `{ if: { vlan: \"ix\", src: \"10.0.0.0/8\" }, then: \"drop\" }`. No `if`: every packet.\n */\nexport type AclRule<V extends string> = {\n /** Shown beside the rule on the device. */\n description?: string;\n /** Which packets the rule applies to. Every packet when absent. */\n if?: AclMatch<V>;\n /** What happens to a matching packet. */\n then: \"accept\" | \"drop\";\n /** Accepted traffic above this rate is dropped. On RouterOS it did not limit traffic the CPU routes; see its TRAPS.md. */\n rate?: Rate;\n};\n\n/** Spanning tree on every switched port. A port may override it. */\nexport type Stp = {\n /** Which variant runs. RSTP when absent. */\n mode?: \"stp\" | \"rstp\" | \"mstp\";\n /** Bridge priority, 0 to 61440 in steps of 4096. Lower wins the root election. */\n priority?: number;\n};\n\n/** Layer 2 that the ports share. `V` is the device's VLAN names, `A` its ACL names, `I` its ports and interfaces. `Untrusted` is whether the network turns on `untrusted_vlans`. */\nexport type Switching<V extends string, A extends string, I extends string, Untrusted extends boolean = true> = {\n /** VLANs, keyed by name. A port, a LAG or a VLAN interface names one of these. */\n vlans?: Named<V, Vlan<Untrusted>>;\n /** Spanning tree on every switched port. A port may override it. */\n stp?: Stp;\n /** Advertise and learn neighbors with LLDP on every port (`true`), or only on the interfaces listed. A port may override it. */\n lldp?:\n | true\n | {\n /** Some of the device's ports and interfaces. */\n interfaces: readonly I[];\n };\n /** Hardware ACLs, keyed by name, applied to a port with `ingress_acl`. Each is rules in order, written whole in one place: the first runs first. */\n acls?: Named<A, Ordered<AclRule<NoInfer<V>>>>;\n};\n",
23
+ "config/system.ts": "// The box itself: `system.timezone`, `.banner`, `.release_channel`, `.ip_stack`, `.alarms`, `.dns`, `.ntp`, `.logging`, `.hardware_offload`, `.users`, `.certificates`, `.vdoms`.\n\nimport type { IP, Named, Secret, Severity } from \"./common\";\nimport type { StaticRoute } from \"./routing\";\nimport type { TimeZone } from \"./time-zones\";\n\n/** A software release track, as RouterOS names them. `long-term` changes least; `development` is the newest and least tested. */\nexport type ReleaseChannel = \"long-term\" | \"stable\" | \"testing\" | \"development\";\n\n/** How bad an alarm is, most severe first, as ITU-T X.733 ranks them. */\nexport type AlarmSeverity = \"critical\" | \"major\" | \"minor\" | \"warning\";\n\n/** A login on the device. */\nexport type User = {\n /** What the user may do. */\n role: \"admin\" | \"operator\" | \"read-only\";\n /** The login password. */\n password: Secret;\n /** Who the account is for. */\n description?: string;\n /** Public keys accepted instead of the password over SSH. */\n ssh_keys?: readonly string[];\n};\n\n/** A certificate the device presents or trusts. */\nexport type Certificate = {\n /** The certificate, a PEM file (`.pem` or `.crt`): a path from the project root. Public, so it is kept in the repository. */\n certificate_file: string;\n /** Its private key: a PEM file through `secretFile`, or PEM base64-encoded as one line through `secret`. A certificate the device only trusts, such as a CA, has none. */\n private_key?: Secret;\n};\n\n/**\n * Virtual domains, on FortiOS: the device's own traffic kept apart from the traffic it routes. Everything is in `root` when absent.\n *\n * Multi-VDOM mode is turned on by hand, not by a plan: it moves the whole configuration into `root` and ends every session.\n */\nexport type Vdoms<I extends string> = {\n /** A VDOM for the device's own traffic: FortiGuard, NTP, DNS and logging leave from it, by its own routes, so they never follow the routed traffic's default route. Everything not listed here stays in `root`. */\n management: {\n /** Its name, such as `mgmt`. Not `root`. */\n name: string;\n /** Some of the device's ports and interfaces: the ones in it. A LAG's member ports go with the LAG. */\n interfaces: readonly I[];\n /** Its static routes, such as its own default route. */\n static_routes?: readonly StaticRoute<I, never, never>[];\n };\n};\n\n/** Device-wide settings: the box itself. `U` is the device's user names, `C` its certificate names, `I` its ports and interfaces. */\nexport type System<U extends string, C extends string, I extends string> = {\n /** IANA zone, for example `Asia/Jakarta`. The editor suggests every name. UTC when absent. */\n timezone?: TimeZone;\n /** Shown at login. */\n banner?: string;\n /** Software release track the device updates from. `stable` when absent. */\n release_channel?: ReleaseChannel;\n /** Host behaviour of the IP stack. */\n ip_stack?: {\n /** Send ICMP redirects. Off when absent. */\n icmp_redirects?: boolean;\n /** Answer a TCP SYN flood with SYN cookies. Off when absent. */\n syn_cookies?: boolean;\n };\n /** Alarms raised at another severity than the platform's own, keyed by the alarm's name as the platform spells it, such as `hwOpticalUnAuthorized` on VRP. */\n alarms?: Record<\n string,\n {\n /** The severity it is raised at. */\n severity: AlarmSeverity;\n }\n >;\n /** Resolvers the device uses. Answering others is `services.dns_server`. */\n dns?: {\n /** Resolvers, in order of preference. */\n servers: readonly IP[];\n };\n /** How the device keeps time. Answering others is `services.ntp_server`. */\n ntp?: {\n /** Time sources, by address (alone or with its length) or name, in order of preference. */\n servers: readonly string[];\n };\n /** Log messages kept on the device itself. Sending them to collectors is `monitoring.syslog`. */\n logging?: {\n /** Kept on the device's own storage, from `level` up. */\n local?: {\n /** The least severe message kept. */\n level: Severity;\n /** How many rotated files to keep. */\n files?: number;\n /** Lines in each file before it rotates. */\n lines_per_file?: number;\n };\n };\n /** Forward in hardware where the platform can. An interface may override it. */\n hardware_offload?: boolean;\n /** Logins, keyed by user name. Users not listed are removed. */\n users?: Named<U, User>;\n /** Certificates the device presents or trusts, keyed by name. `management.https` and `management.api_tls` name one. */\n certificates?: Named<C, Certificate>;\n /** Virtual domains, on FortiOS only: a management VDOM apart from the routed traffic. */\n vdoms?: Vdoms<I>;\n};\n",
24
+ "config/time-zones.ts": "// Every IANA time zone name Circuit knows, copied from Circuit 0.43 (`src/schema/time-zones.ts`, generated from the runtime).\n\n/** An IANA time zone name, such as `Asia/Jakarta`, or `UTC`. */\nexport type TimeZone =\n | \"UTC\"\n | \"Africa/Abidjan\"\n | \"Africa/Accra\"\n | \"Africa/Addis_Ababa\"\n | \"Africa/Algiers\"\n | \"Africa/Asmara\"\n | \"Africa/Asmera\"\n | \"Africa/Bamako\"\n | \"Africa/Bangui\"\n | \"Africa/Banjul\"\n | \"Africa/Bissau\"\n | \"Africa/Blantyre\"\n | \"Africa/Brazzaville\"\n | \"Africa/Bujumbura\"\n | \"Africa/Cairo\"\n | \"Africa/Casablanca\"\n | \"Africa/Ceuta\"\n | \"Africa/Conakry\"\n | \"Africa/Dakar\"\n | \"Africa/Dar_es_Salaam\"\n | \"Africa/Djibouti\"\n | \"Africa/Douala\"\n | \"Africa/El_Aaiun\"\n | \"Africa/Freetown\"\n | \"Africa/Gaborone\"\n | \"Africa/Harare\"\n | \"Africa/Johannesburg\"\n | \"Africa/Juba\"\n | \"Africa/Kampala\"\n | \"Africa/Khartoum\"\n | \"Africa/Kigali\"\n | \"Africa/Kinshasa\"\n | \"Africa/Lagos\"\n | \"Africa/Libreville\"\n | \"Africa/Lome\"\n | \"Africa/Luanda\"\n | \"Africa/Lubumbashi\"\n | \"Africa/Lusaka\"\n | \"Africa/Malabo\"\n | \"Africa/Maputo\"\n | \"Africa/Maseru\"\n | \"Africa/Mbabane\"\n | \"Africa/Mogadishu\"\n | \"Africa/Monrovia\"\n | \"Africa/Nairobi\"\n | \"Africa/Ndjamena\"\n | \"Africa/Niamey\"\n | \"Africa/Nouakchott\"\n | \"Africa/Ouagadougou\"\n | \"Africa/Porto-Novo\"\n | \"Africa/Sao_Tome\"\n | \"Africa/Tripoli\"\n | \"Africa/Tunis\"\n | \"Africa/Windhoek\"\n | \"America/Adak\"\n | \"America/Anchorage\"\n | \"America/Anguilla\"\n | \"America/Antigua\"\n | \"America/Araguaina\"\n | \"America/Argentina/Buenos_Aires\"\n | \"America/Argentina/Catamarca\"\n | \"America/Argentina/Cordoba\"\n | \"America/Argentina/Jujuy\"\n | \"America/Argentina/La_Rioja\"\n | \"America/Argentina/Mendoza\"\n | \"America/Argentina/Rio_Gallegos\"\n | \"America/Argentina/Salta\"\n | \"America/Argentina/San_Juan\"\n | \"America/Argentina/San_Luis\"\n | \"America/Argentina/Tucuman\"\n | \"America/Argentina/Ushuaia\"\n | \"America/Aruba\"\n | \"America/Asuncion\"\n | \"America/Atikokan\"\n | \"America/Bahia\"\n | \"America/Bahia_Banderas\"\n | \"America/Barbados\"\n | \"America/Belem\"\n | \"America/Belize\"\n | \"America/Blanc-Sablon\"\n | \"America/Boa_Vista\"\n | \"America/Bogota\"\n | \"America/Boise\"\n | \"America/Buenos_Aires\"\n | \"America/Cambridge_Bay\"\n | \"America/Campo_Grande\"\n | \"America/Cancun\"\n | \"America/Caracas\"\n | \"America/Catamarca\"\n | \"America/Cayenne\"\n | \"America/Cayman\"\n | \"America/Chicago\"\n | \"America/Chihuahua\"\n | \"America/Ciudad_Juarez\"\n | \"America/Coral_Harbour\"\n | \"America/Cordoba\"\n | \"America/Costa_Rica\"\n | \"America/Creston\"\n | \"America/Cuiaba\"\n | \"America/Curacao\"\n | \"America/Danmarkshavn\"\n | \"America/Dawson\"\n | \"America/Dawson_Creek\"\n | \"America/Denver\"\n | \"America/Detroit\"\n | \"America/Dominica\"\n | \"America/Edmonton\"\n | \"America/Eirunepe\"\n | \"America/El_Salvador\"\n | \"America/Fort_Nelson\"\n | \"America/Fortaleza\"\n | \"America/Glace_Bay\"\n | \"America/Godthab\"\n | \"America/Goose_Bay\"\n | \"America/Grand_Turk\"\n | \"America/Grenada\"\n | \"America/Guadeloupe\"\n | \"America/Guatemala\"\n | \"America/Guayaquil\"\n | \"America/Guyana\"\n | \"America/Halifax\"\n | \"America/Havana\"\n | \"America/Hermosillo\"\n | \"America/Indiana/Indianapolis\"\n | \"America/Indiana/Knox\"\n | \"America/Indiana/Marengo\"\n | \"America/Indiana/Petersburg\"\n | \"America/Indiana/Tell_City\"\n | \"America/Indiana/Vevay\"\n | \"America/Indiana/Vincennes\"\n | \"America/Indiana/Winamac\"\n | \"America/Indianapolis\"\n | \"America/Inuvik\"\n | \"America/Iqaluit\"\n | \"America/Jamaica\"\n | \"America/Jujuy\"\n | \"America/Juneau\"\n | \"America/Kentucky/Louisville\"\n | \"America/Kentucky/Monticello\"\n | \"America/Kralendijk\"\n | \"America/La_Paz\"\n | \"America/Lima\"\n | \"America/Los_Angeles\"\n | \"America/Louisville\"\n | \"America/Lower_Princes\"\n | \"America/Maceio\"\n | \"America/Managua\"\n | \"America/Manaus\"\n | \"America/Marigot\"\n | \"America/Martinique\"\n | \"America/Matamoros\"\n | \"America/Mazatlan\"\n | \"America/Mendoza\"\n | \"America/Menominee\"\n | \"America/Merida\"\n | \"America/Metlakatla\"\n | \"America/Mexico_City\"\n | \"America/Miquelon\"\n | \"America/Moncton\"\n | \"America/Monterrey\"\n | \"America/Montevideo\"\n | \"America/Montserrat\"\n | \"America/Nassau\"\n | \"America/New_York\"\n | \"America/Nipigon\"\n | \"America/Nome\"\n | \"America/Noronha\"\n | \"America/North_Dakota/Beulah\"\n | \"America/North_Dakota/Center\"\n | \"America/North_Dakota/New_Salem\"\n | \"America/Nuuk\"\n | \"America/Ojinaga\"\n | \"America/Panama\"\n | \"America/Pangnirtung\"\n | \"America/Paramaribo\"\n | \"America/Phoenix\"\n | \"America/Port-au-Prince\"\n | \"America/Port_of_Spain\"\n | \"America/Porto_Velho\"\n | \"America/Puerto_Rico\"\n | \"America/Punta_Arenas\"\n | \"America/Rainy_River\"\n | \"America/Rankin_Inlet\"\n | \"America/Recife\"\n | \"America/Regina\"\n | \"America/Resolute\"\n | \"America/Rio_Branco\"\n | \"America/Santa_Isabel\"\n | \"America/Santarem\"\n | \"America/Santiago\"\n | \"America/Santo_Domingo\"\n | \"America/Sao_Paulo\"\n | \"America/Scoresbysund\"\n | \"America/Sitka\"\n | \"America/St_Barthelemy\"\n | \"America/St_Johns\"\n | \"America/St_Kitts\"\n | \"America/St_Lucia\"\n | \"America/St_Thomas\"\n | \"America/St_Vincent\"\n | \"America/Swift_Current\"\n | \"America/Tegucigalpa\"\n | \"America/Thule\"\n | \"America/Thunder_Bay\"\n | \"America/Tijuana\"\n | \"America/Toronto\"\n | \"America/Tortola\"\n | \"America/Vancouver\"\n | \"America/Whitehorse\"\n | \"America/Winnipeg\"\n | \"America/Yakutat\"\n | \"America/Yellowknife\"\n | \"Antarctica/Casey\"\n | \"Antarctica/Davis\"\n | \"Antarctica/DumontDUrville\"\n | \"Antarctica/Macquarie\"\n | \"Antarctica/Mawson\"\n | \"Antarctica/McMurdo\"\n | \"Antarctica/Palmer\"\n | \"Antarctica/Rothera\"\n | \"Antarctica/Syowa\"\n | \"Antarctica/Troll\"\n | \"Antarctica/Vostok\"\n | \"Arctic/Longyearbyen\"\n | \"Asia/Aden\"\n | \"Asia/Almaty\"\n | \"Asia/Amman\"\n | \"Asia/Anadyr\"\n | \"Asia/Aqtau\"\n | \"Asia/Aqtobe\"\n | \"Asia/Ashgabat\"\n | \"Asia/Atyrau\"\n | \"Asia/Baghdad\"\n | \"Asia/Bahrain\"\n | \"Asia/Baku\"\n | \"Asia/Bangkok\"\n | \"Asia/Barnaul\"\n | \"Asia/Beirut\"\n | \"Asia/Bishkek\"\n | \"Asia/Brunei\"\n | \"Asia/Calcutta\"\n | \"Asia/Chita\"\n | \"Asia/Choibalsan\"\n | \"Asia/Chongqing\"\n | \"Asia/Colombo\"\n | \"Asia/Damascus\"\n | \"Asia/Dhaka\"\n | \"Asia/Dili\"\n | \"Asia/Dubai\"\n | \"Asia/Dushanbe\"\n | \"Asia/Famagusta\"\n | \"Asia/Gaza\"\n | \"Asia/Hebron\"\n | \"Asia/Ho_Chi_Minh\"\n | \"Asia/Hong_Kong\"\n | \"Asia/Hovd\"\n | \"Asia/Irkutsk\"\n | \"Asia/Jakarta\"\n | \"Asia/Jayapura\"\n | \"Asia/Jerusalem\"\n | \"Asia/Kabul\"\n | \"Asia/Kamchatka\"\n | \"Asia/Karachi\"\n | \"Asia/Kathmandu\"\n | \"Asia/Katmandu\"\n | \"Asia/Khandyga\"\n | \"Asia/Kolkata\"\n | \"Asia/Krasnoyarsk\"\n | \"Asia/Kuala_Lumpur\"\n | \"Asia/Kuching\"\n | \"Asia/Kuwait\"\n | \"Asia/Macau\"\n | \"Asia/Magadan\"\n | \"Asia/Makassar\"\n | \"Asia/Manila\"\n | \"Asia/Muscat\"\n | \"Asia/Nicosia\"\n | \"Asia/Novokuznetsk\"\n | \"Asia/Novosibirsk\"\n | \"Asia/Omsk\"\n | \"Asia/Oral\"\n | \"Asia/Phnom_Penh\"\n | \"Asia/Pontianak\"\n | \"Asia/Pyongyang\"\n | \"Asia/Qatar\"\n | \"Asia/Qostanay\"\n | \"Asia/Qyzylorda\"\n | \"Asia/Rangoon\"\n | \"Asia/Riyadh\"\n | \"Asia/Saigon\"\n | \"Asia/Sakhalin\"\n | \"Asia/Samarkand\"\n | \"Asia/Seoul\"\n | \"Asia/Shanghai\"\n | \"Asia/Singapore\"\n | \"Asia/Srednekolymsk\"\n | \"Asia/Taipei\"\n | \"Asia/Tashkent\"\n | \"Asia/Tbilisi\"\n | \"Asia/Tehran\"\n | \"Asia/Thimphu\"\n | \"Asia/Tokyo\"\n | \"Asia/Tomsk\"\n | \"Asia/Ulaanbaatar\"\n | \"Asia/Urumqi\"\n | \"Asia/Ust-Nera\"\n | \"Asia/Vientiane\"\n | \"Asia/Vladivostok\"\n | \"Asia/Yakutsk\"\n | \"Asia/Yangon\"\n | \"Asia/Yekaterinburg\"\n | \"Asia/Yerevan\"\n | \"Atlantic/Azores\"\n | \"Atlantic/Bermuda\"\n | \"Atlantic/Canary\"\n | \"Atlantic/Cape_Verde\"\n | \"Atlantic/Faeroe\"\n | \"Atlantic/Faroe\"\n | \"Atlantic/Madeira\"\n | \"Atlantic/Reykjavik\"\n | \"Atlantic/South_Georgia\"\n | \"Atlantic/St_Helena\"\n | \"Atlantic/Stanley\"\n | \"Australia/Adelaide\"\n | \"Australia/Brisbane\"\n | \"Australia/Broken_Hill\"\n | \"Australia/Canberra\"\n | \"Australia/Currie\"\n | \"Australia/Darwin\"\n | \"Australia/Eucla\"\n | \"Australia/Hobart\"\n | \"Australia/Lindeman\"\n | \"Australia/Lord_Howe\"\n | \"Australia/Melbourne\"\n | \"Australia/Perth\"\n | \"Australia/Sydney\"\n | \"Etc/GMT\"\n | \"Etc/GMT+1\"\n | \"Etc/GMT+10\"\n | \"Etc/GMT+11\"\n | \"Etc/GMT+12\"\n | \"Etc/GMT+2\"\n | \"Etc/GMT+3\"\n | \"Etc/GMT+4\"\n | \"Etc/GMT+5\"\n | \"Etc/GMT+6\"\n | \"Etc/GMT+7\"\n | \"Etc/GMT+8\"\n | \"Etc/GMT+9\"\n | \"Etc/GMT-1\"\n | \"Etc/GMT-10\"\n | \"Etc/GMT-11\"\n | \"Etc/GMT-12\"\n | \"Etc/GMT-13\"\n | \"Etc/GMT-14\"\n | \"Etc/GMT-2\"\n | \"Etc/GMT-3\"\n | \"Etc/GMT-4\"\n | \"Etc/GMT-5\"\n | \"Etc/GMT-6\"\n | \"Etc/GMT-7\"\n | \"Etc/GMT-8\"\n | \"Etc/GMT-9\"\n | \"Etc/UTC\"\n | \"Europe/Amsterdam\"\n | \"Europe/Andorra\"\n | \"Europe/Astrakhan\"\n | \"Europe/Athens\"\n | \"Europe/Belgrade\"\n | \"Europe/Berlin\"\n | \"Europe/Bratislava\"\n | \"Europe/Brussels\"\n | \"Europe/Bucharest\"\n | \"Europe/Budapest\"\n | \"Europe/Busingen\"\n | \"Europe/Chisinau\"\n | \"Europe/Copenhagen\"\n | \"Europe/Dublin\"\n | \"Europe/Gibraltar\"\n | \"Europe/Guernsey\"\n | \"Europe/Helsinki\"\n | \"Europe/Isle_of_Man\"\n | \"Europe/Istanbul\"\n | \"Europe/Jersey\"\n | \"Europe/Kaliningrad\"\n | \"Europe/Kiev\"\n | \"Europe/Kirov\"\n | \"Europe/Kyiv\"\n | \"Europe/Lisbon\"\n | \"Europe/Ljubljana\"\n | \"Europe/London\"\n | \"Europe/Luxembourg\"\n | \"Europe/Madrid\"\n | \"Europe/Malta\"\n | \"Europe/Mariehamn\"\n | \"Europe/Minsk\"\n | \"Europe/Monaco\"\n | \"Europe/Moscow\"\n | \"Europe/Oslo\"\n | \"Europe/Paris\"\n | \"Europe/Podgorica\"\n | \"Europe/Prague\"\n | \"Europe/Riga\"\n | \"Europe/Rome\"\n | \"Europe/Samara\"\n | \"Europe/San_Marino\"\n | \"Europe/Sarajevo\"\n | \"Europe/Saratov\"\n | \"Europe/Simferopol\"\n | \"Europe/Skopje\"\n | \"Europe/Sofia\"\n | \"Europe/Stockholm\"\n | \"Europe/Tallinn\"\n | \"Europe/Tirane\"\n | \"Europe/Ulyanovsk\"\n | \"Europe/Uzhgorod\"\n | \"Europe/Vaduz\"\n | \"Europe/Vatican\"\n | \"Europe/Vienna\"\n | \"Europe/Vilnius\"\n | \"Europe/Volgograd\"\n | \"Europe/Warsaw\"\n | \"Europe/Zagreb\"\n | \"Europe/Zaporozhye\"\n | \"Europe/Zurich\"\n | \"Indian/Antananarivo\"\n | \"Indian/Chagos\"\n | \"Indian/Christmas\"\n | \"Indian/Cocos\"\n | \"Indian/Comoro\"\n | \"Indian/Kerguelen\"\n | \"Indian/Mahe\"\n | \"Indian/Maldives\"\n | \"Indian/Mauritius\"\n | \"Indian/Mayotte\"\n | \"Indian/Reunion\"\n | \"Pacific/Apia\"\n | \"Pacific/Auckland\"\n | \"Pacific/Bougainville\"\n | \"Pacific/Chatham\"\n | \"Pacific/Chuuk\"\n | \"Pacific/Easter\"\n | \"Pacific/Efate\"\n | \"Pacific/Enderbury\"\n | \"Pacific/Fakaofo\"\n | \"Pacific/Fiji\"\n | \"Pacific/Funafuti\"\n | \"Pacific/Galapagos\"\n | \"Pacific/Gambier\"\n | \"Pacific/Guadalcanal\"\n | \"Pacific/Guam\"\n | \"Pacific/Honolulu\"\n | \"Pacific/Johnston\"\n | \"Pacific/Kanton\"\n | \"Pacific/Kiritimati\"\n | \"Pacific/Kosrae\"\n | \"Pacific/Kwajalein\"\n | \"Pacific/Majuro\"\n | \"Pacific/Marquesas\"\n | \"Pacific/Midway\"\n | \"Pacific/Nauru\"\n | \"Pacific/Niue\"\n | \"Pacific/Norfolk\"\n | \"Pacific/Noumea\"\n | \"Pacific/Pago_Pago\"\n | \"Pacific/Palau\"\n | \"Pacific/Pitcairn\"\n | \"Pacific/Pohnpei\"\n | \"Pacific/Ponape\"\n | \"Pacific/Port_Moresby\"\n | \"Pacific/Rarotonga\"\n | \"Pacific/Saipan\"\n | \"Pacific/Tahiti\"\n | \"Pacific/Tarawa\"\n | \"Pacific/Tongatapu\"\n | \"Pacific/Truk\"\n | \"Pacific/Wake\"\n | \"Pacific/Wallis\";\n",
25
+ "platforms/fortios/models.ts": "// Port counts per speed class, and how the platform spells a port. `{n}` is the position within the class; `{n+4}` shifts it, because the platform numbers every data port in one run. The out-of-band management ports are the `1g` class.\nexport const models = {\n \"FG-3700D\": {\n ports: { \"40g\": 4, \"10g\": 28, \"1g\": 2 },\n spelling: { \"40g\": \"port{n}\", \"10g\": \"port{n+4}\", \"1g\": \"mgmt{n}\" },\n },\n} as const;\n",
26
+ "platforms/junos/models.ts": "// Port counts per speed class, and how the platform spells a port. `{n}` is the position within the class; `{n-1}` and `{n+47}` shift it, because the platform counts every port of a switch from 0. The out-of-band management ports are the `1g` class: they route, and switch nothing.\nexport const models = {\n \"QFX5100-48S-6Q\": {\n ports: { \"40g\": 6, \"10g\": 48, \"1g\": 2 },\n spelling: { \"40g\": \"et-0/0/{n+47}\", \"10g\": \"xe-0/0/{n-1}\", \"1g\": \"em{n-1}\" },\n },\n} as const;\n",
27
+ "platforms/raisecom-ros/models.ts": "// Port counts per speed class, and how the platform spells a port. `{n}` is the position within the class. The out-of-band management port is not a switch port and is left out.\nexport const models = {\n \"RAX721-C-6C48\": {\n ports: { \"100g\": 6, \"25g\": 24, \"10g\": 24 },\n spelling: { \"100g\": \"hundredgige 1/1/{n}\", \"25g\": \"twenty-fivegige 1/2/{n}\", \"10g\": \"tengigabitethernet 1/3/{n}\" },\n },\n} as const;\n",
28
+ "platforms/routeros/models.ts": "// Port counts per speed class, and how the platform spells a port. `{n}` is the position within the class.\nexport const models = {\n // A QSFP28 cage is named by cage then lane; an unbroken port is lane 1.\n \"CCR2216-1G-12XS-2XQ\": {\n ports: { \"100g\": 2, \"25g\": 12, \"1g\": 1 },\n spelling: { \"100g\": \"qsfp28-{n}-1\", \"25g\": \"sfp28-{n}\", \"1g\": \"ether{n}\" },\n // Seen on a real device: a LAG with 12 active members crash-looped it, and the switch chip failed at the sixth distinct port range. It held 70 rules with two ranges, so the rule count alone is not the limit.\n limits: { lag_members: 8, switch_port_ranges: 5 },\n },\n} as const;\n",
29
+ "platforms/vrp/models.ts": "// Port counts per speed class, and how the platform spells a port. `{n}` is the position within the class.\nexport const models = {\n \"CE6855-48S6Q-HI\": { ports: { \"10g\": 48, \"40g\": 6 }, spelling: { \"10g\": \"10GE1/0/{n}\", \"40g\": \"40GE1/0/{n}\" } },\n} as const;\n"
30
+ };