@takodotid/circuit 0.38.0 → 0.39.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.
@@ -1,10 +1,68 @@
1
- import {
2
- Catalog
3
- } from "./index-6wb41q67.js";
4
1
  import {
5
2
  Address2
6
3
  } from "./index-n7w3tg5y.js";
7
4
 
5
+ // src/platforms/fortios/models.ts
6
+ var models = {
7
+ "FG-3700D": {
8
+ ports: { "40g": 4, "10g": 28, "1g": 2 },
9
+ spelling: { "40g": "port{n}", "10g": "port{n+4}", "1g": "mgmt{n}" }
10
+ }
11
+ };
12
+
13
+ // src/platforms/junos/models.ts
14
+ var models2 = {
15
+ "QFX5100-48S-6Q": {
16
+ ports: { "40g": 6, "10g": 48, "1g": 2 },
17
+ spelling: { "40g": "et-0/0/{n+47}", "10g": "xe-0/0/{n-1}", "1g": "em{n-1}" }
18
+ }
19
+ };
20
+
21
+ // src/platforms/raisecom-ros/models.ts
22
+ var models3 = {
23
+ "RAX721-C-6C48": {
24
+ ports: { "100g": 6, "25g": 24, "10g": 24 },
25
+ spelling: { "100g": "hundredgige 1/1/{n}", "25g": "twenty-fivegige 1/2/{n}", "10g": "tengigabitethernet 1/3/{n}" }
26
+ }
27
+ };
28
+
29
+ // src/platforms/routeros/models.ts
30
+ var models4 = {
31
+ "CCR2216-1G-12XS-2XQ": {
32
+ ports: { "100g": 2, "25g": 12, "1g": 1 },
33
+ spelling: { "100g": "qsfp28-{n}-1", "25g": "sfp28-{n}", "1g": "ether{n}" },
34
+ limits: { lag_members: 8, switch_port_ranges: 5 }
35
+ }
36
+ };
37
+
38
+ // src/platforms/vrp/models.ts
39
+ var models5 = {
40
+ "CE6855-48S6Q-HI": { ports: { "10g": 48, "40g": 6 }, spelling: { "10g": "10GE1/0/{n}", "40g": "40GE1/0/{n}" } }
41
+ };
42
+
43
+ // src/platforms/catalog.ts
44
+ class Catalog {
45
+ static MODELS = { routeros: models4, vrp: models5, "raisecom-ros": models3, junos: models2, fortios: models };
46
+ static ports(platform, model) {
47
+ const spec = Catalog.MODELS[platform]?.[model];
48
+ if (!spec)
49
+ throw new Error(`${platform} has no model ${model}`);
50
+ return Object.entries(spec.ports).flatMap(([speed, count]) => Array.from({ length: count }, (_, i) => `${speed}-${i + 1}`));
51
+ }
52
+ static limits(platform, model) {
53
+ const spec = Catalog.MODELS[platform]?.[model];
54
+ return spec?.limits ?? {};
55
+ }
56
+ static spell(platform, model, port) {
57
+ const spec = Catalog.MODELS[platform]?.[model];
58
+ const parts = /^(\d+[gm])-(\d+)$/.exec(port);
59
+ const template = parts && spec?.spelling[parts[1]];
60
+ if (!template)
61
+ throw new Error(`${model} has no port ${port}`);
62
+ return template.replace(/\{n([+-]\d+)?\}/, (_, shift) => String(Number(parts[2]) + Number(shift ?? 0)));
63
+ }
64
+ }
65
+
8
66
  // src/core/topology.ts
9
67
  class Builder {
10
68
  nodes = new Map;
@@ -918,4 +976,4 @@ class StudioModel {
918
976
  }
919
977
  }
920
978
 
921
- export { Topology, AddressSpace, StudioModel };
979
+ export { Catalog, Topology, AddressSpace, StudioModel };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  StudioModel
3
- } from "./index-yzw86rc8.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
@@ -4,13 +4,11 @@ import {
4
4
  Secrets2
5
5
  } from "../chunks/index-20kcjxjg.js";
6
6
  import {
7
+ Catalog,
7
8
  Topology,
8
9
  AddressSpace,
9
10
  StudioModel
10
- } from "../chunks/index-yzw86rc8.js";
11
- import {
12
- Catalog
13
- } from "../chunks/index-6wb41q67.js";
11
+ } from "../chunks/index-k38bby43.js";
14
12
  import {
15
13
  Address2
16
14
  } from "../chunks/index-n7w3tg5y.js";
@@ -3750,6 +3748,28 @@ class Cabling {
3750
3748
  }
3751
3749
  return findings;
3752
3750
  }
