@homeflare/alchemy 0.33.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/grafana/alert-rule-group-drop-warning.d.ts +25 -0
  2. package/dist/grafana/alert-rule-group-drop-warning.d.ts.map +1 -0
  3. package/dist/grafana/alert-rule-group-fixtures.d.ts +73 -0
  4. package/dist/grafana/alert-rule-group-fixtures.d.ts.map +1 -0
  5. package/dist/grafana/alert-rule-group-model.d.ts +28 -0
  6. package/dist/grafana/alert-rule-group-model.d.ts.map +1 -0
  7. package/dist/grafana/alert-rule-group.d.ts +60 -0
  8. package/dist/grafana/alert-rule-group.d.ts.map +1 -0
  9. package/dist/grafana/alerting-provenance.d.ts +80 -0
  10. package/dist/grafana/alerting-provenance.d.ts.map +1 -0
  11. package/dist/grafana/contact-point-fixtures.d.ts +28 -0
  12. package/dist/grafana/contact-point-fixtures.d.ts.map +1 -0
  13. package/dist/grafana/contact-point.d.ts +99 -0
  14. package/dist/grafana/contact-point.d.ts.map +1 -0
  15. package/dist/grafana/dashboard-model.d.ts +8 -38
  16. package/dist/grafana/dashboard-model.d.ts.map +1 -1
  17. package/dist/grafana/datasource.d.ts +2 -9
  18. package/dist/grafana/datasource.d.ts.map +1 -1
  19. package/dist/grafana/index.d.ts +11 -4
  20. package/dist/grafana/index.d.ts.map +1 -1
  21. package/dist/grafana/index.js +339 -80
  22. package/dist/grafana/index.js.map +16 -7
  23. package/dist/grafana/message-template.d.ts +70 -0
  24. package/dist/grafana/message-template.d.ts.map +1 -0
  25. package/dist/grafana/mute-timing.d.ts +112 -0
  26. package/dist/grafana/mute-timing.d.ts.map +1 -0
  27. package/dist/grafana/providers.d.ts +1 -1
  28. package/dist/grafana/providers.d.ts.map +1 -1
  29. package/dist/grafana/secret-refs.d.ts +19 -0
  30. package/dist/grafana/secret-refs.d.ts.map +1 -0
  31. package/dist/grafana/subset-match.d.ts +2 -0
  32. package/dist/grafana/subset-match.d.ts.map +1 -0
  33. package/dist/linux/index.d.ts +1 -1
  34. package/dist/linux/index.d.ts.map +1 -1
  35. package/dist/linux/index.js +3 -1
  36. package/dist/linux/index.js.map +1 -1
  37. package/dist/proxmox/index.js +349 -287
  38. package/dist/proxmox/index.js.map +8 -7
  39. package/dist/proxmox/network-apply-read.d.ts +12 -0
  40. package/dist/proxmox/network-apply-read.d.ts.map +1 -1
  41. package/dist/proxmox/network-apply.d.ts +2 -0
  42. package/dist/proxmox/network-apply.d.ts.map +1 -1
  43. package/dist/proxmox/node-network-form.d.ts +86 -54
  44. package/dist/proxmox/node-network-form.d.ts.map +1 -1
  45. package/dist/proxmox/node-network-wire.d.ts +100 -0
  46. package/dist/proxmox/node-network-wire.d.ts.map +1 -0
  47. package/dist/proxmox/node-network.d.ts +21 -106
  48. package/dist/proxmox/node-network.d.ts.map +1 -1
  49. package/docs/grafana-alerting-rules.md +143 -0
  50. package/docs/grafana-alerting.md +147 -0
  51. package/docs/grafana.md +6 -3
  52. package/docs/upstream-conformance.md +38 -4
  53. package/package.json +1 -1
  54. package/src/grafana/alert-rule-group-drop-warning.test.ts +107 -0
  55. package/src/grafana/alert-rule-group-drop-warning.ts +41 -0
  56. package/src/grafana/alert-rule-group-fixtures.ts +46 -0
  57. package/src/grafana/alert-rule-group-model.test.ts +75 -0
  58. package/src/grafana/alert-rule-group-model.ts +66 -0
  59. package/src/grafana/alert-rule-group-provenance.test.ts +116 -0
  60. package/src/grafana/alert-rule-group.test.ts +164 -0
  61. package/src/grafana/alert-rule-group.ts +231 -0
  62. package/src/grafana/alerting-provenance.ts +109 -0
  63. package/src/grafana/contact-point-fixtures.ts +25 -0
  64. package/src/grafana/contact-point-provenance.test.ts +113 -0
  65. package/src/grafana/contact-point-secrets.test.ts +117 -0
  66. package/src/grafana/contact-point.test.ts +123 -0
  67. package/src/grafana/contact-point.ts +179 -0
  68. package/src/grafana/dashboard-model.ts +8 -57
  69. package/src/grafana/datasource.ts +5 -27
  70. package/src/grafana/index.ts +31 -4
  71. package/src/grafana/message-template.test.ts +212 -0
  72. package/src/grafana/message-template.ts +114 -0
  73. package/src/grafana/mute-timing.test.ts +227 -0
  74. package/src/grafana/mute-timing.ts +160 -0
  75. package/src/grafana/providers.ts +8 -0
  76. package/src/grafana/secret-refs.ts +48 -0
  77. package/src/grafana/subset-match.ts +71 -0
  78. package/src/linux/index-export.test.ts +25 -0
  79. package/src/linux/index.ts +15 -1
  80. package/src/proxmox/constraints-host-forms.test.ts +2 -2
  81. package/src/proxmox/network-apply-read.ts +12 -0
  82. package/src/proxmox/network-apply.ts +2 -0
  83. package/src/proxmox/node-network-form.ts +93 -99
  84. package/src/proxmox/node-network-read-failure.test.ts +206 -0
  85. package/src/proxmox/node-network-wire.ts +239 -0
  86. package/src/proxmox/node-network.ts +155 -167
@@ -18,6 +18,12 @@
18
18
  * drops the envelope, so the one readable pending signal is invisible to every other resource in
19
19
  * this package. Without the fetch below, `Proxmox.NetworkApply` could not diff honestly at all.
20
20
  * ★ `pveEnvelopeWith` in client.ts now carries the same member failover as every other call.
