@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,88 @@
1
+ /**
2
+ * The in-flight cap on OpenBao exchanges (`BaoGate` in bao-http.ts), counted where the listener that
3
+ * died on 2026-09-14 counted it: on the server. The fake holds every request for a few milliseconds
4
+ * and records the most it ever held at once.
5
+ */
6
+ import assert from 'node:assert/strict';
7
+ import { describe, it } from 'node:test';
8
+ import * as Effect from 'effect/Effect';
9
+ import * as Semaphore from 'effect/Semaphore';
10
+ import { BaoGate, MAX_IN_FLIGHT, baoRead } from './bao-http.ts';
11
+ import { type Reply, run, runFailure, withFake } from './fake-bao.ts';
12
+
13
+ /** A fake that holds each request `holdMs`, answering `reply`, and remembers its peak overlap. */
14
+ const holding = (reply: (path: string) => Reply, holdMs = 15) => {
15
+ const counts = { inFlight: 0, peak: 0 };
16
+ const answer = async (seen: { path: string }) => {
17
+ counts.inFlight += 1;
18
+ counts.peak = Math.max(counts.peak, counts.inFlight);
19
+ await Bun.sleep(holdMs);
20
+ counts.inFlight -= 1;
21
+ return reply(seen.path);
22
+ };
23
+ return { answer, counts };
24
+ };
25
+
26
+ const OK: Reply = { json: { data: {} }, status: 200 };
27
+
28
+ const burst = (address: string, calls: number, gate?: Semaphore.Semaphore) =>
29
+ Promise.all(
30
+ Array.from({ length: calls }, (_, index) => {
31
+ const read = baoRead(`kv/burst/${String(index)}`);
32
+ return run(
33
+ { BAO_ADDR: address },
34
+ gate === undefined ? read : read.pipe(Effect.provideService(BaoGate, gate)),
35
+ );
36
+ }),
37
+ );
38
+
39
+ describe('BaoGate', () => {
40
+ it('never lets more than the gate through at once, and still answers every call', async () => {
41
+ const fake = holding(() => OK);
42
+ await withFake(fake.answer, async (bao) => {
43
+ const results = await burst(bao.address, 40, Semaphore.makeUnsafe(4));
44
+ assert.equal(results.length, 40);
45
+ assert.equal(bao.seen.length, 40);
46
+ // ★ EQUAL, NOT AT-MOST: 4 proves the calls really overlapped, so the cap was what held them.
47
+ assert.equal(fake.counts.peak, 4);
48
+ });
49
+ });
50
+
51
+ it('caps an unprovided burst at MAX_IN_FLIGHT — the gate every provider shares', async () => {
52
+ const fake = holding(() => OK);
53
+ await withFake(fake.answer, async (bao) => {
54
+ await burst(bao.address, MAX_IN_FLIGHT * 3);
55
+ assert.equal(fake.counts.peak, MAX_IN_FLIGHT);
56
+ });
57
+ });
58
+
59
+ // ★ THE SENSITIVITY CHECK: the same burst through a wide gate overlaps far past 4, so the tests
60
+ // above would fail if the permit were dropped from baoCall.
61
+ it('lets the burst overlap when the gate is wide, so the cap above is doing the holding', async () => {
62
+ const fake = holding(() => OK);
63
+ await withFake(fake.answer, async (bao) => {
64
+ await burst(bao.address, 40, Semaphore.makeUnsafe(1000));
65
+ assert.ok(fake.counts.peak > 4, `peak was ${String(fake.counts.peak)}`);
66
+ });
67
+ });
68
+
69
+ it('gives a permit back when OpenBao refuses the call', async () => {
70
+ const denied: Reply = { json: { errors: ['permission denied'] }, status: 403 };
71
+ const fake = holding((path) => (path.includes('/refused/') ? denied : OK), 2);
72
+ await withFake(fake.answer, async (bao) => {
73
+ const gate = Semaphore.makeUnsafe(2);
74
+ const env = { BAO_ADDR: bao.address };
75
+ const gated = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
76
+ effect.pipe(Effect.provideService(BaoGate, gate));
77
+ await Promise.all(
78
+ Array.from({ length: 10 }, (_, index) =>
79
+ runFailure(env, gated(baoRead(`kv/refused/${String(index)}`))),
80
+ ),
81
+ );
82
+ // ⛔ A LEAKED PERMIT HANGS HERE rather than failing, so the race turns a hang into a failure.
83
+ const after = run(env, gated(baoRead('kv/after')));
84
+ const hung = Bun.sleep(2000).then(() => 'hung' as const);
85
+ assert.notEqual(await Promise.race([after, hung]), 'hung');
86
+ });
87
+ });
88
+ });
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The unix-socket path, against a fake agent listener on Bun.serve's `unix` option.
3
+ *
4
+ * ★ An OpenBao Agent `listener "unix"` serves the API on a socket; BAO_ADDR / BAO_AGENT_ADDR name
5
+ * it as `unix:///path`. These prove the request really leaves over the socket — through the real
6
+ * FetchHttpClient layer, with no TCP server to fall back on.
7
+ */
8
+ import assert from 'node:assert/strict';
9
+ import { rmSync } from 'node:fs';
10
+ import { tmpdir } from 'node:os';
11
+ import { join } from 'node:path';
12
+ import { describe, it } from 'node:test';
13
+ import { baoDelete, baoRead } from './bao-http.ts';
14
+ import { fakeBao, run, withFake } from './fake-bao.ts';
15
+
16
+ const socketPath = (label: string) => {
17
+ const path = join(tmpdir(), `fake-bao-${label}-${String(process.pid)}.sock`);
18
+ rmSync(path, { force: true });
19
+ return path;
20
+ };
21
+
22
+ describe('a unix:// address', () => {
23
+ it('reads over the socket, in agent mode, with the namespace', async () => {
24
+ const path = socketPath('read');
25
+ await withFake(
26
+ () => ({ json: { data: { type: 'kv' } }, status: 200 }),
27
+ async (agent) => {
28
+ const env = { BAO_ADDR: agent.address, BAO_NAMESPACE: 'homeflare' };
29
+ assert.deepEqual(await run(env, baoRead('sys/mounts/kv')), { type: 'kv' });
30
+ assert.equal(agent.seen[0]?.path, '/v1/sys/mounts/kv');
31
+ assert.equal(agent.seen[0]?.headers.get('x-vault-namespace'), 'homeflare');
32
+ assert.equal(agent.seen[0]?.headers.has('x-vault-token'), false);
33
+ },
34
+ path,
35
+ );
36
+ rmSync(path, { force: true });
37
+ });
38
+
39
+ it('classifies over the socket the same way: 404 read absent, 404 delete success', async () => {
40
+ const path = socketPath('absent');
41
+ await withFake(
42
+ () => ({ json: { errors: [] }, status: 404 }),
43
+ async (agent) => {
44
+ const env = { BAO_ADDR: agent.address };
45
+ assert.equal(await run(env, baoRead('pki/roles/zz')), undefined);
46
+ await run(env, baoDelete('pki/roles/zz'));
47
+ assert.equal(agent.seen.length, 2);
48
+ },
49
+ path,
50
+ );
51
+ rmSync(path, { force: true });
52
+ });
53
+
54
+ // ★ api/client.go:761-762 — BAO_AGENT_ADDR wins. BAO_ADDR here points at a server that is gone,
55
+ // so the call only succeeds if it went to the agent socket.
56
+ it('prefers a BAO_AGENT_ADDR socket over BAO_ADDR', async () => {
57
+ const gone = fakeBao(() => ({ status: 200 }));
58
+ gone.stop();
59
+ const path = socketPath('agent');
60
+ await withFake(
61
+ () => ({ json: { data: { ok: true } }, status: 200 }),
62
+ async (agent) => {
63
+ const env = { BAO_ADDR: gone.address, BAO_AGENT_ADDR: agent.address };
64
+ assert.deepEqual(await run(env, baoRead('kv/config')), { ok: true });
65
+ },
66
+ path,
67
+ );
68
+ rmSync(path, { force: true });
69
+ });
70
+ });
@@ -0,0 +1,161 @@
1
+ /**
2
+ * The HTTP client against a fake OpenBao on Bun.serve: status classification end to end, the
3
+ * namespace header, the token header present with BAO_TOKEN and ABSENT in agent mode, and the
4
+ * token kept out of every error and every span.
5
+ */
6
+ import assert from 'node:assert/strict';
7
+ import { describe, it } from 'node:test';
8
+ import * as Effect from 'effect/Effect';
9
+ import * as Headers from 'effect/unstable/http/Headers';
10
+ import * as HttpClient from 'effect/unstable/http/HttpClient';
11
+ import * as HttpClientResponse from 'effect/unstable/http/HttpClientResponse';
12
+ import { BaoEnv, baoDelete, baoRead, baoWrite } from './bao-http.ts';
13
+ import { BaoError } from './bao-status.ts';
14
+ import { fakeBao, run, runFailure, withFake } from './fake-bao.ts';
15
+
16
+ const TOKEN = 'hvs.FAKE-sentinel-never-a-real-token';
17
+ const NONE = { errors: [] };
18
+
19
+ const keepsTokenOut = (error: unknown) => {
20
+ assert.ok(error instanceof BaoError);
21
+ assert.ok(!error.message.includes(TOKEN), 'token in message');
22
+ assert.ok(!JSON.stringify(error).includes(TOKEN), 'token in JSON');
23
+ assert.ok(!Bun.inspect(error).includes(TOKEN), 'token in inspect');
24
+ return error;
25
+ };
26
+
27
+ describe('baoRead', () => {
28
+ it('returns data, sending the token, the namespace and X-Vault-Request', async () => {
29
+ const reply = { json: { data: { mint_user: 'hf-read@pve', ttl: 3600 } }, status: 200 };
30
+ await withFake(
31
+ () => reply,
32
+ async (bao) => {
33
+ const env = { BAO_ADDR: bao.address, BAO_NAMESPACE: 'homeflare', BAO_TOKEN: TOKEN };
34
+ const data = await run(env, baoRead('proxmox-tb4/roles/read'));
35
+ assert.deepEqual(data, { mint_user: 'hf-read@pve', ttl: 3600 });
36
+ const [seen] = bao.seen;
37
+ assert.equal(seen?.method, 'GET');
38
+ assert.equal(seen?.path, '/v1/proxmox-tb4/roles/read');
39
+ assert.equal(seen?.headers.get('x-vault-namespace'), 'homeflare');
40
+ assert.equal(seen?.headers.get('x-vault-token'), TOKEN);
41
+ assert.equal(seen?.headers.get('x-vault-request'), 'true');
42
+ },
43
+ );
44
+ });
45
+
46
+ // ★ AGENT MODE: the listener injects its auto-auth token only into a request that carries none.
47
+ it('sends no token header at all when BAO_TOKEN is unset', async () => {
48
+ await withFake(
49
+ () => ({ json: { data: {} }, status: 200 }),
50
+ async (bao) => {
51
+ await run({ BAO_ADDR: bao.address }, baoRead('kv/config'));
52
+ assert.equal(bao.seen[0]?.headers.has('x-vault-token'), false);
53
+ assert.equal(bao.seen[0]?.headers.has('x-vault-namespace'), false);
54
+ },
55
+ );
56
+ });
57
+
58
+ it('reads a 404 as absent', async () => {
59
+ await withFake(
60
+ () => ({ json: NONE, status: 404 }),
61
+ async (bao) => {
62
+ assert.equal(await run({ BAO_ADDR: bao.address }, baoRead('pki/roles/zz')), undefined);
63
+ },
64
+ );
65
+ });
66
+
67
+ it('fails a 403 with OpenBao errors and without the token', async () => {
68
+ const denied = {
69
+ json: { errors: ['1 error occurred:\n\t* permission denied\n\n'] },
70
+ status: 403,
71
+ };
72
+ await withFake(
73
+ () => denied,
74
+ async (bao) => {
75
+ const env = { BAO_ADDR: bao.address, BAO_TOKEN: TOKEN };
76
+ const error = keepsTokenOut(await runFailure(env, baoRead('sys/policies/acl/x')));
77
+ assert.equal(error.status, 403);
78
+ assert.deepEqual(error.errors, denied.json.errors);
79
+ },
80
+ );
81
+ });
82
+
83
+ it('fails a sealed server (503)', async () => {
84
+ await withFake(
85
+ () => ({ json: { errors: ['Vault is sealed'] }, status: 503 }),
86
+ async (bao) => {
87
+ const env = { BAO_ADDR: bao.address, BAO_TOKEN: TOKEN };
88
+ assert.equal(keepsTokenOut(await runFailure(env, baoRead('kv/config'))).status, 503);
89
+ },
90
+ );
91
+ });
92
+
93
+ it('fails a server that is not listening, as no response', async () => {
94
+ const gone = fakeBao(() => ({ status: 200 }));
95
+ gone.stop();
96
+ const env = { BAO_ADDR: gone.address, BAO_TOKEN: TOKEN };
97
+ const error = keepsTokenOut(await runFailure(env, baoRead('kv/config')));
98
+ assert.equal(error.status, 0);
99
+ assert.match(error.message, /-> no response: /);
100
+ });
101
+
102
+ // ⛔ Effect writes request headers onto the client span; the token must be a redacted name.
103
+ it('adds x-vault-token to the header names Effect redacts', async () => {
104
+ let names: ReadonlyArray<RegExp | string> = [];
105
+ const recording = HttpClient.make((request, _url, _signal, fiber) => {
106
+ names = fiber.getRef(Headers.CurrentRedactedNames);
107
+ const response = new Response('{"data":{}}', { status: 200 });
108
+ return Effect.succeed(HttpClientResponse.fromWeb(request, response));
109
+ });
110
+ await Effect.runPromise(
111
+ baoRead('kv/config').pipe(
112
+ Effect.provideService(BaoEnv, { BAO_ADDR: 'http://fake.invalid', BAO_TOKEN: TOKEN }),
113
+ Effect.provideService(HttpClient.HttpClient, recording),
114
+ ),
115
+ );
116
+ assert.ok(names.includes('x-vault-token'));
117
+ assert.ok(names.includes('authorization'), 'the defaults are kept');
118
+ });
119
+ });
120
+
121
+ describe('baoDelete', () => {
122
+ it('succeeds on a 404 — already gone — and on a 204', async () => {
123
+ for (const reply of [{ json: NONE, status: 404 }, { status: 204 }]) {
124
+ await withFake(
125
+ () => reply,
126
+ async (bao) => {
127
+ await run({ BAO_ADDR: bao.address }, baoDelete('ssh/roles/zz'));
128
+ assert.equal(bao.seen[0]?.method, 'DELETE');
129
+ },
130
+ );
131
+ }
132
+ });
133
+
134
+ it('fails a refused delete (403) and a sealed one (503)', async () => {
135
+ for (const status of [403, 503]) {
136
+ await withFake(
137
+ () => ({ json: { errors: ['nope'] }, status }),
138
+ async (bao) => {
139
+ const error = await runFailure({ BAO_ADDR: bao.address }, baoDelete('ssh/roles/zz'));
140
+ assert.equal(keepsTokenOut(error).status, status);
141
+ },
142
+ );
143
+ }
144
+ });
145
+ });
146
+
147
+ describe('baoWrite', () => {
148
+ it('sends the body as JSON with its content type, and fails a 404', async () => {
149
+ await withFake(
150
+ (seen) => (seen.path === '/v1/ok/roles/x' ? { status: 204 } : { json: NONE, status: 404 }),
151
+ async (bao) => {
152
+ const env = { BAO_ADDR: bao.address };
153
+ await run(env, baoWrite('PUT', 'ok/roles/x', { ttl: '3600' }));
154
+ assert.equal(bao.seen[0]?.headers.get('content-type'), 'application/json');
155
+ assert.deepEqual(JSON.parse(bao.seen[0]?.body ?? ''), { ttl: '3600' });
156
+ const error = await runFailure(env, baoWrite('PUT', 'nope/roles/x', { ttl: '1' }));
157
+ assert.equal(keepsTokenOut(error).status, 404);
158
+ },
159
+ );
160
+ });
161
+ });
@@ -0,0 +1,185 @@
1
+ /**
2
+ * One call to OpenBao's HTTP API through Effect's `HttpClient` — the transport every Bao.* family
3
+ * uses since it stopped shelling out to `bao`.
4
+ *
5
+ * ★ `HttpClient`, THE SAME PATTERN house/proxmox and house/forgejo USE AGAINST THEIR APIs, decided
6
+ * 2026-09-14. It buys a typed error channel, interruption when a plan is cancelled, the runtime's
7
+ * tracing, and a client a test can point at a fake server — and it needs no Alchemy session, so
8
+ * every call here works from all four operations. (`CommandExecutor.run` was tried on Bao.Policy
9
+ * first and blocked read/diff: Alchemy passes a session to reconcile/delete but NOT to read/diff,
10
+ * Provider.ts:258-289. The ChildProcessSpawner that fixed that is gone with the CLI.)
11
+ * ⚠️ THE STACK MUST PROVIDE `FetchHttpClient.layer`. alchemy.run.ts does, the way
12
+ * house/proxmox/alchemy.run.ts does.
13
+ */
14
+ import * as Context from 'effect/Context';
15
+ import * as Effect from 'effect/Effect';
16
+ import * as Option from 'effect/Option';
17
+ import * as Semaphore from 'effect/Semaphore';
18
+ import * as FetchHttpClient from 'effect/unstable/http/FetchHttpClient';
19
+ import * as Headers from 'effect/unstable/http/Headers';
20
+ import * as HttpClient from 'effect/unstable/http/HttpClient';
21
+ import * as HttpClientRequest from 'effect/unstable/http/HttpClientRequest';
22
+ import { type BaoEnvironment, headersFor, resolveAddress } from './bao-address.ts';
23
+ import { BaoError, type BaoIntent, settle } from './bao-status.ts';
24
+
25
+ /**
26
+ * The environment a call resolves BAO_ADDR, BAO_NAMESPACE and BAO_TOKEN from — `process.env`,
27
+ * read at call time, unless a test provides another.
28
+ */
29
+ export const BaoEnv = Context.Reference<BaoEnvironment>('homeflare/openbao/BaoEnv', {
30
+ defaultValue: () => process.env,
31
+ });
32
+
33
+ /**
34
+ * ⛔ AT MOST EIGHT EXCHANGES WITH OPENBAO AT ONCE — ACROSS EVERY Bao.* FAMILY, IN ONE PROCESS.
35
+ *
36
+ * 🔴 MEASURED 2026-09-14 10:41: A DEPLOY OF THIS STACK TOOK OPENBAO's API OFF THE MINI. Alchemy fans
37
+ * resources out with a hard-coded `concurrency: "unbounded"` (alchemy Plan.ts:476, Apply.ts:259,
38
+ * :263, :303 — no option reaches it), and 555 Bao.CloudflareRole resources each read, diff,
39
+ * reconcile and read back: well over a thousand requests at one loopback listener within seconds.
40
+ * OpenBao logged
41
+ * HTTP server (listening on 127.0.0.1:8200) exited with error: set tcp
42
+ * 127.0.0.1:8200->127.0.0.1:57974: setsockopt: invalid argument
43
+ * and KEPT RUNNING with 8200 closed. launchd saw a live process and restarted nothing. Everything
44
+ * on the mini that talks to 127.0.0.1:8200 was cut off, starting with the agent's templates, until
45
+ * a manual restart.
46
+ * ★ THE LISTENER DYING IS A GO-ON-DARWIN FAILURE, NOT OURS TO FIX. Traefik lost its entrypoints the
47
+ * same way on darwin/arm64 (traefik issue 8841). The BURST is ours. The adoption deploy an hour
48
+ * earlier made the same calls and survived, so a cap narrows a race; it does not prove it closed.
49
+ * ★ A SEMAPHORE, NOT `HttpClient.withRateLimiter`. That limits requests per window and needs a
50
+ * RateLimiter service. The failure is simultaneous connections, and only a cap on in-flight
51
+ * exchanges bounds those whatever each one's latency.
52
+ * ⚠️ A Context.Reference SO A TEST CAN PROVIDE ITS OWN GATE. The default is built ONCE and cached on
53
+ * the reference (effect Context.ts:1582-1588), so every call in the process shares one gate.
54
+ * ⚠️ EIGHT IS A CHOICE, NOT A MEASUREMENT OF WHAT THE LISTENER TOLERATES. Nothing here can find that
55
+ * number without re-running the failure against production.
56
+ */
57
+ export const MAX_IN_FLIGHT = 8;
58
+
59
+ export const BaoGate = Context.Reference<Semaphore.Semaphore>('homeflare/openbao/BaoGate', {
60
+ defaultValue: () => Semaphore.makeUnsafe(MAX_IN_FLIGHT),
61
+ });
62
+
63
+ /**
64
+ * ⚠️ THE CLI's DEFAULT CLIENT TIMEOUT (openbao v2.6.2 api/client.go:324). A hung agent or a server
65
+ * that accepts and never answers would otherwise hang the plan with no line saying why.
66
+ */
67
+ const TIMEOUT = '60 seconds';
68
+
69
+ /**
70
+ * ⛔ EFFECT RECORDS EVERY REQUEST HEADER ON THE CLIENT SPAN and redacts only the names in
71
+ * `Headers.CurrentRedactedNames` — by default authorization, cookie, set-cookie and x-api-key
72
+ * (effect Headers.ts:753-762; HttpClient.ts:842-849, with `TracerHeaderFilter` defaulting to
73
+ * true). `X-Vault-Token` is not on that list, so under any tracer the token would be exported as
74
+ * a span attribute. It is added for every call made here.
75
+ */
76
+ const redactingToken = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
77
+ Effect.gen(function* () {
78
+ const names = yield* Headers.CurrentRedactedNames;
79
+ return yield* Effect.provideService(effect, Headers.CurrentRedactedNames, [
80
+ ...names,
81
+ 'x-vault-token',
82
+ ]);
83
+ });
84
+
85
+ /**
86
+ * Route the exchange over a unix socket when the address is one.
87
+ *
88
+ * ★ BUN's `fetch` TAKES A `unix` OPTION (bun-types globals.d.ts BunFetchRequestInit.unix), AND
89
+ * `FetchHttpClient.RequestInit` IS EFFECT'S DOCUMENTED WAY TO PASS FETCH OPTIONS. The fetch client
90
+ * spreads that service into every call (effect FetchHttpClient.ts:59-72), reading it from the
91
+ * CALLING fiber's context — and `layerMergedContext` merges the caller's context over the layer's
92
+ * (HttpClient.ts:2065-2076), so providing it around one call routes that call alone. Any defaults
93
+ * already provided are kept.
94
+ * ⛔ IT ONLY WORKS ON BUN, SO ANYWHERE ELSE IT REFUSES. Node's fetch ignores `unix` and would send
95
+ * the request to http://localhost:80 — a silent wrong server, which is the one outcome this
96
+ * package exists to prevent. It also assumes the provided `HttpClient` is the fetch one.
97
+ */
98
+ const overSocket =
99
+ (socket: string | undefined, method: string, path: string) =>
100
+ <A, R>(effect: Effect.Effect<A, BaoError, R>): Effect.Effect<A, BaoError, R> => {
101
+ if (socket === undefined) return effect;
102
+ if (typeof Bun === 'undefined') {
103
+ return Effect.fail(
104
+ new BaoError(0, method, path, ['a unix:// address needs Bun fetch; this is not Bun']),
105
+ );
106
+ }
107
+ return Effect.flatMap(Effect.serviceOption(FetchHttpClient.RequestInit), (defaults) => {
108
+ const init: BunFetchRequestInit = { ...Option.getOrElse(defaults, () => ({})), unix: socket };
109
+ return Effect.provideService(effect, FetchHttpClient.RequestInit, init);
110
+ });
111
+ };
112
+
113
+ /** One exchange, classified. The body is the parsed JSON object, or undefined for absent / 204. */
114
+ export const baoCall = (
115
+ intent: BaoIntent,
116
+ method: 'DELETE' | 'GET' | 'POST' | 'PUT',
117
+ path: string,
118
+ body?: Readonly<Record<string, unknown>>,
119
+ ) =>
120
+ Effect.gen(function* () {
121
+ const env = yield* BaoEnv;
122
+ const address = resolveAddress(env);
123
+ const client = yield* HttpClient.HttpClient;
124
+ // ⚠️ The content type goes on `bodyText`, never in the header map: `bodyText` overwrites it.
125
+ // house/proxmox/src/client.ts has the scar.
126
+ const request = HttpClientRequest.make(method)(`${address.base}/v1/${path}`).pipe(
127
+ HttpClientRequest.setHeaders(headersFor(address, env)),
128
+ body === undefined
129
+ ? (self) => self
130
+ : HttpClientRequest.bodyText(JSON.stringify(body), 'application/json'),
131
+ );
132
+ // ★ Status first, body read exactly once, as text — an error body is not reliably JSON.
133
+ const exchange = client.execute(request).pipe(
134
+ Effect.flatMap((response) =>
135
+ Effect.map(response.text, (text) => ({ status: response.status, text })),
136
+ ),
137
+ Effect.timeout(TIMEOUT),
138
+ // ⛔ `.message` ONLY: the HttpClientError message is the reason, method and URL. The error
139
+ // object itself holds the request, whose headers hold the token.
140
+ Effect.mapError((cause) => new BaoError(0, method, path, [cause.message])),
141
+ );
142
+ // ★ THE PERMIT WRAPS THE TIMEOUT, NOT THE REVERSE: a call queued behind the gate is not yet
143
+ // talking to OpenBao, so its wait must not count against the 60 seconds.
144
+ const gate = yield* BaoGate;
145
+ const { status, text } = yield* exchange.pipe(
146
+ overSocket(address.socket, method, path),
147
+ redactingToken,
148
+ Semaphore.withPermits(gate, 1),
149
+ );
150
+ const outcome = settle(intent, method, path, status, text);
151
+ return 'error' in outcome ? yield* Effect.fail(outcome.error) : outcome.body;
152
+ });
153
+
154
+ /**
155
+ * The `data` of a read, or undefined when OpenBao says the object is ABSENT (404).
156
+ *
157
+ * ★ `bao read -format=json` printed this same response body; the old helper took `data` when it was
158
+ * an object and the whole body otherwise, and so does this.
159
+ */
160
+ export const baoRead = (path: string) =>
161
+ Effect.map(baoCall('read', 'GET', path), (body) => {
162
+ if (body === undefined) return undefined;
163
+ const data = body['data'];
164
+ return typeof data === 'object' && data !== null
165
+ ? (data as Record<string, unknown>)
166
+ : (body as Record<string, unknown>);
167
+ });
168
+
169
+ /**
170
+ * A write whose SUCCESS MATTERS. Any non-2xx fails, 404 included.
171
+ *
172
+ * ★ `PUT` IS WHAT `bao write` SENT (openbao v2.6.2 api/logical.go Write); `sys/mounts` enable and
173
+ * tune are `POST` in the same client (api/sys_mounts.go:57, :201).
174
+ * ⚠️ ROLE BODIES STAY ALL-STRING ON PURPOSE. `bao write k=v` put every value in the JSON body as a
175
+ * string — MEASURED with `-output-curl-string`, see ssh-role-form.ts — and the form files still
176
+ * build exactly that body, so the server parses the same request it was always sent.
177
+ */
178
+ export const baoWrite = (
179
+ method: 'POST' | 'PUT',
180
+ path: string,
181
+ body: Readonly<Record<string, unknown>>,
182
+ ) => Effect.asVoid(baoCall('write', method, path, body));
183
+
184
+ /** A delete: idempotent — already gone (404) is success — and refused (403, 5xx) is a failure. */
185
+ export const baoDelete = (path: string) => Effect.asVoid(baoCall('delete', 'DELETE', path));
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The classification every read and every delete in this package rests on.
3
+ *
4
+ * ⛔ BOTH DIRECTIONS ARE PINNED, BECAUSE EACH HAS ALREADY BEEN WRONG ONCE. Every non-zero exit used
5
+ * to read as "absent" — a bad token planned 33 updates against a vault that was fine — and one
6
+ * command's absence wording was assumed for another's, so a policy that did not exist yet could
7
+ * not be created. These replace bao-shell.test.ts's stderr cases with the status codes those
8
+ * stderr lines were rendering: a missing role (404), a refused policy read under the agent lane
9
+ * (403 — `Code: 403 … permission denied`, measured 2026-09-14), a missing policy under the admin
10
+ * lane (404), and a server that is not listening (no response at all, bao-http.test.ts).
11
+ */
12
+ import assert from 'node:assert/strict';
13
+ import { describe, it } from 'node:test';
14
+ import { BaoError, settle } from './bao-status.ts';
15
+
16
+ const ROLE = 'pki/roles/does-not-exist-zzz';
17
+ const POLICY = 'sys/policies/acl/zz-new-policy';
18
+ const NONE = JSON.stringify({ errors: [] });
19
+ const DENIED = JSON.stringify({ errors: ['1 error occurred:\n\t* permission denied\n\n'] });
20
+ const SEALED = JSON.stringify({ errors: ['Vault is sealed'] });
21
+
22
+ const errorOf = (outcome: ReturnType<typeof settle>) => {
23
+ assert.ok('error' in outcome, 'expected a failure');
24
+ return outcome.error;
25
+ };
26
+
27
+ describe('settle', () => {
28
+ it('passes a JSON body through on success', () => {
29
+ assert.deepEqual(settle('read', 'GET', ROLE, 200, '{"data":{"ttl":3600}}'), {
30
+ body: { data: { ttl: 3600 } },
31
+ });
32
+ });
33
+
34
+ it('reads an OpenBao 404 as absent on a read', () => {
35
+ assert.deepEqual(settle('read', 'GET', ROLE, 404, NONE), { body: undefined });
36
+ });
37
+
38
+ // ⛔ THE BUG SHAPE b3c18a33e FIXED. A policy that does not exist yet is the same 404 as any other
39
+ // absent object — there is no second wording left to get wrong.
40
+ it('reads a policy that does not exist yet as absent', () => {
41
+ assert.deepEqual(settle('read', 'GET', POLICY, 404, NONE), { body: undefined });
42
+ });
43
+
44
+ it('treats a 404 as success on a delete, because a delete must be idempotent', () => {
45
+ assert.deepEqual(settle('delete', 'DELETE', POLICY, 404, NONE), { body: undefined });
46
+ });
47
+
48
+ it('accepts a 204 with no body on a write and on a delete', () => {
49
+ assert.deepEqual(settle('write', 'PUT', POLICY, 204, ''), { body: undefined });
50
+ assert.deepEqual(settle('delete', 'DELETE', POLICY, 204, ''), { body: undefined });
51
+ });
52
+
53
+ it('fails a 404 on a write, carrying OpenBao errors', () => {
54
+ const errors = ['no handler for route "nope/roles/x". route entry not found.'];
55
+ const error = errorOf(settle('write', 'PUT', 'nope/roles/x', 404, JSON.stringify({ errors })));
56
+ assert.deepEqual(error.errors, errors);
57
+ });
58
+
59
+ // ⛔ AND DENIAL IS NEVER ABSENCE, on a read or on a delete.
60
+ it('fails a refused read and a refused delete (403)', () => {
61
+ assert.equal(errorOf(settle('read', 'GET', POLICY, 403, DENIED)).status, 403);
62
+ assert.equal(errorOf(settle('delete', 'DELETE', POLICY, 403, DENIED)).status, 403);
63
+ });
64
+
65
+ it('fails a sealed server (503) on every intent', () => {
66
+ for (const intent of ['read', 'write', 'delete'] as const) {
67
+ assert.equal(errorOf(settle(intent, 'GET', ROLE, 503, SEALED)).status, 503);
68
+ }
69
+ });
70
+
71
+ it('fails a 400 and a 500', () => {
72
+ assert.equal(errorOf(settle('read', 'GET', ROLE, 400, NONE)).status, 400);
73
+ assert.equal(errorOf(settle('read', 'GET', ROLE, 500, NONE)).status, 500);
74
+ });
75
+
76
+ // ⚠️ A BAO_ADDR pointed at the wrong server must not plan CREATE for everything.
77
+ it('does not read a 404 that is not an OpenBao body as absent', () => {
78
+ const error = errorOf(settle('read', 'GET', ROLE, 404, '<html>Not Found</html>'));
79
+ assert.match(error.message, /not an OpenBao error body/);
80
+ assert.ok('error' in settle('delete', 'DELETE', ROLE, 404, '<html>Not Found</html>'));
81
+ });
82
+
83
+ it('fails a successful read that has no body', () => {
84
+ assert.equal(errorOf(settle('read', 'GET', ROLE, 200, '')).status, 200);
85
+ });
86
+
87
+ // ⛔ A success body can hold a credential, so a malformed one is described and never quoted.
88
+ it('fails an unparseable success body without quoting it', () => {
89
+ const error = errorOf(settle('read', 'GET', 'pbs-tb4/creds/read', 200, 'secret=abc123 {'));
90
+ assert.ok(!error.message.includes('abc123'));
91
+ });
92
+
93
+ // ⛔ A JSON gateway's error shape is not OpenBao's, even on a 404 (grok, 2026-09-14).
94
+ it('does not read a 404 whose errors are objects as absent, on a read or a delete', () => {
95
+ const gateway = JSON.stringify({ errors: [{ code: 1000, message: 'Not Found' }] });
96
+ const error = errorOf(settle('read', 'GET', ROLE, 404, gateway));
97
+ assert.match(error.message, /not an OpenBao error body/);
98
+ assert.ok('error' in settle('delete', 'DELETE', ROLE, 404, gateway));
99
+ });
100
+
101
+ // ⛔ An error page that echoes the request back must not carry the token into the message.
102
+ it('describes a foreign error body without quoting it', () => {
103
+ const echo = '<pre>X-Vault-Token: hvs.sentinel-not-a-token</pre>';
104
+ const error = errorOf(settle('read', 'GET', ROLE, 502, echo));
105
+ assert.ok(!error.message.includes('hvs.sentinel'));
106
+ });
107
+
108
+ it('names the method and path, and carries the errors array', () => {
109
+ const error = errorOf(settle('read', 'GET', POLICY, 403, DENIED));
110
+ assert.ok(error instanceof BaoError);
111
+ assert.match(error.message, /^OpenBao GET \/v1\/sys\/policies\/acl\/zz-new-policy -> 403: /);
112
+ assert.deepEqual(error.errors, ['1 error occurred:\n\t* permission denied\n\n']);
113
+ });
114
+ });