@homeflare/alchemy 0.1.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 (417) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/dist/cloudflare/client.d.ts +45 -0
  4. package/dist/cloudflare/client.d.ts.map +1 -0
  5. package/dist/cloudflare/index.d.ts +11 -0
  6. package/dist/cloudflare/index.d.ts.map +1 -0
  7. package/dist/cloudflare/index.js +129 -0
  8. package/dist/cloudflare/index.js.map +13 -0
  9. package/dist/cloudflare/lock-rules.d.ts +50 -0
  10. package/dist/cloudflare/lock-rules.d.ts.map +1 -0
  11. package/dist/cloudflare/providers.d.ts +19 -0
  12. package/dist/cloudflare/providers.d.ts.map +1 -0
  13. package/dist/cloudflare/r2-bucket-lock.d.ts +100 -0
  14. package/dist/cloudflare/r2-bucket-lock.d.ts.map +1 -0
  15. package/dist/forgejo/branch-protection-form.d.ts +19 -0
  16. package/dist/forgejo/branch-protection-form.d.ts.map +1 -0
  17. package/dist/forgejo/branch-protection.d.ts +47 -0
  18. package/dist/forgejo/branch-protection.d.ts.map +1 -0
  19. package/dist/forgejo/client.d.ts +37 -0
  20. package/dist/forgejo/client.d.ts.map +1 -0
  21. package/dist/forgejo/index.d.ts +18 -0
  22. package/dist/forgejo/index.d.ts.map +1 -0
  23. package/dist/forgejo/index.js +479 -0
  24. package/dist/forgejo/index.js.map +21 -0
  25. package/dist/forgejo/org-actions-secrets.d.ts +45 -0
  26. package/dist/forgejo/org-actions-secrets.d.ts.map +1 -0
  27. package/dist/forgejo/org-label.d.ts +46 -0
  28. package/dist/forgejo/org-label.d.ts.map +1 -0
  29. package/dist/forgejo/org-team.d.ts +42 -0
  30. package/dist/forgejo/org-team.d.ts.map +1 -0
  31. package/dist/forgejo/repo-webhook-form.d.ts +27 -0
  32. package/dist/forgejo/repo-webhook-form.d.ts.map +1 -0
  33. package/dist/forgejo/repo-webhook.d.ts +46 -0
  34. package/dist/forgejo/repo-webhook.d.ts.map +1 -0
  35. package/dist/forgejo/repository.d.ts +45 -0
  36. package/dist/forgejo/repository.d.ts.map +1 -0
  37. package/dist/forgejo/resource.d.ts +83 -0
  38. package/dist/forgejo/resource.d.ts.map +1 -0
  39. package/dist/forgejo/team-member.d.ts +39 -0
  40. package/dist/forgejo/team-member.d.ts.map +1 -0
  41. package/dist/forgejo/values.d.ts +24 -0
  42. package/dist/forgejo/values.d.ts.map +1 -0
  43. package/dist/index.d.ts +18 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +3 -0
  46. package/dist/index.js.map +9 -0
  47. package/dist/openbao/auth-role-form.d.ts +38 -0
  48. package/dist/openbao/auth-role-form.d.ts.map +1 -0
  49. package/dist/openbao/auth-role.d.ts +20 -0
  50. package/dist/openbao/auth-role.d.ts.map +1 -0
  51. package/dist/openbao/bao-address.d.ts +72 -0
  52. package/dist/openbao/bao-address.d.ts.map +1 -0
  53. package/dist/openbao/bao-http.d.ts +71 -0
  54. package/dist/openbao/bao-http.d.ts.map +1 -0
  55. package/dist/openbao/bao-status.d.ts +74 -0
  56. package/dist/openbao/bao-status.d.ts.map +1 -0
  57. package/dist/openbao/cloudflare-group-scope.d.ts +73 -0
  58. package/dist/openbao/cloudflare-group-scope.d.ts.map +1 -0
  59. package/dist/openbao/cloudflare-parity-catalog.d.ts +28 -0
  60. package/dist/openbao/cloudflare-parity-catalog.d.ts.map +1 -0
  61. package/dist/openbao/cloudflare-parity-snapshot.d.ts +31 -0
  62. package/dist/openbao/cloudflare-parity-snapshot.d.ts.map +1 -0
  63. package/dist/openbao/cloudflare-parity.d.ts +14 -0
  64. package/dist/openbao/cloudflare-parity.d.ts.map +1 -0
  65. package/dist/openbao/cloudflare-permission-groups-solve.d.ts +85 -0
  66. package/dist/openbao/cloudflare-permission-groups-solve.d.ts.map +1 -0
  67. package/dist/openbao/cloudflare-permission-groups.d.ts +87 -0
  68. package/dist/openbao/cloudflare-permission-groups.d.ts.map +1 -0
  69. package/dist/openbao/cloudflare-policy.d.ts +65 -0
  70. package/dist/openbao/cloudflare-policy.d.ts.map +1 -0
  71. package/dist/openbao/cloudflare-role-form.d.ts +110 -0
  72. package/dist/openbao/cloudflare-role-form.d.ts.map +1 -0
  73. package/dist/openbao/cloudflare-role-wire.d.ts +27 -0
  74. package/dist/openbao/cloudflare-role-wire.d.ts.map +1 -0
  75. package/dist/openbao/cloudflare-role.d.ts +35 -0
  76. package/dist/openbao/cloudflare-role.d.ts.map +1 -0
  77. package/dist/openbao/cloudflare-roles-config.d.ts +67 -0
  78. package/dist/openbao/cloudflare-roles-config.d.ts.map +1 -0
  79. package/dist/openbao/cloudflare-roles-expand.d.ts +76 -0
  80. package/dist/openbao/cloudflare-roles-expand.d.ts.map +1 -0
  81. package/dist/openbao/digest.d.ts +24 -0
  82. package/dist/openbao/digest.d.ts.map +1 -0
  83. package/dist/openbao/fake-bao.d.ts +40 -0
  84. package/dist/openbao/fake-bao.d.ts.map +1 -0
  85. package/dist/openbao/forgejo-bootstrap-parse.d.ts +46 -0
  86. package/dist/openbao/forgejo-bootstrap-parse.d.ts.map +1 -0
  87. package/dist/openbao/forgejo-bootstrap.d.ts +29 -0
  88. package/dist/openbao/forgejo-bootstrap.d.ts.map +1 -0
  89. package/dist/openbao/index.d.ts +18 -0
  90. package/dist/openbao/index.d.ts.map +1 -0
  91. package/dist/openbao/index.js +1158 -0
  92. package/dist/openbao/index.js.map +33 -0
  93. package/dist/openbao/mount-form.d.ts +60 -0
  94. package/dist/openbao/mount-form.d.ts.map +1 -0
  95. package/dist/openbao/mount-wire.d.ts +24 -0
  96. package/dist/openbao/mount-wire.d.ts.map +1 -0
  97. package/dist/openbao/mount.d.ts +20 -0
  98. package/dist/openbao/mount.d.ts.map +1 -0
  99. package/dist/openbao/pki-role-form.d.ts +118 -0
  100. package/dist/openbao/pki-role-form.d.ts.map +1 -0
  101. package/dist/openbao/pki-role.d.ts +50 -0
  102. package/dist/openbao/pki-role.d.ts.map +1 -0
  103. package/dist/openbao/policy-assembly.d.ts +13 -0
  104. package/dist/openbao/policy-assembly.d.ts.map +1 -0
  105. package/dist/openbao/policy-wire.d.ts +41 -0
  106. package/dist/openbao/policy-wire.d.ts.map +1 -0
  107. package/dist/openbao/policy.d.ts +83 -0
  108. package/dist/openbao/policy.d.ts.map +1 -0
  109. package/dist/openbao/proxmox-role-form.d.ts +92 -0
  110. package/dist/openbao/proxmox-role-form.d.ts.map +1 -0
  111. package/dist/openbao/proxmox-role.d.ts +47 -0
  112. package/dist/openbao/proxmox-role.d.ts.map +1 -0
  113. package/dist/openbao/ssh-role-form.d.ts +99 -0
  114. package/dist/openbao/ssh-role-form.d.ts.map +1 -0
  115. package/dist/openbao/ssh-role.d.ts +43 -0
  116. package/dist/openbao/ssh-role.d.ts.map +1 -0
  117. package/dist/proxmox/acl.d.ts +72 -0
  118. package/dist/proxmox/acl.d.ts.map +1 -0
  119. package/dist/proxmox/api-token-form.d.ts +59 -0
  120. package/dist/proxmox/api-token-form.d.ts.map +1 -0
  121. package/dist/proxmox/api-token.d.ts +159 -0
  122. package/dist/proxmox/api-token.d.ts.map +1 -0
  123. package/dist/proxmox/backup-job.d.ts +112 -0
  124. package/dist/proxmox/backup-job.d.ts.map +1 -0
  125. package/dist/proxmox/ceph-daemon-form.d.ts +70 -0
  126. package/dist/proxmox/ceph-daemon-form.d.ts.map +1 -0
  127. package/dist/proxmox/ceph-daemon.d.ts +162 -0
  128. package/dist/proxmox/ceph-daemon.d.ts.map +1 -0
  129. package/dist/proxmox/ceph-flag.d.ts +131 -0
  130. package/dist/proxmox/ceph-flag.d.ts.map +1 -0
  131. package/dist/proxmox/ceph-fs-wire.d.ts +106 -0
  132. package/dist/proxmox/ceph-fs-wire.d.ts.map +1 -0
  133. package/dist/proxmox/ceph-fs.d.ts +124 -0
  134. package/dist/proxmox/ceph-fs.d.ts.map +1 -0
  135. package/dist/proxmox/ceph-osd-tree.d.ts +154 -0
  136. package/dist/proxmox/ceph-osd-tree.d.ts.map +1 -0
  137. package/dist/proxmox/ceph-osd-write.d.ts +36 -0
  138. package/dist/proxmox/ceph-osd-write.d.ts.map +1 -0
  139. package/dist/proxmox/ceph-osd.d.ts +102 -0
  140. package/dist/proxmox/ceph-osd.d.ts.map +1 -0
  141. package/dist/proxmox/ceph-pool-form.d.ts +217 -0
  142. package/dist/proxmox/ceph-pool-form.d.ts.map +1 -0
  143. package/dist/proxmox/ceph-pool-settle.d.ts +39 -0
  144. package/dist/proxmox/ceph-pool-settle.d.ts.map +1 -0
  145. package/dist/proxmox/ceph-pool.d.ts +48 -0
  146. package/dist/proxmox/ceph-pool.d.ts.map +1 -0
  147. package/dist/proxmox/client.d.ts +64 -0
  148. package/dist/proxmox/client.d.ts.map +1 -0
  149. package/dist/proxmox/credentials.d.ts +153 -0
  150. package/dist/proxmox/credentials.d.ts.map +1 -0
  151. package/dist/proxmox/firewall-alias-form.d.ts +98 -0
  152. package/dist/proxmox/firewall-alias-form.d.ts.map +1 -0
  153. package/dist/proxmox/firewall-alias.d.ts +141 -0
  154. package/dist/proxmox/firewall-alias.d.ts.map +1 -0
  155. package/dist/proxmox/generated/pbs.d.ts +388 -0
  156. package/dist/proxmox/generated/pbs.d.ts.map +1 -0
  157. package/dist/proxmox/generated/pve.d.ts +4713 -0
  158. package/dist/proxmox/generated/pve.d.ts.map +1 -0
  159. package/dist/proxmox/group.d.ts +104 -0
  160. package/dist/proxmox/group.d.ts.map +1 -0
  161. package/dist/proxmox/ha-resource.d.ts +104 -0
  162. package/dist/proxmox/ha-resource.d.ts.map +1 -0
  163. package/dist/proxmox/ha-rule-form.d.ts +103 -0
  164. package/dist/proxmox/ha-rule-form.d.ts.map +1 -0
  165. package/dist/proxmox/ha-rule.d.ts +156 -0
  166. package/dist/proxmox/ha-rule.d.ts.map +1 -0
  167. package/dist/proxmox/index.d.ts +43 -0
  168. package/dist/proxmox/index.d.ts.map +1 -0
  169. package/dist/proxmox/index.js +2322 -0
  170. package/dist/proxmox/index.js.map +70 -0
  171. package/dist/proxmox/lease-cache.d.ts +98 -0
  172. package/dist/proxmox/lease-cache.d.ts.map +1 -0
  173. package/dist/proxmox/lxc.d.ts +59 -0
  174. package/dist/proxmox/lxc.d.ts.map +1 -0
  175. package/dist/proxmox/members.d.ts +56 -0
  176. package/dist/proxmox/members.d.ts.map +1 -0
  177. package/dist/proxmox/metric-server-form.d.ts +53 -0
  178. package/dist/proxmox/metric-server-form.d.ts.map +1 -0
  179. package/dist/proxmox/metric-server-otel.d.ts +49 -0
  180. package/dist/proxmox/metric-server-otel.d.ts.map +1 -0
  181. package/dist/proxmox/metric-server.d.ts +123 -0
  182. package/dist/proxmox/metric-server.d.ts.map +1 -0
  183. package/dist/proxmox/network-apply-read.d.ts +93 -0
  184. package/dist/proxmox/network-apply-read.d.ts.map +1 -0
  185. package/dist/proxmox/network-apply.d.ts +102 -0
  186. package/dist/proxmox/network-apply.d.ts.map +1 -0
  187. package/dist/proxmox/node-network-form.d.ts +128 -0
  188. package/dist/proxmox/node-network-form.d.ts.map +1 -0
  189. package/dist/proxmox/node-network.d.ts +173 -0
  190. package/dist/proxmox/node-network.d.ts.map +1 -0
  191. package/dist/proxmox/notification-target-form.d.ts +44 -0
  192. package/dist/proxmox/notification-target-form.d.ts.map +1 -0
  193. package/dist/proxmox/notification-target.d.ts +146 -0
  194. package/dist/proxmox/notification-target.d.ts.map +1 -0
  195. package/dist/proxmox/pbs-datastore-form.d.ts +147 -0
  196. package/dist/proxmox/pbs-datastore-form.d.ts.map +1 -0
  197. package/dist/proxmox/pbs-datastore-guard.d.ts +55 -0
  198. package/dist/proxmox/pbs-datastore-guard.d.ts.map +1 -0
  199. package/dist/proxmox/pbs-datastore.d.ts +140 -0
  200. package/dist/proxmox/pbs-datastore.d.ts.map +1 -0
  201. package/dist/proxmox/pbs-prune-job-form.d.ts +97 -0
  202. package/dist/proxmox/pbs-prune-job-form.d.ts.map +1 -0
  203. package/dist/proxmox/pbs-prune-job.d.ts +168 -0
  204. package/dist/proxmox/pbs-prune-job.d.ts.map +1 -0
  205. package/dist/proxmox/pbs-sync-job-form.d.ts +83 -0
  206. package/dist/proxmox/pbs-sync-job-form.d.ts.map +1 -0
  207. package/dist/proxmox/pbs-sync-job.d.ts +246 -0
  208. package/dist/proxmox/pbs-sync-job.d.ts.map +1 -0
  209. package/dist/proxmox/pbs-verify-job-form.d.ts +114 -0
  210. package/dist/proxmox/pbs-verify-job-form.d.ts.map +1 -0
  211. package/dist/proxmox/pbs-verify-job.d.ts +145 -0
  212. package/dist/proxmox/pbs-verify-job.d.ts.map +1 -0
  213. package/dist/proxmox/pool.d.ts +42 -0
  214. package/dist/proxmox/pool.d.ts.map +1 -0
  215. package/dist/proxmox/qemu.d.ts +46 -0
  216. package/dist/proxmox/qemu.d.ts.map +1 -0
  217. package/dist/proxmox/replication-job-form.d.ts +71 -0
  218. package/dist/proxmox/replication-job-form.d.ts.map +1 -0
  219. package/dist/proxmox/replication-job.d.ts +162 -0
  220. package/dist/proxmox/replication-job.d.ts.map +1 -0
  221. package/dist/proxmox/resource.d.ts +157 -0
  222. package/dist/proxmox/resource.d.ts.map +1 -0
  223. package/dist/proxmox/role.d.ts +97 -0
  224. package/dist/proxmox/role.d.ts.map +1 -0
  225. package/dist/proxmox/sdn-apply-read.d.ts +80 -0
  226. package/dist/proxmox/sdn-apply-read.d.ts.map +1 -0
  227. package/dist/proxmox/sdn-apply.d.ts +63 -0
  228. package/dist/proxmox/sdn-apply.d.ts.map +1 -0
  229. package/dist/proxmox/sdn-subnet-form.d.ts +111 -0
  230. package/dist/proxmox/sdn-subnet-form.d.ts.map +1 -0
  231. package/dist/proxmox/sdn-subnet.d.ts +161 -0
  232. package/dist/proxmox/sdn-subnet.d.ts.map +1 -0
  233. package/dist/proxmox/sdn-vnet.d.ts +113 -0
  234. package/dist/proxmox/sdn-vnet.d.ts.map +1 -0
  235. package/dist/proxmox/sdn-zone.d.ts +126 -0
  236. package/dist/proxmox/sdn-zone.d.ts.map +1 -0
  237. package/dist/proxmox/storage.d.ts +120 -0
  238. package/dist/proxmox/storage.d.ts.map +1 -0
  239. package/dist/proxmox/user.d.ts +107 -0
  240. package/dist/proxmox/user.d.ts.map +1 -0
  241. package/dist/proxmox/values.d.ts +134 -0
  242. package/dist/proxmox/values.d.ts.map +1 -0
  243. package/dist/proxmox/zfs-pool-write.d.ts +76 -0
  244. package/dist/proxmox/zfs-pool-write.d.ts.map +1 -0
  245. package/dist/proxmox/zfs-pool.d.ts +146 -0
  246. package/dist/proxmox/zfs-pool.d.ts.map +1 -0
  247. package/dist/talos/credentials.d.ts +30 -0
  248. package/dist/talos/credentials.d.ts.map +1 -0
  249. package/dist/talos/index.d.ts +15 -0
  250. package/dist/talos/index.d.ts.map +1 -0
  251. package/dist/talos/index.js +392 -0
  252. package/dist/talos/index.js.map +16 -0
  253. package/dist/talos/kubeconfig.d.ts +53 -0
  254. package/dist/talos/kubeconfig.d.ts.map +1 -0
  255. package/dist/talos/resource.d.ts +32 -0
  256. package/dist/talos/resource.d.ts.map +1 -0
  257. package/dist/talos/talos-bootstrap.d.ts +27 -0
  258. package/dist/talos/talos-bootstrap.d.ts.map +1 -0
  259. package/dist/talos/talos-cluster-health.d.ts +36 -0
  260. package/dist/talos/talos-cluster-health.d.ts.map +1 -0
  261. package/dist/talos/talos-machine-config.d.ts +41 -0
  262. package/dist/talos/talos-machine-config.d.ts.map +1 -0
  263. package/dist/talos/talosctl.d.ts +29 -0
  264. package/dist/talos/talosctl.d.ts.map +1 -0
  265. package/dist/talos/values.d.ts +20 -0
  266. package/dist/talos/values.d.ts.map +1 -0
  267. package/package.json +57 -0
  268. package/src/cloudflare/client.ts +76 -0
  269. package/src/cloudflare/index.ts +10 -0
  270. package/src/cloudflare/lock-rules.ts +60 -0
  271. package/src/cloudflare/providers.ts +23 -0
  272. package/src/cloudflare/r2-bucket-lock.ts +220 -0
  273. package/src/forgejo/branch-protection-form.ts +29 -0
  274. package/src/forgejo/branch-protection.ts +101 -0
  275. package/src/forgejo/client.ts +136 -0
  276. package/src/forgejo/index.ts +17 -0
  277. package/src/forgejo/org-actions-secrets.ts +99 -0
  278. package/src/forgejo/org-label.ts +100 -0
  279. package/src/forgejo/org-team.ts +105 -0
  280. package/src/forgejo/repo-webhook-form.ts +47 -0
  281. package/src/forgejo/repo-webhook.ts +118 -0
  282. package/src/forgejo/repository.ts +126 -0
  283. package/src/forgejo/resource.ts +171 -0
  284. package/src/forgejo/team-member.ts +97 -0
  285. package/src/forgejo/values.test.ts +48 -0
  286. package/src/forgejo/values.ts +61 -0
  287. package/src/index.ts +18 -0
  288. package/src/openbao/auth-role-form.ts +112 -0
  289. package/src/openbao/auth-role.ts +101 -0
  290. package/src/openbao/bao-address.test.ts +89 -0
  291. package/src/openbao/bao-address.ts +97 -0
  292. package/src/openbao/bao-http-gate.test.ts +88 -0
  293. package/src/openbao/bao-http-unix.test.ts +70 -0
  294. package/src/openbao/bao-http.test.ts +161 -0
  295. package/src/openbao/bao-http.ts +185 -0
  296. package/src/openbao/bao-status.test.ts +114 -0
  297. package/src/openbao/bao-status.ts +137 -0
  298. package/src/openbao/cloudflare-group-scope.test.ts +103 -0
  299. package/src/openbao/cloudflare-group-scope.ts +83 -0
  300. package/src/openbao/cloudflare-parity-catalog.ts +104 -0
  301. package/src/openbao/cloudflare-parity-snapshot.ts +84 -0
  302. package/src/openbao/cloudflare-parity.ts +152 -0
  303. package/src/openbao/cloudflare-permission-groups-solve.test.ts +171 -0
  304. package/src/openbao/cloudflare-permission-groups-solve.ts +197 -0
  305. package/src/openbao/cloudflare-permission-groups.test.ts +369 -0
  306. package/src/openbao/cloudflare-permission-groups.ts +299 -0
  307. package/src/openbao/cloudflare-policy.ts +180 -0
  308. package/src/openbao/cloudflare-role-form.test.ts +177 -0
  309. package/src/openbao/cloudflare-role-form.ts +183 -0
  310. package/src/openbao/cloudflare-role-wire.test.ts +132 -0
  311. package/src/openbao/cloudflare-role-wire.ts +41 -0
  312. package/src/openbao/cloudflare-role.ts +189 -0
  313. package/src/openbao/cloudflare-roles-config.ts +230 -0
  314. package/src/openbao/cloudflare-roles-expand-bucket.test.ts +134 -0
  315. package/src/openbao/cloudflare-roles-expand.ts +231 -0
  316. package/src/openbao/digest.ts +26 -0
  317. package/src/openbao/fake-bao.ts +84 -0
  318. package/src/openbao/forgejo-bootstrap-parse.test.ts +76 -0
  319. package/src/openbao/forgejo-bootstrap-parse.ts +90 -0
  320. package/src/openbao/forgejo-bootstrap.ts +181 -0
  321. package/src/openbao/index.ts +17 -0
  322. package/src/openbao/mount-form.ts +147 -0
  323. package/src/openbao/mount-wire.test.ts +108 -0
  324. package/src/openbao/mount-wire.ts +79 -0
  325. package/src/openbao/mount.ts +132 -0
  326. package/src/openbao/pki-role-form.ts +248 -0
  327. package/src/openbao/pki-role.ts +202 -0
  328. package/src/openbao/policy-assembly.test.ts +28 -0
  329. package/src/openbao/policy-assembly.ts +13 -0
  330. package/src/openbao/policy-wire.test.ts +90 -0
  331. package/src/openbao/policy-wire.ts +53 -0
  332. package/src/openbao/policy.ts +212 -0
  333. package/src/openbao/proxmox-role-form.ts +174 -0
  334. package/src/openbao/proxmox-role.ts +214 -0
  335. package/src/openbao/ssh-role-form.ts +248 -0
  336. package/src/openbao/ssh-role.ts +240 -0
  337. package/src/proxmox/acl.ts +267 -0
  338. package/src/proxmox/api-token-form.ts +122 -0
  339. package/src/proxmox/api-token.ts +222 -0
  340. package/src/proxmox/backup-job.ts +213 -0
  341. package/src/proxmox/ceph-daemon-form.ts +84 -0
  342. package/src/proxmox/ceph-daemon.ts +243 -0
  343. package/src/proxmox/ceph-flag.ts +249 -0
  344. package/src/proxmox/ceph-fs-wire.ts +246 -0
  345. package/src/proxmox/ceph-fs.ts +249 -0
  346. package/src/proxmox/ceph-osd-tree.ts +193 -0
  347. package/src/proxmox/ceph-osd-write.ts +76 -0
  348. package/src/proxmox/ceph-osd.ts +244 -0
  349. package/src/proxmox/ceph-pool-form.ts +249 -0
  350. package/src/proxmox/ceph-pool-settle.ts +70 -0
  351. package/src/proxmox/ceph-pool.ts +212 -0
  352. package/src/proxmox/client.test.ts +185 -0
  353. package/src/proxmox/client.ts +240 -0
  354. package/src/proxmox/credentials.test.ts +144 -0
  355. package/src/proxmox/credentials.ts +254 -0
  356. package/src/proxmox/firewall-alias-form.ts +112 -0
  357. package/src/proxmox/firewall-alias.ts +208 -0
  358. package/src/proxmox/generated/pbs.ts +461 -0
  359. package/src/proxmox/generated/pve.ts +7344 -0
  360. package/src/proxmox/group.ts +199 -0
  361. package/src/proxmox/ha-resource.ts +169 -0
  362. package/src/proxmox/ha-rule-form.ts +185 -0
  363. package/src/proxmox/ha-rule.ts +240 -0
  364. package/src/proxmox/index.ts +42 -0
  365. package/src/proxmox/lease-cache.test.ts +163 -0
  366. package/src/proxmox/lease-cache.ts +131 -0
  367. package/src/proxmox/lxc.ts +117 -0
  368. package/src/proxmox/members.test.ts +54 -0
  369. package/src/proxmox/members.ts +174 -0
  370. package/src/proxmox/metric-server-form.ts +128 -0
  371. package/src/proxmox/metric-server-otel.ts +99 -0
  372. package/src/proxmox/metric-server.ts +214 -0
  373. package/src/proxmox/network-apply-read.ts +228 -0
  374. package/src/proxmox/network-apply.ts +244 -0
  375. package/src/proxmox/node-network-form.ts +213 -0
  376. package/src/proxmox/node-network.ts +243 -0
  377. package/src/proxmox/notification-target-form.ts +104 -0
  378. package/src/proxmox/notification-target.ts +222 -0
  379. package/src/proxmox/pbs-datastore-form.ts +248 -0
  380. package/src/proxmox/pbs-datastore-guard.ts +105 -0
  381. package/src/proxmox/pbs-datastore.ts +271 -0
  382. package/src/proxmox/pbs-prune-job-form.ts +208 -0
  383. package/src/proxmox/pbs-prune-job.ts +227 -0
  384. package/src/proxmox/pbs-sync-job-form.ts +234 -0
  385. package/src/proxmox/pbs-sync-job.ts +258 -0
  386. package/src/proxmox/pbs-verify-job-form.ts +131 -0
  387. package/src/proxmox/pbs-verify-job.ts +227 -0
  388. package/src/proxmox/pool.test.ts +42 -0
  389. package/src/proxmox/pool.ts +80 -0
  390. package/src/proxmox/qemu.ts +89 -0
  391. package/src/proxmox/replication-job-form.ts +89 -0
  392. package/src/proxmox/replication-job.ts +248 -0
  393. package/src/proxmox/resource.ts +257 -0
  394. package/src/proxmox/role.ts +161 -0
  395. package/src/proxmox/sdn-apply-read.test.ts +152 -0
  396. package/src/proxmox/sdn-apply-read.ts +189 -0
  397. package/src/proxmox/sdn-apply.ts +154 -0
  398. package/src/proxmox/sdn-subnet-form.ts +156 -0
  399. package/src/proxmox/sdn-subnet.ts +238 -0
  400. package/src/proxmox/sdn-vnet.ts +228 -0
  401. package/src/proxmox/sdn-zone.ts +246 -0
  402. package/src/proxmox/storage.ts +245 -0
  403. package/src/proxmox/user.ts +214 -0
  404. package/src/proxmox/values.test.ts +193 -0
  405. package/src/proxmox/values.ts +203 -0
  406. package/src/proxmox/zfs-pool-write.ts +130 -0
  407. package/src/proxmox/zfs-pool.ts +253 -0
  408. package/src/talos/credentials.ts +136 -0
  409. package/src/talos/index.ts +14 -0
  410. package/src/talos/kubeconfig.ts +170 -0
  411. package/src/talos/resource.ts +31 -0
  412. package/src/talos/talos-bootstrap.ts +109 -0
  413. package/src/talos/talos-cluster-health.ts +119 -0
  414. package/src/talos/talos-machine-config.ts +146 -0
  415. package/src/talos/talosctl.ts +94 -0
  416. package/src/talos/values.test.ts +80 -0
  417. package/src/talos/values.ts +70 -0