3751
+ static describeLinks(devices) {
3752
+ const byName = new Map(devices.map((device) => [device.name, device]));
3753
+ return devices.map((device) => {
3754
+ const ports = Object.entries(device.ports ?? {});
3755
+ if (!ports.some(([, port]) => port?.link && port.description === undefined))
3756
+ return device;
3757
+ const described = ports.map(([name, port]) => {
3758
+ const far = port?.link ? byName.get(port.link.device) : undefined;
3759
+ if (!port?.link || port.description !== undefined || !far)
3760
+ return [name, port];
3761
+ return [name, { ...port, description: `${far.name} ${Cabling.labelOf(far, port.link.port)}` }];
3762
+ });
3763
+ return { ...device, ports: Object.fromEntries(described) };
3764
+ });
3765
+ }
3766
+ static labelOf(device, port) {
3767
+ try {
3768
+ return Catalog.spell(device.platform, device.model, port);
3769
+ } catch {
3770
+ return port;
3771
+ }
3772
+ }
3753
3773
  static isDevice(system, name) {
3754
3774
  return system === name || system.startsWith(`${name}.`);
3755
3775
  }
@@ -11569,10 +11589,14 @@ class Cli {
11569
11589
  return new Cli(configPath, network, options);
11570
11590
  }
11571
11591
  static async load(path) {
11572
- if (process.versions.bun)
11573
- return (await import(path)).default;
11574
- const { createJiti } = await import("jiti");
11575
- return createJiti(import.meta.url).import(path, { default: true });
11592
+ let network;
11593
+ if (process.versions.bun) {
11594
+ network = (await import(path)).default;
11595
+ } else {
11596
+ const { createJiti } = await import("jiti");
11597
+ network = await createJiti(import.meta.url).import(path, { default: true });
11598
+ }
11599
+ return { ...network, devices: Cabling.describeLinks(network.devices) };
11576
11600
  }
