@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,248 @@
1
+ import { sha256 } from './digest.ts';
2
+ import { mountPath, parseDuration, ttlSeconds } from './mount-form.ts';
3
+
4
+ /** ★ MEASURED: the OpenAPI enum for `key_type` is exactly these four, lowercase. */
5
+ export type BaoPkiKeyType = 'any' | 'ec' | 'ed25519' | 'rsa';
6
+
7
+ /** ★ MEASURED: live roles carry `["hostname"]` or the default `["email","hostname"]`. */
8
+ export type BaoPkiCnValidation = 'disabled' | 'email' | 'hostname';
9
+
10
+ export interface BaoPkiRoleProps {
11
+ /** Role name, as `pki/roles/{name}` takes it. */
12
+ name: string;
13
+ /** Mount holding the engine. Defaults to `pki` — the only PKI mount live today. */
14
+ mount?: string;
15
+ /**
16
+ * Domains this role may issue for.
17
+ *
18
+ * ⛔ AN EMPTY LIST IS REFUSED IN reconcile, NOT WRITTEN. With `allow_any_name` false — the
19
+ * default, and what all eight live roles use — a role with no allowed domains can issue
20
+ * nothing, so a mistyped or unresolved prop would quietly brick every renewal under it.
21
+ */
22
+ allowedDomains: readonly string[];
23
+ /** Permit `foo.<allowed domain>`. Default false. */
24
+ allowSubdomains?: boolean;
25
+ /** Permit the allowed domain itself as the CN. Default false. */
26
+ allowBareDomains?: boolean;
27
+ /** Treat `*` inside an allowed domain as a glob. Default false. */
28
+ allowGlobDomains?: boolean;
29
+ /**
30
+ * Default certificate lifetime, as a duration string (`720h`).
31
+ *
32
+ * ⚠️ WRITTEN AS A DURATION, READ BACK AS SECONDS — `ttl` and `max_ttl` both carry
33
+ * `format: seconds` in the OpenAPI and come back as integers (`2592000`). The prop
34
+ * string reaches `bao write` verbatim; the compare happens in seconds. See canonical().
35
+ */
36
+ ttl: string;
37
+ /** Ceiling on a requested lifetime, as a duration string (`8760h`). */
38
+ maxTtl: string;
39
+ /** Key algorithm. Default `rsa` — OpenBao's own default. */
40
+ keyType?: BaoPkiKeyType;
41
+ /** Key size; `0` means "engine default for keyType". rsa 2048/3072/4096, ec 224/256/384/521. */
42
+ keyBits?: number;
43
+ /** Flag certificates for TLS server auth. ⚠️ OpenBao defaults this TRUE. */
44
+ serverFlag?: boolean;
45
+ /** Flag certificates for TLS client auth. ⚠️ OpenBao defaults this TRUE. */
46
+ clientFlag?: boolean;
47
+ /** Extended key usages minus the `ExtKeyUsage` prefix (`ServerAuth`). Empty on all 8 live roles. */
48
+ extKeyUsage?: readonly string[];
49
+ /** Permit IP SANs. ⚠️ OpenBao defaults TRUE; this resource defaults FALSE — see pki-role.ts. */
50
+ allowIpSans?: boolean;
51
+ /** Permit `localhost`/`localdomain` CNs. ⚠️ OpenBao TRUE, this resource FALSE. */
52
+ allowLocalhost?: boolean;
53
+ /** Permit wildcard CNs. ⚠️ OpenBao TRUE, this resource FALSE. */
54
+ allowWildcardCertificates?: boolean;
55
+ /** CN validations to run. ⚠️ OpenBao `["email","hostname"]`, this resource `["hostname"]`. */
56
+ cnValidations?: readonly BaoPkiCnValidation[];
57
+ /** Signature hash bits; `0` auto-detects from key length. One live role pins 256. */
58
+ signatureBits?: number;
59
+ }
60
+
61
+ /**
62
+ * The managed field set, normalised so both sides of a diff are byte-comparable.
63
+ *
64
+ * ⚠️ KEY ORDER IS THE DIGEST. The props side and the live side are both built by the single
65
+ * canonical() literal below, so `JSON.stringify` emits the same key order for each. Two
66
+ * hand-written object literals would drift apart and produce a forever-diff nobody could
67
+ * read from the plan output.
68
+ */
69
+ export interface BaoPkiRoleCanonical {
70
+ allowBareDomains: boolean;
71
+ allowGlobDomains: boolean;
72
+ allowIpSans: boolean;
73
+ allowLocalhost: boolean;
74
+ allowSubdomains: boolean;
75
+ allowWildcardCertificates: boolean;
76
+ allowedDomains: readonly string[];
77
+ clientFlag: boolean;
78
+ cnValidations: readonly string[];
79
+ extKeyUsage: readonly string[];
80
+ keyBits: number;
81
+ keyType: string;
82
+ /** ⚠️ `-1` means the duration prop did not parse — reconcile refuses rather than writing it. */
83
+ maxTtlSeconds: number;
84
+ serverFlag: boolean;
85
+ signatureBits: number;
86
+ ttlSeconds: number;
87
+ }
88
+
89
+ export interface BaoPkiRoleAttributes extends BaoPkiRoleCanonical {
90
+ name: string;
91
+ mount: string;
92
+ /** Human rendering of ttlSeconds — the compare uses the seconds, this is for the plan. */
93
+ ttl: string;
94
+ maxTtl: string;
95
+ /** SHA-256 of the canonical form — safe to persist; see policy.ts. */
96
+ digest: string;
97
+ }
98
+
99
+ const sorted = (values: readonly string[]) => [...values].sort();
100
+
101
+ /** ⚠️ Case-folded for the COMPARE only; the write keeps the author's casing.
102
+ * Why, and what was never measured, is in the pki-role.ts header. */
103
+ const foldedUsages = (values: readonly string[]) => sorted(values.map((v) => v.toLowerCase()));
104
+
105
+ /** The one literal that fixes key order for both sides of the diff. */
106
+ const canonical = (input: BaoPkiRoleCanonical): BaoPkiRoleCanonical => ({
107
+ allowBareDomains: input.allowBareDomains,
108
+ allowGlobDomains: input.allowGlobDomains,
109
+ allowIpSans: input.allowIpSans,
110
+ allowLocalhost: input.allowLocalhost,
111
+ allowSubdomains: input.allowSubdomains,
112
+ allowWildcardCertificates: input.allowWildcardCertificates,
113
+ allowedDomains: sorted(input.allowedDomains),
114
+ clientFlag: input.clientFlag,
115
+ cnValidations: sorted(input.cnValidations),
116
+ extKeyUsage: foldedUsages(input.extKeyUsage),
117
+ keyBits: input.keyBits,
118
+ keyType: input.keyType,
119
+ maxTtlSeconds: input.maxTtlSeconds,
120
+ serverFlag: input.serverFlag,
121
+ signatureBits: input.signatureBits,
122
+ ttlSeconds: input.ttlSeconds,
123
+ });
124
+
125
+ const seconds = (text: string) => parseDuration(text) ?? -1;
126
+
127
+ export const canonicalFromProps = (props: BaoPkiRoleProps): BaoPkiRoleCanonical =>
128
+ canonical({
129
+ allowBareDomains: props.allowBareDomains ?? false,
130
+ allowGlobDomains: props.allowGlobDomains ?? false,
131
+ allowIpSans: props.allowIpSans ?? false,
132
+ allowLocalhost: props.allowLocalhost ?? false,
133
+ allowSubdomains: props.allowSubdomains ?? false,
134
+ allowWildcardCertificates: props.allowWildcardCertificates ?? false,
135
+ allowedDomains: props.allowedDomains,
136
+ clientFlag: props.clientFlag ?? true,
137
+ cnValidations: props.cnValidations ?? ['hostname'],
138
+ extKeyUsage: props.extKeyUsage ?? [],
139
+ keyBits: props.keyBits ?? 0,
140
+ keyType: props.keyType ?? 'rsa',
141
+ maxTtlSeconds: seconds(props.maxTtl),
142
+ serverFlag: props.serverFlag ?? true,
143
+ signatureBits: props.signatureBits ?? 0,
144
+ ttlSeconds: seconds(props.ttl),
145
+ });
146
+
147
+ const bool = (value: unknown, fallback: boolean) => (typeof value === 'boolean' ? value : fallback);
148
+
149
+ const int = (value: unknown, fallback: number) =>
150
+ typeof value === 'number' && Number.isFinite(value) ? value : fallback;
151
+
152
+ const list = (value: unknown): readonly string[] =>
153
+ Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : [];
154
+
155
+ const str = (value: unknown, fallback: string) => (typeof value === 'string' ? value : fallback);
156
+
157
+ export const canonicalFromLive = (live: Record<string, unknown>): BaoPkiRoleCanonical =>
158
+ canonical({
159
+ allowBareDomains: bool(live['allow_bare_domains'], false),
160
+ allowGlobDomains: bool(live['allow_glob_domains'], false),
161
+ allowIpSans: bool(live['allow_ip_sans'], false),
162
+ allowLocalhost: bool(live['allow_localhost'], false),
163
+ allowSubdomains: bool(live['allow_subdomains'], false),
164
+ allowWildcardCertificates: bool(live['allow_wildcard_certificates'], false),
165
+ allowedDomains: list(live['allowed_domains']),
166
+ clientFlag: bool(live['client_flag'], true),
167
+ cnValidations: list(live['cn_validations']),
168
+ extKeyUsage: list(live['ext_key_usage']),
169
+ keyBits: int(live['key_bits'], 0),
170
+ keyType: str(live['key_type'], 'rsa'),
171
+ maxTtlSeconds: ttlSeconds(live['max_ttl']) ?? -1,
172
+ serverFlag: bool(live['server_flag'], true),
173
+ signatureBits: int(live['signature_bits'], 0),
174
+ ttlSeconds: ttlSeconds(live['ttl']) ?? -1,
175
+ });
176
+
177
+ /** ⚠️ Same rendering auth-role-form.ts uses, so `720h` and `2592000` read alike in a plan. */
178
+ const ttlText = (value: number) => {
179
+ if (value < 0) return '';
180
+ if (value !== 0 && value % 86400 === 0) return `${String(value / 86400)}d`;
181
+ if (value !== 0 && value % 3600 === 0) return `${String(value / 3600)}h`;
182
+ if (value !== 0 && value % 60 === 0) return `${String(value / 60)}m`;
183
+ return `${String(value)}s`;
184
+ };
185
+
186
+ export const digestOf = (form: BaoPkiRoleCanonical) => sha256(JSON.stringify(form));
187
+
188
+ export const rolePath = (props: { name: string; mount?: string }) =>
189
+ `${mountPath(props.mount ?? 'pki')}/roles/${props.name}`;
190
+
191
+ export const attributesOf = (
192
+ props: BaoPkiRoleProps,
193
+ live: Record<string, unknown>,
194
+ ): BaoPkiRoleAttributes => {
195
+ const form = canonicalFromLive(live);
196
+ return {
197
+ ...form,
198
+ name: props.name,
199
+ mount: mountPath(props.mount ?? 'pki'),
200
+ ttl: ttlText(form.ttlSeconds),
201
+ maxTtl: ttlText(form.maxTtlSeconds),
202
+ digest: digestOf(form),
203
+ };
204
+ };
205
+
206
+ /**
207
+ * ⚠️ ARRAYS GO OVER AS COMMA-JOINED STRINGS. `allowed_domains` and `ext_key_usage` are
208
+ * documented as "a comma-separated string or list"; `cn_validations` is not, so its
209
+ * splitting is REASONED, not measured. reconcile re-reads and re-compares after every
210
+ * write, so a value that arrived unsplit fails loudly instead of landing wrong.
211
+ *
212
+ * ⚠️ TTLs GO OVER AS THE AUTHOR'S OWN DURATION STRING, never as a re-rendered one — so this
213
+ * never has to know whether OpenBao's parser accepts the `d` suffix ttlText() produces for
214
+ * display. Rendering is for humans; the wire only ever sees what was declared.
215
+ *
216
+ * ★ `ext_key_usage` is OMITTED when empty rather than sent as `ext_key_usage=`. The write
217
+ * replaces the whole role, so an absent field already lands on the `[]` default, and
218
+ * omitting sidesteps how an empty value would be encoded.
219
+ *
220
+ * ★ THE BODY FOR `PUT <mount>/roles/<name>`, ALL STRINGS — the same `k=v` pairs `bao write` sent.
221
+ */
222
+ export const writeBody = (props: BaoPkiRoleProps): Record<string, string> => {
223
+ const form = canonicalFromProps(props);
224
+ const body: Record<string, string> = {
225
+ allowed_domains: form.allowedDomains.join(','),
226
+ allow_bare_domains: String(form.allowBareDomains),
227
+ allow_glob_domains: String(form.allowGlobDomains),
228
+ allow_subdomains: String(form.allowSubdomains),
229
+ allow_ip_sans: String(form.allowIpSans),
230
+ allow_localhost: String(form.allowLocalhost),
231
+ allow_wildcard_certificates: String(form.allowWildcardCertificates),
232
+ cn_validations: form.cnValidations.join(','),
233
+ client_flag: String(form.clientFlag),
234
+ server_flag: String(form.serverFlag),
235
+ key_type: form.keyType,
236
+ key_bits: String(form.keyBits),
237
+ signature_bits: String(form.signatureBits),
238
+ ttl: props.ttl,
239
+ max_ttl: props.maxTtl,
240
+ };
241
+ const usages = props.extKeyUsage ?? [];
242
+ if (usages.length > 0) body['ext_key_usage'] = usages.join(',');
243
+ return body;
244
+ };
245
+
246
+ /** True when live already matches the declaration on every managed field. */
247
+ export const matches = (attributes: BaoPkiRoleAttributes, props: BaoPkiRoleProps) =>
248
+ attributes.digest === digestOf(canonicalFromProps(props));
@@ -0,0 +1,202 @@
1
+ /**
2
+ * An OpenBao PKI role — the policy a future issuance is checked against, and nothing else.
3
+ *
4
+ * ⛔ THE ROLE IS THE POLICY, NEVER THE CERTIFICATE. Nothing here touches `pki/issue/*` (which
5
+ * RETURNS a private key) or `pki/root/generate/*` (which CREATES one). Every value this
6
+ * resource stores is a domain name, a flag, a bit count or a TTL — safe to sit in Alchemy's
7
+ * UNENCRYPTED Postgres state. See policy.ts for why that line is load-bearing here.
8
+ *
9
+ * ⛔ `bao write pki/roles/<name>` REPLACES THE WHOLE ROLE — it is not a merge. OpenBao builds
10
+ * a fresh role entry from the request, so every field left out reverts to its SCHEMA
11
+ * DEFAULT. MEASURED 2026-09-13 against the live OpenBao 2.6.2, from its own
12
+ * `sys/internal/specs/openapi` cross-read against all eight live `pki/roles/*`, four of
13
+ * those defaults are WIDER than what this estate actually runs:
14
+ *
15
+ * allow_ip_sans default true — false on 5 of the 8 roles
16
+ * allow_localhost default true — false on 6 of the 8
17
+ * allow_wildcard_certificates default true — false on 6 of the 8
18
+ * cn_validations default ["email","hostname"] — ["hostname"] on 5 of the 8
19
+ *
20
+ * A resource that managed only the eleven fields the brief named would therefore, on any
21
+ * unrelated TTL edit, silently re-open wildcards and localhost on six production roles.
22
+ * That is why those four are props in pki-role-form.ts though they were not in the brief,
23
+ * and why their default there is the CLOSED value rather than OpenBao's open one.
24
+ *
25
+ * ★ NARROWING IS THE FAIL-SAFE DIRECTION. A default tighter than OpenBao's can only refuse a
26
+ * certificate that should have been issued — loud, and cured by declaring the field. A
27
+ * default looser than OpenBao's issues one that should have been refused, in silence.
28
+ *
29
+ * ⚠️ WHAT WAS NOT MEASURED. Only reads were run against the live engine; no `bao write`
30
+ * happened, so nothing below is a verified round-trip. Two consequences carried into the
31
+ * form file: every live role has `ext_key_usage: []`, so the stored casing of a non-empty
32
+ * list was never observed (the compare folds case because OpenBao matches these
33
+ * case-insensitively at issuance); and every live role has exactly ONE allowed domain, so
34
+ * whether OpenBao preserves list order was never observed either (the compare sorts,
35
+ * because allowed_domains is a set). Both choices can only mask a cosmetic difference,
36
+ * never a policy one — but neither is a measurement.
37
+ *
38
+ * ★ `defaultRemovalPolicy: 'retain'` — see the delete comment below. Opt in with
39
+ * `.pipe(RemovalPolicy.destroy())`; see resource.ts in house/proxmox.
40
+ */
41
+ import { Resource } from 'alchemy';
42
+ import { isResolved } from 'alchemy/Diff';
43
+ import * as Provider from 'alchemy/Provider';
44
+ import * as Effect from 'effect/Effect';
45
+ import type * as HttpClient from 'effect/unstable/http/HttpClient';
46
+ import { baoDelete, baoRead, baoWrite } from './bao-http.ts';
47
+ import { mountPath, parseDuration } from './mount-form.ts';
48
+ import {
49
+ type BaoPkiRoleAttributes,
50
+ type BaoPkiRoleProps,
51
+ attributesOf,
52
+ matches,
53
+ rolePath,
54
+ writeBody,
55
+ } from './pki-role-form.ts';
56
+
57
+ export type { BaoPkiRoleAttributes, BaoPkiRoleProps };
58
+
59
+ export interface BaoPkiRole extends Resource<
60
+ 'Bao.PkiRole',
61
+ BaoPkiRoleProps,
62
+ BaoPkiRoleAttributes,
63
+ never,
64
+ HttpClient.HttpClient
65
+ > {}
66
+
67
+ export const BaoPkiRole = Resource<BaoPkiRole>('Bao.PkiRole', {
68
+ defaultRemovalPolicy: 'retain',
69
+ });
70
+
71
+ const readRole = (props: BaoPkiRoleProps) =>
72
+ Effect.gen(function* () {
73
+ const live = yield* baoRead(rolePath(props));
74
+ if (live === undefined) return undefined;
75
+ return attributesOf(props, live);
76
+ });
77
+
78
+ /** Duration props `bao write` must never see — reconcile turns these into a refusal. */
79
+ const badDurations = (props: BaoPkiRoleProps) =>
80
+ [['ttl', props.ttl] as const, ['maxTtl', props.maxTtl] as const].filter(
81
+ ([, text]) => parseDuration(text) === undefined,
82
+ );
83
+
84
+ export const BaoPkiRoleProvider = () =>
85
+ Provider.effect(
86
+ BaoPkiRole,
87
+ Effect.succeed(
88
+ BaoPkiRole.Provider.of({
89
+ /**
90
+ * ⛔ `bao list pki/roles` IS NOT A LIST OF THINGS THIS OWNS. All eight roles in the
91
+ * live engine answer it, and every one of them predates this resource. Returning
92
+ * them would invite Alchemy to adopt — and then delete — roles it never created.
93
+ */
94
+ list: () => Effect.succeed([]),
95
+
96
+ read: Effect.fn(function* ({ olds }) {
97
+ return yield* readRole(olds);
98
+ }),
99
+
100
+ /**
101
+ * ⛔ IT COMPARES A DIGEST OF THE LIVE ROLE, NOT THE STORED ONE. The attributes fed to
102
+ * `matches` are always freshly derived from a live read in this same operation, so
103
+ * a role widened by hand in the OpenBao UI still reads as drift — which is the
104
+ * whole reason the check-* gates exist. Comparing one digest rather than sixteen
105
+ * fields is also what stops a field being added to props and quietly forgotten in
106
+ * the comparison.
107
+ */
108
+ diff: Effect.fn(function* ({ news, output }) {
109
+ if (output === undefined || !isResolved(news)) return undefined;
110
+ /**
111
+ * ⚠️ A ROLE IS IDENTIFIED BY MOUNT **AND** NAME. Moving a declaration to another
112
+ * engine is a different object under a different CA, never an in-place edit —
113
+ * `replace`, so Alchemy creates the new one and retires the old.
114
+ */
115
+ if (mountPath(news.mount ?? 'pki') !== output.mount)
116
+ return { action: 'replace' } as const;
117
+ /**
118
+ * ⚠️ A DECLARATION reconcile WOULD REFUSE MUST NEVER PLAN AS noop. An empty
119
+ * allowedDomains, or a duration OpenBao cannot parse, can compare equal to a
120
+ * live role already in that state — and a noop there hides the refusal behind a
121
+ * clean plan until the next deploy. Route it to reconcile, which says why.
122
+ */
123
+ if (news.allowedDomains.length === 0 || badDurations(news).length > 0) {
124
+ return { action: 'update' } as const;
125
+ }
126
+ const live = yield* readRole(news);
127
+ if (live === undefined) return { action: 'update' } as const;
128
+ return matches(live, news)
129
+ ? ({ action: 'noop' } as const)
130
+ : ({ action: 'update' } as const);
131
+ }),
132
+
133
+ reconcile: Effect.fn(function* ({ news }) {
134
+ const path = rolePath(news);
135
+ /**
136
+ * ⛔ AN EMPTY allowedDomains IS A REFUSAL, NOT AN EMPTY ROLE. `allow_any_name` is
137
+ * false on every live role, so a role with no allowed domains can issue NOTHING —
138
+ * and an unresolved or mistyped prop reads exactly like an author who meant it.
139
+ * policy.ts refuses an empty fragment directory for the same reason.
140
+ */
141
+ if (news.allowedDomains.length === 0) {
142
+ return yield* Effect.die(
143
+ new Error(
144
+ `Bao.PkiRole ${path}: allowedDomains is empty. With allow_any_name false ` +
145
+ 'that role can issue no certificate at all.',
146
+ ),
147
+ );
148
+ }
149
+ const bad = badDurations(news);
150
+ if (bad.length > 0) {
151
+ return yield* Effect.die(
152
+ new Error(
153
+ `Bao.PkiRole ${path}: ${bad.map(([k, v]) => `${k}=${v}`).join(', ')} ` +
154
+ 'is not a duration OpenBao parses (expect 30m, 720h, 8760h or 0).',
155
+ ),
156
+ );
157
+ }
158
+ const live = yield* readRole(news);
159
+ if (live === undefined || !matches(live, news)) {
160
+ yield* baoWrite('PUT', path, writeBody(news));
161
+ }
162
+ const after = yield* readRole(news);
163
+ if (after === undefined) {
164
+ return yield* Effect.die(
165
+ new Error(`Bao.PkiRole ${path}: write reported success but the role is absent.`),
166
+ );
167
+ }
168
+ /**
169
+ * ⛔ THE WRITE IS NOT TRUSTED, IT IS RE-READ AND RE-COMPARED. A write succeeds (2xx)
170
+ * for a field it accepted but stored differently — the live worry being the
171
+ * comma-joined list values in writeBody, whose splitting is documented for
172
+ * allowed_domains and ext_key_usage but NOT for cn_validations. Without this
173
+ * check a role could land with `cn_validations: ["email,hostname"]`, report
174
+ * success, and then refuse every renewal at issuance time instead.
175
+ */
176
+ if (!matches(after, news)) {
177
+ return yield* Effect.die(
178
+ new Error(
179
+ `Bao.PkiRole ${path}: wrote cleanly but the role read back different. ` +
180
+ 'OpenBao stored something other than what was declared — inspect with ' +
181
+ `\`bao read ${path}\` before retrying.`,
182
+ ),
183
+ );
184
+ }
185
+ return after;
186
+ }),
187
+
188
+ /**
189
+ * ⛔ DELETING A ROLE DOES NOT REVOKE THE CERTIFICATES IT ISSUED — they stay valid to
190
+ * their own expiry, so this is quiet now and loud much later: every renewal through
191
+ * the role fails once the current certificate runs out. MEASURED: the live roles
192
+ * carry ttl 604800 to 31536000 seconds, so the fuse on `rpi5-otel-client` is a full
193
+ * year. Idempotent as Alchemy requires — already gone is success — but retain is
194
+ * the default for exactly this reason.
195
+ */
196
+ delete: Effect.fn(function* ({ output }) {
197
+ yield* baoDelete(rolePath(output));
198
+ return undefined;
199
+ }),
200
+ }),
201
+ ),
202
+ );
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The refusal that stands between a bad checkout and a policy with every grant revoked.
3
+ */
4
+ import assert from 'node:assert/strict';
5
+ import { describe, it } from 'node:test';
6
+ import { isEmptyAssembly } from './policy-assembly.ts';
7
+
8
+ describe('isEmptyAssembly', () => {
9
+ it('refuses a directory with no fragment files', () => {
10
+ assert.equal(isEmptyAssembly(0, ''), true);
11
+ });
12
+
13
+ // ⛔ The case the first guard missed: files exist, grants do not.
14
+ it('refuses fragments that are empty or whitespace', () => {
15
+ assert.equal(isEmptyAssembly(1, '\n'), true);
16
+ assert.equal(isEmptyAssembly(2, ' \n\t\n'), true);
17
+ });
18
+
19
+ it('refuses fragments that carry comments but no path grant', () => {
20
+ assert.equal(isEmptyAssembly(1, '# homeflare-agent\n# grants follow\n'), true);
21
+ });
22
+
23
+ it('allows an assembly with at least one path grant, indented or not', () => {
24
+ const kv = '# kv\npath "kv/data/apps/*" {\n capabilities = ["read"]\n}\n';
25
+ assert.equal(isEmptyAssembly(1, kv), false);
26
+ assert.equal(isEmptyAssembly(1, ' path "sys/mounts" { capabilities = ["read"] }\n'), false);
27
+ });
28
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Whether an assembled policy would carry NO GRANT — the one write `Bao.Policy` must refuse.
3
+ *
4
+ * ⛔ EMPTY MEANS NO `path` GRANT, NOT NO FILES. Writing `sys/policies/acl/<name>` with nothing in it
5
+ * SILENTLY REVOKES every grant the policy carries, for every token bound to it. The first guard
6
+ * only counted fragment files, so a directory whose one fragment was empty, whitespace or
7
+ * comments — a truncated edit, a bad checkout — assembled to text with no grant in it, passed,
8
+ * and would have replaced the live policy with nothing. Found by grok's review on 2026-09-14.
9
+ * ★ A GRANT IS A `path "…"` STANZA, the unit apply-agent-policy.sh counts and `grantsOf` in
10
+ * policy.ts reports. Leading whitespace is allowed here, so an indented stanza still counts.
11
+ */
12
+ export const isEmptyAssembly = (parts: number, joined: string): boolean =>
13
+ parts === 0 || !/^\s*path\s+"/m.test(joined);
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Bao.Policy's calls against a fake that behaves like OpenBao where it matters: it STRIPS the
3
+ * policy's trailing newline on write (measured on the live server — see digest.ts), answers 404 for
4
+ * a policy it does not have, and 204 for a delete whether or not the policy existed.
5
+ */
6
+ import assert from 'node:assert/strict';
7
+ import { describe, it } from 'node:test';
8
+ import { BaoError } from './bao-status.ts';
9
+ import { sha256 } from './digest.ts';
10
+ import { type Seen, run, runFailure, withFake } from './fake-bao.ts';
11
+ import { deletePolicy, readPolicy, writePolicy } from './policy-wire.ts';
12
+
13
+ const PREFIX = '/v1/sys/policies/acl/';
14
+
15
+ const policyStore = () => {
16
+ const policies = new Map<string, string>();
17
+ return (seen: Seen) => {
18
+ const name = seen.path.slice(PREFIX.length);
19
+ if (seen.method === 'PUT') {
20
+ const { policy } = JSON.parse(seen.body) as { policy: string };
21
+ policies.set(name, policy.replace(/\n+$/, ''));
22
+ return { status: 204 };
23
+ }
24
+ if (seen.method === 'DELETE') {
25
+ policies.delete(name);
26
+ return { status: 204 };
27
+ }
28
+ const policy = policies.get(name);
29
+ return policy === undefined
30
+ ? { json: { errors: [] }, status: 404 }
31
+ : { json: { data: { name, policy } }, status: 200 };
32
+ };
33
+ };
34
+
35
+ /** Two fragments, each followed by a newline — exactly how policy.ts assembles them. */
36
+ const ASSEMBLED = 'path "kv/data/a" {\n capabilities = ["read"]\n}\n' + 'path "kv/data/b" {}\n';
37
+
38
+ describe('policy wire', () => {
39
+ it('reads a policy that was never written as absent', async () => {
40
+ await withFake(policyStore(), async (bao) => {
41
+ assert.equal(await run({ BAO_ADDR: bao.address }, readPolicy('zz-new-policy')), '');
42
+ });
43
+ });
44
+
45
+ // ⛔ THE FOREVER-DIFF. Live text comes back one newline short; the digests must still agree, or
46
+ // every plan says `update` and reconcile rewrites a policy that already matches.
47
+ it('round-trips through the stripped newline with equal digests', async () => {
48
+ await withFake(policyStore(), async (bao) => {
49
+ const env = { BAO_ADDR: bao.address };
50
+ await run(env, writePolicy('homeflare-llm', ASSEMBLED));
51
+ assert.equal(bao.seen[0]?.method, 'PUT');
52
+ assert.equal(bao.seen[0]?.path, `${PREFIX}homeflare-llm`);
53
+ assert.deepEqual(JSON.parse(bao.seen[0]?.body ?? ''), { policy: ASSEMBLED });
54
+ const live = await run(env, readPolicy('homeflare-llm'));
55
+ assert.notEqual(live, ASSEMBLED, 'the fake strips the newline, as OpenBao does');
56
+ assert.equal(sha256(live), sha256(ASSEMBLED));
57
+ });
58
+ });
59
+
60
+ it('fails a refused read (403) instead of calling the policy absent', async () => {
61
+ await withFake(
62
+ () => ({ json: { errors: ['permission denied'] }, status: 403 }),
63
+ async (bao) => {
64
+ const error = await runFailure({ BAO_ADDR: bao.address }, readPolicy('homeflare-admin'));
65
+ assert.ok(error instanceof BaoError);
66
+ assert.equal(error.status, 403);
67
+ },
68
+ );
69
+ });
70
+
71
+ it('fails a 200 that carries no policy text', async () => {
72
+ await withFake(
73
+ () => ({ json: { data: { name: 'x' } }, status: 200 }),
74
+ async (bao) => {
75
+ const error = await runFailure({ BAO_ADDR: bao.address }, readPolicy('x'));
76
+ assert.match(String(error), /no data\.policy/);
77
+ },
78
+ );
79
+ });
80
+
81
+ it('deletes idempotently: a policy already gone is success', async () => {
82
+ await withFake(
83
+ () => ({ json: { errors: [] }, status: 404 }),
84
+ async (bao) => {
85
+ await run({ BAO_ADDR: bao.address }, deletePolicy('zz-gone'));
86
+ assert.equal(bao.seen[0]?.method, 'DELETE');
87
+ },
88
+ );
89
+ });
90
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The three OpenBao calls Bao.Policy makes — read, write and delete of `sys/policies/acl/<name>`.
3
+ * Split from policy.ts so the trailing-newline round trip can be tested against a fake server.
4
+ */
5
+ import * as Effect from 'effect/Effect';
6
+ import { baoDelete, baoRead, baoWrite } from './bao-http.ts';
7
+ import { BaoError } from './bao-status.ts';
8
+
9
+ /** The endpoint the CLI's policy commands used — openbao v2.6.2 api/sys_policy.go:60, :100, :122. */
10
+ export const policyPath = (name: string) => `sys/policies/acl/${name}`;
11
+
12
+ /**
13
+ * The live policy text, or '' when the policy is genuinely absent. Anything else FAILS.
14
+ *
15
+ * 🔴 THE ABSENCE WORDING WAS THE POLICY COMMAND'S OWN, AND ASSUMING OTHERWISE MADE A NEW POLICY
16
+ * UNCREATABLE. `No value found at` was measured for `bao read`; `bao policy read` never printed
17
+ * it. Its API client turned a 404 into an empty string and the CLI reported that in its own
18
+ * words — openbao v2.6.2 api/sys_policy.go:65-67 and command/policy_read.go:85-87:
19
+ *
20
+ * No policy named: <name> exit 2
21
+ *
22
+ * So `read` on a policy that did not exist yet FAILED instead of answering absent, and
23
+ * `reconcile` failed at its own pre-write read. Every policy declared so far already existed,
24
+ * which is the only reason nothing broke. MEASURED 2026-09-14 under the admin lane —
25
+ * `bao policy read zz-probe-absent-policy-4c1e` printed exactly that line and exited 2. (The
26
+ * agent lane cannot measure it: it is refused `sys/policies/acl` with a 403 first.)
27
+ * ★ THE WORDING PROBLEM IS GONE WITH THE CLI. The line above was the CLI's rendering of the 404 that
28
+ * the server returns for a missing policy (vault/logical_system.go handlePoliciesRead returns no
29
+ * response, which RespondErrorCommon turns into 404). That status is now read directly, and a
30
+ * refused read is a 403 — two codes, no text to confuse.
31
+ *
32
+ * ⚠️ `data.policy` IS THE TEXT, and a 200 without it is a failure, as it was in the CLI's client
33
+ * ("no policy found in response", api/sys_policy.go:81-85).
34
+ */
35
+ export const readPolicy = (name: string) =>
36
+ Effect.flatMap(baoRead(policyPath(name)), (data) => {
37
+ if (data === undefined) return Effect.succeed('');
38
+ const text = data['policy'];
39
+ return typeof text === 'string'
40
+ ? Effect.succeed(text)
41
+ : Effect.fail(new BaoError(200, 'GET', policyPath(name), ['no data.policy in the response']));
42
+ });
43
+
44
+ /**
45
+ * ★ THE HCL TRAVELS IN THE REQUEST BODY (`{"policy": …}`, api/sys_policy.go:96-101). The CLI path
46
+ * needed a scoped temp file because passing HCL on argv would have put every grant in the process
47
+ * list; a JSON body never reaches argv, so the temp file is gone with it.
48
+ */
49
+ export const writePolicy = (name: string, hcl: string) =>
50
+ baoWrite('PUT', policyPath(name), { policy: hcl });
51
+
52
+ /** Deleting a policy that is already gone answers success (handlePoliciesDelete returns nothing). */
53
+ export const deletePolicy = (name: string) => baoDelete(policyPath(name));