@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,246 @@
1
+ /**
2
+ * `Proxmox.SdnZone` — the network a declared guest sits on, finally declared too.
3
+ *
4
+ * ★ WHY THIS ONE MATTERS MORE THAN IT LOOKS. Every guest NIC here names a bridge —
5
+ * `net0=name=eth0,bridge=vmbr0` — that nothing in this package declares: a hand-built interface,
6
+ * repeated per node, that a container silently depends on. A zone is the first piece of that
7
+ * dependency PVE will let a plan own.
8
+ *
9
+ * ⛔ SDN WRITES ARE STAGED, NOT APPLIED, AND THAT DEFEATS THE FACTORY'S READ-BACK GUARANTEE.
10
+ * POST and PUT under `cluster/sdn/zones` edit `/etc/pve/sdn/zones.cfg` and change NOTHING on any
11
+ * node. The plain GET reads that same staged file back, so `pveOperations` sees its own write,
12
+ * finds the object present, and reports a success in which no node gained an interface. The
13
+ * applied state is a different document — `.running-config` beside it — and `?pending=1` is the
14
+ * read that tells them apart: it returns a merged view with a `state` of new/changed/deleted.
15
+ *
16
+ * ⛔ THE APPLY CANNOT LIVE IN THIS RECONCILE, so it is absent rather than forgotten.
17
+ * `PUT /cluster/sdn` is the atomic pending-to-apply and it applies the WHOLE staged config —
18
+ * every zone, vnet and subnet, on every node. Called from a per-resource reconcile it would push
19
+ * some OTHER resource's half-written zone out to the cluster at whatever point the plan ordering
20
+ * reached this one, and PVE has no pre-apply hook to veto that. The apply belongs to ONE object
21
+ * that depends on all of them — a sibling `Proxmox.SdnApply` over `cluster/sdn`, or a stack-level
22
+ * hook that runs last — so that "declared" and "live on the nodes" stay two visible steps.
23
+ *
24
+ * ⚠️ SO THESE ATTRIBUTES DESCRIBE THE STAGED ZONE, and there is deliberately no `applied` flag:
25
+ * the factory uses ONE path for GET, PUT and DELETE, and PVE's parameter schemas are closed, so
26
+ * an unexpected `pending` on the PUT is a 400 rather than an ignored hint. Reading `?pending=1`
27
+ * means re-implementing the operations this file exists to share, and an `applied: true` that
28
+ * was never measured is worse than an absent one. Deploying this resource does not change the
29
+ * network; it changes what the next apply will do.
30
+ *
31
+ * ⛔ THE READ THIS RESOURCE PERFORMS NEEDS SDN.Allocate, NOT SDN.Audit, AND AN EARLIER DRAFT OF
32
+ * THIS COMMENT SAID OTHERWISE. Read off the cluster's own schema on 2026-09-13:
33
+ *
34
+ * GET /cluster/sdn/zones -> "list entries where you have SDN.Audit or SDN.Allocate"
35
+ * GET /cluster/sdn/zones/{zone} -> {"check": ["perm", "/sdn/zones/{zone}", ["SDN.Allocate"]]}
36
+ *
37
+ * `pveOperations.read` calls the ITEM endpoint, so an auditor-shaped `read` lease is REFUSED —
38
+ * and the refusal does not surface as a 403. `read` folds every failure into `undefined`, which
39
+ * the factory reads as "absent", so the plan says create, the POST goes out, and PVE answers
40
+ * that the zone already exists. This is the same class as Proxmox.Storage; see
41
+ * docs/privileges.md, "The failure that does not look like a permission problem".
42
+ *
43
+ * ⚠️ THE REST OF THE PRIVILEGES. Writes need SDN.Allocate (`/sdn/zones` to create,
44
+ * `/sdn/zones/{zone}` to update or delete); the apply — `Proxmox.SdnApply`, which is what
45
+ * actually publishes any of this — needs SDN.Allocate on `/sdn` itself. SDN.Use is NOT among
46
+ * them: it permits attaching a guest to an existing vnet and nothing else.
47
+ */
48
+ import { Resource } from 'alchemy';
49
+ import * as Provider from 'alchemy/Provider';
50
+ import * as Effect from 'effect/Effect';
51
+ import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
52
+ import { csv, int, text, withClears } from './values.ts';
53
+
54
+ /** PVE's zone plugins. Which one a zone is decides which fields below it will even accept. */
55
+ export type SdnZoneType = 'evpn' | 'qinq' | 'simple' | 'vlan' | 'vxlan';
56
+
57
+ export interface SdnZoneProps extends WithTarget {
58
+ /**
59
+ * PVE's primary key. ⚠️ AT MOST EIGHT LOWERCASE ALPHANUMERICS, first one a letter: the id lands
60
+ * in generated interface names, so PVE enforces it with a regex whose rejection reads as
61
+ * "value does not match the regex pattern" rather than as "your name is too long".
62
+ */
63
+ zone: string;
64
+ /**
65
+ * ⛔ CREATE-TIME ONLY. `PUT /cluster/sdn/zones/{zone}` has no `type` parameter at all — a
66
+ * section's type is fixed when it is written. Changing it here is not an update PVE can
67
+ * perform; the ⛔ in `attributes` turns that edit into a refusal instead of a silent no-op.
68
+ */
69
+ type: SdnZoneType;
70
+ /** Accepted by every zone type. Unset means the zone inherits the underlying bridge's MTU. */
71
+ mtu?: number;
72
+ /** ⚠️ WHICH NODES THE ZONE IS DEPLOYED TO. Unset means EVERY node, not none. */
73
+ nodes?: string[];
74
+ /** IPAM plugin id. Unset is the built-in `pve` one — the two are one zone, see `sameIpam`. */
75
+ ipam?: string;
76
+ /** DNS plugin id used to register guest records. */
77
+ dns?: string;
78
+ /** The domain those records are registered under, e.g. `example.internal`. */
79
+ dnszone?: string;
80
+ /** vxlan only, and REQUIRED there: the peer addresses the tunnel mesh is built from. */
81
+ peers?: string[];
82
+ /** evpn only, and required there. PVE's own hyphenated name, so no mapping to get wrong. */
83
+ 'vrf-vxlan'?: number;
84
+ /** qinq only, and required there: the service VLAN tag the zone's vnets are stacked inside. */
85
+ tag?: number;
86
+ /** vlan and qinq only, and required in both: the existing bridge the zone is carved out of. */
87
+ bridge?: string;
88
+ }
89
+
90
+ /**
91
+ * ⛔ NO `digest` HERE, ON PURPOSE. PVE returns one, but it is the digest of the WHOLE zones.cfg
92
+ * file rather than of this section — declaring a SECOND zone would rewrite this one's stored
93
+ * attributes, and comparing it would report an update on a zone nobody touched.
94
+ */
95
+ export interface SdnZoneAttributes {
96
+ zone: string;
97
+ type: SdnZoneType;
98
+ /** 0 when unset, which is PVE's "inherit from the bridge" rather than an MTU of zero. */
99
+ mtu: number;
100
+ /** Normalised: split, sorted, rejoined. Never compare a raw PVE string to it — see `csv`. */
101
+ nodes: string;
102
+ /** The EFFECTIVE ipam: an absent one is reported as `pve`, because that is what will be used. */
103
+ ipam: string;
104
+ dns: string;
105
+ dnszone: string;
106
+ peers: string;
107
+ 'vrf-vxlan': number;
108
+ tag: number;
109
+ bridge: string;
110
+ /** ⚠️ THE REMINDER IN THE STATE ITSELF: staged config. No value here says a node has the zone. */
111
+ readonly staged: true;
112
+ }
113
+
114
+ export interface ProxmoxSdnZone extends Resource<
115
+ 'Proxmox.SdnZone',
116
+ SdnZoneProps,
117
+ SdnZoneAttributes,
118
+ never,
119
+ PveRequirements
120
+ > {}
121
+
122
+ export const ProxmoxSdnZone = Resource<ProxmoxSdnZone>('Proxmox.SdnZone');
123
+
124
+ /**
125
+ * ⚠️ AN ABSENT `ipam` AND `ipam pve` ARE THE SAME ZONE. `pve` is the built-in default, and whether
126
+ * PVE materialises that word into zones.cfg on create is version-dependent — so comparing the
127
+ * raw values reports an update on the plan immediately AFTER the create, on a zone this provider
128
+ * just wrote itself. Normalising both sides is loop-proof under either behaviour.
129
+ */
130
+ const sameIpam = (live: string, declared: string | undefined) =>
131
+ (live === '' ? 'pve' : live) === (declared === undefined || declared === '' ? 'pve' : declared);
132
+
133
+ /**
134
+ * The mutable fields, spread in only when defined.
135
+ *
136
+ * ⚠️ AN EMPTY STRING IS NOT "UNSET" FOR AN INTEGER FIELD. `mtu=` fails PVE's parameter check
137
+ * outright instead of clearing the value, so the pool.ts habit of sending `''` for an absent
138
+ * optional does not carry over here. Clearing is a separate verb — see `clearable`.
139
+ */
140
+ const body = (props: SdnZoneProps): Record<string, string> => ({
141
+ ...(props.bridge === undefined ? {} : { bridge: props.bridge }),
142
+ ...(props.dns === undefined ? {} : { dns: props.dns }),
143
+ ...(props.dnszone === undefined ? {} : { dnszone: props.dnszone }),
144
+ ...(props.ipam === undefined ? {} : { ipam: props.ipam }),
145
+ ...(props.mtu === undefined ? {} : { mtu: String(props.mtu) }),
146
+ ...(props.nodes === undefined ? {} : { nodes: csv(props.nodes) }),
147
+ ...(props.peers === undefined ? {} : { peers: csv(props.peers) }),
148
+ ...(props.tag === undefined ? {} : { tag: String(props.tag) }),
149
+ ...(props['vrf-vxlan'] === undefined ? {} : { 'vrf-vxlan': String(props['vrf-vxlan']) }),
150
+ });
151
+
152
+ /**
153
+ * Which managed fields PVE will let `delete=` clear, per zone type.
154
+ *
155
+ * ⛔ A PUT THAT SIMPLY OMITS A FIELD DOES NOT CLEAR IT. PVE merges the form into the existing
156
+ * section, so dropping `mtu` from a declaration leaves the old MTU in place and `matches`
157
+ * reports an update on every plan forever — the exact bug this table exists to prevent.
158
+ * ⛔ AND THE LIST CANNOT BE THE SAME FOR EVERY TYPE. `delete=<opt>` dies with "no such option"
159
+ * when the option is not in that plugin's schema (`peers` on a simple zone) and with "unable to
160
+ * delete required option" when it is required there. `bridge`, `tag`, `peers` and `vrf-vxlan`
161
+ * are required by the only types that accept them, so none of the four is ever clearable: a
162
+ * zone that lost one is a replace, not an update.
163
+ * ⚠️ `ipam` IS OMITTED FOR evpn DELIBERATELY. It is optional on the other four; on evpn its
164
+ * optionality has moved between releases, and a wrong entry here is a hard 400 on EVERY update
165
+ * rather than a cosmetic diff. Dropping `ipam` from an evpn declaration therefore reports an
166
+ * update that does not converge — visible, unlike a wrong delete that breaks the zone.
167
+ */
168
+ const clearable: Record<SdnZoneType, readonly string[]> = {
169
+ evpn: ['dns', 'dnszone', 'mtu', 'nodes'],
170
+ qinq: ['dns', 'dnszone', 'ipam', 'mtu', 'nodes'],
171
+ simple: ['dns', 'dnszone', 'ipam', 'mtu', 'nodes'],
172
+ vlan: ['dns', 'dnszone', 'ipam', 'mtu', 'nodes'],
173
+ vxlan: ['dns', 'dnszone', 'ipam', 'mtu', 'nodes'],
174
+ };
175
+
176
+ const handlers = pveHandlers<SdnZoneProps, SdnZoneAttributes>({
177
+ // ⛔ PVE gates this family's ITEM read on the allocate privilege, not the audit one — see
178
+ // `readRole` in resource.ts. The auditor-shaped lease reads 403, which `read` turns into
179
+ // "absent", and the plan then says create for an object that is plainly there.
180
+ readRole: 'provision',
181
+ attributes: (live, props) => {
182
+ /**
183
+ * ⛔ A ZONE OF ANOTHER TYPE IS ANOTHER OBJECT, AND "ABSENT" IS THE HONEST ANSWER. A PUT cannot
184
+ * change `type`, and the factory only calls a change `replace` for a resource with no update
185
+ * path — which this is not. Reporting the foreign zone as missing makes reconcile POST, and
186
+ * PVE refuses with "sdn zone ID 'x' already defined": a loud, accurate error instead of a
187
+ * silent noop over somebody else's zone. An older PVE that omits `type` falls through.
188
+ */
189
+ const liveType = text(live['type']);
190
+ if (liveType !== '' && liveType !== props.type) return undefined;
191
+ return {
192
+ bridge: text(live['bridge']),
193
+ dns: text(live['dns']),
194
+ dnszone: text(live['dnszone']),
195
+ ipam: text(live['ipam']) === '' ? 'pve' : text(live['ipam']),
196
+ mtu: int(live['mtu'], 0),
197
+ nodes: csv(text(live['nodes'])),
198
+ peers: csv(text(live['peers'])),
199
+ staged: true,
200
+ tag: int(live['tag'], 0),
201
+ type: props.type,
202
+ 'vrf-vxlan': int(live['vrf-vxlan'], 0),
203
+ zone: props.zone,
204
+ };
205
+ },
206
+ collection: () => 'cluster/sdn/zones',
207
+ createForm: (props) => ({ ...body(props), type: props.type, zone: props.zone }),
208
+ /**
209
+ * ⚠️ `type` AND `zone` ARE NOT COMPARED. `zone` is the key the object was read by, and `type` is
210
+ * handled above by refusing to recognise a zone of the wrong one. Comparing either here would
211
+ * produce a diff whose update can never satisfy it.
212
+ */
213
+ matches: (attributes, props) =>
214
+ attributes.mtu === (props.mtu ?? 0) &&
215
+ attributes.nodes === csv(props.nodes) &&
216
+ sameIpam(attributes.ipam, props.ipam) &&
217
+ attributes.dns === (props.dns ?? '') &&
218
+ attributes.dnszone === (props.dnszone ?? '') &&
219
+ attributes.peers === csv(props.peers) &&
220
+ attributes['vrf-vxlan'] === (props['vrf-vxlan'] ?? 0) &&
221
+ attributes.tag === (props.tag ?? 0) &&
222
+ attributes.bridge === (props.bridge ?? ''),
223
+ path: (props) => `cluster/sdn/zones/${props.zone}`,
224
+ updateForm: (props) => {
225
+ const fields = body(props);
226
+ const clear = clearable[props.type].filter((option) => fields[option] === undefined);
227
+ return withClears(fields, clear);
228
+ },
229
+ });
230
+
231
+ /**
232
+ * ⛔ Empty for the same reason as every other resource here, and with extra force for SDN:
233
+ * `GET /cluster/sdn/zones` answers with the zones the cluster's whole network already
234
+ * runs on. Adopting those would make a later `alchemy destroy` a cluster-wide outage.
235
+
236
+ *
237
+ * ⛔ A DELETE IS STAGED TOO, AND THAT IS THE HALF PEOPLE FORGET. The section leaves
238
+ * zones.cfg and Alchemy drops the resource, but the zone KEEPS RUNNING on every node
239
+ * until something applies `PUT /cluster/sdn`: a destroy nobody applies reads as complete
240
+ * in the plan and has not happened on the cluster.
241
+ * ⚠️ PVE also refuses to remove a zone that still holds vnets. That refusal is kept — it is
242
+ * the cluster declining to orphan a network because a line left a file.
243
+
244
+ */
245
+ export const ProxmoxSdnZoneProvider = () =>
246
+ Provider.effect(ProxmoxSdnZone, Effect.succeed(ProxmoxSdnZone.Provider.of(handlers)));
@@ -0,0 +1,245 @@
1
+ /**
2
+ * `Proxmox.Storage` — one entry in `/etc/pve/storage.cfg`, declared. Cluster-scoped like a pool,
3
+ * which is why `path` and `collection` carry no node segment though a storage is used per node.
4
+ *
5
+ * ★ THIS IS THE OBJECT EVERY CONTAINER ALREADY DEPENDS ON AND NOTHING DECLARES. `LxcProps.storage`
6
+ * and the prefix of `LxcProps.ostemplate` (`local:vztmpl/...`) are bare strings, so a container's
7
+ * rootfs points at something outside the graph: rebuild a node and the plan still reads `noop`
8
+ * right up to the create that fails because the storage is not there. Passing this resource's
9
+ * `storage` attribute into `LxcProps.storage` makes the dependency real — Alchemy orders by data
10
+ * flow, so the storage reconciles first because the container reads a value out of it.
11
+ *
12
+ * ⛔ ONE PRIVILEGE COVERS THIS WHOLE FAMILY AND IT IS `Datastore.Allocate` ON `/storage`. Create,
13
+ * update, delete AND the single-object read all check it — `Datastore.Audit` is not enough for
14
+ * any of them. MEASURED: `GET /storage/local` under the auditor-scoped `read` lease answers 403
15
+ * "Permission check failed (/storage/local, Datastore.Allocate)". `pveOperations.read` folds
16
+ * every failure into `undefined`, i.e. "absent", because a 404 is a legitimate answer there and
17
+ * it cannot tell the two apart — so under that lease this resource NEVER CONVERGES, in two
18
+ * directions: reconcile reads "absent" and POSTs a create over a storage that is already defined,
19
+ * and where a create does land the read-back is 403 too, so reconcile dies with "the write
20
+ * returned no error but the object is still absent" about an object it has just built. Both
21
+ * messages point away from the cause.
22
+ * ★ SO WIDEN OR RE-POINT BEFORE THE FIRST PLAN, outside this file: give the credential mount's
23
+ * `read` role `Datastore.Allocate` on `/storage`, or read this family with `provision`. The
24
+ * provision role in the estate this was written for holds `Datastore.AllocateSpace` and
25
+ * `Datastore.Audit`, and NEITHER IS IT — AllocateSpace writes volumes INTO a storage, Allocate
26
+ * DEFINES one — so it 403s here until widened the way `Pool.Allocate` was for `Proxmox.Pool`.
27
+ *
28
+ * ⛔ THERE IS NO `password` PROP AND THERE MUST NEVER BE ONE. PBS and CIFS storages take one at
29
+ * create time, and Alchemy persists attributes UNENCRYPTED — a secret in props is one careless
30
+ * line away from being an attribute, and from there it is in the state store's nightly dump. So
31
+ * `StorageLocator` types `password`, `keyring` and `encryption-key` as `never`: declaring one is
32
+ * a COMPILE ERROR rather than a leak somebody finds in a backup months later. Set it once out of
33
+ * band (`pvesm set <id> --password`); PVE never returns it on read, so nothing here could diff it
34
+ * anyway.
35
+ */
36
+ import { Resource } from 'alchemy';
37
+ import * as Provider from 'alchemy/Provider';
38
+ import * as Effect from 'effect/Effect';
39
+ import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
40
+ import { bool, flag } from './values.ts';
41
+
42
+ /**
43
+ * The create-time, plugin-specific half of a storage: `dir` wants `path`, `nfs` `server`+`export`,
44
+ * `cifs` `server`+`share`, `pbs` `server`+`datastore`, `rbd` `pool`+`monhost`, `lvmthin`
45
+ * `vgname`+`thinpool`, `zfspool` `pool`. A bag rather than two dozen named fields, because PVE
46
+ * ships about that many plugins and each brings its own locator. ⚠️ Never diffed — see `matches`.
47
+ *
48
+ * ⛔ `password`, `keyring` AND `encryption-key` ARE `never` ON PURPOSE — see the ⛔ in the header.
49
+ * POSITIVE-CONTROLLED rather than assumed: all three are a compile error, `TS2322: Type 'string'
50
+ * is not assignable to type 'undefined'`.
51
+ */
52
+ export type StorageLocator = Record<string, string> & {
53
+ password?: never;
54
+ keyring?: never;
55
+ 'encryption-key'?: never;
56
+ };
57
+
58
+ export interface StorageProps extends WithTarget {
59
+ /** PVE's primary key, cluster-wide — the string an LXC's `storage` and `ostemplate` name. */
60
+ storage: string;
61
+ /**
62
+ * `dir` | `nfs` | `cifs` | `pbs` | `zfspool` | `lvmthin` | `rbd` | … ⚠️ CREATE-ONLY, so a changed
63
+ * `type` plans as `noop` (see `matches`). PVE cannot retype a storage: remove and redeclare.
64
+ */
65
+ type: string;
66
+ /** ⚠️ Create-only, one shape per plugin. See `StorageLocator`. */
67
+ locator?: StorageLocator;
68
+ /** `images,rootdir,vztmpl,iso,backup,snippets`. PVE reports `none` for an empty set. */
69
+ content?: string;
70
+ /**
71
+ * Node restriction, e.g. `n1,n2`. ⚠️ A STORAGE WITH A `nodes` LIST DOES NOT EXIST ANYWHERE ELSE:
72
+ * an LXC declared on a node outside it fails at create with "storage not available on node".
73
+ */
74
+ nodes?: string;
75
+ /** Keeps the definition but stops PVE using it. */
76
+ disable?: boolean;
77
+ /** Tells PVE the same volumes are visible from every node — a claim, not a mechanism. */
78
+ shared?: boolean;
79
+ /** `off` | `metadata` | `falloc` | `full`. File-based plugins only. */
80
+ preallocation?: string;
81
+ /**
82
+ * `keep-last=3,keep-daily=7,…`. ⚠️ THE WIRE NAME IS KEPT, HYPHEN AND ALL: a `pruneBackups` that
83
+ * quietly becomes `prune-backups` in the form body is a second name for one thing.
84
+ */
85
+ 'prune-backups'?: string;
86
+ comment?: string;
87
+ /**
88
+ * ⚠️ THE DEPRECATED PREDECESSOR OF `prune-backups`, and the two do not mix — declare one or the
89
+ * other. `0` means UNLIMITED to PVE rather than "unset", hence the `-1` in the attribute.
90
+ */
91
+ maxfiles?: number;
92
+ }
93
+
94
+ export interface StorageAttributes {
95
+ storage: string;
96
+ /** Reported so a plan can show what it points at; never diffed — see `type` above. */
97
+ type: string;
98
+ content: string;
99
+ nodes: string;
100
+ disable: boolean;
101
+ shared: boolean;
102
+ preallocation: string;
103
+ 'prune-backups': string;
104
+ comment: string;
105
+ /** ⚠️ `-1` means "no maxfiles in storage.cfg". `0` is a real setting meaning unlimited. */
106
+ maxfiles: number;
107
+ }
108
+
109
+ export interface ProxmoxStorage extends Resource<
110
+ 'Proxmox.Storage',
111
+ StorageProps,
112
+ StorageAttributes,
113
+ never,
114
+ PveRequirements
115
+ > {}
116
+
117
+ /** ★ `retain` by default — a storage holding volumes cannot be rebuilt. See the ★ in resource.ts. */
118
+ export const ProxmoxStorage = Resource<ProxmoxStorage>('Proxmox.Storage', {
119
+ defaultRemovalPolicy: 'retain',
120
+ });
121
+
122
+ const str = (value: unknown, fallback = '') => (typeof value === 'string' ? value : fallback);
123
+
124
+ const field = (name: string, value: string | undefined): Record<string, string> =>
125
+ value === undefined ? {} : { [name]: value };
126
+
127
+ /**
128
+ * ⚠️ THE CAST DROPS THE THREE `never` GUARDS AND NOTHING ELSE, and it is load-bearing rather than
129
+ * lazy: an intersection carrying OPTIONAL properties is not assignable to `Record<string,string>`
130
+ * at all, so the guards and the form body cannot both exist without one conversion. At runtime a
131
+ * locator is a plain string map; the guards only ever existed to fail a declaration.
132
+ */
133
+ const locatorForm = (locator: StorageLocator | undefined) =>
134
+ ({ ...locator }) as Record<string, string>;
135
+
136
+ /**
137
+ * ⚠️ PVE RE-SERIALISES ITS LIST FIELDS IN ITS OWN ORDER — `content` and `nodes` are parsed into a
138
+ * set and written back sorted, `prune-backups` is a property string that comes back in schema
139
+ * order. Comparing raw strings would report an update for `iso,backup` against `backup,iso` on
140
+ * every plan, and the PUT would "fix" it into the identical string. Trimming matters too: a
141
+ * declared `iso, backup` would otherwise never equal what PVE stores.
142
+ */
143
+ const tokens = (value: string) =>
144
+ value
145
+ .split(',')
146
+ .map((part) => part.trim())
147
+ .filter((part) => part !== '')
148
+ .sort()
149
+ .join(',');
150
+
151
+ const sameList = (declared: string | undefined, live: string) =>
152
+ declared === undefined || tokens(declared) === tokens(live);
153
+
154
+ const same = <T>(declared: T | undefined, live: T) => declared === undefined || declared === live;
155
+
156
+ /**
157
+ * The mutable half, for create and update alike.
158
+ *
159
+ * ⚠️ AN UNDECLARED FIELD IS NEITHER SENT NOR COMPARED: undeclared means UNMANAGED here, unlike
160
+ * `Proxmox.Pool` where an absent comment means `''`. A storage has no single default to fall back
161
+ * on — `content` defaults per plugin, and clearing a field needs an explicit `delete=` parameter
162
+ * rather than an empty value — so a guessed default would rewrite a storage somebody tuned by
163
+ * hand, and comparing one would report drift nobody declared.
164
+ *
165
+ * ⚠️ RECONCILE PUTs UNCONDITIONALLY whenever the object exists (resource.ts), so this form must be
166
+ * safe to re-apply to a storage that already matches. It is: the declared set and nothing else,
167
+ * and PVE leaves absent parameters alone.
168
+ */
169
+ const mutable = (props: StorageProps): Record<string, string> => ({
170
+ ...field('comment', props.comment),
171
+ ...field('content', props.content),
172
+ ...field('disable', flag(props.disable)),
173
+ ...field('maxfiles', props.maxfiles === undefined ? undefined : String(props.maxfiles)),
174
+ ...field('nodes', props.nodes),
175
+ ...field('preallocation', props.preallocation),
176
+ ...field('prune-backups', props['prune-backups']),
177
+ ...field('shared', flag(props.shared)),
178
+ });
179
+
180
+ const handlers = pveHandlers<StorageProps, StorageAttributes>({
181
+ // ⛔ PVE gates this family's ITEM read on the allocate privilege, not the audit one — see
182
+ // `readRole` in resource.ts. The auditor-shaped lease reads 403, which `read` turns into
183
+ // "absent", and the plan then says create for an object that is plainly there.
184
+ readRole: 'provision',
185
+ /**
186
+ * ⚠️ `digest` IS DELIBERATELY NOT AN ATTRIBUTE. PVE returns one on every storage read and it
187
+ * covers storage.cfg as a FILE, not this section of it, so keeping it would rewrite this
188
+ * resource's state whenever an unrelated storage was edited — churn that reads like drift.
189
+ */
190
+ attributes: (live, props) => ({
191
+ comment: str(live['comment']),
192
+ content: str(live['content']),
193
+ disable: bool(live['disable']),
194
+ maxfiles: typeof live['maxfiles'] === 'number' ? live['maxfiles'] : -1,
195
+ nodes: str(live['nodes']),
196
+ preallocation: str(live['preallocation']),
197
+ 'prune-backups': str(live['prune-backups']),
198
+ shared: bool(live['shared']),
199
+ storage: props.storage,
200
+ type: str(live['type'], props.type),
201
+ }),
202
+ collection: () => 'storage',
203
+ /**
204
+ * ⚠️ THE BAG IS SPREAD FIRST SO NOTHING IN IT CAN SHADOW A FIELD THIS RESOURCE MANAGES. A stray
205
+ * `storage` or `type` in a locator would otherwise rename the object being created, and PVE
206
+ * would build the wrong thing under a name Alchemy then records as the declared one.
207
+ */
208
+ createForm: (props) => ({
209
+ ...locatorForm(props.locator),
210
+ ...mutable(props),
211
+ storage: props.storage,
212
+ type: props.type,
213
+ }),
214
+ /**
215
+ * ⚠️ `type` AND THE LOCATOR ARE NEVER COMPARED, AND AN UNDECLARED FIELD IS NOT COMPARED EITHER.
216
+ * PVE returns the create-only fields on read but refuses them on PUT, so diffing one could only
217
+ * plan an update that no write can apply: a plan that reports work on every run, forever.
218
+ */
219
+ matches: (attributes, props) =>
220
+ sameList(props.content, attributes.content) &&
221
+ sameList(props.nodes, attributes.nodes) &&
222
+ sameList(props['prune-backups'], attributes['prune-backups']) &&
223
+ same(props.disable, attributes.disable) &&
224
+ same(props.shared, attributes.shared) &&
225
+ same(props.preallocation, attributes.preallocation) &&
226
+ same(props.comment, attributes.comment) &&
227
+ same(props.maxfiles, attributes.maxfiles),
228
+ path: (props) => `storage/${props.storage}`,
229
+ updateForm: mutable,
230
+ });
231
+
232
+ /**
233
+ * ⛔ EMPTY, AND HERE IT MATTERS MORE THAN ANYWHERE ELSE IN THIS PACKAGE. `GET /storage`
234
+ * returns every definition on the cluster, `local` and `local-lvm`/`local-zfs` included:
235
+ * the installer's own, that every guest's rootfs sits on. Adopting those would put
236
+ * Alchemy one `delete` away from the cluster's disks. Adoption is explicit, always.
237
+
238
+ *
239
+ * ⚠️ THIS REMOVES THE DEFINITION, NOT THE DATA. PVE drops the section from storage.cfg and
240
+ * leaves the volumes on disk untouched — so the loss is not the bytes, it is that guests
241
+ * whose config still names this storage can no longer resolve their own disks.
242
+
243
+ */
244
+ export const ProxmoxStorageProvider = () =>
245
+ Provider.effect(ProxmoxStorage, Effect.succeed(ProxmoxStorage.Provider.of(handlers)));