@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,130 @@
1
+ /**
2
+ * The create lane for `Proxmox.ZfsPool`: the form PVE wants, and the wait for the worker it forks.
3
+ *
4
+ * ★ SPLIT OUT OF zfs-pool.ts TO KEEP BOTH FILES UNDER THE 250-LINE CAP, and the seam is a real one
5
+ * rather than a convenient line number. This file answers "how does a declaration become a pool
6
+ * on disks, and how do we know it landed". zfs-pool.ts answers "what is a zpool, what can be
7
+ * said about a live one, and when has it changed" — and on this family the answer to the last of
8
+ * those is "never", which is why the two halves have so little to say to each other.
9
+ *
10
+ * ⚠️ THE `import type` BACK TO zfs-pool.ts IS A CYCLE ON PAPER ONLY — it is type-only, so it is
11
+ * erased before anything runs, and the resource's public shape stays in the file that declares
12
+ * the resource rather than being moved somewhere odd to dodge the arrow. Same as
13
+ * metric-server-form.ts.
14
+ *
15
+ * ⚠️ `createPool` TAKES THE READ AS A PARAMETER, GENERIC IN ITS ERROR AND REQUIREMENT CHANNELS,
16
+ * rather than importing `pveOperations` and rebuilding the spec here. Two `pveOperations` over
17
+ * one spec would be two closures nobody can prove are the same; passing the one the resource
18
+ * already built means this file cannot accidentally read the cluster differently from `diff`.
19
+ */
20
+ import * as Effect from 'effect/Effect';
21
+ import { pve } from './client.ts';
22
+ import { text } from './values.ts';
23
+ import type { ZfsPoolAttributes, ZfsPoolProps } from './zfs-pool.ts';
24
+
25
+ /**
26
+ * The POST body. Every field in it is write-only — nothing here is ever read back.
27
+ *
28
+ * ⚠️ `node` IS NOT IN THE BODY: it is already the `nodes/{node}` segment of the URL being POSTed
29
+ * to, and a second copy can only disagree with it. Same reasoning as `id` in metric-server.ts.
30
+ * ⚠️ `devices` IS JOINED IN DECLARED ORDER AND NOT PUT THROUGH `csv()` FROM values.ts. `csv` sorts,
31
+ * and PVE walks this list two at a time to pair `raid10` mirrors — MEASURED in the node's own
32
+ * `PVE::API2::Disks::ZFS` — so sorting it builds a different pool from the one declared. The
33
+ * list is never compared against anything, so it needs no normalisation, only faithful order.
34
+ * ⚠️ AN OMITTED OPTIONAL IS NOT SENT. PVE's own defaults (ashift 12, compression on) are then
35
+ * applied by the node. Sending a guessed default would be indistinguishable here — the values
36
+ * are unreadable afterwards either way — but it would put a number in the request that no
37
+ * declaration asked for, and this is the one call that writes to physical disks.
38
+ */
39
+ export const createForm = (props: ZfsPoolProps): Record<string, string> => ({
40
+ devices: props.devices.join(','),
41
+ name: props.name,
42
+ raidlevel: props.raidlevel,
43
+ ...(props.ashift === undefined ? {} : { ashift: String(props.ashift) }),
44
+ ...(props.compression === undefined ? {} : { compression: props.compression }),
45
+ ...(props['draid-config'] === undefined ? {} : { 'draid-config': props['draid-config'] }),
46
+ });
47
+
48
+ /** 30 reads, 2s apart. A create that has not landed in a minute has gone wrong, not gone slow. */
49
+ const SETTLE_ATTEMPTS = 30;
50
+
51
+ /**
52
+ * Create the pool if it is absent, and otherwise leave it completely alone.
53
+ *
54
+ * ⛔ THE POST ONLY FORKS A WORKER. MEASURED in `/usr/share/perl5/PVE/API2/Disks/ZFS.pm` on n2: the
55
+ * create handler ends in `$rpcenv->fork_worker('zfscreate', ...)` and its HTTP answer is a UPID
56
+ * returned the instant the worker is forked — before `zpool create` has run, let alone finished.
57
+ * A read-back taken immediately, as `pveOperations.reconcile` takes it, finds nothing and would
58
+ * report a successful create as the failure "the write returned no error but the object is still
59
+ * absent". THAT is why this function exists instead of the factory's reconcile.
60
+ *
61
+ * ⚠️ THERE IS NO UPDATE BRANCH BECAUSE PVE HAS NO PUT HERE. An existing pool is returned exactly as
62
+ * read, unwritten: the only write this resource ever makes is the create of a pool that is not
63
+ * there. Do not add a "repair" write; there is no field it could set.
64
+ *
65
+ * ⛔ AND IF THE READ IS WRONG, THE POST IS STILL SAFE — measured, not hoped for. `read` folds a 403
66
+ * or an unreachable node into "absent", so a too-narrow lease would send this down the create
67
+ * path over a live pool. PVE's handler calls `get_pool_data()` before it forks anything and dies
68
+ * with "pool '<name>' already exists on node '<node>'", and `assert_disk_unused` refuses every
69
+ * device that pool is holding. A create aimed at a pool that is really there fails loudly rather
70
+ * than wiping it.
71
+ *
72
+ * ⚠️ SHORT POLLS WITH A HARD CAP, NOT ONE LONG WAIT. `read` cannot tell "not yet" from "forbidden"
73
+ * — both are `undefined` — so the loop gives up with the UPID rather than retrying forever, and
74
+ * the UPID is the only thing that leads to the worker's real error.
75
+ */
76
+ export const createPool = <E, R>(
77
+ props: ZfsPoolProps,
78
+ collection: string,
79
+ path: string,
80
+ read: (props: ZfsPoolProps) => Effect.Effect<ZfsPoolAttributes | undefined, E, R>,
81
+ ) =>
82
+ Effect.gen(function* () {
83
+ const existing = yield* read(props);
84
+ if (existing !== undefined) return existing;
85
+
86
+ const upid = yield* pve<string>(
87
+ props.target,
88
+ 'provision',
89
+ 'POST',
90
+ collection,
91
+ createForm(props),
92
+ );
93
+
94
+ const settle = (attempts: number): Effect.Effect<ZfsPoolAttributes | undefined, E, R> =>
95
+ Effect.gen(function* () {
96
+ const live = yield* read(props);
97
+ if (live !== undefined || attempts <= 0) return live;
98
+ yield* Effect.sleep('2 seconds');
99
+ return yield* settle(attempts - 1);
100
+ });
101
+
102
+ const after = yield* settle(SETTLE_ATTEMPTS);
103
+ if (after === undefined) {
104
+ return yield* Effect.die(
105
+ new Error(
106
+ `${path}: POST returned ${text(upid, '(no UPID)')} but the pool has not appeared. That ` +
107
+ 'POST only forks a `zfscreate` worker, so the task holds the real error -- read it ' +
108
+ `with \`pvesh get /nodes/${props.node}/tasks/<upid>/log\`. NOTHING WAS RETRIED AND ` +
109
+ 'NOTHING WAS CLEANED UP: if the worker reached `zpool create`, the declared devices ' +
110
+ 'have already been written to.',
111
+ ),
112
+ );
113
+ }
114
+ return after;
115
+ });
116
+
117
+ /**
118
+ * ⛔ `DELETE /nodes/{node}/disks/zfs/{name}` RUNS `zpool destroy` AND THE DATASETS GO WITH IT.
119
+ * It is implemented rather than stubbed, because a `delete` that silently does nothing lies to
120
+ * whoever reads the plan. What keeps a deleted line from destroying a pool is the resource's
121
+ * `defaultRemovalPolicy: 'retain'` above — an orphaned pool is forgotten, not destroyed, and a
122
+ * caller who means it says `.pipe(RemovalPolicy.destroy())`.
123
+ *
124
+ * ⚠️ `cleanup-config` IS NOT SENT. It would additionally remove the PVE storage entry pointing
125
+ * at this pool, which is a different object with its own resource (`Proxmox.Storage`); one
126
+ * resource reaching over to delete another's is how a stack ends up with state describing
127
+ * something that is gone.
128
+ */
129
+ export const destroyPool = (olds: ZfsPoolProps, path: string) =>
130
+ pve(olds.target, 'provision', 'DELETE', path);
@@ -0,0 +1,253 @@
1
+ /**
2
+ * `Proxmox.ZfsPool` — a zpool built on one node's physical disks, declared.
3
+ *
4
+ * ⛔ A CREATE HERE WIPES THE DISKS IT IS GIVEN AND THERE IS NO UNDO. `POST /nodes/{node}/disks/zfs`
5
+ * runs `zpool create` on the block devices named in `devices`; whatever was on them is gone the
6
+ * moment the worker runs. Every other decision in this file follows from that one sentence, and
7
+ * each of them is written out below rather than left to be inferred.
8
+ *
9
+ * ⛔ DESTROY IS REFUSED. `delete` MAKES NO API CALL — removing this resource from a stack leaves
10
+ * the zpool and its data exactly where they are, and Alchemy simply forgets about them. The
11
+ * asymmetry is the whole argument: an ORPHANED pool is recoverable (declare it again, or destroy
12
+ * it by hand once a human has looked at which disks are in it), a DESTROYED pool is not. Alchemy
13
+ * deletes for reasons that have nothing to do with anybody intending it — a line removed from a
14
+ * stack file, a resource renamed so the old id is dropped, a whole stack torn down by the wrong
15
+ * command, a replace triggered by a prop nobody meant to touch. Handing `zpool destroy` to any
16
+ * of those is not a feature. MEASURED in the node's own source: the DELETE handler forks
17
+ * `zfsremove`, which runs `zpool destroy <name>` unconditionally, and with `cleanup-disks` then
18
+ * calls `wipe_blockdev` on every member. Neither flag is offered as a prop, because there is no
19
+ * call here for them to reach.
20
+ * ★ THE OPERATOR'S PATH IS `pvesh delete /nodes/<node>/disks/zfs/<name>`, typed by a person who
21
+ * has just looked at the pool. Do not "complete the CRUD" by wiring `delete` to `ops.destroy`:
22
+ * that one line turns a text edit into an unrecoverable one.
23
+ *
24
+ * ⛔ THERE IS NO PUT ON THIS FAMILY AT ALL. MEASURED from the cluster's own published schema
25
+ * (`/usr/share/pve-docs/api-viewer/apidoc.js`, read on n2 2026-09-13): `/nodes/{node}/disks/zfs`
26
+ * has GET and POST, `/nodes/{node}/disks/zfs/{name}` has GET and DELETE, and that is the whole
27
+ * surface. So `updateForm` is omitted — which, per `resource.ts`, makes any `matches` = false a
28
+ * REPLACE. A replace is a delete followed by a create, and on this family the delete is the
29
+ * thing above. Read the ⛔ on `matches` before adding anything to it.
30
+ *
31
+ * ⛔ NOT ONE CREATE PARAMETER COMES BACK ON READ, SO `matches` COMPARES NOTHING. MEASURED against
32
+ * the live cluster: `GET /nodes/n2/disks/zfs/rpool` answers exactly
33
+ * `{action, children, errors, leaf, name, scan, state, status}` — no `ashift`, no `compression`,
34
+ * no `raidlevel`, no `devices`, no `add_storage`. Every create parameter is write-only. There is
35
+ * therefore no field a declaration and a live pool can both be asked about, and a `matches` that
36
+ * invented one would plan a replace — i.e. a `zpool destroy` — over a difference it could never
37
+ * verify in the first place.
38
+ *
39
+ * ⚠️ AND WHAT THE INDEX RETURNS IS LIVE TELEMETRY, NOT CONFIGURATION. `GET /nodes/n2/disks/zfs`
40
+ * gives `alloc`, `free`, `frag`, `dedup`, `size`, `health`. MEASURED: two reads of n2 seconds
41
+ * apart returned `alloc` 14474944512 then 14472740864 — it moves on its own, with no declaration
42
+ * anywhere near it. `scan` in the detail read moves the same way (it carries the last scrub), and
43
+ * `status`/`action` appear and vanish as ZFS feature flags and faults come and go. None of them
44
+ * is compared, and the three churning strings are kept out of the state store entirely rather
45
+ * than rewriting this resource's row every time somebody writes a file.
46
+ *
47
+ * ⚠️ TWO HANDLERS ARE OVERRIDDEN AND THE OTHER THREE COME FROM THE FACTORY — see the ★ above
48
+ * `handlers`. `delete` is the ⛔ above. `reconcile` is because the create is a FORKED WORKER, so
49
+ * the factory's immediate read-back would report a successful create as a failure; the measured
50
+ * detail is in the ⛔ on `createPool` in zfs-pool-create.ts.
51
+ *
52
+ * ⚠️ PRIVILEGES, FROM THE SCHEMA. Read and diff need `Sys.Audit` on `/` — both GETs check it.
53
+ * Reconcile needs `Sys.Modify` on `/` for the POST. `Datastore.Allocate` on `/storage` is NOT
54
+ * needed, because `add_storage` is not offered — see the ⛔ on `ZfsPoolProps`. `delete` needs
55
+ * nothing at all, since it calls nothing.
56
+ */
57
+ import { Resource } from 'alchemy';
58
+ import * as Provider from 'alchemy/Provider';
59
+ import * as Effect from 'effect/Effect';
60
+ import {
61
+ type PveRequirements,
62
+ type PveSpec,
63
+ type WithTarget,
64
+ pveHandlers,
65
+ pveOperations,
66
+ } from './resource.ts';
67
+ import { text } from './values.ts';
68
+ import { createForm, createPool, destroyPool } from './zfs-pool-write.ts';
69
+
70
+ /** PVE's layouts. ⚠️ Each has a minimum disk count PVE enforces, and `raid10` needs an even one. */
71
+ export type ZfsRaidLevel =
72
+ | 'single'
73
+ | 'mirror'
74
+ | 'raid10'
75
+ | 'raidz'
76
+ | 'raidz2'
77
+ | 'raidz3'
78
+ | 'draid'
79
+ | 'draid2'
80
+ | 'draid3';
81
+
82
+ export type ZfsCompression = 'on' | 'off' | 'gzip' | 'lz4' | 'lzjb' | 'zle' | 'zstd';
83
+
84
+ /**
85
+ * ⛔ THERE IS NO `add_storage`, `cleanup-config` OR `cleanup-disks` PROP, AND THAT IS NOT AN
86
+ * OVERSIGHT. The last two belong to the DELETE this resource refuses to make. `add_storage=1`
87
+ * would have the POST build a `Proxmox.Storage`-shaped object this resource does not own, cannot
88
+ * see on any later read, and cannot remove — invisible to every diff forever, and a collision
89
+ * waiting for the day somebody also declares the storage properly. Declare a `Proxmox.Storage`
90
+ * with `type: 'zfspool'` and `locator: { pool: <this pool's name> }` instead, reading the `name`
91
+ * attribute below so Alchemy orders the storage after the pool.
92
+ */
93
+ export interface ZfsPoolProps extends WithTarget {
94
+ /**
95
+ * ⛔ A ZPOOL IS NODE-LOCAL AND THIS IS HALF ITS IDENTITY. Changing it moves nothing: `path` then
96
+ * points at a different node, the read answers absent, and reconcile tries to CREATE the pool
97
+ * over there — on whatever `/dev/...` happens to answer to those names on that machine.
98
+ * Declare a separate resource per node instead.
99
+ */
100
+ node: string;
101
+ /** The zpool name, and PVE's key for it on this node. ⚠️ Changing it creates a SECOND pool. */
102
+ name: string;
103
+ /**
104
+ * The block devices to build it on.
105
+ *
106
+ * ⛔ NAME THEM BY `/dev/disk/by-id/...`, NOT `/dev/sdb`. Kernel names are not stable across
107
+ * reboots; a by-id link is. MEASURED in the node's source: PVE resolves what it is handed to a
108
+ * by-id link only AFTER it has decided the device is free, so a stale `/dev/sdb` in a stack
109
+ * file is checked against, and then wiped, whichever disk currently answers to that name.
110
+ * ⚠️ ORDER IS MEANING FOR `raid10` AND IS PRESERVED — PVE walks the list two at a time and makes
111
+ * each pair a mirror. This is why the form uses a plain join and NOT `csv()` from values.ts:
112
+ * `csv` sorts, and sorting this list silently re-pairs the mirrors into a different pool.
113
+ * ⚠️ PVE's only guard is `assert_disk_unused`, i.e. its own `disk_is_used` over the same `used`
114
+ * column the Disks view shows. A blank disk it considers free is wiped with no confirmation.
115
+ */
116
+ devices: readonly string[];
117
+ raidlevel: ZfsRaidLevel;
118
+ /**
119
+ * Sector size exponent, 9–16, PVE default 12.
120
+ *
121
+ * ⛔ WRITE-ONLY AND IMMUTABLE FOR THE LIFE OF THE POOL. It is consumed by `zpool create -o
122
+ * ashift=` and never returned by any read on this family, so changing it later plans as `noop`
123
+ * and changes nothing. A different ashift means destroying and rebuilding the pool, by hand,
124
+ * with the data moved off first.
125
+ */
126
+ ashift?: number;
127
+ /**
128
+ * ⚠️ Write-only in the same way as `ashift`: applied by `zfs set compression=` at create time and
129
+ * never read back, so a later change plans `noop`. Change it with `zfs set` on the node.
130
+ */
131
+ compression?: ZfsCompression;
132
+ /**
133
+ * `data=<integer>,spares=<integer>`. ⚠️ THE WIRE NAME IS KEPT, HYPHEN AND ALL, for the reason
134
+ * `storage.ts` keeps `prune-backups`: a second spelling is a second name for one thing.
135
+ * ⚠️ PVE refuses it outright unless `raidlevel` is one of the `draid*` levels.
136
+ */
137
+ 'draid-config'?: string;
138
+ }
139
+
140
+ /**
141
+ * ⚠️ EVERYTHING HERE IS REPORTED, NOTHING HERE IS COMPARED — see the ⛔ on `matches`. These exist
142
+ * so a plan and the state store can say what the pool actually is, on a family where the
143
+ * declaration and the live object share no comparable field at all.
144
+ */
145
+ export interface ZfsPoolAttributes {
146
+ /** ★ The value a `Proxmox.Storage` should read to order itself after this pool. */
147
+ name: string;
148
+ node: string;
149
+ /** `ONLINE` | `DEGRADED` | `FAULTED` | … Health, not configuration. */
150
+ state: string;
151
+ /** ZFS's own error summary, e.g. `No known data errors`. */
152
+ errors: string;
153
+ /**
154
+ * The leaf devices ZFS reports, in vdev order, comma-joined.
155
+ *
156
+ * ⚠️ THIS IS WHAT ZFS RESOLVED, NOT WHAT WAS DECLARED, and the two normally differ: PVE rewrites
157
+ * a `/dev/sdb` into a by-id link before creating, and ZFS reports partition paths (`…-part3`)
158
+ * for a pool built on partitions — MEASURED, that is exactly what n2's `rpool` reports. Never
159
+ * compare it with `props.devices`.
160
+ */
161
+ devices: string;
162
+ }
163
+
164
+ export interface ProxmoxZfsPool extends Resource<
165
+ 'Proxmox.ZfsPool',
166
+ ZfsPoolProps,
167
+ ZfsPoolAttributes,
168
+ never,
169
+ PveRequirements
170
+ > {}
171
+
172
+ /** ★ `retain` by default — a pool holding datasets cannot be rebuilt. See the ★ in resource.ts. */
173
+ export const ProxmoxZfsPool = Resource<ProxmoxZfsPool>('Proxmox.ZfsPool', {
174
+ defaultRemovalPolicy: 'retain',
175
+ });
176
+
177
+ /**
178
+ * The device paths at the bottom of PVE's vdev tree, in the order ZFS reports them.
179
+ *
180
+ * ⚠️ "NO CHILDREN" IS THE LEAF TEST, AND IT IS NOT A GUESS — MEASURED in the node's `preparetree`,
181
+ * which sets `leaf` to 0 when there are children and 1 otherwise. Reading the flag would work
182
+ * equally well; recursing on `children` needs no second field to be present and is what makes
183
+ * nested sections (mirrors inside a raid10, `spares`, `cache`) flatten correctly.
184
+ */
185
+ const leafDevices = (children: unknown): string[] =>
186
+ Array.isArray(children)
187
+ ? children.flatMap((entry: unknown) => {
188
+ const vdev = entry as { children?: unknown; name?: unknown };
189
+ const nested = leafDevices(vdev.children);
190
+ return nested.length > 0 ? nested : [text(vdev.name)].filter((name) => name !== '');
191
+ })
192
+ : [];
193
+
194
+ const spec: PveSpec<ZfsPoolProps, ZfsPoolAttributes> = {
195
+ /**
196
+ * ⚠️ `scan`, `status` AND `action` ARE DELIBERATELY NOT ATTRIBUTES. All three are advisory
197
+ * strings ZFS rewrites on its own — `scan` on every scrub, the other two as feature flags and
198
+ * faults come and go — so keeping them would rewrite this resource's state row for reasons no
199
+ * declaration caused. Same reasoning as `digest` in storage.ts.
200
+ */
201
+ attributes: (live, props) => {
202
+ // ⚠️ A detail answer with no `name` is not a pool. `name` is non-optional in PVE's schema, so
203
+ // this is a shape check rather than a default — and "absent" is the honest reading of a
204
+ // reply that does not describe the object that was asked for.
205
+ if (text(live['name']) === '') return undefined;
206
+ return {
207
+ devices: leafDevices(live['children']).join(','),
208
+ errors: text(live['errors'], 'unknown'),
209
+ name: props.name,
210
+ node: props.node,
211
+ state: text(live['state'], 'unknown'),
212
+ };
213
+ },
214
+ collection: (props) => `nodes/${props.node}/disks/zfs`,
215
+ createForm,
216
+ /**
217
+ * ⛔ IT ALWAYS ANSWERS TRUE, AND THAT IS THE POINT OF THIS FILE. A pool that is there under the
218
+ * declared name on the declared node IS the declaration, because there is nothing else the two
219
+ * can be compared on: every create parameter is write-only (see the ⛔ in the header) and
220
+ * everything the read does return is telemetry that moves by itself. So declaring what is live
221
+ * plans as `noop` — the only honest answer available, and the one the live cluster needs:
222
+ * `rpool` exists on n2, n3 and n4 with nothing about its construction readable.
223
+ * ⛔ ANYTHING ADDED HERE IS A REPLACE, NOT AN UPDATE. `updateForm` is omitted because PVE has
224
+ * no PUT, so `resource.ts` turns a false into `{action:'replace'}` — delete then create — on
225
+ * a family whose delete is `zpool destroy`. A comparison added here would be one plan away
226
+ * from destroying a pool over a field it cannot even read back. If a future PVE grows a PUT,
227
+ * add `updateForm` FIRST and only then consider comparing what that PUT accepts.
228
+ */
229
+ matches: () => true,
230
+ path: (props) => `nodes/${props.node}/disks/zfs/${props.name}`,
231
+ };
232
+
233
+ const ops = pveOperations(spec);
234
+
235
+ /**
236
+ * ★ THREE HANDLERS COME FROM THE FACTORY AND TWO DO NOT, SO THE TWO ARE THE ONLY ONES WRITTEN OUT.
237
+ * `list`, `read` and `diff` are the factory's, unchanged — the spread is what proves it, rather
238
+ * than three re-typed delegations nobody rereads. `reconcile` and `delete` are the two the header
239
+ * explains, and they are the only places this family departs from every other resource here.
240
+ * This is the acl.ts precedent, for a different reason: there the factory's delete hits an
241
+ * endpoint PVE does not implement, here it hits one that works and destroys the data.
242
+ */
243
+ const handlers = {
244
+ ...pveHandlers(spec),
245
+ delete: Effect.fn(function* ({ olds }: { olds: ZfsPoolProps }) {
246
+ yield* destroyPool(olds, spec.path(olds));
247
+ }),
248
+ reconcile: ({ news }: { news: ZfsPoolProps }) =>
249
+ createPool(news, spec.collection(news), spec.path(news), ops.read),
250
+ };
251
+
252
+ export const ProxmoxZfsPoolProvider = () =>
253
+ Provider.effect(ProxmoxZfsPool, Effect.succeed(ProxmoxZfsPool.Provider.of(handlers)));
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The Talos client credential a reconcile runs with: minted from OpenBao, written to a temp file.
3
+ *
4
+ * ⛔ TALOSCONFIG AND secrets.yaml HOLD CLIENT CERTIFICATES AND CLUSTER CA PRIVATE KEYS. Alchemy
5
+ * persists resource attributes WITHOUT encryption — StateEncoding.ts writes `Redacted` as
6
+ * `{"@redacted": <plaintext>}` — and this estate's state store is the `alchemy` Postgres that
7
+ * pg-backup.sh dumps nightly. Neither file may be a prop, an attribute, or a log line.
8
+ *
9
+ * ★ MODELED ON house/proxmox/src/credentials.ts: mint at call time through `ChildProcessSpawner`,
10
+ * use the material to build a client, drop it when the scope ends. The difference is Talos wants a
11
+ * FILE (`--talosconfig` / `TALOSCONFIG`), so mint writes a temp path and registers cleanup.
12
+ *
13
+ * ⚠️ REASONED FROM THE PUBLISHED TALOS WORKFLOW, NOT MEASURED ON THIS ESTATE. There is no Talos
14
+ * cluster here yet; paths below follow the same `mount/data/…` shape other infra mounts use.
15
+ */
16
+ import { closeSync, openSync, writeFileSync } from 'node:fs';
17
+ import * as Effect from 'effect/Effect';
18
+ import * as Stream from 'effect/Stream';
19
+ import * as ChildProcess from 'effect/unstable/process/ChildProcess';
20
+ import * as ChildProcessSpawner from 'effect/unstable/process/ChildProcessSpawner';
21
+
22
+ /** Where credentials come from. HomeFlare-specific mount names live in the stack, not here. */
23
+ export type TalosTarget = {
24
+ /** OpenBao KV mount holding Talos material, e.g. `talos-tb4`. */
25
+ readonly mount: string;
26
+ /** Logical cluster name — safe to persist and log. */
27
+ readonly cluster: string;
28
+ /**
29
+ * Path under the mount for the talosconfig YAML.
30
+ *
31
+ * ⚠️ DEFAULT IS REASONED. The first real deploy will confirm or correct the path; a wrong path
32
+ * fails closed with a bao error rather than writing an empty talosconfig.
33
+ */
34
+ readonly talosconfigKey?: string;
35
+ };
36
+
37
+ export type TalosCredential = {
38
+ /** Absolute path to a temp talosconfig. ⛔ NEVER PERSIST, NEVER LOG CONTENTS. */
39
+ readonly talosconfigPath: string;
40
+ };
41
+
42
+ const defaultKey = (target: TalosTarget) => target.talosconfigKey ?? 'data/talosconfig';
43
+
44
+ /**
45
+ * Mint one talosconfig file for `target`.
46
+ *
47
+ * ★ SHELLS OUT TO `bao` RATHER THAN SPEAKING THE HTTP API — same reasoning as proxmox credentials.
48
+ * The estate's BAO_ADDR / BAO_NAMESPACE / approle contract already lives in that binary.
49
+ */
50
+ export const mintTalosconfig = (target: TalosTarget) =>
51
+ Effect.scoped(
52
+ Effect.gen(function* () {
53
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
54
+ const key = defaultKey(target);
55
+ const result = yield* ChildProcess.make(
56
+ 'bao',
57
+ ['kv', 'get', '-format=json', `${target.mount}/${key}`],
58
+ { detached: false, extendEnv: true, stderr: 'pipe', stdin: 'ignore', stdout: 'pipe' },
59
+ ).pipe(
60
+ spawner.spawn,
61
+ Effect.flatMap((child) =>
62
+ Effect.all(
63
+ {
64
+ exitCode: child.exitCode,
65
+ stderr: child.stderr.pipe(
66
+ Stream.decodeText,
67
+ Stream.mkString,
68
+ Effect.map((text) => text.trim()),
69
+ ),
70
+ stdout: child.stdout.pipe(
71
+ Stream.decodeText,
72
+ Stream.mkString,
73
+ Effect.map((text) => text.trim()),
74
+ ),
75
+ },
76
+ { concurrency: 'unbounded' },
77
+ ),
78
+ ),
79
+ Effect.scoped,
80
+ );
81
+
82
+ if (result.exitCode !== 0) {
83
+ // ⛔ stderr ONLY — stdout may hold talosconfig YAML with client credentials.
84
+ return yield* Effect.fail(
85
+ new Error(
86
+ `bao kv get ${target.mount}/${key} exited ${String(result.exitCode)}: ${result.stderr}`,
87
+ ),
88
+ );
89
+ }
90
+
91
+ const parsed = JSON.parse(result.stdout) as { data?: { data?: Record<string, unknown> } };
92
+ const data = parsed.data?.data;
93
+ const raw = data?.['talosconfig'] ?? data?.['config'];
94
+ if (typeof raw !== 'string' || raw.trim() === '') {
95
+ return yield* Effect.fail(
96
+ new Error(
97
+ `${target.mount}/${key} returned no talosconfig field. Expected a KV key named 'talosconfig' or 'config' — never store the value in Alchemy props.`,
98
+ ),
99
+ );
100
+ }
101
+
102
+ const talosconfigPath = `${Bun.env['TMPDIR'] ?? '/tmp'}/hf-talos-${target.cluster}-${Bun.randomUUIDv7()}.yaml`;
103
+ /**
104
+ * ⛔ 0600, CREATED EXCLUSIVELY, AND `Bun.write` CANNOT DO EITHER. This file holds the
105
+ * cluster's admin client certificate — everything talosctl needs to reconfigure or wipe a
106
+ * node. `Bun.write` takes no mode, so it lands at the process umask, which on this estate
107
+ * is 0644: world-readable, in a world-readable directory, for as long as the resource
108
+ * runs. The name is unguessable, and "unguessable" is not a permission.
109
+ * ⚠️ `wx` IS THE OTHER HALF. Opening with O_EXCL means this cannot be made to write through
110
+ * a path an attacker pre-created — the classic /tmp symlink race — and with a UUIDv7 name
111
+ * a collision is a genuine error rather than something to paper over.
112
+ * ⚠️ THE FINALIZER BELOW REMOVES IT ON EVERY EXIT PATH, but a hard kill (SIGKILL, power
113
+ * loss) leaves it behind. 0600 is what makes that survivable rather than a disclosure.
114
+ */
115
+ yield* Effect.try({
116
+ try: () => {
117
+ const fd = openSync(talosconfigPath, 'wx', 0o600);
118
+ try {
119
+ writeFileSync(fd, raw);
120
+ } finally {
121
+ closeSync(fd);
122
+ }
123
+ },
124
+ catch: (cause) => new Error(`writing temp talosconfig: ${String(cause)}`),
125
+ });
126
+
127
+ yield* Effect.addFinalizer(() =>
128
+ Effect.tryPromise({
129
+ try: () => Bun.file(talosconfigPath).delete(),
130
+ catch: () => undefined,
131
+ }).pipe(Effect.orElseSucceed(() => undefined)),
132
+ );
133
+
134
+ return { talosconfigPath } satisfies TalosCredential;
135
+ }),
136
+ );
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Talos providers for Alchemy.
3
+ *
4
+ * ⛔ THIS BARREL IS THE PUBLIC API, AND IT IS DELIBERATELY SMALLER THAN THE DIRECTORY.
5
+ * These are the symbols a real stack consumes; the rest of the files are internals a
6
+ * provider needs but a consumer should not depend on. An `export *` here would publish
7
+ * every helper as API and make the next refactor a breaking change.
8
+ * ★ Anything unlisted is still reachable by path if you genuinely need it — that is a
9
+ * deliberate, visible act rather than an accident of barrelling.
10
+ */
11
+ export { TalosKubeconfigProvider } from './kubeconfig.ts';
12
+ export { TalosBootstrapProvider } from './talos-bootstrap.ts';
13
+ export { TalosClusterHealthProvider } from './talos-cluster-health.ts';
14
+ export { TalosMachineConfigProvider } from './talos-machine-config.ts';