@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,212 @@
1
+ import { Resource } from 'alchemy';
2
+ import { isResolved } from 'alchemy/Diff';
3
+ import * as Provider from 'alchemy/Provider';
4
+ import * as Effect from 'effect/Effect';
5
+ import * as FileSystem from 'effect/FileSystem';
6
+ import * as Path from 'effect/Path';
7
+ import type * as HttpClient from 'effect/unstable/http/HttpClient';
8
+ import { sha256 } from './digest.ts';
9
+ import { isEmptyAssembly } from './policy-assembly.ts';
10
+ import { deletePolicy, readPolicy, writePolicy } from './policy-wire.ts';
11
+
12
+ /**
13
+ * An OpenBao ACL policy, assembled from the HCL fragments that declare it.
14
+ *
15
+ * ★ WHY THIS EXISTS. The estate already hand-rolled both halves of what Alchemy provides:
16
+ * five `apply-*.sh`/`apply-*.py` scripts converge OpenBao, and separate `check-*.sh`
17
+ * gates compare live to repo and refuse on drift. That is `reconcile` and `diff` written
18
+ * twice, in two languages, with nothing recording what was applied — and one of those
19
+ * gates blocked a deploy today because its own path had rotted.
20
+ *
21
+ * ⛔ THERE IS NO `Bao.Secret`, AND THERE MUST NEVER BE ONE. Alchemy persists attributes to
22
+ * its state store and does NOT encrypt them: StateEncoding.ts unwraps `Redacted` to
23
+ * plaintext under a marker key, and nothing under alchemy/src/State encrypts. This
24
+ * estate's state store is the `alchemy` Postgres database, which pg-backup.sh dumps
25
+ * nightly (it enumerates EVERY non-template database), copies to CT100, and PBS backs up
26
+ * from there. A KV value here would exist in four places, none of them OpenBao.
27
+ *
28
+ * So this declares the SHAPE AROUND secrets. Every attribute it stores is a name, a
29
+ * count, or a hash.
30
+ *
31
+ * ⛔ EVERY PRIMITIVE IS ALCHEMY'S OR EFFECT'S, NOT NODE'S. An earlier cut of this file used
32
+ * `node:child_process` + `promisify(execFile)` and `node:fs/promises`, which is
33
+ * hand-rolling inside a bun + Effect codebase: it loses the Scope, the typed
34
+ * PlatformError, and the Layer that makes the provider testable without a real vault.
35
+ * ★ `CommandExecutor.run` was tried first and blocked read/diff: Alchemy passes a session
36
+ * to reconcile/delete but NOT to read/diff (Provider.ts:258-289), so the fix was Alchemy's
37
+ * OWN Docker pattern — `ChildProcessSpawner` + `ChildProcess.make`, which needs only a
38
+ * Scope (Docker.ts:476-553). ★ SINCE 2026-09-14 THE CALLS ARE OpenBao's HTTP API through
39
+ * Effect's `HttpClient` (bao-http.ts), which needs no session either, so read/diff still work.
40
+ * FileSystem and Path come from `effect/*` directly — in Effect 4 they moved into core.
41
+ */
42
+ export interface BaoPolicyProps {
43
+ /** Policy name, as `sys/policies/acl/<name>` takes it. */
44
+ name: string;
45
+ /**
46
+ * Directory of `*.hcl` fragments.
47
+ *
48
+ * ⚠️ CONCATENATED IN SORTED ORDER, EACH FOLLOWED BY A NEWLINE — byte-identical to
49
+ * apply-agent-policy.sh, which is what wrote the live policy. Any other joining makes
50
+ * the first plan report an update on a policy nobody changed.
51
+ */
52
+ fragments: string;
53
+ }
54
+
55
+ export interface BaoPolicyAttributes {
56
+ /** Policy name. */
57
+ name: string;
58
+ /** SHA-256 of the assembled HCL. The whole diff, and safe to persist. */
59
+ digest: string;
60
+ /** `path "..."` grants — the number apply-agent-policy.sh prints. */
61
+ grants: number;
62
+ /** Fragment files that produced it, so a plan reads like the directory. */
63
+ parts: number;
64
+ }
65
+
66
+ export interface BaoPolicy extends Resource<
67
+ 'Bao.Policy',
68
+ BaoPolicyProps,
69
+ BaoPolicyAttributes,
70
+ never,
71
+ HttpClient.HttpClient | FileSystem.FileSystem | Path.Path
72
+ > {}
73
+
74
+ /**
75
+ * ⛔ `retain` — DELETING A POLICY REVOKES EVERY GRANT IT CARRIES, and this is the ONLY family in
76
+ * this package with live instances, so it is the only one where the default could ever fire.
77
+ *
78
+ * 🔴 IT WAS THE ONLY FAMILY WITHOUT IT. Every sibling passes `{ defaultRemovalPolicy: 'retain' }`
79
+ * — auth-role.ts:37, mount.ts:40, pki-role.ts, proxmox-role.ts, ssh-role.ts — each with a ★
80
+ * explaining what deletion destroys. This file has the same warning on its own `delete` handler
81
+ * and did not carry the option, which is the worst possible combination: the danger is
82
+ * documented and the guard is missing.
83
+ * ⛔ WHAT THAT MEANT IN PRACTICE, on 2026-09-13: ten live policies are named in a `const` array
84
+ * in alchemy.run.ts. Removing a name from that array orphans the resource, and without
85
+ * `retain` the next deploy calls `bao policy delete` on it — including the 86-line policy that
86
+ * decides what the agent working in this repo can reach. A one-word edit, no plan line saying
87
+ * "revoke", and the grants are gone. The array grew from four names to ten that same day,
88
+ * which multiplied the blast radius of a mistake nobody had guarded against.
89
+ *
90
+ * ⚠️ `retain` DOES NOT WEAKEN `delete`, WHICH STAYS FULLY IMPLEMENTED. The engine skips it for an
91
+ * orphaned resource; a caller that means it opts in with `.pipe(RemovalPolicy.destroy())`. This
92
+ * is Terraform's `prevent_destroy`, not a stubbed operation — a delete that silently did nothing
93
+ * would lie to whoever read the plan.
94
+ */
95
+ export const BaoPolicy = Resource<BaoPolicy>('Bao.Policy', {
96
+ defaultRemovalPolicy: 'retain',
97
+ });
98
+
99
+ /** `path "..."` grant count — the same number the apply script prints. */
100
+ const grantsOf = (hcl: string) => (hcl.match(/^path /gm) ?? []).length;
101
+
102
+ const attributesOf = (name: string, hcl: string, parts: number): BaoPolicyAttributes => ({
103
+ name,
104
+ digest: sha256(hcl),
105
+ grants: grantsOf(hcl),
106
+ parts,
107
+ });
108
+
109
+ export const BaoPolicyProvider = () =>
110
+ Provider.effect(
111
+ BaoPolicy,
112
+ Effect.gen(function* () {
113
+ const fs = yield* FileSystem.FileSystem;
114
+ const path = yield* Path.Path;
115
+
116
+ /** Assemble fragments exactly as apply-agent-policy.sh does. */
117
+ const assemble = (dir: string) =>
118
+ Effect.gen(function* () {
119
+ const entries = (yield* fs.readDirectory(dir)).filter((f) => f.endsWith('.hcl')).sort();
120
+ let joined = '';
121
+ for (const f of entries) joined += `${yield* fs.readFileString(path.join(dir, f))}\n`;
122
+ return { joined, parts: entries.length };
123
+ });
124
+
125
+ return BaoPolicy.Provider.of({
126
+ /**
127
+ * ⛔ `bao policy list` IS NOT A LIST OF THINGS THIS OWNS. Every policy in the
128
+ * namespace answers, including ones written by hand years ago. Returning them
129
+ * would invite Alchemy to adopt — and then delete — policies it never created.
130
+ */
131
+ list: () => Effect.succeed([]),
132
+
133
+ // ★ '' means genuinely absent (a 404); a refusal fails. policy-wire.ts has the history.
134
+ read: Effect.fn(function* ({ olds }) {
135
+ const live = yield* readPolicy(olds.name);
136
+ return live.length === 0 ? undefined : attributesOf(olds.name, live, 0);
137
+ }),
138
+
139
+ /**
140
+ * ⛔ IT COMPARES THE LIVE POLICY, NOT THE STORED DIGEST. A policy edited in the
141
+ * OpenBao UI is exactly the drift the check-* gates exist to catch, and a
142
+ * provider that trusted its own state would report `noop` straight through it.
143
+ */
144
+ diff: Effect.fn(function* ({ news, output }) {
145
+ // ⚠️ A prop can still be an unresolved Output or Config at plan time. Docker's
146
+ // own providers guard with isResolved and skip rather than guess; a diff that
147
+ // read a Config as a string would compare a placeholder to real HCL.
148
+ if (output === undefined || !isResolved(news)) return undefined;
149
+ const { joined } = yield* assemble(news.fragments);
150
+ const live = yield* readPolicy(news.name);
151
+ return live.length > 0 && sha256(live) === sha256(joined)
152
+ ? ({ action: 'noop' } as const)
153
+ : ({ action: 'update' } as const);
154
+ }),
155
+
156
+ reconcile: Effect.fn(function* ({ news }) {
157
+ const name = news.name;
158
+ const { joined, parts } = yield* assemble(news.fragments);
159
+ /**
160
+ * ⛔ AN EMPTY ASSEMBLY IS A REFUSAL, NOT AN EMPTY POLICY. `bao policy write` with
161
+ * no paths SILENTLY REVOKES every grant the policy carries, for every token
162
+ * bound to it — and a mistyped or moved fragments directory reads exactly like
163
+ * a policy with nothing in it. This estate lost a deploy today to a directory
164
+ * that moved; it must not lose a vault to one.
165
+ */
166
+ // ⛔ No FILES was the first guard; no GRANT is the real one — see policy-assembly.ts.
167
+ if (isEmptyAssembly(parts, joined))
168
+ return yield* Effect.die(
169
+ new Error(
170
+ `Bao.Policy ${name}: the fragments under ${news.fragments} carry no path grant ` +
171
+ `(${String(parts)} file(s)). Writing it would revoke every grant the policy carries.`,
172
+ ),
173
+ );
174
+ /**
175
+ * ⛔ ADOPTING A POLICY THAT ALREADY MATCHES MUST NOT REWRITE IT. Alchemy's `adopted`
176
+ * action is NOT a read — Apply.ts routes it down the same branch as `update` and calls
177
+ * reconcile — so the first deploy after declaring the four policies this estate
178
+ * already has would have run `bao policy write` over all four. Byte-identical in the
179
+ * happy case, and a SILENT REWRITE OF LIVE GRANTS the moment the repo fragments have
180
+ * drifted from what is deployed.
181
+ *
182
+ * ★ THE GUARD IS THE SAME EXPRESSION `diff` USES, deliberately: one definition of
183
+ * "settled" for this resource, so the plan and the write can never disagree. The
184
+ * sibling PVE provider does exactly this in pveOperations.reconcile.
185
+ */
186
+ const current = yield* readPolicy(name);
187
+ if (current.length > 0 && sha256(current) === sha256(joined)) {
188
+ return attributesOf(name, joined, parts);
189
+ }
190
+ // ⚠️ In the request body, never on argv — see `writePolicy` for why the temp file went.
191
+ yield* writePolicy(name, joined);
192
+ return attributesOf(name, joined, parts);
193
+ }),
194
+
195
+ /**
196
+ * ⛔ DELETING A POLICY REVOKES EVERY GRANT IT CARRIES. Idempotent as Alchemy
197
+ * requires — already gone is success — but this is the one operation with teeth.
198
+ */
199
+ delete: Effect.fn(function* ({ output }) {
200
+ /**
201
+ * ⛔ A REFUSED DELETE FAILS. This is "the one operation with teeth" the note above
202
+ * names, and it used to run through the read helper, which discarded the exit code —
203
+ * so a REFUSED policy delete reported success and the row left Alchemy's state while
204
+ * every grant it carried stayed live. Idempotence is preserved: a 404 is still
205
+ * success, and a 403 is not (bao-status.ts).
206
+ */
207
+ yield* deletePolicy(output.name);
208
+ return undefined;
209
+ }),
210
+ });
211
+ }),
212
+ );
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Wire and comparison half of Bao.ProxmoxRole — the resource itself is proxmox-role.ts.
3
+ *
4
+ * MEASURED 2026-09-13, read-only, against the live server (BAO_ADDR=http://127.0.0.1:8200,
5
+ * BAO_NAMESPACE=homeflare). No write of any kind was issued to a role or a mount:
6
+ *
7
+ * bao read -format=json proxmox-tb4/roles/read
8
+ * → { max_ttl: 21600, mint_user: "hf-read@pve", name: "read", ttl: 3600 }
9
+ * bao read -format=json proxmox-tb4/roles/provision
10
+ * → { max_ttl: 1800, mint_user: "hf-provision@pve", name: "provision", ttl: 300 }
11
+ * GET /v1/proxmox-tb4/?help=1 → components.schemas.WriteRolesNameRequest:
12
+ * mint_user string — the ONLY required field, described as "The `user@realm` whose
13
+ * standing ACL is the minted token's entire privilege set. Created
14
+ * out-of-band; this engine never grants ACLs."
15
+ * ttl integer — "format": "seconds"
16
+ * max_ttl integer — "format": "seconds", "Narrows the mount's; can never widen it."
17
+ *
18
+ * ★ name + mint_user + ttl + max_ttl IS THE WHOLE ROLE. The plugin stores nothing else, so
19
+ * `matches` below covers 100% of the server-side state: there is no unmanaged field left to
20
+ * drift behind a green plan. That completeness is bought by making the TTLs REQUIRED props
21
+ * rather than the optional-means-unmanaged shape Bao.Mount uses — see the ⛔ on writeBody.
22
+ */
23
+ import { sha256 } from './digest.ts';
24
+ import { mountPath, parseDuration, ttlSeconds } from './mount-form.ts';
25
+
26
+ export interface BaoProxmoxRoleProps {
27
+ /** Mount path of the proxmox secrets engine, e.g. `proxmox-tb4`. A trailing slash is fine. */
28
+ mount: string;
29
+ /**
30
+ * Role name, as `<mount>/roles/<name>` takes it.
31
+ *
32
+ * ⚠️ The plugin's own schema says the name "Appears in the minted token's id, so keep it
33
+ * short." Both live roles are one word (`read`, `provision`). A long name here becomes a
34
+ * long PVE token id on every mint.
35
+ */
36
+ name: string;
37
+ /**
38
+ * ⛔ THE ENTIRE SECURITY BOUNDARY OF THIS RESOURCE. A minted token inherits this PVE user's
39
+ * standing ACL and nothing else — the engine never grants ACLs, it only borrows one. Change
40
+ * this string and every credential the role issues from then on carries a different
41
+ * privilege set, with no other field in the declaration changing to hint at it. That is why
42
+ * it is compared in `matches` below, and why reconcile refuses to move it without
43
+ * `allowMintUserChange`.
44
+ */
45
+ mintUser: string;
46
+ /** Default lease TTL for minted tokens, e.g. `1h`. REQUIRED — see the ⛔ on writeBody. */
47
+ ttl: string;
48
+ /**
49
+ * Maximum lease TTL, e.g. `6h`. REQUIRED — see the ⛔ on writeBody.
50
+ *
51
+ * ⚠️ "Narrows the mount's; can never widen it." A role declaring `6h` under a mount whose
52
+ * max_lease_ttl is `1h` still READS BACK as 21600, so this resource reports noop and the
53
+ * plan is green — while every actual mint is capped at 3600 by the mount. The role is not
54
+ * lying and neither is the plan; the ceiling simply lives one level up. If a consumer gets
55
+ * a shorter lease than declared here, check `bao read sys/mounts/<mount>` before touching
56
+ * the role.
57
+ */
58
+ maxTtl: string;
59
+ /**
60
+ * Permission to re-scope `mintUser` on an EXISTING role: set it to the CURRENT live mint_user
61
+ * that this change is allowed to replace.
62
+ *
63
+ * ★ IT IS A STRING AND NOT A BOOLEAN, AND THAT IS THE WHOLE POINT. A `true` left behind after
64
+ * one legitimate rename would wave through every future re-scope of that role forever —
65
+ * including one pasted in by whoever copies this block as a template. Naming the value being
66
+ * replaced makes the permission SELF-EXPIRING: the moment the change lands, live equals
67
+ * `mintUser`, the guard stops consulting it, and the next re-scope finds a stale string that
68
+ * does not match live and is refused on its own merits.
69
+ *
70
+ * Not persisted, not in attributes, not in `matches` — it describes the operator's intent, not
71
+ * the server, so changing it alone is correctly a noop. See the ⛔ in proxmox-role.ts reconcile.
72
+ */
73
+ allowMintUserChange?: string;
74
+ }
75
+
76
+ export interface BaoProxmoxRoleAttributes {
77
+ /** Mount path, trailing slash stripped. */
78
+ mount: string;
79
+ /** Role name. */
80
+ name: string;
81
+ /** The PVE `user@realm` live tokens are minted under. A name, never a credential. */
82
+ mintUser: string;
83
+ /** Live default lease TTL, rendered back into the duration shape props use. */
84
+ ttl: string;
85
+ /** Live maximum lease TTL, same shape. */
86
+ maxTtl: string;
87
+ /** SHA-256 of the managed fields — safe to persist; see policy.ts. */
88
+ digest: string;
89
+ }
90
+
91
+ const text = (value: unknown) => (typeof value === 'string' ? value : '');
92
+
93
+ /**
94
+ * Seconds back into the duration shape the props use.
95
+ *
96
+ * ⚠️ Third private copy of this function in the package (mount-form.ts and auth-role-form.ts
97
+ * each keep one). Exporting a single copy means editing a file this change does not own, so
98
+ * it is duplicated deliberately rather than silently. Worth collapsing in a follow-up.
99
+ */
100
+ const ttlText = (seconds: number | undefined, fallback = '0') => {
101
+ if (seconds === undefined) return fallback;
102
+ if (seconds % 86400 === 0) return `${String(seconds / 86400)}d`;
103
+ if (seconds % 3600 === 0) return `${String(seconds / 3600)}h`;
104
+ if (seconds % 60 === 0) return `${String(seconds / 60)}m`;
105
+ return `${String(seconds)}s`;
106
+ };
107
+
108
+ /** `proxmox-tb4/roles/read` — read, write and delete all address the role here. */
109
+ export const rolePath = (mount: string, name: string) => `${mountPath(mount)}/roles/${name}`;
110
+
111
+ export const attributesOf = (
112
+ props: BaoProxmoxRoleProps,
113
+ live: Record<string, unknown>,
114
+ ): BaoProxmoxRoleAttributes => {
115
+ const attrs = {
116
+ mount: mountPath(props.mount),
117
+ // ⚠️ The plugin echoes `name` in a role read, so it is taken from `live` with the prop only
118
+ // as a fallback. Every other value below is live-only: the same split the sibling
119
+ // resources use, so a read never launders a declaration back into itself as "current".
120
+ name: text(live['name']) || props.name,
121
+ mintUser: text(live['mint_user']),
122
+ ttl: ttlText(ttlSeconds(live['ttl'])),
123
+ maxTtl: ttlText(ttlSeconds(live['max_ttl'])),
124
+ };
125
+ return { ...attrs, digest: sha256(JSON.stringify(attrs)) };
126
+ };
127
+
128
+ /**
129
+ * The body for `PUT <mount>/roles/<name>` — all strings, the `k=v` pairs `bao write` sent — or
130
+ * the prop names whose duration would not parse.
131
+ *
132
+ * ⛔ A WRITE TO roles/{name} REPLACES THE ROLE, IT DOES NOT MERGE. An omitted `ttl` is written
133
+ * as the field's zero value, so a resource that treated undeclared TTLs as "unmanaged" — the
134
+ * Bao.Mount shape — would silently reset a lease ceiling every time it rewrote the role for an
135
+ * unrelated reason. Both TTLs are therefore required props and EVERY write carries all three
136
+ * fields. REASONED from the Vault secrets-framework write semantics plus this plugin's schema
137
+ * marking only `mint_user` required; NOT measured, because this session was read-only and
138
+ * proving it would have meant writing to the live engine.
139
+ *
140
+ * ⚠️ Seconds go on the wire as integers, not as `1h`. The schema declares both TTLs
141
+ * `"format": "seconds"`, and an integer is accepted unambiguously by every path that reads
142
+ * them back — which is what keeps the round-trip through `matches` stable.
143
+ */
144
+ export const writeBody = (
145
+ props: BaoProxmoxRoleProps,
146
+ ): { ok: true; body: Record<string, string> } | { ok: false; bad: string[] } => {
147
+ const ttl = parseDuration(props.ttl);
148
+ const maxTtl = parseDuration(props.maxTtl);
149
+ const bad: string[] = [];
150
+ if (ttl === undefined) bad.push(`ttl=${props.ttl}`);
151
+ if (maxTtl === undefined) bad.push(`maxTtl=${props.maxTtl}`);
152
+ if (ttl === undefined || maxTtl === undefined) return { ok: false, bad };
153
+ return {
154
+ ok: true,
155
+ body: { mint_user: props.mintUser, ttl: String(ttl), max_ttl: String(maxTtl) },
156
+ };
157
+ };
158
+
159
+ /**
160
+ * ⚠️ An unparseable declaration never equals anything, so it surfaces as an update that will
161
+ * never settle rather than as a false noop. reconcile turns that loop into one named error;
162
+ * see the `writeBody` refusal there.
163
+ */
164
+ const sameTtl = (want: string, have: string) => {
165
+ const wantSeconds = parseDuration(want);
166
+ const haveSeconds = ttlSeconds(have);
167
+ return wantSeconds !== undefined && wantSeconds === haveSeconds;
168
+ };
169
+
170
+ /** True when the live role already matches the declaration on every field the plugin stores. */
171
+ export const matches = (attributes: BaoProxmoxRoleAttributes, props: BaoProxmoxRoleProps) =>
172
+ attributes.mintUser === props.mintUser &&
173
+ sameTtl(props.ttl, attributes.ttl) &&
174
+ sameTtl(props.maxTtl, attributes.maxTtl);
@@ -0,0 +1,214 @@
1
+ /**
2
+ * A role on the estate's proxmox secrets plugin — the mapping from one consumer to one PVE
3
+ * mint user, plus the lease ceilings that consumer's tokens get. METADATA ONLY.
4
+ *
5
+ * ★ WHY THIS FAMILY MATTERS MORE THAN IT LOOKS. Declaring these roles is how the estate
6
+ * replaces HAND-MADE PERMANENT PVE TOKENS with short-lived minted ones. On TB4, SEVEN tokens
7
+ * carry expire=0 and privsep=0 — root@pam!proxbox-sync, mcp@pve!executor,
8
+ * monitoring@pve!exporter, sablier@pve!sablier, tofu@pve!ro, tofu@pve!apply and
9
+ * vaultmint@pve!engine. Six of those are one Bao.ProxmoxRole plus a scoped PVE user away from
10
+ * being a 5-minute lease instead of a forever key.
11
+ *
12
+ * The seventh cannot be. vaultmint@pve!engine is the credential the ENGINE ITSELF
13
+ * authenticates with in order to mint anything at all: the thing that issues short-lived
14
+ * tokens needs a standing one, and no role can resolve that chicken-and-egg. It stays
15
+ * permanent, which makes it the single most valuable token on the cluster and the one whose
16
+ * rotation must be a deliberate, hand-run procedure rather than a plan.
17
+ *
18
+ * MEASURED here: `bao read proxmox-tb4/config` returns parent_token_id
19
+ * `vaultmint@pve!engine`, parent_secret_set true, parent_secret_tail `…eb01` — the engine's
20
+ * own key, present and, by the schema's own "Write-only: never returned by a read", not
21
+ * retrievable. The seven-token inventory was handed to this change and NOT re-counted against
22
+ * the PVE API in this session.
23
+ *
24
+ * ⛔ NO SECRET IN PROPS OR ATTRIBUTES — see policy.ts for the full reasoning. Every attribute
25
+ * here is a mount path, a role name, a PVE username or a TTL. Nothing this resource stores
26
+ * would help anyone authenticate to anything.
27
+ *
28
+ * ⛔ ISSUANCE IS NOT THIS RESOURCE, AND MUST NEVER BECOME ONE. `<mount>/creds/<name>` returns a
29
+ * LIVE PVE TOKEN. Alchemy persists attributes unencrypted (StateEncoding.ts tags Redacted
30
+ * values rather than encrypting them) into the `alchemy` Postgres, which pg-backup.sh dumps
31
+ * nightly to CT100, from where PBS backs it up. A minted token declared as a resource would
32
+ * outlive its own lease in at least four places, none of them OpenBao. This resource declares
33
+ * the DOOR; walking through it is a runtime call, not a plan.
34
+ *
35
+ * ★ `defaultRemovalPolicy: 'retain'` — deleting a role breaks every consumer that mints from
36
+ * it. Opt in with `.pipe(RemovalPolicy.destroy())`; see resource.ts in house/proxmox.
37
+ */
38
+ import { Resource } from 'alchemy';
39
+ import { isResolved } from 'alchemy/Diff';
40
+ import * as Provider from 'alchemy/Provider';
41
+ import * as Effect from 'effect/Effect';
42
+ import type * as HttpClient from 'effect/unstable/http/HttpClient';
43
+ import { baoDelete, baoRead, baoWrite } from './bao-http.ts';
44
+ import {
45
+ type BaoProxmoxRoleAttributes,
46
+ type BaoProxmoxRoleProps,
47
+ attributesOf,
48
+ matches,
49
+ rolePath,
50
+ writeBody,
51
+ } from './proxmox-role-form.ts';
52
+
53
+ export type { BaoProxmoxRoleAttributes, BaoProxmoxRoleProps };
54
+
55
+ export interface BaoProxmoxRole extends Resource<
56
+ 'Bao.ProxmoxRole',
57
+ BaoProxmoxRoleProps,
58
+ BaoProxmoxRoleAttributes,
59
+ never,
60
+ HttpClient.HttpClient
61
+ > {}
62
+
63
+ export const BaoProxmoxRole = Resource<BaoProxmoxRole>('Bao.ProxmoxRole', {
64
+ defaultRemovalPolicy: 'retain',
65
+ });
66
+
67
+ /**
68
+ * ⚠️ A missing MOUNT and a missing ROLE both read as undefined here, because both answer 404:
69
+ * an absent role is a read that found nothing, and a path under no mount is OpenBao's
70
+ * unsupported-path error, which is also a 404 (sdk/logical/response_util.go). They are not the
71
+ * same failure — one is a typo in `mount`, the other is a role that has yet to be created — so
72
+ * reconcile lets OpenBao's own `errors` through on the write rather than inventing a message.
73
+ * A bad mount says "no handler for route".
74
+ */
75
+ const readRole = (props: BaoProxmoxRoleProps) =>
76
+ Effect.gen(function* () {
77
+ const live = yield* baoRead(rolePath(props.mount, props.name));
78
+ if (live === undefined) return undefined;
79
+ return attributesOf(props, live);
80
+ });
81
+
82
+ export const BaoProxmoxRoleProvider = () =>
83
+ Provider.effect(
84
+ BaoProxmoxRole,
85
+ Effect.succeed(
86
+ BaoProxmoxRole.Provider.of({
87
+ /**
88
+ * ⛔ `bao list <mount>/roles` IS NOT A LIST OF THINGS THIS OWNS. MEASURED today:
89
+ * proxmox-tb4/roles answers `provision, read` and proxmox-ops/roles answers `read` —
90
+ * four roles created by hand at bringup, none of them declared anywhere yet.
91
+ * Returning them would invite Alchemy to adopt, and then delete, roles it never
92
+ * created, and deleting a role is how every consumer of it stops being able to mint.
93
+ */
94
+ list: () => Effect.succeed([]),
95
+
96
+ read: Effect.fn(function* ({ olds }) {
97
+ return yield* readRole(olds);
98
+ }),
99
+
100
+ /**
101
+ * ⛔ IT COMPARES THE LIVE ROLE, NOT THE STORED DIGEST. A mint_user edited in the
102
+ * OpenBao UI re-scopes every future credential while the digest in Postgres still
103
+ * says everything is fine — exactly the drift the hand-written check-* gates exist to
104
+ * catch, and exactly what a provider that trusted its own state would walk past.
105
+ */
106
+ diff: Effect.fn(function* ({ news, output }) {
107
+ if (output === undefined || !isResolved(news)) return undefined;
108
+ const live = yield* readRole(news);
109
+ if (live === undefined) return { action: 'update' } as const;
110
+ if (matches(live, news)) return { action: 'noop' } as const;
111
+ /**
112
+ * ⛔ NEVER `replace`, NOT EVEN FOR A mint_user CHANGE. A replace deletes the role and
113
+ * writes it back; between those steps `<mount>/creds/<name>` 404s, so anything
114
+ * minting in that window fails. The end state is byte-identical to the in-place
115
+ * write, because the plugin stores name, mint_user, ttl and max_ttl and nothing
116
+ * else — there is no immutable identity a recreate could reset. So a replace here
117
+ * buys an outage window in exchange for a louder-looking plan. The loudness a
118
+ * re-scope deserves comes from reconcile REFUSING it below, which blocks rather
119
+ * than destroys.
120
+ *
121
+ * ⚠️ CONSEQUENCE FOR THE READER OF A PLAN: a mint_user re-scope shows up here as a
122
+ * plain `update`, indistinguishable from a TTL tweak. The refusal lands at DEPLOY,
123
+ * not at plan. That is the safe ordering — nothing is written before the refusal —
124
+ * but do not read a green plan as evidence that a mint_user change is approved.
125
+ */
126
+ return { action: 'update' } as const;
127
+ }),
128
+
129
+ reconcile: Effect.fn(function* ({ news }) {
130
+ const live = yield* readRole(news);
131
+ /**
132
+ * ⛔ A mint_user CHANGE RE-SCOPES EVERY CREDENTIAL THE ROLE WILL EVER MINT, SILENTLY.
133
+ * The minted token inherits that PVE user's standing ACL and nothing else, so
134
+ * swapping `hf-read@pve` for `hf-provision@pve` turns a read-only consumer into a
135
+ * provisioning one with no other line of the declaration changing. A plan diff of
136
+ * one string is not proportionate warning for that, so an existing role refuses the
137
+ * move unless the declaration NAMES THE VALUE IT IS REPLACING. Creating a role never
138
+ * trips this — there is nothing to re-scope — and the permission expires by itself
139
+ * once the change lands, because live then equals the declaration.
140
+ */
141
+ if (
142
+ live !== undefined &&
143
+ live.mintUser !== news.mintUser &&
144
+ news.allowMintUserChange !== live.mintUser
145
+ ) {
146
+ return yield* Effect.die(
147
+ new Error(
148
+ `Bao.ProxmoxRole ${rolePath(news.mount, news.name)}: live mint_user ` +
149
+ `${live.mintUser} != declared ${news.mintUser}. Every token this role mints ` +
150
+ 'inherits the ACL of that PVE user. To re-scope deliberately, declare ' +
151
+ `allowMintUserChange: '${live.mintUser}'.`,
152
+ ),
153
+ );
154
+ }
155
+ if (live === undefined || !matches(live, news)) {
156
+ const write = writeBody(news);
157
+ /**
158
+ * ⛔ AN UNPARSEABLE TTL IS A REFUSAL, NOT A ZERO. parseDuration accepts `3600s`,
159
+ * `1h`, `6h`, `1d` and bare `0`, and NOTHING ELSE — a bare `3600` or a compound
160
+ * `1h30m` returns undefined. Left alone that never equals the live value, so the
161
+ * plan would report an update forever and each deploy would write a zero TTL.
162
+ * Die with the offending props named instead.
163
+ */
164
+ if (!write.ok) {
165
+ return yield* Effect.die(
166
+ new Error(
167
+ `Bao.ProxmoxRole ${rolePath(news.mount, news.name)}: unparseable duration ` +
168
+ `${write.bad.join(', ')}. Use 30s / 5m / 1h / 1d, or 0.`,
169
+ ),
170
+ );
171
+ }
172
+ yield* baoWrite('PUT', rolePath(news.mount, news.name), write.body);
173
+ }
174
+ const after = yield* readRole(news);
175
+ if (after === undefined) {
176
+ return yield* Effect.die(
177
+ new Error(
178
+ `Bao.ProxmoxRole ${rolePath(news.mount, news.name)}: write returned no error ` +
179
+ 'but the role is still absent.',
180
+ ),
181
+ );
182
+ }
183
+ /**
184
+ * ⚠️ A GREEN RECONCILE DOES NOT MEAN THE ROLE CAN MINT. The engine never grants ACLs
185
+ * — it borrows the mint user's — so declaring `mint_user: hf-read@pve` neither
186
+ * creates that PVE user nor gives it a single permission. If the user does not
187
+ * exist, or exists with no ACL, the role writes and reads back perfectly and the
188
+ * failure surfaces at `creds/<name>` time, in whatever consumer is unlucky. Verify
189
+ * the PVE side out-of-band; this resource cannot and does not.
190
+ */
191
+ return after;
192
+ }),
193
+
194
+ /**
195
+ * ⛔ DELETING A ROLE DOES NOT REVOKE THE TOKENS IT ALREADY MINTED — it only stops the
196
+ * next mint. Outstanding leases run to their own expiry against the PVE user's ACL.
197
+ * So this is an availability change for every consumer of `creds/<name>` and NOT a
198
+ * containment action: to contain a leaked credential, revoke the lease (which deletes
199
+ * the PVE token) or delete the token in PVE. Deleting the role only hides the door it
200
+ * came through. REASONED from Vault lease semantics; not exercised here.
201
+ *
202
+ * ⚠️ IDEMPOTENT AS ALCHEMY REQUIRES — already gone is success. It used to get that by
203
+ * going through `baoReadText`, which discarded the exit code entirely, so a REFUSED
204
+ * delete was reported as a successful one and the resource left Alchemy's state while
205
+ * the role stayed live. `baoDelete` keeps the idempotence — a 404 is success — and
206
+ * fails on everything else, 403 and 503 included.
207
+ */
208
+ delete: Effect.fn(function* ({ output }) {
209
+ yield* baoDelete(rolePath(output.mount, output.name));
210
+ return undefined;
211
+ }),
212
+ }),
213
+ ),
214
+ );