@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,243 @@
1
+ /**
2
+ * `Proxmox.NodeNetwork` — one stanza of a node's `/etc/network/interfaces`, declared.
3
+ *
4
+ * ⛔ DECLARING THIS RESOURCE IS SAFE. APPLYING IT IS A SEPARATE, DELIBERATE ACT, AND IT IS NOT
5
+ * PERFORMED HERE. Every write under `/nodes/{node}/network/{iface}` lands in
6
+ * `/etc/network/interfaces.new` and changes no running interface. MEASURED, from PVE::INotify:
7
+ * `$shadowfiles = { '/etc/network/interfaces' => '/etc/network/interfaces.new' }`, and
8
+ * `read_file` reads the SHADOW when it exists. The apply is `PUT /nodes/{node}/network`, which
9
+ * renames the shadow over the real file and runs `ifreload -a`; the revert is
10
+ * `DELETE /nodes/{node}/network`, which simply unlinks the shadow. A sibling resource models
11
+ * that apply — this one never calls it, exactly as `Proxmox.SdnZone` never calls
12
+ * `Proxmox.SdnApply`.
13
+ *
14
+ * ⛔ CEPH ON TB4 RIDES TWO NETWORKS AND THIS API ONLY SEES ONE OF THEM. Measured from
15
+ * `/etc/pve/ceph.conf` on 2026-09-13:
16
+ *
17
+ * public_network = 10.20.11.0/24 -> vmbr1.11, which this resource DOES see
18
+ * cluster_network = 10.100.0.0/24 -> every OSD's cluster_addr: .102 / .103 / .104
19
+ *
20
+ * The cluster network — OSD REPLICATION, the traffic that rebuilds a lost replica — lives on
21
+ * `10.100.0.10X/32` addresses carried by `en05`, `en06` and `dummy_tb4`: the Thunderbolt mesh.
22
+ *
23
+ * ⛔ AND THOSE ADDRESSES ARE INVISIBLE HERE, WHICH IS THE TRAP. They are declared in
24
+ * `/etc/network/interfaces.d/sdn`, and PVE's network API does not parse that directory.
25
+ * MEASURED: `GET /nodes/n2/network/en05` answers `{"method":"manual","type":"eth",…}` with NO
26
+ * address, while `ip -br -4 addr` shows `en05 UP 10.100.0.102/32`. So this resource reports
27
+ * en05 and en06 as address-less manual ports and will plan `noop` over them — implicitly
28
+ * asserting they carry no address, which is FALSE.
29
+ *
30
+ * The consequence, stated plainly: declaring en05/en06 from what this API reports and then
31
+ * DESTROYING or applying over them takes out OSD replication on a cluster whose pools are
32
+ * size 3 / min_size 2. They read as the most boring interfaces on the node and are the two most
33
+ * dangerous. Leave them undeclared; if they must be managed, manage the file that actually
34
+ * owns them, not this resource.
35
+ *
36
+ * ⚠️ Two ways this file limits the damage on the interfaces it CAN see: `cidr` is compared
37
+ * unconditionally (the ⛔ on `matches`), and a live interface whose `type` disagrees with the
38
+ * declaration is reported ABSENT rather than retyped (the ⛔ in `attributes`).
39
+ *
40
+ * ★ THE REASON IT EXISTS IS THAT THE STAGED READ CANNOT TELL YOU THE NETWORK IS RIGHT — it can
41
+ * only tell you the FILE is. Once anything is staged, `GET /nodes/{node}/network/{iface}`
42
+ * answers from `interfaces.new`, so `matches` compares a declaration against a pending file and
43
+ * reports `noop` over a node whose running config has not moved. The apply resource is what
44
+ * closes that gap; this resource's `noop` means "the file says what you said", no more.
45
+ *
46
+ * ⚠️ PRIVILEGES ARE COARSER HERE THAN ANYWHERE ELSE IN THE PACKAGE: reads want `Sys.Audit` on
47
+ * `/nodes/{node}`, every write `Sys.Modify` on it — and there is no `/nodes/{node}/network` ACL
48
+ * object to scope to, so that also buys the node's services, certificates, DNS and time.
49
+ * Collected with every other family in docs/privileges.md.
50
+ *
51
+ * ★ A RECONCILE OVER A MATCHING INTERFACE IS A NO-WRITE, AND AN EARLIER DRAFT OF THIS COMMENT
52
+ * SAID THE OPPOSITE. `pveOperations.reconcile` gates on `matches` BEFORE the PUT
53
+ * (`resource.ts`: `else if (spec.updateForm !== undefined && !spec.matches(live, news))`), and
54
+ * the ⛔ above that line names THIS family as the reason the guard was added — adopting an
55
+ * interface a node already has would otherwise leave it holding a pending network change.
56
+ *
57
+ * ⚠️ WHAT IS STILL TRUE: when `matches` is false the PUT rewrites the whole stanza, and that can
58
+ * ADD lines the file did not have — a `vlan-raw-device vmbr1` under `vmbr1.11`, where PVE had
59
+ * been deriving it from the name. Harmless in content, and it costs nothing until somebody
60
+ * applies; one more reason the apply is a separate resource.
61
+ *
62
+ * ⚠️ `list` IS EMPTY, AND HERE THAT MATTERS MORE THAN ANYWHERE ELSE IN THIS PACKAGE. The index
63
+ * returns every interface the node has, `vmbr0`, `bond0` and the Ceph vlan included. Adopting
64
+ * them would put Alchemy one `destroy` away from staging the removal of a node's uplink — and
65
+ * `delete_network` has NO in-use check at all: MEASURED, it deletes the hash entry and writes
66
+ * the file. Adoption stays explicit.
67
+ */
68
+ import { Resource } from 'alchemy';
69
+ import * as Provider from 'alchemy/Provider';
70
+ import * as Effect from 'effect/Effect';
71
+ import {
72
+ body,
73
+ readAttributes,
74
+ same,
75
+ sameComment,
76
+ sameList,
77
+ updateBody,
78
+ } from './node-network-form.ts';
79
+ import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
80
+
81
+ /**
82
+ * PVE's interface types. `unknown` is what a NIC it does not manage reports — n2 and n3 both
83
+ * report `wlp91s0` that way, and n4 has no such interface at all.
84
+ *
85
+ * ⚠️ THAT ASYMMETRY IS A TRAP FOR A DECLARATION SHARED ACROSS NODES. On n4 the read answers 404,
86
+ * which this provider cannot tell from "deleted", so reconcile POSTs — writing a stanza for a
87
+ * card that is not in the machine. Declare per node what each node actually has; a physical
88
+ * interface is discovered, not decided.
89
+ */
90
+ export type NodeNetworkType =
91
+ | 'OVSBond'
92
+ | 'OVSBridge'
93
+ | 'OVSIntPort'
94
+ | 'OVSPort'
95
+ | 'alias'
96
+ | 'bond'
97
+ | 'bridge'
98
+ | 'eth'
99
+ | 'fabric'
100
+ | 'unknown'
101
+ | 'vlan'
102
+ | 'vnet';
103
+
104
+ export interface NodeNetworkProps extends WithTarget {
105
+ /** Which node's file this stanza lives in. Interfaces are per node, never cluster-wide. */
106
+ node: string;
107
+ /** `vmbr0`, `bond0`, `vmbr1.11`. 2-20 characters, PVE's `pve-iface` format. */
108
+ iface: string;
109
+ /** ⛔ REQUIRED ON EVERY WRITE, update included, never used to retype — see `readAttributes`. */
110
+ type: NodeNetworkType;
111
+ /**
112
+ * `10.20.11.12/24`. ⛔ THE ONE FIELD WITH NO UNMANAGED MODE: leaving it out is an instruction
113
+ * to make the interface `manual`, not an instruction to leave its address alone. The ⛔ on
114
+ * `updateBody` in node-network-form.ts has the measurement and the consequence.
115
+ */
116
+ cidr?: string;
117
+ /** ⚠️ PVE allows exactly ONE default gateway per node and refuses a second with "Default
118
+ * gateway already exists on interface '<other>'". On TB4 it is vmbr0.10's. */
119
+ gateway?: string;
120
+ /**
121
+ * `auto <iface>` in the file. Absent on read means off, which is why `bool`'s fallback is used.
122
+ * ⚠️ UNDECLARED IS UNMANAGED ON AN UPDATE AND OFF ON A CREATE — the same asymmetry storage.ts
123
+ * has, and it bites harder here: an interface created without `autostart` is one the node
124
+ * will not bring up at boot. Every TB4 bridge, bond and vlan carries it.
125
+ */
126
+ autostart?: boolean;
127
+ /** 1280-65520. Unset leaves the file without an `mtu` line and the kernel default in force. */
128
+ mtu?: number;
129
+ /** ⚠️ Round-trips only after normalisation — see `comment` in node-network-form.ts. */
130
+ comments?: string;
131
+ /** Space-separated, a SET: `enp87s0`, or `bond0`. ⚠️ PVE refuses a port already used elsewhere. */
132
+ bridge_ports?: string;
133
+ /** `2-4094`, or `2 100-200`. Only written when `bridge_vlan_aware` is on. */
134
+ bridge_vids?: string;
135
+ /** ⛔ A `false` here is sent as `delete=`, never as `0` — the ⛔ on `body` says why. */
136
+ bridge_vlan_aware?: boolean;
137
+ /**
138
+ * A bond's members, space separated. ⚠️ THE PARAMETER IS `slaves`, NOT `bond_slaves`, in BOTH
139
+ * directions on this PVE: MEASURED, the POST/PUT schema names only `slaves` and the GET returns
140
+ * `"slaves":"enp2s0f0np0 enp2s0f1np1"`. `bond_slaves` appears nowhere in Network.pm here.
141
+ */
142
+ slaves?: string;
143
+ bond_mode?: string;
144
+ /** ⚠️ Only written when `bond_mode` is `balance-xor` or `802.3ad`; ignored otherwise. */
145
+ bond_xmit_hash_policy?: string;
146
+ /** active-backup only. Kept hyphenated because that is the wire name. */
147
+ 'bond-primary'?: string;
148
+ /** ⚠️ DERIVED FROM A DOTTED NAME. `vmbr1.11` reports `vlan-id` 11 with no such line in the file;
149
+ * declaring it there is harmless but adds a line the file did not have. */
150
+ 'vlan-id'?: number;
151
+ 'vlan-raw-device'?: string;
152
+ }
153
+
154
+ /**
155
+ * ⛔ THE LAST SEVEN ARE REPORTED AND NEVER COMPARED — each is a MEASURED forever-diff, and the
156
+ * evidence for every one of them is in node-network-form.ts, beside the code that reads them.
157
+ */
158
+ export interface NodeNetworkAttributes {
159
+ node: string;
160
+ iface: string;
161
+ type: string;
162
+ cidr: string;
163
+ gateway: string;
164
+ autostart: boolean;
165
+ mtu: number;
166
+ comments: string;
167
+ bridge_ports: string;
168
+ bridge_vids: string;
169
+ bridge_vlan_aware: boolean;
170
+ slaves: string;
171
+ bond_mode: string;
172
+ bond_xmit_hash_policy: string;
173
+ 'bond-primary': string;
174
+ 'vlan-id': number;
175
+ 'vlan-raw-device': string;
176
+ priority: number;
177
+ method: string;
178
+ families: string;
179
+ active: boolean;
180
+ exists: boolean;
181
+ bond_miimon: string;
182
+ bridge_stp: string;
183
+ bridge_fd: string;
184
+ }
185
+
186
+ export interface ProxmoxNodeNetwork extends Resource<
187
+ 'Proxmox.NodeNetwork',
188
+ NodeNetworkProps,
189
+ NodeNetworkAttributes,
190
+ never,
191
+ PveRequirements
192
+ > {}
193
+
194
+ /** ★ `retain` by default — a node's only uplink cannot be rebuilt. See the ★ in resource.ts. */
195
+ export const ProxmoxNodeNetwork = Resource<ProxmoxNodeNetwork>('Proxmox.NodeNetwork', {
196
+ defaultRemovalPolicy: 'retain',
197
+ });
198
+
199
+ const handlers = pveHandlers<NodeNetworkProps, NodeNetworkAttributes>({
200
+ attributes: readAttributes,
201
+ collection: (props) => `nodes/${props.node}/network`,
202
+ /** ⚠️ No `delete` parameter on a POST — see the ⚠️ on `updateBody`. */
203
+ createForm: body,
204
+ /**
205
+ * ⛔ `cidr` IS THE ONLY FIELD COMPARED WHEN UNDECLARED, AND THAT ASYMMETRY IS THE POINT. Every
206
+ * other field here follows storage.ts: undeclared means unmanaged, so it is neither sent nor
207
+ * compared and a plan cannot report an update no write could satisfy. `cidr` cannot be
208
+ * unmanaged, because PVE derives `method` from the presence of an address IN THE FORM — an
209
+ * omission is an edit. Comparing it unconditionally is what makes that edit visible in `plan`
210
+ * instead of at `ifreload` time.
211
+ *
212
+ * ⚠️ NOTHING IN THE REPORTED-ONLY BLOCK IS HERE, and that is what makes the TB4 declaration
213
+ * plan as `noop` on all three nodes at once — the ⛔ above `NodeNetworkAttributes` lists them
214
+ * and the measurement behind each.
215
+ */
216
+ matches: (attributes, props) =>
217
+ attributes.cidr === (props.cidr ?? '') &&
218
+ same(props.gateway, attributes.gateway) &&
219
+ same(props.autostart, attributes.autostart) &&
220
+ same(props.mtu, attributes.mtu) &&
221
+ sameComment(props.comments, attributes.comments) &&
222
+ sameList(props.bridge_ports, attributes.bridge_ports) &&
223
+ sameList(props.bridge_vids, attributes.bridge_vids) &&
224
+ same(props.bridge_vlan_aware, attributes.bridge_vlan_aware) &&
225
+ sameList(props.slaves, attributes.slaves) &&
226
+ same(props.bond_mode, attributes.bond_mode) &&
227
+ same(props.bond_xmit_hash_policy, attributes.bond_xmit_hash_policy) &&
228
+ same(props['bond-primary'], attributes['bond-primary']) &&
229
+ same(props['vlan-id'], attributes['vlan-id']) &&
230
+ same(props['vlan-raw-device'], attributes['vlan-raw-device']),
231
+ path: (props) => `nodes/${props.node}/network/${props.iface}`,
232
+ updateForm: updateBody,
233
+ });
234
+
235
+ /**
236
+ * ⚠️ THE DESTROY IS STAGED LIKE EVERY OTHER WRITE, WHICH MAKES IT THE ONE RECOVERABLE DESTROY IN
237
+ * THIS PACKAGE. It removes the stanza from `interfaces.new` and nothing else;
238
+ * `DELETE /nodes/{node}/network` puts the node back by unlinking that file. It becomes
239
+ * irreversible the moment somebody applies — and PVE performs NO in-use check before removing
240
+ * an interface other bridges or Ceph still stand on.
241
+ */
242
+ export const ProxmoxNodeNetworkProvider = () =>
243
+ Provider.effect(ProxmoxNodeNetwork, Effect.succeed(ProxmoxNodeNetwork.Provider.of(handlers)));
@@ -0,0 +1,104 @@
1
+ /**
2
+ * A notification target's props, as the form PVE wants — including the one field in this package
3
+ * that cannot be spelled as a comma list.
4
+ *
5
+ * ★ SPLIT OUT OF notification-target.ts TO KEEP BOTH UNDER THE 250-LINE CAP. The seam is the same
6
+ * one metric-server uses: this file turns a declaration into a form, and the resource file says
7
+ * what a target IS and when it has changed. The `import type` back is erased, so it is a cycle
8
+ * on paper only.
9
+ */
10
+ import type { NotificationTargetProps } from './notification-target.ts';
11
+ import { text } from './values.ts';
12
+
13
+ /**
14
+ * ⛔ A MULTI-VALUED PARAMETER GOES ON THE WIRE DIFFERENTLY HERE THAN ANYWHERE ELSE IN THIS PACKAGE,
15
+ * AND THE DIFFERENCE IS MEASURED. Every other PVE endpoint spells a list `type: string, format:
16
+ * pve-configid-list` — comma-joined, which is what sdn-vnet.ts sends. On the notification
17
+ * endpoints `mailto`, `mailto-user`, `header` and `delete` are `type: array`, and PVE's own web
18
+ * UI submits those as REPEATED KEYS (`header=a&header=b`). `Record<string, string>` cannot repeat
19
+ * a key, so one value carrying a NUL goes instead: it percent-encodes to `%00`, and PVE's
20
+ * urlencoded decoder joins repeated keys with exactly that byte, rebuilding the same parameter.
21
+ * ⚠️ ONLY `header` NEEDS IT, and only past one entry. A header item is ITSELF the property string
22
+ * `name=<name>,value=<base64>`, so a comma cannot express even one of them. Addresses hold no
23
+ * commas and PVE's UI submits `mailto` as a comma-separated text field, so those go that way.
24
+ * ⚠️ REASONED FROM THE DECODER AND THE UI, NOT PROVEN BY A LIVE MULTI-HEADER WRITE. One header
25
+ * carries no separator and is unaffected either way; a longer list, if rejected, fails with a
26
+ * 400 naming `header` rather than quietly dropping one, and the fix is one line here.
27
+ */
28
+ const REPEATED_KEY = '\0';
29
+
30
+ /** ⚠️ `0` is "PVE holds no port". Integers have come back as strings elsewhere, so parse both. */
31
+ export const portOf = (value: unknown): number => {
32
+ const parsed = typeof value === 'number' ? value : Number.parseInt(String(value ?? ''), 10);
33
+ return Number.isNaN(parsed) ? 0 : parsed;
34
+ };
35
+
36
+ const sorted = (parts: readonly string[]): string =>
37
+ parts
38
+ .map((part) => part.trim())
39
+ .filter((part) => part !== '')
40
+ .sort()
41
+ .join(',');
42
+
43
+ /**
44
+ * ⚠️ ORDER IS NOT MEANING IN A RECIPIENT LIST, SO IT MUST NOT BE A DIFF — backup-job.ts's reasoning
45
+ * about guest ids, applied to addresses. Both sides are sorted before they meet.
46
+ *
47
+ * ⚠️ PVE ANSWERS WITH AN ARRAY AND ACCEPTS A DELIMITED STRING, and this package has already been
48
+ * bitten by a list coming back flattened on one version and structured on another (`groups` in
49
+ * user.ts). PVE's UI calls `mailto` "separated by spaces, commas or semicolons", so all three are
50
+ * split here and both shapes funnel into one comparable value.
51
+ */
52
+ export const addressList = (value: unknown): string =>
53
+ sorted(
54
+ Array.isArray(value)
55
+ ? value.map((item: unknown) => String(item))
56
+ : typeof value === 'string'
57
+ ? value.split(/[\s,;]+/)
58
+ : [],
59
+ );
60
+
61
+ /** ⛔ NEVER SPLIT ON A COMMA: a header item contains one. A lone string is one item, not a list. */
62
+ export const headerList = (value: unknown): string =>
63
+ sorted(Array.isArray(value) ? value.map((item: unknown) => String(item)) : [text(value)]);
64
+
65
+ const set = (key: string, value: string | undefined): Record<string, string> =>
66
+ value === undefined ? {} : { [key]: value };
67
+
68
+ /**
69
+ * The form for both create and update.
70
+ *
71
+ * ⛔ A FIELD THE DECLARATION LEAVES OUT IS NOT SENT AND NOT COMPARED — backup-job.ts's rule rather
72
+ * than sdn-vnet.ts's `delete` list, and here the choice is not close. That list would have to
73
+ * name `mailto`, so a target adopted from the UI and declared without its recipients would have
74
+ * them stripped on the first deploy: the one object that makes failures visible, turned back into
75
+ * silence. `delete` on this family is also `type: array` rather than the comma string every other
76
+ * endpoint takes, so sdn-vnet's encoding would not even carry.
77
+ * ⚠️ THE PRICE IS THAT REMOVING A LINE DOES NOT CLEAR A FIELD. Declare it as `''` to clear it,
78
+ * which IS sent: an empty string is a value, an absent prop is no request at all.
79
+ *
80
+ * ⚠️ `disable` IS ALWAYS SENT AND ALWAYS COMPARED, `0` INCLUDED. PVE documents the default as 0, so
81
+ * sending it explicitly is what makes "not declared" and "declared false" one state instead of a
82
+ * plan that asks for an update the PUT never performs. It is also the drift that matters most
83
+ * here: a target disabled by hand during a maintenance window and never re-enabled looks exactly
84
+ * like a healthy cluster.
85
+ */
86
+ export const shape = (props: NotificationTargetProps): Record<string, string> => ({
87
+ disable: props.disable === true ? '1' : '0',
88
+ ...set('author', props.author),
89
+ ...set('body', props.body),
90
+ ...set('comment', props.comment),
91
+ ...set('from-address', props['from-address']),
92
+ ...set('header', props.header === undefined ? undefined : props.header.join(REPEATED_KEY)),
93
+ ...set('mailto', props.mailto === undefined ? undefined : addressList(props.mailto)),
94
+ ...set(
95
+ 'mailto-user',
96
+ props['mailto-user'] === undefined ? undefined : addressList(props['mailto-user']),
97
+ ),
98
+ ...set('method', props.method),
99
+ ...set('mode', props.mode),
100
+ ...set('port', props.port === undefined ? undefined : String(props.port)),
101
+ ...set('server', props.server),
102
+ ...set('url', props.url),
103
+ ...set('username', props.username),
104
+ });
@@ -0,0 +1,222 @@
1
+ /**
2
+ * `Proxmox.NotificationTarget` — where a failed backup goes to be seen.
3
+ *
4
+ * ★ THE FAILURE MODE THIS REMOVES IS SILENCE. A failed vzdump, a lost HA service and a broken
5
+ * replication job all reach a human through one mechanism: a notification target. Out of the box
6
+ * a cluster has exactly one, the built-in `mail-to-root` — one mailbox, on one node, delivered by
7
+ * that node's local MTA. Declared, a failure becomes visible; undeclared, nothing looks wrong
8
+ * until somebody goes looking for a backup that was never taken. Every other resource in this
9
+ * package fails loudly. This is the one whose absence fails quietly.
10
+ *
11
+ * ⛔ THE PER-OBJECT READ GOES THROUGH `endpoints/{type}/{name}`, NEVER `targets/{name}`.
12
+ * `GET /cluster/notifications/targets/{name}` answers "Method not implemented" — MEASURED, and
13
+ * corroborated by the cluster's own apidoc, where that path node carries no methods at all.
14
+ * `/targets` is a READ-ONLY UNION VIEW implementing the collection GET and `{name}/test` and
15
+ * nothing else. So `type` is IDENTITY here, not a setting: it appears in both `path()` and
16
+ * `collection()`, and changing it is a replace rather than an update.
17
+ *
18
+ * ⚠️ A BUILT-IN TARGET CAN BE ADOPTED AND STILL NOT BE OWNED. The shipped entries report
19
+ * `origin: builtin` or `modified-builtin`, and a DELETE on one REVERTS it to shipped defaults
20
+ * instead of removing it: Alchemy drops the resource from state while the target is still on the
21
+ * cluster delivering mail. Check `origin` on `/cluster/notifications/targets` before declaring a
22
+ * name you did not create. ⚠️ `origin` IS DELIBERATELY NOT AN ATTRIBUTE — the schema returns it
23
+ * on the collection view only, never on the per-object read, so an attribute would read "unknown"
24
+ * for every object forever, and an attribute that always lies is worse than a true comment.
25
+ *
26
+ * ⛔ NO SECRET IS A PROP, LET ALONE AN ATTRIBUTE. smtp `password`, gotify `token` and webhook
27
+ * `secret` are write-only and none is declarable here. Props are persisted too, not just
28
+ * attributes — `delete` is handed `olds`, which can only have come from the state store — so a
29
+ * secret prop would sit unencrypted in the state Postgres for as long as the resource exists.
30
+ * ⚠️ THE COST IS REAL: `token` is REQUIRED on a gotify create, so a gotify target cannot be
31
+ * created from here at all. Create one out of band with its secret and then declare it — the
32
+ * read finds it, reconcile takes the PUT path, and PVE leaves any field the body does not
33
+ * mention alone. The secret half stays a human's; the rest is declared. Same for an
34
+ * authenticated smtp target and a signed webhook.
35
+ *
36
+ * ★ WHAT THIS NEEDS, READ OFF THE CLUSTER'S OWN SCHEMA:
37
+ * read/diff GET endpoints/{type}/{name} Mapping.Audit (or Mapping.Modify) on
38
+ * /mapping/notifications
39
+ * reconcile POST + PUT Mapping.Modify on /mapping/notifications AND one of
40
+ * Sys.Audit / Sys.Modify / Sys.AccessNetwork on /
41
+ * delete DELETE Mapping.Modify on /mapping/notifications
42
+ * `LXCProvisioner` already carries Sys.Audit on `/` (see lxc.ts), so the delta is the two Mapping
43
+ * privileges — notably NOT the `Sys.Modify` a backup job costs.
44
+ * ⚠️ PREFER A SECOND ROLE GRANTED AT `/mapping/notifications` OVER WIDENING THE PROVISIONING ROLE
45
+ * ON `/`. `/mapping` also holds the PCI and USB passthrough maps, so Mapping.Modify at the root
46
+ * lets a credential scoped to make containers rewire somebody's hardware.
47
+ * ⛔ A MISSING READ PRIVILEGE DOES NOT LOOK LIKE A 403. `pveOperations.read` ends in
48
+ * `orElseSucceed(() => undefined)`, so a 403 on the GET is indistinguishable from "absent": the
49
+ * plan says create, PVE answers "already exists", and the fault was an ACL all along. That pair
50
+ * is the signature — grant Mapping.Audit to the READ role before changing anything else.
51
+ */
52
+ import { Resource } from 'alchemy';
53
+ import * as Provider from 'alchemy/Provider';
54
+ import * as Effect from 'effect/Effect';
55
+ import { addressList, headerList, portOf, shape } from './notification-target-form.ts';
56
+ import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
57
+ import { text } from './values.ts';
58
+
59
+ /** The four families PVE ships. `GET /cluster/notifications/endpoints` lists exactly these. */
60
+ export type NotificationTargetType = 'gotify' | 'sendmail' | 'smtp' | 'webhook';
61
+
62
+ /**
63
+ * ⚠️ PVE'S OWN KEY NAMES, DASHES INCLUDED, for the reason backup-job.ts gives: the form is then a
64
+ * copy rather than a translation table, and a translation table is one more place for a key to be
65
+ * renamed and silently never sent.
66
+ *
67
+ * ⚠️ A FIELD THAT DOES NOT BELONG TO `type` IS STILL SENT, and PVE refuses it by name. That 400
68
+ * beats this file quietly dropping a `mailto` somebody wrote on a webhook.
69
+ */
70
+ export interface NotificationTargetProps extends WithTarget {
71
+ /** ⛔ IDENTITY: it picks the family AND the URL, so changing it is a replace. */
72
+ type: NotificationTargetType;
73
+ /**
74
+ * PVE's primary key, unique ACROSS ALL FOUR FAMILIES: a webhook cannot be called `mail-to-root`,
75
+ * because a sendmail target already is. Format `pve-configid`.
76
+ */
77
+ name: string;
78
+ /** Free text in the UI. */
79
+ comment?: string;
80
+ /** Configured but not delivering. Always sent and always compared — see `shape`. */
81
+ disable?: boolean;
82
+ /** sendmail, smtp: recipients. ⚠️ Order is not meaning — see `addressList`. */
83
+ mailto?: readonly string[];
84
+ /** sendmail, smtp: PVE users, whose own configured address receives the mail. */
85
+ 'mailto-user'?: readonly string[];
86
+ /** smtp: the SMTP host. gotify: the server URL. PVE requires it on create for both. */
87
+ server?: string;
88
+ /** smtp only. Unset lets PVE pick from `mode`: 465 tls, 587 starttls, 25 insecure. */
89
+ port?: number;
90
+ /** smtp only. PVE's schema default is `tls`. */
91
+ mode?: 'insecure' | 'starttls' | 'tls';
92
+ /** smtp only. ⛔ The matching `password` is not declarable — see the header. */
93
+ username?: string;
94
+ /** smtp: PVE requires it on create. sendmail: optional. */
95
+ 'from-address'?: string;
96
+ /** Display name on the mail. PVE's smtp schema defaults it to `Proxmox VE`. */
97
+ author?: string;
98
+ /** webhook: PVE requires it on create. */
99
+ url?: string;
100
+ /** webhook: PVE requires it on create. Lower case — PVE's enum is `post|put|get`. */
101
+ method?: 'get' | 'post' | 'put';
102
+ /**
103
+ * webhook: `name=<name>,value=<base64 of value>` property strings, as PVE stores them.
104
+ * ⚠️ MORE THAN ONE HEADER IS THE ONE THING HERE NOT PROVEN ON A WIRE — see `REPEATED_KEY`.
105
+ */
106
+ header?: readonly string[];
107
+ /**
108
+ * webhook: the request body, base64, as PVE stores it.
109
+ * ⚠️ THE UI SHOWS THE DECODED BODY AND THIS DOES NOT. Declaring the base64 is what keeps
110
+ * `matches` symmetric with the read; decoding for display would mean encoding on write, and a
111
+ * round trip PVE normalises differently is a forever-update. The cost is an unreadable diff.
112
+ */
113
+ body?: string;
114
+ }
115
+
116
+ /**
117
+ * ⚠️ EVERY FIELD IS PRESENT FOR EVERY FAMILY, with `''` / `0` meaning "PVE holds nothing". A
118
+ * webhook reads back an empty `mailto`, which is also what a webhook declares, so the
119
+ * cross-family fields cost one comparison each and never a false update.
120
+ */
121
+ export interface NotificationTargetAttributes {
122
+ name: string;
123
+ type: NotificationTargetType;
124
+ comment: string;
125
+ disable: boolean;
126
+ /** Sorted and comma-joined, so re-ordering a declaration is not a diff. See `addressList`. */
127
+ mailto: string;
128
+ 'mailto-user': string;
129
+ server: string;
130
+ /** 0 means PVE holds no port and will pick one from `mode`. */
131
+ port: number;
132
+ mode: string;
133
+ username: string;
134
+ 'from-address': string;
135
+ author: string;
136
+ url: string;
137
+ method: string;
138
+ /** Sorted and comma-joined for COMPARISON only; the wire form differs. See `headerList`. */
139
+ header: string;
140
+ body: string;
141
+ }
142
+
143
+ export interface ProxmoxNotificationTarget extends Resource<
144
+ 'Proxmox.NotificationTarget',
145
+ NotificationTargetProps,
146
+ NotificationTargetAttributes,
147
+ never,
148
+ PveRequirements
149
+ > {}
150
+
151
+ export const ProxmoxNotificationTarget = Resource<ProxmoxNotificationTarget>(
152
+ 'Proxmox.NotificationTarget',
153
+ );
154
+
155
+ const handlers = pveHandlers<NotificationTargetProps, NotificationTargetAttributes>({
156
+ /**
157
+ * ⛔ NAMED FIELDS, NEVER A SPREAD OF `live`. gotify's `token` and smtp's `password` really are
158
+ * absent from their read schemas — but webhook's read schema DECLARES `secret`, so "PVE will
159
+ * not hand it to us" is not a defence. Listing what is kept is what keeps secret material out
160
+ * of a state row, whatever a future PVE decides to start returning.
161
+ */
162
+ attributes: (live, props) => ({
163
+ author: text(live['author']),
164
+ body: text(live['body']),
165
+ comment: text(live['comment']),
166
+ disable: live['disable'] === 1 || live['disable'] === true,
167
+ 'from-address': text(live['from-address']),
168
+ header: headerList(live['header']),
169
+ mailto: addressList(live['mailto']),
170
+ 'mailto-user': addressList(live['mailto-user']),
171
+ method: text(live['method']),
172
+ mode: text(live['mode']),
173
+ name: props.name,
174
+ port: portOf(live['port']),
175
+ server: text(live['server']),
176
+ type: props.type,
177
+ url: text(live['url']),
178
+ username: text(live['username']),
179
+ }),
180
+ collection: (props) => `cluster/notifications/endpoints/${props.type}`,
181
+ /** ⛔ `name` is create-only: PVE takes it in the body once, and in the path forever after. */
182
+ createForm: (props) => ({ ...shape(props), name: props.name }),
183
+ /**
184
+ * Each line reads "not declared, or equal"; `disable` is the one field with no undeclared case.
185
+ *
186
+ * ⚠️ IF A PLAN NEVER SETTLES, SUSPECT A SCHEMA DEFAULT COMING BACK AS A VALUE. PVE defaults smtp
187
+ * `mode` to `tls` and smtp `author` to `Proxmox VE`, and applies both when the mail is sent
188
+ * rather than writing them into the config — so an undeclared field reads back empty and its
189
+ * line is skipped. A version that STORED them would answer with a field nobody declared. The
190
+ * fix is to declare the field, never to stop comparing it.
191
+ */
192
+ matches: (attributes, props) =>
193
+ attributes.disable === (props.disable === true) &&
194
+ (props.comment === undefined || attributes.comment === props.comment) &&
195
+ (props.server === undefined || attributes.server === props.server) &&
196
+ (props.port === undefined || attributes.port === props.port) &&
197
+ (props.mode === undefined || attributes.mode === props.mode) &&
198
+ (props.username === undefined || attributes.username === props.username) &&
199
+ (props['from-address'] === undefined || attributes['from-address'] === props['from-address']) &&
200
+ (props.author === undefined || attributes.author === props.author) &&
201
+ (props.url === undefined || attributes.url === props.url) &&
202
+ (props.method === undefined || attributes.method === props.method) &&
203
+ (props.body === undefined || attributes.body === props.body) &&
204
+ (props.mailto === undefined || attributes.mailto === addressList(props.mailto)) &&
205
+ (props['mailto-user'] === undefined ||
206
+ attributes['mailto-user'] === addressList(props['mailto-user'])) &&
207
+ (props.header === undefined || attributes.header === headerList(props.header)),
208
+ path: (props) => `cluster/notifications/endpoints/${props.type}/${props.name}`,
209
+ updateForm: shape,
210
+ });
211
+
212
+ /**
213
+ * ⚠️ ON A BUILT-IN TARGET A DELETE REVERTS RATHER THAN REMOVES, and PVE reports success either way.
214
+ * It is passed through unguarded because everywhere else in this package the cluster's answer is
215
+ * the answer — but this is the one case where a green plan and the cluster disagree, and the
216
+ * object left standing is the one carrying the estate's failure mail.
217
+ */
218
+ export const ProxmoxNotificationTargetProvider = () =>
219
+ Provider.effect(
220
+ ProxmoxNotificationTarget,
221
+ Effect.succeed(ProxmoxNotificationTarget.Provider.of(handlers)),
222
+ );