@takodotid/circuit 0.36.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/chunks/{index-2r4bbhac.js → index-k38bby43.js} +5 -3
  2. package/dist/chunks/{server-gzsf5me5.js → server-ggertnce.js} +1 -1
  3. package/dist/cli/index.js +8 -2
  4. package/dist/types/core/address-space.d.ts +1 -1
  5. package/dist/types/core/address.d.ts +1 -1
  6. package/dist/types/core/cabling.d.ts +3 -3
  7. package/dist/types/core/cross-checks.d.ts +3 -3
  8. package/dist/types/core/define.d.ts +2 -2
  9. package/dist/types/core/secrets.d.ts +2 -2
  10. package/dist/types/core/topology.d.ts +2 -2
  11. package/dist/types/core/validator.d.ts +1 -1
  12. package/dist/types/helpers/bgp.d.ts +4 -4
  13. package/dist/types/helpers/checks.d.ts +1 -1
  14. package/dist/types/helpers/exchange.d.ts +3 -3
  15. package/dist/types/helpers/filters.d.ts +1 -1
  16. package/dist/types/helpers/index.d.ts +10 -10
  17. package/dist/types/helpers/route-server.d.ts +2 -2
  18. package/dist/types/index.d.ts +8 -6
  19. package/dist/types/integrations/librenms.d.ts +2 -2
  20. package/dist/types/integrations/netbox.d.ts +1 -1
  21. package/dist/types/integrations/peeringdb.d.ts +1 -1
  22. package/dist/types/integrations/prometheus.d.ts +2 -2
  23. package/dist/types/integrations/registries.d.ts +2 -2
  24. package/dist/types/platforms/bgp-wait.d.ts +1 -1
  25. package/dist/types/platforms/derive.d.ts +1 -1
  26. package/dist/types/platforms/fortios/firewall.d.ts +2 -2
  27. package/dist/types/platforms/fortios/index.d.ts +4 -4
  28. package/dist/types/platforms/fortios/names.d.ts +1 -1
  29. package/dist/types/platforms/fortios/plan.d.ts +1 -1
  30. package/dist/types/platforms/fortios/render.d.ts +1 -1
  31. package/dist/types/platforms/fortios/routing.d.ts +2 -2
  32. package/dist/types/platforms/index.d.ts +1 -1
  33. package/dist/types/platforms/junos/bgp.d.ts +2 -2
  34. package/dist/types/platforms/junos/index.d.ts +4 -4
  35. package/dist/types/platforms/junos/render.d.ts +1 -1
  36. package/dist/types/platforms/line-diff.d.ts +1 -1
  37. package/dist/types/platforms/platform.d.ts +5 -5
  38. package/dist/types/platforms/raisecom-ros/index.d.ts +4 -4
  39. package/dist/types/platforms/raisecom-ros/render.d.ts +1 -1
  40. package/dist/types/platforms/routeros/index.d.ts +4 -4
  41. package/dist/types/platforms/routeros/plan.d.ts +1 -1
  42. package/dist/types/platforms/routeros/render.d.ts +1 -1
  43. package/dist/types/platforms/vrp/index.d.ts +4 -4
  44. package/dist/types/platforms/vrp/render.d.ts +1 -1
  45. package/dist/types/schema/device.d.ts +6 -6
  46. package/dist/types/schema/firewall.d.ts +1 -1
  47. package/dist/types/schema/index.d.ts +8 -8
  48. package/dist/types/schema/interfaces.d.ts +5 -1
  49. package/dist/types/schema/policy.d.ts +1 -1
  50. package/dist/types/schema/routing.d.ts +1 -1
  51. package/dist/types/schema/system.d.ts +2 -2
  52. package/dist/types/transport/console.d.ts +1 -1
  53. package/dist/types/transport/ssh.d.ts +1 -1
  54. package/package.json +2 -2
  55. package/src/core/cabling.ts +4 -1
  56. package/src/core/cross-checks.ts +6 -1
  57. package/src/core/topology.ts +4 -3
  58. package/src/index.ts +3 -0
  59. package/src/schema/interfaces.ts +4 -0
  60. package/src/studio/data.ts +3 -0
  61. package/src/studio/public/tables.js +15 -1
@@ -160,7 +160,8 @@ class Topology {
160
160
  const farPort = far?.ports?.[link.port];
161
161
  const linksBack = farPort?.link?.device === device.name && farPort.link.port === portName;
162
162
  const to = far ? Topology.deviceId(far.name) : graph.node(`missing:${link.device}`, "outside", link.device, ["named by a link, and not in the network"]);
163
- const cableLine = `${here} to ${there}`;
163
+ const through = link.through ? `, through ${link.through}` : "";
164
+ const cableLine = `${here} to ${there}${through}`;
164
165
  const lag = port.lag;
165
166
  const farLag = farPort?.lag;
166
167
  const bundleKey = `${device.name} ${lag}|${link.device} ${farLag}`;
@@ -168,7 +169,7 @@ class Topology {
168
169
  if (bundle) {
169
170
  bundle.members++;
170
171
  bundle.cable.detail.push(cableLine);
171
- bundle.cable.label = `${lag} to ${farLag}, ${bundle.members} cables`;
172
+ bundle.cable.label = `${lag} to ${farLag}, ${bundle.members} cables${through}`;
172
173
  continue;
173
174
  }
174
175
  const isBundle = Boolean(lag && farLag && linksBack);
@@ -176,7 +177,7 @@ class Topology {
176
177
  layer: "l1",
177
178
  from: Topology.deviceId(device.name),
178
179
  to,
179
- label: isBundle ? `${lag} to ${farLag}` : `${portName} to ${link.port}`,
180
+ label: `${isBundle ? `${lag} to ${farLag}` : `${portName} to ${link.port}`}${through}`,
180
181
  detail: [cableLine],
181
182
  ...linksBack ? {} : { oneSided: true }
182
183
  });
@@ -950,6 +951,7 @@ class StudioModel {
950
951
  ...port.lag ? { lag: port.lag } : {},
951
952
  ...farPort?.lag ? { farLag: farPort.lag } : {},
952
953
  ...port.description ? { description: port.description } : {},
954
+ ...link.through ? { through: link.through } : {},
953
955
  vlans: vlansOf(device, portName),
954
956
  ...linksBack ? {} : { oneSided: true }
955
957
  });
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  StudioModel
3
- } from "./index-2r4bbhac.js";
3
+ } from "./index-k38bby43.js";
4
4
 
