@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,248 @@
1
+ /**
2
+ * `Proxmox.ReplicationJob` — a guest's ZFS storage replication to a second node, declared.
3
+ *
4
+ * ⛔ NOTHING ON TB4 CAN USE THIS FAMILY, AND THAT IS SAID HERE RATHER THAN FOUND OUT LATER. PVE
5
+ * storage replication is ZFS-only: `POST /cluster/replication` calls `get_replicatable_volumes`
6
+ * and refuses a guest whose disks are anywhere else with "No replicatable volumes found". TB4 is
7
+ * Ceph/RBD-backed — shared storage, where replication means nothing because every node already
8
+ * sees the volume — so `GET /cluster/replication` answers `[]` (MEASURED on n2, 2026-09-13) and
9
+ * on this cluster always will. The file is written for a ZFS cluster; against TB4 a declaration
10
+ * is a create that fails with PVE's own sentence, not a plan that quietly does nothing.
11
+ *
12
+ * ★ WHAT IS MEASURED HERE AND WHAT IS REASONED. Measured on n2 (pve-manager 9.2.11), read-only:
13
+ * the published schema for all five methods; `PVE/ReplicationConfig.pm` and
14
+ * `PVE/API2/ReplicationConfig.pm` line by line; `GET /cluster/replication` -> `[]`; and the ACL
15
+ * row `{"path":"/","propagate":1,"roleid":"PVEAuditor","ugid":"hf-read@pve"}`. NOT measured: any
16
+ * round trip. There is no live job to adopt and none can be created on this cluster, so the
17
+ * argument that a matching declaration plans as `noop` rests on the schema and on PVE's own
18
+ * source — not on a plan anybody ran. Nobody should claim otherwise until a ZFS cluster exists.
19
+ *
20
+ * ⛔ THE ID IS STRUCTURED AND PVE RE-SPELLS IT, so this resource is keyed on the two numbers rather
21
+ * than on the string — a deliberate break from `backup-job.ts`'s "props are PVE's own key names".
22
+ * MEASURED: `parse_replication_job_id` matches `^(\d+)-(\d+)$`, runs BOTH halves through `int()`
23
+ * and returns "$guest-$jobnum", and `parse_section_header` does the same when reading
24
+ * replication.cfg. So a declared `100-007` is read back as `100-7`, and a provider holding the
25
+ * raw string would compare two spellings of one id forever. Two numbers cannot be spelled two
26
+ * ways. PVE also refuses `guest < 100` ("guest IDs < 100 are reserved") and the API pattern
27
+ * `[1-9][0-9]{2,8}-\d{1,9}` caps both halves at nine digits.
28
+ * ⛔ BOTH HALVES ARE REQUIRED AND NEITHER IS DEFAULTED, for `backup-job.ts`'s reason: a provider
29
+ * that picks a job number picks a DIFFERENT job from the one a person meant, and then adopts or
30
+ * creates beside the real one. Declare the numbers of the job you mean.
31
+ *
32
+ * ⛔ PVE'S `target` AND THIS PACKAGE'S `target` ARE DIFFERENT THINGS, AND THE COMPILER SAID SO
33
+ * BEFORE A REVIEWER DID. Every resource here extends `WithTarget`, whose `target` is the CLUSTER
34
+ * a call goes to (`PveTarget`, carrying the API base and the credential mount). PVE's replication
35
+ * `target` is a NODE NAME. They cannot both be called `target` in one props interface — tsc:
36
+ * "Interface 'ReplicationJobProps' incorrectly extends interface 'WithTarget'" — so the prop and
37
+ * the attribute are `targetNode`, and PVE's spelling appears exactly once, in `createBody`, where
38
+ * it is the wire key. That is the only safe shape: had the collision been resolvable by silence,
39
+ * `createForm` would have serialised a whole `PveTarget` object into a form field called `target`
40
+ * and PVE would have answered that the node does not exist.
41
+ *
42
+ * ⛔ ONE JOB PER (GUEST, TARGET), ENFORCED BY PVE AND NOT BY THIS FILE. MEASURED: `write_config`
43
+ * dies "replication job for guest '$vmid' to target '$tid' already exists" where `$tid` is
44
+ * `local/<target>`, and `parse_config` DELETES the loser on read if two ever reach the file. Two
45
+ * declarations differing only in `jobnum` but naming the same target are not two jobs; the second
46
+ * is a create that fails.
47
+ *
48
+ * ⚠️ RECONCILE NEEDS `VM.Replicate` ON `/vms/<guest>`, WHICH `LXCProvisioner` DOES NOT HOLD. All
49
+ * three writes check it (MEASURED in API2/ReplicationConfig.pm: create, update and delete each
50
+ * call `$rpcenv->check($authuser, "/vms/$vmid", ['VM.Replicate'])`), and the role's 27 privileges
51
+ * include `VM.Allocate`, `VM.Audit`, `VM.Backup`, `VM.Config.*` and `VM.PowerMgmt` but not
52
+ * `VM.Replicate`. Widen it deliberately over SSH, preserving what is there, as `pool.ts` records:
53
+ * `pveum role modify LXCProvisioner --privs "<existing>,VM.Replicate"`.
54
+ * ★ THE READ LANE NEEDS NOTHING EXTRA, which is why there is no `readRole` below. The ITEM read
55
+ * checks `VM.Audit` on `/vms/<guest>`, `hf-read@pve` holds the built-in `PVEAuditor` at `/`
56
+ * with propagate — both MEASURED — and `PVEAuditor` carries `VM.Audit`. This family is not a
57
+ * fourth one that must borrow the provision lease.
58
+ * ⚠️ A MISSING JOB IS A 500, NOT A 404 — "no such replication job '100-0'" — and the factory
59
+ * folds any failed read into "absent", so a 403 reads as absent too. A plan insisting on
60
+ * creating a job that plainly exists is a credential problem; the honest error arrives one step
61
+ * later, from the create.
62
+ *
63
+ * ⛔ DELETE DOES NOT DELETE, AND A DESTROY IS NOT FREE. MEASURED: with neither `force` nor `keep` —
64
+ * and the factory's `destroy` sends no form at all — the handler only sets `remove_job = 'full'`
65
+ * on the job and writes the config back. The job stays in replication.cfg until the SOURCE node's
66
+ * `pvesr` timer next runs it, at which point it removes the local replication snapshots, removes
67
+ * the replicated volumes on the target, and finally removes itself. So a destroy costs the
68
+ * standby copy and the incremental base: re-declaring the job afterwards is a FULL send, not a
69
+ * resume. The guest's own disks are untouched, which is exactly why this family does NOT carry
70
+ * `defaultRemovalPolicy: 'retain'` the way `ZfsPool` and `Storage` do — what it destroys is a
71
+ * copy a re-sync rebuilds, expensively, rather than something irreplaceable.
72
+ * ⚠️ AND IF THE SOURCE NODE IS DOWN THE REMOVAL NEVER RUNS. Alchemy drops the state row on a
73
+ * DELETE that returned no error, so a destroy against a dead source leaves a marked job on the
74
+ * cluster that nothing declares any more. `GET /cluster/replication` is where it shows up.
75
+ *
76
+ * ⚠️ THE RUNTIME STATUS IS NOT HERE, AND NOT BECAUSE IT WAS FORGOTTEN. `last_sync`, `last_try`,
77
+ * `fail_count`, `error`, `duration` and `pid` come from the per-node replication STATE file and
78
+ * are merged in by `GET /nodes/{node}/replication` (MEASURED: API2/Replication.pm line 179,
79
+ * `foreach my $k (qw(last_sync last_try fail_count error duration))`). `GET
80
+ * /cluster/replication/{id}` — the only endpoint this resource reads — returns not one of them.
81
+ * Reporting them would mean a second, per-node call on the read path answering a different value
82
+ * every fifteen minutes, and a plan whose output changes when nothing changed is a plan people
83
+ * stop reading. Ask the node for status; ask the cluster for configuration.
84
+ */
85
+ import { Resource } from 'alchemy';
86
+ import * as Provider from 'alchemy/Provider';
87
+ import * as Effect from 'effect/Effect';
88
+ import { DEFAULT_SCHEDULE, createBody, jobId, updateBody } from './replication-job-form.ts';
89
+ import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
90
+ import { bool, int, text } from './values.ts';
91
+
92
+ export interface ReplicationJobProps extends WithTarget {
93
+ /** ⛔ The guest being replicated. 100 or above — PVE reserves everything below. Identity. */
94
+ guest: number;
95
+ /** ⛔ The job number within that guest, 0 upwards. Identity. Never generated — see the header. */
96
+ jobnum: number;
97
+ /**
98
+ * ⛔ THE NODE THE COPY LANDS ON — PVE's `target`, RENAMED; see the ⛔ in the header — AND IT IS
99
+ * CREATE-ONLY. MEASURED: the plugin declares `target => { fixed => 1, optional => 0 }`, so
100
+ * `updateSchema()` omits it entirely — `PUT` has no `target` parameter at all, and
101
+ * `delete=target` answers "unable to delete fixed option". It is therefore NOT compared; see
102
+ * the ⚠️ on `matches`. Changing it in a declaration plans as NOOP. Retire the declaration and
103
+ * add another, exactly as `user.ts` says for `userid`.
104
+ * ⚠️ PVE refuses a target equal to the guest's current node ("Source and target must not be
105
+ * identical") and one that is not a cluster member. Neither is checkable from here.
106
+ */
107
+ targetNode: string;
108
+ /**
109
+ * A subset of systemd calendar events. Unset means PVE's own default — `DEFAULT_SCHEDULE`,
110
+ * every fifteen minutes — and that default is what gets SENT and compared, not silence.
111
+ * ⚠️ THIS COMPARES STRINGS, NOT MEANINGS. A calendar event has many equivalent spellings and
112
+ * PVE stores the one it was handed, verbatim, so declare the spelling the cluster echoes.
113
+ */
114
+ schedule?: string;
115
+ /** Rate limit in mbps as a float, 1 or above. Unset sends nothing and compares nothing. */
116
+ rate?: number;
117
+ /** Keep the job but stop replicating. Unset is an ACTIVE job, and that is what is compared. */
118
+ disable?: boolean;
119
+ /** Free text shown in the UI. Round-trips through PVE's `encode_text`/`decode_text`. */
120
+ comment?: string;
121
+ }
122
+
123
+ export interface ReplicationJobAttributes {
124
+ /** `100-0`, in PVE's own int-parsed spelling. */
125
+ id: string;
126
+ /**
127
+ * ⚠️ REPORTED AS INTEGERS, NEVER COMPARED, AND THIS IS THE "id written as a name" TRAP. MEASURED:
128
+ * `parse_config` sets both from the section header via `int()`, so they are the id re-spelled
129
+ * as numbers and no write accepts either. Compared against anything they would be an update
130
+ * forever; reported, they let a plan say which guest is about to lose a replica.
131
+ */
132
+ guest: number;
133
+ jobnum: number;
134
+ /** Always `local` — the only section type PVE registers. Create-only, reported, not compared. */
135
+ type: string;
136
+ /**
137
+ * ⚠️ REPORTED, NEVER COMPARED. Create-only AND rewritten by PVE — see `matches`. Named for the
138
+ * prop rather than for PVE's wire key `target`, so that one file never means two things by one
139
+ * word; the header says why the wire name could not be kept.
140
+ */
141
+ targetNode: string;
142
+ /** ⚠️ REPORTED, NEVER SENT. PVE's own bookkeeping — see the ⛔ in replication-job-form.ts. */
143
+ source: string;
144
+ schedule: string;
145
+ /** 0 when unset. See `rateOf`. */
146
+ rate: number;
147
+ disable: boolean;
148
+ comment: string;
149
+ /**
150
+ * `''`, `local` or `full`. ⛔ NOT COSMETIC: a non-empty value is a job that is deleting itself,
151
+ * so `matches` refuses it and `updateBody` clears it. See both.
152
+ */
153
+ remove_job: string;
154
+ }
155
+
156
+ export interface ProxmoxReplicationJob extends Resource<
157
+ 'Proxmox.ReplicationJob',
158
+ ReplicationJobProps,
159
+ ReplicationJobAttributes,
160
+ never,
161
+ PveRequirements
162
+ > {}
163
+
164
+ export const ProxmoxReplicationJob = Resource<ProxmoxReplicationJob>('Proxmox.ReplicationJob');
165
+
166
+ /**
167
+ * ⚠️ `rate` IS THE ONLY FLOAT IN THIS PACKAGE, so it gets a coercion of its own rather than a sixth
168
+ * entry in `values.ts` — the rule that module states for itself: a coercion only one PVE object
169
+ * needs belongs in that object's file, next to the field it serves. `int` cannot serve it, since
170
+ * `Number.parseInt('10.5')` is 10 and a rate silently rounded down is both a limit nobody chose
171
+ * and a comparison that never settles. It accepts a string for the reason `int` does: these are
172
+ * SectionConfig sections, and whether `10` arrives as a number is a property of the release.
173
+ * ⚠️ 0 MEANS UNSET, and is safe as the marker because PVE's schema gives `rate` a minimum of 1 —
174
+ * the same trick as `metric-server.ts`'s `UNSET = -1`, for the same reason.
175
+ */
176
+ const rateOf = (value: unknown): number => {
177
+ if (typeof value === 'number') return value;
178
+ const parsed = Number.parseFloat(text(value));
179
+ return Number.isFinite(parsed) ? parsed : 0;
180
+ };
181
+
182
+ const handlers = pveHandlers<ReplicationJobProps, ReplicationJobAttributes>({
183
+ /**
184
+ * ⚠️ EVERY FALLBACK IS PVE'S DOCUMENTED DEFAULT, NOT `false`/`''` PICKED FOR CONVENIENCE, so the
185
+ * answer does not depend on whether a given release echoes back a key it never wrote.
186
+ * SectionConfig stores what it was handed; a job created in the UI without a schedule has no
187
+ * schedule line, and reading that absence as anything but `DEFAULT_SCHEDULE` would report
188
+ * drift on a job nobody touched and then WRITE the reading back.
189
+ * ⛔ NO "IS IT REALLY THERE" GUARD, for `backup-job.ts`'s reason: absence is the API declining to
190
+ * answer, which `pveOperations.read` already handles, not a key missing from an answer that did
191
+ * arrive. `digest` is deliberately absent too — it changes when ANY job in the file changes,
192
+ * including one somebody else declared (`ha-resource.ts` hit exactly that).
193
+ */
194
+ attributes: (live, props) => ({
195
+ comment: text(live['comment'], ''),
196
+ disable: bool(live['disable'], false),
197
+ guest: int(live['guest'], props.guest),
198
+ id: text(live['id'], jobId(props)),
199
+ jobnum: int(live['jobnum'], props.jobnum),
200
+ rate: rateOf(live['rate']),
201
+ remove_job: text(live['remove_job'], ''),
202
+ schedule: text(live['schedule'], DEFAULT_SCHEDULE),
203
+ source: text(live['source'], ''),
204
+ targetNode: text(live['target'], props.targetNode),
205
+ type: text(live['type'], 'local'),
206
+ }),
207
+ collection: () => 'cluster/replication',
208
+ createForm: createBody,
209
+ /**
210
+ * ⚠️ `targetNode` AND `source` ARE OUT OF THIS COMPARISON FOR TWO REASONS EACH, AND EITHER ALONE
211
+ * WOULD BE ENOUGH. They are create-only — `target` is a `fixed` option, `source` has no honest
212
+ * declared value — AND PVE REWRITES BOTH BEHIND YOUR BACK. MEASURED in
213
+ * `switch_replication_job_target_nolock`: when a guest migrates, PVE sets
214
+ * `$jobcfg->{target} = $new_target` and `$jobcfg->{source} = $old_target`, so the job follows
215
+ * the guest. Compared, a single migration would make every later plan report an update that the
216
+ * PUT cannot perform, forever. The cost of leaving them out is stated on the `targetNode` prop:
217
+ * a changed target plans as noop, and a migrated job is not dragged back.
218
+ * ⚠️ `guest`, `jobnum`, `type` AND `id` ARE NOT COMPARED EITHER: the first two are the id in
219
+ * integer form, `type` has one legal value, and `id` is the path — a change there is a
220
+ * different object, which reads as absent and is created.
221
+ * ⛔ `remove_job` IS COMPARED, AND IT IS THE ONE FIELD HERE THAT LOOKS LIKE A FOREVER-UPDATE AND
222
+ * IS NOT. A DELETE marks the job rather than removing it, so a re-declared id can name a job
223
+ * that is busy deleting itself, and matching that would be a noop over a vanishing object. It
224
+ * settles because `updateBody` actually clears the marker — see there.
225
+ */
226
+ matches: (attributes, props) =>
227
+ attributes.remove_job === '' &&
228
+ attributes.schedule === (props.schedule ?? DEFAULT_SCHEDULE) &&
229
+ attributes.disable === (props.disable === true) &&
230
+ attributes.comment === (props.comment ?? '') &&
231
+ (props.rate === undefined || attributes.rate === props.rate),
232
+ path: (props) => `cluster/replication/${jobId(props)}`,
233
+ updateForm: updateBody,
234
+ });
235
+
236
+ /**
237
+ * ⛔ Empty `list` like every other resource here: `GET /cluster/replication` hands back every job an
238
+ * operator ever made, and adopting one is how a later `alchemy destroy` takes away a standby copy
239
+ * nobody declared. Adoption is an explicit act — declare the `guest` and `jobnum` you mean.
240
+ * ⚠️ NOTHING BRAKES THIS DESTROY. PVE refuses to delete a pool that still holds guests; it accepts
241
+ * this without a word, because nothing depends on a replication job. Re-read the DELETE ⛔ at the
242
+ * top of this file before approving a plan line that removes one.
243
+ */
244
+ export const ProxmoxReplicationJobProvider = () =>
245
+ Provider.effect(
246
+ ProxmoxReplicationJob,
247
+ Effect.succeed(ProxmoxReplicationJob.Provider.of(handlers)),
248
+ );
@@ -0,0 +1,257 @@
1
+ /**
2
+ * One shape for every PVE object, so the provider can cover Proxmox rather than a corner of it.
3
+ *
4
+ * ⛔ THE ALTERNATIVE IS TWENTY COPIES OF THE SAME 130 LINES. Proxmox exposes LXC, QEMU, storage,
5
+ * pools, users, groups, roles, ACLs, API tokens, SDN zones/vnets/subnets, firewall rules and
6
+ * groups, HA resources, backup and replication jobs, metric servers and notification targets.
7
+ * Each is the same four operations over a different path and a different form body. Written by
8
+ * hand, resource number six is where someone quietly drops the read-back or the isResolved guard
9
+ * and nobody notices until a plan lies.
10
+ *
11
+ * ★ SO THE FOUR OPERATIONS LIVE HERE, ONCE, AND A RESOURCE DECLARES ONLY WHAT IS DIFFERENT:
12
+ * where it lives, how to recognise it, and which fields are mutable. A new PVE object should be
13
+ * thirty lines, not a hundred and thirty.
14
+ *
15
+ * ⚠️ EVERY PVE ANSWER IS WRAPPED IN `{"data": ...}` AND A FAILED CALL CAN STILL BE HTTP 200 with
16
+ * `{"data": null}`. That is why `reconcile` below READS BACK and refuses when the object is still
17
+ * absent, rather than trusting a status code — see the ⛔ in that function.
18
+ */
19
+ import { isResolved } from 'alchemy/Diff';
20
+ import type { Input } from 'alchemy/Input';
21
+ import * as Effect from 'effect/Effect';
22
+ import type * as HttpClient from 'effect/unstable/http/HttpClient';
23
+ import { pve } from './client.ts';
24
+ import type { ApiTarget, PbsTarget, PveRole, PveTarget } from './credentials.ts';
25
+
26
+ /**
27
+ * ★ WHY SEVERAL FAMILIES HERE DECLARE `defaultRemovalPolicy: 'retain'`, WRITTEN ONCE.
28
+ *
29
+ * Alchemy's own convention: a few resource types default to retain "because their contents are
30
+ * irreplaceable" — `GitHub.Repository` and `Cloudflare.Zone` do — and those opt into deletion
31
+ * with `destroy()`. `retain` means the engine SKIPS `provider.delete` entirely when a resource
32
+ * is orphaned or destroyed: the state row is dropped, the cluster object lives on.
33
+ *
34
+ * So the PVE families whose contents cannot be rebuilt from a line of TypeScript — CephOsd,
35
+ * CephPool, CephFs, CephDaemon, ZfsPool, Storage, NodeNetwork — carry that default. Their
36
+ * `delete` is FULLY IMPLEMENTED and runs the moment a caller opts in with
37
+ * `.pipe(RemovalPolicy.destroy())`. This is deliberately the Terraform `prevent_destroy` shape
38
+ * rather than a stubbed-out operation: a `delete` that silently does nothing lies to whoever is
39
+ * reading the plan, and the lie is discovered at the worst possible time.
40
+ *
41
+ * ⚠️ THE POLICY IS A DECORATION, NOT A PROP, so changing it produces NO DIFF — the resource plans
42
+ * as a noop and the deploy re-commits the row's policy in place. A policy change therefore takes
43
+ * effect from the very next deploy even though the plan shows nothing.
44
+ */
45
+
46
+ /**
47
+ * ⛔ A RESOURCE IS PINNED TO ITS PRODUCT; ONLY THE FACTORY TAKES EITHER. The two hosts are the same
48
+ * two strings, so without a required discriminant TypeScript would accept a PBS host wherever a
49
+ * PVE one belongs — silently, with every call 401ing and `read` folding that into "absent". The
50
+ * whole argument is on `scheme` in credentials.ts.
51
+ */
52
+ /**
53
+ * ★ WHAT EVERY OPERATION IN THIS PACKAGE NEEDS FROM THE RUNTIME, NAMED ONCE.
54
+ *
55
+ * `HttpClient`, for both halves of every call: the credential is minted from OpenBao's HTTP API
56
+ * and the PVE call goes through the same client. That is what Alchemy's own providers do
57
+ * (src/Hetzner/Providers.ts builds on `FetchHttpClient.layer`, and there is no bare `fetch` in
58
+ * its Docker, Kubernetes or GitHub providers).
59
+ * ★ `ChildProcessSpawner` WAS HERE UNTIL 2026-09-14, while the mint shelled out to `bao`. The HTTP
60
+ * client replaced that (credentials.ts), so the requirement went with it.
61
+ *
62
+ * ⚠️ SPELLED OUT AT EACH OF THE FORTY-ODD RESOURCES this would be a union nobody keeps in step —
63
+ * one file left on the old shape is a type error at the stack, far from the cause. Named here,
64
+ * adding a third service later is one edit.
65
+ */
66
+ export type PveRequirements = HttpClient.HttpClient;
67
+
68
+ export type WithTarget = { target: PveTarget };
69
+ export type WithPbsTarget = { target: PbsTarget };
70
+ export type WithApiTarget = { target: ApiTarget };
71
+
72
+ export type PveSpec<Props extends WithApiTarget, Attributes> = {
73
+ /** `pools/house`, `nodes/n2/lxc/101` — where ONE object is read, updated and deleted. */
74
+ readonly path: (props: Props) => string;
75
+ /** `pools`, `nodes/n2/lxc` — where a NEW one is POSTed. */
76
+ readonly collection: (props: Props) => string;
77
+ /** Live JSON to attributes. Returning undefined means "this is not really there". */
78
+ readonly attributes: (live: Record<string, unknown>, props: Props) => Attributes | undefined;
79
+ /** The form PVE wants on create. ⚠️ PVE takes form encoding, not JSON. */
80
+ readonly createForm: (props: Props) => Record<string, string>;
81
+ /**
82
+ * The form for an update, or undefined when the object has no mutable fields.
83
+ *
84
+ * ⚠️ SOME PVE OBJECTS CANNOT BE UPDATED AT ALL. Returning undefined makes a changed prop a
85
+ * REPLACE rather than a silent no-op, which is the honest answer for an immutable object.
86
+ */
87
+ readonly updateForm?: (props: Props) => Record<string, string>;
88
+ /** True when live already matches props. Decides noop vs update. */
89
+ readonly matches: (attributes: Attributes, props: Props) => boolean;
90
+ /**
91
+ * Which lease reads this family. Defaults to `read`, the 3600s auditor-shaped one.
92
+ *
93
+ * ⛔ THREE FAMILIES SET THIS TO `provision`, AND THE FAILURE IT AVOIDS IS SILENT. PVE gates some
94
+ * SINGLE-OBJECT reads on the allocate privilege rather than the audit one — `/storage/{id}`,
95
+ * `/cluster/sdn/zones/{zone}`, `/cluster/sdn/vnets/{vnet}` — while their COLLECTION reads
96
+ * accept audit, so nothing looks wrong until a resource reads one object. `read` below folds
97
+ * every failure into `undefined`, right for a 404 and wrong for a 403, so the plan says create
98
+ * and PVE answers that the object already exists. Measured both ways on `local`, and written
99
+ * up with the alternative that was rejected, in docs/privileges.md.
100
+ */
101
+ readonly readRole?: PveRole;
102
+ };
103
+
104
+ export const pveOperations = <Props extends WithApiTarget, Attributes>(
105
+ spec: PveSpec<Props, Attributes>,
106
+ ) => {
107
+ /** The live object, or undefined. ⚠️ A 404 is an ANSWER here, not a failure. */
108
+ const read = (props: Props) =>
109
+ pve<Record<string, unknown>>(
110
+ props.target,
111
+ spec.readRole ?? 'read',
112
+ 'GET',
113
+ spec.path(props),
114
+ ).pipe(
115
+ Effect.map((data) => (data === undefined ? undefined : spec.attributes(data, props))),
116
+ Effect.orElseSucceed(() => undefined),
117
+ );
118
+
119
+ return {
120
+ read,
121
+
122
+ // ⚠️ `Input<Props>`, NOT `Props`. At plan time a prop can still be an unresolved Output or
123
+ // Config; `isResolved` is the narrowing, so the parameter has to admit the wider type or
124
+ // every caller casts — and a cast here would be a lie about what plan actually has.
125
+ diff: (news: Input<Props>, output: Attributes | undefined) =>
126
+ Effect.gen(function* () {
127
+ // ⚠️ A prop can still be an unresolved Output at plan time. Alchemy's own providers guard
128
+ // with isResolved and skip rather than guess; comparing a placeholder to a live value
129
+ // reports an update nobody asked for.
130
+ if (output === undefined || !isResolved(news)) return undefined;
131
+ const live = yield* read(news);
132
+ // ⚠️ `update`, NOT `create` — Alchemy's Diff admits only noop/update/replace. An object
133
+ // Alchemy has state for but the cluster does not is drift, and reconcile repairs it.
134
+ if (live === undefined) return { action: 'update' } as const;
135
+ if (spec.matches(live, news)) return { action: 'noop' } as const;
136
+ // An object with no update path cannot be edited in place; say replace and mean it.
137
+ return spec.updateForm === undefined
138
+ ? ({ action: 'replace' } as const)
139
+ : ({ action: 'update' } as const);
140
+ }),
141
+
142
+ reconcile: (news: Props) =>
143
+ Effect.gen(function* () {
144
+ const live = yield* read(news);
145
+ if (live === undefined) {
146
+ yield* pve(
147
+ news.target,
148
+ 'provision',
149
+ 'POST',
150
+ spec.collection(news),
151
+ spec.createForm(news),
152
+ );
153
+ } else if (spec.updateForm !== undefined && !spec.matches(live, news)) {
154
+ /**
155
+ * ⛔ ADOPTING AN OBJECT THAT ALREADY MATCHES MUST NOT WRITE TO IT, AND WITHOUT THE
156
+ * `matches` GUARD IT DID. Alchemy's `adopted` action is NOT a read: Apply.ts routes it
157
+ * down the same branch as `update` and calls `reconcile` (Apply.ts:980 and :1045). So
158
+ * the first time a declaration names an object Alchemy has no state for — which is
159
+ * exactly what adopting the live estate means — reconcile runs, finds it present, and
160
+ * fell through to this PUT.
161
+ *
162
+ * For most families that was a pointless write-back of identical values. For
163
+ * `Proxmox.NodeNetwork` it is worse than pointless: a PUT under `/nodes/{node}/network`
164
+ * STAGES a change into `/etc/network/interfaces.new`, so merely adopting the interface
165
+ * a node already has would leave that node with a pending network change waiting for an
166
+ * apply — on a cluster whose Ceph traffic rides vmbr1.11. Adoption has to be free.
167
+ *
168
+ * ★ SO THE CONDITION IS THE SAME PREDICATE `diff` USES. If `matches` is true the object
169
+ * already says what the declaration says, and there is nothing to write — by
170
+ * definition, since every field this provider manages is a field `matches` compares.
171
+ * A field deliberately left OUT of `matches` is one this resource does not manage, so
172
+ * its drift is not this provider's to repair.
173
+ *
174
+ * ⚠️ AN EMPTY FORM IS ALSO NOT A WRITE. `updateForm` can legitimately answer `{}` — a
175
+ * storage declaring only `storage`, `type` and its locator has no mutable field at all
176
+ * — and PUTting an empty body is a pointless write at best and a 400 that reads as a
177
+ * broken provider at worst.
178
+ */
179
+ const form = spec.updateForm(news);
180
+ if (Object.keys(form).length > 0) {
181
+ yield* pve(news.target, 'provision', 'PUT', spec.path(news), form);
182
+ }
183
+ }
184
+ const after = yield* read(news);
185
+ if (after === undefined) {
186
+ /**
187
+ * ⛔ REFUSE RATHER THAN RETURN THE PROPS AS THOUGH THEY LANDED. PVE answers 200 with
188
+ * `{"data": null}` on several endpoints, so "no error" is not evidence of a write. A
189
+ * provider that returned its own inputs here would record state for an object that does
190
+ * not exist, and the next plan would read `noop` over the gap.
191
+ */
192
+ return yield* Effect.die(
193
+ new Error(
194
+ `${spec.path(news)}: the write returned no error but the object is still absent. ` +
195
+ 'PVE wraps every answer in {"data":...} and can report success on a call that did ' +
196
+ 'nothing -- read back rather than trusting the status code.',
197
+ ),
198
+ );
199
+ }
200
+ return after;
201
+ }),
202
+
203
+ /**
204
+ * ⚠️ PVE REFUSES TO DELETE THINGS THAT ARE STILL IN USE — a pool holding guests, a storage with
205
+ * volumes. That refusal is the cluster declining to orphan something because a line left a
206
+ * file, so it is surfaced as-is rather than retried with a force flag.
207
+ */
208
+ destroy: (olds: Props) => pve(olds.target, 'provision', 'DELETE', spec.path(olds)),
209
+ };
210
+ };
211
+
212
+ /**
213
+ * The five provider handlers for a spec'd PVE object, wired once.
214
+ *
215
+ * ⛔ THIS BLOCK WAS WRITTEN TEN TIMES BEFORE IT WAS EXTRACTED, and every copy was character for
216
+ * character identical apart from its comments: `list` answering empty, and four handlers whose
217
+ * entire body was `yield* ops.<same name>(<same argument>)`. Ten copies of a delegation is ten
218
+ * chances to delegate to the wrong one — `ops.read(olds)` inside `reconcile` typechecks, returns
219
+ * a plausible value, and turns every deploy into a no-op that reports success.
220
+ *
221
+ * ★ IT TAKES THE SPEC, NOT THE OPERATIONS, so a resource declares its four spec functions and
222
+ * stops. `pveOperations` stays exported for an object that genuinely needs a handler of its own;
223
+ * nothing in this package does yet, and the day one does it should be visibly different from the
224
+ * ten that are not.
225
+ *
226
+ * ⛔ IT STOPS AT THE HANDLERS AND DOES NOT RETURN THE LAYER, AND THAT IS NOT AN OVERSIGHT — it was
227
+ * written the other way first and reverted. Wrapping `Provider.effect(cls, …)` here means naming
228
+ * the resource class generically, and Alchemy's `Props<R>` and `R["Attributes"]` are not the
229
+ * plain `Props` and `Attributes` this spec is written against: `Attributes` comes back as
230
+ * `Attributes & AttrOutput<Attributes["Attributes"]>`. Against a free type variable TypeScript
231
+ * cannot prove those line up — "could be instantiated with a different subtype" — so the only
232
+ * way to compile a Layer-returning version is a cast, and a cast is a LIE ABOUT WHETHER THE
233
+ * HANDLERS MATCH THE RESOURCE, which is the one thing the call site exists to check. Leaving
234
+ * `Cls.Provider.of(...)` in each file keeps that check where the types are concrete and real.
235
+ * One line per resource is the price of it, and it is worth paying.
236
+ *
237
+ * ⚠️ `list` IS OPTIONAL IN `ProviderServiceInput` AND IS PASSED ANYWAY. An empty list is a claim —
238
+ * this provider adopts nothing — and it is made here for every PVE object at once, because the
239
+ * reasoning is identical for all of them: every PVE index endpoint answers with the whole
240
+ * cluster's objects, PVE's own built-ins included. `GET /pools` returns pools a human made years
241
+ * ago; `GET /access/roles` returns `Administrator`. Returning any of them would invite Alchemy to
242
+ * adopt an object it never created, and therefore one day to narrow or delete it. Adoption stays
243
+ * an explicit act. A resource with a reason to differ passes its own `list` and says why.
244
+ */
245
+ export const pveHandlers = <Props extends WithApiTarget, Attributes extends object>(
246
+ spec: PveSpec<Props, Attributes>,
247
+ ) => {
248
+ const ops = pveOperations(spec);
249
+ return {
250
+ list: () => Effect.succeed([]),
251
+ read: ({ olds }: { olds: Props }) => ops.read(olds),
252
+ diff: ({ news, output }: { news: Input<Props>; output: Attributes | undefined }) =>
253
+ ops.diff(news, output),
254
+ reconcile: ({ news }: { news: Props }) => ops.reconcile(news),
255
+ delete: ({ olds }: { olds: Props }) => ops.destroy(olds),
256
+ };
257
+ };