@@ -0,0 +1,156 @@
1
+ /**
2
+ * How a subnet crosses the wire in both directions, and what PVE actually calls its path.
3
+ *
4
+ * ★ SPLIT OUT OF sdn-subnet.ts TO KEEP BOTH FILES UNDER THE 250-LINE CAP, on the seam
5
+ * node-network-form.ts cuts rather than at a convenient line number: this file owns the
6
+ * COERCIONS in BOTH directions plus the derived id, because a read that normalises differently
7
+ * from the write that produced it is exactly how a forever-diff is born. sdn-subnet.ts answers
8
+ * "what is a subnet, and when has it changed". Nothing here calls the cluster.
9
+ *
10
+ * ⚠️ THE `import type` BACK TO sdn-subnet.ts IS A CYCLE ON PAPER ONLY — type-only, so it is erased
11
+ * before anything runs and `SdnSubnetProps` stays in the file that declares the resource.
12
+ */
13
+ import type { SdnSubnetProps } from './sdn-subnet.ts';
14
+ import { propertyString, withClears } from './values.ts';
15
+
16
+ /** One DHCP pool. PVE's property-string keys are `start-address`/`end-address`; see `rangeString`. */
17
+ export type DhcpRange = { start: string; end: string };
18
+
19
+ /**
20
+ * The id PVE files a subnet under — and therefore the path segment GET, PUT and DELETE address it
21
+ * by, which is NOT the value POST is handed.
22
+ *
23
+ * ⛔ MEASURED from `/usr/share/perl5/PVE/API2/Network/SDN/Subnets.pm` on pve-manager 9.2.11, in
24
+ * the create handler: `my $id = $cidr =~ s/\//-/r; $id = "$zoneid-$id";`. So `subnet=10.0.0.0/24`
25
+ * goes out and `house-10.0.0.0-24` is what exists afterwards. A CIDR cannot be a path segment
26
+ * anyway — it carries a slash — so there is no spelling of this endpoint that takes the CIDR.
27
+ *
28
+ * ⚠️ `.replace`, NOT `.replaceAll`, AND THE PARITY IS DELIBERATE. Perl's `s/\//-/r` has no `/g`, so
29
+ * it rewrites the FIRST slash only. A CIDR has exactly one, so the two agree on every legal
30
+ * input; they would stop agreeing on an illegal one, and agreeing with PVE is the point.
31
+ */
32
+ export const subnetId = (props: Pick<SdnSubnetProps, 'cidr' | 'zone'>) =>
33
+ `${props.zone}-${props.cidr.replace('/', '-')}`;
34
+
35
+ /** A range on its way OUT. PVE stores the string verbatim; nothing rewrites it. */
36
+ const rangeString = (range: DhcpRange) => `start-address=${range.start},end-address=${range.end}`;
37
+
38
+ /**
39
+ * DHCP ranges, from either shape, as one comparable string.
40
+ *
41
+ * ⛔ THIS FAMILY'S WRITE SHAPE AND READ SHAPE ARE DIFFERENT, WHICH IS THE CLASSIC FOREVER-DIFF.
42
+ * MEASURED from the API module: `$scfg->{'dhcp-range'} = get_dhcp_ranges($scfg)` runs
43
+ * `parse_property_string` over every stored element, so a range WRITTEN as the string
44
+ * `start-address=10.0.0.10,end-address=10.0.0.20` is READ BACK as the object
45
+ * `{"start-address":"10.0.0.10","end-address":"10.0.0.20"}`. `propertyString` in values.ts
46
+ * flattens both shapes to one sorted form and exists for exactly this — see its own ⚠️.
47
+ *
48
+ * ⚠️ AND IT IS A LIST, SO ORDER MUST NOT BE MEANING. PVE validates only that ranges do not overlap
49
+ * and stores them in the order given; two declarations naming the same pools the other way round
50
+ * are the same subnet. Sorting is what stops that being an update, exactly as `csv` does for a
51
+ * zone's `nodes`.
52
+ *
53
+ * ⚠️ `;` IS THE JOINER BECAUSE `,` AND `=` ARE BOTH INSIDE THE VALUES. It never appears in an IPv4
54
+ * or IPv6 address or in a property-string key, so the joined form cannot be ambiguous.
55
+ */
56
+ export const dhcpRanges = (value: unknown): string =>
57
+ (Array.isArray(value) ? value : [])
58
+ .map((entry: unknown) => propertyString(entry))
59
+ .filter((entry) => entry !== '')
60
+ .sort()
61
+ .join(';');
62
+
63
+ /** The declared side of that same comparison, through the same funnel so it cannot drift. */
64
+ export const declaredRanges = (range: DhcpRange | undefined) =>
65
+ range === undefined ? '' : dhcpRanges([rangeString(range)]);
66
+
67
+ /**
68
+ * ⚠️ AN EMPTY STRING IS HOW A CALLER SPELLS "UNSET", AND IT MUST NOT REACH PVE. `gateway` and
69
+ * `dhcp-dns-server` are `format => 'ip'` and `dnszoneprefix` is `format => 'dns-name'`, so `''`
70
+ * is a parameter-verification 400 rather than a clear. Clearing is a separate verb — `delete=`.
71
+ */
72
+ const unset = (value: string | undefined) =>
73
+ value === undefined || value === '' ? undefined : value;
74
+
75
+ /**
76
+ * Every optional this resource MANAGES: the value to send, or `undefined` meaning "clear it".
77
+ *
78
+ * ⛔ ONE TABLE FEEDS BOTH HALVES OF AN UPDATE, the way metric-server-form.ts does it, and for the
79
+ * same measured reason: A PUT THAT OMITS A FIELD DOES NOT CLEAR IT. PVE merges the form into the
80
+ * existing section (`$data->{$_} = $opts->{$_} for keys $opts->%*`), so dropping `gateway` from a
81
+ * declaration leaves the old gateway in place and `matches` asks for the same update forever.
82
+ * Deriving the form and the `delete=` list from one map makes "managed but not clearable"
83
+ * impossible to write here.
84
+ *
85
+ * ⛔ AND THE TWO HALVES MUST NOT OVERLAP. MEASURED in `PVE::SectionConfig::delete_from_config`
86
+ * (SectionConfig.pm:1853): naming a key in BOTH the form and `delete=` dies with "cannot set and
87
+ * delete property '<k>' at the same time!". Because both halves are read off this one map, an
88
+ * entry is in exactly one of them by construction.
89
+ *
90
+ * ⛔ NOTHING SET OUT OF BAND MAY BE LISTED HERE. A field in this map is CLEARED the moment it is
91
+ * undeclared, so a live subnet whose DHCP pool an operator widened in the UI is narrowed back on
92
+ * the next deploy. That is full ownership, the same bargain sdn-vnet.ts strikes over `alias`, and
93
+ * it is a bargain rather than an accident only while this list stays deliberate.
94
+ *
95
+ * ⚠️ `snat` IS ABSENT ON PURPOSE AND LIVES IN `body` INSTEAD. It is a boolean, and `0` and "never
96
+ * declared" are the same subnet to PVE and to `matches`, so sending an explicit `0` converges
97
+ * without a `delete=` — the same choice sdn-vnet.ts makes for `vlanaware` and `isolate-ports`.
98
+ */
99
+ export const optional = (props: SdnSubnetProps): Record<string, string | undefined> => ({
100
+ 'dhcp-dns-server': unset(props.dhcpDnsServer),
101
+ 'dhcp-range': props.dhcpRange === undefined ? undefined : rangeString(props.dhcpRange),
102
+ dnszoneprefix: unset(props.dnszoneprefix),
103
+ gateway: unset(props.gateway),
104
+ });
105
+
106
+ /**
107
+ * The fields sent on EVERY write, create and update alike.
108
+ *
109
+ * ⚠️ `vnet` IS SENT THOUGH THE URL ALREADY CARRIES IT. PVE's handler reads `$param->{vnet}` to
110
+ * resolve the zone for its permission check, and it is a declared parameter of both the create
111
+ * and the update schema, so sending the same value the path carries is accepted and removes this
112
+ * file's dependence on the router injecting a path placeholder into the parameter hash.
113
+ */
114
+ export const body = (props: SdnSubnetProps): Record<string, string> => ({
115
+ snat: props.snat === true ? '1' : '0',
116
+ vnet: props.vnet,
117
+ ...Object.fromEntries(
118
+ Object.entries(optional(props)).filter(
119
+ (entry): entry is [string, string] => entry[1] !== undefined,
120
+ ),
121
+ ),
122
+ });
123
+
124
+ /**
125
+ * ⚠️ `type` IS HARDCODED, NOT A PROP. PVE's create schema declares it as an enum with exactly one
126
+ * member, `subnet`, so a prop for it could only ever hold one value — and the POST refuses to
127
+ * proceed without it, which is the parameter-verification error sdn-vnet.ts records.
128
+ */
129
+ export const createForm = (props: SdnSubnetProps): Record<string, string> => ({
130
+ ...body(props),
131
+ subnet: props.cidr,
132
+ type: 'subnet',
133
+ });
134
+
135
+ /**
136
+ * ★ CLEARING AN OPTION THAT WAS NEVER SET IS A SILENT NO-OP, AND THIS IS MEASURED RATHER THAN
137
+ * HOPED FOR. sdn-vnet.ts flags the same `delete=` habit as REASONED-not-measured and warns that a
138
+ * first update on a bare object would carry `delete=alias,tag`. Read off the code both endpoints
139
+ * share — `PVE::SectionConfig::delete_from_config`, SectionConfig.pm:1853-1866 — the loop dies
140
+ * only for an option that is unknown to the plugin, not optional, fixed, or also being set; for
141
+ * an option simply absent from the section it runs `delete $config->{$k}` on a missing key and
142
+ * moves on. All four keys above are `optional => 1` in `SubnetPlugin::options`, so a bare
143
+ * subnet's first update carries `delete=dhcp-dns-server,dhcp-range,dnszoneprefix,gateway` and
144
+ * PVE accepts it. `vnet` is `optional => 0` there and is never in this list.
145
+ *
146
+ * ⚠️ TWO OF THOSE FOUR NAMES ARE HYPHENATED, WHICH NO OTHER FAMILY HERE HAS PUT IN A `delete=`.
147
+ * The parameter's format is `pve-configid-list`, and a name that failed it would 400 the whole
148
+ * update rather than skip one field. MEASURED: `$CONFIGID_RE` is `qr/[a-z][a-z0-9_-]+/i`
149
+ * (JSONSchema.pm:30), so a hyphen after the first character is legal and all four pass.
150
+ */
151
+ export const updateForm = (props: SdnSubnetProps): Record<string, string> => {
152
+ const clear = Object.entries(optional(props))
153
+ .filter(([, value]) => value === undefined)
154
+ .map(([key]) => key);
155
+ return withClears(body(props), clear);
156
+ };
@@ -0,0 +1,238 @@
1
+ /**
2
+ * `Proxmox.SdnSubnet` — the addresses on a declared bridge: the CIDR, its gateway, its DHCP pool.
3
+ *
4
+ * ★ THE THIRD LINK OF ONE CHAIN, AND THE ORDER IS DATA FLOW RATHER THAN CONVENTION. A subnet
5
+ * belongs to a vnet and a vnet belongs to a zone. MEASURED and recorded in sdn-vnet.ts: creating
6
+ * a vnet is permission-checked at `/sdn/zones/-`, so a missing zone surfaces as a PERMISSION
7
+ * error rather than as "no such zone". One level deeper the same shape holds — PVE's
8
+ * `check_vnet_access` looks the vnet's ZONE up and checks `/sdn/zones/<zone>/<vnet>` — so a
9
+ * subnet declared against a vnet that does not exist yet dies inside that lookup, with the vnet
10
+ * named and the zone not. Pass the zone and vnet resources' OWN attributes into `zone` and
11
+ * `vnet`; a literal buys no ordering, and here it also buys a wrong path.
12
+ *
13
+ * ⛔ THE ID PVE FILES A SUBNET UNDER IS NOT THE ONE YOU WRITE, AND IT IS THIS FAMILY'S SHARPEST
14
+ * EDGE. `POST` takes `subnet=10.0.0.0/24` while GET, PUT and DELETE address `house-10.0.0.0-24`.
15
+ * One PVE parameter name, `subnet`, carrying two different values — so they are named APART here,
16
+ * `cidr` for the declared one and `subnetId` for the derived one, for the reason values.ts gives
17
+ * about `flag`: a name that means two things is a bug waiting for whoever reads one meaning. The
18
+ * derivation, and its measurement, live in `subnetId` in sdn-subnet-form.ts.
19
+ *
20
+ * ⛔ WHICH IS WHY `zone` IS A PROP THOUGH PVE NEVER ACCEPTS IT ON A SUBNET WRITE. The path cannot
21
+ * be built without it and it is knowable only from the VNET. Declare the wrong zone and the id is
22
+ * wrong: the POST still succeeds, because PVE computes the real id itself from the vnet, and the
23
+ * read-back then looks for a subnet that is not there. The factory's guard in resource.ts DIES on
24
+ * that first deploy — "the write returned no error but the object is still absent" — which is
25
+ * loud and immediate, and much better than the alternative it replaces: a plan that says create,
26
+ * forever, against a subnet PVE insists is already defined.
27
+ *
28
+ * ⛔ SDN WRITES ARE STAGED, NOT APPLIED — the same trap as sdn-zone.ts and sdn-vnet.ts, and it
29
+ * walks straight through the factory's read-back, which proves the staging file and nothing more.
30
+ * Nothing here applies. `Proxmox.SdnApply` (sdn-apply.ts) publishes the whole staged config with
31
+ * `PUT /cluster/sdn`; declare one and pass this resource's `subnetId` in its `after`.
32
+ *
33
+ * ⛔ AND `Proxmox.SdnApply` CANNOT YET SEE A STAGED SUBNET, SO ITS PLAN WILL LIE ABOUT THIS FAMILY.
34
+ * MEASURED: its `COLLECTIONS` list is `cluster/sdn/zones` and `cluster/sdn/vnets`, while
35
+ * `PVE::Network::SDN::pending_config` keys the pending view on a SEPARATE `subnets` section of
36
+ * the running config. A run that stages only a subnet therefore leaves both counted collections
37
+ * at zero, the apply diffs as `noop`, and the subnet never reaches a node. It is NOT fixable by
38
+ * adding a string to that list: the subnet index is PER VNET
39
+ * (`cluster/sdn/vnets/{vnet}/subnets?pending=1`), so SdnApply needs the vnet names before it can
40
+ * count them. Until that lands, a subnet-only change needs `pvesh set /cluster/sdn` by hand.
41
+ *
42
+ * ★ THE READ LANE IS ENOUGH HERE, AND THAT IS A REAL DIFFERENCE FROM BOTH SIBLINGS. The zone and
43
+ * vnet ITEM reads are gated on SDN.Allocate, which is why each sets `readRole: 'provision'`. This
44
+ * one is not. MEASURED from the cluster's own schema on 2026-09-13:
45
+ *
46
+ * GET /cluster/sdn/vnets/{vnet}/subnets/{subnet}
47
+ * -> "Require 'SDN.Audit' or 'SDN.Allocate' permissions on '/sdn/zones/<zone>/<vnet>'"
48
+ *
49
+ * and `GET /access/acl` the same day shows `hf-read@pve` holding the built-in PVEAuditor — whose
50
+ * seven privileges include SDN.Audit — at `/` with propagate=1. So the default 3600s auditor
51
+ * lease reads this family, and a `readRole: 'provision'` here would be privilege nobody needed.
52
+ * Writes are a different answer: POST, PUT and DELETE all require SDN.Allocate on
53
+ * `/sdn/zones/<zone>/<vnet>`, which `LXCProvisioner` gained on 2026-09-13.
54
+ *
55
+ * ⚠️ NO LIVE SUBNET EXISTS ON TB4 TO ROUND-TRIP AGAINST — there are no SDN zones and no vnets, so
56
+ * there can be no subnets. Every claim above is read off PVE 9.2.11's own schema and Perl sources
57
+ * on n2, and the noop argument below rests on those rather than on a plan this file has watched.
58
+ *
59
+ * ⚠️ TWO REFUSALS TO EXPECT, both from `SubnetPlugin::on_update_hook`: PVE will not put a subnet on
60
+ * a vnet with `vlanaware` set ("you can't add a subnet on a vlanaware vnet"), and it rejects a
61
+ * gateway outside the CIDR unless the mask is /32. Both are create-time 400s, not drift.
62
+ */
63
+ import { Resource } from 'alchemy';
64
+ import * as Provider from 'alchemy/Provider';
65
+ import * as Effect from 'effect/Effect';
66
+ import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
67
+ import {
68
+ type DhcpRange,
69
+ createForm,
70
+ declaredRanges,
71
+ dhcpRanges,
72
+ subnetId,
73
+ updateForm,
74
+ } from './sdn-subnet-form.ts';
75
+ import { bool, int, text } from './values.ts';
76
+
77
+ export interface SdnSubnetProps extends WithTarget {
78
+ /**
79
+ * The vnet whose bridge carries these addresses. Identity AND a written field.
80
+ *
81
+ * ⛔ MOVING A SUBNET BETWEEN VNETS IS NOT SOMETHING THIS RESOURCE CAN DO, and the symptom is
82
+ * worth knowing before you meet it. The id is built from the zone and the CIDR, so it does not
83
+ * change; but the read goes to `.../vnets/<declared>/subnets/<id>`, and PVE answers a subnet
84
+ * filed under another vnet with `raise_param_exc({ vnet => "wrong vnet" })`. `read` folds that
85
+ * into "absent", reconcile POSTs, and PVE refuses with "sdn subnet object ID ... already
86
+ * defined". Loud and accurate, but it does not converge: change the vnet by removing the
87
+ * declaration and writing a new one.
88
+ */
89
+ vnet: string;
90
+ /**
91
+ * The zone that vnet lives in. ⚠️ PATH INPUT ONLY — PVE accepts no `zone` on a subnet write, and
92
+ * nothing here sends one. It is a prop because the path cannot be spelled without it.
93
+ */
94
+ zone: string;
95
+ /** `10.0.0.0/24`. Identity: PVE builds the id from it, so a new CIDR is a new subnet. */
96
+ cidr: string;
97
+ /** Handed to guests as their default route, and registered in the zone's IPAM as `<vnet>-gw`. */
98
+ gateway?: string;
99
+ /**
100
+ * Masquerade this subnet. ⚠️ INERT UNTIL THE CLUSTER FIREWALL IS ON — PVE's own description is
101
+ * "enable masquerade for this subnet if pve-firewall", and TB4's cluster firewall was empty and
102
+ * disabled when this was written. It still round-trips, so declaring it does not diff.
103
+ */
104
+ snat?: boolean;
105
+ /** `adm` -> `<hostname>.adm.example.internal` for records the zone's DNS plugin registers. */
106
+ dnszoneprefix?: string;
107
+ /** ⚠️ WIRE NAME `dhcp-dns-server`; see the ⚠️ on `isolatePorts` in sdn-vnet.ts about mappings. */
108
+ dhcpDnsServer?: string;
109
+ /**
110
+ * ⛔ ONE POOL, NOT A LIST, AND THE LIMIT IS THE CLIENT'S RATHER THAN PVE'S. `dhcp-range` is a PVE
111
+ * `type => 'array'` parameter, and an array reaches a form-encoded body only as a REPEATED KEY
112
+ * — which `client.ts` cannot express, because `pve()` takes `Record<string, string>` and one
113
+ * key holds one value. A single occurrence works today because `PVE::RESTHandler`'s
114
+ * `$normalize_legacy_param_formats` wraps a scalar into `[$value]` for an array parameter, so
115
+ * the type is declared as one range and a second one is unspellable rather than silently lost.
116
+ * ⛔ THAT COERCION IS MARKED FOR REMOVAL. Its own comment in RESTHandler.pm reads "mark the
117
+ * behaviour deprecated with 9.x, and remove it with 10.x", and this cluster runs 9.2.11 — so on
118
+ * PVE 10 every write carrying a range becomes a hard 400. The fix is not here: it is a form
119
+ * type in client.ts that admits repeated keys.
120
+ * ⚠️ A LIVE SUBNET CARRYING TWO POOLS IS THEREFORE NARROWED TO ONE by the first deploy that
121
+ * declares one. `matches` compares the WHOLE live list, so the narrowing is visible in the plan
122
+ * as an update rather than happening quietly, and it converges.
123
+ */
124
+ dhcpRange?: DhcpRange;
125
+ }
126
+
127
+ /**
128
+ * ⛔ NO `digest` HERE, for the reason sdn-zone.ts gives: PVE returns the digest of the WHOLE
129
+ * subnets.cfg, so declaring a SECOND subnet would rewrite this one's stored attributes and
130
+ * comparing it would report an update on a subnet nobody touched.
131
+ */
132
+ export interface SdnSubnetAttributes {
133
+ /** `<zone>-<network>-<mask>` — the path segment, and the name to use in `pvesh`. */
134
+ subnetId: string;
135
+ vnet: string;
136
+ zone: string;
137
+ /** `10.0.0.0/24`, as PVE reassembles it from the id. */
138
+ cidr: string;
139
+ /** Prefix length, so a guest's `ip=10.0.0.5/24` can be built from a reference. */
140
+ mask: number;
141
+ gateway: string;
142
+ snat: boolean;
143
+ dnszoneprefix: string;
144
+ dhcpDnsServer: string;
145
+ /** Normalised and sorted — never compare a raw PVE value to it. See `dhcpRanges`. */
146
+ dhcpRange: string;
147
+ /** ⚠️ THE REMINDER IN THE STATE ITSELF: staged config. No value here says a node has the subnet. */
148
+ readonly staged: true;
149
+ }
150
+
151
+ export interface ProxmoxSdnSubnet extends Resource<
152
+ 'Proxmox.SdnSubnet',
153
+ SdnSubnetProps,
154
+ SdnSubnetAttributes,
155
+ never,
156
+ PveRequirements
157
+ > {}
158
+
159
+ /**
160
+ * ⚠️ NO `defaultRemovalPolicy: 'retain'`, UNLIKE THE SEVEN FAMILIES THAT CARRY IT, AND THE ★ IN
161
+ * resource.ts IS THE TEST IT FAILS: a subnet's whole content is a CIDR, a gateway and a pool,
162
+ * which a line of TypeScript rebuilds exactly. The part that is NOT rebuildable is the guest
163
+ * addresses the zone's IPAM handed out, and PVE guards that itself — MEASURED in the built-in
164
+ * `pve` IPAM plugin, which answers a delete with "cannot delete subnet '<cidr>', not empty"
165
+ * whenever anything beyond the gateway still holds an address. That refusal is surfaced as-is.
166
+ */
167
+ export const ProxmoxSdnSubnet = Resource<ProxmoxSdnSubnet>('Proxmox.SdnSubnet');
168
+
169
+ const handlers = pveHandlers<SdnSubnetProps, SdnSubnetAttributes>({
170
+ attributes: (live, props) => {
171
+ /**
172
+ * ⚠️ UNREACHABLE ON A HEALTHY CLUSTER, AND KEPT BECAUSE "UNREACHABLE" IS A FACT ABOUT TODAY'S
173
+ * PVE. The read handler already refuses a subnet filed under another vnet, so this row should
174
+ * never arrive — see the ⛔ on `vnet` above for what that refusal costs. If a future PVE
175
+ * answers instead of raising, reporting the foreign subnet as absent makes reconcile POST and
176
+ * PVE say "already defined": still loud, rather than a silent noop over somebody else's
177
+ * addresses.
178
+ */
179
+ const liveVnet = text(live['vnet']);
180
+ if (liveVnet !== '' && liveVnet !== props.vnet) return undefined;
181
+ return {
182
+ cidr: text(live['cidr']),
183
+ dhcpDnsServer: text(live['dhcp-dns-server']),
184
+ dhcpRange: dhcpRanges(live['dhcp-range']),
185
+ dnszoneprefix: text(live['dnszoneprefix']),
186
+ gateway: text(live['gateway']),
187
+ mask: int(live['mask'], 0),
188
+ snat: bool(live['snat']),
189
+ staged: true,
190
+ subnetId: subnetId(props),
191
+ vnet: props.vnet,
192
+ zone: text(live['zone']),
193
+ };
194
+ },
195
+ collection: (props) => `cluster/sdn/vnets/${props.vnet}/subnets`,
196
+ createForm,
197
+ /**
198
+ * ⛔ FIVE REPORTED FIELDS ARE DELIBERATELY ABSENT FROM THIS COMPARISON, and each is a
199
+ * forever-diff if it goes back in.
200
+ * `subnetId`, `cidr`, `zone` and `mask` are all DERIVED BY PVE FROM THE ID — measured in
201
+ * `Subnets::sdn_subnets_config`, which splits the id and synthesises cidr/zone/network/mask on
202
+ * every read. None of the four is a POST or PUT parameter, so a difference could never be
203
+ * written; and since the id IS the path this read used, a difference cannot arise either — the
204
+ * GET would have failed and the factory would report "absent" instead.
205
+ * `vnet` is the FILTER that produced this row rather than a reading of it: PVE refuses to
206
+ * answer with a subnet on another vnet, so comparing it can only ever be true.
207
+ * `staged` is a fact about the cluster, not about the declaration: diffing it would report an
208
+ * update on every plan until somebody applied, then another the moment anyone staged anything.
209
+ * `digest` is not even reported — see the ⛔ above the attributes.
210
+ */
211
+ matches: (attributes, props) =>
212
+ attributes.gateway === (props.gateway ?? '') &&
213
+ attributes.snat === (props.snat === true) &&
214
+ attributes.dnszoneprefix === (props.dnszoneprefix ?? '') &&
215
+ attributes.dhcpDnsServer === (props.dhcpDnsServer ?? '') &&
216
+ attributes.dhcpRange === declaredRanges(props.dhcpRange),
217
+ /**
218
+ * ⚠️ NO `?pending=1` AND NO `?running=1`, HOWEVER TEMPTING — the same refusal sdn-vnet.ts makes.
219
+ * The factory uses this one path for GET, PUT and DELETE alike, every one of those schemas
220
+ * declares `additionalProperties => 0`, and PVE answers an unexpected parameter on a write with
221
+ * "parameter verification failed". Staged-ness is documented at the top of this file instead.
222
+ */
223
+ path: (props) => `cluster/sdn/vnets/${props.vnet}/subnets/${subnetId(props)}`,
224
+ updateForm,
225
+ });
226
+
227
+ /**
228
+ * ⛔ `list` IS EMPTY LIKE EVERY OTHER RESOURCE HERE. `GET /cluster/sdn/vnets/{vnet}/subnets` answers
229
+ * with every subnet on that bridge, guest addresses and all; adopting one would invite Alchemy to
230
+ * narrow or delete a network nobody declared. Adoption stays an explicit act.
231
+ *
232
+ * ⛔ AND A DELETE IS STAGED TOO, WHICH IS THE HALF PEOPLE FORGET. The section leaves subnets.cfg and
233
+ * Alchemy drops the resource, but the gateway stays on the bridge and DHCP keeps answering until
234
+ * something applies `PUT /cluster/sdn`: a destroy nobody applies reads as complete in the plan and
235
+ * has not happened on the cluster.
236
+ */
237
+ export const ProxmoxSdnSubnetProvider = () =>
238
+ Provider.effect(ProxmoxSdnSubnet, Effect.succeed(ProxmoxSdnSubnet.Provider.of(handlers)));
@@ -0,0 +1,228 @@
1
+ /**
2
+ * `Proxmox.SdnVnet` — the bridge a guest actually attaches to.
3
+ *
4
+ * ★ THIS IS THE OBJECT THAT CLOSES THE GAP BETWEEN A DECLARED CONTAINER AND A DECLARED NETWORK.
5
+ * `Proxmox.Lxc` takes `net0: name=eth0,bridge=vmbr0,ip=dhcp`, in which the bridge is a string
6
+ * nothing checks. A vnet's id IS the bridge PVE builds on every node of its zone, so declaring
7
+ * the vnet and passing its `bridge` attribute into `net0` turns that string into a reference
8
+ * Alchemy can order and a plan can show.
9
+ *
10
+ * ⚠️ MEASURED: CREATING A VNET CHECKS PERMISSION AT `/sdn/zones/-`, NOT AT THE VNET. The ACL path
11
+ * is the ZONE's, so a vnet whose zone does not exist yet fails as a PERMISSION error rather than
12
+ * as "no such zone" — and you go off widening a role that was never the problem. `zone` is
13
+ * therefore a hard dependency edge, not a convention: pass the zone resource's own attribute so
14
+ * Alchemy creates the zone first. A zone name typed in as a literal buys no ordering at all.
15
+ *
16
+ * ⛔ THE READ-BACK AFTER A WRITE PROVES THE STAGING FILE AND NOTHING MORE — the same trap as
17
+ * `sdn-zone`, and it walks straight through the factory's guard. PVE's SDN endpoints write
18
+ * `/etc/pve/sdn/vnets.cfg`; nothing reaches a node's `/etc/network/interfaces` until
19
+ * `PUT /cluster/sdn` applies the staged config. `pveOperations.reconcile` reads back and refuses
20
+ * when an object is absent, and a vnet that exists only on paper SATISFIES that read: reconcile
21
+ * reports success, the next plan says noop, and no bridge exists anywhere on the cluster.
22
+ * This resource does NOT issue the apply, deliberately. One vnet's reconcile cannot decide to
23
+ * commit every other pending SDN change in the cluster, including ones a human staged in the UI
24
+ * and has not finished. Apply stays an operator's act — `pvesh set /cluster/sdn`.
25
+ *
26
+ * ★ SUBNETS ARE A SIBLING RESOURCE, NOT A FIELD HERE. The gateway and the DHCP ranges guests take
27
+ * addresses from hang off `cluster/sdn/vnets/{vnet}/subnets`, which MEASURED refuses a POST that
28
+ * omits `type` or `subnet` ("parameter verification failed"). Same factory, one path deeper, and
29
+ * it depends on this resource the way this one depends on the zone.
30
+ *
31
+ * ⛔ read/diff NEED SDN.Allocate TOO, NOT SDN.Audit — corrected 2026-09-13 against the cluster's
32
+ * schema. `GET /cluster/sdn/vnets/{vnet}` is checked against SDN.Allocate exactly as the zone
33
+ * item endpoint is, while only the COLLECTION read accepts SDN.Audit. An auditor-shaped `read`
34
+ * lease is therefore refused, and `pveOperations.read` turns that refusal into "absent" rather
35
+ * than an error — see the ⛔ in sdn-zone.ts and docs/privileges.md.
36
+ *
37
+ * ⚠️ PRIVILEGES, AND THE ESTATE'S CREDENTIAL HELD NONE OF THEM UNTIL 2026-09-13. reconcile and
38
+ * delete mint `provision` and need SDN.Allocate on `/sdn/zones`, or on the one zone if scoped. `LXCProvisioner` grants
39
+ * SDN.Use, which is what a GUEST needs in order to attach to a vnet and NOT what creating one
40
+ * needs, so a container declared on this vnet works today while the vnet itself 403s. Widen
41
+ * deliberately, the way Pool.Allocate was:
42
+ * `pveum role modify LXCProvisioner --privs "<existing>,SDN.Allocate,SDN.Audit"` over SSH,
43
+ * preserving every existing privilege.
44
+ */
45
+ import { Resource } from 'alchemy';
46
+ import * as Provider from 'alchemy/Provider';
47
+ import * as Effect from 'effect/Effect';
48
+ import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
49
+ import { bool, withClears } from './values.ts';
50
+
51
+ export interface SdnVnetProps extends WithTarget {
52
+ /**
53
+ * ⛔ THE PRIMARY KEY AND THE BRIDGE NAME AT ONCE. PVE caps it at eight alphanumeric characters
54
+ * because it becomes a real interface on every node in the zone. There is no rename: a new id
55
+ * is a new bridge, and every guest whose `net0` still names the old one stays on the old one.
56
+ */
57
+ vnet: string;
58
+ /**
59
+ * The SDN zone that owns it. Mutable — a vnet can be moved between zones — but see the ⚠️ at the
60
+ * top of this file: the zone has to exist first, and passing a zone resource's attribute instead
61
+ * of a literal is the only thing that makes Alchemy order them.
62
+ */
63
+ zone: string;
64
+ /** Free text shown in the UI. Mutable; clear it by dropping the prop, never by passing `''`. */
65
+ alias?: string;
66
+ /** VLAN id or VXLAN VNI, depending on the zone's type. Mutable. Absent means untagged. */
67
+ tag?: number;
68
+ /** Let guests on this bridge carry their own VLAN tags. Mutable. */
69
+ vlanaware?: boolean;
70
+ /**
71
+ * Stop guests on this bridge from reaching each other directly. Mutable.
72
+ * ⚠️ The wire name is `isolate-ports`, with a hyphen — see the ⚠️ on `shape` for why that costs
73
+ * attention in two places rather than one.
74
+ */
75
+ isolatePorts?: boolean;
76
+ }
77
+
78
+ export interface SdnVnetAttributes {
79
+ vnet: string;
80
+ /**
81
+ * The same string as `vnet`, duplicated on purpose: it is the name a guest's `net0` must carry.
82
+ * Referencing it from a `Proxmox.Lxc`'s props is what makes Alchemy build the network before the
83
+ * container. A hardcoded `vmbr1` in `net0` is a string nothing checks and nothing orders.
84
+ */
85
+ bridge: string;
86
+ zone: string;
87
+ alias: string;
88
+ /**
89
+ * Absent when the vnet is untagged — see `tagOf` for why this is optional and not `0`.
90
+ * ⚠️ `| undefined` IS LOAD-BEARING under exactOptionalPropertyTypes: the attributes
91
+ * builder always sets this key, writing `undefined` for an untagged vnet, so the type
92
+ * must permit a present-but-undefined value as well as an absent one. Without it the
93
+ * read path does not typecheck, and 0 is a real VLAN id so it cannot be the sentinel.
94
+ */
95
+ tag?: number | undefined;
96
+ vlanaware: boolean;
97
+ isolatePorts: boolean;
98
+ }
99
+
100
+ export interface ProxmoxSdnVnet extends Resource<
101
+ 'Proxmox.SdnVnet',
102
+ SdnVnetProps,
103
+ SdnVnetAttributes,
104
+ never,
105
+ PveRequirements
106
+ > {}
107
+
108
+ export const ProxmoxSdnVnet = Resource<ProxmoxSdnVnet>('Proxmox.SdnVnet');
109
+
110
+ /**
111
+ * ⚠️ AN ABSENT TAG IS `undefined`, NOT 0, which is why `SdnVnetAttributes.tag` is optional rather
112
+ * than falling back the way `memory` does in lxc.ts. VLAN id 0 and VXLAN VNI 0 are real values,
113
+ * so a 0 fallback would report an untagged vnet as tagged 0 and then diff forever against a
114
+ * declaration that asks for no tag at all.
115
+ */
116
+ const tagOf = (value: unknown) => (typeof value === 'number' ? value : undefined);
117
+
118
+ /**
119
+ * The fields PVE accepts on create and on update alike.
120
+ *
121
+ * ⚠️ THE WIRE NAME IS `isolate-ports`, WITH A HYPHEN; the prop is `isolatePorts` so no call site
122
+ * has to quote it. Both halves of that mapping have to be right: the form key here AND the read
123
+ * in `attributes`. `live['isolatePorts']` would be `undefined` forever, which reads as "the
124
+ * cluster does not have it set" and makes every plan ask for the same update, for good.
125
+ */
126
+ const shape = (props: SdnVnetProps): Record<string, string> => {
127
+ const alias = props.alias ?? '';
128
+ return {
129
+ // ⚠️ THE TWO BOOLEANS ARE ALWAYS SENT, `0` INCLUDED, AND THAT IS NOT AN OVERSIGHT. `0` and
130
+ // "never declared" mean the same thing to PVE and to `matches` below, so an explicit `0`
131
+ // makes the two converge — which is why neither ever appears in the `delete` list.
132
+ 'isolate-ports': props.isolatePorts === true ? '1' : '0',
133
+ vlanaware: props.vlanaware === true ? '1' : '0',
134
+ zone: props.zone,
135
+ // ⚠️ AN EMPTY ALIAS IS OMITTED, NOT SENT AS `''`. `''` is how this file spells "unset", and
136
+ // unset goes through the `delete` parameter below; sending it as a value would ask PVE to
137
+ // store an empty description, which is a different request from having none.
138
+ ...(alias === '' ? {} : { alias }),
139
+ ...(props.tag === undefined ? {} : { tag: String(props.tag) }),
140
+ };
141
+ };
142
+
143
+ /**
144
+ * ⛔ OMITTING A FIELD DOES NOT CLEAR IT. PVE keeps whatever the config already holds, so a
145
+ * declaration that DROPS its alias would diff as `update` forever: `matches` compares `''`
146
+ * against the live text, the PUT says nothing about alias, and the next plan asks for the very
147
+ * same update. PVE's own answer is the `delete` parameter — a comma-separated list of properties
148
+ * to unset — which exists on the update endpoint only. That is why create does not come through
149
+ * here and calls `shape` directly.
150
+ *
151
+ * ⚠️ REASONED FROM PVE'S UPDATE SCHEMA, NOT MEASURED ON A CLUSTER — unlike the two MEASURED facts
152
+ * at the top of this file, and the difference is worth a reader's attention. `delete` is what
153
+ * `SectionConfig::updateSchema` adds to every PVE update endpoint, and unsetting a property that
154
+ * was never set is a no-op there. If THIS endpoint turns out to disagree, the blast radius is
155
+ * wider than the feature: a plain `{ vnet, zone }` vnet has neither alias nor tag, so its very
156
+ * first update carries `delete=alias,tag` and every update fails, not just an unset. The symptom
157
+ * is a 400 "parameter verification failed" naming `delete`; the fix is to drop this list and
158
+ * accept the forever-update on unset instead. Verify it once against a live vnet.
159
+ */
160
+ const updateForm = (props: SdnVnetProps): Record<string, string> => {
161
+ const unset = [
162
+ ...((props.alias ?? '') === '' ? ['alias'] : []),
163
+ ...(props.tag === undefined ? ['tag'] : []),
164
+ ];
165
+ return withClears(shape(props), unset);
166
+ };
167
+
168
+ const handlers = pveHandlers<SdnVnetProps, SdnVnetAttributes>({
169
+ // ⛔ PVE gates this family's ITEM read on the allocate privilege, not the audit one — see
170
+ // `readRole` in resource.ts. The auditor-shaped lease reads 403, which `read` turns into
171
+ // "absent", and the plan then says create for an object that is plainly there.
172
+ readRole: 'provision',
173
+ attributes: (live, props) => ({
174
+ alias: typeof live['alias'] === 'string' ? live['alias'] : '',
175
+ bridge: props.vnet,
176
+ isolatePorts: bool(live['isolate-ports']),
177
+ tag: tagOf(live['tag']),
178
+ vlanaware: bool(live['vlanaware']),
179
+ vnet: props.vnet,
180
+ /**
181
+ * ⚠️ `''` RATHER THAN `props.zone` WHEN PVE DOES NOT SAY. Echoing the declared zone back would
182
+ * make an unreadable zone look like agreement; `''` matches no declared zone, so the plan
183
+ * reports drift instead. A vnet in the wrong zone is attached to the wrong network, and this
184
+ * is the one field here where a spurious update costs less than a missed one.
185
+ */
186
+ zone: typeof live['zone'] === 'string' ? live['zone'] : '',
187
+ }),
188
+ collection: () => 'cluster/sdn/vnets',
189
+ createForm: (props) => ({ ...shape(props), vnet: props.vnet }),
190
+ /**
191
+ * ⚠️ `vnet`, `bridge` AND WHETHER THE CONFIG IS APPLIED ARE ABSENT FROM THIS COMPARISON ON
192
+ * PURPOSE. The first two are the path — a changed id is a different object, not an edit — and
193
+ * staged-vs-running is a fact about the cluster rather than about the declaration. Diffing it
194
+ * would report an update on every plan until somebody ran the apply, and then report another
195
+ * one the moment anybody else staged anything at all.
196
+ */
197
+ matches: (attributes, props) =>
198
+ attributes.zone === props.zone &&
199
+ attributes.alias === (props.alias ?? '') &&
200
+ attributes.tag === props.tag &&
201
+ attributes.vlanaware === (props.vlanaware === true) &&
202
+ attributes.isolatePorts === (props.isolatePorts === true),
203
+ /**
204
+ * ⚠️ NO QUERY STRING HERE, HOWEVER TEMPTING. `?pending=1` would surface whether the staged
205
+ * config has been applied, but the factory uses this one path for GET, PUT and DELETE alike,
206
+ * and PVE answers an unexpected parameter on a write with "parameter verification failed".
207
+ * That is why staged-ness is documented at the top of this file rather than made an attribute.
208
+ */
209
+ path: (props) => `cluster/sdn/vnets/${props.vnet}`,
210
+ updateForm,
211
+ });
212
+
213
+ /**
214
+ * ⛔ EMPTY, LIKE EVERY OTHER RESOURCE IN THIS PACKAGE. `GET /cluster/sdn/vnets` answers
215
+ * with every vnet on the cluster, and on an estate already running SDN those are
216
+ * load-bearing bridges with guests on them. Returning them would invite Alchemy to
217
+ * adopt — and therefore one day delete — a network nobody declared here. Adoption is an
218
+ * explicit act.
219
+
220
+ *
221
+ * ⛔ A DELETE IS STAGED LIKE EVERY OTHER SDN WRITE. The vnet leaves `vnets.cfg` at once and
222
+ * the bridge stays up on every node until `PUT /cluster/sdn` applies the removal, so a
223
+ * plan reporting "1 deleted" has taken nothing off the network yet. Nothing here applies
224
+ * on the operator's behalf: see the ⛔ at the top of this file.
225
+
226
+ */
227
+ export const ProxmoxSdnVnetProvider = () =>
228
+ Provider.effect(ProxmoxSdnVnet, Effect.succeed(ProxmoxSdnVnet.Provider.of(handlers)));