21
+ * ⛔ WHY THIS FAMILY DID NOT MOVE TO DISTILLED WITH `Proxmox.NodeNetwork` (2026-09-24, decision
22
+ * 43's walk-down): CONFIRMED against `@distilled.cloud/proxmox`'s own `protocol.ts` that its
23
+ * `transformResponse` unconditionally returns `body.data ?? {}` for every operation, before a
24
+ * generated call's typed output is even built — no typed call, current or future, could see
25
+ * `changes` without a protocol-level change upstream. `pveEnvelopeWith` stays because nothing
26
+ * else preserves the sibling this function needs.
21
27
  *
22
28
  * ⛔ AND THE INTERFACE ROWS CANNOT TELL YOU EITHER. MEASURED in PVE/INotify.pm `read_file`: when
23
29
  * `/etc/network/interfaces.new` exists, THAT is the file that gets parsed, so
@@ -88,6 +94,12 @@ export declare const UNREACHABLE = "the task could not be read -- the node stopp
88
94
  * of the task". Every mint returns a NEW token id, so a second mint is a different identity and
89
95
  * is not the owner — which is why this takes a `PveCredential` and the caller keeps one lease
90
96
  * across both calls, instead of widening the provisioning role to read its own tasks.
97
+ *
98
+ * ⛔ THE OTHER REASON THIS FAMILY DID NOT MOVE TO DISTILLED (2026-09-24): CHECKED against
99
+ * `@distilled.cloud/proxmox`'s own `Task.awaitTask` (`src/task.ts`) before deciding — it
100
+ * propagates a poll failure as a genuine typed `GetNodeTaskStatusError` rather than collapsing
101
+ * it to `UNREACHABLE` the way this function does, which would turn the ⛔ two paragraphs above
102
+ * into a failed `reconcile` on exactly the poll a mid-reload connection drop is expected to hit.
91
103
  */
92
104
  export declare const awaitTask: (target: PveTarget, credential: PveCredential, node: string, upid: string, attempts?: number) => Effect.Effect<string, never, import("effect/unstable/http/HttpClient").HttpClient>;
93
105
  //# sourceMappingURL=network-apply-read.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"network-apply-read.d.ts","sourceRoot":"","sources":["../../src/proxmox/network-apply-read.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAatE,oGAAoG;AACpG,MAAM,MAAM,aAAa,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;AA0B7E;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY,SAAU,MAAM,WAWxC,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,YAAY,WAAY,SAAS,QAAQ,MAAM,8IACH,CAAC;AAiC1D;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,WAAY,SAAS,0JAO7C,CAAC;AAEJ;;;GAGG;AACH,eAAO,MAAM,WAAW,0FACiE,CAAC;AAY1F;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,SAAS,WACZ,SAAS,cACL,aAAa,QACnB,MAAM,QACN,MAAM,aAEF,MAAM,uFAgCd,CAAC"}
1
+ {"version":3,"file":"network-apply-read.d.ts","sourceRoot":"","sources":["../../src/proxmox/network-apply-read.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAatE,oGAAoG;AACpG,MAAM,MAAM,aAAa,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;AA0B7E;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY,SAAU,MAAM,WAWxC,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,YAAY,WAAY,SAAS,QAAQ,MAAM,8IACH,CAAC;AAiC1D;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,WAAY,SAAS,0JAO7C,CAAC;AAEJ;;;GAGG;AACH,eAAO,MAAM,WAAW,0FACiE,CAAC;AAY1F;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,SAAS,WACZ,SAAS,cACL,aAAa,QACnB,MAAM,QACN,MAAM,aAEF,MAAM,uFAgCd,CAAC"}
@@ -44,6 +44,8 @@
44
44
  * task it starts so it can poll it without `Sys.Audit`. ⛔ `Sys.Modify` ON A NODE IS NOT A SMALL
45
45
  * GRANT — it also carries that node's DNS, hosts file, time and service configuration. Granting
46
46
  * it cluster-wide to run this resource widens the provisioning role well beyond networking.
47
+ *
48
+ * ⛔ STAYS ON `client.ts` (2026-09-24) — network-apply-read.ts's header has the two MEASURED reasons.
47
49
  */
48
50
  import { Resource } from 'alchemy';
49
51
  import * as Provider from 'alchemy/Provider';
@@ -1 +1 @@
1
- {"version":3,"file":"network-apply.d.ts","sourceRoot":"","sources":["../../src/proxmox/network-apply.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAM7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,MAAM,WAAW,iBAAkB,SAAQ,UAAU;IACnD;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,sBAAsB;IACrC,+FAA+F;IAC/F,IAAI,EAAE,MAAM,CAAC;IAEb;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,mBAAoB,SAAQ,QAAQ,CACnD,sBAAsB,EACtB,iBAAiB,EACjB,sBAAsB,EACtB,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,eAAO,MAAM,mBAAmB,sDAAwD,CAAC;AAiIzF,eAAO,MAAM,2BAA2B,yIACyD,CAAC"}
1
+ {"version":3,"file":"network-apply.d.ts","sourceRoot":"","sources":["../../src/proxmox/network-apply.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAM7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,MAAM,WAAW,iBAAkB,SAAQ,UAAU;IACnD;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,sBAAsB;IACrC,+FAA+F;IAC/F,IAAI,EAAE,MAAM,CAAC;IAEb;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,mBAAoB,SAAQ,QAAQ,CACnD,sBAAsB,EACtB,iBAAiB,EACjB,sBAAsB,EACtB,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,eAAO,MAAM,mBAAmB,sDAAwD,CAAC;AAiIzF,eAAO,MAAM,2BAA2B,yIACyD,CAAC"}
@@ -1,31 +1,63 @@
1
1
  /**
2
- * How a node interface crosses the wire in both directions — props to a PVE form, a PVE answer to
3
- * a comparable attribute — and when two of those values count as the same value.
4
- *
5
- * ★ SPLIT OUT OF node-network.ts TO KEEP BOTH FILES UNDER THE 250-LINE CAP, and the seam is the
6
- * same one metric-server-form.ts uses, one field wider: this file owns the COERCIONS, in both
7
- * directions, because a read that normalises differently from the write that produced it is
8
- * exactly how a forever-diff is born — keeping `ifaceList` next to both ends is what stops that.
9
- * node-network.ts answers "what is an interface, and which of its fields are safe to compare at
10
- * all". Nothing here calls the cluster.
11
- *
12
- * ⚠️ THE `import type` BACK TO node-network.ts IS A CYCLE ON PAPER ONLY — type-only, erased before
13
- * anything runs, so `NodeNetworkProps` stays in the file that declares the resource.
14
- *
15
- * * ⛔ THE LAST SEVEN ARE REPORTED AND NEVER COMPARED, AND EACH ONE IS A MEASURED FOREVER-DIFF.
16
- * `priority` is assigned by PVE FROM FILE ORDER — `my $priority = 2; ... $d->{priority} =
17
- * $priority++` — and it is not a POST or PUT parameter at all. MEASURED ACROSS C1: vmbr1.42 is
18
- * priority 16 on node-b and node-c and 15 on node-d, because node-d has no `wlan0` above it. One declaration
19
- * reused across three nodes would diff on exactly one of them, forever, over nothing.
20
- * `method` is RECOMPUTED on every write from whether the form carried an address, `families` is
21
- * recomputed the same way and never written to the file, `active` and `exists` describe the
22
- * kernel rather than the config, and `bond_miimon`, `bridge_stp` and `bridge_fd` are emitted by
23
- * PVE's writer with defaults (100, `off`, 0) while appearing in NEITHER write schema —
24
- * `additionalProperties => 0` means sending one is a 400. Live proof of all three: node-b's bond0
25
- * returns `bond_miimon "100"` and vmbr1 returns `bridge_stp "off"`, `bridge_fd "0"`, none of
26
- * which any declaration can set.
2
+ * `Proxmox.NodeNetwork`'s declared shape (`NodeNetworkProps`) and WRITE side, plus the wire
3
+ * coercions the read side needs too — kept here since neither `matches` nor `updateForm` makes
4
+ * sense without agreeing on the same normalisation. Split out of node-network.ts (2026-09-24,
5
+ * the distilled migration; `Props` joined it in this PR's later line-cap trim) — the
6
+ * storage.ts/storage-form.ts seam.
27
7
  */
28
- import type { NodeNetworkAttributes, NodeNetworkProps } from './node-network.ts';
8
+ import type { NodeNetworkType } from './node-network-wire.ts';
9
+ import { type WithTarget } from './resource-spec.ts';
10
+ export interface NodeNetworkProps extends WithTarget {
11
+ /** Which node's file this stanza lives in. Interfaces are per node, never cluster-wide. */
12
+ node: string;
13
+ /** `vmbr0`, `bond0`, `vmbr1.42`. 2-20 characters, PVE's `pve-iface` format. */
14
+ iface: string;
15
+ /** ⛔ REQUIRED ON EVERY WRITE, update included, never used to retype — see node-network-wire.ts. */
16
+ type: NodeNetworkType;
17
+ /**
18
+ * `198.51.100.12/24`. ⛔ THE ONE FIELD WITH NO UNMANAGED MODE: leaving it out is an instruction
19
+ * to make the interface `manual`, not an instruction to leave its address alone. The ⛔ on
20
+ * `updateForm` in node-network-form.ts has the measurement and the consequence.
21
+ */
22
+ cidr?: string;
23
+ /** ⚠️ PVE allows exactly ONE default gateway per node and refuses a second with "Default
24
+ * gateway already exists on interface '<other>'". On C1 it is vmbr0.41's. */
25
+ gateway?: string;
26
+ /**
27
+ * `auto <iface>` in the file. Absent on read means off, which is why `bool`'s fallback is used.
28
+ * ⚠️ UNDECLARED IS UNMANAGED ON AN UPDATE AND OFF ON A CREATE — the same asymmetry storage.ts
29
+ * has, and it bites harder here: an interface created without `autostart` is one the node
30
+ * will not bring up at boot. Every C1 bridge, bond and vlan carries it.
31
+ */
32
+ autostart?: boolean;
33
+ /** 1280-65520. Unset leaves the file without an `mtu` line and the kernel default in force. */
34
+ mtu?: number;
35
+ /** ⚠️ Round-trips only after normalisation — see `comment` in node-network-wire.ts. */
36
+ comments?: string;
37
+ /** Space-separated, a SET: `enp87s0`, or `bond0`. ⚠️ PVE refuses a port already used elsewhere. */
38
+ bridge_ports?: string;
39
+ /** `2-4094`, or `2 100-200`. Only written when `bridge_vlan_aware` is on. */
40
+ bridge_vids?: string;
41
+ /** ⛔ A `false` here is sent as `delete=`, never as `0` — node-network-form.ts's ⛔ says why. */
42
+ bridge_vlan_aware?: boolean;
43
+ /**
44
+ * A bond's members, space separated. ⚠️ THE PARAMETER IS `slaves`, NOT `bond_slaves`, in BOTH
45
+ * directions on this PVE: MEASURED, the POST/PUT schema names only `slaves` and the GET returns
46
+ * `"slaves":"enp2s0f0np0 enp2s0f1np1"`. `bond_slaves` appears nowhere in Network.pm here.
47
+ */
48
+ slaves?: string;
49
+ bond_mode?: string;
50
+ /** ⚠️ Only written when `bond_mode` is `balance-xor` or `802.3ad`; ignored otherwise. */
51
+ bond_xmit_hash_policy?: string;
52
+ /** active-backup only. Kept hyphenated because that is the wire name. */
53
+ 'bond-primary'?: string;
54
+ /** ⚠️ DERIVED FROM A DOTTED NAME. `vmbr1.42` reports `vlan-id` 42 with no such line in the file;
55
+ * declaring it there is harmless but adds a line the file did not have. */
56
+ 'vlan-id'?: number;
57
+ 'vlan-raw-device'?: string;
58
+ }
59
+ export declare const NODE_NETWORK_CREATE = "pve:POST /nodes/{node}/network";
60
+ export declare const NODE_NETWORK_UPDATE = "pve:PUT /nodes/{node}/network/{iface}";
29
61
  /**
30
62
  * "Not set", for the three integers this family reports.
31
63
  *
@@ -68,7 +100,7 @@ export declare const ifaceList: (value: unknown) => string;
68
100
  * C1 has none; on an interface that does, leave `comments` undeclared rather than fighting it.
69
101
  */
70
102
  export declare const comment: (value: unknown) => string;
71
- /** Undeclared is unmanaged: neither sent nor compared. See the ⚠️ on `body`. */
103
+ /** Undeclared is unmanaged: neither sent nor compared. */
72
104
  export declare const same: <T>(declared: T | undefined, live: T) => boolean;
73
105
  export declare const sameList: (declared: string | undefined, live: string) => boolean;
74
106
  export declare const sameComment: (declared: string | undefined, live: string) => boolean;
@@ -78,12 +110,13 @@ export declare const sameComment: (declared: string | undefined, live: string) =
78
110
  * ⚠️ UNDECLARED IS UNMANAGED — not sent, and not compared either. PVE's PUT MERGES the form into
79
111
  * the existing stanza (`foreach my $k (keys %$param) { $ifaces->{$iface}->{$k} = $param->{$k} }`),
80
112
  * so an omitted parameter is left exactly as it was. `cidr` is the one exception and it is not
81
- * a small one — see the ⛔ in node-network.ts.
113
+ * a small one — see node-network.ts's own ⛔.
82
114
  *
83
115
  * ⛔ `type` IS REQUIRED ON THE PUT, NOT ONLY ON THE POST. It carries no `optional` in either
84
- * schema, and the merge above writes it straight into the stanza — so a PUT naming the wrong
85
- * type would RETYPE A LIVE INTERFACE in the file and the writer would then emit a bridge stanza
86
- * for what is really a vlan. node-network.ts refuses that case before it can reach here.
116
+ * schema (CONFIRMED again in distilled's generated `PutNodeNetwork2Request`/
117
+ * `CreateNodeNetworkRequest`), and the merge above writes it straight into the stanza — so a PUT
118
+ * naming the wrong type would RETYPE A LIVE INTERFACE in the file. node-network-wire.ts's
119
+ * `attributesOf` refuses that case before it can reach here.
87
120
  *
88
121
  * ⛔ `bridge_vlan_aware` IS SENT ONLY WHEN TRUE, AND A FALSE ONE IS A `delete=`. MEASURED in the
89
122
  * writer: the test is `if (defined($d->{bridge_vlan_aware}))`, not a truth test, so `0` is
@@ -94,23 +127,31 @@ export declare const sameComment: (declared: string | undefined, live: string) =
94
127
  * "netmask conflicts with cidr" when both are sent, and the reader rewrites whatever was written
95
128
  * into prefix form — MEASURED: node-b's vmbr1.42 reports `"netmask":"24"`, never `255.255.255.0`.
96
129
  * A resource accepting both spellings would let a declaration diff against its own value.
130
+ *
131
+ * ★ HYPHENATED, THE WIRE SHAPE — `guardWrite` (node-network.ts) checks this form directly, keyed
132
+ * by PVE's own names (`generated/constraints/pve-nodes-network.ts`). `toDistilledCreate`/
133
+ * `toDistilledUpdate` below translate a COPY for the actual SDK call — storage-form.ts's
134
+ * `underscored` has the fuller measurement of why the wire itself does not need this rename
135
+ * (distilled's own `T.Body()` annotation re-hyphenates on the way out either way), reused here
136
+ * only for the three fields this family's schema renames: `bond-primary`, `vlan-id`,
137
+ * `vlan-raw-device` -> `bond_primary`, `vlan_id`, `vlan_raw_device`.
97
138
  */
98
139
  export declare const body: (props: NodeNetworkProps) => Record<string, string>;
99
140
  /**
100
141
  * The create form: `body` plus the name of the interface being created.
101
142
  *
102
- * 🔴 `iface` WAS MISSING UNTIL 2026-09-22, AND EVERY CREATE THIS FAMILY HAS EVER PLANNED WOULD
103
- * HAVE 400ed. `POST /nodes/{node}/network` declares `iface` REQUIRED (`pve-iface`, 2..20) and
104
- * `{node}` is its only path parameter, so the name has to travel in the BODY — unlike the PUT,
105
- * where `/network/{iface}` carries it and `updateBody` is right to leave it out. Nothing caught
106
- * it because no NodeNetwork has been created from a declaration yet: C1's three nodes were all
107
- * adopted, which takes the PUT path. Found by wiring this family to the vendor's own table,
108
- * which is exactly the 128-character comment again with a different parameter.
143
+ * 🔴 `iface` MUST BE IN THE BODY, NOT ONLY THE PATH — `POST /nodes/{node}/network` declares
144
+ * `iface` REQUIRED (`pve-iface`, 2..20) and `{node}` is its only path parameter, CONFIRMED again
145
+ * in distilled's generated `CreateNodeNetworkRequest`, which carries `iface` as a plain body
146
+ * field, never a `T.Label()`. The pre-distilled client.ts version of this file shipped without
147
+ * it for a time (see the git history around 2026-09-22) — every create this family had ever
148
+ * planned would have 400ed, caught only because every C1 interface had been adopted, never
149
+ * created, so nothing exercised the POST until the vendor table was wired in.
109
150
  *
110
151
  * ⚠️ NOT ADDED TO `body`, BECAUSE THE PUT MUST NOT CARRY IT. `additionalProperties => 0` on the
111
152
  * update schema makes a second copy of the name a 400 rather than an ignored hint.
112
153
  */
113
- export declare const createBody: (props: NodeNetworkProps) => Record<string, string>;
154
+ export declare const createForm: (props: NodeNetworkProps) => Record<string, string>;
114
155
  /**
115
156
  * The update form: `body` plus the two fields that can only be cleared explicitly.
116
157
  *
@@ -119,25 +160,16 @@ export declare const createBody: (props: NodeNetworkProps) => Record<string, str
119
160
  * `$param->{method} = $param->{address} ? 'static' : 'manual'` — so a PUT WITHOUT an address
120
161
  * makes the interface manual no matter what the caller intended. Left at that, the merge would
121
162
  * leave the old `address` and `netmask` in the hash and the writer would emit an `address` line
122
- * under `iface … inet manual`: a stanza PVE's own UI cannot produce, whose behaviour under
123
- * `ifreload -a` I did NOT measure. Clearing cidr explicitly at least produces a clean, honest
124
- * manual interface — and `matches` compares `cidr` UNCONDITIONALLY, so the plan says so first.
163
+ * under `iface … inet manual`: a stanza PVE's own UI cannot produce. Clearing cidr explicitly at
164
+ * least produces a clean, honest manual interface — and `matches` compares `cidr`
165
+ * UNCONDITIONALLY, so the plan says so first.
125
166
  *
126
167
  * ⛔ ON vmbr1.42 THAT IS THE CEPH TRANSPORT. A declaration of that interface without its `cidr` is
127
168
  * a declaration that it should have no address, applied across three nodes. The plan will read
128
169
  * `1 to update` rather than `noop`; do not wave it through.
129
170
  *
130
- * ⚠️ `delete` IS A PUT-ONLY PARAMETER. The POST schema is `additionalProperties => 0` and has no
131
- * `delete`, so sending it on a create is a 400 — which is why `createForm` calls `body` directly.
132
- */
133
- export declare const updateBody: (props: NodeNetworkProps) => Record<string, string>;
134
- /**
135
- * One live interface as attributes, or `undefined` — "this is not really there".
136
- *
137
- * ⚠️ EVERY FIELD PVE REPORTS IS CARRIED HERE, INCLUDING THE SEVEN NO DECLARATION CAN SET. A plan
138
- * that cannot show `priority`, `method` or `bond_miimon` cannot explain why it is ignoring them,
139
- * and the next person to read a forever-diff would start by adding them to `matches`. They are
140
- * reported precisely so that they are visibly OUT of it — see the ⛔ above the attribute type.
171
+ * ⚠️ `delete` IS A PUT-ONLY PARAMETER. The POST schema has no `delete` at all, which is why
172
+ * `createForm` calls `body` directly rather than going through this function.
141
173
  */
142
- export declare const readAttributes: (live: Record<string, unknown>, props: NodeNetworkProps) => NodeNetworkAttributes | undefined;
174
+ export declare const updateForm: (props: NodeNetworkProps) => Record<string, string>;
143
175
  //# sourceMappingURL=node-network-form.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"node-network-form.d.ts","sourceRoot":"","sources":["../../src/proxmox/node-network-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,KAAK,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAGjF;;;;;;;GAOG;AACH,eAAO,MAAM,KAAK,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,SAAS,UAAW,OAAO,WAK1B,CAAC;AAEf;;;;;;;;;GASG;AACH,eAAO,MAAM,OAAO,UAAW,OAAO,WAAoC,CAAC;AAE3E,gFAAgF;AAChF,eAAO,MAAM,IAAI,GAAI,CAAC,YAAY,CAAC,GAAG,SAAS,QAAQ,CAAC,YACX,CAAC;AAE9C,eAAO,MAAM,QAAQ,aAAc,MAAM,GAAG,SAAS,QAAQ,MAAM,YACX,CAAC;AAEzD,eAAO,MAAM,WAAW,aAAc,MAAM,GAAG,SAAS,QAAQ,MAAM,YAChB,CAAC;AAKvD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,IAAI,UAAW,gBAAgB,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAgBlE,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,UAAU,UAAW,gBAAgB,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAGxE,CAAC;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,UAAU,UAAW,gBAAgB,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAOzE,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,SACnB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,SACtB,gBAAgB,KACtB,qBAAqB,GAAG,SAuC1B,CAAC"}
1
+ {"version":3,"file":"node-network-form.d.ts","sourceRoot":"","sources":["../../src/proxmox/node-network-form.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGrD,MAAM,WAAW,gBAAiB,SAAQ,UAAU;IAClD,2FAA2F;IAC3F,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,KAAK,EAAE,MAAM,CAAC;IACd,mGAAmG;IACnG,IAAI,EAAE,eAAe,CAAC;IACtB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;kFAC8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+FAA+F;IAC/F,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mGAAmG;IACnG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+FAA+F;IAC/F,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yFAAyF;IACzF,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;gFAC4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,eAAO,MAAM,mBAAmB,mCAAmC,CAAC;AACpE,eAAO,MAAM,mBAAmB,0CAA0C,CAAC;AAE3E;;;;;;;GAOG;AACH,eAAO,MAAM,KAAK,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,SAAS,UAAW,OAAO,WAK1B,CAAC;AAEf;;;;;;;;;GASG;AACH,eAAO,MAAM,OAAO,UAAW,OAAO,WAAoC,CAAC;AAE3E,0DAA0D;AAC1D,eAAO,MAAM,IAAI,GAAI,CAAC,YAAY,CAAC,GAAG,SAAS,QAAQ,CAAC,YACX,CAAC;AAE9C,eAAO,MAAM,QAAQ,aAAc,MAAM,GAAG,SAAS,QAAQ,MAAM,YACX,CAAC;AAEzD,eAAO,MAAM,WAAW,aAAc,MAAM,GAAG,SAAS,QAAQ,MAAM,YAChB,CAAC;AAKvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,IAAI,UAAW,gBAAgB,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAgBlE,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,UAAU,UAAW,gBAAgB,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAGxE,CAAC;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,UAAU,UAAW,gBAAgB,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAMzE,CAAC"}
@@ -0,0 +1,100 @@
1
+ import * as Effect from 'effect/Effect';
2
+ import { type NodeNetworkProps } from './node-network-form.ts';
3
+ import { UNREADABLE, type Unreadable } from './unreadable-read.ts';
4
+ /**
5
+ * PVE's interface types. `unknown` is what a NIC it does not manage reports — node-b and node-c
6
+ * both report `wlan0` that way, and node-d has no such interface at all.
7
+ *
8
+ * ⚠️ THAT ASYMMETRY IS A TRAP FOR A DECLARATION SHARED ACROSS NODES. On node-d the read answers
9
+ * 400 ("interface does not exist"), which this provider cannot tell from "deleted", so
10
+ * reconcile POSTs — writing a stanza for a card that is not in the machine. Declare per node
11
+ * what each node actually has; a physical interface is discovered, not decided.
12
+ */
13
+ export type NodeNetworkType = 'OVSBond' | 'OVSBridge' | 'OVSIntPort' | 'OVSPort' | 'alias' | 'bond' | 'bridge' | 'eth' | 'fabric' | 'unknown' | 'vlan' | 'vnet';
14
+ /**
15
+ * ⛔ THE LAST SEVEN ARE REPORTED AND NEVER COMPARED — each is a MEASURED forever-diff, and the
16
+ * evidence for every one of them is below, beside the code that reads them.
17
+ */
18
+ export interface NodeNetworkAttributes {
19
+ node: string;
20
+ iface: string;
21
+ type: string;
22
+ cidr: string;
23
+ gateway: string;
24
+ autostart: boolean;
25
+ mtu: number;
26
+ comments: string;
27
+ bridge_ports: string;
28
+ bridge_vids: string;
29
+ bridge_vlan_aware: boolean;
30
+ slaves: string;
31
+ bond_mode: string;
32
+ bond_xmit_hash_policy: string;
33
+ 'bond-primary': string;
34
+ 'vlan-id': number;
35
+ 'vlan-raw-device': string;
36
+ priority: number;
37
+ method: string;
38
+ families: string;
39
+ active: boolean;
40
+ exists: boolean;
41
+ bond_miimon: string;
42
+ bridge_stp: string;
43
+ bridge_fd: string;
44
+ }
45
+ /**
46
+ * ★ Default `read` role — `Sys.Audit` on `/nodes/{node}` (node-network.ts's header).
47
+ * ⛔ `{ node: props.node, iface: props.iface }`, NEVER THE WHOLE `props` — storage.ts's/user.ts's
48
+ * own ⛔, the kit 0.31.1 regression: `GetNodeNetworkRequest`'s schema declares only `node` and
49
+ * `iface` (both path labels); any OTHER key on the object passed to `nodes.getNodeNetwork` is
50
+ * treated by distilled's `buildRequest` as an "unknown key" and JSON-encoded onto this bodyless
51
+ * GET, which a stricter fetch client refuses outright.
52
+ * ⛔ A MISSING INTERFACE IS A 400, NOT A 500 AND NOT A 404 — MEASURED against the live cluster
53
+ * (TB4 `n2`, read-role, read-only probe, 2026-09-24): `GET /nodes/n2/network/vmbr9` answers
54
+ * `{"errors":{"iface":"interface does not exist"},"data":null,"message":"Parameter verification
55
+ * failed.\n"}` at HTTP 400 — a THIRD shape, different from every other migrated family's
56
+ * measured 500. `@distilled.cloud/proxmox` types this as a genuine typed error,
57
+ * `ParameterVerificationFailed` (`errors.ts`), carrying the per-field `errors` object rather
58
+ * than collapsing it to an opaque `BadRequest` — PVE's OWN 400 is never one shape (it is
59
+ * whatever the endpoint's parameter verification rejected), so `errors.ts`'s own header spells
60
+ * out why this ONE typed error is global rather than per-operation.
61
+ * ⛔ ONLY THIS EXACT SIGNAL MEANS ABSENT, EVEN WITH THE FOLD BELOW. A different 400 reason, a
62
+ * permission gap, a network blip, or ANY OTHER `ParameterVerificationFailed` reason is never
63
+ * read as "this interface is not there" — `diff` calls this directly and propagates every one
64
+ * of them loudly.
65
+ */
66
+ export declare const readInterfaceOrFail: (props: NodeNetworkProps) => Effect.Effect<typeof UNREADABLE | NodeNetworkAttributes | undefined, import("@distilled.cloud/caddy").BadGateway | import("@distilled.cloud/caddy").BadRequest | import("@distilled.cloud/caddy").ConfigError | Error | import("@distilled.cloud/caddy").GatewayTimeout | import("effect/unstable/http/HttpClientError").HttpClientError | import("@distilled.cloud/caddy").InternalServerError | import("@distilled.cloud/proxmox/Errors").ParameterVerificationFailed | import("@distilled.cloud/proxmox/Errors").ProxmoxParseError | import("./distilled-pve.ts").PveClusterExhausted | import("@distilled.cloud/caddy").ServiceUnavailable | import("effect/Cause").TimeoutError | import("@distilled.cloud/caddy").TooManyRequests | import("@distilled.cloud/caddy").Unauthorized | import("@distilled.cloud/proxmox/Errors").UnknownProxmoxError, import("effect/unstable/http/HttpClient").HttpClient>;
67
+ /**
68
+ * ⛔ FOLDS A GENUINE READ FAILURE TO `undefined` TOO, AND ONLY `read`'s adoption/recovery branch
69
+ * AND `reconcile` MAY USE IT — FOUND BY ADVERSARIAL REVIEW, 2026-09-24: a first draft used
70
+ * `readInterfaceOrFail` (precise, non-folding) for EVERY caller, on the reasoning that a typed,
71
+ * parsed absence signal made folding unnecessary. That reasoning conflated two different
72
+ * questions: HOW PRECISELY a signal means absent (this family's is unusually precise), and
73
+ * WHETHER a given caller can safely treat an UNRELATED failure as "proceed as though absent"
74
+ * (storage.ts's/user.ts's own reasoning: a wrongful fold costs at most a redundant
75
+ * `createNodeNetwork` POST, refused loudly). Plan.ts's cold-start adoption probe and
76
+ * interrupted-create recovery, and Apply.ts's delete recovery, all call `read` with
77
+ * `output: undefined` and NO catch of their own (`Plan.ts` v2.0.0-beta.79 ~line 1308) — and
78
+ * Plan.ts fails the WHOLE PLAN, every other resource included, if that call throws. A malformed
79
+ * `iface` in ONE brand-new declaration (a DIFFERENT `ParameterVerificationFailed` than the
80
+ * absence one) would abort `bun run plan` for the entire stack under the precise-only design,
81
+ * where every other migrated family instead defers that same malformed declaration to a scoped,
82
+ * loud refusal at apply. This restores that same fold for those three call sites.
83
+ */
84
+ export declare const readInterface: (props: NodeNetworkProps) => Effect.Effect<typeof UNREADABLE | NodeNetworkAttributes | undefined, never, import("effect/unstable/http/HttpClient").HttpClient>;
85
+ /** `read`/`reconcile` return `Attributes | undefined`; only `diff` tells `UNREADABLE` apart. */
86
+ export declare const dropUnreadable: (live: NodeNetworkAttributes | Unreadable | undefined) => NodeNetworkAttributes | undefined;
87
+ /**
88
+ * ⛔ `cidr` IS THE ONLY FIELD COMPARED WHEN UNDECLARED, AND THAT ASYMMETRY IS THE POINT. Every
89
+ * other field here follows storage.ts: undeclared means unmanaged, so it is neither sent nor
90
+ * compared and a plan cannot report an update no write could satisfy. `cidr` cannot be
91
+ * unmanaged, because PVE derives `method` from the presence of an address IN THE FORM — an
92
+ * omission is an edit. Comparing it unconditionally is what makes that edit visible in `plan`
93
+ * instead of at `ifreload` time.
94
+ *
95
+ * ⚠️ NOTHING IN THE REPORTED-ONLY BLOCK IS HERE, and that is what makes the C1 declaration
96
+ * plan as `noop` on all three nodes at once — the module header lists them and the
97
+ * measurement behind each.
98
+ */
99
+ export declare const matches: (attributes: NodeNetworkAttributes, props: NodeNetworkProps) => boolean;
100
+ //# sourceMappingURL=node-network-wire.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-network-wire.d.ts","sourceRoot":"","sources":["../../src/proxmox/node-network-wire.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,EACL,KAAK,gBAAgB,EAOtB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,KAAK,UAAU,EAAoB,MAAM,sBAAsB,CAAC;AAGrF;;;;;;;;GAQG;AACH,MAAM,MAAM,eAAe,GACvB,SAAS,GACT,WAAW,GACX,YAAY,GACZ,SAAS,GACT,OAAO,GACP,MAAM,GACN,QAAQ,GACR,KAAK,GACL,QAAQ,GACR,SAAS,GACT,MAAM,GACN,MAAM,CAAC;AAEX;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AA4DD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,mBAAmB,UAAW,gBAAgB,m3BAmBxD,CAAC;AAEJ;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,aAAa,UAAW,gBAAgB,sIACmB,CAAC;AAEzE,gGAAgG;AAChG,eAAO,MAAM,cAAc,SAAU,qBAAqB,GAAG,UAAU,GAAG,SAAS,sCAC3C,CAAC;AAEzC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,OAAO,eAAgB,qBAAqB,SAAS,gBAAgB,YAcnB,CAAC"}
@@ -11,6 +11,9 @@
11
11
  * that apply — this one never calls it, exactly as `Proxmox.SdnZone` never calls
12
12
  * `Proxmox.SdnApply`.
13
13
  *
14
+ * ⛔ `Proxmox.NetworkApply` STAYS ON `client.ts`, NOT MIGRATED WITH THIS FILE — network-apply-
15
+ * read.ts's own header has the two MEASURED reasons distilled cannot represent yet.
16
+ *
14
17
  * ⛔ CEPH ON C1 RIDES TWO NETWORKS AND THIS API ONLY SEES ONE OF THEM. Measured from
15
18
  * `/etc/pve/ceph.conf` on 2026-09-13:
16
19
  *
@@ -18,7 +21,8 @@
18
21
  * cluster_network = 203.0.113.0/24 -> every OSD's cluster_addr: .102 / .103 / .104
19
22
  *
20
23
  * The cluster network — OSD REPLICATION, the traffic that rebuilds a lost replica — lives on
21
- * `203.0.113.10X/32` addresses carried by `tb0`, `tb1` and `dummy_c1`: the Thunderbolt mesh.
24
+ * `203.0.113.10X/32` addresses carried by `tb0`, `tb1` and `dummy_c1`: the Thunderbolt mesh —
25
+ * TB4's own fabric ports, EXCLUDED from this family by decision 28/29/9 (SdnFabric owns them).
22
26
  *
23
27
  * ⛔ AND THOSE ADDRESSES ARE INVISIBLE HERE, WHICH IS THE TRAP. They are declared in
24
28
  * `/etc/network/interfaces.d/sdn`, and PVE's network API does not parse that directory.
@@ -34,8 +38,9 @@
34
38
  * owns them, not this resource.
35
39
  *
36
40
  * ⚠️ Two ways this file limits the damage on the interfaces it CAN see: `cidr` is compared
37
- * unconditionally (the ⛔ on `matches`), and a live interface whose `type` disagrees with the
38
- * declaration is reported ABSENT rather than retyped (the ⛔ in `attributes`).
41
+ * unconditionally (node-network-wire.ts's own ⛔ on `matches`), and a live interface whose
42
+ * `type` disagrees with the declaration is reported ABSENT rather than retyped
43
+ * (node-network-wire.ts's `attributesOf`).
39
44
  *
40
45
  * ★ THE REASON IT EXISTS IS THAT THE STAGED READ CANNOT TELL YOU THE NETWORK IS RIGHT — it can
41
46
  * only tell you the FILE is. Once anything is staged, `GET /nodes/{node}/network/{iface}`
@@ -48,11 +53,9 @@
48
53
  * object to scope to, so that also buys the node's services, certificates, DNS and time.
49
54
  * Collected with every other family in docs/privileges.md.
50
55
  *
51
- * ★ A RECONCILE OVER A MATCHING INTERFACE IS A NO-WRITE, AND AN EARLIER DRAFT OF THIS COMMENT
52
- * SAID THE OPPOSITE. `pveOperations.reconcile` gates on `matches` BEFORE the PUT
53
- * (`resource.ts`: `else if (spec.updateForm !== undefined && !spec.matches(live, news))`), and
54
- * the ⛔ above that line names THIS family as the reason the guard was added — adopting an
55
- * interface a node already has would otherwise leave it holding a pending network change.
56
+ * ★ A RECONCILE OVER A MATCHING INTERFACE IS A NO-WRITE. `reconcile` below gates the PUT on
57
+ * `matches` first — adopting an interface a node already has must not leave it holding a
58
+ * pending network change, since a PUT under `/nodes/{node}/network/{iface}` STAGES one.
56
59
  *
57
60
  * ⚠️ WHAT IS STILL TRUE: when `matches` is false the PUT rewrites the whole stanza, and that can
58
61
  * ADD lines the file did not have — a `vlan-raw-device vmbr1` under `vmbr1.42`, where PVE had
@@ -64,110 +67,22 @@
64
67
  * them would put Alchemy one `destroy` away from staging the removal of a node's uplink — and
65
68
  * `delete_network` has NO in-use check at all: MEASURED, it deletes the hash entry and writes
66
69
  * the file. Adoption stays explicit.
70
+ *
71
+ * ★ MIGRATED OFF `client.ts`'s generic `pve()`/`pveHandlers` ONTO `@distilled.cloud/proxmox`'s
72
+ * typed `nodes.getNodeNetwork`/`createNodeNetwork`/`putNodeNetwork2`/`deleteNodeNetwork2`
73
+ * (2026-09-24, decision 43). Dual-path read and `read`'s `output`-branching wired like every
74
+ * other migrated family; node-network-wire.ts's `readInterfaceOrFail` has the new nuance.
67
75
  */
68
76
  import { Resource } from 'alchemy';
69
77
  import * as Provider from 'alchemy/Provider';
70
- import { type PveRequirements, type WithTarget } from './resource.ts';
71
- /**
72
- * PVE's interface types. `unknown` is what a NIC it does not manage reports — node-b and node-c both
73
- * report `wlan0` that way, and node-d has no such interface at all.
74
- *
75
- * ⚠️ THAT ASYMMETRY IS A TRAP FOR A DECLARATION SHARED ACROSS NODES. On node-d the read answers 404,
76
- * which this provider cannot tell from "deleted", so reconcile POSTs — writing a stanza for a
77
- * card that is not in the machine. Declare per node what each node actually has; a physical
78
- * interface is discovered, not decided.
79
- */
80
- export type NodeNetworkType = 'OVSBond' | 'OVSBridge' | 'OVSIntPort' | 'OVSPort' | 'alias' | 'bond' | 'bridge' | 'eth' | 'fabric' | 'unknown' | 'vlan' | 'vnet';
81
- export interface NodeNetworkProps extends WithTarget {
82
- /** Which node's file this stanza lives in. Interfaces are per node, never cluster-wide. */
83
- node: string;
84
- /** `vmbr0`, `bond0`, `vmbr1.42`. 2-20 characters, PVE's `pve-iface` format. */
85
- iface: string;
86
- /** ⛔ REQUIRED ON EVERY WRITE, update included, never used to retype — see `readAttributes`. */
87
- type: NodeNetworkType;
88
- /**
89
- * `198.51.100.12/24`. ⛔ THE ONE FIELD WITH NO UNMANAGED MODE: leaving it out is an instruction
90
- * to make the interface `manual`, not an instruction to leave its address alone. The ⛔ on
91
- * `updateBody` in node-network-form.ts has the measurement and the consequence.
92
- */
93
- cidr?: string;
94
- /** ⚠️ PVE allows exactly ONE default gateway per node and refuses a second with "Default
95
- * gateway already exists on interface '<other>'". On C1 it is vmbr0.41's. */
96
- gateway?: string;
97
- /**
98
- * `auto <iface>` in the file. Absent on read means off, which is why `bool`'s fallback is used.
99
- * ⚠️ UNDECLARED IS UNMANAGED ON AN UPDATE AND OFF ON A CREATE — the same asymmetry storage.ts
100
- * has, and it bites harder here: an interface created without `autostart` is one the node
101
- * will not bring up at boot. Every C1 bridge, bond and vlan carries it.
102
- */
103
- autostart?: boolean;
104
- /** 1280-65520. Unset leaves the file without an `mtu` line and the kernel default in force. */
105
- mtu?: number;
106
- /** ⚠️ Round-trips only after normalisation — see `comment` in node-network-form.ts. */
107
- comments?: string;
108
- /** Space-separated, a SET: `enp87s0`, or `bond0`. ⚠️ PVE refuses a port already used elsewhere. */
109
- bridge_ports?: string;
110
- /** `2-4094`, or `2 100-200`. Only written when `bridge_vlan_aware` is on. */
111
- bridge_vids?: string;
112
- /** ⛔ A `false` here is sent as `delete=`, never as `0` — the ⛔ on `body` says why. */
113
- bridge_vlan_aware?: boolean;
114
- /**
115
- * A bond's members, space separated. ⚠️ THE PARAMETER IS `slaves`, NOT `bond_slaves`, in BOTH
116
- * directions on this PVE: MEASURED, the POST/PUT schema names only `slaves` and the GET returns
117
- * `"slaves":"enp2s0f0np0 enp2s0f1np1"`. `bond_slaves` appears nowhere in Network.pm here.
118
- */
119
- slaves?: string;
120
- bond_mode?: string;
121
- /** ⚠️ Only written when `bond_mode` is `balance-xor` or `802.3ad`; ignored otherwise. */
122
- bond_xmit_hash_policy?: string;
123
- /** active-backup only. Kept hyphenated because that is the wire name. */
124
- 'bond-primary'?: string;
125
- /** ⚠️ DERIVED FROM A DOTTED NAME. `vmbr1.42` reports `vlan-id` 42 with no such line in the file;
126
- * declaring it there is harmless but adds a line the file did not have. */
127
- 'vlan-id'?: number;
128
- 'vlan-raw-device'?: string;
129
- }
130
- /**
131
- * ⛔ THE LAST SEVEN ARE REPORTED AND NEVER COMPARED — each is a MEASURED forever-diff, and the
132
- * evidence for every one of them is in node-network-form.ts, beside the code that reads them.
133
- */
134
- export interface NodeNetworkAttributes {
135
- node: string;
136
- iface: string;
137
- type: string;
138
- cidr: string;
139
- gateway: string;
140
- autostart: boolean;
141
- mtu: number;
142
- comments: string;
143
- bridge_ports: string;
144
- bridge_vids: string;
145
- bridge_vlan_aware: boolean;
146
- slaves: string;
147
- bond_mode: string;
148
- bond_xmit_hash_policy: string;
149
- 'bond-primary': string;
150
- 'vlan-id': number;
151
- 'vlan-raw-device': string;
152
- priority: number;
153
- method: string;
154
- families: string;
155
- active: boolean;
156
- exists: boolean;
157
- bond_miimon: string;
158
- bridge_stp: string;
159
- bridge_fd: string;
160
- }
78
+ import { type NodeNetworkProps } from './node-network-form.ts';
79
+ import type { NodeNetworkAttributes } from './node-network-wire.ts';
80
+ import { type PveRequirements } from './resource-spec.ts';
81
+ export type { NodeNetworkAttributes, NodeNetworkType } from './node-network-wire.ts';
82
+ export type { NodeNetworkProps } from './node-network-form.ts';
161
83
  export interface ProxmoxNodeNetwork extends Resource<'Proxmox.NodeNetwork', NodeNetworkProps, NodeNetworkAttributes, never, PveRequirements> {
162
84
  }
163
85
  /** ★ `retain` by default — a node's only uplink cannot be rebuilt. See the ★ in resource.ts. */
164
86
  export declare const ProxmoxNodeNetwork: import("alchemy").ResourceClass<ProxmoxNodeNetwork>;
165
- /**
166
- * ⚠️ THE DESTROY IS STAGED LIKE EVERY OTHER WRITE, WHICH MAKES IT THE ONE RECOVERABLE DESTROY IN
167
- * THIS PACKAGE. It removes the stanza from `interfaces.new` and nothing else;
168
- * `DELETE /nodes/{node}/network` puts the node back by unlinking that file. It becomes
169
- * irreversible the moment somebody applies — and PVE performs NO in-use check before removing
170
- * an interface other bridges or Ceph still stand on.
171
- */
172
87
  export declare const ProxmoxNodeNetworkProvider: () => import("effect/Layer").Layer<Provider.Provider<ProxmoxNodeNetwork>, never, import("effect/unstable/http/HttpClient").HttpClient>;
173
88
  //# sourceMappingURL=node-network.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"node-network.d.ts","sourceRoot":"","sources":["../../src/proxmox/node-network.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAU7C,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAe,MAAM,eAAe,CAAC;AAEnF;;;;;;;;GAQG;AACH,MAAM,MAAM,eAAe,GACvB,SAAS,GACT,WAAW,GACX,YAAY,GACZ,SAAS,GACT,OAAO,GACP,MAAM,GACN,QAAQ,GACR,KAAK,GACL,QAAQ,GACR,SAAS,GACT,MAAM,GACN,MAAM,CAAC;AAEX,MAAM,WAAW,gBAAiB,SAAQ,UAAU;IAClD,2FAA2F;IAC3F,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,KAAK,EAAE,MAAM,CAAC;IACd,+FAA+F;IAC/F,IAAI,EAAE,eAAe,CAAC;IACtB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;kFAC8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+FAA+F;IAC/F,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mGAAmG;IACnG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sFAAsF;IACtF,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yFAAyF;IACzF,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;gFAC4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAmB,SAAQ,QAAQ,CAClD,qBAAqB,EACrB,gBAAgB,EAChB,qBAAqB,EACrB,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,gGAAgG;AAChG,eAAO,MAAM,kBAAkB,qDAE7B,CAAC;AA4CH;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,wIACwD,CAAC"}
1
+ {"version":3,"file":"node-network.d.ts","sourceRoot":"","sources":["../../src/proxmox/node-network.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0EG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAI7C,OAAO,EAGL,KAAK,gBAAgB,EAGtB,MAAM,wBAAwB,CAAC;AAOhC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAEpE,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAG1D,YAAY,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAIrF,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,MAAM,WAAW,kBAAmB,SAAQ,QAAQ,CAClD,qBAAqB,EACrB,gBAAgB,EAChB,qBAAqB,EACrB,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,gGAAgG;AAChG,eAAO,MAAM,kBAAkB,qDAE7B,CAAC;AAiCH,eAAO,MAAM,0BAA0B,wIAuFpC,CAAC"}