11577
11601
  static fail(message) {
11578
11602
  console.error(message);
@@ -12377,7 +12401,7 @@ Renamed. Run circuit validate, then circuit apply ${to}: the device keeps its ol
12377
12401
  const port = Number(flags.port);
12378
12402
  if (!Number.isInteger(port) || port < 1 || port > 65535)
12379
12403
  Cli.fail("--port takes a number from 1 to 65535");
12380
- const { StudioServer } = await import("../chunks/server-cxr9ynxc.js");
12404
+ const { StudioServer } = await import("../chunks/server-ggertnce.js");
12381
12405
  const host = flags.host;
12382
12406
  const allowHosts = (flags.allowHost ?? "").split(",").filter(Boolean);
12383
12407
  if (!StudioServer.isLoopback(host) && !allowHosts.length) {
@@ -1,6 +1,3 @@
1
- import {
2
- Catalog
3
- } from "../chunks/index-6wb41q67.js";
4
1
  import {
5
2
  Address2
6
3
  } from "../chunks/index-n7w3tg5y.js";
@@ -235,8 +232,8 @@ class Bgp {
235
232
  class Cables {
236
233
  static link(spec) {
237
234
  return {
238
- a: { [spec.a.port]: Cables.end(spec, spec.b, spec.b.port) },
239
- b: { [spec.b.port]: Cables.end(spec, spec.a, spec.a.port) }
235
+ a: { [spec.a.port]: Cables.end(spec, spec.b.device, spec.b.port) },
236
+ b: { [spec.b.port]: Cables.end(spec, spec.a.device, spec.a.port) }
240
237
  };
241
238
  }
242
239
  static lag(spec) {
@@ -248,18 +245,17 @@ class Cables {
248
245
  throw new Error(`Cables.lag: ${spec.b.device} ${far} is cabled to both ${other} and ${near} on ${spec.a.device}`);
249
246
  taken.set(far, near);
250
247
  }
251
- const a = Object.fromEntries(pairs.map(([near, far]) => [near, { ...Cables.end(spec, spec.b, far), lag: spec.a.lag }]));
252
- const b = Object.fromEntries(pairs.map(([near, far]) => [far, { ...Cables.end(spec, spec.a, near), lag: spec.b.lag }]));
248
+ const a = Object.fromEntries(pairs.map(([near, far]) => [near, { ...Cables.end(spec, spec.b.device, far), lag: spec.a.lag }]));
249
+ const b = Object.fromEntries(pairs.map(([near, far]) => [far, { ...Cables.end(spec, spec.a.device, near), lag: spec.b.lag }]));
253
250
  return {
254
251
  a,
255
252
  b
256
253
  };
257
254
  }
258
- static end(shared, far, port) {
259
- const farEnd = { device: far.device, port, label: Catalog.spell(far.platform, far.model, port) };
255
+ static end(shared, device, port) {
260
256
  return {
261
- description: shared.describe ? shared.describe(farEnd) : `${farEnd.device} ${farEnd.label}`,
262
- link: { device: far.device, port, ...shared.through ? { through: shared.through } : {} }
257
+ link: { device, port, ...shared.through ? { through: shared.through } : {} },
258
+ ...shared.describe ? { description: shared.describe({ device, port }) } : {}
263
259
  };
264
260
  }
265
261
  }
@@ -668,21 +664,61 @@ class Filters {
668
664
  }
669
665
  }
670
666
  // src/helpers/upstreams.ts
667
+ var DEFAULTS = {
668
+ vlan: "{key}",
669
+ native: "{key}-native",
670
+ interface: "{key}",
671
+ import: "{KEY}-IN",
672
+ export: "{KEY}-OUT",
673
+ group: "{key}"
674
+ };
675
+
671
676
  class Upstreams {
672
- static define(catalog) {
677
+ static define(catalog, options = {}) {
678
+ const names = options.names ?? {};
673
679
  const all = Object.entries(catalog);
674
680
  const nameOf = (key, upstream) => upstream.name ?? key;
675
- const policy = (key) => key.toUpperCase();
676
681
  const grouped = (upstream) => upstream.role === "rs" || Boolean(upstream.multihop);
682
+ const named = (field, key, upstream) => {
683
+ const given = names[field];
684
+ const pattern = given === undefined || typeof given === "string" ? given ?? DEFAULTS[field] : (upstream.tunnel && "tunnel" in given ? given.tunnel : given[upstream.role]) ?? DEFAULTS[field];
685
+ return pattern.replaceAll("{key}", key).replaceAll("{KEY}", key.toUpperCase()).replaceAll("{number}", () => {
686
+ const number = upstream.handoff?.number ?? upstream.tunnel?.number;
687
+ if (number === undefined)
688
+ throw new Error(`Upstreams: ${key} needs a number for ${field} name ${pattern}`);
689
+ return String(number);
690
+ });
691
+ };
692
+ for (const field of Object.keys(DEFAULTS)) {
693
+ const taken = new Map;
694
+ for (const [key, upstream] of all) {
695
+ if (field === "native" && upstream.handoff?.native === undefined)
696
+ continue;
697
+ if (field === "import" && !upstream.import)
698
+ continue;
699
+ if (field === "group" && !grouped(upstream))
700
+ continue;
701
+ if (field === "vlan" && !upstream.handoff || field === "interface" && !upstream.handoff && !upstream.tunnel)
702
+ continue;
703
+ const name = named(field, key, upstream);
704
+ const other = taken.get(name);
705
+ if (other)
706
+ throw new Error(`Upstreams: ${other} and ${key} both have the ${field} name ${name}`);
707
+ taken.set(name, key);
708
+ }
709
+ }
710
+ const vlan = (key, upstream) => named("vlan", key, upstream);
711
+ const native = (key, upstream) => named("native", key, upstream);
712
+ const policy = (direction, key, upstream) => named(direction, key, upstream);
677
713
  const vlans = (device) => {
678
714
  const result = {};
679
715
  for (const [key, upstream] of all) {
680
716
  const handoff = upstream.handoff;
681
717
  if (!handoff || device !== undefined && handoff.port?.device !== device)
682
718
  continue;
683
- result[key] = { id: handoff.vlan, description: nameOf(key, upstream) };
719
+ result[vlan(key, upstream)] = { id: handoff.vlan, description: nameOf(key, upstream) };
684
720
  if (device !== undefined && handoff.native !== undefined) {
685
- result[`${key}-native`] = {
721
+ result[native(key, upstream)] = {
686
722
  id: handoff.native,
687
723
  description: `Native toward ${nameOf(key, upstream)}, nothing else in it`
688
724
  };
@@ -690,7 +726,7 @@ class Upstreams {
690
726
  }
691
727
  return result;
692
728
  };
693
- const vlansOn = (device) => all.filter(([, upstream]) => upstream.handoff?.port?.device === device).map(([key]) => key);
729
+ const vlansOn = (device) => all.filter(([, upstream]) => upstream.handoff?.port?.device === device).map(([key, upstream]) => vlan(key, upstream));
694
730
  const ports = (device) => {
695
731
  const result = {};
696
732
  for (const [key, upstream] of all) {
@@ -698,8 +734,8 @@ class Upstreams {
698
734
  if (!handoff?.port || handoff.port.device !== device)
699
735
  continue;
700
736
  result[handoff.port.port] = {
701
- description: nameOf(key, upstream),
702
- ...handoff.native !== undefined ? { trunk_vlans: [key], native_vlan: `${key}-native` } : { access_vlan: key },
737
+ description: names.port ? names.port(key, upstream) : nameOf(key, upstream),
738
+ ...handoff.native !== undefined ? { trunk_vlans: [vlan(key, upstream)], native_vlan: native(key, upstream) } : { access_vlan: vlan(key, upstream) },
703
739
  lldp: false,
704
740
  stp: false,
705
741
  ...handoff.port.storm_control ? { storm_control: handoff.port.storm_control } : {}
@@ -711,27 +747,29 @@ class Upstreams {
711
747
  const result = {};
712
748
  for (const [key, upstream] of all) {
713
749
  const description = nameOf(key, upstream);
750
+ const name = named("interface", key, upstream);
714
751
  if (upstream.tunnel) {
715
752
  const { local, remote, addresses, mtu } = upstream.tunnel;
716
- result[key] = { type: "gre", description, local, remote, addresses, ...mtu ? { mtu } : {} };
753
+ result[name] = { type: "gre", description, local, remote, addresses, ...mtu ? { mtu } : {} };
717
754
  } else if (upstream.handoff) {
718
755
  const { addresses, exchange } = upstream.handoff;
719
- result[key] = { type: "vlan", vlan: key, description, addresses, ...exchange ? { exchange } : {} };
756
+ result[name] = { type: "vlan", vlan: vlan(key, upstream), description, addresses, ...exchange ? { exchange } : {} };
720
757
  }
721
758
  }
722
759
  return result;
723
760
  };
761
+ const uplinks = () => all.filter(([, upstream]) => upstream.handoff).map(([key, upstream]) => named("interface", key, upstream));
724
762
  const settings = (key, upstream) => ({
725
763
  remote_as: upstream.asn,
726
764
  description: nameOf(key, upstream),
727
765
  role: upstream.role,
728
- ...upstream.import ? { import: `${policy(key)}-IN` } : {},
729
- export: `${policy(key)}-OUT`
766
+ ...upstream.import ? { import: policy("import", key, upstream) } : {},
767
+ export: policy("export", key, upstream)
730
768
  });
731
769
  const groups = () => Object.fromEntries(all.filter(([, upstream]) => grouped(upstream)).map(([key, upstream]) => {
732
770
  const multihop = upstream.multihop;
733
771
  return [
734
- key,
772
+ named("group", key, upstream),
735
773
  {
736
774
  ...settings(key, upstream),
737
775
  ...multihop ? {
@@ -749,7 +787,7 @@ class Upstreams {
749
787
  name,
750
788
  {
751
789
  ...session,
752
- ...grouped(upstream) ? { group: key } : settings(key, upstream),
790
+ ...grouped(upstream) ? { group: named("group", key, upstream) } : settings(key, upstream),
753
791
  ...limit ? { max_prefixes: limit } : {}
754
792
  }
755
793
  ];
@@ -757,7 +795,7 @@ class Upstreams {
757
795
  const policies = (options = {}) => {
758
796
  const result = {};
759
797
  for (const [key, upstream] of all) {
760
- result[`${policy(key)}-OUT`] = [
798
+ result[policy("export", key, upstream)] = [
761
799
  ...options.actions?.(key, upstream) ?? [],
762
800
  ...upstream.export ?? [],
763
801
  ...Bgp.announce(upstream.announce)
@@ -766,7 +804,7 @@ class Upstreams {
766
804
  if (!taken)
767
805
  continue;
768
806
  const tag = options.tag?.(key, upstream);
769
- result[`${policy(key)}-IN`] = [
807
+ result[policy("import", key, upstream)] = [
770
808
  ...taken.default ? Bgp.defaultOnly({ ...taken.default, ...options.sources ? { sources: options.sources } : {} }) : [],
771
809
  ...taken.learn ? Bgp.learn({
772
810
  ...taken.learn.before ? { before: taken.learn.before } : {},
@@ -779,9 +817,12 @@ class Upstreams {
779
817
  }
780
818
  return result;
781
819
  };
782
- const untrusted = () => [...all.filter(([, upstream]) => upstream.handoff).map(([key]) => key), ...natives()];
783
- const natives = () => all.filter(([, upstream]) => upstream.handoff?.native !== undefined).map(([key]) => `${key}-native`);
784
- const privateDrops = () => all.filter(([, upstream]) => upstream.handoff && upstream.drop_private).map(([key]) => key);
820
+ const untrusted = () => [
821
+ ...all.filter(([, upstream]) => upstream.handoff).map(([key, upstream]) => vlan(key, upstream)),
822
+ ...natives()
823
+ ];
824
+ const natives = () => all.filter(([, upstream]) => upstream.handoff?.native !== undefined).map(([key, upstream]) => native(key, upstream));
825
+ const privateDrops = () => all.filter(([, upstream]) => upstream.handoff && upstream.drop_private).map(([key, upstream]) => vlan(key, upstream));
785
826
  const tunnelRules = () => all.flatMap(([key, upstream]) => upstream.tunnel ? [
786
827
  {
787
828
  description: `GRE from ${nameOf(key, upstream)}`,
@@ -798,6 +839,7 @@ class Upstreams {
798
839
  vlansOn,
799
840
  ports,
800
841
  interfaces,
842
+ uplinks,
801
843
  groups,
802
844
  neighbors,
803
845
  policies,
@@ -31,6 +31,12 @@ export declare class Cabling {
31
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
+ /**
35
+ * Each linked port without a description gets one from its far end: the far device and the far port as printed on it, such as `firewall-1 port6`. The far device's own file has its platform and model, so the label is never written twice. A port whose far device is not in the network keeps none.
36
+ */
37
+ static describeLinks(devices: readonly Device[]): Device[];
38
+ /** A port as printed on the device, or Circuit's name for it when the model has no spelling for it. */
39
+ private static labelOf;
34
40
  /** A system name is the device's name, alone or with a domain after it. */
35
41
  private static isDevice;
36
42
  private static deviceOf;
@@ -1,32 +1,24 @@
1
- import { type ModelName, type PlatformName, type PortName } from "../platforms/catalog.js";
2
- /** One end of a cable: the device, and its platform and model, which name its ports. */
3
- export type CableEnd<P extends PlatformName, M extends ModelName<P>> = {
4
- device: string;
5
- platform: P;
6
- model: M;
7
- };
8
- /** The far end of a port, for its description: the device, the port by Circuit's name, and by the name printed on the device. */
1
+ /** The far end of a port, for a description of your own: the device and its port by Circuit's name. */
9
2
  export type FarEnd = {
10
3
  device: string;
11
4
  port: string;
12
- label: string;
13
5
  };
14
6
  /** What every cable of one entry shares. */
15
7
  type Shared = {
16
8
  /** What is between the two ends, such as a carrier's local loop. Both ends get it on their `link`. */
17
9
  through?: string;
18
- /** A port's description from its far end. The far device and the far port's label when absent, such as `firewall-1 port6`. */
10
+ /** A port's description from its far end. When absent, Circuit writes the far device and the far port as printed on it, such as `firewall-1 port6`, from the far device's own file. */
19
11
  describe?: (far: FarEnd) => string;
20
12
  };
21
- /** A port a cable builds: what it is, the LAG it is in, and the other end. */
13
+ /** A port a cable builds: the other end, the LAG it is in, and a description when `describe` gives one. */
22
14
  type CablePort<L extends string> = {
23
- description: string;
24
15
  link: {
25
16
  device: string;
26
17
  port: string;
27
18
  through?: string;
28
19
  };
29
20
  lag?: L;
21
+ description?: string;
30
22
  };
31
23
  /** Ports for both ends of the cables between two of our devices, each cable written once. */
32
24
  export declare class Cables {
@@ -34,15 +26,17 @@ export declare class Cables {
34
26
  * One cable, both ends. Spread each end into its device's `ports`:
35
27
  *
36
28
  * ```ts
37
- * const uplink = Cables.link({ a: { device: "router-1", platform: "routeros", model: "CCR2216-1G-12XS-2XQ", port: "25g-1" }, b: { ... } });
29
+ * const uplink = Cables.link({ a: { device: "router-1", port: "25g-1" }, b: { device: "switch-1", port: "10g-48" } });
38
30
  * // router-1: ports: { ...uplink.a }
39
31
  * ```
40
32
  */
41
- static link<const PA extends PlatformName, const MA extends ModelName<PA>, const NA extends PortName<PA, MA>, const PB extends PlatformName, const MB extends ModelName<PB>, const NB extends PortName<PB, MB>>(spec: Shared & {
42
- a: CableEnd<PA, MA> & {
33
+ static link<const NA extends string, const NB extends string>(spec: Shared & {
34
+ a: {
35
+ device: string;
43
36
  port: NA;
44
37
  };
45
- b: CableEnd<PB, MB> & {
38
+ b: {
39
+ device: string;
46
40
  port: NB;
47
41
  };
48
42
  }): {
@@ -52,17 +46,19 @@ export declare class Cables {
52
46
  /**
53
47
  * The cables of a LAG, both ends, from one list read from end A: `{ "25g-1": "10g-2" }` is a cable from A's 25g-1 to B's 10g-2. Each port is a member of its end's LAG. Spread each end into its device's `ports`; the LAG itself stays on each device.
54
48
  */
55
- static lag<const PA extends PlatformName, const MA extends ModelName<PA>, const LA extends string, const PB extends PlatformName, const MB extends ModelName<PB>, const LB extends string, const C extends Partial<Record<PortName<PA, MA>, PortName<PB, MB>>>>(spec: Shared & {
56
- a: CableEnd<PA, MA> & {
49
+ static lag<const LA extends string, const LB extends string, const C extends Readonly<Record<string, string>>>(spec: Shared & {
50
+ a: {
51
+ device: string;
57
52
  lag: LA;
58
53
  };
59
- b: CableEnd<PB, MB> & {
54
+ b: {
55
+ device: string;
60
56
  lag: LB;
61
57
  };
62
58
  cables: C;
63
59
  }): {
64
60
  a: Record<keyof C & string, CablePort<LA>>;
65
- b: Record<NonNullable<C[keyof C]> & string, CablePort<LB>>;
61
+ b: Record<C[keyof C], CablePort<LB>>;
66
62
  };
67
63
  /** A port facing `port` on the far device. */
68
64
  private static end;
@@ -1,7 +1,7 @@
1
1
  export { Bgp } from "./bgp.js";
2
2
  export type { Announced, CommunityScheme, DefaultOnlyOptions, LearnOptions, SanityLengths } from "./bgp.js";
3
3
  export { Cables } from "./cables.js";
4
- export type { CableEnd, FarEnd } from "./cables.js";
4
+ export type { FarEnd } from "./cables.js";
5
5
  export { Checks } from "./checks.js";
6
6
  export type { TrustBoundary } from "./checks.js";
7
7
  export { Exchange } from "./exchange.js";
@@ -13,6 +13,8 @@ export type Upstream = {
13
13
  vlan: number;
14
14
  /** It sends its VLAN tagged: its port gets a native VLAN of its own with this ID, which nothing else is in. Untagged when absent. */
15
15
  native?: number;
16
+ /** Its number in names with `{number}`, such as `core-ipt01-transita`. Written here, so removing another upstream renumbers nothing. */
17
+ number?: number;
16
18
  /** Our addresses on it. `{ address, peer }` for a point-to-point address whose far end is outside its own prefix. */
17
19
  addresses: readonly (InterfaceAddress | {
18
20
  address: InterfaceAddress;
@@ -27,12 +29,13 @@ export type Upstream = {
27
29
  /** An exchange's LAN, for `circuit peeringdb`. */
28
30
  exchange?: ExchangeLan;
29
31
  };
30
- /** A GRE tunnel to it. */
32
+ /** A GRE tunnel to it. `number` is its number in names with `{number}`. */
31
33
  tunnel?: {
32
34
  local: IP;
33
35
  remote: IP;
34
36
  addresses: readonly InterfaceAddress[];
35
37
  mtu?: number;
38
+ number?: number;
36
39
  };
37
40
  /** Our BGP sessions with it, by name: its address, and ours when it is not the interface's. */
38
41
  sessions: Readonly<Record<string, {
@@ -72,6 +75,36 @@ export type Upstream = {
72
75
  };
73
76
  /** Upstreams by key. The key names everything built from it. */
74
77
  export type UpstreamCatalog = Readonly<Record<string, Upstream>>;
78
+ /** A name made from an upstream: `{key}` is its key, `{KEY}` the key in capitals, `{number}` its `handoff.number` or `tunnel.number`. */
79
+ type Pattern = string;
80
+ /** A pattern for every upstream, or one per role. A role left out takes the default. */
81
+ type ByRole = Pattern | {
82
+ readonly provider?: Pattern;
83
+ readonly rs?: Pattern;
84
+ readonly peer?: Pattern;
85
+ };
86
+ /** How the names built from each upstream are made. Each one left out takes its default. */
87
+ export type UpstreamNames = {
88
+ /** The VLAN a handoff is on. `{key}` when absent. */
89
+ vlan?: ByRole;
90
+ /** The native VLAN of a handoff sent tagged. `{key}-native` when absent. */
91
+ native?: Pattern;
92
+ /** The router's interface: per role, and `tunnel` for one reached over a tunnel. `{key}` when absent. */
93
+ interface?: Pattern | {
94
+ readonly provider?: Pattern;
95
+ readonly rs?: Pattern;
96
+ readonly peer?: Pattern;
97
+ readonly tunnel?: Pattern;
98
+ };
99
+ /** The import policy. `{KEY}-IN` when absent. */
100
+ import?: Pattern;
101
+ /** The export policy. `{KEY}-OUT` when absent. */
102
+ export?: Pattern;
103
+ /** The BGP group of an exchange's route servers or of multihop sessions. `{key}` when absent. */
104
+ group?: Pattern;
105
+ /** The description of a handoff's switch port. Its `name` when absent. No device names it, so it may be any function. */
106
+ port?: (key: string, upstream: Upstream) => string;
107
+ };
75
108
  /** How each upstream's policies are written: shared by every upstream of a catalog. */
76
109
  export type UpstreamPolicies<P extends string> = {
77
110
  /** A policy holding `Bgp.sanity()` that each import calls. The sanity rules are written in each import when absent. */
@@ -83,6 +116,17 @@ export type UpstreamPolicies<P extends string> = {
83
116
  /** The source address for routes of its family, one per family. */
84
117
  sources?: readonly IP[];
85
118
  };
119
+ /** The names a device can refer to, and their patterns when the project gives none. */
120
+ declare const DEFAULTS: {
121
+ readonly vlan: "{key}";
122
+ readonly native: "{key}-native";
123
+ readonly interface: "{key}";
124
+ readonly import: "{KEY}-IN";
125
+ readonly export: "{KEY}-OUT";
126
+ readonly group: "{key}";
127
+ };
128
+ type Defaults = typeof DEFAULTS;
129
+ type Field = keyof Defaults;
86
130
  type Key<C> = keyof C & string;
87
131
  /** The upstreams that land on a VLAN of ours. */
88
132
  type Landed<C> = {
@@ -118,37 +162,76 @@ type Importing<C> = {
118
162
  import: object;
119
163
  } ? K : never;
120
164
  }[Key<C>];
121
- /** A native VLAN's name. */
122
- type Native<K extends string> = `${K}-native`;
123
- /** Each upstream's policies: `TRANSIT_A-IN` and `TRANSIT_A-OUT`. */
124
- type PolicyName<C> = `${Uppercase<Importing<C>>}-IN` | `${Uppercase<Key<C>>}-OUT`;
165
+ /** The project's pattern for a name, or the default. */
166
+ type PatternOf<N, F extends Field> = N extends {
167
+ [X in F]: infer P;
168
+ } ? P : Defaults[F];
169
+ /** The pattern an upstream takes: the only one, its tunnel's, its role's, or the default. */
170
+ type Chosen<P, U, F extends Field> = P extends string ? P : U extends {
171
+ tunnel: object;
172
+ } ? P extends {
173
+ tunnel: infer T extends string;
174
+ } ? T : Defaults[F] : U extends {
175
+ role: infer R extends string;
176
+ } ? P extends {
177
+ [X in R]: infer T extends string;
178
+ } ? T : Defaults[F] : Defaults[F];
179
+ /** Every `From` in `S` replaced. */
180
+ type Replace<S extends string, From extends string, To extends string> = S extends `${infer A}${From}${infer B}` ? `${A}${To}${Replace<B, From, To>}` : S;
181
+ /** An upstream's `handoff.number` or `tunnel.number`. */
182
+ type NumberOf<U> = U extends {
183
+ handoff: {
184
+ number: infer N extends number;
185
+ };
186
+ } ? N : U extends {
187
+ tunnel: {
188
+ number: infer N extends number;
189
+ };
190
+ } ? N : number;
191
+ /** The name built for upstream `K`: its pattern with the key, the key in capitals and its number filled in. */
192
+ type Named<C, N, F extends Field, K extends Key<C>> = Replace<Replace<Replace<Chosen<PatternOf<N, F>, C[K], F>, "{key}", K>, "{KEY}", Uppercase<K>>, "{number}", `${NumberOf<C[K]>}`>;
193
+ /** Each of these upstreams' names. */
194
+ type Names<C, N, F extends Field, Ks extends Key<C>> = {
195
+ [K in Ks]: Named<C, N, F, K>;
196
+ }[Ks];
125
197
  /** The catalog as written, with any key the shape does not have refused, at every level. */
126
- type Exact<S, T> = T extends readonly unknown[] | string | number | boolean | Secret ? T : {
198
+ type Exact<S, T> = T extends readonly unknown[] | string | number | boolean | Secret | ((...args: never[]) => unknown) ? T : {
127
199
  [K in keyof T]: K extends keyof S ? Exact<NonNullable<S[K]>, T[K]> : never;
128
200
  };
201
+ /** The names as written, with a name or a role Upstreams does not make refused. */
202
+ type ExactNames<N> = {
203
+ [F in keyof N]: F extends keyof UpstreamNames ? N[F] extends string | ((...args: never[]) => unknown) ? N[F] : {
204
+ [R in keyof N[F]]: R extends "provider" | "rs" | "peer" | "tunnel" ? N[F][R] : never;
205
+ } : never;
206
+ };
129
207
  /** A network's upstreams written once, and each device's part built from them. */
130
208
  export declare class Upstreams {
131
209
  /**
132
- * Each upstream written once: its AS, where it lands, its sessions, limits and policy. Everything a device needs is built from it, under names made from each key, so a device that names one is checked:
210
+ * Each upstream written once: its AS, where it lands, its sessions, limits and policy. Everything a device needs is built from it, under names made from each key, so a device that names one is checked. By default:
133
211
  *
134
212
  * - VLAN `<key>`, and `<key>-native` for one that sends its VLAN tagged. The router's interface is `<key>` too, a VLAN interface or a GRE tunnel.
135
213
  * - Policies `<KEY>-IN` and `<KEY>-OUT`; a BGP group `<key>` for an exchange's route servers or multihop sessions.
136
214
  *
137
- * On the router: `vlans()`, `interfaces()`, `groups()`, `neighbors()`, `policies({ ... })`, `tunnelRules()`, `addresses()`. On a switch: `vlans(device)`, `ports(device)`, `vlansOn(device)`. For checks and ACLs: `untrusted()`, `natives()`, `privateDrops()`. Lists follow the catalog's order, where order counts on a device.
215
+ * `names` gives patterns of your own, such as `{ interface: { provider: "core-ipt0{number}-{key}" } }`; see `UpstreamNames`.
216
+ *
217
+ * On the router: `vlans()`, `interfaces()`, `uplinks()`, `groups()`, `neighbors()`, `policies({ ... })`, `tunnelRules()`, `addresses()`. On a switch: `vlans(device)`, `ports(device)`, `vlansOn(device)`. For checks and ACLs: `untrusted()`, `natives()`, `privateDrops()`. Lists follow the catalog's order, where order counts on a device.
138
218
  */
139
- static define<const C extends UpstreamCatalog>(catalog: C & {
219
+ static define<const C extends UpstreamCatalog, const N extends UpstreamNames = {}>(catalog: C & {
140
220
  [K in keyof C]: Exact<Upstream, C[K]>;
221
+ }, options?: {
222
+ names?: N & ExactNames<N>;
141
223
  }): {
142
- vlans: <const D extends string | undefined = undefined>(device?: D) => D extends string ? { [K in Landed<C>]: Vlan; } & { [K in Tagged<C> as Native<K>]: Vlan; } : { [K in Landed<C>]: Vlan; };
143
- vlansOn: (device: string) => Landed<C>[];
144
- ports: (device: string) => Record<string, Port<`${Tagged<C>}-native` | Landed<C>, never, never, never>>;
145
- interfaces: () => { [K in Landed<C>]: VlanInterface<Landed<C>, never>; } & { [K in Tunneled<C>]: Gre<never>; };
146
- groups: () => Record<Grouped<C>, NeighborSettings<PolicyName<C>>>;
147
- neighbors: () => Record<string, Neighbor<PolicyName<C>, Grouped<C>>>;
148
- policies: <const P extends string = never>(options?: UpstreamPolicies<P>) => Record<PolicyName<C>, PolicyRule<P, never>[]>;
149
- untrusted: () => (`${Tagged<C>}-native` | Landed<C>)[];
150
- natives: () => `${Tagged<C>}-native`[];
151
- privateDrops: () => Landed<C>[];
224
+ vlans: <const D extends string | undefined = undefined>(device?: D) => D extends string ? { [K in Names<C, N, "vlan", Landed<C>>]: Vlan; } & { [K in Names<C, N, "native", Tagged<C>>]: Vlan; } : { [K in Names<C, N, "vlan", Landed<C>>]: Vlan; };
225
+ vlansOn: (device: string) => Names<C, N, "vlan", Landed<C>>[];
226
+ ports: (device: string) => Record<string, Port<Names<C, N, "native", Tagged<C>> | Names<C, N, "vlan", Landed<C>>, never, never, never>>;
227
+ interfaces: () => { [K in Names<C, N, "interface", Landed<C>>]: VlanInterface<Names<C, N, "vlan", Landed<C>>, never>; } & { [K in Names<C, N, "interface", Tunneled<C>>]: Gre<never>; };
228
+ uplinks: () => Names<C, N, "interface", Landed<C>>[];
229
+ groups: () => Record<Names<C, N, "group", Grouped<C>>, NeighborSettings<Names<C, N, "export", Key<C>> | Names<C, N, "import", Importing<C>>>>;
230
+ neighbors: () => Record<string, Neighbor<Names<C, N, "export", Key<C>> | Names<C, N, "import", Importing<C>>, Names<C, N, "group", Grouped<C>>>>;
231
+ policies: <const P extends string = never>(options?: UpstreamPolicies<P>) => Record<Names<C, N, "export", Key<C>> | Names<C, N, "import", Importing<C>>, PolicyRule<P, never>[]>;
232
+ untrusted: () => (Names<C, N, "native", Tagged<C>> | Names<C, N, "vlan", Landed<C>>)[];
233
+ natives: () => Names<C, N, "native", Tagged<C>>[];
234
+ privateDrops: () => Names<C, N, "vlan", Landed<C>>[];
152
235
  tunnelRules: () => FilterRule<never, never>[];
153
236
  addresses: () => IP[];
154
237
  };