5
5
  // src/studio/server.ts
6
6
  import { execFile as execFile2, spawn } from "node:child_process";
package/dist/cli/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  Topology,
9
9
  AddressSpace,
10
10
  StudioModel
11
- } from "../chunks/index-2r4bbhac.js";
11
+ } from "../chunks/index-k38bby43.js";
12
12
  import {
13
13
  Address2
14
14
  } from "../chunks/index-n7w3tg5y.js";
@@ -3732,6 +3732,8 @@ class Cabling {
3732
3732
  }
3733
3733
  continue;
3734
3734
  }
3735
+ if (link.through)
3736
+ continue;
3735
3737
  const far = devices.find((candidate) => candidate.name === link.device);
3736
3738
  if (!far || !Cabling.runsOn(far, link.port))
3737
3739
  continue;
@@ -10694,6 +10696,10 @@ class CrossChecks {
10694
10696
  report(`${link.device} ${link.port} does not link back to it`);
10695
10697
  continue;
10696
10698
  }
10699
+ if (link.through !== farPort.link?.through) {
10700
+ const say = (through) => through ? `through ${through}` : "direct";
10701
+ report(`${say(link.through)} here, and ${say(farPort.link?.through)} on ${link.device} ${link.port}`);
10702
+ }
10697
10703
  const near = CrossChecks.carrierOf(device, portName, port);
10698
10704
  const away = CrossChecks.carrierOf(far, link.port, farPort);
10699
10705
  const inLag = near.name !== portName;
@@ -12280,7 +12286,7 @@ Renamed. Run circuit validate, then circuit apply ${to}: the device keeps its ol
12280
12286
  const port = Number(flags.port);
12281
12287
  if (!Number.isInteger(port) || port < 1 || port > 65535)
12282
12288
  Cli.fail("--port takes a number from 1 to 65535");
12283
- const { StudioServer } = await import("../chunks/server-gzsf5me5.js");
12289
+ const { StudioServer } = await import("../chunks/server-ggertnce.js");
12284
12290
  const host = flags.host;
12285
12291
  const allowHosts = (flags.allowHost ?? "").split(",").filter(Boolean);
