@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,97 @@
1
+ /**
2
+ * `Forgejo.TeamMember` — one user in one organization team.
3
+ *
4
+ * ★ WHY IT EXISTS: forgejo-provision, the service account the Forgejo engine mints as, belongs to the
5
+ * HomeFlare Owners team — Tim, 2026-09-14. Membership is what its tokens can reach; the role's
6
+ * scopes only cap it (house/openbao/src/forgejo-bootstrap.ts).
7
+ *
8
+ * ★ READ OFF THE LIVE 16.0.3 SWAGGER (127.0.0.1:3000/swagger.v1.json, 2026-09-14): GET, PUT and
9
+ * DELETE `/teams/{id}/members/{username}` answer 200, 204 and 204, and 404 when absent. The team
10
+ * is addressed by numeric id, so it is found by name first — the same seam as org-team.ts.
11
+ *
12
+ * ⚠️ EXISTENCE IS THE WHOLE OBJECT. Nothing about a membership can be updated, so `matches` is
13
+ * always true; a different team or user is a different resource id in alchemy.run.ts.
14
+ *
15
+ * ⚠️ THE TEAM ITSELF IS ONLY LOCATED HERE, NEVER MANAGED. A missing team fails the plan by name
16
+ * instead of folding into "absent", because a PUT can add a member but cannot create a team.
17
+ *
18
+ * ⛔ TOKEN NEEDS `write:organization` TO ADD OR REMOVE, and an identity allowed to manage the team.
19
+ */
20
+ import { Resource } from 'alchemy';
21
+ import * as Provider from 'alchemy/Provider';
22
+ import * as Effect from 'effect/Effect';
23
+ import { ForgejoError, forgejo } from './client.ts';
24
+ import { type ForgejoRequirements, forgejoHandlers } from './resource.ts';
25
+
26
+ export interface TeamMemberProps {
27
+ org: string;
28
+ /** Team name as Forgejo shows it, e.g. `Owners`. */
29
+ team: string;
30
+ username: string;
31
+ }
32
+
33
+ export interface TeamMemberAttributes {
34
+ org: string;
35
+ team: string;
36
+ teamId: number;
37
+ username: string;
38
+ }
39
+
40
+ export interface ForgejoTeamMember extends Resource<
41
+ 'Forgejo.TeamMember',
42
+ TeamMemberProps,
43
+ TeamMemberAttributes,
44
+ never,
45
+ ForgejoRequirements
46
+ > {}
47
+
48
+ export const ForgejoTeamMember = Resource<ForgejoTeamMember>('Forgejo.TeamMember');
49
+
50
+ /** Where `locate` leaves the team id on the live row, for `attributes` and `wirePath`. */
51
+ const TEAM_ID = '__team_id';
52
+
53
+ const teamIdOf = (props: TeamMemberProps) =>
54
+ Effect.gen(function* () {
55
+ const path = `orgs/${props.org}/teams?limit=200`;
56
+ const rows = yield* forgejo<Record<string, unknown>[]>('GET', path);
57
+ const id = (Array.isArray(rows) ? rows : []).find((row) => row['name'] === props.team)?.['id'];
58
+ if (typeof id !== 'number') {
59
+ return yield* Effect.fail(
60
+ new ForgejoError(0, 'GET', path, `no team named ${props.team} in org ${props.org}`),
61
+ );
62
+ }
63
+ return id;
64
+ });
65
+
66
+ const memberPath = (id: number, props: TeamMemberProps) =>
67
+ `teams/${String(id)}/members/${props.username}`;
68
+
69
+ const handlers = forgejoHandlers<TeamMemberProps, TeamMemberAttributes>({
70
+ attributes: (live, props) => {
71
+ const id = live[TEAM_ID];
72
+ const login = live['login'];
73
+ if (typeof id !== 'number' || typeof login !== 'string') return undefined;
74
+ if (login.toLowerCase() !== props.username.toLowerCase()) return undefined;
75
+ return { org: props.org, team: props.team, teamId: id, username: props.username };
76
+ },
77
+ collection: (props) => `orgs/${props.org}/teams`,
78
+ createForm: () => ({}),
79
+ locate: (props) =>
80
+ Effect.gen(function* () {
81
+ const id = yield* teamIdOf(props);
82
+ const user = yield* forgejo<Record<string, unknown>>('GET', memberPath(id, props));
83
+ return user === undefined ? undefined : { ...user, [TEAM_ID]: id };
84
+ }),
85
+ matches: () => true,
86
+ path: (props) => `orgs/${props.org}/teams/${props.team}/members/${props.username}`,
87
+ upsert: (props, before) =>
88
+ before !== undefined
89
+ ? Effect.void
90
+ : Effect.flatMap(teamIdOf(props), (id) =>
91
+ Effect.asVoid(forgejo('PUT', memberPath(id, props))),
92
+ ),
93
+ wirePath: (props, live) => memberPath(Number(live[TEAM_ID]), props),
94
+ });
95
+
96
+ export const ForgejoTeamMemberProvider = () =>
97
+ Provider.effect(ForgejoTeamMember, Effect.succeed(ForgejoTeamMember.Provider.of(handlers)));
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Wire coercions for Forgejo resources — each case is a trap values.ts documents.
3
+ *
4
+ * ⚠️ `@homeflare/forgejo` had no tests; `bun test` exited 1 on "No tests found" and blocked verify.
5
+ */
6
+ import assert from 'node:assert/strict';
7
+ import { describe, it } from 'node:test';
8
+ import {
9
+ hookConfigPublic,
10
+ orgSecretEnvKey,
11
+ recordEqual,
12
+ stringArray,
13
+ stringRecord,
14
+ } from './values.ts';
15
+
16
+ describe('stringArray', () => {
17
+ it('sorts for stable comparison', () => {
18
+ assert.deepEqual(stringArray(['push', 'create']), ['create', 'push']);
19
+ });
20
+ });
21
+
22
+ describe('recordEqual', () => {
23
+ it('ignores key order', () => {
24
+ assert.equal(recordEqual({ a: '1', b: '2' }, { b: '2', a: '1' }), true);
25
+ assert.equal(recordEqual({ a: '1' }, { a: '2' }), false);
26
+ });
27
+ });
28
+
29
+ describe('hookConfigPublic', () => {
30
+ it('drops secret from config', () => {
31
+ assert.deepEqual(hookConfigPublic({ secret: 'x', url: 'https://example.test' }), {
32
+ url: 'https://example.test',
33
+ });
34
+ });
35
+ });
36
+
37
+ describe('orgSecretEnvKey', () => {
38
+ it('normalizes org and name into an env key', () => {
39
+ assert.equal(orgSecretEnvKey('homeflare', 'ci-token'), 'FORGEJO_ORG_SECRET_HOMEFLARE_CI_TOKEN');
40
+ });
41
+ });
42
+
43
+ describe('stringRecord', () => {
44
+ it('returns empty for non-objects', () => {
45
+ assert.deepEqual(stringRecord(null), {});
46
+ assert.deepEqual(stringRecord(['x']), {});
47
+ });
48
+ });
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Wire coercions shared across Forgejo resources.
3
+ *
4
+ * ★ GITEA/ FORGEJO JSON USES snake_case ON THE WIRE and camelCase in props — each resource
5
+ * translates at the form boundary; these helpers only normalise values for comparison.
6
+ */
7
+
8
+ export const text = (value: unknown, fallback = '') =>
9
+ typeof value === 'string' ? value : fallback;
10
+
11
+ /** Forgejo returns label colours without `#`; declarations may include it. */
12
+ export const color = (value: unknown) => text(value).replace(/^#/, '').toLowerCase();
13
+
14
+ export const bool = (value: unknown, fallback = false) =>
15
+ value === undefined || value === null ? fallback : value === true || value === 1 || value === '1';
16
+
17
+ export const int = (value: unknown, fallback = 0) => {
18
+ if (typeof value === 'number' && Number.isFinite(value)) return Math.trunc(value);
19
+ if (typeof value === 'string' && value.trim() !== '') {
20
+ const parsed = Number.parseInt(value, 10);
21
+ if (Number.isFinite(parsed)) return parsed;
22
+ }
23
+ return fallback;
24
+ };
25
+
26
+ /** Wire arrays compared in sorted order — Gitea reshuffles nothing but callers should not depend on order. */
27
+ export const stringArray = (value: unknown) =>
28
+ Array.isArray(value)
29
+ ? value.filter((entry): entry is string => typeof entry === 'string').toSorted()
30
+ : [];
31
+
32
+ export const stringRecord = (value: unknown): Record<string, string> => {
33
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return {};
34
+ const out: Record<string, string> = {};
35
+ for (const [key, entry] of Object.entries(value)) {
36
+ if (typeof entry === 'string') out[key] = entry;
37
+ }
38
+ return out;
39
+ };
40
+
41
+ export const recordEqual = (left: Record<string, string>, right: Record<string, string>) => {
42
+ const leftKeys = Object.keys(left).toSorted();
43
+ const rightKeys = Object.keys(right).toSorted();
44
+ if (leftKeys.length !== rightKeys.length) return false;
45
+ return leftKeys.every((key, index) => rightKeys[index] === key && left[key] === right[key]);
46
+ };
47
+
48
+ /** Hook `config` without `secret` — the value must never land in Alchemy attributes. */
49
+ export const hookConfigPublic = (value: unknown) => {
50
+ const config = stringRecord(value);
51
+ const { secret: _secret, ...rest } = config;
52
+ return rest;
53
+ };
54
+
55
+ /** Env key for a write-only org actions secret — value read at PUT time only, never stored. */
56
+ export const orgSecretEnvKey = (org: string, name: string) =>
57
+ `FORGEJO_ORG_SECRET_${org}_${name}`.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase();
58
+
59
+ /** Env key for a write-only repo webhook HMAC secret — omitted from attributes and props. */
60
+ export const hookSecretEnvKey = (owner: string, repo: string, name: string) =>
61
+ `FORGEJO_HOOK_SECRET_${owner}_${repo}_${name}`.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase();
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @homeflare/alchemy — custom Alchemy providers for systems the vendor has none for.
3
+ *
4
+ * ⛔ IMPORT A SUBPATH, NOT THIS FILE. Each system carries its own peers and its own
5
+ * client; a root barrel would pull Proxmox into a stack that only wanted Forgejo.
6
+ *
7
+ * import { R2BucketLock } from '@homeflare/alchemy/cloudflare';
8
+ * import { BranchProtection } from '@homeflare/alchemy/forgejo';
9
+ * import { BaoMount } from '@homeflare/alchemy/openbao';
10
+ * import { TalosCluster } from '@homeflare/alchemy/talos';
11
+ * import { ProxmoxAcl } from '@homeflare/alchemy/proxmox';
12
+ *
13
+ * ★ WHY THESE EXIST AT ALL. When Alchemy has no resource for something, the alternative
14
+ * is a runbook step a human runs once — and a plan can never show a missing runbook
15
+ * step. A custom provider makes the drift visible in `plan` like everything else.
16
+ */
17
+
18
+ export {};
@@ -0,0 +1,112 @@
1
+ import { sha256 } from './digest.ts';
2
+ import { parseDuration, ttlSeconds } from './mount-form.ts';
3
+
4
+ export interface BaoAuthRoleProps {
5
+ /** AppRole name, as `auth/approle/role/{name}` takes it. */
6
+ name: string;
7
+ /** Policies bound to tokens minted through this role. Names only — never a secret. */
8
+ tokenPolicies: readonly string[];
9
+ /** Token TTL, e.g. `15m`. */
10
+ tokenTtl: string;
11
+ /** Token max TTL, e.g. `1h`. */
12
+ tokenMaxTtl: string;
13
+ /** Secret ID TTL — `0` means non-expiring, as reconcile.sh and apply-consumers.py set. */
14
+ secretIdTtl: string;
15
+ /** Whether login requires a secret_id. Defaults to true when omitted on write. */
16
+ bindSecretId?: boolean;
17
+ /** `0` means unlimited uses — the shape reconcile.sh writes for headless roles. */
18
+ secretIdNumUses?: number;
19
+ }
20
+
21
+ export interface BaoAuthRoleAttributes {
22
+ name: string;
23
+ tokenPolicies: readonly string[];
24
+ tokenTtl: string;
25
+ tokenMaxTtl: string;
26
+ secretIdTtl: string;
27
+ bindSecretId: boolean;
28
+ secretIdNumUses: number;
29
+ /** SHA-256 of the managed fields — safe to persist; see policy.ts. */
30
+ digest: string;
31
+ }
32
+
33
+ const policiesOf = (live: Record<string, unknown>): readonly string[] => {
34
+ const raw = live['token_policies'];
35
+ if (!Array.isArray(raw)) return [];
36
+ return raw
37
+ .filter((entry): entry is string => typeof entry === 'string')
38
+ .slice()
39
+ .sort();
40
+ };
41
+
42
+ const bool = (value: unknown, fallback: boolean) => (typeof value === 'boolean' ? value : fallback);
43
+
44
+ const int = (value: unknown, fallback: number) =>
45
+ typeof value === 'number' && Number.isFinite(value) ? value : fallback;
46
+
47
+ const ttlText = (seconds: number | undefined, fallback = '') => {
48
+ if (seconds === undefined) return fallback;
49
+ if (seconds % 86400 === 0) return `${String(seconds / 86400)}d`;
50
+ if (seconds % 3600 === 0) return `${String(seconds / 3600)}h`;
51
+ if (seconds % 60 === 0) return `${String(seconds / 60)}m`;
52
+ return `${String(seconds)}s`;
53
+ };
54
+
55
+ export const attributesOf = (
56
+ props: BaoAuthRoleProps,
57
+ live: Record<string, unknown>,
58
+ ): BaoAuthRoleAttributes => {
59
+ const attrs = {
60
+ name: props.name,
61
+ tokenPolicies: policiesOf(live),
62
+ tokenTtl: ttlText(ttlSeconds(live['token_ttl'])),
63
+ tokenMaxTtl: ttlText(ttlSeconds(live['token_max_ttl'])),
64
+ secretIdTtl: ttlText(ttlSeconds(live['secret_id_ttl']), '0'),
65
+ bindSecretId: bool(live['bind_secret_id'], true),
66
+ secretIdNumUses: int(live['secret_id_num_uses'], 0),
67
+ };
68
+ return { ...attrs, digest: sha256(JSON.stringify(attrs)) };
69
+ };
70
+
71
+ export const readPath = (name: string) => `auth/approle/role/${name}`;
72
+
73
+ /**
74
+ * The body for `PUT auth/approle/role/<name>`.
75
+ *
76
+ * ★ ALL STRINGS, EXACTLY THE `k=v` PAIRS `bao write` USED TO SEND — see `baoWrite` in bao-http.ts.
77
+ */
78
+ export const writeBody = (props: BaoAuthRoleProps): Record<string, string> => {
79
+ const body: Record<string, string> = {
80
+ token_policies: props.tokenPolicies.join(','),
81
+ token_ttl: props.tokenTtl,
82
+ token_max_ttl: props.tokenMaxTtl,
83
+ secret_id_ttl: props.secretIdTtl,
84
+ };
85
+ if (props.bindSecretId !== undefined) body['bind_secret_id'] = String(props.bindSecretId);
86
+ if (props.secretIdNumUses !== undefined) {
87
+ body['secret_id_num_uses'] = String(props.secretIdNumUses);
88
+ }
89
+ return body;
90
+ };
91
+
92
+ const samePolicies = (want: readonly string[], have: readonly string[]) =>
93
+ want.length === have.length && want.every((policy, index) => policy === have[index]);
94
+
95
+ const sameTtl = (want: string, have: string) => {
96
+ const wantSeconds = parseDuration(want);
97
+ const haveSeconds = ttlSeconds(have);
98
+ return wantSeconds !== undefined && wantSeconds === haveSeconds;
99
+ };
100
+
101
+ /** True when live already matches the declaration on every managed field. */
102
+ export const matches = (attributes: BaoAuthRoleAttributes, props: BaoAuthRoleProps) => {
103
+ const wantPolicies = props.tokenPolicies.slice().sort();
104
+ return (
105
+ samePolicies(wantPolicies, attributes.tokenPolicies) &&
106
+ sameTtl(props.tokenTtl, attributes.tokenTtl) &&
107
+ sameTtl(props.tokenMaxTtl, attributes.tokenMaxTtl) &&
108
+ sameTtl(props.secretIdTtl, attributes.secretIdTtl) &&
109
+ (props.bindSecretId === undefined || attributes.bindSecretId === props.bindSecretId) &&
110
+ (props.secretIdNumUses === undefined || attributes.secretIdNumUses === props.secretIdNumUses)
111
+ );
112
+ };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * An OpenBao AppRole auth role — token_policies, TTLs and bind flags. METADATA ONLY.
3
+ *
4
+ * ⛔ NO SECRET IN PROPS OR ATTRIBUTES — see policy.ts for why. `role_id` and `secret_id`
5
+ * are credential issuance, not configuration, and are OUT OF SCOPE. This declares the
6
+ * role shape an approle login inherits — policy names and TTL strings only.
7
+ *
8
+ * ★ `defaultRemovalPolicy: 'retain'` — deleting a role revokes every token bound to it.
9
+ * Opt in with `.pipe(RemovalPolicy.destroy())`; see resource.ts in house/proxmox.
10
+ */
11
+ import { Resource } from 'alchemy';
12
+ import { isResolved } from 'alchemy/Diff';
13
+ import * as Provider from 'alchemy/Provider';
14
+ import * as Effect from 'effect/Effect';
15
+ import type * as HttpClient from 'effect/unstable/http/HttpClient';
16
+ import {
17
+ type BaoAuthRoleAttributes,
18
+ type BaoAuthRoleProps,
19
+ attributesOf,
20
+ matches,
21
+ readPath,
22
+ writeBody,
23
+ } from './auth-role-form.ts';
24
+ import { baoDelete, baoRead, baoWrite } from './bao-http.ts';
25
+
26
+ export type { BaoAuthRoleAttributes, BaoAuthRoleProps };
27
+
28
+ export interface BaoAuthRole extends Resource<
29
+ 'Bao.AuthRole',
30
+ BaoAuthRoleProps,
31
+ BaoAuthRoleAttributes,
32
+ never,
33
+ HttpClient.HttpClient
34
+ > {}
35
+
36
+ export const BaoAuthRole = Resource<BaoAuthRole>('Bao.AuthRole', {
37
+ defaultRemovalPolicy: 'retain',
38
+ });
39
+
40
+ const readRole = (props: BaoAuthRoleProps) =>
41
+ Effect.gen(function* () {
42
+ const live = yield* baoRead(readPath(props.name));
43
+ if (live === undefined) return undefined;
44
+ return attributesOf(props, live);
45
+ });
46
+
47
+ export const BaoAuthRoleProvider = () =>
48
+ Provider.effect(
49
+ BaoAuthRole,
50
+ Effect.succeed(
51
+ BaoAuthRole.Provider.of({
52
+ /**
53
+ * ⛔ `bao list auth/approle/role` IS NOT A LIST OF THINGS THIS OWNS. Every role
54
+ * in the namespace answers, including break-glass and bootstrap ones. Returning
55
+ * them would invite Alchemy to adopt — and then delete — roles it never created.
56
+ */
57
+ list: () => Effect.succeed([]),
58
+
59
+ read: Effect.fn(function* ({ olds }) {
60
+ return yield* readRole(olds);
61
+ }),
62
+
63
+ /**
64
+ * ⛔ IT COMPARES THE LIVE ROLE, NOT THE STORED DIGEST. A role edited in the OpenBao
65
+ * UI is exactly the drift the check-* gates exist to catch.
66
+ */
67
+ diff: Effect.fn(function* ({ news, output }) {
68
+ if (output === undefined || !isResolved(news)) return undefined;
69
+ const live = yield* readRole(news);
70
+ if (live === undefined) return { action: 'update' } as const;
71
+ if (matches(live, news)) return { action: 'noop' } as const;
72
+ return { action: 'update' } as const;
73
+ }),
74
+
75
+ reconcile: Effect.fn(function* ({ news }) {
76
+ const live = yield* readRole(news);
77
+ if (live === undefined || !matches(live, news)) {
78
+ yield* baoWrite('PUT', readPath(news.name), writeBody(news));
79
+ }
80
+ const after = yield* readRole(news);
81
+ if (after === undefined) {
82
+ return yield* Effect.die(
83
+ new Error(
84
+ `Bao.AuthRole ${news.name}: write returned no error but the role is still absent.`,
85
+ ),
86
+ );
87
+ }
88
+ return after;
89
+ }),
90
+
91
+ /**
92
+ * ⛔ DELETING A ROLE REVOKES EVERY TOKEN BOUND TO IT. Idempotent as Alchemy requires
93
+ * — already gone is success — but retain is the default for a reason.
94
+ */
95
+ delete: Effect.fn(function* ({ output }) {
96
+ yield* baoDelete(readPath(output.name));
97
+ return undefined;
98
+ }),
99
+ }),
100
+ ),
101
+ );
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The environment contract, pinned to what openbao v2.6.2's CLI client does — see the citations in
3
+ * bao-address.ts. Pure: nothing here opens a connection.
4
+ */
5
+ import assert from 'node:assert/strict';
6
+ import { describe, it } from 'node:test';
7
+ import { DEFAULT_ADDRESS, headersFor, resolveAddress } from './bao-address.ts';
8
+
9
+ const TOKEN = 'hvs.FAKE-sentinel-never-a-real-token';
10
+
11
+ describe('resolveAddress', () => {
12
+ it("falls back to the CLI's default address when nothing is set", () => {
13
+ assert.deepEqual(resolveAddress({}), {
14
+ base: DEFAULT_ADDRESS,
15
+ namespace: undefined,
16
+ socket: undefined,
17
+ });
18
+ });
19
+
20
+ it('strips a trailing slash and keeps a path prefix', () => {
21
+ assert.equal(
22
+ resolveAddress({ BAO_ADDR: 'https://bao.example:8200/' }).base,
23
+ 'https://bao.example:8200',
24
+ );
25
+ assert.equal(
26
+ resolveAddress({ BAO_ADDR: 'https://edge.example/bao/' }).base,
27
+ 'https://edge.example/bao',
28
+ );
29
+ });
30
+
31
+ it('lets BAO_AGENT_ADDR win over BAO_ADDR', () => {
32
+ const env = {
33
+ BAO_ADDR: 'https://server.example:8200',
34
+ BAO_AGENT_ADDR: 'http://127.0.0.1:8100',
35
+ };
36
+ assert.equal(resolveAddress(env).base, 'http://127.0.0.1:8100');
37
+ });
38
+
39
+ it('falls back to VAULT_ADDR, but a present-and-empty BAO_ADDR still wins', () => {
40
+ assert.equal(
41
+ resolveAddress({ VAULT_ADDR: 'https://vault.example' }).base,
42
+ 'https://vault.example',
43
+ );
44
+ assert.equal(
45
+ resolveAddress({ BAO_ADDR: '', VAULT_ADDR: 'https://vault.example' }).base,
46
+ DEFAULT_ADDRESS,
47
+ );
48
+ });
49
+
50
+ it('turns unix:///path into plain HTTP to localhost over that socket', () => {
51
+ assert.deepEqual(resolveAddress({ BAO_ADDR: 'unix:///run/openbao/agent.sock' }), {
52
+ base: 'http://localhost',
53
+ namespace: undefined,
54
+ socket: '/run/openbao/agent.sock',
55
+ });
56
+ });
57
+
58
+ it('carries a namespace only when it is non-empty', () => {
59
+ assert.equal(resolveAddress({ BAO_NAMESPACE: 'homeflare' }).namespace, 'homeflare');
60
+ assert.equal(resolveAddress({ BAO_NAMESPACE: '' }).namespace, undefined);
61
+ });
62
+ });
63
+
64
+ describe('headersFor', () => {
65
+ it('sends the token and the namespace when both are set', () => {
66
+ const env = { BAO_NAMESPACE: 'homeflare', BAO_TOKEN: TOKEN };
67
+ assert.deepEqual(headersFor(resolveAddress(env), env), {
68
+ 'X-Vault-Namespace': 'homeflare',
69
+ 'X-Vault-Request': 'true',
70
+ 'X-Vault-Token': TOKEN,
71
+ });
72
+ });
73
+
74
+ // ★ AGENT MODE. The agent substitutes its auto-auth token only when the request carries none.
75
+ it('sends NO token header when BAO_TOKEN is unset or empty', () => {
76
+ for (const env of [{}, { BAO_TOKEN: '' }]) {
77
+ assert.deepEqual(headersFor(resolveAddress(env), env), { 'X-Vault-Request': 'true' });
78
+ }
79
+ });
80
+
81
+ it('falls back to VAULT_TOKEN, as the CLI does', () => {
82
+ const env = { VAULT_TOKEN: TOKEN };
83
+ assert.equal(headersFor(resolveAddress(env), env)['X-Vault-Token'], TOKEN);
84
+ });
85
+
86
+ it('keeps the token out of the resolved address', () => {
87
+ assert.ok(!JSON.stringify(resolveAddress({ BAO_TOKEN: TOKEN })).includes(TOKEN));
88
+ });
89
+ });
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Where an OpenBao call goes and which headers it carries — resolved from the environment the way
3
+ * the `bao` CLI resolves it, so the provider and `bao kv get` can never disagree about which server
4
+ * or namespace this machine is talking to.
5
+ *
6
+ * ★ READ FROM openbao v2.6.2, NOT RECALLED:
7
+ * · api/client.go:34-66 — BAO_ADDR, BAO_AGENT_ADDR, BAO_NAMESPACE, BAO_TOKEN, and the header
8
+ * names `X-Vault-Namespace` and `X-Vault-Token`.
9
+ * · api/client.go:370 — the default address, `https://127.0.0.1:8200`.
10
+ * · api/client.go:503-507, 617-618, 761-762 — BAO_AGENT_ADDR, when set, WINS over BAO_ADDR.
11
+ * · api/env.go:22-34 — every BAO_* falls back to its VAULT_* twin, but a BAO_* that is present
12
+ * and EMPTY still wins over the VAULT_* one.
13
+ * · api/client.go:776-785 — the token and namespace are applied only when non-empty, and every
14
+ * request carries `X-Vault-Request: true`.
15
+ * · api/client.go:1349-1354 — the request path is joined onto the address's own path.
16
+ *
17
+ * ⛔ WHAT THE CLI DOES AND THIS DELIBERATELY DOES NOT: with BAO_TOKEN unset, `bao` falls back to its
18
+ * token helper (a token file in $HOME). This never reads that file. No BAO_TOKEN means AGENT
19
+ * MODE — see `headersFor`.
20
+ * ⚠️ NOT CARRIED OVER: BAO_CACERT / BAO_SKIP_VERIFY / client certificates, SRV lookup, and the CLI's
21
+ * retries on 5xx. An https address needs a certificate the runtime already trusts; a TLS failure
22
+ * fails the call loudly rather than being skipped.
23
+ */
24
+
25
+ export type BaoEnvironment = Readonly<Record<string, string | undefined>>;
26
+
27
+ export type BaoAddress = {
28
+ /** Scheme, host and any path prefix, no trailing slash. `http://localhost` for a socket. */
29
+ readonly base: string;
30
+ /** The socket path when the address is `unix:///path`, otherwise undefined. */
31
+ readonly socket: string | undefined;
32
+ /** The namespace to send, when there is one. */
33
+ readonly namespace: string | undefined;
34
+ };
35
+
36
+ export const DEFAULT_ADDRESS = 'https://127.0.0.1:8200';
37
+
38
+ const UNIX = 'unix://';
39
+
40
+ /** api/env.go ReadBaoVariable: present (even empty) BAO_* wins, else the VAULT_* twin. */
41
+ export const readBaoVariable = (env: BaoEnvironment, name: `BAO_${string}`): string =>
42
+ env[name] ?? env[`VAULT_${name.slice('BAO_'.length)}`] ?? '';
43
+
44
+ /**
45
+ * The address and namespace for this environment. Carries no token, so it is safe to log.
46
+ *
47
+ * ★ UNIX SOCKETS, BECAUSE AN AGENT LISTENER CAN BE ONE. OpenBao Agent's `listener "unix"` serves
48
+ * the same API on a socket (website/content/docs/agent-and-proxy/agent/index.mdx:382-384), and the
49
+ * agent advertises such a listener as `unix://<path>` (command/agent.go:565-566). The CLI handles
50
+ * that address by dialling the socket and speaking plain HTTP to host `localhost`
51
+ * (api/client.go:654-667); `base` and `socket` are exactly those two halves.
52
+ */
53
+ export const resolveAddress = (env: BaoEnvironment): BaoAddress => {
54
+ const agent = readBaoVariable(env, 'BAO_AGENT_ADDR');
55
+ const direct = readBaoVariable(env, 'BAO_ADDR');
56
+ const raw = agent !== '' ? agent : direct !== '' ? direct : DEFAULT_ADDRESS;
57
+ const namespace = readBaoVariable(env, 'BAO_NAMESPACE');
58
+ const socket = raw.startsWith(UNIX) ? raw.slice(UNIX.length) : undefined;
59
+ return {
60
+ base: socket === undefined ? raw.replace(/\/+$/, '') : 'http://localhost',
61
+ namespace: namespace === '' ? undefined : namespace,
62
+ socket,
63
+ };
64
+ };
65
+
66
+ /**
67
+ * The headers one call sends.
68
+ *
69
+ * ★ AGENT MODE IS "NO TOKEN HEADER AT ALL", AND THAT IS THE AGENT'S OWN CONTRACT. Read in openbao
70
+ * v2.6.2 command/agentproxyshared/cache/handler.go:29-38: the proxy handler takes the request's
71
+ * `X-Vault-Token`, and only when it is EMPTY and an auto-auth sink exists does it substitute the
72
+ * agent's own token. command/agent.go:517-541 creates that sink exactly when
73
+ * `api_proxy { use_auto_auth_token = true }` is set; command/agent/config/config.go:288-292 and
74
+ * 635-637 map the older `cache { use_auto_auth_token }` onto the same setting; and
75
+ * command/proxy.go:495-518 wires OpenBao Proxy identically. The docs say the same in words
76
+ * (website/content/docs/agent-and-proxy/agent/apiproxy.mdx:57-62): a request without a token is
77
+ * forwarded with the auto-auth token, a request that bears one keeps its own, and `"force"`
78
+ * overwrites it.
79
+ * ⛔ SO AN EMPTY `X-Vault-Token: ` WOULD BE WRONG IN BOTH DIRECTIONS: harmless to the agent,
80
+ * which reads empty as absent, but an explicit unauthenticated request to a server. The header
81
+ * is omitted, never blanked.
82
+ * ★ THE NAMESPACE RIDES THROUGH THE AGENT UNTOUCHED. The proxy clones its client and replaces that
83
+ * client's headers with the incoming request's (cache/api_proxy.go:50-54 and :78), so the
84
+ * `X-Vault-Namespace` sent here is the one OpenBao sees.
85
+ * ⚠️ `X-Vault-Request: true` IS NOT DECORATION. An agent listener with `require_request_header`
86
+ * refuses any request without it (command/agent.go:544-548); the CLI always sends it.
87
+ *
88
+ * ⛔ THE RETURN VALUE HOLDS THE TOKEN. Hand it to the request and nowhere else: never to an error,
89
+ * a log line, an attribute, or a span — bao-http.ts adds the header to the redacted names.
90
+ */
91
+ export const headersFor = (address: BaoAddress, env: BaoEnvironment): Record<string, string> => {
92
+ const headers: Record<string, string> = { 'X-Vault-Request': 'true' };
93
+ if (address.namespace !== undefined) headers['X-Vault-Namespace'] = address.namespace;
94
+ const token = readBaoVariable(env, 'BAO_TOKEN');
95
+ if (token !== '') headers['X-Vault-Token'] = token;
96
+ return headers;
97
+ };