@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,33 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/openbao/auth-role.ts", "../src/openbao/digest.ts", "../src/openbao/mount-form.ts", "../src/openbao/auth-role-form.ts", "../src/openbao/bao-http.ts", "../src/openbao/bao-address.ts", "../src/openbao/bao-status.ts", "../src/openbao/cloudflare-role.ts", "../src/openbao/cloudflare-group-scope.ts", "../src/openbao/cloudflare-permission-groups.ts", "../src/openbao/cloudflare-policy.ts", "../src/openbao/cloudflare-role-wire.ts", "../src/openbao/cloudflare-role-form.ts", "../src/openbao/mount.ts", "../src/openbao/mount-wire.ts", "../src/openbao/pki-role.ts", "../src/openbao/pki-role-form.ts", "../src/openbao/policy.ts", "../src/openbao/policy-assembly.ts", "../src/openbao/policy-wire.ts", "../src/openbao/proxmox-role.ts", "../src/openbao/proxmox-role-form.ts", "../src/openbao/ssh-role.ts", "../src/openbao/ssh-role-form.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * An OpenBao AppRole auth role — token_policies, TTLs and bind flags. METADATA ONLY.\n *\n * ⛔ NO SECRET IN PROPS OR ATTRIBUTES — see policy.ts for why. `role_id` and `secret_id`\n * are credential issuance, not configuration, and are OUT OF SCOPE. This declares the\n * role shape an approle login inherits — policy names and TTL strings only.\n *\n * ★ `defaultRemovalPolicy: 'retain'` — deleting a role revokes every token bound to it.\n * Opt in with `.pipe(RemovalPolicy.destroy())`; see resource.ts in house/proxmox.\n */\nimport { Resource } from 'alchemy';\nimport { isResolved } from 'alchemy/Diff';\nimport * as Provider from 'alchemy/Provider';\nimport * as Effect from 'effect/Effect';\nimport type * as HttpClient from 'effect/unstable/http/HttpClient';\nimport {\n type BaoAuthRoleAttributes,\n type BaoAuthRoleProps,\n attributesOf,\n matches,\n readPath,\n writeBody,\n} from './auth-role-form.ts';\nimport { baoDelete, baoRead, baoWrite } from './bao-http.ts';\n\nexport type { BaoAuthRoleAttributes, BaoAuthRoleProps };\n\nexport interface BaoAuthRole extends Resource<\n 'Bao.AuthRole',\n BaoAuthRoleProps,\n BaoAuthRoleAttributes,\n never,\n HttpClient.HttpClient\n> {}\n\nexport const BaoAuthRole = Resource<BaoAuthRole>('Bao.AuthRole', {\n defaultRemovalPolicy: 'retain',\n});\n\nconst readRole = (props: BaoAuthRoleProps) =>\n Effect.gen(function* () {\n const live = yield* baoRead(readPath(props.name));\n if (live === undefined) return undefined;\n return attributesOf(props, live);\n });\n\nexport const BaoAuthRoleProvider = () =>\n Provider.effect(\n BaoAuthRole,\n Effect.succeed(\n BaoAuthRole.Provider.of({\n /**\n * ⛔ `bao list auth/approle/role` IS NOT A LIST OF THINGS THIS OWNS. Every role\n * in the namespace answers, including break-glass and bootstrap ones. Returning\n * them would invite Alchemy to adopt — and then delete — roles it never created.\n */\n list: () => Effect.succeed([]),\n\n read: Effect.fn(function* ({ olds }) {\n return yield* readRole(olds);\n }),\n\n /**\n * ⛔ IT COMPARES THE LIVE ROLE, NOT THE STORED DIGEST. A role edited in the OpenBao\n * UI is exactly the drift the check-* gates exist to catch.\n */\n diff: Effect.fn(function* ({ news, output }) {\n if (output === undefined || !isResolved(news)) return undefined;\n const live = yield* readRole(news);\n if (live === undefined) return { action: 'update' } as const;\n if (matches(live, news)) return { action: 'noop' } as const;\n return { action: 'update' } as const;\n }),\n\n reconcile: Effect.fn(function* ({ news }) {\n const live = yield* readRole(news);\n if (live === undefined || !matches(live, news)) {\n yield* baoWrite('PUT', readPath(news.name), writeBody(news));\n }\n const after = yield* readRole(news);\n if (after === undefined) {\n return yield* Effect.die(\n new Error(\n `Bao.AuthRole ${news.name}: write returned no error but the role is still absent.`,\n ),\n );\n }\n return after;\n }),\n\n /**\n * ⛔ DELETING A ROLE REVOKES EVERY TOKEN BOUND TO IT. Idempotent as Alchemy requires\n * — already gone is success — but retain is the default for a reason.\n */\n delete: Effect.fn(function* ({ output }) {\n yield* baoDelete(readPath(output.name));\n return undefined;\n }),\n }),\n ),\n );\n",
6
+ "/**\n * The digest every Bao.* family persists instead of a value — moved here from the retired\n * bao-shell.ts, unchanged, when the package stopped shelling out to `bao`.\n */\n\n/** ⚠️ Bun's hasher, not node:crypto — this runs under `bun --bun alchemy`. */\n/**\n * The only form a policy body may be hashed or compared in.\n *\n * ⛔ OPENBAO STRIPS THE TRAILING NEWLINE AND THAT ALONE WAS A FOREVER-DIFF. MEASURED on the live\n * server: the assembler emits each fragment followed by a newline, so `homeflare-llm` goes in as\n * 41 lines and `bao policy read` hands back 40 — identical text, one `\\n` apart, two different\n * digests. Every plan therefore reported `update`, every deploy rewrote the same policy, and the\n * stack could never settle. It was invisible until the stack planned for the first time today.\n * ★ THE HTTP API HANDS BACK THE SAME STRIPPED TEXT. `bao policy read` printed `data.policy` of\n * `GET /v1/sys/policies/acl/<name>` (openbao v2.6.2 api/sys_policy.go:60-81), which is what\n * policy-wire.ts now reads directly — so the canonicalisation is still load-bearing.\n *\n * ⚠️ `trimEnd` ONLY. Leading and interior whitespace are part of the HCL and must not be touched —\n * a policy that differs by an indent really is a different document, and quietly folding that\n * away would hide an edit rather than a formatting artifact.\n */\nexport const canonical = (text: string) => text.trimEnd();\n\nexport const sha256 = (text: string) =>\n new Bun.CryptoHasher('sha256').update(canonical(text)).digest('hex');\n",
7
+ "import { sha256 } from './digest.ts';\n\n/** Mount path without a trailing slash — props and `sys/mounts/<path>` use this shape. */\nexport const mountPath = (path: string) => path.replace(/\\/+$/, '');\n\nconst DURATION = /^(\\d+)([smhd])$/;\n\n/** Parse a Vault duration string (`15m`, `768h`, `0`) to seconds. */\nexport const parseDuration = (text: string): number | undefined => {\n const trimmed = text.trim();\n if (trimmed === '0') return 0;\n const match = DURATION.exec(trimmed);\n if (match === null) return undefined;\n const amount = Number(match[1]);\n switch (match[2]) {\n case 's':\n return amount;\n case 'm':\n return amount * 60;\n case 'h':\n return amount * 3600;\n case 'd':\n return amount * 86400;\n default:\n return undefined;\n }\n};\n\n/** Seconds from live JSON (`2764800`) or a duration prop (`768h`). */\nexport const ttlSeconds = (value: unknown): number | undefined => {\n if (typeof value === 'number' && Number.isFinite(value)) return value;\n if (typeof value === 'string') return parseDuration(value);\n return undefined;\n};\n\nexport interface BaoMountProps {\n /** Mount path without trailing slash, as `sys/mounts/<path>` takes it. */\n path: string;\n /** Secrets engine type, e.g. `kv`, `pki`, `openbao-plugin-secrets-cloudflare`. */\n type: string;\n /** Human description — tuned via `sys/mounts/<path>/tune`, as apply-roles.py describe_mounts does. */\n description?: string;\n /** Default lease TTL, e.g. `768h`. */\n defaultLeaseTtl?: string;\n /** Max lease TTL, e.g. `8760h`. */\n maxLeaseTtl?: string;\n /** KV only — sent as `options.version` on enable, as `-version=2` was. Immutable after create. */\n version?: 1 | 2;\n}\n\nexport interface BaoMountAttributes {\n path: string;\n type: string;\n description: string;\n defaultLeaseTtl: string;\n maxLeaseTtl: string;\n /** SHA-256 of the managed fields — safe to persist; see policy.ts. */\n digest: string;\n}\n\nconst configOf = (live: Record<string, unknown>) =>\n typeof live['config'] === 'object' && live['config'] !== null\n ? (live['config'] as Record<string, unknown>)\n : {};\n\nconst text = (value: unknown) => (typeof value === 'string' ? value : '');\n\nconst ttlProp = (seconds: number | undefined, fallback = '') => {\n if (seconds === undefined) return fallback;\n if (seconds % 86400 === 0) return `${String(seconds / 86400)}d`;\n if (seconds % 3600 === 0) return `${String(seconds / 3600)}h`;\n if (seconds % 60 === 0) return `${String(seconds / 60)}m`;\n return `${String(seconds)}s`;\n};\n\nexport const attributesOf = (\n props: BaoMountProps,\n live: Record<string, unknown>,\n): BaoMountAttributes => {\n const config = configOf(live);\n const defaultLeaseTtl = ttlProp(ttlSeconds(config['default_lease_ttl']));\n const maxLeaseTtl = ttlProp(ttlSeconds(config['max_lease_ttl']));\n const attrs = {\n path: mountPath(props.path),\n type: text(live['type']) || props.type,\n description: text(live['description']),\n defaultLeaseTtl,\n maxLeaseTtl,\n };\n return { ...attrs, digest: sha256(JSON.stringify(attrs)) };\n};\n\n/** `sys/mounts/<path>` — read, enable and disable all address the mount here. */\nexport const readPath = (path: string) => `sys/mounts/${mountPath(path)}`;\n\n/**\n * ★ NO TRAILING SLASH NEEDED ON THE WIRE. `bao secrets tune` appended one itself; the server's\n * route `mounts/(?P<path>.+?)/tune$` captures the bare path and `sanitizePath` appends the slash\n * OpenBao stores in sys/mounts (openbao v2.6.2 vault/logical_system_paths.go:4248,\n * vault/logical_system.go:4825-4833).\n */\nexport const tunePath = (path: string) => `${readPath(path)}/tune`;\n\n/**\n * The enable body. `-version=2` on the CLI became `options.version = \"2\"` (openbao v2.6.2\n * command/secrets_enable.go:278 into api MountInput.Options), so that is what is sent.\n *\n * ⚠️ DESCRIPTION AND TTLs ARE NOT SENT HERE. reconcile tunes them straight after the enable,\n * exactly as the CLI path did, so a create and an update converge through the same call.\n */\nexport const enableBody = (props: BaoMountProps): Record<string, unknown> =>\n props.version === 2 ? { options: { version: '2' }, type: props.type } : { type: props.type };\n\n/**\n * The tune body — only the fields the declaration names.\n *\n * ★ AN OMITTED FIELD IS LEFT ALONE. The server reads an absent TTL as \"\" and keeps the mount's\n * current value for \"\" (vault/logical_system.go:1614-1630), and applies `description` only when\n * present (:1725). The CLI sent unset TTL flags as \"\" and set description only when given\n * (command/secrets_tune.go Visit), so omitting them is the same request.\n */\nexport const tuneBody = (props: BaoMountProps): Record<string, string> => {\n const body: Record<string, string> = {};\n if (props.description !== undefined) body['description'] = props.description;\n if (props.defaultLeaseTtl !== undefined) body['default_lease_ttl'] = props.defaultLeaseTtl;\n if (props.maxLeaseTtl !== undefined) body['max_lease_ttl'] = props.maxLeaseTtl;\n return body;\n};\n\nconst sameTtl = (want: string | undefined, have: string) => {\n if (want === undefined) return true;\n const wantSeconds = ttlSeconds(want);\n const haveSeconds = ttlSeconds(have);\n return wantSeconds !== undefined && wantSeconds === haveSeconds;\n};\n\n/** True when live already matches the declaration on every managed field. */\nexport const matches = (attributes: BaoMountAttributes, props: BaoMountProps) =>\n attributes.type === props.type &&\n (props.description === undefined || attributes.description === props.description) &&\n sameTtl(props.defaultLeaseTtl, attributes.defaultLeaseTtl) &&\n sameTtl(props.maxLeaseTtl, attributes.maxLeaseTtl);\n\nexport const wantsTune = (props: BaoMountProps) =>\n props.description !== undefined ||\n props.defaultLeaseTtl !== undefined ||\n props.maxLeaseTtl !== undefined;\n",
8
+ "import { sha256 } from './digest.ts';\nimport { parseDuration, ttlSeconds } from './mount-form.ts';\n\nexport interface BaoAuthRoleProps {\n /** AppRole name, as `auth/approle/role/{name}` takes it. */\n name: string;\n /** Policies bound to tokens minted through this role. Names only — never a secret. */\n tokenPolicies: readonly string[];\n /** Token TTL, e.g. `15m`. */\n tokenTtl: string;\n /** Token max TTL, e.g. `1h`. */\n tokenMaxTtl: string;\n /** Secret ID TTL — `0` means non-expiring, as reconcile.sh and apply-consumers.py set. */\n secretIdTtl: string;\n /** Whether login requires a secret_id. Defaults to true when omitted on write. */\n bindSecretId?: boolean;\n /** `0` means unlimited uses — the shape reconcile.sh writes for headless roles. */\n secretIdNumUses?: number;\n}\n\nexport interface BaoAuthRoleAttributes {\n name: string;\n tokenPolicies: readonly string[];\n tokenTtl: string;\n tokenMaxTtl: string;\n secretIdTtl: string;\n bindSecretId: boolean;\n secretIdNumUses: number;\n /** SHA-256 of the managed fields — safe to persist; see policy.ts. */\n digest: string;\n}\n\nconst policiesOf = (live: Record<string, unknown>): readonly string[] => {\n const raw = live['token_policies'];\n if (!Array.isArray(raw)) return [];\n return raw\n .filter((entry): entry is string => typeof entry === 'string')\n .slice()\n .sort();\n};\n\nconst bool = (value: unknown, fallback: boolean) => (typeof value === 'boolean' ? value : fallback);\n\nconst int = (value: unknown, fallback: number) =>\n typeof value === 'number' && Number.isFinite(value) ? value : fallback;\n\nconst ttlText = (seconds: number | undefined, fallback = '') => {\n if (seconds === undefined) return fallback;\n if (seconds % 86400 === 0) return `${String(seconds / 86400)}d`;\n if (seconds % 3600 === 0) return `${String(seconds / 3600)}h`;\n if (seconds % 60 === 0) return `${String(seconds / 60)}m`;\n return `${String(seconds)}s`;\n};\n\nexport const attributesOf = (\n props: BaoAuthRoleProps,\n live: Record<string, unknown>,\n): BaoAuthRoleAttributes => {\n const attrs = {\n name: props.name,\n tokenPolicies: policiesOf(live),\n tokenTtl: ttlText(ttlSeconds(live['token_ttl'])),\n tokenMaxTtl: ttlText(ttlSeconds(live['token_max_ttl'])),\n secretIdTtl: ttlText(ttlSeconds(live['secret_id_ttl']), '0'),\n bindSecretId: bool(live['bind_secret_id'], true),\n secretIdNumUses: int(live['secret_id_num_uses'], 0),\n };\n return { ...attrs, digest: sha256(JSON.stringify(attrs)) };\n};\n\nexport const readPath = (name: string) => `auth/approle/role/${name}`;\n\n/**\n * The body for `PUT auth/approle/role/<name>`.\n *\n * ★ ALL STRINGS, EXACTLY THE `k=v` PAIRS `bao write` USED TO SEND — see `baoWrite` in bao-http.ts.\n */\nexport const writeBody = (props: BaoAuthRoleProps): Record<string, string> => {\n const body: Record<string, string> = {\n token_policies: props.tokenPolicies.join(','),\n token_ttl: props.tokenTtl,\n token_max_ttl: props.tokenMaxTtl,\n secret_id_ttl: props.secretIdTtl,\n };\n if (props.bindSecretId !== undefined) body['bind_secret_id'] = String(props.bindSecretId);\n if (props.secretIdNumUses !== undefined) {\n body['secret_id_num_uses'] = String(props.secretIdNumUses);\n }\n return body;\n};\n\nconst samePolicies = (want: readonly string[], have: readonly string[]) =>\n want.length === have.length && want.every((policy, index) => policy === have[index]);\n\nconst sameTtl = (want: string, have: string) => {\n const wantSeconds = parseDuration(want);\n const haveSeconds = ttlSeconds(have);\n return wantSeconds !== undefined && wantSeconds === haveSeconds;\n};\n\n/** True when live already matches the declaration on every managed field. */\nexport const matches = (attributes: BaoAuthRoleAttributes, props: BaoAuthRoleProps) => {\n const wantPolicies = props.tokenPolicies.slice().sort();\n return (\n samePolicies(wantPolicies, attributes.tokenPolicies) &&\n sameTtl(props.tokenTtl, attributes.tokenTtl) &&\n sameTtl(props.tokenMaxTtl, attributes.tokenMaxTtl) &&\n sameTtl(props.secretIdTtl, attributes.secretIdTtl) &&\n (props.bindSecretId === undefined || attributes.bindSecretId === props.bindSecretId) &&\n (props.secretIdNumUses === undefined || attributes.secretIdNumUses === props.secretIdNumUses)\n );\n};\n",
9
+ "/**\n * One call to OpenBao's HTTP API through Effect's `HttpClient` — the transport every Bao.* family\n * uses since it stopped shelling out to `bao`.\n *\n * ★ `HttpClient`, THE SAME PATTERN house/proxmox and house/forgejo USE AGAINST THEIR APIs, decided\n * 2026-09-14. It buys a typed error channel, interruption when a plan is cancelled, the runtime's\n * tracing, and a client a test can point at a fake server — and it needs no Alchemy session, so\n * every call here works from all four operations. (`CommandExecutor.run` was tried on Bao.Policy\n * first and blocked read/diff: Alchemy passes a session to reconcile/delete but NOT to read/diff,\n * Provider.ts:258-289. The ChildProcessSpawner that fixed that is gone with the CLI.)\n * ⚠️ THE STACK MUST PROVIDE `FetchHttpClient.layer`. alchemy.run.ts does, the way\n * house/proxmox/alchemy.run.ts does.\n */\nimport * as Context from 'effect/Context';\nimport * as Effect from 'effect/Effect';\nimport * as Option from 'effect/Option';\nimport * as Semaphore from 'effect/Semaphore';\nimport * as FetchHttpClient from 'effect/unstable/http/FetchHttpClient';\nimport * as Headers from 'effect/unstable/http/Headers';\nimport * as HttpClient from 'effect/unstable/http/HttpClient';\nimport * as HttpClientRequest from 'effect/unstable/http/HttpClientRequest';\nimport { type BaoEnvironment, headersFor, resolveAddress } from './bao-address.ts';\nimport { BaoError, type BaoIntent, settle } from './bao-status.ts';\n\n/**\n * The environment a call resolves BAO_ADDR, BAO_NAMESPACE and BAO_TOKEN from — `process.env`,\n * read at call time, unless a test provides another.\n */\nexport const BaoEnv = Context.Reference<BaoEnvironment>('homeflare/openbao/BaoEnv', {\n defaultValue: () => process.env,\n});\n\n/**\n * ⛔ AT MOST EIGHT EXCHANGES WITH OPENBAO AT ONCE — ACROSS EVERY Bao.* FAMILY, IN ONE PROCESS.\n *\n * 🔴 MEASURED 2026-09-14 10:41: A DEPLOY OF THIS STACK TOOK OPENBAO's API OFF THE MINI. Alchemy fans\n * resources out with a hard-coded `concurrency: \"unbounded\"` (alchemy Plan.ts:476, Apply.ts:259,\n * :263, :303 — no option reaches it), and 555 Bao.CloudflareRole resources each read, diff,\n * reconcile and read back: well over a thousand requests at one loopback listener within seconds.\n * OpenBao logged\n * HTTP server (listening on 127.0.0.1:8200) exited with error: set tcp\n * 127.0.0.1:8200->127.0.0.1:57974: setsockopt: invalid argument\n * and KEPT RUNNING with 8200 closed. launchd saw a live process and restarted nothing. Everything\n * on the mini that talks to 127.0.0.1:8200 was cut off, starting with the agent's templates, until\n * a manual restart.\n * ★ THE LISTENER DYING IS A GO-ON-DARWIN FAILURE, NOT OURS TO FIX. Traefik lost its entrypoints the\n * same way on darwin/arm64 (traefik issue 8841). The BURST is ours. The adoption deploy an hour\n * earlier made the same calls and survived, so a cap narrows a race; it does not prove it closed.\n * ★ A SEMAPHORE, NOT `HttpClient.withRateLimiter`. That limits requests per window and needs a\n * RateLimiter service. The failure is simultaneous connections, and only a cap on in-flight\n * exchanges bounds those whatever each one's latency.\n * ⚠️ A Context.Reference SO A TEST CAN PROVIDE ITS OWN GATE. The default is built ONCE and cached on\n * the reference (effect Context.ts:1582-1588), so every call in the process shares one gate.\n * ⚠️ EIGHT IS A CHOICE, NOT A MEASUREMENT OF WHAT THE LISTENER TOLERATES. Nothing here can find that\n * number without re-running the failure against production.\n */\nexport const MAX_IN_FLIGHT = 8;\n\nexport const BaoGate = Context.Reference<Semaphore.Semaphore>('homeflare/openbao/BaoGate', {\n defaultValue: () => Semaphore.makeUnsafe(MAX_IN_FLIGHT),\n});\n\n/**\n * ⚠️ THE CLI's DEFAULT CLIENT TIMEOUT (openbao v2.6.2 api/client.go:324). A hung agent or a server\n * that accepts and never answers would otherwise hang the plan with no line saying why.\n */\nconst TIMEOUT = '60 seconds';\n\n/**\n * ⛔ EFFECT RECORDS EVERY REQUEST HEADER ON THE CLIENT SPAN and redacts only the names in\n * `Headers.CurrentRedactedNames` — by default authorization, cookie, set-cookie and x-api-key\n * (effect Headers.ts:753-762; HttpClient.ts:842-849, with `TracerHeaderFilter` defaulting to\n * true). `X-Vault-Token` is not on that list, so under any tracer the token would be exported as\n * a span attribute. It is added for every call made here.\n */\nconst redactingToken = <A, E, R>(effect: Effect.Effect<A, E, R>) =>\n Effect.gen(function* () {\n const names = yield* Headers.CurrentRedactedNames;\n return yield* Effect.provideService(effect, Headers.CurrentRedactedNames, [\n ...names,\n 'x-vault-token',\n ]);\n });\n\n/**\n * Route the exchange over a unix socket when the address is one.\n *\n * ★ BUN's `fetch` TAKES A `unix` OPTION (bun-types globals.d.ts BunFetchRequestInit.unix), AND\n * `FetchHttpClient.RequestInit` IS EFFECT'S DOCUMENTED WAY TO PASS FETCH OPTIONS. The fetch client\n * spreads that service into every call (effect FetchHttpClient.ts:59-72), reading it from the\n * CALLING fiber's context — and `layerMergedContext` merges the caller's context over the layer's\n * (HttpClient.ts:2065-2076), so providing it around one call routes that call alone. Any defaults\n * already provided are kept.\n * ⛔ IT ONLY WORKS ON BUN, SO ANYWHERE ELSE IT REFUSES. Node's fetch ignores `unix` and would send\n * the request to http://localhost:80 — a silent wrong server, which is the one outcome this\n * package exists to prevent. It also assumes the provided `HttpClient` is the fetch one.\n */\nconst overSocket =\n (socket: string | undefined, method: string, path: string) =>\n <A, R>(effect: Effect.Effect<A, BaoError, R>): Effect.Effect<A, BaoError, R> => {\n if (socket === undefined) return effect;\n if (typeof Bun === 'undefined') {\n return Effect.fail(\n new BaoError(0, method, path, ['a unix:// address needs Bun fetch; this is not Bun']),\n );\n }\n return Effect.flatMap(Effect.serviceOption(FetchHttpClient.RequestInit), (defaults) => {\n const init: BunFetchRequestInit = { ...Option.getOrElse(defaults, () => ({})), unix: socket };\n return Effect.provideService(effect, FetchHttpClient.RequestInit, init);\n });\n };\n\n/** One exchange, classified. The body is the parsed JSON object, or undefined for absent / 204. */\nexport const baoCall = (\n intent: BaoIntent,\n method: 'DELETE' | 'GET' | 'POST' | 'PUT',\n path: string,\n body?: Readonly<Record<string, unknown>>,\n) =>\n Effect.gen(function* () {\n const env = yield* BaoEnv;\n const address = resolveAddress(env);\n const client = yield* HttpClient.HttpClient;\n // ⚠️ The content type goes on `bodyText`, never in the header map: `bodyText` overwrites it.\n // house/proxmox/src/client.ts has the scar.\n const request = HttpClientRequest.make(method)(`${address.base}/v1/${path}`).pipe(\n HttpClientRequest.setHeaders(headersFor(address, env)),\n body === undefined\n ? (self) => self\n : HttpClientRequest.bodyText(JSON.stringify(body), 'application/json'),\n );\n // ★ Status first, body read exactly once, as text — an error body is not reliably JSON.\n const exchange = client.execute(request).pipe(\n Effect.flatMap((response) =>\n Effect.map(response.text, (text) => ({ status: response.status, text })),\n ),\n Effect.timeout(TIMEOUT),\n // ⛔ `.message` ONLY: the HttpClientError message is the reason, method and URL. The error\n // object itself holds the request, whose headers hold the token.\n Effect.mapError((cause) => new BaoError(0, method, path, [cause.message])),\n );\n // ★ THE PERMIT WRAPS THE TIMEOUT, NOT THE REVERSE: a call queued behind the gate is not yet\n // talking to OpenBao, so its wait must not count against the 60 seconds.\n const gate = yield* BaoGate;\n const { status, text } = yield* exchange.pipe(\n overSocket(address.socket, method, path),\n redactingToken,\n Semaphore.withPermits(gate, 1),\n );\n const outcome = settle(intent, method, path, status, text);\n return 'error' in outcome ? yield* Effect.fail(outcome.error) : outcome.body;\n });\n\n/**\n * The `data` of a read, or undefined when OpenBao says the object is ABSENT (404).\n *\n * ★ `bao read -format=json` printed this same response body; the old helper took `data` when it was\n * an object and the whole body otherwise, and so does this.\n */\nexport const baoRead = (path: string) =>\n Effect.map(baoCall('read', 'GET', path), (body) => {\n if (body === undefined) return undefined;\n const data = body['data'];\n return typeof data === 'object' && data !== null\n ? (data as Record<string, unknown>)\n : (body as Record<string, unknown>);\n });\n\n/**\n * A write whose SUCCESS MATTERS. Any non-2xx fails, 404 included.\n *\n * ★ `PUT` IS WHAT `bao write` SENT (openbao v2.6.2 api/logical.go Write); `sys/mounts` enable and\n * tune are `POST` in the same client (api/sys_mounts.go:57, :201).\n * ⚠️ ROLE BODIES STAY ALL-STRING ON PURPOSE. `bao write k=v` put every value in the JSON body as a\n * string — MEASURED with `-output-curl-string`, see ssh-role-form.ts — and the form files still\n * build exactly that body, so the server parses the same request it was always sent.\n */\nexport const baoWrite = (\n method: 'POST' | 'PUT',\n path: string,\n body: Readonly<Record<string, unknown>>,\n) => Effect.asVoid(baoCall('write', method, path, body));\n\n/** A delete: idempotent — already gone (404) is success — and refused (403, 5xx) is a failure. */\nexport const baoDelete = (path: string) => Effect.asVoid(baoCall('delete', 'DELETE', path));\n",
10
+ "/**\n * Where an OpenBao call goes and which headers it carries — resolved from the environment the way\n * the `bao` CLI resolves it, so the provider and `bao kv get` can never disagree about which server\n * or namespace this machine is talking to.\n *\n * ★ READ FROM openbao v2.6.2, NOT RECALLED:\n * · api/client.go:34-66 — BAO_ADDR, BAO_AGENT_ADDR, BAO_NAMESPACE, BAO_TOKEN, and the header\n * names `X-Vault-Namespace` and `X-Vault-Token`.\n * · api/client.go:370 — the default address, `https://127.0.0.1:8200`.\n * · api/client.go:503-507, 617-618, 761-762 — BAO_AGENT_ADDR, when set, WINS over BAO_ADDR.\n * · api/env.go:22-34 — every BAO_* falls back to its VAULT_* twin, but a BAO_* that is present\n * and EMPTY still wins over the VAULT_* one.\n * · api/client.go:776-785 — the token and namespace are applied only when non-empty, and every\n * request carries `X-Vault-Request: true`.\n * · api/client.go:1349-1354 — the request path is joined onto the address's own path.\n *\n * ⛔ WHAT THE CLI DOES AND THIS DELIBERATELY DOES NOT: with BAO_TOKEN unset, `bao` falls back to its\n * token helper (a token file in $HOME). This never reads that file. No BAO_TOKEN means AGENT\n * MODE — see `headersFor`.\n * ⚠️ NOT CARRIED OVER: BAO_CACERT / BAO_SKIP_VERIFY / client certificates, SRV lookup, and the CLI's\n * retries on 5xx. An https address needs a certificate the runtime already trusts; a TLS failure\n * fails the call loudly rather than being skipped.\n */\n\nexport type BaoEnvironment = Readonly<Record<string, string | undefined>>;\n\nexport type BaoAddress = {\n /** Scheme, host and any path prefix, no trailing slash. `http://localhost` for a socket. */\n readonly base: string;\n /** The socket path when the address is `unix:///path`, otherwise undefined. */\n readonly socket: string | undefined;\n /** The namespace to send, when there is one. */\n readonly namespace: string | undefined;\n};\n\nexport const DEFAULT_ADDRESS = 'https://127.0.0.1:8200';\n\nconst UNIX = 'unix://';\n\n/** api/env.go ReadBaoVariable: present (even empty) BAO_* wins, else the VAULT_* twin. */\nexport const readBaoVariable = (env: BaoEnvironment, name: `BAO_${string}`): string =>\n env[name] ?? env[`VAULT_${name.slice('BAO_'.length)}`] ?? '';\n\n/**\n * The address and namespace for this environment. Carries no token, so it is safe to log.\n *\n * ★ UNIX SOCKETS, BECAUSE AN AGENT LISTENER CAN BE ONE. OpenBao Agent's `listener \"unix\"` serves\n * the same API on a socket (website/content/docs/agent-and-proxy/agent/index.mdx:382-384), and the\n * agent advertises such a listener as `unix://<path>` (command/agent.go:565-566). The CLI handles\n * that address by dialling the socket and speaking plain HTTP to host `localhost`\n * (api/client.go:654-667); `base` and `socket` are exactly those two halves.\n */\nexport const resolveAddress = (env: BaoEnvironment): BaoAddress => {\n const agent = readBaoVariable(env, 'BAO_AGENT_ADDR');\n const direct = readBaoVariable(env, 'BAO_ADDR');\n const raw = agent !== '' ? agent : direct !== '' ? direct : DEFAULT_ADDRESS;\n const namespace = readBaoVariable(env, 'BAO_NAMESPACE');\n const socket = raw.startsWith(UNIX) ? raw.slice(UNIX.length) : undefined;\n return {\n base: socket === undefined ? raw.replace(/\\/+$/, '') : 'http://localhost',\n namespace: namespace === '' ? undefined : namespace,\n socket,\n };\n};\n\n/**\n * The headers one call sends.\n *\n * ★ AGENT MODE IS \"NO TOKEN HEADER AT ALL\", AND THAT IS THE AGENT'S OWN CONTRACT. Read in openbao\n * v2.6.2 command/agentproxyshared/cache/handler.go:29-38: the proxy handler takes the request's\n * `X-Vault-Token`, and only when it is EMPTY and an auto-auth sink exists does it substitute the\n * agent's own token. command/agent.go:517-541 creates that sink exactly when\n * `api_proxy { use_auto_auth_token = true }` is set; command/agent/config/config.go:288-292 and\n * 635-637 map the older `cache { use_auto_auth_token }` onto the same setting; and\n * command/proxy.go:495-518 wires OpenBao Proxy identically. The docs say the same in words\n * (website/content/docs/agent-and-proxy/agent/apiproxy.mdx:57-62): a request without a token is\n * forwarded with the auto-auth token, a request that bears one keeps its own, and `\"force\"`\n * overwrites it.\n * ⛔ SO AN EMPTY `X-Vault-Token: ` WOULD BE WRONG IN BOTH DIRECTIONS: harmless to the agent,\n * which reads empty as absent, but an explicit unauthenticated request to a server. The header\n * is omitted, never blanked.\n * ★ THE NAMESPACE RIDES THROUGH THE AGENT UNTOUCHED. The proxy clones its client and replaces that\n * client's headers with the incoming request's (cache/api_proxy.go:50-54 and :78), so the\n * `X-Vault-Namespace` sent here is the one OpenBao sees.\n * ⚠️ `X-Vault-Request: true` IS NOT DECORATION. An agent listener with `require_request_header`\n * refuses any request without it (command/agent.go:544-548); the CLI always sends it.\n *\n * ⛔ THE RETURN VALUE HOLDS THE TOKEN. Hand it to the request and nowhere else: never to an error,\n * a log line, an attribute, or a span — bao-http.ts adds the header to the redacted names.\n */\nexport const headersFor = (address: BaoAddress, env: BaoEnvironment): Record<string, string> => {\n const headers: Record<string, string> = { 'X-Vault-Request': 'true' };\n if (address.namespace !== undefined) headers['X-Vault-Namespace'] = address.namespace;\n const token = readBaoVariable(env, 'BAO_TOKEN');\n if (token !== '') headers['X-Vault-Token'] = token;\n return headers;\n};\n",
11
+ "/**\n * How a finished OpenBao HTTP exchange reads: a body, a genuinely absent object, or an error.\n *\n * ★ STATUS CODES, NEVER WORDS. The `bao` CLI this replaced reported every outcome as stderr text,\n * and deciding by that text broke twice: every failure once read as absent (a bad token planned 33\n * updates), and then `bao policy read` turned out to say \"absent\" in different words from\n * `bao read`, so a new policy could not be created (b3c18a33e). The API itself answers 404 for\n * absent, 403 for denied and 503 for sealed — openbao v2.6.2 sdk/logical/response_util.go:\n * RespondErrorCommon (a read that finds nothing is 404; ErrPermissionDenied is 403) and\n * AdjustErrorStatusCode (ErrSealed is 503).\n */\n\nexport type BaoIntent = 'read' | 'write' | 'delete';\n\n/**\n * A failed call, naming the method, the path and OpenBao's own `errors` array.\n *\n * ⛔ IT NEVER CARRIES THE TOKEN, AND IT NEVER CARRIES A RESPONSE BODY IT DID NOT PARSE. It is built\n * from the method, the API path and OpenBao's error strings only — no request, no headers, no\n * `cause` — because an Effect `HttpClientRequest` inspects its headers through a redaction list\n * that does not name `x-vault-token`. A 2xx body can hold a credential, and a foreign error page\n * can echo the request back, so a body that is not OpenBao's is described, never quoted.\n */\nexport class BaoError extends Error {\n constructor(\n readonly status: number,\n readonly method: string,\n readonly path: string,\n readonly errors: readonly string[],\n ) {\n const code = status === 0 ? 'no response' : String(status);\n super(`OpenBao ${method} /v1/${path} -> ${code}: ${errors.join('; ') || '(no errors given)'}`);\n this.name = 'BaoError';\n }\n}\n\nexport type Settled =\n | { readonly body: Record<string, unknown> | undefined }\n | { readonly error: BaoError };\n\nconst objectOf = (text: string): Record<string, unknown> | undefined => {\n try {\n const parsed: unknown = JSON.parse(text);\n return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : undefined;\n } catch {\n return undefined;\n }\n};\n\n/**\n * OpenBao's `errors` array, or undefined for a body that is not OpenBao's.\n *\n * ★ EVERY ERROR OPENBAO WRITES IS `{\"errors\": [...]}` WITH A JSON CONTENT TYPE, 404 included —\n * sdk/logical/response_util.go RespondError:180-196 — and every entry is a STRING.\n * ⛔ AN ARRAY WITH ANY NON-STRING ENTRY IS NOT OPENBAO. The first version filtered the non-strings\n * out, which turned a JSON gateway's `{\"errors\":[{\"code\":1000,\"message\":\"Not Found\"}]}` into an\n * empty OpenBao error list — and a 404 carrying it into \"absent\" on a read and \"done\" on a delete,\n * the plan-CREATE-over-something-that-exists failure arriving through a wrong BAO_ADDR. Found by\n * grok's review on 2026-09-14. An EMPTY array is still OpenBao's: a plain 404 carries exactly that.\n */\nconst errorsOf = (text: string): readonly string[] | undefined => {\n const raw = objectOf(text)?.['errors'];\n return Array.isArray(raw) && raw.every((entry) => typeof entry === 'string')\n ? (raw as string[])\n : undefined;\n};\n\n/**\n * Classify one exchange.\n *\n * ★ PURE AND EXPORTED SO THE ONE DECISION EVERY READ AND DELETE HERE RESTS ON CAN BE TESTED WITHOUT\n * A VAULT. It has been wrong in both directions already — every failure once read as absent, and\n * one command's absence wording was assumed for another's — see bao-status.test.ts.\n *\n * ⛔ 404 IS ABSENCE ONLY ON A READ, AND ONLY WHEN OPENBAO SAID IT. MEASURED 2026-09-13 with the CLI,\n * and the discriminator is real but NARROW: a genuinely absent object, read by a token that IS\n * allowed to read that path, is a 404. ⛔ AND THE SAME READ UNDER A NARROWER TOKEN IS A DENIAL:\n * under the agent lane the identical read answered `Code: 403 … permission denied`, because the\n * policy grants named paths and an unnamed one is refused rather than reported missing. So\n * \"absent\" is only knowable to a caller that could have read it — which is exactly why 403 is an\n * ERROR rather than a shrug. A resource that cannot tell absence from denial plans CREATE for\n * something that exists, which is the failure this estate has now found four separate times.\n * ⚠️ A 404 WHOSE BODY IS NOT `{\"errors\": […strings]}` IS NOT OPENBAO. A BAO_ADDR pointing at the\n * wrong server — a reverse proxy's HTML 404, a gateway's JSON one — must not read as \"nothing\".\n *\n * 🔴 THE SHELL HELPER USED TO RETURN `''` FOR EVERY NON-ZERO EXIT, AND THAT IS A LIE WITH\n * CONSEQUENCES. An expired BAO_TOKEN, a sealed server or a missing grant all came back as \"the\n * mount is not there\", so `diff` answered `update`, `reconcile` ran `secrets enable`, and the plan\n * reported a CREATE on infrastructure that exists. The failure the operator sees is about the\n * wrong thing entirely, at the worst possible moment — a sealed vault. ⚠️ SO 403, 5xx AND ANYTHING\n * ELSE THAT IS NOT 2xx FAILS. The cost is that a genuinely-absent read under a too-narrow token\n * becomes an error instead of a create, and that is the right way round: \"I could not tell\" is a\n * true statement, and \"it is not there\" was not.\n *\n * 🔴 THE DELETES ALL USED THE READ HELPER, WHICH DISCARDED THE EXIT CODE. A refused\n * `secrets disable` was reported as a successful delete: the resource left Alchemy's state while\n * the mount stayed live, so the next plan showed nothing and the object was orphaned in silence.\n * ⚠️ ABSENCE IS STILL NOT AN ERROR FOR A DELETE, because a delete must be idempotent: Alchemy\n * retries, and the second attempt legitimately finds nothing. So a delete's 404 is success and\n * its 403 is not.\n * ★ DISABLING A MOUNT THAT IS ALREADY GONE IS NOT EVEN A 404. A review on 2026-09-14 said it failed\n * with `no matching mount` and would wedge a retried delete. The server says otherwise: it answers\n * success for a missing mount so as not to reveal whether one existed — openbao v2.6.2\n * vault/logical_system.go:1211-1216 — and the CLI printed \"Disabled the secrets engine (if it\n * existed)\" (command/secrets_disable.go:87).\n */\nexport const settle = (\n intent: BaoIntent,\n method: string,\n path: string,\n status: number,\n text: string,\n): Settled => {\n const fail = (errors: readonly string[]) => ({\n error: new BaoError(status, method, path, errors),\n });\n if (status < 200 || status >= 300) {\n const errors = errorsOf(text);\n if (errors === undefined) {\n // ⛔ DESCRIBED, NEVER QUOTED. A proxy or gateway error page can echo the request back, headers\n // and all, so quoting it could carry X-Vault-Token into an error that Alchemy logs (grok,\n // 2026-09-14). mint in house/proxmox/src/credentials.ts already did it this way.\n return fail([`not an OpenBao error body (${String(text.length)} bytes)`]);\n }\n if (status === 404 && intent !== 'write') return { body: undefined };\n return fail(errors);\n }\n // ★ A write or delete answering 204 has nothing to say. A READ with no body has told us nothing,\n // and OpenBao never does that — a read that finds nothing is a 404 (RespondErrorCommon).\n if (text.trim() === '') {\n return intent === 'read' ? fail(['a successful read with an empty body']) : { body: undefined };\n }\n const body = objectOf(text);\n return body === undefined ? fail(['a success body that is not a JSON object']) : { body };\n};\n",
12
+ "/**\n * A role on one of the six Cloudflare secrets-engine mounts — what one consumer may mint: the\n * Cloudflare policy its tokens carry and the lease bounds they get. METADATA ONLY.\n *\n * ★ THE PORT OF apply-roles.py's WRITE. roles.yaml expands to these (cloudflare-roles-expand.ts),\n * and what used to be `bao write <mount>/roles/<name> ttl= max_ttl= description= policies=` is\n * reconcile below. The props name permission groups; CloudflarePermissionGroups turns them into\n * IDs at diff and reconcile time.\n *\n * ⛔ NO SECRET IN PROPS OR ATTRIBUTES — see cloudflare-role-form.ts. ⛔ AND ISSUANCE IS NOT THIS\n * RESOURCE: `<mount>/creds/<name>` returns a LIVE Cloudflare token, and a token declared as a\n * resource would outlive its lease in Alchemy's unencrypted state (proxmox-role.ts has the four\n * places it would land). This declares the door; minting is a runtime call.\n *\n * ★ `defaultRemovalPolicy: 'retain'` — deleting a role breaks every consumer that mints from it,\n * and apply-roles.py never deleted one either: an unexplained role was reported UNTRACKED and\n * left alone (apply-roles.py:190-196). Opt in with `.pipe(RemovalPolicy.destroy())`.\n */\nimport { Resource } from 'alchemy';\nimport { isResolved } from 'alchemy/Diff';\nimport * as Provider from 'alchemy/Provider';\nimport * as Effect from 'effect/Effect';\nimport type * as HttpClient from 'effect/unstable/http/HttpClient';\nimport { groupKey, scopeOfResource } from './cloudflare-group-scope.ts';\nimport { CloudflarePermissionGroups, type GroupRef } from './cloudflare-permission-groups.ts';\nimport { resolvePolicies } from './cloudflare-policy.ts';\nimport {\n type BaoCloudflareRoleAttributes,\n type BaoCloudflareRoleProps,\n attributesOf,\n differences,\n refusalOf,\n rolePath,\n writeBody,\n} from './cloudflare-role-form.ts';\nimport {\n deleteCloudflareRole,\n readCloudflareRole,\n writeCloudflareRole,\n} from './cloudflare-role-wire.ts';\n\nexport type { BaoCloudflareRoleAttributes, BaoCloudflareRoleProps };\n\n/**\n * ⚠️ CloudflarePermissionGroups IS NOT A REQUIREMENT OF THE RESOURCE. The provider takes it once, when\n * it is built (`yield* CloudflarePermissionGroups` below), and alchemy.run.ts supplies it to the\n * provider layer with `Layer.provide`. Listing it here as well made it a requirement of the STACK\n * body, which Alchemy only lets name its own provider and stack services — tsc refused the wiring.\n */\nexport interface BaoCloudflareRole extends Resource<\n 'Bao.CloudflareRole',\n BaoCloudflareRoleProps,\n BaoCloudflareRoleAttributes,\n never,\n HttpClient.HttpClient\n> {}\n\nexport const BaoCloudflareRole = Resource<BaoCloudflareRole>('Bao.CloudflareRole', {\n defaultRemovalPolicy: 'retain',\n});\n\nexport const BaoCloudflareRoleProvider = () =>\n Provider.effect(\n BaoCloudflareRole,\n Effect.gen(function* () {\n const groups = yield* CloudflarePermissionGroups;\n\n /**\n * The declaration with IDs. ⛔ A name that does not resolve fails here — never a guess.\n *\n * ⛔ ONE REF PER (NAME, SCOPE), NOT PER NAME. The entry's resource is what says which scope\n * its groups are gated at, so a role whose zone entry and account entry name one group asks\n * for both ids; deduplicating by name alone would ask for one and use it for both.\n */\n const resolved = (props: BaoCloudflareRoleProps) =>\n Effect.gen(function* () {\n const refs = new Map<string, GroupRef>();\n for (const policy of props.policies) {\n /** ⛔ THE SAME PREFERENCE resolvePolicies MAKES — ask for the id the entry will use. */\n const scope = policy.scope ?? scopeOfResource(policy.resource);\n for (const name of policy.groups) refs.set(groupKey(name, scope), { name, scope });\n }\n const ids = yield* groups.resolve(props.mount, [...refs.values()]);\n const { missing, policies } = resolvePolicies(props.policies, ids);\n if (missing.length > 0) {\n return yield* Effect.die(\n new Error(\n `Bao.CloudflareRole ${rolePath(props.mount, props.name)}: groups ` +\n `${missing.join(', ')} came back unresolved from a lookup that reported success.`,\n ),\n );\n }\n return policies;\n });\n\n return BaoCloudflareRole.Provider.of({\n /**\n * ⛔ `<mount>/roles` IS NOT A LIST OF THINGS THIS OWNS. MEASURED 2026-09-14: 586 live roles,\n * 585 of which roles.yaml declares — and the 586th, cloudflare-<account>-platform/\n * alchemy-os-create, is exactly the role nobody has decided about. Returning it would\n * invite Alchemy to adopt, then delete, a role that may be someone's live credential.\n */\n list: () => Effect.succeed([]),\n\n read: Effect.fn(function* ({ olds }) {\n const live = yield* readCloudflareRole(olds.mount, olds.name);\n return live === undefined ? undefined : attributesOf(olds, live);\n }),\n\n /**\n * ⛔ IT COMPARES THE LIVE ROLE, NOT THE STORED DIGEST. A policy widened by hand in the\n * OpenBao UI changes what every future token can do while the digest in Postgres still\n * says everything is fine.\n *\n * ⚠️ NO `replace`, EVEN FOR A NAME OR MOUNT CHANGE. A different path is a different object,\n * so diff reads the new path, finds it absent and plans `update`; reconcile creates it and\n * the old role is left live, retained and unmanaged — the same outcome apply-roles.py's\n * UNTRACKED report had. declareCloudflareRoles puts mount and name in the resource id, so\n * a rename there is a new resource anyway.\n */\n diff: Effect.fn(function* ({ news, output }) {\n if (output === undefined || !isResolved(news)) return undefined;\n /**\n * ⚠️ A DECLARATION reconcile WOULD REFUSE MUST NEVER PLAN AS noop — route it to reconcile,\n * which says why (pki-role.ts has the same guard).\n */\n if (refusalOf(news) !== undefined) return { action: 'update' } as const;\n const policies = yield* resolved(news);\n const live = yield* readCloudflareRole(news.mount, news.name);\n if (live === undefined) return { action: 'update' } as const;\n return differences(news, policies, live).length === 0\n ? ({ action: 'noop' } as const)\n : ({ action: 'update' } as const);\n }),\n\n reconcile: Effect.fn(function* ({ news }) {\n const path = rolePath(news.mount, news.name);\n const refusal = refusalOf(news);\n if (refusal !== undefined) {\n return yield* Effect.die(new Error(`Bao.CloudflareRole ${path}: ${refusal}.`));\n }\n const policies = yield* resolved(news);\n const live = yield* readCloudflareRole(news.mount, news.name);\n /**\n * ⛔ ADOPTING A ROLE THAT ALREADY MATCHES MUST NOT REWRITE IT. Alchemy's `adopted` action\n * routes through reconcile (policy.ts has the Apply.ts reading), so without this guard\n * the first deploy would PUT all 585 roles — byte-identical in the happy case, and a\n * silent rewrite of live grants the moment the port disagreed with production.\n */\n if (live === undefined || differences(news, policies, live).length > 0) {\n yield* writeCloudflareRole(news.mount, news.name, writeBody(news, policies));\n }\n const after = yield* readCloudflareRole(news.mount, news.name);\n if (after === undefined) {\n return yield* Effect.die(\n new Error(`Bao.CloudflareRole ${path}: the write succeeded but the role is absent.`),\n );\n }\n /**\n * ⛔ THE WRITE IS NOT TRUSTED, IT IS RE-READ. The engine re-marshals `policies` from structs\n * (path_roles.go:199-207), so a document it stored differently from what was sent — a\n * dropped group, a merged resource — shows up here as a named field, not as a green\n * deploy followed by a 403 at mint time.\n */\n const left = differences(news, policies, after);\n if (left.length > 0) {\n return yield* Effect.die(\n new Error(\n `Bao.CloudflareRole ${path}: wrote cleanly but reads back different in ` +\n `${left.map((d) => d.field).join(', ')}. Inspect the role before retrying.`,\n ),\n );\n }\n return attributesOf(news, after);\n }),\n\n /**\n * ⛔ DELETING A ROLE DOES NOT REVOKE THE TOKENS IT MINTED. They are leases on Cloudflare API\n * tokens and live to their own expiry, so this is an availability change for every\n * consumer of `creds/<name>`, never a containment action — to contain a leak, revoke the\n * lease. Idempotent: already gone is success (bao-status.ts).\n */\n delete: Effect.fn(function* ({ output }) {\n yield* deleteCloudflareRole(output.mount, output.name);\n return undefined;\n }),\n });\n }),\n );\n",
13
+ "/**\n * The three resources a Cloudflare policy entry can name, and the SCOPE its permission groups are\n * looked up at — one vocabulary, both directions.\n *\n * ★ EXTRACTED FROM cloudflare-roles-expand.ts 2026-09-15, which had reached 270 lines (AGENTS.md\n * rule 5, code ≤250). The split is by CONCERN, not by size: expand.ts turns roles.yaml into\n * roles, this answers \"which of the two ids named `Logs Read` does this entry mean\".\n */\n\n/** cfhf.py:172-173. */\nexport const zoneResource = (zoneId: string) => `com.cloudflare.api.account.zone.${zoneId}`;\nexport const accountResource = (accountId: string) => `com.cloudflare.api.account.${accountId}`;\n\n/**\n * One R2 bucket, which is a DIFFERENT RESOURCE NAMESPACE from the two above.\n *\n * ⛔ `com.cloudflare.edge.r2.bucket.…`, NOT `com.cloudflare.api.account.…`, and the three parts are\n * joined by UNDERSCORES inside the last segment — `<accountId>_<jurisdiction>_<bucket>`. Getting\n * the separator wrong produces a syntactically fine resource that matches no bucket, and the\n * token mints successfully and is refused on first use, which reads exactly like a missing\n * permission group. Per Cloudflare's R2 API-token documentation, read 2026-09-15.\n * ⚠️ AND IT ONLY BINDS THE `Bucket Item` GROUPS. `Workers R2 Storage Write` / `… Read` are gated at\n * the ACCOUNT scope; the bucket-scoped pair is `Workers R2 Storage Bucket Item Write` / `… Read`.\n * Pairing an account group with this resource is the same silent 403.\n */\nexport const bucketResource = (accountId: string, jurisdiction: string, bucket: string) =>\n `com.cloudflare.edge.r2.bucket.${accountId}_${jurisdiction}_${bucket}`;\n\n/**\n * The three scopes this expansion can ask for — the namespaces of the three resources above, which\n * is exactly what the engine reports as a group's `scopes`.\n *\n * ★ MEASURED on `cloudflare-<account>-platform/permission-groups`, 2026-09-15 12:10: 401 groups,\n * every one with exactly ONE scope, over five values — 297 `com.cloudflare.api.account`, 94\n * `com.cloudflare.api.account.zone`, 2 `com.cloudflare.edge.r2.bucket` and four each of\n * `…account.flagship.app` and `com.cloudflare.edge.worker.script`, which no resource here builds.\n * The testing account's list is byte-identical, so the ids are Cloudflare's, not an account's.\n */\nexport const GROUP_SCOPES = {\n account: 'com.cloudflare.api.account',\n bucket: 'com.cloudflare.edge.r2.bucket',\n zone: 'com.cloudflare.api.account.zone',\n} as const;\n\n/**\n * The short word roles.yaml writes in `groups_scope:` → the namespace: `zone` becomes\n * `com.cloudflare.api.account.zone`. Undefined for a word that is not one of the three KEYS above.\n *\n * ⛔ THE yaml SAYS THE SHORT WORD AND NEVER THE NAMESPACE. A namespace typed into roles.yaml would\n * be accepted by any check that only asked for a string, and a misspelt one resolves no group at\n * all — an unknown-group failure naming a scope nobody wrote on purpose, instead of the line.\n * ⚠️ `Object.entries(…).find`, NOT an index with a narrowing `as`: codex standard 2 bans the cast,\n * and the lookup has three entries.\n */\nexport const groupScopeNamed = (word: string): string | undefined =>\n Object.entries(GROUP_SCOPES).find(([name]) => name === word)?.[1];\n\n/**\n * The scope the groups of one policy entry are gated at: its resource namespace, the instance id\n * dropped. A zone entry asks for zone groups, an account entry for account groups, a bucket entry\n * for the two `com.cloudflare.edge.r2.bucket` ones.\n *\n * ⛔ ZONE IS TESTED BEFORE ACCOUNT, because `com.cloudflare.api.account.zone.<id>` also starts with\n * the account namespace, and the wrong order would call every zone entry an account one.\n * ⚠️ A RESOURCE IN NO KNOWN NAMESPACE BECOMES ITS OWN SCOPE, which matches no group, so it fails as\n * an unresolvable name naming the scope it asked for — never by borrowing another scope's id.\n * ⚠️ IT IS THE DEFAULT, NOT THE LAST WORD. An entry carrying an explicit `scope` overrides it —\n * see DeclaredPolicy.scope (cloudflare-roles-expand.ts) and resolvePolicies.\n */\nexport const scopeOfResource = (resource: string) => {\n const scopes = [GROUP_SCOPES.zone, GROUP_SCOPES.bucket, GROUP_SCOPES.account];\n return scopes.find((scope) => resource.startsWith(`${scope}.`)) ?? resource;\n};\n\n/**\n * What a resolved group is filed under.\n *\n * ⛔ A NAME ALONE IS NOT A KEY. MEASURED 2026-09-15 on the live list: SEVEN names are listed twice,\n * once at `com.cloudflare.api.account` and once at `com.cloudflare.api.account.zone` — `Access:\n * Apps and Policies Read` / `… Revoke` / `… Write`, `Disable ESC Read` / `… Write`, `Logs Read`\n * and `Logs Write`. cfhf.py:148 keyed by name and the last one read won in silence.\n */\nexport const groupKey = (name: string, scope: string) => `${scope} ${name}`;\n",
14
+ "/**\n * Permission-group (name, SCOPE) → ID, behind ONE interface with two sources.\n *\n * ⛔ THE KEY IS THE PAIR, AND THAT IS NOT PEDANTRY. MEASURED 2026-09-15 the hour the grant went\n * live: of the account's 401 groups, SEVEN names are listed twice — `Access: Apps and Policies\n * Read`/`Revoke`/`Write`, `Disable ESC Read`/`Write`, `Logs Read`/`Write` — each pair one id\n * scoped `com.cloudflare.api.account` and one scoped `com.cloudflare.api.account.zone`. Keyed by\n * name alone the whole plan fails as ambiguous; keyed by name alone and forgiving, cfhf.py:148's\n * last-one-wins picks a group gated at the wrong scope and the token is refused at first use.\n * The asking ENTRY says which: its `scope` (roles.yaml `groups_scope:`), else its resource.\n *\n * ★ A SERVICE, NOT A TABLE. roles.yaml names groups (`DNS Read`); a role stores IDs. apply-roles.py\n * resolved them per account with a Cloudflare parent credential (cfhf.py:147-148), which agents\n * may not use. So Bao.CloudflareRole asks this service, and the stack chooses the source:\n *\n * a) `permissionGroupsFromEngine` — `GET <mount>/permission-groups` on the Cloudflare engine,\n * answered under the mount's own parent. ★ DEPLOYED AND WIRED 2026-09-15: `bao path-help\n * cloudflare-<account>-platform` lists `^permission-groups$` on plugin v0.0.0-dev, and\n * cloudflare-roles.ts now provides this source to the stack.\n * b) `permissionGroupsFromLiveRoles` — ⚠️ TEMPORARY, AND STILL HERE ON PURPOSE. 2026-09-15: no\n * longer the stack's source, and NOT yet deletable. Two things gate that — (1) the live\n * `result_info` envelope pinned in ../../platform/secrets/vault/plugin/cloudflare/\n * client_result_info.go, whose own header names this deletion as what it gates, and (2)\n * cloudflare-parity.ts, which calls cloudflare-permission-groups-solve.ts directly for the\n * OFFLINE proof against a production export and has no engine to ask. So (b) and the solver\n * stay, one import from being the layer again (cloudflare-roles.ts), until the last step of\n * docs/ledger/openbao-permission-groups-engine-2026-09-15.md is done.\n *\n * ⛔ AN UNKNOWN NAME FAILS THE OPERATION. Dropping it would write a policy one group short, and the\n * engine refuses an empty ID (path_roles.go:181-185) but not a SHORTER list: the minted token is\n * quietly narrower than declared, and the consumer finds out at 403.\n */\nimport * as Context from 'effect/Context';\nimport * as Effect from 'effect/Effect';\nimport type * as HttpClient from 'effect/unstable/http/HttpClient';\nimport { baoRead } from './bao-http.ts';\nimport type { BaoError } from './bao-status.ts';\nimport { groupKey } from './cloudflare-group-scope.ts';\nimport {\n describeProblems,\n observationsOf,\n solveGroups,\n} from './cloudflare-permission-groups-solve.ts';\nimport { type WirePolicy, atEveryScope } from './cloudflare-policy.ts';\nimport { readCloudflareRole } from './cloudflare-role-wire.ts';\nimport type { ExpandedRole } from './cloudflare-roles-expand.ts';\nimport { mountPath } from './mount-form.ts';\n\nexport class CloudflareGroupsError extends Error {\n constructor(\n readonly scope: string,\n readonly problems: readonly string[],\n ) {\n super(`Cloudflare permission groups for ${scope}: ${problems.join('; ')}`);\n this.name = 'CloudflareGroupsError';\n }\n}\n\n/** ⛔ Keyed by `groupKey(name, scope)` throughout — see cloudflare-group-scope.ts. */\ntype Groups = ReadonlyMap<string, string>;\ntype Lookup = Effect.Effect<Groups, BaoError | CloudflareGroupsError, HttpClient.HttpClient>;\n\n/** One group a role asks for: its name, and the scope the entry naming it is gated at. */\nexport interface GroupRef {\n readonly name: string;\n readonly scope: string;\n}\n\nexport class CloudflarePermissionGroups extends Context.Service<\n CloudflarePermissionGroups,\n {\n /** The ID of every ref, keyed by `groupKey` — or a failure naming what did not resolve. */\n readonly resolve: (\n mount: string,\n refs: readonly GroupRef[],\n ) => Effect.Effect<Groups, BaoError | CloudflareGroupsError, HttpClient.HttpClient>;\n }\n>()('homeflare/openbao/CloudflarePermissionGroups') {}\n\nconst pick = (mount: string, groups: Groups, refs: readonly GroupRef[], hint: string) => {\n const unknown = refs.filter((ref) => !groups.has(groupKey(ref.name, ref.scope)));\n if (unknown.length > 0) {\n const problems = unknown.map(\n (ref) => `unknown group ${JSON.stringify(ref.name)} at scope ${ref.scope} — ${hint}`,\n );\n return Effect.fail(new CloudflareGroupsError(mount, problems));\n }\n return Effect.succeed(\n new Map(\n refs.flatMap((ref) => {\n const key = groupKey(ref.name, ref.scope);\n const id = groups.get(key);\n return id === undefined ? [] : [[key, id] as const];\n }),\n ),\n );\n};\n\nconst isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/**\n * ⛔ A 403 IS A MISSING GRANT, AND IT SAYS SO. `BaoError` alone reports `GET /v1/<mount>/\n * permission-groups -> 403: permission denied` — true, and it points nowhere: the path is\n * deployed, the token is valid, and the one thing to change is a policy file. MEASURED 2026-09-15\n * 10:19 on the macmini-admin lane, where `bao write sys/capabilities-self` answered `[deny]` on\n * this exact path. bao-status.ts argues that a failure must name what it is; this is that\n * argument one level up.\n */\nconst deniedHint = (mount: string) =>\n `GET ${mountPath(mount)}/permission-groups answered 403 — this token's policy carries no ` +\n `\\`path \"${mountPath(mount)}/permission-groups\" { capabilities = [\"read\"] }\\` grant. ` +\n 'It is declared in ../platform/secrets/vault/policies/homeflare-admin/' +\n '20-cloudflare-permission-groups.hcl and applies with `alchemy deploy` of this stack';\n\nconst deniedNamed = <A, R>(mount: string, read: Effect.Effect<A, BaoError, R>) =>\n Effect.catchIf(\n read,\n (error) => error.status === 403,\n () => Effect.fail(new CloudflareGroupsError(mount, [deniedHint(mount)])),\n );\n\n/**\n * a) The engine's list, parsed.\n *\n * ★ THE SHAPE IS THE PLUGIN'S OWN, READ FROM ITS SOURCE 2026-09-15:\n * `{\"permission_groups\":[{\"id\",\"name\",\"scopes\"}]}` built row by row in\n * ../../platform/secrets/vault/plugin/cloudflare/path_permission_groups.go:42-54. baoRead takes\n * `data` when the body has one and the whole body otherwise (bao-http.ts:160-167), so either\n * envelope parses.\n * ⚠️ PAGINATION IS THE PLUGIN'S PROBLEM, NOT THIS PARSER'S. client_permission_groups.go:52-119 walks\n * Cloudflare's pages and ends every guard in an error rather than a shorter list, so what arrives\n * here is complete or absent. ⛔ ITS `result_info` GUARDS ARE STILL UNMEASURED against a live\n * envelope (client_result_info.go:22-28) — that capture, not this file, is what gates deleting (b).\n * ⛔ ONE NAME WITH TWO IDS IS NOT AN ERROR; ONE (NAME, SCOPE) WITH TWO IDS IS. cfhf.py:148 built\n * `{g[\"name\"]: g[\"id\"]}` and the LAST duplicate won in silence. MEASURED 2026-09-15 on the live\n * list: seven names ARE listed twice, each pair one account-scoped id and one zone-scoped id, so\n * the plugin's \"does not choose between them\" (path_permission_groups.go:69-70) is the shape in\n * front of us, not a hypothetical. `scopes` tells them apart; the header above says who picks.\n * ⚠️ AN EMPTY `scopes` IS NOT AN ERROR. Such a group is reachable only by the one-id rule below —\n * a name nothing else claims answers wherever it is asked — so a group Cloudflare ships without\n * a scope resolves, and never holds the other 398 hostage.\n */\nconst engineGroups = (mount: string): Lookup =>\n Effect.flatMap(deniedNamed(mount, baoRead(`${mountPath(mount)}/permission-groups`)), (data) => {\n const list = data?.['permission_groups'];\n if (data === undefined) {\n // ⚠️ 404 IS NOW A MOUNT-SHAPED ANSWER, NOT THE EXPECTED ONE. The path IS deployed\n // estate-wide (measured 2026-09-15), so a 404 here means this mount is served by an older\n // plugin binary or BAO_ADDR is not the mini — never \"no groups\".\n const why = 'GET <mount>/permission-groups answered 404 — the endpoint is not deployed here';\n return Effect.fail(new CloudflareGroupsError(mount, [why]));\n }\n if (!Array.isArray(list)) {\n return Effect.fail(new CloudflareGroupsError(mount, ['no permission_groups array in reply']));\n }\n const scoped = new Map<string, string>();\n const idsOf = new Map<string, Set<string>>();\n const problems: string[] = [];\n for (const item of list) {\n const name = isRecord(item) ? item['name'] : undefined;\n const id = isRecord(item) ? item['id'] : undefined;\n const scopes = isRecord(item) ? item['scopes'] : undefined;\n if (\n typeof name !== 'string' ||\n typeof id !== 'string' ||\n !Array.isArray(scopes) ||\n !scopes.every((scope) => typeof scope === 'string')\n ) {\n problems.push('an entry without a string name, id and scopes');\n continue;\n }\n idsOf.set(name, (idsOf.get(name) ?? new Set()).add(id));\n for (const scope of scopes) {\n const key = groupKey(name, scope);\n if (scoped.has(key) && scoped.get(key) !== id) {\n problems.push(\n `ambiguous: ${JSON.stringify(name)} is listed with two ids at scope ${scope}`,\n );\n } else {\n scoped.set(key, id);\n }\n }\n }\n /**\n * ★ A NAME WITH ONE ID NEEDS NO SCOPE, and answers at every scope an entry can ask for: there\n * is nothing to choose between, and 385 of the list's 392 names are in that case. It is only\n * the seven two-id names the asking scope has to separate — and for those, a scope the name\n * is not listed at resolves to nothing and fails by name, never to the other scope's id.\n */\n const sole = new Map(\n [...idsOf].flatMap(([name, ids]) => {\n const only = [...ids];\n return ids.size === 1 && only[0] !== undefined ? [[name, only[0]] as const] : [];\n }),\n );\n return problems.length > 0\n ? Effect.fail(new CloudflareGroupsError(mount, problems))\n : Effect.succeed(new Map([...atEveryScope(sole), ...scoped]));\n });\n\n/**\n * ★ ONE FETCH PER MOUNT PER RUN, not one per role — 586 roles would otherwise ask 586 times.\n * ⚠️ A FAILURE IS CACHED TOO (Effect.cached keeps the Exit), so one sealed-vault 503 fails every\n * role on that mount for the rest of the run. That is the honest outcome for a plan: retrying\n * 586 times against a sealed server would only make the failure slower.\n */\nexport const permissionGroupsFromEngine = () =>\n Effect.sync(() => {\n const cache = new Map<string, Lookup>();\n const cachedFor = (mount: string) =>\n Effect.suspend(() => {\n const hit = cache.get(mountPath(mount));\n if (hit !== undefined) return hit;\n return Effect.flatMap(Effect.cached(engineGroups(mount)), (lookup) => {\n cache.set(mountPath(mount), lookup);\n return lookup;\n });\n });\n return CloudflarePermissionGroups.of({\n resolve: (mount, refs) =>\n Effect.flatMap(cachedFor(mount), (groups) =>\n pick(mount, groups, refs, 'the engine does not list it for this account at that scope'),\n ),\n });\n });\n\n/**\n * b) ⚠️ TEMPORARY, AND NO LONGER THE STACK'S SOURCE SINCE 2026-09-15 — see the header for the two\n * things that gate deleting it. Reads every DECLARED role of an account — no LIST, because an\n * undeclared live role carries IDs with no names to pair them with — and solves the account at\n * once.\n * 🔴 WHAT RETIRED IT, 2026-09-15 10:13: `alchemy deploy` could not create\n * `cloudflare-{<account>,testing}-platform/roles/r2-backup-mini` — `unknown group \"Workers R2\n * Storage Bucket Item Write\" — no live role on account <account> carries it; a NEW group needs\n * the engine endpoint`. That is this source's documented blind spot arriving, not a bug in it.\n *\n * ★ PER ACCOUNT, NOT PER MOUNT, BECAUSE ONE MOUNT CANNOT SOLVE ITSELF. MEASURED on the snapshot:\n * `aimto-app-alchemy-os-create` on the platform mount pairs `Email Routing Rules Write` and\n * `DNS Write` with two unknown IDs, and neither group appears alone on that mount. The dns and\n * security mounts of the same account isolate them. apply-roles.py's map was per account too\n * (apply-roles.py:117).\n */\nexport const permissionGroupsFromLiveRoles = (\n declared: ReadonlyMap<string, readonly ExpandedRole[]>,\n) =>\n Effect.gen(function* () {\n const accountOf = new Map<string, string>();\n const byAccount = new Map<string, ExpandedRole[]>();\n for (const [mount, roles] of declared) {\n for (const role of roles) {\n accountOf.set(mount, role.account);\n byAccount.set(role.account, [...(byAccount.get(role.account) ?? []), role]);\n }\n }\n const solved = new Map<string, Lookup>();\n for (const [account, roles] of byAccount) {\n const solve = Effect.gen(function* () {\n const live = new Map<string, readonly WirePolicy[]>();\n // ★ `Effect.all` over mapped reads, not `Effect.forEach`: oxlint's unicorn/no-array-for-each\n // matches the method NAME and cannot tell Effect's from Array's.\n yield* Effect.all(\n roles.map((role) =>\n Effect.map(readCloudflareRole(role.mount, role.name), (read) => {\n if (read?.policies !== undefined) {\n live.set(`${role.mount}/${role.name}`, read.policies);\n }\n }),\n ),\n { concurrency: 8, discard: true },\n );\n const result = solveGroups(observationsOf(roles, live));\n const problems = describeProblems(result);\n if (problems.length > 0) {\n return yield* Effect.fail(new CloudflareGroupsError(`account ${account}`, problems));\n }\n /**\n * ⚠️ IT ANSWERS AT EVERY SCOPE BECAUSE IT CANNOT SEE ONE. A live role carries ids and no\n * scopes, so the solver learns `name → id` and nothing more; the seven two-id names\n * resolve here to whichever id the estate happens to carry, at whatever scope is asked.\n * That is this source's blind spot, not a second opinion — (a) keys by (name, scope).\n */\n return atEveryScope(result.groups);\n });\n solved.set(account, yield* Effect.cached(solve));\n }\n return CloudflarePermissionGroups.of({\n resolve: (mount, refs) => {\n const account = accountOf.get(mountPath(mount));\n const lookup = account === undefined ? undefined : solved.get(account);\n if (account === undefined || lookup === undefined) {\n const why = 'no declared role on this mount, so no live role can vouch for its groups';\n return Effect.fail(new CloudflareGroupsError(mount, [why]));\n }\n const hint = `no live role on account ${account} carries it; a NEW group needs the engine endpoint`;\n return Effect.flatMap(lookup, (groups) => pick(mount, groups, refs, hint));\n },\n });\n });\n",
15
+ "/**\n * A Cloudflare policy document as the engine stores it — resolution from names, parsing from a\n * read, and the one comparison every Bao.CloudflareRole plan rests on.\n *\n * MEASURED from the engine source (house/platform/secrets/vault/plugin/cloudflare/path_roles.go)\n * and the 2026-09-14 snapshot of all 586 live roles:\n * • `policies` is a STRING field holding a JSON array (path_roles.go:67-72). Each entry is\n * `{effect, resources: map[string]string, permission_groups: [{id}]}` (:25-33).\n * • A read hands back `json.Marshal` of the stored slice (:199-207): no whitespace, entry and\n * group order preserved (Go slices), resource keys sorted (Go maps). Every snapshot role has\n * exactly that shape.\n */\nimport { GROUP_SCOPES, groupKey, scopeOfResource } from './cloudflare-group-scope.ts';\nimport type { DeclaredPolicy } from './cloudflare-roles-expand.ts';\n\nexport interface WirePolicy {\n readonly effect: string;\n readonly resources: Readonly<Record<string, string>>;\n readonly groupIds: readonly string[];\n}\n\n/** A declared entry with its IDs, still carrying how its group order is to be compared. */\nexport interface ResolvedPolicy extends WirePolicy {\n readonly groupOrder: DeclaredPolicy['groupOrder'];\n}\n\n/**\n * One name at every scope — what a source that cannot see scopes can honestly offer.\n *\n * ⚠️ IT IS A WIDENING, AND IT IS THE ASYMMETRY BETWEEN THE TWO SOURCES. The live-roles bootstrap\n * and cloudflare-permission-groups-solve.ts recover `name → id` from roles that carry ids and no\n * scopes, so they cannot tell the seven two-id names apart; the id they learned is offered for\n * whatever scope an entry asks. The engine source keys by (name, scope) and never does this.\n */\nexport const atEveryScope = (byName: ReadonlyMap<string, string>): ReadonlyMap<string, string> =>\n new Map(\n [...byName].flatMap(([name, id]) =>\n Object.values(GROUP_SCOPES).map((scope) => [groupKey(name, scope), id] as const),\n ),\n );\n\n/**\n * Declared names to IDs, or the names `ids` lacks.\n *\n * ⛔ AN ENTRY LOOKS ITS GROUPS UP AT ITS OWN SCOPE (`groupKey`), not by name: a zone entry and an\n * account entry of the same role can name one group and mean two different ids.\n * ★ THE ACCOUNT ENTRY IS SORTED BY ID, EXACTLY AS cfhf.py:180 DID. ⚠️ Plain `sort()`, never\n * `localeCompare`: Python's `sorted()` on str is codepoint order, and JavaScript's default sort\n * is UTF-16 code-unit order, which agree on the lowercase hex these IDs are made of.\n */\nexport const resolvePolicies = (\n declared: readonly DeclaredPolicy[],\n ids: ReadonlyMap<string, string>,\n): { readonly policies: ResolvedPolicy[]; readonly missing: string[] } => {\n const missing = new Set<string>();\n const policies = declared.map((policy) => {\n /**\n * ⛔ THE ENTRY'S OWN WORD BEATS ITS RESOURCE. `scope` is roles.yaml `groups_scope:` reaching\n * here unchanged; `scopeOfResource` is the default for the 639 of 643 references that say\n * nothing. The four that do are `logs`/`logs-read` on both security mounts — ACCOUNT roles\n * whose live entry carries the ZONE-scoped `Logs` ids, which is how Cloudflare writes \"all\n * zones from an account\" (cf-mint-analytics-tokens.py:155-158).\n */\n const scope = policy.scope ?? scopeOfResource(policy.resource);\n const groupIds = policy.groups.flatMap((name) => {\n const id = ids.get(groupKey(name, scope));\n if (id === undefined) missing.add(name);\n return id === undefined ? [] : [id];\n });\n return {\n effect: policy.effect,\n groupIds: policy.groupOrder === 'by-id' ? [...groupIds].sort() : groupIds,\n groupOrder: policy.groupOrder,\n resources: { [policy.resource]: '*' },\n };\n });\n return { missing: [...missing].sort(), policies };\n};\n\nconst isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst stringMap = (value: unknown): Readonly<Record<string, string>> | undefined => {\n if (!isRecord(value)) return undefined;\n const out: Record<string, string> = {};\n for (const [key, item] of Object.entries(value)) {\n if (typeof item !== 'string') return undefined;\n out[key] = item;\n }\n return out;\n};\n\nconst entryOf = (entry: unknown): WirePolicy | undefined => {\n if (!isRecord(entry) || typeof entry['effect'] !== 'string') return undefined;\n const resources = stringMap(entry['resources']);\n const groups = entry['permission_groups'];\n if (resources === undefined || !Array.isArray(groups)) return undefined;\n const groupIds = groups.flatMap((group) =>\n isRecord(group) && typeof group['id'] === 'string' ? [group['id']] : [],\n );\n return groupIds.length === groups.length\n ? { effect: entry['effect'], groupIds, resources }\n : undefined;\n};\n\n/**\n * The live `policies` string parsed, or undefined when it is not a policy document.\n *\n * ⚠️ UNDEFINED IS NOT ABSENCE. It means the role exists and its policies could not be read, which\n * compares unequal to every declaration — so it plans `update`, and reconcile's re-read refuses\n * to call a write that still reads back like that a success.\n */\nexport const parseLivePolicies = (raw: unknown): WirePolicy[] | undefined => {\n if (typeof raw !== 'string') return undefined;\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n if (!Array.isArray(parsed)) return undefined;\n const policies = parsed.map(entryOf);\n return policies.every((policy): policy is WirePolicy => policy !== undefined)\n ? policies\n : undefined;\n};\n\n/**\n * The `policies` field for a write — the array cfhf.py:174-180 built, serialized.\n *\n * ⚠️ NOT BYTE-IDENTICAL TO WHAT PYTHON SENT, AND IT NEED NOT BE. `json.dumps` put a space after\n * every `:` and `,`; this emits none. The engine unmarshals the string into structs\n * (path_roles.go:161-166) and stores those, never the text, so the stored role and the read\n * that diff compares are the same either way.\n */\nexport const policiesJson = (policies: readonly WirePolicy[]) =>\n JSON.stringify(\n policies.map((policy) => ({\n effect: policy.effect,\n resources: policy.resources,\n permission_groups: policy.groupIds.map((id) => ({ id })),\n })),\n );\n\nconst sameList = (a: readonly string[], b: readonly string[]) =>\n a.length === b.length && a.every((item, i) => item === b[i]);\n\nconst sameResources = (\n a: Readonly<Record<string, string>>,\n b: Readonly<Record<string, string>>,\n) => {\n const keys = Object.keys(a);\n return keys.length === Object.keys(b).length && keys.every((key) => a[key] === b[key]);\n};\n\n/**\n * ⛔ THE POLICY EQUALITY.\n *\n * • Entry count and entry ORDER must match: cfhf.py always wrote the scope entry first and the\n * account entry second (cfhf.py:174-180).\n * • Within an entry: effect, the resource map, and the group IDs — IN ORDER for a `declared`\n * entry, because cfhf.py:175 kept yaml order; AS A SORTED LIST for a `by-id` entry, because\n * cfhf.py:180 sorted it, so its order never carried meaning.\n *\n * ⚠️ NOT ORDER-INSENSITIVE EVERYWHERE, ON PURPOSE. Cloudflare treats a group list as a set, so a\n * reordered `declared` entry mints an identical token — and still plans `update` here. The\n * Python would have rewritten it as well, and an adoption that is noop only up to reordering\n * would not prove the port exact.\n */\nexport const samePolicies = (want: readonly ResolvedPolicy[], have: readonly WirePolicy[]) =>\n want.length === have.length &&\n want.every((w, i) => {\n const h = have[i];\n if (h === undefined || w.effect !== h.effect || !sameResources(w.resources, h.resources)) {\n return false;\n }\n return w.groupOrder === 'by-id'\n ? sameList([...w.groupIds].sort(), [...h.groupIds].sort())\n : sameList(w.groupIds, h.groupIds);\n });\n",
16
+ "/**\n * The OpenBao calls Bao.CloudflareRole and the live-roles bootstrap make — read, write and delete\n * of `<mount>/roles/<name>`. Split out so each can be tested against fake-bao.\n */\nimport * as Effect from 'effect/Effect';\nimport type * as HttpClient from 'effect/unstable/http/HttpClient';\nimport { baoDelete, baoRead, baoWrite } from './bao-http.ts';\nimport type { BaoError } from './bao-status.ts';\nimport { type LiveCloudflareRole, liveRoleOf, rolePath } from './cloudflare-role-form.ts';\n\n/**\n * The live role, or undefined when the engine answers 404.\n *\n * ⚠️ A MISSING MOUNT AND A MISSING ROLE BOTH READ AS UNDEFINED — both are a 404 from OpenBao, the\n * first as \"no handler for route\". That is proxmox-role.ts's ⚠️ in full: the WRITE is what tells\n * them apart, and it lets OpenBao's own `errors` through. A 403 or 503 is never absence\n * (bao-status.ts).\n */\nexport const readCloudflareRole = (\n mount: string,\n name: string,\n): Effect.Effect<LiveCloudflareRole | undefined, BaoError, HttpClient.HttpClient> =>\n Effect.map(baoRead(rolePath(mount, name)), (data) =>\n data === undefined ? undefined : liveRoleOf(data),\n );\n\n/** ★ `PUT`, which is what `bao write` sent (bao-http.ts). A 404 here fails — see readCloudflareRole. */\nexport const writeCloudflareRole = (\n mount: string,\n name: string,\n body: Readonly<Record<string, string>>,\n) => baoWrite('PUT', rolePath(mount, name), body);\n\n/**\n * Idempotent as Alchemy requires — already gone (404) is success, a refusal is not.\n *\n * ⚠️ THE ENGINE'S DELETE IS ONE STORAGE DELETE (path_roles.go:214-216). Tokens the role already\n * minted are leases, and they live to their own expiry.\n */\nexport const deleteCloudflareRole = (mount: string, name: string) =>\n baoDelete(rolePath(mount, name));\n",
17
+ "/**\n * Props, attributes and field comparison for Bao.CloudflareRole — the resource is\n * cloudflare-role.ts, the policy document rules are cloudflare-policy.ts.\n *\n * MEASURED from the engine (house/platform/secrets/vault/plugin/cloudflare/path_roles.go) and the\n * 2026-09-14 snapshot of all 586 live roles:\n * • A role is name, description, policies, ttl and max_ttl AND NOTHING ELSE (path_roles.go:14-20\n * stores those; :204-210 returns them). Every snapshot read carries exactly those five keys, so\n * `differences` below covers the whole server-side object — no unmanaged field can drift\n * behind a green plan.\n * • ttl and max_ttl are TypeDurationSecond (:73-80) and read back as integer seconds.\n * • ⚠️ A WRITE MERGES, IT DOES NOT REPLACE (:117-146). The handler loads the stored role and\n * overwrites only the fields present — the opposite of the proxmox engine, where an omitted\n * field is zeroed. writeBody sends all four anyway, so a write always lands the whole\n * declaration and can never leave an old description behind a new policy.\n *\n * ⛔ NO SECRET IN PROPS OR ATTRIBUTES, AND THERE IS NONE TO LEAK. A role is permission-group IDs, a\n * zone or account resource, a sentence and two TTLs. The parent token lives at `<mount>/config`\n * and a minted token at `<mount>/creds/<name>`; nothing in this family reads either path. Keep\n * it that way — Alchemy persists attributes unencrypted (see policy.ts).\n */\nimport {\n type ResolvedPolicy,\n type WirePolicy,\n parseLivePolicies,\n policiesJson,\n samePolicies,\n} from './cloudflare-policy.ts';\nimport type { DeclaredPolicy } from './cloudflare-roles-expand.ts';\nimport { sha256 } from './digest.ts';\nimport { mountPath, parseDuration } from './mount-form.ts';\n\nexport interface BaoCloudflareRoleProps {\n /** Mount of the Cloudflare secrets engine, e.g. `cloudflare-<account>-dns`. */\n mount: string;\n /** Role name. ⛔ Consumers mint by it — see cloudflare-roles-expand.ts before changing one. */\n name: string;\n /** The description as written: a zone role's carries its ` (zone: <name>)` suffix. */\n description: string;\n /** Default lease TTL, e.g. `5m`. */\n ttl: string;\n /** Maximum lease TTL, e.g. `1h`. */\n maxTtl: string;\n /**\n * Policy entries, groups by NAME. The provider resolves them through CloudflarePermissionGroups\n * at diff and reconcile time; an ID is never a prop, so a plan can never persist a guessed one.\n */\n policies: DeclaredPolicy[];\n}\n\nexport interface BaoCloudflareRoleAttributes {\n mount: string;\n name: string;\n description: string;\n /** Live ttl in seconds, as the engine stores it. */\n ttl: number;\n /** Live max_ttl in seconds. */\n maxTtl: number;\n /** Live `policies` JSON — group IDs and a resource, never a credential. */\n policies: string;\n /** SHA-256 of the fields above — safe to persist; see policy.ts. */\n digest: string;\n}\n\n/** One live role, as a read returns it. */\nexport interface LiveCloudflareRole {\n readonly name: string;\n readonly description: string;\n readonly ttl: number | undefined;\n readonly maxTtl: number | undefined;\n readonly policiesText: string;\n /** Undefined when `policies` is not a policy document — see parseLivePolicies. */\n readonly policies: readonly WirePolicy[] | undefined;\n}\n\nexport interface RoleDifference {\n readonly field: 'description' | 'max_ttl' | 'policies' | 'ttl';\n readonly want: string;\n readonly have: string;\n}\n\n/** `cloudflare-<account>-dns/roles/homeflare-dev-dns-read` — read, write and delete use it. */\nexport const rolePath = (mount: string, name: string) => `${mountPath(mount)}/roles/${name}`;\n\nconst seconds = (value: unknown) =>\n typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n\n/** A read's `data`, shaped. Lenient on purpose: a malformed field compares unequal, loudly. */\nexport const liveRoleOf = (data: Readonly<Record<string, unknown>>): LiveCloudflareRole => ({\n description: typeof data['description'] === 'string' ? data['description'] : '',\n maxTtl: seconds(data['max_ttl']),\n name: typeof data['name'] === 'string' ? data['name'] : '',\n policies: parseLivePolicies(data['policies']),\n policiesText: typeof data['policies'] === 'string' ? data['policies'] : '',\n ttl: seconds(data['ttl']),\n});\n\n/**\n * ⚠️ Every value is taken from `live`, the prop only standing in for a missing `name` — the same\n * split the sibling families use, so a read never launders the declaration back as \"current\".\n */\nexport const attributesOf = (\n props: Pick<BaoCloudflareRoleProps, 'mount' | 'name'>,\n live: LiveCloudflareRole,\n): BaoCloudflareRoleAttributes => {\n const attrs = {\n description: live.description,\n maxTtl: live.maxTtl ?? 0,\n mount: mountPath(props.mount),\n name: live.name || props.name,\n policies: live.policiesText,\n ttl: live.ttl ?? 0,\n };\n return { ...attrs, digest: sha256(JSON.stringify(attrs)) };\n};\n\n/**\n * Why reconcile must refuse this declaration, or undefined.\n *\n * ⛔ AN UNPARSEABLE TTL IS A REFUSAL, NOT A ZERO: it can never equal the live seconds, so it would\n * plan `update` forever. ⛔ AN EMPTY POLICY LIST OR AN ENTRY WITH NO GROUPS IS A REFUSAL TOO — the\n * engine rejects both (path_roles.go:147-149, :175-180), and saying so before the write names the\n * declaration instead of quoting the server.\n */\nexport const refusalOf = (props: BaoCloudflareRoleProps): string | undefined => {\n const bad = [\n ...(parseDuration(props.ttl) === undefined ? [`ttl=${props.ttl}`] : []),\n ...(parseDuration(props.maxTtl) === undefined ? [`maxTtl=${props.maxTtl}`] : []),\n ];\n if (bad.length > 0) return `unparseable duration ${bad.join(', ')} (use 30s / 5m / 1h / 1d)`;\n if (props.policies.length === 0) return 'no policy entries';\n if (props.policies.some((policy) => policy.groups.length === 0)) return 'a policy with no groups';\n return undefined;\n};\n\n/**\n * The body for `PUT <mount>/roles/<name>` — the four `k=v` pairs apply-roles.py:157-162 sent.\n *\n * ★ THE TTLs GO AS THE DECLARED TEXT (`5m`), which is what `bao write ttl=5m` put in the JSON body,\n * and TypeDurationSecond parses it server-side. `policies` is a string because the field is a\n * string (path_roles.go:67-68), not because the engine wants the whole body stringly-typed.\n */\nexport const writeBody = (props: BaoCloudflareRoleProps, policies: readonly WirePolicy[]) => ({\n description: props.description,\n max_ttl: props.maxTtl,\n policies: policiesJson(policies),\n ttl: props.ttl,\n});\n\nconst sameTtl = (want: string, have: number | undefined) => {\n const wantSeconds = parseDuration(want);\n return wantSeconds !== undefined && wantSeconds === have;\n};\n\n/**\n * Every field where live differs from the resolved declaration. Empty means noop.\n *\n * ⛔ apply-roles.py --verify COMPARED NONE OF THESE. It lists `<mount>/roles` and reports names\n * only: MISSING when roles.yaml has a role the vault lacks, UNTRACKED for the reverse\n * (apply-roles.py:136-141, :167-173, :179-185). It never read a role. So this equality is new: the\n * four fields --apply WRITES (apply-roles.py:157-162), compared the way the engine reads them back\n * — durations as seconds, description exactly, policies by the rules in samePolicies.\n */\nexport const differences = (\n props: Pick<BaoCloudflareRoleProps, 'description' | 'maxTtl' | 'ttl'>,\n policies: readonly ResolvedPolicy[],\n live: LiveCloudflareRole,\n): RoleDifference[] => {\n const out: RoleDifference[] = [];\n if (props.description !== live.description) {\n out.push({ field: 'description', have: live.description, want: props.description });\n }\n if (!sameTtl(props.ttl, live.ttl)) {\n out.push({ field: 'ttl', have: String(live.ttl), want: props.ttl });\n }\n if (!sameTtl(props.maxTtl, live.maxTtl)) {\n out.push({ field: 'max_ttl', have: String(live.maxTtl), want: props.maxTtl });\n }\n if (live.policies === undefined || !samePolicies(policies, live.policies)) {\n out.push({ field: 'policies', have: live.policiesText, want: policiesJson(policies) });\n }\n return out;\n};\n",
18
+ "/**\n * An OpenBao secrets engine mount — path, type, description and lease TTLs.\n *\n * ⛔ NO SECRET IN PROPS OR ATTRIBUTES — see policy.ts for why. This declares mount\n * SHAPE only: names, types, TTL strings and a digest. KV values and CA material live\n * in OpenBao, not Alchemy state.\n *\n * ★ `defaultRemovalPolicy: 'retain'` — disabling a mount destroys every secret under it.\n * Opt in with `.pipe(RemovalPolicy.destroy())`; see resource.ts in house/proxmox.\n */\nimport { Resource } from 'alchemy';\nimport { isResolved } from 'alchemy/Diff';\nimport * as Provider from 'alchemy/Provider';\nimport * as Effect from 'effect/Effect';\nimport type * as HttpClient from 'effect/unstable/http/HttpClient';\nimport {\n type BaoMountAttributes,\n type BaoMountProps,\n attributesOf,\n matches,\n mountPath,\n wantsTune,\n} from './mount-form.ts';\nimport { disableMount, enableMount, readMountData, tuneMount } from './mount-wire.ts';\n\nexport type { BaoMountAttributes, BaoMountProps };\n\nexport interface BaoMount extends Resource<\n 'Bao.Mount',\n BaoMountProps,\n BaoMountAttributes,\n never,\n HttpClient.HttpClient\n> {}\n\nexport const BaoMount = Resource<BaoMount>('Bao.Mount', {\n defaultRemovalPolicy: 'retain',\n});\n\nconst readMount = (props: BaoMountProps) =>\n Effect.gen(function* () {\n const live = yield* readMountData(props.path);\n if (live === undefined) return undefined;\n return attributesOf(props, live);\n });\n\nexport const BaoMountProvider = () =>\n Provider.effect(\n BaoMount,\n Effect.succeed(\n BaoMount.Provider.of({\n /**\n * ⛔ `bao secrets list` IS NOT A LIST OF THINGS THIS OWNS. Every mount in the\n * namespace answers, including ones created by hand at bringup. Returning them\n * would invite Alchemy to adopt — and then disable — mounts it never created.\n * ⚠️ mount-wire.ts does read that listing, for one question only: whether a mount a 400\n * named is really absent. It never becomes a list of resources.\n */\n list: () => Effect.succeed([]),\n\n read: Effect.fn(function* ({ olds }) {\n return yield* readMount(olds);\n }),\n\n diff: Effect.fn(function* ({ news, output }) {\n if (output === undefined || !isResolved(news)) return undefined;\n const live = yield* readMount(news);\n if (live === undefined) return { action: 'update' } as const;\n /**\n * ⛔ A CHANGED `type` IS A REPLACE, AND THIS FAMILY'S DELETE IS A MOUNT DISABLE — the\n * destruction of every secret under the mount. `kv/` is the estate's entire\n * secret shelf, so the question \"does `retain` guard the delete half of a REPLACE, or\n * only an orphan?\" is the difference between a typo and a catastrophe.\n * ★ IT GUARDS BOTH — VERIFIED IN ALCHEMY'S OWN SOURCE, not assumed. Apply.ts:2164-2173:\n * \"Honor `retain` for the old generation of a replacement, mirroring the orphan-delete\n * path above\", with `retainOldGeneration = node.removalPolicy === 'retain'`, and when\n * it is true the engine logs \"Retaining replaced resource (removal policy: retain)\"\n * and skips the delete entirely. Every mount this package declares carries the retain\n * default (see the ★ on the resource below), so a wrong `type` string cannot disable a\n * live mount. What it CAN do is try to enable a second mount at an occupied path,\n * which OpenBao refuses loudly — a failed deploy, not a lost shelf.\n * ⚠️ THAT SAFETY IS THE DECORATION'S, NOT THIS LINE'S. A caller who opts into\n * `.pipe(RemovalPolicy.destroy())` gets the destructive replace, which is the correct\n * meaning of opting in and is worth knowing before you type it on a mount.\n */\n if (live.type !== news.type) return { action: 'replace' } as const;\n if (matches(live, news)) return { action: 'noop' } as const;\n return { action: 'update' } as const;\n }),\n\n /**\n * ★ A REFUSED ENABLE OR TUNE FAILS THE EFFECT WITH OpenBao's OWN `errors`, naming the\n * method and path — the job the per-command exit-code checks used to do by hand.\n */\n reconcile: Effect.fn(function* ({ news }) {\n let live = yield* readMount(news);\n if (live === undefined) {\n yield* enableMount(news);\n if (wantsTune(news)) yield* tuneMount(news);\n } else if (!matches(live, news)) {\n if (live.type !== news.type) {\n return yield* Effect.die(\n new Error(\n `Bao.Mount ${mountPath(news.path)}: live type ${live.type} != ${news.type}. ` +\n 'Secrets engine type is immutable — replace manually.',\n ),\n );\n }\n yield* tuneMount(news);\n }\n live = yield* readMount(news);\n if (live === undefined) {\n return yield* Effect.die(\n new Error(\n `Bao.Mount ${mountPath(news.path)}: write returned no error but the mount is still absent.`,\n ),\n );\n }\n return live;\n }),\n\n /**\n * ⛔ DISABLING A MOUNT DESTROYS EVERY SECRET UNDER IT. Idempotent as Alchemy\n * requires — already gone is success — but retain is the default for a reason.\n */\n delete: Effect.fn(function* ({ output }) {\n yield* disableMount(output.path);\n return undefined;\n }),\n }),\n ),\n );\n",
19
+ "/**\n * The OpenBao calls Bao.Mount makes — read one mount, enable, tune, disable. Split from mount.ts so\n * the classification of a MISSING mount can be tested against a fake server.\n *\n * ★ ENDPOINTS, READ FROM openbao v2.6.2 api/sys_mounts.go — the client the CLI used:\n * read GET sys/mounts/<path> (MountInfo, :254) — what `bao read sys/mounts/<path>` hit\n * enable POST sys/mounts/<path> (Mount, :57) — `bao secrets enable`\n * tune POST sys/mounts/<path>/tune (TuneMount, :201) — `bao secrets tune`\n * disable DELETE sys/mounts/<path> (Unmount, :79) — `bao secrets disable`\n */\nimport * as Effect from 'effect/Effect';\nimport { baoDelete, baoRead, baoWrite } from './bao-http.ts';\nimport { BaoError } from './bao-status.ts';\nimport {\n type BaoMountProps,\n enableBody,\n mountPath,\n readPath,\n tuneBody,\n tunePath,\n} from './mount-form.ts';\n\n/**\n * Confirm a mount is absent from the mount table, or give back the error that asked.\n *\n * ⛔ A MISSING MOUNT IS NOT A 404, IT IS A 400 — AND THAT MADE A NEW MOUNT UNCREATABLE, SILENTLY,\n * THE SAME WAY A NEW POLICY WAS (b3c18a33e). `handleReadMount` answers a path with no mount with a\n * bare error response, \"No secret engine mount at <path>\" (vault/logical_system.go:1171-1178), and\n * RespondErrorCommon turns an error response with no coded error into 400\n * (sdk/logical/response_util.go). The CLI therefore exited 2 with that text, which was not\n * `No value found at`, so every read of an undeclared-yet mount FAILED and reconcile never\n * reached `secrets enable`. All four declared mounts already exist, which is the only reason\n * nothing broke. REASONED FROM SOURCE, NOT MEASURED — no read of a missing mount was run.\n * ★ SO A 400 IS SETTLED BY A SECOND, STATUS-CLASSIFIED QUESTION rather than by matching that text:\n * `GET sys/mounts` lists the namespace's mount table, keyed `<path>/`. Only a 200 table WITHOUT\n * the key is absence. A listed mount, a refused listing, or a 404 listing leaves the original 400\n * standing — \"I could not tell\" is an error, never a create.\n * ⚠️ THE LISTING NEEDS `read` ON `sys/mounts`. The admin lane has it\n * (policies/homeflare-admin/live-captured-2026-09-02.hcl:20). A narrower token still fails, which\n * is what it did before.\n */\nconst absentFromTable = (path: string, asked: BaoError) =>\n baoRead('sys/mounts').pipe(\n // ⚠️ A listing that fails keeps the 400, and says why it could not settle it.\n Effect.mapError(\n (listing) =>\n new BaoError(asked.status, asked.method, asked.path, [\n ...asked.errors,\n `sys/mounts could not confirm the mount is absent: ${listing.message}`,\n ]),\n ),\n Effect.flatMap((table) =>\n table !== undefined && !Object.hasOwn(table, `${mountPath(path)}/`)\n ? Effect.succeed(undefined)\n : Effect.fail(asked),\n ),\n );\n\n/** One mount's config (`type`, `description`, `config.*_lease_ttl`), or undefined when absent. */\nexport const readMountData = (path: string) =>\n baoRead(readPath(path)).pipe(\n Effect.catchIf(\n (error) => error.status === 400,\n (error) => absentFromTable(path, error),\n ),\n );\n\nexport const enableMount = (props: BaoMountProps) =>\n baoWrite('POST', readPath(props.path), enableBody(props));\n\nexport const tuneMount = (props: BaoMountProps) =>\n baoWrite('POST', tunePath(props.path), tuneBody(props));\n\n/**\n * ⚠️ THERE IS NO FORCE. The old delete ran `bao secrets disable -force`, and v2.6.2's\n * command/secrets_disable.go defines no `-force` flag, so that argv would have been refused at\n * flag parsing. `Unmount` is a plain DELETE (api/sys_mounts.go:79); nothing else was ever sent.\n */\nexport const disableMount = (path: string) => baoDelete(readPath(path));\n",
20
+ "/**\n * An OpenBao PKI role — the policy a future issuance is checked against, and nothing else.\n *\n * ⛔ THE ROLE IS THE POLICY, NEVER THE CERTIFICATE. Nothing here touches `pki/issue/*` (which\n * RETURNS a private key) or `pki/root/generate/*` (which CREATES one). Every value this\n * resource stores is a domain name, a flag, a bit count or a TTL — safe to sit in Alchemy's\n * UNENCRYPTED Postgres state. See policy.ts for why that line is load-bearing here.\n *\n * ⛔ `bao write pki/roles/<name>` REPLACES THE WHOLE ROLE — it is not a merge. OpenBao builds\n * a fresh role entry from the request, so every field left out reverts to its SCHEMA\n * DEFAULT. MEASURED 2026-09-13 against the live OpenBao 2.6.2, from its own\n * `sys/internal/specs/openapi` cross-read against all eight live `pki/roles/*`, four of\n * those defaults are WIDER than what this estate actually runs:\n *\n * allow_ip_sans default true — false on 5 of the 8 roles\n * allow_localhost default true — false on 6 of the 8\n * allow_wildcard_certificates default true — false on 6 of the 8\n * cn_validations default [\"email\",\"hostname\"] — [\"hostname\"] on 5 of the 8\n *\n * A resource that managed only the eleven fields the brief named would therefore, on any\n * unrelated TTL edit, silently re-open wildcards and localhost on six production roles.\n * That is why those four are props in pki-role-form.ts though they were not in the brief,\n * and why their default there is the CLOSED value rather than OpenBao's open one.\n *\n * ★ NARROWING IS THE FAIL-SAFE DIRECTION. A default tighter than OpenBao's can only refuse a\n * certificate that should have been issued — loud, and cured by declaring the field. A\n * default looser than OpenBao's issues one that should have been refused, in silence.\n *\n * ⚠️ WHAT WAS NOT MEASURED. Only reads were run against the live engine; no `bao write`\n * happened, so nothing below is a verified round-trip. Two consequences carried into the\n * form file: every live role has `ext_key_usage: []`, so the stored casing of a non-empty\n * list was never observed (the compare folds case because OpenBao matches these\n * case-insensitively at issuance); and every live role has exactly ONE allowed domain, so\n * whether OpenBao preserves list order was never observed either (the compare sorts,\n * because allowed_domains is a set). Both choices can only mask a cosmetic difference,\n * never a policy one — but neither is a measurement.\n *\n * ★ `defaultRemovalPolicy: 'retain'` — see the delete comment below. Opt in with\n * `.pipe(RemovalPolicy.destroy())`; see resource.ts in house/proxmox.\n */\nimport { Resource } from 'alchemy';\nimport { isResolved } from 'alchemy/Diff';\nimport * as Provider from 'alchemy/Provider';\nimport * as Effect from 'effect/Effect';\nimport type * as HttpClient from 'effect/unstable/http/HttpClient';\nimport { baoDelete, baoRead, baoWrite } from './bao-http.ts';\nimport { mountPath, parseDuration } from './mount-form.ts';\nimport {\n type BaoPkiRoleAttributes,\n type BaoPkiRoleProps,\n attributesOf,\n matches,\n rolePath,\n writeBody,\n} from './pki-role-form.ts';\n\nexport type { BaoPkiRoleAttributes, BaoPkiRoleProps };\n\nexport interface BaoPkiRole extends Resource<\n 'Bao.PkiRole',\n BaoPkiRoleProps,\n BaoPkiRoleAttributes,\n never,\n HttpClient.HttpClient\n> {}\n\nexport const BaoPkiRole = Resource<BaoPkiRole>('Bao.PkiRole', {\n defaultRemovalPolicy: 'retain',\n});\n\nconst readRole = (props: BaoPkiRoleProps) =>\n Effect.gen(function* () {\n const live = yield* baoRead(rolePath(props));\n if (live === undefined) return undefined;\n return attributesOf(props, live);\n });\n\n/** Duration props `bao write` must never see — reconcile turns these into a refusal. */\nconst badDurations = (props: BaoPkiRoleProps) =>\n [['ttl', props.ttl] as const, ['maxTtl', props.maxTtl] as const].filter(\n ([, text]) => parseDuration(text) === undefined,\n );\n\nexport const BaoPkiRoleProvider = () =>\n Provider.effect(\n BaoPkiRole,\n Effect.succeed(\n BaoPkiRole.Provider.of({\n /**\n * ⛔ `bao list pki/roles` IS NOT A LIST OF THINGS THIS OWNS. All eight roles in the\n * live engine answer it, and every one of them predates this resource. Returning\n * them would invite Alchemy to adopt — and then delete — roles it never created.\n */\n list: () => Effect.succeed([]),\n\n read: Effect.fn(function* ({ olds }) {\n return yield* readRole(olds);\n }),\n\n /**\n * ⛔ IT COMPARES A DIGEST OF THE LIVE ROLE, NOT THE STORED ONE. The attributes fed to\n * `matches` are always freshly derived from a live read in this same operation, so\n * a role widened by hand in the OpenBao UI still reads as drift — which is the\n * whole reason the check-* gates exist. Comparing one digest rather than sixteen\n * fields is also what stops a field being added to props and quietly forgotten in\n * the comparison.\n */\n diff: Effect.fn(function* ({ news, output }) {\n if (output === undefined || !isResolved(news)) return undefined;\n /**\n * ⚠️ A ROLE IS IDENTIFIED BY MOUNT **AND** NAME. Moving a declaration to another\n * engine is a different object under a different CA, never an in-place edit —\n * `replace`, so Alchemy creates the new one and retires the old.\n */\n if (mountPath(news.mount ?? 'pki') !== output.mount)\n return { action: 'replace' } as const;\n /**\n * ⚠️ A DECLARATION reconcile WOULD REFUSE MUST NEVER PLAN AS noop. An empty\n * allowedDomains, or a duration OpenBao cannot parse, can compare equal to a\n * live role already in that state — and a noop there hides the refusal behind a\n * clean plan until the next deploy. Route it to reconcile, which says why.\n */\n if (news.allowedDomains.length === 0 || badDurations(news).length > 0) {\n return { action: 'update' } as const;\n }\n const live = yield* readRole(news);\n if (live === undefined) return { action: 'update' } as const;\n return matches(live, news)\n ? ({ action: 'noop' } as const)\n : ({ action: 'update' } as const);\n }),\n\n reconcile: Effect.fn(function* ({ news }) {\n const path = rolePath(news);\n /**\n * ⛔ AN EMPTY allowedDomains IS A REFUSAL, NOT AN EMPTY ROLE. `allow_any_name` is\n * false on every live role, so a role with no allowed domains can issue NOTHING —\n * and an unresolved or mistyped prop reads exactly like an author who meant it.\n * policy.ts refuses an empty fragment directory for the same reason.\n */\n if (news.allowedDomains.length === 0) {\n return yield* Effect.die(\n new Error(\n `Bao.PkiRole ${path}: allowedDomains is empty. With allow_any_name false ` +\n 'that role can issue no certificate at all.',\n ),\n );\n }\n const bad = badDurations(news);\n if (bad.length > 0) {\n return yield* Effect.die(\n new Error(\n `Bao.PkiRole ${path}: ${bad.map(([k, v]) => `${k}=${v}`).join(', ')} ` +\n 'is not a duration OpenBao parses (expect 30m, 720h, 8760h or 0).',\n ),\n );\n }\n const live = yield* readRole(news);\n if (live === undefined || !matches(live, news)) {\n yield* baoWrite('PUT', path, writeBody(news));\n }\n const after = yield* readRole(news);\n if (after === undefined) {\n return yield* Effect.die(\n new Error(`Bao.PkiRole ${path}: write reported success but the role is absent.`),\n );\n }\n /**\n * ⛔ THE WRITE IS NOT TRUSTED, IT IS RE-READ AND RE-COMPARED. A write succeeds (2xx)\n * for a field it accepted but stored differently — the live worry being the\n * comma-joined list values in writeBody, whose splitting is documented for\n * allowed_domains and ext_key_usage but NOT for cn_validations. Without this\n * check a role could land with `cn_validations: [\"email,hostname\"]`, report\n * success, and then refuse every renewal at issuance time instead.\n */\n if (!matches(after, news)) {\n return yield* Effect.die(\n new Error(\n `Bao.PkiRole ${path}: wrote cleanly but the role read back different. ` +\n 'OpenBao stored something other than what was declared — inspect with ' +\n `\\`bao read ${path}\\` before retrying.`,\n ),\n );\n }\n return after;\n }),\n\n /**\n * ⛔ DELETING A ROLE DOES NOT REVOKE THE CERTIFICATES IT ISSUED — they stay valid to\n * their own expiry, so this is quiet now and loud much later: every renewal through\n * the role fails once the current certificate runs out. MEASURED: the live roles\n * carry ttl 604800 to 31536000 seconds, so the fuse on `rpi5-otel-client` is a full\n * year. Idempotent as Alchemy requires — already gone is success — but retain is\n * the default for exactly this reason.\n */\n delete: Effect.fn(function* ({ output }) {\n yield* baoDelete(rolePath(output));\n return undefined;\n }),\n }),\n ),\n );\n",
21
+ "import { sha256 } from './digest.ts';\nimport { mountPath, parseDuration, ttlSeconds } from './mount-form.ts';\n\n/** ★ MEASURED: the OpenAPI enum for `key_type` is exactly these four, lowercase. */\nexport type BaoPkiKeyType = 'any' | 'ec' | 'ed25519' | 'rsa';\n\n/** ★ MEASURED: live roles carry `[\"hostname\"]` or the default `[\"email\",\"hostname\"]`. */\nexport type BaoPkiCnValidation = 'disabled' | 'email' | 'hostname';\n\nexport interface BaoPkiRoleProps {\n /** Role name, as `pki/roles/{name}` takes it. */\n name: string;\n /** Mount holding the engine. Defaults to `pki` — the only PKI mount live today. */\n mount?: string;\n /**\n * Domains this role may issue for.\n *\n * ⛔ AN EMPTY LIST IS REFUSED IN reconcile, NOT WRITTEN. With `allow_any_name` false — the\n * default, and what all eight live roles use — a role with no allowed domains can issue\n * nothing, so a mistyped or unresolved prop would quietly brick every renewal under it.\n */\n allowedDomains: readonly string[];\n /** Permit `foo.<allowed domain>`. Default false. */\n allowSubdomains?: boolean;\n /** Permit the allowed domain itself as the CN. Default false. */\n allowBareDomains?: boolean;\n /** Treat `*` inside an allowed domain as a glob. Default false. */\n allowGlobDomains?: boolean;\n /**\n * Default certificate lifetime, as a duration string (`720h`).\n *\n * ⚠️ WRITTEN AS A DURATION, READ BACK AS SECONDS — `ttl` and `max_ttl` both carry\n * `format: seconds` in the OpenAPI and come back as integers (`2592000`). The prop\n * string reaches `bao write` verbatim; the compare happens in seconds. See canonical().\n */\n ttl: string;\n /** Ceiling on a requested lifetime, as a duration string (`8760h`). */\n maxTtl: string;\n /** Key algorithm. Default `rsa` — OpenBao's own default. */\n keyType?: BaoPkiKeyType;\n /** Key size; `0` means \"engine default for keyType\". rsa 2048/3072/4096, ec 224/256/384/521. */\n keyBits?: number;\n /** Flag certificates for TLS server auth. ⚠️ OpenBao defaults this TRUE. */\n serverFlag?: boolean;\n /** Flag certificates for TLS client auth. ⚠️ OpenBao defaults this TRUE. */\n clientFlag?: boolean;\n /** Extended key usages minus the `ExtKeyUsage` prefix (`ServerAuth`). Empty on all 8 live roles. */\n extKeyUsage?: readonly string[];\n /** Permit IP SANs. ⚠️ OpenBao defaults TRUE; this resource defaults FALSE — see pki-role.ts. */\n allowIpSans?: boolean;\n /** Permit `localhost`/`localdomain` CNs. ⚠️ OpenBao TRUE, this resource FALSE. */\n allowLocalhost?: boolean;\n /** Permit wildcard CNs. ⚠️ OpenBao TRUE, this resource FALSE. */\n allowWildcardCertificates?: boolean;\n /** CN validations to run. ⚠️ OpenBao `[\"email\",\"hostname\"]`, this resource `[\"hostname\"]`. */\n cnValidations?: readonly BaoPkiCnValidation[];\n /** Signature hash bits; `0` auto-detects from key length. One live role pins 256. */\n signatureBits?: number;\n}\n\n/**\n * The managed field set, normalised so both sides of a diff are byte-comparable.\n *\n * ⚠️ KEY ORDER IS THE DIGEST. The props side and the live side are both built by the single\n * canonical() literal below, so `JSON.stringify` emits the same key order for each. Two\n * hand-written object literals would drift apart and produce a forever-diff nobody could\n * read from the plan output.\n */\nexport interface BaoPkiRoleCanonical {\n allowBareDomains: boolean;\n allowGlobDomains: boolean;\n allowIpSans: boolean;\n allowLocalhost: boolean;\n allowSubdomains: boolean;\n allowWildcardCertificates: boolean;\n allowedDomains: readonly string[];\n clientFlag: boolean;\n cnValidations: readonly string[];\n extKeyUsage: readonly string[];\n keyBits: number;\n keyType: string;\n /** ⚠️ `-1` means the duration prop did not parse — reconcile refuses rather than writing it. */\n maxTtlSeconds: number;\n serverFlag: boolean;\n signatureBits: number;\n ttlSeconds: number;\n}\n\nexport interface BaoPkiRoleAttributes extends BaoPkiRoleCanonical {\n name: string;\n mount: string;\n /** Human rendering of ttlSeconds — the compare uses the seconds, this is for the plan. */\n ttl: string;\n maxTtl: string;\n /** SHA-256 of the canonical form — safe to persist; see policy.ts. */\n digest: string;\n}\n\nconst sorted = (values: readonly string[]) => [...values].sort();\n\n/** ⚠️ Case-folded for the COMPARE only; the write keeps the author's casing.\n * Why, and what was never measured, is in the pki-role.ts header. */\nconst foldedUsages = (values: readonly string[]) => sorted(values.map((v) => v.toLowerCase()));\n\n/** The one literal that fixes key order for both sides of the diff. */\nconst canonical = (input: BaoPkiRoleCanonical): BaoPkiRoleCanonical => ({\n allowBareDomains: input.allowBareDomains,\n allowGlobDomains: input.allowGlobDomains,\n allowIpSans: input.allowIpSans,\n allowLocalhost: input.allowLocalhost,\n allowSubdomains: input.allowSubdomains,\n allowWildcardCertificates: input.allowWildcardCertificates,\n allowedDomains: sorted(input.allowedDomains),\n clientFlag: input.clientFlag,\n cnValidations: sorted(input.cnValidations),\n extKeyUsage: foldedUsages(input.extKeyUsage),\n keyBits: input.keyBits,\n keyType: input.keyType,\n maxTtlSeconds: input.maxTtlSeconds,\n serverFlag: input.serverFlag,\n signatureBits: input.signatureBits,\n ttlSeconds: input.ttlSeconds,\n});\n\nconst seconds = (text: string) => parseDuration(text) ?? -1;\n\nexport const canonicalFromProps = (props: BaoPkiRoleProps): BaoPkiRoleCanonical =>\n canonical({\n allowBareDomains: props.allowBareDomains ?? false,\n allowGlobDomains: props.allowGlobDomains ?? false,\n allowIpSans: props.allowIpSans ?? false,\n allowLocalhost: props.allowLocalhost ?? false,\n allowSubdomains: props.allowSubdomains ?? false,\n allowWildcardCertificates: props.allowWildcardCertificates ?? false,\n allowedDomains: props.allowedDomains,\n clientFlag: props.clientFlag ?? true,\n cnValidations: props.cnValidations ?? ['hostname'],\n extKeyUsage: props.extKeyUsage ?? [],\n keyBits: props.keyBits ?? 0,\n keyType: props.keyType ?? 'rsa',\n maxTtlSeconds: seconds(props.maxTtl),\n serverFlag: props.serverFlag ?? true,\n signatureBits: props.signatureBits ?? 0,\n ttlSeconds: seconds(props.ttl),\n });\n\nconst bool = (value: unknown, fallback: boolean) => (typeof value === 'boolean' ? value : fallback);\n\nconst int = (value: unknown, fallback: number) =>\n typeof value === 'number' && Number.isFinite(value) ? value : fallback;\n\nconst list = (value: unknown): readonly string[] =>\n Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : [];\n\nconst str = (value: unknown, fallback: string) => (typeof value === 'string' ? value : fallback);\n\nexport const canonicalFromLive = (live: Record<string, unknown>): BaoPkiRoleCanonical =>\n canonical({\n allowBareDomains: bool(live['allow_bare_domains'], false),\n allowGlobDomains: bool(live['allow_glob_domains'], false),\n allowIpSans: bool(live['allow_ip_sans'], false),\n allowLocalhost: bool(live['allow_localhost'], false),\n allowSubdomains: bool(live['allow_subdomains'], false),\n allowWildcardCertificates: bool(live['allow_wildcard_certificates'], false),\n allowedDomains: list(live['allowed_domains']),\n clientFlag: bool(live['client_flag'], true),\n cnValidations: list(live['cn_validations']),\n extKeyUsage: list(live['ext_key_usage']),\n keyBits: int(live['key_bits'], 0),\n keyType: str(live['key_type'], 'rsa'),\n maxTtlSeconds: ttlSeconds(live['max_ttl']) ?? -1,\n serverFlag: bool(live['server_flag'], true),\n signatureBits: int(live['signature_bits'], 0),\n ttlSeconds: ttlSeconds(live['ttl']) ?? -1,\n });\n\n/** ⚠️ Same rendering auth-role-form.ts uses, so `720h` and `2592000` read alike in a plan. */\nconst ttlText = (value: number) => {\n if (value < 0) return '';\n if (value !== 0 && value % 86400 === 0) return `${String(value / 86400)}d`;\n if (value !== 0 && value % 3600 === 0) return `${String(value / 3600)}h`;\n if (value !== 0 && value % 60 === 0) return `${String(value / 60)}m`;\n return `${String(value)}s`;\n};\n\nexport const digestOf = (form: BaoPkiRoleCanonical) => sha256(JSON.stringify(form));\n\nexport const rolePath = (props: { name: string; mount?: string }) =>\n `${mountPath(props.mount ?? 'pki')}/roles/${props.name}`;\n\nexport const attributesOf = (\n props: BaoPkiRoleProps,\n live: Record<string, unknown>,\n): BaoPkiRoleAttributes => {\n const form = canonicalFromLive(live);\n return {\n ...form,\n name: props.name,\n mount: mountPath(props.mount ?? 'pki'),\n ttl: ttlText(form.ttlSeconds),\n maxTtl: ttlText(form.maxTtlSeconds),\n digest: digestOf(form),\n };\n};\n\n/**\n * ⚠️ ARRAYS GO OVER AS COMMA-JOINED STRINGS. `allowed_domains` and `ext_key_usage` are\n * documented as \"a comma-separated string or list\"; `cn_validations` is not, so its\n * splitting is REASONED, not measured. reconcile re-reads and re-compares after every\n * write, so a value that arrived unsplit fails loudly instead of landing wrong.\n *\n * ⚠️ TTLs GO OVER AS THE AUTHOR'S OWN DURATION STRING, never as a re-rendered one — so this\n * never has to know whether OpenBao's parser accepts the `d` suffix ttlText() produces for\n * display. Rendering is for humans; the wire only ever sees what was declared.\n *\n * ★ `ext_key_usage` is OMITTED when empty rather than sent as `ext_key_usage=`. The write\n * replaces the whole role, so an absent field already lands on the `[]` default, and\n * omitting sidesteps how an empty value would be encoded.\n *\n * ★ THE BODY FOR `PUT <mount>/roles/<name>`, ALL STRINGS — the same `k=v` pairs `bao write` sent.\n */\nexport const writeBody = (props: BaoPkiRoleProps): Record<string, string> => {\n const form = canonicalFromProps(props);\n const body: Record<string, string> = {\n allowed_domains: form.allowedDomains.join(','),\n allow_bare_domains: String(form.allowBareDomains),\n allow_glob_domains: String(form.allowGlobDomains),\n allow_subdomains: String(form.allowSubdomains),\n allow_ip_sans: String(form.allowIpSans),\n allow_localhost: String(form.allowLocalhost),\n allow_wildcard_certificates: String(form.allowWildcardCertificates),\n cn_validations: form.cnValidations.join(','),\n client_flag: String(form.clientFlag),\n server_flag: String(form.serverFlag),\n key_type: form.keyType,\n key_bits: String(form.keyBits),\n signature_bits: String(form.signatureBits),\n ttl: props.ttl,\n max_ttl: props.maxTtl,\n };\n const usages = props.extKeyUsage ?? [];\n if (usages.length > 0) body['ext_key_usage'] = usages.join(',');\n return body;\n};\n\n/** True when live already matches the declaration on every managed field. */\nexport const matches = (attributes: BaoPkiRoleAttributes, props: BaoPkiRoleProps) =>\n attributes.digest === digestOf(canonicalFromProps(props));\n",
22
+ "import { Resource } from 'alchemy';\nimport { isResolved } from 'alchemy/Diff';\nimport * as Provider from 'alchemy/Provider';\nimport * as Effect from 'effect/Effect';\nimport * as FileSystem from 'effect/FileSystem';\nimport * as Path from 'effect/Path';\nimport type * as HttpClient from 'effect/unstable/http/HttpClient';\nimport { sha256 } from './digest.ts';\nimport { isEmptyAssembly } from './policy-assembly.ts';\nimport { deletePolicy, readPolicy, writePolicy } from './policy-wire.ts';\n\n/**\n * An OpenBao ACL policy, assembled from the HCL fragments that declare it.\n *\n * ★ WHY THIS EXISTS. The estate already hand-rolled both halves of what Alchemy provides:\n * five `apply-*.sh`/`apply-*.py` scripts converge OpenBao, and separate `check-*.sh`\n * gates compare live to repo and refuse on drift. That is `reconcile` and `diff` written\n * twice, in two languages, with nothing recording what was applied — and one of those\n * gates blocked a deploy today because its own path had rotted.\n *\n * ⛔ THERE IS NO `Bao.Secret`, AND THERE MUST NEVER BE ONE. Alchemy persists attributes to\n * its state store and does NOT encrypt them: StateEncoding.ts unwraps `Redacted` to\n * plaintext under a marker key, and nothing under alchemy/src/State encrypts. This\n * estate's state store is the `alchemy` Postgres database, which pg-backup.sh dumps\n * nightly (it enumerates EVERY non-template database), copies to CT100, and PBS backs up\n * from there. A KV value here would exist in four places, none of them OpenBao.\n *\n * So this declares the SHAPE AROUND secrets. Every attribute it stores is a name, a\n * count, or a hash.\n *\n * ⛔ EVERY PRIMITIVE IS ALCHEMY'S OR EFFECT'S, NOT NODE'S. An earlier cut of this file used\n * `node:child_process` + `promisify(execFile)` and `node:fs/promises`, which is\n * hand-rolling inside a bun + Effect codebase: it loses the Scope, the typed\n * PlatformError, and the Layer that makes the provider testable without a real vault.\n * ★ `CommandExecutor.run` was tried first and blocked read/diff: Alchemy passes a session\n * to reconcile/delete but NOT to read/diff (Provider.ts:258-289), so the fix was Alchemy's\n * OWN Docker pattern — `ChildProcessSpawner` + `ChildProcess.make`, which needs only a\n * Scope (Docker.ts:476-553). ★ SINCE 2026-09-14 THE CALLS ARE OpenBao's HTTP API through\n * Effect's `HttpClient` (bao-http.ts), which needs no session either, so read/diff still work.\n * FileSystem and Path come from `effect/*` directly — in Effect 4 they moved into core.\n */\nexport interface BaoPolicyProps {\n /** Policy name, as `sys/policies/acl/<name>` takes it. */\n name: string;\n /**\n * Directory of `*.hcl` fragments.\n *\n * ⚠️ CONCATENATED IN SORTED ORDER, EACH FOLLOWED BY A NEWLINE — byte-identical to\n * apply-agent-policy.sh, which is what wrote the live policy. Any other joining makes\n * the first plan report an update on a policy nobody changed.\n */\n fragments: string;\n}\n\nexport interface BaoPolicyAttributes {\n /** Policy name. */\n name: string;\n /** SHA-256 of the assembled HCL. The whole diff, and safe to persist. */\n digest: string;\n /** `path \"...\"` grants — the number apply-agent-policy.sh prints. */\n grants: number;\n /** Fragment files that produced it, so a plan reads like the directory. */\n parts: number;\n}\n\nexport interface BaoPolicy extends Resource<\n 'Bao.Policy',\n BaoPolicyProps,\n BaoPolicyAttributes,\n never,\n HttpClient.HttpClient | FileSystem.FileSystem | Path.Path\n> {}\n\n/**\n * ⛔ `retain` — DELETING A POLICY REVOKES EVERY GRANT IT CARRIES, and this is the ONLY family in\n * this package with live instances, so it is the only one where the default could ever fire.\n *\n * 🔴 IT WAS THE ONLY FAMILY WITHOUT IT. Every sibling passes `{ defaultRemovalPolicy: 'retain' }`\n * — auth-role.ts:37, mount.ts:40, pki-role.ts, proxmox-role.ts, ssh-role.ts — each with a ★\n * explaining what deletion destroys. This file has the same warning on its own `delete` handler\n * and did not carry the option, which is the worst possible combination: the danger is\n * documented and the guard is missing.\n * ⛔ WHAT THAT MEANT IN PRACTICE, on 2026-09-13: ten live policies are named in a `const` array\n * in alchemy.run.ts. Removing a name from that array orphans the resource, and without\n * `retain` the next deploy calls `bao policy delete` on it — including the 86-line policy that\n * decides what the agent working in this repo can reach. A one-word edit, no plan line saying\n * \"revoke\", and the grants are gone. The array grew from four names to ten that same day,\n * which multiplied the blast radius of a mistake nobody had guarded against.\n *\n * ⚠️ `retain` DOES NOT WEAKEN `delete`, WHICH STAYS FULLY IMPLEMENTED. The engine skips it for an\n * orphaned resource; a caller that means it opts in with `.pipe(RemovalPolicy.destroy())`. This\n * is Terraform's `prevent_destroy`, not a stubbed operation — a delete that silently did nothing\n * would lie to whoever read the plan.\n */\nexport const BaoPolicy = Resource<BaoPolicy>('Bao.Policy', {\n defaultRemovalPolicy: 'retain',\n});\n\n/** `path \"...\"` grant count — the same number the apply script prints. */\nconst grantsOf = (hcl: string) => (hcl.match(/^path /gm) ?? []).length;\n\nconst attributesOf = (name: string, hcl: string, parts: number): BaoPolicyAttributes => ({\n name,\n digest: sha256(hcl),\n grants: grantsOf(hcl),\n parts,\n});\n\nexport const BaoPolicyProvider = () =>\n Provider.effect(\n BaoPolicy,\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n\n /** Assemble fragments exactly as apply-agent-policy.sh does. */\n const assemble = (dir: string) =>\n Effect.gen(function* () {\n const entries = (yield* fs.readDirectory(dir)).filter((f) => f.endsWith('.hcl')).sort();\n let joined = '';\n for (const f of entries) joined += `${yield* fs.readFileString(path.join(dir, f))}\\n`;\n return { joined, parts: entries.length };\n });\n\n return BaoPolicy.Provider.of({\n /**\n * ⛔ `bao policy list` IS NOT A LIST OF THINGS THIS OWNS. Every policy in the\n * namespace answers, including ones written by hand years ago. Returning them\n * would invite Alchemy to adopt — and then delete — policies it never created.\n */\n list: () => Effect.succeed([]),\n\n // ★ '' means genuinely absent (a 404); a refusal fails. policy-wire.ts has the history.\n read: Effect.fn(function* ({ olds }) {\n const live = yield* readPolicy(olds.name);\n return live.length === 0 ? undefined : attributesOf(olds.name, live, 0);\n }),\n\n /**\n * ⛔ IT COMPARES THE LIVE POLICY, NOT THE STORED DIGEST. A policy edited in the\n * OpenBao UI is exactly the drift the check-* gates exist to catch, and a\n * provider that trusted its own state would report `noop` straight through it.\n */\n diff: Effect.fn(function* ({ news, output }) {\n // ⚠️ A prop can still be an unresolved Output or Config at plan time. Docker's\n // own providers guard with isResolved and skip rather than guess; a diff that\n // read a Config as a string would compare a placeholder to real HCL.\n if (output === undefined || !isResolved(news)) return undefined;\n const { joined } = yield* assemble(news.fragments);\n const live = yield* readPolicy(news.name);\n return live.length > 0 && sha256(live) === sha256(joined)\n ? ({ action: 'noop' } as const)\n : ({ action: 'update' } as const);\n }),\n\n reconcile: Effect.fn(function* ({ news }) {\n const name = news.name;\n const { joined, parts } = yield* assemble(news.fragments);\n /**\n * ⛔ AN EMPTY ASSEMBLY IS A REFUSAL, NOT AN EMPTY POLICY. `bao policy write` with\n * no paths SILENTLY REVOKES every grant the policy carries, for every token\n * bound to it — and a mistyped or moved fragments directory reads exactly like\n * a policy with nothing in it. This estate lost a deploy today to a directory\n * that moved; it must not lose a vault to one.\n */\n // ⛔ No FILES was the first guard; no GRANT is the real one — see policy-assembly.ts.\n if (isEmptyAssembly(parts, joined))\n return yield* Effect.die(\n new Error(\n `Bao.Policy ${name}: the fragments under ${news.fragments} carry no path grant ` +\n `(${String(parts)} file(s)). Writing it would revoke every grant the policy carries.`,\n ),\n );\n /**\n * ⛔ ADOPTING A POLICY THAT ALREADY MATCHES MUST NOT REWRITE IT. Alchemy's `adopted`\n * action is NOT a read — Apply.ts routes it down the same branch as `update` and calls\n * reconcile — so the first deploy after declaring the four policies this estate\n * already has would have run `bao policy write` over all four. Byte-identical in the\n * happy case, and a SILENT REWRITE OF LIVE GRANTS the moment the repo fragments have\n * drifted from what is deployed.\n *\n * ★ THE GUARD IS THE SAME EXPRESSION `diff` USES, deliberately: one definition of\n * \"settled\" for this resource, so the plan and the write can never disagree. The\n * sibling PVE provider does exactly this in pveOperations.reconcile.\n */\n const current = yield* readPolicy(name);\n if (current.length > 0 && sha256(current) === sha256(joined)) {\n return attributesOf(name, joined, parts);\n }\n // ⚠️ In the request body, never on argv — see `writePolicy` for why the temp file went.\n yield* writePolicy(name, joined);\n return attributesOf(name, joined, parts);\n }),\n\n /**\n * ⛔ DELETING A POLICY REVOKES EVERY GRANT IT CARRIES. Idempotent as Alchemy\n * requires — already gone is success — but this is the one operation with teeth.\n */\n delete: Effect.fn(function* ({ output }) {\n /**\n * ⛔ A REFUSED DELETE FAILS. This is \"the one operation with teeth\" the note above\n * names, and it used to run through the read helper, which discarded the exit code —\n * so a REFUSED policy delete reported success and the row left Alchemy's state while\n * every grant it carried stayed live. Idempotence is preserved: a 404 is still\n * success, and a 403 is not (bao-status.ts).\n */\n yield* deletePolicy(output.name);\n return undefined;\n }),\n });\n }),\n );\n",
23
+ "/**\n * Whether an assembled policy would carry NO GRANT — the one write `Bao.Policy` must refuse.\n *\n * ⛔ EMPTY MEANS NO `path` GRANT, NOT NO FILES. Writing `sys/policies/acl/<name>` with nothing in it\n * SILENTLY REVOKES every grant the policy carries, for every token bound to it. The first guard\n * only counted fragment files, so a directory whose one fragment was empty, whitespace or\n * comments — a truncated edit, a bad checkout — assembled to text with no grant in it, passed,\n * and would have replaced the live policy with nothing. Found by grok's review on 2026-09-14.\n * ★ A GRANT IS A `path \"…\"` STANZA, the unit apply-agent-policy.sh counts and `grantsOf` in\n * policy.ts reports. Leading whitespace is allowed here, so an indented stanza still counts.\n */\nexport const isEmptyAssembly = (parts: number, joined: string): boolean =>\n parts === 0 || !/^\\s*path\\s+\"/m.test(joined);\n",
24
+ "/**\n * The three OpenBao calls Bao.Policy makes — read, write and delete of `sys/policies/acl/<name>`.\n * Split from policy.ts so the trailing-newline round trip can be tested against a fake server.\n */\nimport * as Effect from 'effect/Effect';\nimport { baoDelete, baoRead, baoWrite } from './bao-http.ts';\nimport { BaoError } from './bao-status.ts';\n\n/** The endpoint the CLI's policy commands used — openbao v2.6.2 api/sys_policy.go:60, :100, :122. */\nexport const policyPath = (name: string) => `sys/policies/acl/${name}`;\n\n/**\n * The live policy text, or '' when the policy is genuinely absent. Anything else FAILS.\n *\n * 🔴 THE ABSENCE WORDING WAS THE POLICY COMMAND'S OWN, AND ASSUMING OTHERWISE MADE A NEW POLICY\n * UNCREATABLE. `No value found at` was measured for `bao read`; `bao policy read` never printed\n * it. Its API client turned a 404 into an empty string and the CLI reported that in its own\n * words — openbao v2.6.2 api/sys_policy.go:65-67 and command/policy_read.go:85-87:\n *\n * No policy named: <name> exit 2\n *\n * So `read` on a policy that did not exist yet FAILED instead of answering absent, and\n * `reconcile` failed at its own pre-write read. Every policy declared so far already existed,\n * which is the only reason nothing broke. MEASURED 2026-09-14 under the admin lane —\n * `bao policy read zz-probe-absent-policy-4c1e` printed exactly that line and exited 2. (The\n * agent lane cannot measure it: it is refused `sys/policies/acl` with a 403 first.)\n * ★ THE WORDING PROBLEM IS GONE WITH THE CLI. The line above was the CLI's rendering of the 404 that\n * the server returns for a missing policy (vault/logical_system.go handlePoliciesRead returns no\n * response, which RespondErrorCommon turns into 404). That status is now read directly, and a\n * refused read is a 403 — two codes, no text to confuse.\n *\n * ⚠️ `data.policy` IS THE TEXT, and a 200 without it is a failure, as it was in the CLI's client\n * (\"no policy found in response\", api/sys_policy.go:81-85).\n */\nexport const readPolicy = (name: string) =>\n Effect.flatMap(baoRead(policyPath(name)), (data) => {\n if (data === undefined) return Effect.succeed('');\n const text = data['policy'];\n return typeof text === 'string'\n ? Effect.succeed(text)\n : Effect.fail(new BaoError(200, 'GET', policyPath(name), ['no data.policy in the response']));\n });\n\n/**\n * ★ THE HCL TRAVELS IN THE REQUEST BODY (`{\"policy\": …}`, api/sys_policy.go:96-101). The CLI path\n * needed a scoped temp file because passing HCL on argv would have put every grant in the process\n * list; a JSON body never reaches argv, so the temp file is gone with it.\n */\nexport const writePolicy = (name: string, hcl: string) =>\n baoWrite('PUT', policyPath(name), { policy: hcl });\n\n/** Deleting a policy that is already gone answers success (handlePoliciesDelete returns nothing). */\nexport const deletePolicy = (name: string) => baoDelete(policyPath(name));\n",
25
+ "/**\n * A role on the estate's proxmox secrets plugin — the mapping from one consumer to one PVE\n * mint user, plus the lease ceilings that consumer's tokens get. METADATA ONLY.\n *\n * ★ WHY THIS FAMILY MATTERS MORE THAN IT LOOKS. Declaring these roles is how the estate\n * replaces HAND-MADE PERMANENT PVE TOKENS with short-lived minted ones. On TB4, SEVEN tokens\n * carry expire=0 and privsep=0 — root@pam!proxbox-sync, mcp@pve!executor,\n * monitoring@pve!exporter, sablier@pve!sablier, tofu@pve!ro, tofu@pve!apply and\n * vaultmint@pve!engine. Six of those are one Bao.ProxmoxRole plus a scoped PVE user away from\n * being a 5-minute lease instead of a forever key.\n *\n * The seventh cannot be. vaultmint@pve!engine is the credential the ENGINE ITSELF\n * authenticates with in order to mint anything at all: the thing that issues short-lived\n * tokens needs a standing one, and no role can resolve that chicken-and-egg. It stays\n * permanent, which makes it the single most valuable token on the cluster and the one whose\n * rotation must be a deliberate, hand-run procedure rather than a plan.\n *\n * MEASURED here: `bao read proxmox-tb4/config` returns parent_token_id\n * `vaultmint@pve!engine`, parent_secret_set true, parent_secret_tail `…eb01` — the engine's\n * own key, present and, by the schema's own \"Write-only: never returned by a read\", not\n * retrievable. The seven-token inventory was handed to this change and NOT re-counted against\n * the PVE API in this session.\n *\n * ⛔ NO SECRET IN PROPS OR ATTRIBUTES — see policy.ts for the full reasoning. Every attribute\n * here is a mount path, a role name, a PVE username or a TTL. Nothing this resource stores\n * would help anyone authenticate to anything.\n *\n * ⛔ ISSUANCE IS NOT THIS RESOURCE, AND MUST NEVER BECOME ONE. `<mount>/creds/<name>` returns a\n * LIVE PVE TOKEN. Alchemy persists attributes unencrypted (StateEncoding.ts tags Redacted\n * values rather than encrypting them) into the `alchemy` Postgres, which pg-backup.sh dumps\n * nightly to CT100, from where PBS backs it up. A minted token declared as a resource would\n * outlive its own lease in at least four places, none of them OpenBao. This resource declares\n * the DOOR; walking through it is a runtime call, not a plan.\n *\n * ★ `defaultRemovalPolicy: 'retain'` — deleting a role breaks every consumer that mints from\n * it. Opt in with `.pipe(RemovalPolicy.destroy())`; see resource.ts in house/proxmox.\n */\nimport { Resource } from 'alchemy';\nimport { isResolved } from 'alchemy/Diff';\nimport * as Provider from 'alchemy/Provider';\nimport * as Effect from 'effect/Effect';\nimport type * as HttpClient from 'effect/unstable/http/HttpClient';\nimport { baoDelete, baoRead, baoWrite } from './bao-http.ts';\nimport {\n type BaoProxmoxRoleAttributes,\n type BaoProxmoxRoleProps,\n attributesOf,\n matches,\n rolePath,\n writeBody,\n} from './proxmox-role-form.ts';\n\nexport type { BaoProxmoxRoleAttributes, BaoProxmoxRoleProps };\n\nexport interface BaoProxmoxRole extends Resource<\n 'Bao.ProxmoxRole',\n BaoProxmoxRoleProps,\n BaoProxmoxRoleAttributes,\n never,\n HttpClient.HttpClient\n> {}\n\nexport const BaoProxmoxRole = Resource<BaoProxmoxRole>('Bao.ProxmoxRole', {\n defaultRemovalPolicy: 'retain',\n});\n\n/**\n * ⚠️ A missing MOUNT and a missing ROLE both read as undefined here, because both answer 404:\n * an absent role is a read that found nothing, and a path under no mount is OpenBao's\n * unsupported-path error, which is also a 404 (sdk/logical/response_util.go). They are not the\n * same failure — one is a typo in `mount`, the other is a role that has yet to be created — so\n * reconcile lets OpenBao's own `errors` through on the write rather than inventing a message.\n * A bad mount says \"no handler for route\".\n */\nconst readRole = (props: BaoProxmoxRoleProps) =>\n Effect.gen(function* () {\n const live = yield* baoRead(rolePath(props.mount, props.name));\n if (live === undefined) return undefined;\n return attributesOf(props, live);\n });\n\nexport const BaoProxmoxRoleProvider = () =>\n Provider.effect(\n BaoProxmoxRole,\n Effect.succeed(\n BaoProxmoxRole.Provider.of({\n /**\n * ⛔ `bao list <mount>/roles` IS NOT A LIST OF THINGS THIS OWNS. MEASURED today:\n * proxmox-tb4/roles answers `provision, read` and proxmox-ops/roles answers `read` —\n * four roles created by hand at bringup, none of them declared anywhere yet.\n * Returning them would invite Alchemy to adopt, and then delete, roles it never\n * created, and deleting a role is how every consumer of it stops being able to mint.\n */\n list: () => Effect.succeed([]),\n\n read: Effect.fn(function* ({ olds }) {\n return yield* readRole(olds);\n }),\n\n /**\n * ⛔ IT COMPARES THE LIVE ROLE, NOT THE STORED DIGEST. A mint_user edited in the\n * OpenBao UI re-scopes every future credential while the digest in Postgres still\n * says everything is fine — exactly the drift the hand-written check-* gates exist to\n * catch, and exactly what a provider that trusted its own state would walk past.\n */\n diff: Effect.fn(function* ({ news, output }) {\n if (output === undefined || !isResolved(news)) return undefined;\n const live = yield* readRole(news);\n if (live === undefined) return { action: 'update' } as const;\n if (matches(live, news)) return { action: 'noop' } as const;\n /**\n * ⛔ NEVER `replace`, NOT EVEN FOR A mint_user CHANGE. A replace deletes the role and\n * writes it back; between those steps `<mount>/creds/<name>` 404s, so anything\n * minting in that window fails. The end state is byte-identical to the in-place\n * write, because the plugin stores name, mint_user, ttl and max_ttl and nothing\n * else — there is no immutable identity a recreate could reset. So a replace here\n * buys an outage window in exchange for a louder-looking plan. The loudness a\n * re-scope deserves comes from reconcile REFUSING it below, which blocks rather\n * than destroys.\n *\n * ⚠️ CONSEQUENCE FOR THE READER OF A PLAN: a mint_user re-scope shows up here as a\n * plain `update`, indistinguishable from a TTL tweak. The refusal lands at DEPLOY,\n * not at plan. That is the safe ordering — nothing is written before the refusal —\n * but do not read a green plan as evidence that a mint_user change is approved.\n */\n return { action: 'update' } as const;\n }),\n\n reconcile: Effect.fn(function* ({ news }) {\n const live = yield* readRole(news);\n /**\n * ⛔ A mint_user CHANGE RE-SCOPES EVERY CREDENTIAL THE ROLE WILL EVER MINT, SILENTLY.\n * The minted token inherits that PVE user's standing ACL and nothing else, so\n * swapping `hf-read@pve` for `hf-provision@pve` turns a read-only consumer into a\n * provisioning one with no other line of the declaration changing. A plan diff of\n * one string is not proportionate warning for that, so an existing role refuses the\n * move unless the declaration NAMES THE VALUE IT IS REPLACING. Creating a role never\n * trips this — there is nothing to re-scope — and the permission expires by itself\n * once the change lands, because live then equals the declaration.\n */\n if (\n live !== undefined &&\n live.mintUser !== news.mintUser &&\n news.allowMintUserChange !== live.mintUser\n ) {\n return yield* Effect.die(\n new Error(\n `Bao.ProxmoxRole ${rolePath(news.mount, news.name)}: live mint_user ` +\n `${live.mintUser} != declared ${news.mintUser}. Every token this role mints ` +\n 'inherits the ACL of that PVE user. To re-scope deliberately, declare ' +\n `allowMintUserChange: '${live.mintUser}'.`,\n ),\n );\n }\n if (live === undefined || !matches(live, news)) {\n const write = writeBody(news);\n /**\n * ⛔ AN UNPARSEABLE TTL IS A REFUSAL, NOT A ZERO. parseDuration accepts `3600s`,\n * `1h`, `6h`, `1d` and bare `0`, and NOTHING ELSE — a bare `3600` or a compound\n * `1h30m` returns undefined. Left alone that never equals the live value, so the\n * plan would report an update forever and each deploy would write a zero TTL.\n * Die with the offending props named instead.\n */\n if (!write.ok) {\n return yield* Effect.die(\n new Error(\n `Bao.ProxmoxRole ${rolePath(news.mount, news.name)}: unparseable duration ` +\n `${write.bad.join(', ')}. Use 30s / 5m / 1h / 1d, or 0.`,\n ),\n );\n }\n yield* baoWrite('PUT', rolePath(news.mount, news.name), write.body);\n }\n const after = yield* readRole(news);\n if (after === undefined) {\n return yield* Effect.die(\n new Error(\n `Bao.ProxmoxRole ${rolePath(news.mount, news.name)}: write returned no error ` +\n 'but the role is still absent.',\n ),\n );\n }\n /**\n * ⚠️ A GREEN RECONCILE DOES NOT MEAN THE ROLE CAN MINT. The engine never grants ACLs\n * — it borrows the mint user's — so declaring `mint_user: hf-read@pve` neither\n * creates that PVE user nor gives it a single permission. If the user does not\n * exist, or exists with no ACL, the role writes and reads back perfectly and the\n * failure surfaces at `creds/<name>` time, in whatever consumer is unlucky. Verify\n * the PVE side out-of-band; this resource cannot and does not.\n */\n return after;\n }),\n\n /**\n * ⛔ DELETING A ROLE DOES NOT REVOKE THE TOKENS IT ALREADY MINTED — it only stops the\n * next mint. Outstanding leases run to their own expiry against the PVE user's ACL.\n * So this is an availability change for every consumer of `creds/<name>` and NOT a\n * containment action: to contain a leaked credential, revoke the lease (which deletes\n * the PVE token) or delete the token in PVE. Deleting the role only hides the door it\n * came through. REASONED from Vault lease semantics; not exercised here.\n *\n * ⚠️ IDEMPOTENT AS ALCHEMY REQUIRES — already gone is success. It used to get that by\n * going through `baoReadText`, which discarded the exit code entirely, so a REFUSED\n * delete was reported as a successful one and the resource left Alchemy's state while\n * the role stayed live. `baoDelete` keeps the idempotence — a 404 is success — and\n * fails on everything else, 403 and 503 included.\n */\n delete: Effect.fn(function* ({ output }) {\n yield* baoDelete(rolePath(output.mount, output.name));\n return undefined;\n }),\n }),\n ),\n );\n",
26
+ "/**\n * Wire and comparison half of Bao.ProxmoxRole — the resource itself is proxmox-role.ts.\n *\n * MEASURED 2026-09-13, read-only, against the live server (BAO_ADDR=http://127.0.0.1:8200,\n * BAO_NAMESPACE=homeflare). No write of any kind was issued to a role or a mount:\n *\n * bao read -format=json proxmox-tb4/roles/read\n * → { max_ttl: 21600, mint_user: \"hf-read@pve\", name: \"read\", ttl: 3600 }\n * bao read -format=json proxmox-tb4/roles/provision\n * → { max_ttl: 1800, mint_user: \"hf-provision@pve\", name: \"provision\", ttl: 300 }\n * GET /v1/proxmox-tb4/?help=1 → components.schemas.WriteRolesNameRequest:\n * mint_user string — the ONLY required field, described as \"The `user@realm` whose\n * standing ACL is the minted token's entire privilege set. Created\n * out-of-band; this engine never grants ACLs.\"\n * ttl integer — \"format\": \"seconds\"\n * max_ttl integer — \"format\": \"seconds\", \"Narrows the mount's; can never widen it.\"\n *\n * ★ name + mint_user + ttl + max_ttl IS THE WHOLE ROLE. The plugin stores nothing else, so\n * `matches` below covers 100% of the server-side state: there is no unmanaged field left to\n * drift behind a green plan. That completeness is bought by making the TTLs REQUIRED props\n * rather than the optional-means-unmanaged shape Bao.Mount uses — see the ⛔ on writeBody.\n */\nimport { sha256 } from './digest.ts';\nimport { mountPath, parseDuration, ttlSeconds } from './mount-form.ts';\n\nexport interface BaoProxmoxRoleProps {\n /** Mount path of the proxmox secrets engine, e.g. `proxmox-tb4`. A trailing slash is fine. */\n mount: string;\n /**\n * Role name, as `<mount>/roles/<name>` takes it.\n *\n * ⚠️ The plugin's own schema says the name \"Appears in the minted token's id, so keep it\n * short.\" Both live roles are one word (`read`, `provision`). A long name here becomes a\n * long PVE token id on every mint.\n */\n name: string;\n /**\n * ⛔ THE ENTIRE SECURITY BOUNDARY OF THIS RESOURCE. A minted token inherits this PVE user's\n * standing ACL and nothing else — the engine never grants ACLs, it only borrows one. Change\n * this string and every credential the role issues from then on carries a different\n * privilege set, with no other field in the declaration changing to hint at it. That is why\n * it is compared in `matches` below, and why reconcile refuses to move it without\n * `allowMintUserChange`.\n */\n mintUser: string;\n /** Default lease TTL for minted tokens, e.g. `1h`. REQUIRED — see the ⛔ on writeBody. */\n ttl: string;\n /**\n * Maximum lease TTL, e.g. `6h`. REQUIRED — see the ⛔ on writeBody.\n *\n * ⚠️ \"Narrows the mount's; can never widen it.\" A role declaring `6h` under a mount whose\n * max_lease_ttl is `1h` still READS BACK as 21600, so this resource reports noop and the\n * plan is green — while every actual mint is capped at 3600 by the mount. The role is not\n * lying and neither is the plan; the ceiling simply lives one level up. If a consumer gets\n * a shorter lease than declared here, check `bao read sys/mounts/<mount>` before touching\n * the role.\n */\n maxTtl: string;\n /**\n * Permission to re-scope `mintUser` on an EXISTING role: set it to the CURRENT live mint_user\n * that this change is allowed to replace.\n *\n * ★ IT IS A STRING AND NOT A BOOLEAN, AND THAT IS THE WHOLE POINT. A `true` left behind after\n * one legitimate rename would wave through every future re-scope of that role forever —\n * including one pasted in by whoever copies this block as a template. Naming the value being\n * replaced makes the permission SELF-EXPIRING: the moment the change lands, live equals\n * `mintUser`, the guard stops consulting it, and the next re-scope finds a stale string that\n * does not match live and is refused on its own merits.\n *\n * Not persisted, not in attributes, not in `matches` — it describes the operator's intent, not\n * the server, so changing it alone is correctly a noop. See the ⛔ in proxmox-role.ts reconcile.\n */\n allowMintUserChange?: string;\n}\n\nexport interface BaoProxmoxRoleAttributes {\n /** Mount path, trailing slash stripped. */\n mount: string;\n /** Role name. */\n name: string;\n /** The PVE `user@realm` live tokens are minted under. A name, never a credential. */\n mintUser: string;\n /** Live default lease TTL, rendered back into the duration shape props use. */\n ttl: string;\n /** Live maximum lease TTL, same shape. */\n maxTtl: string;\n /** SHA-256 of the managed fields — safe to persist; see policy.ts. */\n digest: string;\n}\n\nconst text = (value: unknown) => (typeof value === 'string' ? value : '');\n\n/**\n * Seconds back into the duration shape the props use.\n *\n * ⚠️ Third private copy of this function in the package (mount-form.ts and auth-role-form.ts\n * each keep one). Exporting a single copy means editing a file this change does not own, so\n * it is duplicated deliberately rather than silently. Worth collapsing in a follow-up.\n */\nconst ttlText = (seconds: number | undefined, fallback = '0') => {\n if (seconds === undefined) return fallback;\n if (seconds % 86400 === 0) return `${String(seconds / 86400)}d`;\n if (seconds % 3600 === 0) return `${String(seconds / 3600)}h`;\n if (seconds % 60 === 0) return `${String(seconds / 60)}m`;\n return `${String(seconds)}s`;\n};\n\n/** `proxmox-tb4/roles/read` — read, write and delete all address the role here. */\nexport const rolePath = (mount: string, name: string) => `${mountPath(mount)}/roles/${name}`;\n\nexport const attributesOf = (\n props: BaoProxmoxRoleProps,\n live: Record<string, unknown>,\n): BaoProxmoxRoleAttributes => {\n const attrs = {\n mount: mountPath(props.mount),\n // ⚠️ The plugin echoes `name` in a role read, so it is taken from `live` with the prop only\n // as a fallback. Every other value below is live-only: the same split the sibling\n // resources use, so a read never launders a declaration back into itself as \"current\".\n name: text(live['name']) || props.name,\n mintUser: text(live['mint_user']),\n ttl: ttlText(ttlSeconds(live['ttl'])),\n maxTtl: ttlText(ttlSeconds(live['max_ttl'])),\n };\n return { ...attrs, digest: sha256(JSON.stringify(attrs)) };\n};\n\n/**\n * The body for `PUT <mount>/roles/<name>` — all strings, the `k=v` pairs `bao write` sent — or\n * the prop names whose duration would not parse.\n *\n * ⛔ A WRITE TO roles/{name} REPLACES THE ROLE, IT DOES NOT MERGE. An omitted `ttl` is written\n * as the field's zero value, so a resource that treated undeclared TTLs as \"unmanaged\" — the\n * Bao.Mount shape — would silently reset a lease ceiling every time it rewrote the role for an\n * unrelated reason. Both TTLs are therefore required props and EVERY write carries all three\n * fields. REASONED from the Vault secrets-framework write semantics plus this plugin's schema\n * marking only `mint_user` required; NOT measured, because this session was read-only and\n * proving it would have meant writing to the live engine.\n *\n * ⚠️ Seconds go on the wire as integers, not as `1h`. The schema declares both TTLs\n * `\"format\": \"seconds\"`, and an integer is accepted unambiguously by every path that reads\n * them back — which is what keeps the round-trip through `matches` stable.\n */\nexport const writeBody = (\n props: BaoProxmoxRoleProps,\n): { ok: true; body: Record<string, string> } | { ok: false; bad: string[] } => {\n const ttl = parseDuration(props.ttl);\n const maxTtl = parseDuration(props.maxTtl);\n const bad: string[] = [];\n if (ttl === undefined) bad.push(`ttl=${props.ttl}`);\n if (maxTtl === undefined) bad.push(`maxTtl=${props.maxTtl}`);\n if (ttl === undefined || maxTtl === undefined) return { ok: false, bad };\n return {\n ok: true,\n body: { mint_user: props.mintUser, ttl: String(ttl), max_ttl: String(maxTtl) },\n };\n};\n\n/**\n * ⚠️ An unparseable declaration never equals anything, so it surfaces as an update that will\n * never settle rather than as a false noop. reconcile turns that loop into one named error;\n * see the `writeBody` refusal there.\n */\nconst sameTtl = (want: string, have: string) => {\n const wantSeconds = parseDuration(want);\n const haveSeconds = ttlSeconds(have);\n return wantSeconds !== undefined && wantSeconds === haveSeconds;\n};\n\n/** True when the live role already matches the declaration on every field the plugin stores. */\nexport const matches = (attributes: BaoProxmoxRoleAttributes, props: BaoProxmoxRoleProps) =>\n attributes.mintUser === props.mintUser &&\n sameTtl(props.ttl, attributes.ttl) &&\n sameTtl(props.maxTtl, attributes.maxTtl);\n",
27
+ "/**\n * An OpenBao SSH secrets-engine role — `<mount>/roles/{name}`, `key_type=ca`. METADATA ONLY.\n *\n * ⛔ THE SIGNING KEY IS OUT OF SCOPE AND MUST STAY OUT. `ssh/config/ca` holds (or generates)\n * the CA private key and `ssh/sign/<role>` issues certificates. Neither is declarable\n * here, for the reason policy.ts spells out: Alchemy persists attributes to its state\n * store WITHOUT encrypting them, that store is the `alchemy` Postgres, and pg-backup.sh\n * dumps it nightly to CT100. A CA key there would outlive the vault that guards it. This\n * resource declares the SHAPE a signature must satisfy — principals, extensions, TTLs.\n *\n * ⛔ THE ROLE WRITE IS A FULL REPLACE, NOT A PATCH. OpenBao rebuilds the role entry from the\n * request body, so any field the write omits reverts to its ZERO value. Two things follow:\n * `resolve` in ssh-role-form.ts fills every optional prop with the default the write would\n * produce and `matches` compares all of it (an omitted prop asserts the default, it never\n * means \"don't care\"); and `wouldErase` below names the fields this resource does NOT\n * declare, so reconcile can REFUSE instead of quietly dropping someone's key-length floor.\n *\n * ⛔ `allowEmptyPrincipals: true` WITH AN EMPTY `allowedUsers` IS A SKELETON KEY. OpenBao\n * will sign a certificate carrying no principals, and OpenSSH accepts such a certificate\n * for ANY user on ANY host that trusts the CA. It defaults to false, and all three roles\n * on the live engine carry false (MEASURED). Do not set it true without pinning\n * `defaultCriticalOptions['force-command']` in the same declaration.\n *\n * ⛔ `ttl: '0'` DOES NOT MEAN \"NO EXPIRY\" AND IT DOES NOT MEAN \"SHORT\". It means the mount's\n * `default_lease_ttl`, and the live `ssh` mount leaves that at 0, which means the SYSTEM\n * default — 768h. A 32-day SSH certificate is the opposite of what an SSH CA is for.\n * State the duration. MEASURED: the live `node-admin` and `tim` roles are ttl 14400 /\n * max_ttl 28800 (4h / 8h); `ssh-host/roles/host` is 31536000 (365d), as a host cert should.\n *\n * ★ `defaultRemovalPolicy: 'retain'`. Deleting this role locks every operator out of the\n * estate — the memory note \"operator SSH is homeflare-bot@<host>\" is this CA. Opt in with\n * `.pipe(RemovalPolicy.destroy())`; see resource.ts in house/proxmox.\n */\nimport { Resource } from 'alchemy';\nimport { isResolved } from 'alchemy/Diff';\nimport * as Provider from 'alchemy/Provider';\nimport * as Effect from 'effect/Effect';\nimport type * as HttpClient from 'effect/unstable/http/HttpClient';\nimport { baoDelete, baoRead, baoWrite } from './bao-http.ts';\nimport {\n type BaoSshRoleAttributes,\n type BaoSshRoleForm,\n type BaoSshRoleProps,\n attributesOf,\n badDurations,\n extMap,\n matches,\n readPath,\n resolve,\n writeBody,\n} from './ssh-role-form.ts';\n\nexport type { BaoSshRoleAttributes, BaoSshRoleProps };\n\nexport interface BaoSshRole extends Resource<\n 'Bao.SshRole',\n BaoSshRoleProps,\n BaoSshRoleAttributes,\n never,\n HttpClient.HttpClient\n> {}\n\nexport const BaoSshRole = Resource<BaoSshRole>('Bao.SshRole', {\n defaultRemovalPolicy: 'retain',\n});\n\n/** Live role, plus the raw body — reconcile needs the raw half to see what it would erase. */\nconst readRole = (form: BaoSshRoleForm) =>\n Effect.gen(function* () {\n const live = yield* baoRead(readPath(form.mount, form.name));\n if (live === undefined) return undefined;\n return { attributes: attributesOf(form, live), live };\n });\n\n/**\n * Fields this resource deliberately does not model, mapped to the value OpenBao's own write\n * puts there when nobody says otherwise. The templating flags change how a principal is\n * DERIVED at signing time, which is a different kind of decision from which principals\n * exist; the rest are issuance mechanics. MEASURED against all three live roles — every one\n * of them sits on exactly these values, so the guard below fires on nothing that exists.\n */\nconst UNMANAGED_DEFAULTS: Readonly<Record<string, unknown>> = {\n algorithm_signer: 'default',\n allow_commas_in_identity_templates: false,\n allow_user_key_ids: false,\n allowed_domains_template: false,\n allowed_users_template: false,\n default_extensions_template: false,\n default_user_template: false,\n issuer_ref: 'default',\n key_id_format: '',\n not_before_duration: 30,\n};\n\n/**\n * ⚠️ ABSENT AND EMPTY BOTH COUNT AS \"NEVER SET\". A role this resource wrote omits these\n * fields entirely, so a stricter test would refuse on the provider's own output and\n * deadlock the update after next.\n */\nconst isDefault = (live: Record<string, unknown>, field: string) =>\n live[field] === undefined || live[field] === '' || live[field] === UNMANAGED_DEFAULTS[field];\n\n/**\n * ⛔ THE FIELDS A WRITE WOULD SILENTLY DESTROY. Because the write is a full replace, a role\n * someone tuned by hand — `allowed_user_key_lengths` (a minimum RSA size), a\n * `key_id_format` the audit trail is grepped by, a `not_before_duration` widened to\n * survive clock skew, a non-default issuer — loses all of it the moment this resource\n * decides to update. A vanished key-length floor is a security regression that no plan\n * output would ever mention, and a vanished backdating window breaks signing on exactly\n * the skewed host it was added for. So reconcile refuses instead of writing.\n */\nconst wouldErase = (live: Record<string, unknown>): readonly string[] => {\n const lost = Object.keys(UNMANAGED_DEFAULTS).filter((field) => !isDefault(live, field));\n if (Object.keys(extMap(live['allowed_user_key_lengths'])).length > 0) {\n lost.push('allowed_user_key_lengths');\n }\n return lost.sort();\n};\n\nconst refuse = (message: string) => Effect.die(new Error(message));\n\nexport const BaoSshRoleProvider = () =>\n Provider.effect(\n BaoSshRole,\n Effect.succeed(\n BaoSshRole.Provider.of({\n /**\n * ⛔ `bao list ssh/roles` IS NOT A LIST OF THINGS THIS OWNS. Every role on the mount\n * answers, including the break-glass ones cut by hand at bringup. Returning them\n * would invite Alchemy to adopt — and then delete — the role that is the only way\n * back into the estate.\n */\n list: () => Effect.succeed([]),\n\n read: Effect.fn(function* ({ olds }) {\n const found = yield* readRole(resolve(olds));\n return found?.attributes;\n }),\n\n /**\n * ⛔ IT COMPARES THE LIVE ROLE, NOT THE STORED DIGEST. A role widened in the OpenBao\n * UI — one extra principal in `allowed_users` — is exactly the drift the check-*\n * gates exist to catch, and a provider that trusted its own state would report\n * `noop` straight through it.\n *\n * ⚠️ `wouldErase` is deliberately NOT consulted here. Extra hand-set fields are only\n * at risk when something actually writes; if every managed field already matches,\n * the honest answer is `noop` and nothing gets destroyed.\n */\n diff: Effect.fn(function* ({ news, output }) {\n if (output === undefined || !isResolved(news)) return undefined;\n const form = resolve(news);\n /**\n * ⛔ A RENAMED ROLE IS A NEW PATH, NOT AN EDIT. `ssh/roles/x` and `ssh-host/roles/x`\n * are different mounts with different CAs. Without this, changing `name` or\n * `mount` would write the new role and leave the old one signing certificates\n * for as long as anyone remembered its name.\n */\n if (output.mount !== form.mount || output.name !== form.name) {\n return { action: 'replace' } as const;\n }\n const found = yield* readRole(form);\n if (found === undefined) return { action: 'update' } as const;\n return matches(found.attributes, form)\n ? ({ action: 'noop' } as const)\n : ({ action: 'update' } as const);\n }),\n\n reconcile: Effect.fn(function* ({ news }) {\n const form = resolve(news);\n const path = readPath(form.mount, form.name);\n\n const bad = badDurations(form);\n if (bad.length > 0) {\n return yield* refuse(\n `Bao.SshRole ${path}: ${bad.join(', ')} is not a duration parseDuration understands ` +\n '(`0` or `<n>s|m|h|d`). It would write, then report update on every plan forever.',\n );\n }\n /**\n * ⛔ A USER-CERT ROLE WITH NO PRINCIPALS SIGNS NOTHING. It is not dangerous — it\n * fails closed at sign time — but it is never what was meant, and the failure\n * surfaces hours later as \"ssh stopped working\" rather than here. The opposite\n * case, empty principals that are ALLOWED, is the skeleton key in the header.\n */\n if (\n form.allowUserCertificates &&\n form.allowedUsers.length === 0 &&\n !form.allowEmptyPrincipals\n ) {\n return yield* refuse(\n `Bao.SshRole ${path}: allowUserCertificates with an empty allowedUsers and ` +\n 'allowEmptyPrincipals false. Every sign request against it would be rejected.',\n );\n }\n\n const found = yield* readRole(form);\n if (found === undefined || !matches(found.attributes, form)) {\n if (found !== undefined) {\n const lost = wouldErase(found.live);\n if (lost.length > 0) {\n return yield* refuse(\n `Bao.SshRole ${path}: live role carries ${lost.join(', ')}, which this ` +\n 'resource does not declare and the full-replace write would ERASE. ' +\n 'Fold those settings into the declaration, or clear them by hand first.',\n );\n }\n }\n yield* baoWrite('PUT', path, writeBody(form));\n }\n /**\n * ⚠️ RE-READ RATHER THAN ECHO THE DECLARATION. The write is the only place the\n * JSON-string encoding of `default_extensions` could be wrong (see the ⛔ on\n * mapValue), and a provider that returned its own props would record a role it\n * had never confirmed.\n */\n const after = yield* readRole(form);\n if (after === undefined) {\n return yield* refuse(\n `Bao.SshRole ${path}: write returned no error but the role is still absent.`,\n );\n }\n return after.attributes;\n }),\n\n /**\n * ⛔ DELETING THE ROLE DOES NOT REVOKE THE CERTIFICATES IT ALREADY SIGNED. OpenSSH\n * has no CRL here: every outstanding certificate stays valid for the rest of its\n * TTL, on every host trusting the CA, with no way to call it back short of a KRL\n * on each host. On the live `ssh-host` role that TTL is 365 days. Deleting is\n * therefore a change to who gets in NEXT, never a way to lock someone out now.\n * Idempotent as Alchemy requires — already gone is success.\n */\n delete: Effect.fn(function* ({ output }) {\n yield* baoDelete(readPath(output.mount, output.name));\n return undefined;\n }),\n }),\n ),\n );\n",
28
+ "/**\n * The wire-and-form half of Bao.SshRole: normalisation, argv, and the equality that decides\n * whether a plan says `noop`. Split from ssh-role.ts for the 250-line cap.\n *\n * ⚠️ EVERY FIELD TYPE BELOW WAS MEASURED, not assumed — OpenBao v2.6.2, read from the live\n * engine's own `sys/internal/specs/openapi`, schema `SshWriteRoleRequest`:\n * `allowed_users`, `allowed_extensions`, `allowed_critical_options`, `allowed_domains`\n * → `string`. ONE COMMA-SEPARATED STRING. Not a list, however much it reads like one.\n * `default_extensions`, `default_critical_options`, `allowed_user_key_lengths`\n * → `object`, `format: map`.\n * `ttl`, `max_ttl`, `not_before_duration` → `integer`, SECONDS.\n * A role read echoes exactly those types back, which is why csvList and extMap exist.\n */\nimport { sha256 } from './digest.ts';\nimport { parseDuration, ttlSeconds } from './mount-form.ts';\n\nexport interface BaoSshRoleProps {\n /** Role name, as `<mount>/roles/{name}` takes it. */\n name: string;\n /** Secrets engine mount, no trailing slash. Defaults to `ssh`; `ssh-host` also exists. */\n mount?: string;\n /** Principals a certificate may carry. `['*']` is OpenBao's wildcard. */\n allowedUsers: readonly string[];\n /** Principal used when a sign request does not name one. */\n defaultUser?: string;\n /** Sign user certificates. Defaults to false — OpenBao's own default. */\n allowUserCertificates?: boolean;\n /** Sign host certificates. Defaults to false — OpenBao's own default. */\n allowHostCertificates?: boolean;\n /** Host-cert roles only: the domains a host certificate may claim. */\n allowedDomains?: readonly string[];\n /** Host-cert roles only: allow `host.example.com` under an allowed `example.com`. */\n allowSubdomains?: boolean;\n /** Host-cert roles only: allow the bare allowed domain itself. */\n allowBareDomains?: boolean;\n /**\n * Extensions a REQUESTER may ask for — not the ones that get stamped on. `[]` means none\n * may be requested, which is what all three live roles carry; `['*']` allows any.\n */\n allowedExtensions?: readonly string[];\n /** Extensions stamped on every certificate, e.g. `{ 'permit-pty': '' }`. */\n defaultExtensions?: Readonly<Record<string, string>>;\n /** Critical options a requester may ask for — `force-command`, `source-address`. */\n allowedCriticalOptions?: readonly string[];\n /** Critical options stamped on every certificate. See the ⛔ on erasure in ssh-role.ts. */\n defaultCriticalOptions?: Readonly<Record<string, string>>;\n /** ⛔ Skeleton-key flag — read the ⛔ in ssh-role.ts before setting this true. */\n allowEmptyPrincipals?: boolean;\n /** Certificate lifetime, e.g. `4h`. ⛔ `0` is NOT \"no expiry\" — see ssh-role.ts. */\n ttl: string;\n /** Ceiling on a requested lifetime, e.g. `8h`. */\n maxTtl: string;\n}\n\n/**\n * Every prop resolved to the value the full-replace write will actually produce.\n *\n * ★ Derived rather than retyped: `Required` is the whole difference between the two, and a\n * hand-copied twin is the kind of thing that silently loses a field in review.\n */\nexport type BaoSshRoleForm = Required<BaoSshRoleProps>;\n\n/** ⛔ NO SECRET HERE — names, flags, TTLs and a digest. The CA key is out of scope. */\nexport interface BaoSshRoleAttributes extends Omit<BaoSshRoleForm, 'maxTtl' | 'ttl'> {\n /**\n * ⚠️ SECONDS, NOT A DURATION STRING. OpenBao stores seconds and a plan should show what is\n * stored; `4h` in the props and 14400 here are the same role. Rendering seconds back\n * into `4h` needs a table of unit rules that mount-form.ts and auth-role-form.ts each\n * already carry a private copy of — a third copy is how the three drift apart.\n */\n ttlSeconds: number;\n maxTtlSeconds: number;\n /** Always `ca` on a healthy role. Stored so an otp/dynamic role reads as a mismatch. */\n keyType: string;\n /** SHA-256 of the managed fields — safe to persist; see policy.ts. */\n digest: string;\n}\n\n/** Mount path without a trailing slash; `ssh` when the caller did not say. */\nexport const mountOf = (mount: string | undefined) => (mount ?? 'ssh').replace(/\\/+$/, '');\n\nexport const readPath = (mount: string | undefined, name: string) =>\n `${mountOf(mount)}/roles/${name}`;\n\nconst text = (value: unknown) => (typeof value === 'string' ? value : '');\n\nconst bool = (value: unknown, fallback: boolean) => (typeof value === 'boolean' ? value : fallback);\n\n/**\n * ⚠️ SETS, NOT SEQUENCES. OpenBao stores these as one comma-separated string and promises\n * nothing about the order it hands back. Sorting and de-duplicating on BOTH sides is what\n * stops every plan from reporting an update on a role nobody touched.\n */\nexport const csvList = (value: unknown): readonly string[] => {\n const parts = Array.isArray(value)\n ? value.filter((entry): entry is string => typeof entry === 'string')\n : text(value).split(',');\n return [\n ...new Set(parts.map((entry) => entry.trim()).filter((entry) => entry.length > 0)),\n ].sort();\n};\n\n/**\n * ⚠️ A MAP WITH NO PROMISED ORDER. Key order also decides the digest and the argv, so it is\n * normalised once here and everything else reads the sorted copy.\n */\nexport const extMap = (value: unknown): Record<string, string> => {\n const out: Record<string, string> = {};\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return out;\n const source = value as Record<string, unknown>;\n for (const key of Object.keys(source).sort()) out[key] = text(source[key]);\n return out;\n};\n\n/** ⛔ Every default here is what the WRITE produces, not what felt sensible. */\nexport const resolve = (props: BaoSshRoleProps): BaoSshRoleForm => ({\n name: props.name,\n mount: mountOf(props.mount),\n allowedUsers: csvList(props.allowedUsers),\n defaultUser: props.defaultUser ?? '',\n allowUserCertificates: props.allowUserCertificates ?? false,\n allowHostCertificates: props.allowHostCertificates ?? false,\n allowedDomains: csvList(props.allowedDomains),\n allowSubdomains: props.allowSubdomains ?? false,\n allowBareDomains: props.allowBareDomains ?? false,\n allowedExtensions: csvList(props.allowedExtensions),\n defaultExtensions: extMap(props.defaultExtensions ?? {}),\n allowedCriticalOptions: csvList(props.allowedCriticalOptions),\n defaultCriticalOptions: extMap(props.defaultCriticalOptions ?? {}),\n allowEmptyPrincipals: props.allowEmptyPrincipals ?? false,\n ttl: props.ttl,\n maxTtl: props.maxTtl,\n});\n\nexport const attributesOf = (\n form: BaoSshRoleForm,\n live: Record<string, unknown>,\n): BaoSshRoleAttributes => {\n const attrs = {\n name: form.name,\n mount: form.mount,\n allowedUsers: csvList(live['allowed_users']),\n defaultUser: text(live['default_user']),\n allowUserCertificates: bool(live['allow_user_certificates'], false),\n allowHostCertificates: bool(live['allow_host_certificates'], false),\n allowedDomains: csvList(live['allowed_domains']),\n allowSubdomains: bool(live['allow_subdomains'], false),\n allowBareDomains: bool(live['allow_bare_domains'], false),\n allowedExtensions: csvList(live['allowed_extensions']),\n defaultExtensions: extMap(live['default_extensions']),\n allowedCriticalOptions: csvList(live['allowed_critical_options']),\n defaultCriticalOptions: extMap(live['default_critical_options']),\n allowEmptyPrincipals: bool(live['allow_empty_principals'], false),\n keyType: text(live['key_type']) || 'ca',\n ttlSeconds: ttlSeconds(live['ttl']) ?? 0,\n maxTtlSeconds: ttlSeconds(live['max_ttl']) ?? 0,\n };\n return { ...attrs, digest: sha256(JSON.stringify(attrs)) };\n};\n\n/**\n * ⛔ THE MAPS CROSS THE WIRE AS A JSON STRING, AND THAT IS THE CORRECT SHAPE. MEASURED with\n * `bao write -output-curl-string` against the live v2.6.2 (no write was sent): the CLI\n * puts every `k=v` value in the body as a string, so `default_extensions` leaves as\n * `\"{\\\"permit-pty\\\":\\\"\\\"}\"` and never as an object. OpenBao's own OpenAPI types that field\n * `object`/`format: map` — a `framework` TypeMap, which JSON-decodes a string value.\n * ⚠️ THAT DECODE IS REASONED, NOT MEASURED: writing to the live engine was out of bounds,\n * so the round trip was never executed. If a first deploy lands a role whose\n * default_extensions came out EMPTY, this is the line that was wrong, and the fix is to\n * send the map as a JSON object in writeBody rather than as this string — the HTTP body\n * can now carry either.\n */\nconst mapValue = (map: Readonly<Record<string, string>>) => JSON.stringify(extMap(map));\n\n/**\n * ⛔ EVERY MANAGED FIELD IS SENT ON EVERY WRITE, INCLUDING THE FALSE ONES. The role write is\n * a full replace (see ssh-role.ts), so an omitted field is not \"left alone\" — it is reset.\n * Sending the resolved form makes the body and the declaration the same statement. All\n * strings, exactly the `k=v` pairs `bao write` sent, for `PUT <mount>/roles/<name>`.\n */\nexport const writeBody = (form: BaoSshRoleForm): Record<string, string> => ({\n key_type: 'ca',\n allowed_users: form.allowedUsers.join(','),\n default_user: form.defaultUser,\n allow_user_certificates: String(form.allowUserCertificates),\n allow_host_certificates: String(form.allowHostCertificates),\n allowed_domains: form.allowedDomains.join(','),\n allow_subdomains: String(form.allowSubdomains),\n allow_bare_domains: String(form.allowBareDomains),\n allowed_extensions: form.allowedExtensions.join(','),\n default_extensions: mapValue(form.defaultExtensions),\n allowed_critical_options: form.allowedCriticalOptions.join(','),\n default_critical_options: mapValue(form.defaultCriticalOptions),\n allow_empty_principals: String(form.allowEmptyPrincipals),\n ttl: form.ttl,\n max_ttl: form.maxTtl,\n});\n\nconst sameList = (want: readonly string[], have: readonly string[]) =>\n want.length === have.length && want.every((entry, index) => entry === have[index]);\n\nconst sameMap = (\n want: Readonly<Record<string, string>>,\n have: Readonly<Record<string, string>>,\n) => {\n const keys = Object.keys(want);\n return keys.length === Object.keys(have).length && keys.every((key) => have[key] === want[key]);\n};\n\n/**\n * Compare in seconds, never as text — `60m` and `1h` are the same role.\n * ⚠️ An unparseable prop yields undefined, which equals no number, so it reports drift\n * rather than a false noop. `badDurations` is what turns that into a readable refusal.\n */\nconst sameTtl = (want: string, have: number) => parseDuration(want) === have;\n\n/**\n * ⛔ AN UNPARSEABLE TTL IS A PLAN THAT NEVER GOES GREEN. parseDuration understands `0` and\n * `<n>[smhd]` and nothing else. A prop like `4h30m` writes fine, reads back as 16200,\n * fails to parse on the want side, and reports `update` forever — and a resource that is\n * always dirty is one nobody reads the plan for any more.\n */\nexport const badDurations = (form: BaoSshRoleForm): readonly string[] =>\n (['ttl', 'maxTtl'] as const).filter((field) => parseDuration(form[field]) === undefined);\n\n/**\n * True when live already matches the declaration on every managed field.\n * ⛔ NOTHING IS SKIPPED FOR BEING UNDEFINED. mount.ts and auth-role.ts can treat an absent\n * prop as \"don't care\" because their writes are partial tunes; this one cannot, because\n * the write resets whatever it omits. Comparing the resolved form is what keeps a\n * hand-edited role from surviving forever behind a green plan.\n */\nexport const matches = (attributes: BaoSshRoleAttributes, form: BaoSshRoleForm) =>\n attributes.keyType === 'ca' &&\n sameList(form.allowedUsers, attributes.allowedUsers) &&\n attributes.defaultUser === form.defaultUser &&\n attributes.allowUserCertificates === form.allowUserCertificates &&\n attributes.allowHostCertificates === form.allowHostCertificates &&\n sameList(form.allowedDomains, attributes.allowedDomains) &&\n attributes.allowSubdomains === form.allowSubdomains &&\n attributes.allowBareDomains === form.allowBareDomains &&\n sameList(form.allowedExtensions, attributes.allowedExtensions) &&\n sameMap(form.defaultExtensions, attributes.defaultExtensions) &&\n sameList(form.allowedCriticalOptions, attributes.allowedCriticalOptions) &&\n sameMap(form.defaultCriticalOptions, attributes.defaultCriticalOptions) &&\n attributes.allowEmptyPrincipals === form.allowEmptyPrincipals &&\n sameTtl(form.ttl, attributes.ttlSeconds) &&\n sameTtl(form.maxTtl, attributes.maxTtlSeconds);\n"
29
+ ],
30
+ "mappings": "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",
31
+ "debugId": "72D46173613CE4CB64756E2164756E21",
32
+ "names": []
33
+ }