12286
12292
  if (!StudioServer.isLoopback(host) && !allowHosts.length) {
@@ -1,4 +1,4 @@
1
- import type { Device } from "../schema";
1
+ import type { Device } from "../schema/index.js";
2
2
  /** One address or range in a segment, and what holds it. */
3
3
  export type Use = {
4
4
  /** An address, or `first-last` for a range. */
@@ -1,4 +1,4 @@
1
- import type { Family, IP, IPv4, Prefix } from "../schema";
1
+ import type { Family, IP, IPv4, Prefix } from "../schema/index.js";
2
2
  /** Address helpers, for checks of your own. */
3
3
  export declare class Address {
4
4
  /** IPv4 space another network cannot reach: this host, private, shared, loopback, link-local and documentation, after RFC 6890. */
@@ -1,5 +1,5 @@
1
- import type { Device } from "../schema";
2
- import type { Finding } from "./define";
1
+ import type { Device } from "../schema/index.js";
2
+ import type { Finding } from "./define.js";
3
3
  /** A device one of our ports sees at the far end of its cable, over LLDP. */
4
4
  export type Neighbor = {
5
5
  /** Our port, by Circuit's name, such as `10g-1`. */
@@ -28,7 +28,7 @@ export declare class Cabling {
28
28
  /**
29
29
  * Warnings where LLDP and `link` disagree, from each device's own view: a cable in another port than `link` names, a cable `link` names that LLDP does not see, and another of our devices on a port without `link`.
30
30
  *
31
- * Only ports where both ends run LLDP, by the config, are compared, and only on devices whose neighbors were read. LLDP can be off at the far end, so each is a warning.
31
+ * Only ports where both ends run LLDP, by the config, are compared, and only on devices whose neighbors were read. A link `through` a carrier is not compared. LLDP can be off at the far end, so each is a warning.
32
32
  */
33
33
  static compare(devices: readonly Device[], neighbors: ReadonlyMap<string, readonly Neighbor[]>): Finding[];
34
34
  /** A system name is the device's name, alone or with a domain after it. */
@@ -1,5 +1,5 @@
1
- import type { Device } from "../schema";
2
- import type { Finding } from "./define";
1
+ import type { Device } from "../schema/index.js";
2
+ import type { Finding } from "./define.js";
3
3
  export declare class CrossChecks {
4
4
  /** Every rule across devices. */
5
5
  static run(devices: readonly Device[]): Finding[];
@@ -15,7 +15,7 @@ export declare class CrossChecks {
15
15
  private static plainAddresses;
16
16
  /** Why the two ends of a cable cannot reach each other by address, or undefined when they can. */
17
17
  private static addressDisagreement;
18
- /** A link names a device and port that exist, the far end links back, and both ends carry the same VLANs the same way, with the same MTU, speed and addresses. */
18
+ /** A link names a device and port that exist, the far end links back through the same carrier, and both ends carry the same VLANs the same way, with the same MTU, speed and addresses. */
19
19
  private static checkLinks;
20
20
  /** No address is held by two devices in one VRF. Two loopbacks may hold one on purpose, for anycast, so that is a warning. */
21
21
  private static checkAddresses;
@@ -1,5 +1,5 @@
1
- import type { ModelName, PlatformName, PortName } from "../platforms/catalog";
2
- import type { Device, Secret } from "../schema";
1
+ import type { ModelName, PlatformName, PortName } from "../platforms/catalog.js";
2
+ import type { Device, Secret } from "../schema/index.js";
3
3
  /** A value resolved by name when a device is applied, from the environment or `.env` beside the config, and through 1Password when that value is an `op://` reference. Never the value itself. */
4
4
  export declare const secret: (name: string) => Secret;
5
5
  /** A value read from a file when a device is applied, such as a private key in PEM. The path is relative to the config file; keep the file out of git. */
@@ -1,5 +1,5 @@
1
- import type { Network } from "./define";
2
- import type { Console, Device, IP, Secret } from "../schema";
1
+ import type { Network } from "./define.js";
2
+ import type { Console, Device, IP, Secret } from "../schema/index.js";
3
3
  /** How a device is logged in to: its management address and SSH port, its connection user and that user's password, and its console. */
4
4
  export type DeviceLogin = {
5
5
  /** Absent for a device reached only through its console. */
@@ -1,5 +1,5 @@
1
- import type { Device, Routed } from "../schema";
2
- import type { Finding } from "./define";
1
+ import type { Device, Routed } from "../schema/index.js";
2
+ import type { Finding } from "./define.js";
3
3
  /** One view of the network. Every edge belongs to one. */
4
4
  export type Layer = "l1" | "l2" | "l3" | "bgp" | "ospf" | "tunnels";
5
5
  /** One of our devices, a segment several of them share, or something outside the network. */
@@ -1,4 +1,4 @@
1
- import type { Finding, Network } from "./define";
1
+ import type { Finding, Network } from "./define.js";
2
2
  export declare class Validator {
3
3
  static run(network: Network): Finding[];
4
4
  private static checkDevice;
@@ -1,5 +1,5 @@
1
- import type { Community } from "../core/define";
2
- import type { Device, PolicyRule, Prefix, StandardCommunity } from "../schema";
1
+ import type { Community } from "../core/define.js";
2
+ import type { Device, PolicyRule, Prefix, StandardCommunity } from "../schema/index.js";
3
3
  /** The prefix lengths a BGP import accepts. */
4
4
  export type SanityLengths = {
5
5
  /** Shortest and longest IPv4 prefix accepted. /8 to /24 when absent. */
@@ -63,8 +63,8 @@ export declare class Bgp {
63
63
  /** Each BGP neighbor's own policies and role, or its group's. */
64
64
  static neighborPolicies(device: Device): {
65
65
  name: string;
66
- address: import("..").IP;
67
- role: import("..").BgpRole | undefined;
66
+ address: import("../index.js").IP;
67
+ role: import("../index.js").BgpRole | undefined;
68
68
  import: string | undefined;
69
69
  export: string | undefined;
70
70
  }[];
@@ -1,4 +1,4 @@
1
- import type { Check } from "../core/define";
1
+ import type { Check } from "../core/define.js";
2
2
  /** Where traffic from outside, before anything filters it, may go. Every VLAN not listed as untrusted is trusted, so a VLAN added later is protected without being listed. */
3
3
  export type TrustBoundary = {
4
4
  /** VLANs carrying traffic straight from outside: transits, exchanges, an ISP handoff. */
@@ -1,6 +1,6 @@
1
- import type { Check } from "../core/define";
2
- import type { Ethertype, IP, Port, Prefix } from "../schema";
3
- import { type RouteServer } from "./route-server";
1
+ import type { Check } from "../core/define.js";
2
+ import type { Ethertype, IP, Port, Prefix } from "../schema/index.js";
3
+ import { type RouteServer } from "./route-server.js";
4
4
  /** A member of an internet exchange: a network with a port on the fabric and an address on the peering LAN. */
5
5
  export type ExchangeMember = {
6
6
  /** Lowercase, and never reused. */
@@ -1,4 +1,4 @@
1
- import type { AclRule, FilterRule, Prefix } from "../schema";
1
+ import type { AclRule, FilterRule, Prefix } from "../schema/index.js";
2
2
  /** What `Filters.antiAmplification` leaves alone. */
3
3
  export type AntiAmplificationOptions = {
4
4
  /** Prefixes inside the destinations whose UDP fragments pass: for a host that receives UDP too large for one packet, such as SIP or some VPNs. Every fragment after the first is dropped when absent. */
@@ -1,10 +1,10 @@
1
- export { Bgp } from "./bgp";
2
- export type { CommunityScheme, SanityLengths } from "./bgp";
3
- export { Checks } from "./checks";
4
- export type { TrustBoundary } from "./checks";
5
- export { Exchange } from "./exchange";
6
- export type { ExchangeMember, InternetExchange } from "./exchange";
7
- export { Filters } from "./filters";
8
- export { RouteServerConfig } from "./route-server";
9
- export type { RouteServer } from "./route-server";
10
- export type { AntiAmplificationOptions } from "./filters";
1
+ export { Bgp } from "./bgp.js";
2
+ export type { CommunityScheme, SanityLengths } from "./bgp.js";
3
+ export { Checks } from "./checks.js";
4
+ export type { TrustBoundary } from "./checks.js";
5
+ export { Exchange } from "./exchange.js";
6
+ export type { ExchangeMember, InternetExchange } from "./exchange.js";
7
+ export { Filters } from "./filters.js";
8
+ export { RouteServerConfig } from "./route-server.js";
9
+ export type { RouteServer } from "./route-server.js";
10
+ export type { AntiAmplificationOptions } from "./filters.js";
@@ -1,5 +1,5 @@
1
- import type { Family, IPv4, IPv6, Prefix } from "../schema";
2
- import type { ExchangeMember } from "./exchange";
1
+ import type { Family, IPv4, IPv6, Prefix } from "../schema/index.js";
2
+ import type { ExchangeMember } from "./exchange.js";
3
3
  /** A route server on an exchange's peering LAN: a server running BIRD 2 or OpenBGPD that passes every member's routes to every other member. */
4
4
  export type RouteServer = {
5
5
  /** Its name, such as `rs1`, written at the top of its config. */
@@ -1,8 +1,10 @@
1
- export { defineDevice, defineNetwork, secret, secretFile } from "./core/define";
2
- export { Utils } from "./core/utils";
3
- export type { Check, Community, Finding, NetBoxDevice, Network } from "./core/define";
4
- export type * from "./schema";
1
+ export { defineDevice, defineNetwork, secret, secretFile } from "./core/define.js";
2
+ export { Utils } from "./core/utils.js";
3
+ export type { Check, Community, Finding, NetBoxDevice, Network } from "./core/define.js";
4
+ export type * from "./schema/index.js";
5
+ /** The platforms, their models and each model's ports, for types of your own: `PortName<"junos", "QFX5100-48S-6Q">` is each port of that model, such as `"10g-1"` or `"40g-6"`. */
6
+ export type { ModelName, PlatformName, PortName } from "./platforms/catalog.js";
5
7
  /** Address helpers, for checks of your own. */
6
- export { Address } from "./core/address";
8
+ export { Address } from "./core/address.js";
7
9
  /** Secrets, for tools of your own: a secret's value, text with every secret hidden, and how Circuit logs in to a device. */
8
- export { Secrets } from "./core/secrets";
10
+ export { Secrets } from "./core/secrets.js";
@@ -1,5 +1,5 @@
1
- import type { Network } from "../core/define";
2
- import type { Device } from "../schema";
1
+ import type { Network } from "../core/define.js";
2
+ import type { Device } from "../schema/index.js";
3
3
  /** A device LibreNMS monitors whose settings differ from its config. */
4
4
  export type LibreNmsChange = {
5
5
  device: Device;
@@ -1,4 +1,4 @@
1
- import type { Network } from "../core/define";
1
+ import type { Network } from "../core/define.js";
2
2
  /** Each kind of object Circuit writes, in the order they are made: each refers only to kinds before it, or waits for the next pass. */
3
3
  export type NetBoxKind = "tag" | "site" | "manufacturer" | "platform" | "device-type" | "role" | "vrf" | "vlan" | "device" | "interface" | "prefix" | "ip-range" | "ip" | "cable";
4
4
  /** One object to make, change or delete. */
@@ -1,4 +1,4 @@
1
- import type { Network } from "../core/define";
1
+ import type { Network } from "../core/define.js";
2
2
  /** A change to send. `id` is set when the record exists and is updated in place. */
3
3
  export type PeeringDbChange = {
4
4
  summary: string;
@@ -1,5 +1,5 @@
1
- import type { Network } from "../core/define";
2
- import type { Device } from "../schema";
1
+ import type { Network } from "../core/define.js";
2
+ import type { Device } from "../schema/index.js";
3
3
  export type PrometheusTarget = {
4
4
  targets: string[];
5
5
  labels: Record<string, string>;
@@ -1,5 +1,5 @@
1
- import type { Family } from "../schema";
2
- import type { Registry } from "../schema/policy";
1
+ import type { Family } from "../schema/index.js";
2
+ import type { Registry } from "../schema/policy.js";
3
3
  export declare class Registries {
4
4
  /** The prefixes of one family a registry gives for a query. */
5
5
  static prefixes(name: Registry, query: string, family: Family): Promise<string[]>;
@@ -1,4 +1,4 @@
1
- import type { BgpSession } from "./platform";
1
+ import type { BgpSession } from "./platform.js";
2
2
  /** How long to wait, and how often to read again, both in seconds. */
3
3
  export type WaitLimits = {
4
4
  seconds: number;
@@ -1,4 +1,4 @@
1
- import type { Device } from "../schema";
1
+ import type { Device } from "../schema/index.js";
2
2
  /** A feature a platform renders once proved on one of its devices. */
3
3
  export type ProvedFeature = "bfd-timers" | "static-route-bfd" | "multihop-bfd" | "alarm-severity" | "mac-limit" | "ethertype-filter";
4
4
  export declare class Derive {
@@ -1,5 +1,5 @@
1
- import type { AclRule, Device, Family, FilterMatch, FilterRule, NatRule, Ports, Protocol } from "../../schema";
2
- import { FortiOsBlock } from "./tree";
1
+ import type { AclRule, Device, Family, FilterMatch, FilterRule, NatRule, Ports, Protocol } from "../../schema/index.js";
2
+ import { FortiOsBlock } from "./tree.js";
3
3
  export declare class FortiOsFirewall {
4
4
  /** IP protocol numbers, for a protocol the platform names only by number. */
5
5
  static readonly PROTOCOL_NUMBER: Record<Protocol, number>;
@@ -1,7 +1,7 @@
1
- import type { Neighbor } from "../../core/cabling";
2
- import type { Device, Family } from "../../schema";
3
- import { type ConsoleDialect } from "../../transport/console";
4
- import { type ApplyOptions, type OsSupport, type Plan, type ReadOptions, type ReadSession, Platform } from "../platform";
1
+ import type { Neighbor } from "../../core/cabling.js";
2
+ import type { Device, Family } from "../../schema/index.js";
3
+ import { type ConsoleDialect } from "../../transport/console.js";
4
+ import { type ApplyOptions, type OsSupport, type Plan, type ReadOptions, type ReadSession, Platform } from "../platform.js";
5
5
  /** What a session reads and writes: an SSH shell, or anything else that streams the CLI. */
6
6
  export type Channel = {
7
7
  write(data: string): unknown;
@@ -1,4 +1,4 @@
1
- import type { Device, Family } from "../../schema";
1
+ import type { Device, Family } from "../../schema/index.js";
2
2
  type Addressed = {
3
3
  addresses?: readonly (string | {
4
4
  address: string;
@@ -1,4 +1,4 @@
1
- import type { ApplyOptions, Plan } from "../platform";
1
+ import type { ApplyOptions, Plan } from "../platform.js";
2
2
  export declare class FortiOsPlan {
3
3
  /** Objects every FortiGate running 6.2 is shipped with, which policies and profiles of the factory name. */
4
4
  private static readonly FACTORY_ADDRESSES;
@@ -1,4 +1,4 @@
1
- import type { Device, Helper, Severity } from "../../schema";
1
+ import type { Device, Helper, Severity } from "../../schema/index.js";
2
2
  export type Section = (typeof FortiOsRender.SECTIONS)[number];
3
3
  export declare class FortiOsRender {
4
4
  /** Where the pre-login banner's text is kept. */
@@ -1,5 +1,5 @@
1
- import type { Device, Family, Neighbor, NeighborSettings, PolicyRule } from "../../schema";
2
- import { FortiOsBlock } from "./tree";
1
+ import type { Device, Family, Neighbor, NeighborSettings, PolicyRule } from "../../schema/index.js";
2
+ import { FortiOsBlock } from "./tree.js";
3
3
  type AnyRule = PolicyRule<string, string>;
4
4
  type AnyNeighbor = Neighbor<string, string>;
5
5
  /** The lengths a prefix list's rule takes within its prefix. */
@@ -1,4 +1,4 @@
1
- import type { Platform } from "./platform";
1
+ import type { Platform } from "./platform.js";
2
2
  /** Every platform the framework can render and apply, by name. */
3
3
  export declare class Platforms {
4
4
  private static readonly BY_NAME;
@@ -1,5 +1,5 @@
1
- import type { Device, Neighbor, NeighborSettings } from "../../schema";
2
- import { type JunosStatements } from "./render";
1
+ import type { Device, Neighbor, NeighborSettings } from "../../schema/index.js";
2
+ import { type JunosStatements } from "./render.js";
3
3
  type AnyNeighbor = Neighbor<string, string>;
4
4
  /** BGP sessions, the policies they use, prefix sets and RPKI servers, added to one device's statements. */
5
5
  export declare class JunosBgp {
@@ -1,7 +1,7 @@
1
- import type { Neighbor } from "../../core/cabling";
2
- import type { Device, Family } from "../../schema";
3
- import { type ConsoleDialect } from "../../transport/console";
4
- import { type ApplyOptions, type OsSupport, type Plan, type ReadOptions, type ReadSession, Platform } from "../platform";
1
+ import type { Neighbor } from "../../core/cabling.js";
2
+ import type { Device, Family } from "../../schema/index.js";
3
+ import { type ConsoleDialect } from "../../transport/console.js";
4
+ import { type ApplyOptions, type OsSupport, type Plan, type ReadOptions, type ReadSession, Platform } from "../platform.js";
5
5
  /** How a commit that could cut access is guarded: it rolls back after `minutes` unless `prove` shows access survived. */
6
6
  export type Guard = {
7
7
  minutes: number;
@@ -1,4 +1,4 @@
1
- import type { Device, Interface, Port, Vrrp } from "../../schema";
1
+ import type { Device, Interface, Port, Vrrp } from "../../schema/index.js";
2
2
  type AnyPort = Port<string, string, string, string>;
3
3
  type AnyInterface = Interface<string, string>;
4
4
  type LagInterface = Extract<AnyInterface, {
@@ -1,4 +1,4 @@
1
- import type { Plan } from "./platform";
1
+ import type { Plan } from "./platform.js";
2
2
  export type Dialect = {
3
3
  /** A line at the margin that opens a block, for platforms that do not indent a block's lines. */
4
4
  block: RegExp;
@@ -1,8 +1,8 @@
1
- import type { Neighbor } from "../core/cabling";
2
- import type { Device, Family } from "../schema";
3
- import type { ConsoleDialect } from "../transport/console";
4
- import type { WaitLimits } from "./bgp-wait";
5
- import type { PlatformName } from "./catalog";
1
+ import type { Neighbor } from "../core/cabling.js";
2
+ import type { Device, Family } from "../schema/index.js";
3
+ import type { ConsoleDialect } from "../transport/console.js";
4
+ import type { WaitLimits } from "./bgp-wait.js";
5
+ import type { PlatformName } from "./catalog.js";
6
6
  /** One part of a plan: what a reviewer reads, and what is sent. */
7
7
  export type Step = {
8
8
  title: string;
@@ -1,8 +1,8 @@
1
1
  import type { ClientChannel } from "ssh2";
2
- import type { Neighbor } from "../../core/cabling";
3
- import type { Device } from "../../schema";
4
- import { type ConsoleDialect } from "../../transport/console";
5
- import { type ApplyOptions, type OsSupport, type Plan, type ReadOptions, type ReadSession, Platform } from "../platform";
2
+ import type { Neighbor } from "../../core/cabling.js";
3
+ import type { Device } from "../../schema/index.js";
4
+ import { type ConsoleDialect } from "../../transport/console.js";
5
+ import { type ApplyOptions, type OsSupport, type Plan, type ReadOptions, type ReadSession, Platform } from "../platform.js";
6
6
  /** A session, and how to prove access in a fresh login: a new SSH session, or logging out and in again on the console. */
7
7
  export type Session = {
8
8
  channel: ClientChannel;
@@ -1,4 +1,4 @@
1
- import type { Device, SnmpUser } from "../../schema";
1
+ import type { Device, SnmpUser } from "../../schema/index.js";
2
2
  /** The configuration a Raisecom ROS device should run. */
3
3
  export declare class RaisecomRosRender {
4
4
  /** The access-list that guards every management service. */
@@ -1,7 +1,7 @@
1
- import type { Neighbor } from "../../core/cabling";
2
- import type { Device, Family } from "../../schema";
3
- import { type ConsoleDialect } from "../../transport/console";
4
- import { type ApplyOptions, type BgpLogLine, type BgpSession, type HealthCheck, type HealthOptions, type OsSupport, type Plan, type ReadOptions, type ReadSession, type SwitchRules, Platform } from "../platform";
1
+ import type { Neighbor } from "../../core/cabling.js";
2
+ import type { Device, Family } from "../../schema/index.js";
3
+ import { type ConsoleDialect } from "../../transport/console.js";
4
+ import { type ApplyOptions, type BgpLogLine, type BgpSession, type HealthCheck, type HealthOptions, type OsSupport, type Plan, type ReadOptions, type ReadSession, type SwitchRules, Platform } from "../platform.js";
5
5
  export declare class RouterOs extends Platform {
6
6
  /** How the RouterOS console asks for a login. The flags after the user turn off colours and terminal detection, without which the console redraws every line. */
7
7
  static readonly CONSOLE: ConsoleDialect;
@@ -1,4 +1,4 @@
1
- import type { ApplyOptions, Plan } from "../platform";
1
+ import type { ApplyOptions, Plan } from "../platform.js";
2
2
  export declare class RouterOsPlan {
3
3
  private readonly sendSecrets;
4
4
  /** A plan line the apply handles itself: upload a certificate and its key, and import them. */
@@ -1,4 +1,4 @@
1
- import type { AclRule, Device, Ethertype, Interface, Port } from "../../schema";
1
+ import type { AclRule, Device, Ethertype, Interface, Port } from "../../schema/index.js";
2
2
  /** A property value as a command takes it. `undefined` and `false` leave the property out. */
3
3
  export type Value = string | number | undefined | false;
4
4
  export type AnyPort = Port<string, string, string, string>;
@@ -1,7 +1,7 @@
1
- import type { Neighbor } from "../../core/cabling";
2
- import type { Device } from "../../schema";
3
- import { type ConsoleDialect } from "../../transport/console";
4
- import { type ApplyOptions, type OsSupport, type Plan, type ReadOptions, type ReadSession, Platform } from "../platform";
1
+ import type { Neighbor } from "../../core/cabling.js";
2
+ import type { Device } from "../../schema/index.js";
3
+ import { type ConsoleDialect } from "../../transport/console.js";
4
+ import { type ApplyOptions, type OsSupport, type Plan, type ReadOptions, type ReadSession, Platform } from "../platform.js";
5
5
  export declare class Vrp extends Platform {
6
6
  /** How the console asks for a login: the same question to change the password, always answered no, a system view left with `return`, and the line's banner after a logout or a refused password answered with Enter. Not yet proved on a device. */
7
7
  static readonly CONSOLE: ConsoleDialect;
@@ -1,4 +1,4 @@
1
- import type { Device } from "../../schema";
1
+ import type { Device } from "../../schema/index.js";
2
2
  /** Builds the configuration as blocks: a header line, its indented lines, then `#`. */
3
3
  export declare class VrpRender {
4
4
  private readonly device;
@@ -1,9 +1,9 @@
1
- import type { IP, Secret } from "./common";
2
- import type { AclRule, Firewall } from "./firewall";
3
- import type { Interface, Port, Vlan } from "./interfaces";
4
- import type { PrefixSet, RoutePolicy } from "./policy";
5
- import type { Routing } from "./routing";
6
- import type { Certificate, DhcpRelay, DhcpServer, FlowExport, Management, System, User } from "./system";
1
+ import type { IP, Secret } from "./common.js";
2
+ import type { AclRule, Firewall } from "./firewall.js";
3
+ import type { Interface, Port, Vlan } from "./interfaces.js";
4
+ import type { PrefixSet, RoutePolicy } from "./policy.js";
5
+ import type { Routing } from "./routing.js";
6
+ import type { Certificate, DhcpRelay, DhcpServer, FlowExport, Management, System, User } from "./system.js";
7
7
  /** How the framework reaches the device. */
8
8
  export type Connection<U extends string> = {
9
9
  /** Management address. Absent for a device reached only through its console, `console`. */
@@ -1,4 +1,4 @@
1
- import type { Family, IP, Prefix, Rate } from "./common";
1
+ import type { Family, IP, Prefix, Rate } from "./common.js";
2
2
  /** An IP protocol. `icmp` means ICMPv6 in an IPv6 rule. */
3
3
  export type Protocol = "tcp" | "udp" | "icmp" | "gre" | "esp" | "ah" | "ospf" | "vrrp";
4
4
  /** The state connection tracking assigns a packet. */
@@ -1,8 +1,8 @@
1
- export type * from "./common";
2
- export type * from "./device";
3
- export type * from "./firewall";
4
- export type * from "./interfaces";
5
- export type * from "./policy";
6
- export type * from "./routing";
7
- export type * from "./system";
8
- export type * from "./time-zones";
1
+ export type * from "./common.js";
2
+ export type * from "./device.js";
3
+ export type * from "./firewall.js";
4
+ export type * from "./interfaces.js";
5
+ export type * from "./policy.js";
6
+ export type * from "./routing.js";
7
+ export type * from "./system.js";
8
+ export type * from "./time-zones.js";
@@ -1,4 +1,4 @@
1
- import type { InterfaceAddress, IP, MacAddress, Prefix, Secret } from "./common";
1
+ import type { InterfaceAddress, IP, MacAddress, Prefix, Secret } from "./common.js";
2
2
  /** A VLAN. Its key in `vlans` is its name. */
3
3
  export type Vlan = {
4
4
  /** The VLAN's number on the wire, 1 to 4094. */
@@ -90,6 +90,10 @@ export type Link = {
90
90
  device: string;
91
91
  /** The port on that device. */
92
92
  port: string;
93
+ /**
94
+ * 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.
95
+ */
96
+ through?: string;
93
97
  };
94
98
  /** A physical port. Ports a device does not declare are shut down. */
95
99
  export type Port<V extends string, A extends string, L extends string, F extends string> = Routed<F> & Switched<V> & {
@@ -1,4 +1,4 @@
1
- import type { Family, IP, Prefix } from "./common";
1
+ import type { Family, IP, Prefix } from "./common.js";
2
2
  /** A standard BGP community, `asn:value`, for example `64500:100`. Each part is 0 to 65535. */
3
3
  export type StandardCommunity = `${number}:${number}`;
4
4
  /** A large BGP community, `asn:function:parameter`, for example `64500:1:6939`. Each part is 0 to 4294967295. */
@@ -1,4 +1,4 @@
1
- import type { Family, IP, Prefix, Secret } from "./common";
1
+ import type { Family, IP, Prefix, Secret } from "./common.js";
2
2
  /** A route the device has without learning it. */
3
3
  export type StaticRoute<I extends string, F extends string> = {
4
4
  /** Destination. */
@@ -1,5 +1,5 @@
1
- import type { IP, MacAddress, Prefix, Secret, Severity } from "./common";
2
- import type { TimeZone } from "./time-zones";
1
+ import type { IP, MacAddress, Prefix, Secret, Severity } from "./common.js";
2
+ import type { TimeZone } from "./time-zones.js";
3
3
  /** A software release track, as RouterOS names them. `long-term` changes least; `development` is the newest and least tested. */
4
4
  export type ReleaseChannel = "long-term" | "stable" | "testing" | "development";
5
5
  /** How bad an alarm is, most severe first, as ITU-T X.733 ranks them. */
@@ -1,5 +1,5 @@
1
1
  import { type ClientChannel } from "ssh2";
2
- import type { Console, Device } from "../schema";
2
+ import type { Console, Device } from "../schema/index.js";
3
3
  /** An open console: the stream to the device's CLI, and how to let it go. `close` waits for the console to go quiet, then lets the port go. */
4
4
  export type ConsoleSession = {
5
5
  channel: ClientChannel;
@@ -1,5 +1,5 @@
1
1
  import { Client, type ClientChannel } from "ssh2";
2
- import type { Device } from "../schema";
2
+ import type { Device } from "../schema/index.js";
3
3
  export type Login = {
4
4
  host: string;
5
5
  port?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takodotid/circuit",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "description": "Declarative, vendor-neutral network configuration: describe each device in TypeScript, and Circuit makes the device match. Alpha.",
5
5
  "keywords": [
6
6
  "network",
@@ -89,7 +89,7 @@
89
89
  "release": "bun scripts/release.ts",
90
90
  "prepare": "husky || true",
91
91
  "prepack": "bun run build",
92
- "check": "bun run typecheck && bun run test && bun run format:check && bun run docs:build && bun run build && node dist/cli/index.js validate --config templates/single-site/circuit.config.ts && node dist/cli/index.js validate --config templates/edge-router/circuit.config.ts && node dist/cli/index.js validate --config templates/colocation/circuit.config.ts",
92
+ "check": "bun run typecheck && bun run test && bun run format:check && bun run docs:build && bun run build && bunx tsc -p test/package && node dist/cli/index.js validate --config templates/single-site/circuit.config.ts && node dist/cli/index.js validate --config templates/edge-router/circuit.config.ts && node dist/cli/index.js validate --config templates/colocation/circuit.config.ts",
93
93
  "agents:setup": "npx -y skills experimental_install"
94
94
  },
95
95
  "dependencies": {
@@ -67,7 +67,7 @@ export class Cabling {
67
67
  /**
68
68
  * Warnings where LLDP and `link` disagree, from each device's own view: a cable in another port than `link` names, a cable `link` names that LLDP does not see, and another of our devices on a port without `link`.
69
69
  *
70
- * Only ports where both ends run LLDP, by the config, are compared, and only on devices whose neighbors were read. LLDP can be off at the far end, so each is a warning.
70
+ * Only ports where both ends run LLDP, by the config, are compared, and only on devices whose neighbors were read. A link `through` a carrier is not compared. LLDP can be off at the far end, so each is a warning.
71
71
  */
72
72
  static compare(devices: readonly Device[], neighbors: ReadonlyMap<string, readonly Neighbor[]>): Finding[] {
73
73
  const findings: Finding[] = [];
@@ -97,6 +97,9 @@ export class Cabling {
97
97
  continue;
98
98
  }
99
99
 
100
+ // Through a carrier, LLDP sees the carrier's switches, not the far end.
101
+ if (link.through) continue;
102
+
100
103
  const far = devices.find((candidate) => candidate.name === link.device);
101
104
  if (!far || !Cabling.runsOn(far, link.port)) continue;
102
105
 
@@ -97,7 +97,7 @@ export class CrossChecks {
97
97
  return undefined;
98
98
  }
99
99
 
100
- /** A link names a device and port that exist, the far end links back, and both ends carry the same VLANs the same way, with the same MTU, speed and addresses. */
100
+ /** A link names a device and port that exist, the far end links back through the same carrier, and both ends carry the same VLANs the same way, with the same MTU, speed and addresses. */
101
101
  private static checkLinks(devices: readonly Device[]): Finding[] {
102
102
  const findings: Finding[] = [];
103
103
  const byName = new Map(devices.map((device) => [device.name, device]));
@@ -132,6 +132,11 @@ export class CrossChecks {
132
132
  continue;
133
133
  }
134
134
 
135
+ if (link.through !== farPort.link?.through) {
136
+ const say = (through: string | undefined) => (through ? `through ${through}` : "direct");
137
+ report(`${say(link.through)} here, and ${say(farPort.link?.through)} on ${link.device} ${link.port}`);
138
+ }
139
+
135
140
  const near = CrossChecks.carrierOf(device, portName, port);
136
141
  const away = CrossChecks.carrierOf(far, link.port, farPort);
137
142
 
@@ -203,7 +203,8 @@ export class Topology {
203
203
  ? Topology.deviceId(far.name)
204
204
  : graph.node(`missing:${link.device}`, "outside", link.device, ["named by a link, and not in the network"]);
205
205
 
206
- const cableLine = `${here} to ${there}`;
206
+ const through = link.through ? `, through ${link.through}` : "";
207
+ const cableLine = `${here} to ${there}${through}`;
207
208
 
208
209
  // A LAG's member cables to one LAG on the far side are one edge.
209
210
  const lag = port.lag;
@@ -214,7 +215,7 @@ export class Topology {
214
215
  if (bundle) {
215
216
  bundle.members++;
216
217
  bundle.cable.detail.push(cableLine);
217
- bundle.cable.label = `${lag} to ${farLag}, ${bundle.members} cables`;
218
+ bundle.cable.label = `${lag} to ${farLag}, ${bundle.members} cables${through}`;
218
219
  continue;
219
220
  }
220
221
 
@@ -224,7 +225,7 @@ export class Topology {
224
225
  layer: "l1",
225
226
  from: Topology.deviceId(device.name),
226
227
  to,
227
- label: isBundle ? `${lag} to ${farLag}` : `${portName} to ${link.port}`,
228
+ label: `${isBundle ? `${lag} to ${farLag}` : `${portName} to ${link.port}`}${through}`,
228
229
  detail: [cableLine],
229
230
  ...(linksBack ? {} : { oneSided: true as const }),
230
231
  });
package/src/index.ts CHANGED
@@ -3,6 +3,9 @@ export { Utils } from "./core/utils";
3
3
  export type { Check, Community, Finding, NetBoxDevice, Network } from "./core/define";
4
4
  export type * from "./schema";
5
5
 
6
+ /** The platforms, their models and each model's ports, for types of your own: `PortName<"junos", "QFX5100-48S-6Q">` is each port of that model, such as `"10g-1"` or `"40g-6"`. */
7
+ export type { ModelName, PlatformName, PortName } from "./platforms/catalog";
8
+
6
9
  /** Address helpers, for checks of your own. */
7
10
  export { Address } from "./core/address";
8
11
 
@@ -92,6 +92,10 @@ export type Link = {
92
92
  device: string;
93
93
  /** The port on that device. */
94
94
  port: string;
95
+ /**
96
+ * 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.
97
+ */
98
+ through?: string;
95
99
  };
96
100
 
97
101
  /** A physical port. Ports a device does not declare are shut down. */
@@ -37,6 +37,8 @@ export type Cable = {
37
37
  lag?: string;
38
38
  farLag?: string;
39
39
  description?: string;
40
+ /** What is between the two ends, such as a carrier's local loop. */
41
+ through?: string;
40
42
  /** VLANs carried, as `name (id)`, from the port or its LAG. */
41
43
  vlans: string[];
42
44
  /** The far end does not name this cable back. */
@@ -227,6 +229,7 @@ export class StudioModel {
227
229
  ...(port.lag ? { lag: port.lag } : {}),
228
230
  ...(farPort?.lag ? { farLag: farPort.lag } : {}),
229
231
  ...(port.description ? { description: port.description } : {}),
232
+ ...(link.through ? { through: link.through } : {}),
230
233
  vlans: vlansOf(device, portName),
231
234
  ...(linksBack ? {} : { oneSided: true as const }),
232
235
  });
@@ -1021,19 +1021,33 @@ export class Tables {
1021
1021
 
1022
1022
  /** @param {HTMLElement} container */
1023
1023
  static #cablesView(container) {
1024
+ // A carrier column only when a cable runs through one.
1025
+ const carried = Tables.#data.cables.some((cable) => cable.through);
1026
+
1024
1027
  container.append(
1025
1028
  Tables.heading("Cables", "h2"),
1026
1029
  Tables.empty("Every cable a port's link names, once, from the end that names it first."),
1027
1030
  Tables.#data.cables.length
1028
1031
  ? Tables.of({
1029
1032
  csv: "cables",
1030
- head: ["Device", "Port", "Far device", "Far port", "LAG", "VLANs", "Description", ""].map((label) => ({ label })),
1033
+ head: [
1034
+ "Device",
1035
+ "Port",
1036
+ "Far device",
1037
+ "Far port",
1038
+ ...(carried ? ["Through"] : []),
1039
+ "LAG",
1040
+ "VLANs",
1041
+ "Description",
1042
+ "",
1043
+ ].map((label) => ({ label })),
1031
1044
  rows: Tables.#data.cables.map((cable) => ({
1032
1045
  cells: [
1033
1046
  Tables.deviceLink(cable.device),
1034
1047
  cable.port,
1035
1048
  Tables.deviceLink(cable.farDevice),
1036
1049
  cable.farPort,
1050
+ ...(carried ? [cable.through ?? ""] : []),
1037
1051
  [cable.lag, cable.farLag].filter(Boolean).join(" to "),
1038
1052
  cable.vlans.join(", "),
1039
1053
  cable.description ?? "",