@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,267 @@
1
+ /**
2
+ * `Proxmox.Acl` — one grant: a role bound to a subject at a path. The line that makes a role real.
3
+ *
4
+ * ★ USERS, GROUPS, ROLES AND TOKENS ARE ALL INERT UNTIL AN ACL BINDS THEM. A role is a named list
5
+ * of privileges that grants nothing by itself; a user can do nothing until
6
+ * {path, user|group|token, role} exists. That is what makes those four families worth declaring,
7
+ * and why the grants that let OpenBao mint a PVE token today are only clicks somebody made once.
8
+ *
9
+ * ⛔ THE ONE FAMILY THAT DOES NOT FIT `pveOperations` AS WRITTEN, AND THE MISFIT IS MEASURED. The
10
+ * cluster's own schema — `/pve-docs/api-viewer/apidoc.js`, read unauthenticated 2026-09-13 —
11
+ * lists exactly TWO methods on `/access/acl`: GET and PUT. There is no DELETE (it answers
12
+ * "Method 'DELETE /access/acl' not implemented") and, less famously, NO POST either. So create
13
+ * and update are the SAME call, `PUT /access/acl`, and removal is that PUT with `delete=1` —
14
+ * which is why `delete` below does not use `ops.destroy`.
15
+ * ⚠️ THE REAL FIX BELONGS IN `resource.ts`: give `PveSpec` a `deleteForm` and the method to go
16
+ * with it, and this override disappears. Until then it is local and loud — do not "restore
17
+ * symmetry" by pointing `delete` at `ops.destroy`, which reports every removal as a 501 while
18
+ * the grant stays exactly where it was.
19
+ *
20
+ * ⛔ A SINGLE GRANT HAS NO URL OF ITS OWN. `GET /access/acl` answers ONE FLAT LIST for the whole
21
+ * cluster, so identity is the tuple (path, type, ugid, roleid), matched client-side in
22
+ * `attributes` below. Two resources declaring the SAME tuple are the SAME grant — Alchemy sees
23
+ * two resource ids, not one collision — and deleting either takes the access away from both,
24
+ * the same hazard as two guests declaring one vmid.
25
+ *
26
+ * ⛔ AND THAT LIST IS FILTERED BY WHO IS ASKING. The schema's own words for GET: "The returned list
27
+ * is restricted to objects where you have rights to modify permissions." A credential that can
28
+ * read the cluster but not modify permissions is answered `[]` — not a 403, not an error — so
29
+ * the mount's `read` role needs permission-modify rights on the declared path exactly as
30
+ * `provision` does. Work reported on a grant that is plainly there means that role is too narrow.
31
+ */
32
+ import { Resource } from 'alchemy';
33
+ import { isResolved } from 'alchemy/Diff';
34
+ import * as Provider from 'alchemy/Provider';
35
+ import * as Effect from 'effect/Effect';
36
+ import { pve } from './client.ts';
37
+ import { type PveRequirements, type WithTarget, pveOperations } from './resource.ts';
38
+
39
+ /** PVE's three kinds of subject. The read answers this word; the write wants its plural. */
40
+ export type AclSubjectType = 'user' | 'group' | 'token';
41
+
42
+ export interface AclProps extends WithTarget {
43
+ /**
44
+ * The PVE object path the grant is ON — `/`, `/pool/house`, `/vms/101`, `/storage/local-zfs`.
45
+ * ⚠️ NOT THE API PATH: `spec.path` below is the endpoint (`access/acl`). PVE's API overloads the
46
+ * word, so this file does too rather than renaming a field the cluster calls `path`.
47
+ */
48
+ path: string;
49
+ /** Which kind of subject `ugid` names. Identity: changing it is a different grant. */
50
+ type: AclSubjectType;
51
+ /** `tim@pve`, `admins`, `hf-provision@pve!hf-provision-…`. Identity. */
52
+ ugid: string;
53
+ /** The role bound here, e.g. `PVEAuditor`. ⚠️ It must exist — PVE refuses an unknown roleid. */
54
+ roleid: string;
55
+ /** Inherit down the path. PVE's default is ON, and this is the grant's ONLY mutable field. */
56
+ propagate?: boolean;
57
+ }
58
+
59
+ export interface AclAttributes {
60
+ path: string;
61
+ type: AclSubjectType;
62
+ ugid: string;
63
+ roleid: string;
64
+ propagate: boolean;
65
+ /**
66
+ * ⛔ PRESENCE AS AN ATTRIBUTE, AND IT IS WHAT MAKES THE FACTORY WORK HERE. `attributes` never
67
+ * returns undefined: if it did, `reconcile` would take the create branch and POST to
68
+ * `access/acl`, which PVE does not implement. Present-but-unbound sends every write down the
69
+ * PUT branch instead, the only branch PVE has. The cost: the factory's read-back guard cannot
70
+ * fire, so `reconcile` below re-checks this field itself and dies with the same honesty.
71
+ */
72
+ bound: boolean;
73
+ }
74
+
75
+ export interface ProxmoxAcl extends Resource<
76
+ 'Proxmox.Acl',
77
+ AclProps,
78
+ AclAttributes,
79
+ never,
80
+ PveRequirements
81
+ > {}
82
+
83
+ export const ProxmoxAcl = Resource<ProxmoxAcl>('Proxmox.Acl');
84
+
85
+ /**
86
+ * ⚠️ PVE NORMALISES ACL PATHS AND RETURNS THE NORMALISED FORM, a forever-update trap when only one
87
+ * side of the comparison is normalised. `PVE::AccessControl::normalize_path` collapses repeated
88
+ * slashes and strips the trailing one, so a declared `/pool/house/` reads back as `/pool/house`
89
+ * and a naive match never fires again. It also refuses a path with no leading slash, so one is
90
+ * added here rather than letting `pool/house` 400.
91
+ */
92
+ const normalize = (raw: string) => `/${raw.split('/').filter(Boolean).join('/')}`;
93
+
94
+ /**
95
+ * ⚠️ THE WRITE NAMES THE SUBJECT WITH A PLURAL KEY THAT DIFFERS PER KIND — `users`, `groups`,
96
+ * `tokens` — while the READ answers a singular `type`/`ugid` pair. The PUT declares
97
+ * `additionalProperties: 0`, so `user=` fails with a 400 rather than being ignored. The bad case
98
+ * is the WRONG plural: a valid parameter naming a subject kind you did not mean.
99
+ */
100
+ const SUBJECT_FIELD = { group: 'groups', token: 'tokens', user: 'users' } as const;
101
+
102
+ /** The tuple PVE keys a grant by, as the form it takes. Every write to this family starts here. */
103
+ const tuple = (props: AclProps): Record<string, string> => ({
104
+ path: normalize(props.path),
105
+ roles: props.roleid,
106
+ [SUBJECT_FIELD[props.type]]: props.ugid,
107
+ });
108
+
109
+ /** Create and update are one call: the tuple plus the single mutable field. */
110
+ const bind = (props: AclProps): Record<string, string> => ({
111
+ propagate: props.propagate === false ? '0' : '1',
112
+ ...tuple(props),
113
+ });
114
+
115
+ /** What a change of this string means: not an edit, a different grant. Used by `diff` below. */
116
+ const identity = (grant: Pick<AclAttributes, 'path' | 'roleid' | 'type' | 'ugid'>) =>
117
+ [normalize(grant.path), grant.type, grant.ugid, grant.roleid].join(' ');
118
+
119
+ /**
120
+ * ⚠️ `GET /access/acl` ANSWERS AN ARRAY, while the factory hands `attributes` the
121
+ * `Record<string, unknown>` every other PVE read is shaped like. Rows are narrowed, not trusted,
122
+ * and an empty result is not evidence of an empty cluster — see the last ⛔ in the header.
123
+ */
124
+ const find = (live: unknown, props: AclProps) =>
125
+ (Array.isArray(live) ? live : [])
126
+ .filter((row): row is Record<string, unknown> => typeof row === 'object' && row !== null)
127
+ .find(
128
+ (row) =>
129
+ row['path'] === normalize(props.path) &&
130
+ row['type'] === props.type &&
131
+ row['ugid'] === props.ugid &&
132
+ row['roleid'] === props.roleid,
133
+ );
134
+
135
+ /**
136
+ * ⚠️ PVE ANSWERS `propagate` AS 1/0 RATHER THAN true/false, AND OMITS IT WHEN IT CARRIES THE API
137
+ * DEFAULT, WHICH IS ON. Reading an absent field as `false` would report drift on every plan,
138
+ * forever, for every grant this provider did not create itself.
139
+ */
140
+ const propagates = (row: Record<string, unknown>) => {
141
+ const value = row['propagate'];
142
+ return value === undefined || value === 1 || value === true || value === '1';
143
+ };
144
+
145
+ const ops = pveOperations<AclProps, AclAttributes>({
146
+ attributes: (live, props) => {
147
+ const row = find(live, props);
148
+ return {
149
+ // ⚠️ `propagate` reads false when nothing is bound, because there is no grant to inherit.
150
+ // `bound` is the field that carries presence; this one means something only once it is set.
151
+ bound: row !== undefined,
152
+ path: normalize(props.path),
153
+ propagate: row !== undefined && propagates(row),
154
+ roleid: props.roleid,
155
+ type: props.type,
156
+ ugid: props.ugid,
157
+ };
158
+ },
159
+ /**
160
+ * ⛔ THE CREATE PATH IS UNREACHABLE ON A HEALTHY CLUSTER, AND ITS ERROR WILL MISLEAD YOU. The
161
+ * factory POSTs `collection` only when the read came back undefined, and `attributes` never
162
+ * does — so a POST means the GET itself failed (an expired 300s lease, a node down), not that
163
+ * the grant is missing. PVE answers "Method 'POST /access/acl' not implemented": read that 501
164
+ * as "the read failed" and go and look at the credential, not at the ACL.
165
+ */
166
+ /**
167
+ * ⛔ `provision`, AND WITHOUT IT EVERY GRANT READS BACK AS ABSENT. `GET /access/acl` is
168
+ * FILTERED, NOT GATED — the schema's own words are "The returned list is restricted to objects
169
+ * where you have rights to modify permissions". A credential without Permissions.Modify gets
170
+ * HTTP 200 and an EMPTY ARRAY, never a 403, so there is no error for `read` to fold; it simply
171
+ * sees nothing.
172
+ * ⛔ THE READ LEASE IS EXACTLY SUCH A CREDENTIAL. `hf-read@pve` is PVEAuditor on `/`, whose
173
+ * privilege set is Datastore.Audit, Mapping.Audit, Pool.Audit, SDN.Audit, Sys.Audit,
174
+ * VM.Audit, VM.GuestAgent.Audit — MEASURED in the 2026-09-13 cluster read. No
175
+ * Permissions.Modify. So on the default lease all fourteen live grants read back unbound,
176
+ * `matches` is false for every one, the plan reports fourteen updates for grants that
177
+ * plainly exist, and the deploy PUTs each one before acl.ts's own read-back guard dies.
178
+ * ★ THIS IS THE SAME CLASS AS storage.ts, sdn-zone.ts, sdn-vnet.ts AND api-token.ts, and it is
179
+ * the worst instance of it: those three are gated and answer 403, which is at least an
180
+ * error. A filtered endpoint answers success with less data, which is indistinguishable from
181
+ * the object not being there.
182
+ */
183
+ readRole: 'provision',
184
+ collection: () => 'access/acl',
185
+ createForm: bind,
186
+ /**
187
+ * ⚠️ ONLY `propagate` IS COMPARED, AND ONLY ONCE THE GRANT IS THERE. path/type/ugid/roleid are
188
+ * the FILTER that produced these attributes, not a reading of the cluster: comparing them with
189
+ * the props they came from is true by construction. An identity change is a DIFFERENT grant,
190
+ * which `diff` in the provider handles.
191
+ */
192
+ matches: (attributes, props) =>
193
+ attributes.bound && attributes.propagate === (props.propagate !== false),
194
+ path: () => 'access/acl',
195
+ updateForm: bind,
196
+ });
197
+
198
+ export const ProxmoxAclProvider = () =>
199
+ Provider.effect(
200
+ ProxmoxAcl,
201
+ Effect.succeed(
202
+ ProxmoxAcl.Provider.of({
203
+ /**
204
+ * ⛔ EMPTY LIKE EVERY OTHER RESOURCE HERE, AND MOST OF ALL THIS ONE. `GET /access/acl`
205
+ * hands back every grant a human ever clicked; adopting them would let a later plan
206
+ * DELETE somebody's access as tidy-up. Adoption is an explicit act.
207
+ */
208
+ list: () => Effect.succeed([]),
209
+ read: Effect.fn(function* ({ olds }) {
210
+ return yield* ops.read(olds);
211
+ }),
212
+ /**
213
+ * ⛔ AN IDENTITY CHANGE IS A REPLACE, AND THE FACTORY CANNOT SAY SO ALONE — it answers
214
+ * `replace` only for objects with no update path, and this one has PUT. Left to delegate,
215
+ * editing `roleid` (or the subject, or the path) would PUT the NEW grant and LEAVE THE
216
+ * OLD ONE BOUND: access nobody declared, held indefinitely, invisible in the plan because
217
+ * the provider believes it converged. Replace makes Alchemy call `delete` with the OLD
218
+ * props, which is the only thing that removes it.
219
+ * ⚠️ CREATE-FIRST, DELIBERATELY (Alchemy's default; `deleteFirst: true` would invert it).
220
+ * Delete-first could remove the very grant the provision credential mints against and
221
+ * leave nothing able to put it back. The cost is that a NARROWING change leaves the
222
+ * wider grant bound until Phase 2 collects the old generation.
223
+ */
224
+ diff: Effect.fn(function* ({ news, output }) {
225
+ if (output !== undefined && isResolved(news) && identity(news) !== identity(output)) {
226
+ return { action: 'replace' } as const;
227
+ }
228
+ return yield* ops.diff(news, output);
229
+ }),
230
+ /**
231
+ * ⚠️ THE READ-BACK GUARD, RESTORED FOR A FAMILY WHOSE "ABSENT" IS NOT `undefined`.
232
+ * `ops.reconcile` refuses when the object is still missing after a write, but missing
233
+ * here is `bound: false`, which that check cannot see. PVE answers 200 with
234
+ * `{"data":null}` on calls that did nothing: without this, a PUT that silently no-oped
235
+ * is recorded as a landed grant and the next plan reads noop over the gap.
236
+ */
237
+ reconcile: Effect.fn(function* ({ news }) {
238
+ const after = yield* ops.reconcile(news);
239
+ if (!after.bound) {
240
+ return yield* Effect.die(
241
+ new Error(
242
+ `access/acl: the write returned no error but ${identity(news)} is still not ` +
243
+ 'bound. Either the PUT did nothing, or the read credential cannot see the ' +
244
+ 'grant -- GET /access/acl is filtered to paths you may MODIFY permissions on, ' +
245
+ 'so a read-only role is answered [] rather than 403.',
246
+ ),
247
+ );
248
+ }
249
+ return after;
250
+ }),
251
+ /**
252
+ * ⛔ NOT `ops.destroy`, AND NOT AN OVERSIGHT. `DELETE /access/acl` is not implemented, so a
253
+ * factory delete would fail every destroy — while looking like a permissions problem —
254
+ * and leave the grant bound. Removal is the create call with `delete=1` and the identity
255
+ * tuple, no `propagate`, because removal is keyed on the tuple and nothing else.
256
+ *
257
+ * ⚠️ IDEMPOTENT, AND SHARED: removing a grant that is already gone is a no-op, but the
258
+ * tuple is cluster-global — re-read the second ⛔ in the header before assuming this
259
+ * only takes away what this stack declared.
260
+ */
261
+ delete: Effect.fn(function* ({ olds }) {
262
+ const removal = { delete: '1', ...tuple(olds) };
263
+ yield* pve(olds.target, 'provision', 'PUT', 'access/acl', removal);
264
+ }),
265
+ }),
266
+ ),
267
+ );
@@ -0,0 +1,122 @@
1
+ import type { ApiTokenAttributes, ApiTokenProps } from './api-token.ts';
2
+ /**
3
+ * What a write to a PVE API token actually does, and the form that does it.
4
+ *
5
+ * ★ SPLIT OUT OF api-token.ts TO KEEP BOTH FILES UNDER THE 250-LINE CAP, and the seam is the one
6
+ * metric-server-form.ts draws: this file answers "what does PVE store when this form arrives",
7
+ * api-token.ts answers "what a token is and when it has changed". Nothing here reads the
8
+ * cluster and nothing here decides a diff.
9
+ *
10
+ * ⚠️ THE `import type` BACK TO api-token.ts IS A CYCLE ON PAPER ONLY. It is type-only, so it is
11
+ * erased before anything runs and `ApiTokenProps` stays in the file that declares the resource.
12
+ *
13
+ * ⛔ THE WRITE SEMANTICS BELOW ARE READ FROM THE CLUSTER'S OWN PERL RATHER THAN INFERRED FROM THE
14
+ * API SCHEMA — `/usr/share/perl5/PVE/API2/User.pm`, subs `generate_token` and
15
+ * `update_token_info`, read over SSH on n2 on 2026-09-13. The two disagree in exactly the place
16
+ * that costs a forever-diff: the schema gives `expire` the default "same as user", and the code
17
+ * never implements it. api-token.ts records what that means.
18
+ */
19
+ import type { PveSpec } from './resource.ts';
20
+ import { bool, int, text } from './values.ts';
21
+
22
+ /**
23
+ * `<userid>!<tokenid>` — the name the rest of PVE calls this object by.
24
+ *
25
+ * ★ IT IS WHAT AN ACL BINDS. `Proxmox.Acl` with `type: 'token'` wants exactly this string as its
26
+ * `ugid`, so reporting it as an attribute is what lets a privilege-separated token be granted
27
+ * anything at all without somebody retyping the join by hand.
28
+ *
29
+ * ⚠️ DERIVED, NOT READ. MEASURED: `GET /access/users/monitoring@pve/token/exporter` answers
30
+ * `{"expire":0,"privsep":0}` and echoes back neither half of its own identity. So this is built
31
+ * from props, cannot disagree with the path the read used, and is therefore true by
32
+ * construction — which is why it is reported and never compared.
33
+ */
34
+ export const fullTokenid = (props: ApiTokenProps) => `${props.userid}!${props.tokenid}`;
35
+
36
+ /**
37
+ * Everything a token has, in the form PVE wants. Create and update send exactly this.
38
+ *
39
+ * ⛔ `userid` AND `tokenid` ARE NOT IN IT, and that is not an omission. Both are path segments of
40
+ * `access/users/{userid}/token/{tokenid}` — which is the path the POST goes to as well as the
41
+ * PUT — so a second copy in the body can only ever disagree with the path it was sent to.
42
+ * metric-server.ts omits its `id` for the same reason and says so there.
43
+ *
44
+ * ⚠️ EVERY FIELD IS SENT ON EVERY WRITE, THE EMPTY COMMENT INCLUDED, BECAUSE A PUT MERGES.
45
+ * MEASURED in `update_token_info`: `$token->{comment} = $param->{comment} if defined(...)`
46
+ * followed by `delete $token->{comment} if (!length $token->{comment})`. So `comment=` is how a
47
+ * comment is CLEARED; omitting it leaves the old text in place while `matches` goes on asking
48
+ * for an update the update cannot make. user.ts writes the same reasoning out for `groups`.
49
+ * ⚠️ THE `delete=` PARAMETER IS NOT USED AND WOULD NOT HELP. Its whitelist in that sub is
50
+ * literally `my $deletable = { comment => 1 };`, and anything else answers
51
+ * "unknown option '<k>'" — so `expire` and `privsep` have no clear path at all, which is the
52
+ * other half of why both are required props rather than optional ones.
53
+ *
54
+ * ⚠️ `flag()` FROM values.ts IS DELIBERATELY NOT USED FOR `privsep`. Its entire job is turning an
55
+ * UNDECLARED boolean into `undefined` so the field is omitted from the form; `privsep` is a
56
+ * required prop precisely so that it is never undeclared, so there is nothing for it to do and
57
+ * its `string | undefined` return would have to be asserted away to fit `Record<string,string>`.
58
+ * An assertion here would be a claim about the prop that the type already makes properly.
59
+ */
60
+ export const shape = (props: ApiTokenProps): Record<string, string> => ({
61
+ comment: props.comment ?? '',
62
+ expire: String(props.expire),
63
+ privsep: props.privsep ? '1' : '0',
64
+ });
65
+
66
+ export const apiTokenSpec: PveSpec<ApiTokenProps, ApiTokenAttributes> = {
67
+ // ⛔ THE ITEM READ NEEDS `User.Modify`, WHICH THE AUDITOR-SHAPED READ LEASE DOES NOT HAVE. The
68
+ // measurement and the consequence are in the header's last ⚠️; the mechanism — a silent
69
+ // "absent" rather than a 403 — is the ⛔ on `readRole` in resource.ts.
70
+ readRole: 'provision',
71
+ /**
72
+ * ★ NOTHING PVE REPORTS HERE IS UNWRITABLE, WHICH IS UNUSUAL IN THIS PACKAGE AND IS WHY
73
+ * `matches` can compare everything it reads. The GET returns exactly `comment`, `expire` and
74
+ * `privsep`; PUT accepts exactly those three. There is no autoscaled field, no server-assigned
75
+ * id, no set whose order PVE reshuffles — the three killers the neighbouring files carry
76
+ * ⚠️s about do not arise. The two identity fields and `fullTokenid` come from props and are
77
+ * true by construction, so comparing them would be theatre.
78
+ * ⚠️ `undefined` WHEN NEITHER FLAG IS THERE, BECAUSE BOTH ARE ALWAYS THERE. `user.cfg`
79
+ * materialises `expire` and `privsep` for every token it stores, so an answer carrying neither
80
+ * is not a token — and without this guard `bool`/`int` would invent one out of their fallbacks
81
+ * and the read-back guard in `reconcile` could never fire.
82
+ */
83
+ attributes: (live, props) => {
84
+ const expire = live['expire'];
85
+ const privsep = live['privsep'];
86
+ if (expire === undefined && privsep === undefined) return undefined;
87
+ return {
88
+ comment: text(live['comment']),
89
+ expire: int(expire, 0),
90
+ fullTokenid: fullTokenid(props),
91
+ /** ⚠️ FALLBACK `true`, MATCHING PVE'S API DEFAULT — unreachable given the guard above, but
92
+ * wrong in the safe direction if a future release stops emitting the field. */
93
+ privsep: bool(privsep, true),
94
+ tokenid: props.tokenid,
95
+ userid: props.userid,
96
+ };
97
+ },
98
+ /**
99
+ * ⛔ THE SAME STRING AS `path`, AND NOT A TYPO. A token is POSTed to its OWN url, not to the
100
+ * collection: MEASURED, `/access/users/{userid}/token` carries a GET and nothing else, while
101
+ * POST lives on `{tokenid}`. metric-server.ts has the identical shape for the identical
102
+ * reason and records the experiment there.
103
+ */
104
+ collection: (props) => `access/users/${props.userid}/token/${props.tokenid}`,
105
+ createForm: shape,
106
+ /**
107
+ * ⚠️ EXACTLY THE THREE FIELDS A PUT CAN PUT BACK, WHICH IS ALSO EXACTLY WHAT THE GET REPORTS.
108
+ * MEASURED, by replaying these two functions over the live GET bodies of all four token
109
+ * shapes this cluster has: `monitoring@pve!exporter` (no comment), `tofu@pve!apply` and
110
+ * `sablier@pve!sablier` (commented), and a live OpenBao lease under `hf-read@pve` (a real
111
+ * non-zero `expire`, offered as both `1789327175` and `"1789327175"` since `int` must not care
112
+ * which). All four answer noop; flipping `privsep` answers update; an empty body answers
113
+ * absent. ⚠️ THAT IS THE COMPARISON, NOT THE ENGINE — no `alchemy plan` was run against this
114
+ * family, and nothing here was written to the cluster.
115
+ */
116
+ matches: (attributes, props) =>
117
+ attributes.comment === (props.comment ?? '') &&
118
+ attributes.expire === props.expire &&
119
+ attributes.privsep === props.privsep,
120
+ path: (props) => `access/users/${props.userid}/token/${props.tokenid}`,
121
+ updateForm: shape,
122
+ };
@@ -0,0 +1,222 @@
1
+ /**
2
+ * `Proxmox.ApiToken` — a PVE API token: the thing a service actually authenticates as.
3
+ *
4
+ * ⛔ THE SECRET EXISTS FOR ONE HTTP RESPONSE AND THEN NOWHERE. MEASURED from the cluster's own
5
+ * schema (`/usr/share/pve-docs/api-viewer/apidoc.js` on n2, 2026-09-13): POST returns
6
+ * `["full-tokenid","info","value"]`; PUT returns `["comment","expire","full-tokenid","privsep",
7
+ * "value"]` with `value` present ONLY when `regenerate` was set; GET returns
8
+ * `["comment","expire","privsep"]` and never the secret. A live token agrees —
9
+ * `GET /access/users/monitoring@pve/token/exporter` answers `{"expire":0,"privsep":0}`. PVE says
10
+ * it plainly on the POST: the value "needs to be stored as it cannot be retrieved afterwards".
11
+ *
12
+ * ⛔ SO A TOKEN THIS RESOURCE CREATES IS UNUSABLE, AND SAYING SO IS THE POINT OF THIS BLOCK. The
13
+ * secret may not become an attribute: Alchemy persists attributes UNENCRYPTED, and this estate's
14
+ * state store is a Postgres dumped nightly (the ⛔ in credentials.ts). So `reconcile` drops the
15
+ * value on the floor, and what lands is a live credential in `/etc/pve/user.cfg` whose secret
16
+ * nobody holds. No `regenerate` rescues it, because a regenerated value is discarded the same
17
+ * way. The only escape is an operator who captured the value out of band — which this provider
18
+ * gives them no way to do.
19
+ * ★ SO POINT THIS FAMILY AT TOKENS THAT ALREADY EXIST. `comment`, `expire` and `privsep` are the
20
+ * whole of a token's policy, and declaring them is real work: it is how `expire` stops being
21
+ * whatever somebody typed in 2024. Mint NEW tokens where the secret can be caught — OpenBao's
22
+ * `proxmox-tb4` mount, or a human at `pveum user token add`.
23
+ * ⚠️ THE CREATE PATH IS LEFT REACHABLE RATHER THAN STUBBED. A create that silently did nothing
24
+ * would be the same lie as the stubbed delete resource.ts's ★ refuses; the honest arrangement
25
+ * is a create that works and a header that says what it produces.
26
+ *
27
+ * ⛔ THERE IS NO `regenerate` PROP, AND IT IS NOT AN OVERSIGHT. PVE's own description: "All users
28
+ * of the previous secret will lose access after this operation." A prop for it would revoke a
29
+ * live credential during a deploy whose plan said `update`, and hand back a replacement this
30
+ * provider is obliged to throw away — breakage with no recovery. Rotation belongs to the mount
31
+ * that owns the lease, or to a human who is watching.
32
+ *
33
+ * ⚠️ THERE IS A SECOND WRITER TO THESE OBJECTS AND IT IS THE ONE THIS PROVIDER RUNS ON. OpenBao's
34
+ * `proxmox-tb4` engine mints PVE tokens under `hf-read@pve` and `hf-provision@pve` — read from
35
+ * `house/platform/secrets/vault/plugin-proxmox/proxmox/`: `privsep=0` (client.go), `expire` set
36
+ * to the lease deadline, and an id the plugin CHOOSES,
37
+ * `hf-<role>-<actor>-<entity6>-<stamp>-<nonce>` (tokenname.go). Revocation and WAL rollback look
38
+ * up that one exact id (wal.go); neither sweeps a prefix. So the two systems cannot collide on
39
+ * one object by accident — but they share the USER: `hf-read@pve` held three live leases when
40
+ * this was written, and `Proxmox.User` deleting that account takes every one of them with it.
41
+ * ⛔ DO NOT DECLARE A TOKEN UNDER A MINT USER. `expire` is what makes that dangerous rather than
42
+ * untidy: point this resource at a lease token with the wrong expire and a 300-second
43
+ * credential becomes a permanent one — and on `hf-provision@pve` that credential carries
44
+ * `Permissions.Modify` at `/`. docs/privileges.md records what those 27 privileges buy.
45
+ *
46
+ * ⚠️ PRIVILEGES, AND THE READ LANE CANNOT DO IT. MEASURED: all four methods on this path check
47
+ * `['or', ['userid-param','self'], ['userid-group', ['User.Modify']]]` — with no `Sys.Audit`
48
+ * alternative, unlike `GET /access/users/{userid}`, which has one. And
49
+ * `pvesh get /access/permissions --userid hf-read@pve --path /access/groups` answers the seven
50
+ * PVEAuditor audit privileges and no `User.Modify`, while `hf-provision@pve` has it. So the
51
+ * 3600s read lease reaches at most its OWN account's tokens and is refused for every other
52
+ * userid — which `read` in resource.ts folds into "absent", so the plan says create and the POST
53
+ * then fails with "Token already exists". Hence `readRole: 'provision'` below, for the same
54
+ * reason storage.ts, sdn-zone.ts and sdn-vnet.ts set it, at the same cost: every plan mints a
55
+ * 300s non-renewable provision lease just to read.
56
+ */
57
+ import { Resource } from 'alchemy';
58
+ import * as Provider from 'alchemy/Provider';
59
+ import * as Effect from 'effect/Effect';
60
+ import { apiTokenSpec } from './api-token-form.ts';
61
+ import { type PveRequirements, type WithTarget, pveHandlers, pveOperations } from './resource.ts';
62
+
63
+ export interface ApiTokenProps extends WithTarget {
64
+ /**
65
+ * The account that owns the token, realm-qualified: `tofu@pve`, `root@pam`. Identity.
66
+ *
67
+ * ⚠️ THE ACCOUNT MUST ALREADY EXIST. Every method here runs `check_user_exist` first, so a
68
+ * token named under a missing user fails the READ (folded to "absent") and then fails the
69
+ * create with "no such user" — the honest error, arriving one step after the misleading plan.
70
+ * Sequence a `Proxmox.User` ahead of it and the ordering takes care of itself.
71
+ */
72
+ userid: string;
73
+ /**
74
+ * The token's own name, unique within the account. Identity.
75
+ *
76
+ * ⚠️ PVE HAS NO RENAME, AND HERE THAT COSTS MORE THAN IT DOES FOR A USER. Editing this prop (or
77
+ * `userid`) points `path` at a DIFFERENT token, which reads as absent and is then created —
78
+ * leaving the old token LIVE, with its secret still working, invisible to the plan, while the
79
+ * new one is the unusable kind described at the top. Rename by declaring a delete and a
80
+ * create, the way user.ts says for an account.
81
+ * ⛔ `diff` IS NOT OVERRIDDEN TO SAY `replace` THE WAY acl.ts DOES, AND THE DIFFERENCE IS THE
82
+ * OBJECT. There, replace removes a grant that can be rebuilt from its own tuple; here it
83
+ * would DELETE a live token — destroying an irrecoverable secret, and everything using it —
84
+ * to tidy up a name. Leaving the old token standing is the lesser harm, and saying so here
85
+ * is what keeps it from looking like an omission.
86
+ * ⚠️ PATTERN `[A-Za-z][A-Za-z0-9.\-_]+`: a letter first, two characters minimum. The schema
87
+ * declares no maxLength; the OpenBao plugin assumes a conservative 64 rather than finding the
88
+ * real limit in production, and so should anything else.
89
+ */
90
+ tokenid: string;
91
+ /**
92
+ * Free text in `pveum user token list` and the UI. Empty is how it is cleared — see `shape`.
93
+ * ⚠️ ANY CHARACTER IS SAFE HERE, MEASURED, and it was worth checking: `user.cfg` is a
94
+ * colon-delimited line, so a comment holding a colon or a newline is the obvious place for a
95
+ * round-trip to lose a character and diff forever. PVE escapes it — `encode_text` turns
96
+ * `a:b\nc%d` into `a%3Ab%0Ac%25d` and `decode_text` gives it back byte for byte.
97
+ */
98
+ comment?: string;
99
+ /**
100
+ * Seconds since the epoch, or 0 for "never expires".
101
+ *
102
+ * ⛔ REQUIRED, AND THE SCHEMA WILL TELL YOU IT NEED NOT BE. It declares the default "same as
103
+ * user", and the code never implements it: `generate_token` writes `expire` only when the
104
+ * parameter is defined, `user.cfg` stores a token as `token:<id>:<expire>:<privsep>:<comment>:`
105
+ * with no absent state, and the parser does `$expire = 0 if !$expire` (AccessControl.pm:1624).
106
+ * MEASURED end to end: every token on this cluster that was created without an expire reads
107
+ * back `"expire":0`. So "same as user" is documentation of an intention, not behaviour.
108
+ * ⛔ WHICH IS WHY IT IS NOT OPTIONAL-DEFAULTING-TO-0. That spelling reads identically and is far
109
+ * worse: an omitted `expire` would then quietly WRITE 0 over a live deadline, turning an
110
+ * expiring credential into a permanent one, and the plan would call it an update. Requiring
111
+ * the field makes "this token never expires" a sentence somebody had to type.
112
+ */
113
+ expire: number;
114
+ /**
115
+ * True keeps the token's privileges separate from its owner's — it gets NOTHING until an ACL
116
+ * names `fullTokenid`. False gives it the owner's privileges entire.
117
+ *
118
+ * ⛔ REQUIRED FOR THE SAME REASON AS `expire`, AND THE BLAST RADIUS IS LARGER. PVE's API default
119
+ * is 1, every token on this cluster is 0 (measured), and `user.cfg` materialises it either way
120
+ * — `$privsep = $privsep ? 1 : 0` (AccessControl.pm:1622), so there is no unset state to
121
+ * preserve. Were this optional, adopting a live token without mentioning `privsep` would plan
122
+ * an update to 1 and STRIP a working credential of every privilege it has, silently: PVE
123
+ * returns 401/403 to the service, nothing errors here, and `tofu@pve!apply` simply stops
124
+ * working. Required, so adopting a token is a sentence that states what it is.
125
+ * ⚠️ AND `false` IS NOT A SHRUG. It is a token with its owner's whole privilege set; under a
126
+ * provisioning account that is the account's full authority with a separate secret.
127
+ */
128
+ privsep: boolean;
129
+ }
130
+
131
+ export interface ApiTokenAttributes {
132
+ userid: string;
133
+ tokenid: string;
134
+ /** `<userid>!<tokenid>` — what `Proxmox.Acl` binds as its `ugid`. Derived; never compared. */
135
+ fullTokenid: string;
136
+ comment: string;
137
+ expire: number;
138
+ privsep: boolean;
139
+ /**
140
+ * ⛔ THERE IS NO `value` FIELD HERE AND THERE NEVER MAY BE. Adding one would write a working PVE
141
+ * credential, in clear, into the state Postgres and into every nightly dump of it — the exact
142
+ * leak metric-server.ts types its `token` as `never` to prevent. The read cannot supply one
143
+ * anyway; only create and regenerate can, and both of those responses are dropped.
144
+ */
145
+ }
146
+
147
+ export interface ProxmoxApiToken extends Resource<
148
+ 'Proxmox.ApiToken',
149
+ ApiTokenProps,
150
+ ApiTokenAttributes,
151
+ never,
152
+ PveRequirements
153
+ > {}
154
+
155
+ /**
156
+ * ★ `retain` BY DEFAULT, AND THIS IS THE PLAINEST CASE FOR IT IN THE PACKAGE. A token's contents
157
+ * are one irreplaceable secret: delete it and the value is gone, a replacement is a DIFFERENT
158
+ * value, and every holder loses access the instant `cfs_write_file` returns — with no error
159
+ * raised anywhere near them. The live cluster's tokens are `monitoring@pve!exporter` (the PVE
160
+ * exporter feeding VictoriaMetrics), `mcp@pve!executor`, `tofu@pve!apply`, `tofu@pve!ro`,
161
+ * `sablier@pve!sablier` and `vaultmint@pve!engine` — the last being the parent credential the
162
+ * OpenBao mount itself authenticates with, so orphaning that one would stop every plan in this
163
+ * package, this resource included. `delete` is FULLY IMPLEMENTED (DELETE is a real method on
164
+ * this path, measured) and runs the moment a caller opts in with `.pipe(RemovalPolicy.destroy())`.
165
+ * See the ★ in resource.ts, which explains the convention once.
166
+ */
167
+ export const ProxmoxApiToken = Resource<ProxmoxApiToken>('Proxmox.ApiToken', {
168
+ defaultRemovalPolicy: 'retain',
169
+ });
170
+
171
+ const ops = pveOperations(apiTokenSpec);
172
+
173
+ /**
174
+ * ⛔ THIS FAMILY ADOPTS AND MANAGES TOKENS; IT REFUSES TO MINT ONE, AND THAT IS A DECISION RATHER
175
+ * THAN A GAP. The secret exists ONLY in the create response — MEASURED from the schema: `POST`
176
+ * returns `["full-tokenid","info","value"]`, where `value` is "API token value used for
177
+ * authentication", while `GET` returns `["comment","expire","privsep"]` and never the secret.
178
+ * It may not become an attribute, because Alchemy writes attributes to its state store
179
+ * UNENCRYPTED into a Postgres that is dumped nightly.
180
+ *
181
+ * So a token created here would be A LIVE CREDENTIAL ON THE CLUSTER THAT NOBODY HOLDS: valid,
182
+ * privileged, unusable, and indistinguishable from one somebody meant to keep. Regenerating does
183
+ * not rescue it — a `regenerate` PUT returns the new value down the same discarded path. So
184
+ * `reconcile` refuses by name and says what to do instead.
185
+ *
186
+ * ★ EVERYTHING ELSE STILL WORKS, AND IT IS THE HALF WORTH HAVING. Adopting an existing token and
187
+ * converging its `comment`, `expire` and `privsep` are real operations: TB4 carries SEVEN tokens
188
+ * with `expire=0` and `privsep=0` made by clicks nobody recorded. Declaring those freezes the
189
+ * set, and an eighth appearing shows up as drift.
190
+ *
191
+ * ⚠️ IF DECLARATIVE MINTING IS EVER WANTED, the missing piece is a secret SINK — write the value
192
+ * straight into an OpenBao kv path and return only its address. That is a different resource
193
+ * with a different contract; it must not be bolted onto this one.
194
+ */
195
+ const handlers = {
196
+ ...pveHandlers(apiTokenSpec),
197
+ reconcile: Effect.fn(function* ({ news }: { news: ApiTokenProps }) {
198
+ const live = yield* ops.read(news);
199
+ if (live === undefined) {
200
+ return yield* Effect.die(
201
+ new Error(
202
+ `${news.userid}!${news.tokenid}: this resource does not create API tokens. PVE returns ` +
203
+ 'the secret only in the create response and it cannot be stored, so a token made here ' +
204
+ 'would be a live credential nobody holds. Create it with `pveum user token add` and ' +
205
+ 'capture the value, or mint a short-lived one from the OpenBao proxmox mount ' +
206
+ '(`bao read proxmox-tb4/creds/<role>`) -- then declare it here to manage it.',
207
+ ),
208
+ );
209
+ }
210
+ return yield* ops.reconcile(news);
211
+ }),
212
+ };
213
+
214
+ /**
215
+ * ⛔ `list` IS EMPTY, AND FOR THIS FAMILY THAT MATTERS MORE THAN THE GENERIC ARGUMENT IN
216
+ * resource.ts. A token index handed to Alchemy would offer up `vaultmint@pve!engine` and every
217
+ * live OpenBao lease for adoption — and adoption is what makes a later plan willing to delete.
218
+ * The mount's leases in particular appear and vanish on their own; anything that adopted one
219
+ * would report drift against a credential that was never its to hold.
220
+ */
221
+ export const ProxmoxApiTokenProvider = () =>
222
+ Provider.effect(ProxmoxApiToken, Effect.succeed(ProxmoxApiToken.Provider.of(handlers)));