@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,180 @@
1
+ /**
2
+ * A Cloudflare policy document as the engine stores it — resolution from names, parsing from a
3
+ * read, and the one comparison every Bao.CloudflareRole plan rests on.
4
+ *
5
+ * MEASURED from the engine source (house/platform/secrets/vault/plugin/cloudflare/path_roles.go)
6
+ * and the 2026-09-14 snapshot of all 586 live roles:
7
+ * • `policies` is a STRING field holding a JSON array (path_roles.go:67-72). Each entry is
8
+ * `{effect, resources: map[string]string, permission_groups: [{id}]}` (:25-33).
9
+ * • A read hands back `json.Marshal` of the stored slice (:199-207): no whitespace, entry and
10
+ * group order preserved (Go slices), resource keys sorted (Go maps). Every snapshot role has
11
+ * exactly that shape.
12
+ */
13
+ import { GROUP_SCOPES, groupKey, scopeOfResource } from './cloudflare-group-scope.ts';
14
+ import type { DeclaredPolicy } from './cloudflare-roles-expand.ts';
15
+
16
+ export interface WirePolicy {
17
+ readonly effect: string;
18
+ readonly resources: Readonly<Record<string, string>>;
19
+ readonly groupIds: readonly string[];
20
+ }
21
+
22
+ /** A declared entry with its IDs, still carrying how its group order is to be compared. */
23
+ export interface ResolvedPolicy extends WirePolicy {
24
+ readonly groupOrder: DeclaredPolicy['groupOrder'];
25
+ }
26
+
27
+ /**
28
+ * One name at every scope — what a source that cannot see scopes can honestly offer.
29
+ *
30
+ * ⚠️ IT IS A WIDENING, AND IT IS THE ASYMMETRY BETWEEN THE TWO SOURCES. The live-roles bootstrap
31
+ * and cloudflare-permission-groups-solve.ts recover `name → id` from roles that carry ids and no
32
+ * scopes, so they cannot tell the seven two-id names apart; the id they learned is offered for
33
+ * whatever scope an entry asks. The engine source keys by (name, scope) and never does this.
34
+ */
35
+ export const atEveryScope = (byName: ReadonlyMap<string, string>): ReadonlyMap<string, string> =>
36
+ new Map(
37
+ [...byName].flatMap(([name, id]) =>
38
+ Object.values(GROUP_SCOPES).map((scope) => [groupKey(name, scope), id] as const),
39
+ ),
40
+ );
41
+
42
+ /**
43
+ * Declared names to IDs, or the names `ids` lacks.
44
+ *
45
+ * ⛔ AN ENTRY LOOKS ITS GROUPS UP AT ITS OWN SCOPE (`groupKey`), not by name: a zone entry and an
46
+ * account entry of the same role can name one group and mean two different ids.
47
+ * ★ THE ACCOUNT ENTRY IS SORTED BY ID, EXACTLY AS cfhf.py:180 DID. ⚠️ Plain `sort()`, never
48
+ * `localeCompare`: Python's `sorted()` on str is codepoint order, and JavaScript's default sort
49
+ * is UTF-16 code-unit order, which agree on the lowercase hex these IDs are made of.
50
+ */
51
+ export const resolvePolicies = (
52
+ declared: readonly DeclaredPolicy[],
53
+ ids: ReadonlyMap<string, string>,
54
+ ): { readonly policies: ResolvedPolicy[]; readonly missing: string[] } => {
55
+ const missing = new Set<string>();
56
+ const policies = declared.map((policy) => {
57
+ /**
58
+ * ⛔ THE ENTRY'S OWN WORD BEATS ITS RESOURCE. `scope` is roles.yaml `groups_scope:` reaching
59
+ * here unchanged; `scopeOfResource` is the default for the 639 of 643 references that say
60
+ * nothing. The four that do are `logs`/`logs-read` on both security mounts — ACCOUNT roles
61
+ * whose live entry carries the ZONE-scoped `Logs` ids, which is how Cloudflare writes "all
62
+ * zones from an account" (cf-mint-analytics-tokens.py:155-158).
63
+ */
64
+ const scope = policy.scope ?? scopeOfResource(policy.resource);
65
+ const groupIds = policy.groups.flatMap((name) => {
66
+ const id = ids.get(groupKey(name, scope));
67
+ if (id === undefined) missing.add(name);
68
+ return id === undefined ? [] : [id];
69
+ });
70
+ return {
71
+ effect: policy.effect,
72
+ groupIds: policy.groupOrder === 'by-id' ? [...groupIds].sort() : groupIds,
73
+ groupOrder: policy.groupOrder,
74
+ resources: { [policy.resource]: '*' },
75
+ };
76
+ });
77
+ return { missing: [...missing].sort(), policies };
78
+ };
79
+
80
+ const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
81
+ typeof value === 'object' && value !== null && !Array.isArray(value);
82
+
83
+ const stringMap = (value: unknown): Readonly<Record<string, string>> | undefined => {
84
+ if (!isRecord(value)) return undefined;
85
+ const out: Record<string, string> = {};
86
+ for (const [key, item] of Object.entries(value)) {
87
+ if (typeof item !== 'string') return undefined;
88
+ out[key] = item;
89
+ }
90
+ return out;
91
+ };
92
+
93
+ const entryOf = (entry: unknown): WirePolicy | undefined => {
94
+ if (!isRecord(entry) || typeof entry['effect'] !== 'string') return undefined;
95
+ const resources = stringMap(entry['resources']);
96
+ const groups = entry['permission_groups'];
97
+ if (resources === undefined || !Array.isArray(groups)) return undefined;
98
+ const groupIds = groups.flatMap((group) =>
99
+ isRecord(group) && typeof group['id'] === 'string' ? [group['id']] : [],
100
+ );
101
+ return groupIds.length === groups.length
102
+ ? { effect: entry['effect'], groupIds, resources }
103
+ : undefined;
104
+ };
105
+
106
+ /**
107
+ * The live `policies` string parsed, or undefined when it is not a policy document.
108
+ *
109
+ * ⚠️ UNDEFINED IS NOT ABSENCE. It means the role exists and its policies could not be read, which
110
+ * compares unequal to every declaration — so it plans `update`, and reconcile's re-read refuses
111
+ * to call a write that still reads back like that a success.
112
+ */
113
+ export const parseLivePolicies = (raw: unknown): WirePolicy[] | undefined => {
114
+ if (typeof raw !== 'string') return undefined;
115
+ let parsed: unknown;
116
+ try {
117
+ parsed = JSON.parse(raw);
118
+ } catch {
119
+ return undefined;
120
+ }
121
+ if (!Array.isArray(parsed)) return undefined;
122
+ const policies = parsed.map(entryOf);
123
+ return policies.every((policy): policy is WirePolicy => policy !== undefined)
124
+ ? policies
125
+ : undefined;
126
+ };
127
+
128
+ /**
129
+ * The `policies` field for a write — the array cfhf.py:174-180 built, serialized.
130
+ *
131
+ * ⚠️ NOT BYTE-IDENTICAL TO WHAT PYTHON SENT, AND IT NEED NOT BE. `json.dumps` put a space after
132
+ * every `:` and `,`; this emits none. The engine unmarshals the string into structs
133
+ * (path_roles.go:161-166) and stores those, never the text, so the stored role and the read
134
+ * that diff compares are the same either way.
135
+ */
136
+ export const policiesJson = (policies: readonly WirePolicy[]) =>
137
+ JSON.stringify(
138
+ policies.map((policy) => ({
139
+ effect: policy.effect,
140
+ resources: policy.resources,
141
+ permission_groups: policy.groupIds.map((id) => ({ id })),
142
+ })),
143
+ );
144
+
145
+ const sameList = (a: readonly string[], b: readonly string[]) =>
146
+ a.length === b.length && a.every((item, i) => item === b[i]);
147
+
148
+ const sameResources = (
149
+ a: Readonly<Record<string, string>>,
150
+ b: Readonly<Record<string, string>>,
151
+ ) => {
152
+ const keys = Object.keys(a);
153
+ return keys.length === Object.keys(b).length && keys.every((key) => a[key] === b[key]);
154
+ };
155
+
156
+ /**
157
+ * ⛔ THE POLICY EQUALITY.
158
+ *
159
+ * • Entry count and entry ORDER must match: cfhf.py always wrote the scope entry first and the
160
+ * account entry second (cfhf.py:174-180).
161
+ * • Within an entry: effect, the resource map, and the group IDs — IN ORDER for a `declared`
162
+ * entry, because cfhf.py:175 kept yaml order; AS A SORTED LIST for a `by-id` entry, because
163
+ * cfhf.py:180 sorted it, so its order never carried meaning.
164
+ *
165
+ * ⚠️ NOT ORDER-INSENSITIVE EVERYWHERE, ON PURPOSE. Cloudflare treats a group list as a set, so a
166
+ * reordered `declared` entry mints an identical token — and still plans `update` here. The
167
+ * Python would have rewritten it as well, and an adoption that is noop only up to reordering
168
+ * would not prove the port exact.
169
+ */
170
+ export const samePolicies = (want: readonly ResolvedPolicy[], have: readonly WirePolicy[]) =>
171
+ want.length === have.length &&
172
+ want.every((w, i) => {
173
+ const h = have[i];
174
+ if (h === undefined || w.effect !== h.effect || !sameResources(w.resources, h.resources)) {
175
+ return false;
176
+ }
177
+ return w.groupOrder === 'by-id'
178
+ ? sameList([...w.groupIds].sort(), [...h.groupIds].sort())
179
+ : sameList(w.groupIds, h.groupIds);
180
+ });
@@ -0,0 +1,177 @@
1
+ /**
2
+ * The equality Bao.CloudflareRole's diff and reconcile rest on: TTLs as seconds, description
3
+ * exactly, and policies with order significant in a declared entry and insignificant in the
4
+ * account entry the Python sorted. Plus the write body and the refusals.
5
+ */
6
+ import assert from 'node:assert/strict';
7
+ import { describe, it } from 'node:test';
8
+ import { atEveryScope, resolvePolicies } from './cloudflare-policy.ts';
9
+ import {
10
+ type BaoCloudflareRoleProps,
11
+ attributesOf,
12
+ differences,
13
+ liveRoleOf,
14
+ refusalOf,
15
+ writeBody,
16
+ } from './cloudflare-role-form.ts';
17
+
18
+ const ZONE = 'com.cloudflare.api.account.zone.z1';
19
+ const ACCOUNT = 'com.cloudflare.api.account.acct';
20
+
21
+ const PROPS: BaoCloudflareRoleProps = {
22
+ description: 'Deploy with a route. (zone: example.com)',
23
+ maxTtl: '2h',
24
+ mount: 'cloudflare-acme-platform',
25
+ name: 'example-com-workers-deploy-full',
26
+ policies: [
27
+ {
28
+ effect: 'allow',
29
+ groupOrder: 'declared',
30
+ groups: ['Routes Write', 'DNS Write'],
31
+ resource: ZONE,
32
+ },
33
+ {
34
+ effect: 'allow',
35
+ groupOrder: 'by-id',
36
+ groups: ['Scripts Write', 'Containers Write'],
37
+ resource: ACCOUNT,
38
+ },
39
+ ],
40
+ ttl: '30m',
41
+ };
42
+
43
+ const IDS = new Map([
44
+ ['Containers Write', 'c'],
45
+ ['DNS Write', 'd'],
46
+ ['Routes Write', 'r'],
47
+ ['Scripts Write', 's'],
48
+ ]);
49
+
50
+ const { policies: RESOLVED } = resolvePolicies(PROPS.policies, atEveryScope(IDS));
51
+
52
+ /** A policies string in the engine's own marshalled shape. */
53
+ const wire = (entries: [string, string[]][]) =>
54
+ JSON.stringify(
55
+ entries.map(([resource, ids]) => ({
56
+ effect: 'allow',
57
+ resources: { [resource]: '*' },
58
+ permission_groups: ids.map((id) => ({ id })),
59
+ })),
60
+ );
61
+
62
+ const live = (overrides: Record<string, unknown> = {}) =>
63
+ liveRoleOf({
64
+ description: PROPS.description,
65
+ max_ttl: 7200,
66
+ name: PROPS.name,
67
+ policies: wire([
68
+ [ZONE, ['r', 'd']],
69
+ [ACCOUNT, ['c', 's']],
70
+ ]),
71
+ ttl: 1800,
72
+ ...overrides,
73
+ });
74
+
75
+ const fields = (role = live(), props: BaoCloudflareRoleProps = PROPS) =>
76
+ differences(props, RESOLVED, role).map((d) => d.field);
77
+
78
+ describe('differences', () => {
79
+ it('is empty when live matches, TTL text compared to engine seconds', () => {
80
+ assert.deepEqual(fields(), []);
81
+ });
82
+
83
+ it('reads the account entry as a sorted set, because the Python sorted it', () => {
84
+ const reordered = wire([
85
+ [ZONE, ['r', 'd']],
86
+ [ACCOUNT, ['s', 'c']],
87
+ ]);
88
+ assert.deepEqual(fields(live({ policies: reordered })), []);
89
+ });
90
+
91
+ it('keeps group order significant in a declared entry', () => {
92
+ const reordered = wire([
93
+ [ZONE, ['d', 'r']],
94
+ [ACCOUNT, ['c', 's']],
95
+ ]);
96
+ assert.deepEqual(fields(live({ policies: reordered })), ['policies']);
97
+ });
98
+
99
+ // The drift role's shape on 2026-09-14: the same entries, account first.
100
+ it('keeps entry order significant', () => {
101
+ const swapped = wire([
102
+ [ACCOUNT, ['c', 's']],
103
+ [ZONE, ['r', 'd']],
104
+ ]);
105
+ assert.deepEqual(fields(live({ policies: swapped })), ['policies']);
106
+ });
107
+
108
+ it('catches a different resource, a missing group and an extra entry', () => {
109
+ for (const policies of [
110
+ wire([
111
+ [`${ZONE}x`, ['r', 'd']],
112
+ [ACCOUNT, ['c', 's']],
113
+ ]),
114
+ wire([
115
+ [ZONE, ['r', 'd']],
116
+ [ACCOUNT, ['c']],
117
+ ]),
118
+ wire([
119
+ [ZONE, ['r', 'd']],
120
+ [ACCOUNT, ['c', 's']],
121
+ [ACCOUNT, ['c']],
122
+ ]),
123
+ ]) {
124
+ assert.deepEqual(fields(live({ policies })), ['policies'], policies);
125
+ }
126
+ });
127
+
128
+ it('names description, ttl and max_ttl separately', () => {
129
+ const drifted = live({ description: 'widened by hand', max_ttl: 3600, ttl: 600 });
130
+ assert.deepEqual(fields(drifted), ['description', 'ttl', 'max_ttl']);
131
+ });
132
+
133
+ it('treats live policies that are not a policy document as different', () => {
134
+ assert.deepEqual(fields(live({ policies: 'not json' })), ['policies']);
135
+ assert.deepEqual(fields(live({ policies: '[{"effect":"allow"}]' })), ['policies']);
136
+ });
137
+
138
+ it('never equals an unparseable declared ttl, and reconcile refuses it', () => {
139
+ const props = { ...PROPS, ttl: '1h30m' };
140
+ assert.deepEqual(fields(live(), props), ['ttl']);
141
+ assert.match(refusalOf(props) ?? '', /unparseable duration ttl=1h30m/);
142
+ });
143
+ });
144
+
145
+ describe('writeBody', () => {
146
+ it('sends the four fields apply-roles.py sent, policies in the engine shape with sorted account IDs', () => {
147
+ const body = writeBody(PROPS, RESOLVED);
148
+ assert.deepEqual(Object.keys(body).sort(), ['description', 'max_ttl', 'policies', 'ttl']);
149
+ assert.deepEqual([body.ttl, body.max_ttl, body.description], ['30m', '2h', PROPS.description]);
150
+ assert.equal(
151
+ body.policies,
152
+ wire([
153
+ [ZONE, ['r', 'd']],
154
+ [ACCOUNT, ['c', 's']],
155
+ ]),
156
+ );
157
+ });
158
+ });
159
+
160
+ describe('refusalOf and attributesOf', () => {
161
+ it('refuses no policies and a policy with no groups', () => {
162
+ assert.equal(refusalOf(PROPS), undefined);
163
+ assert.equal(refusalOf({ ...PROPS, policies: [] }), 'no policy entries');
164
+ const empty = [
165
+ { effect: 'allow' as const, groupOrder: 'declared' as const, groups: [], resource: ZONE },
166
+ ];
167
+ assert.equal(refusalOf({ ...PROPS, policies: empty }), 'a policy with no groups');
168
+ });
169
+
170
+ it('builds attributes from live only, with a stable digest', () => {
171
+ const attrs = attributesOf(PROPS, live({ description: 'live says this' }));
172
+ assert.equal(attrs.description, 'live says this');
173
+ assert.deepEqual([attrs.ttl, attrs.maxTtl, attrs.mount], [1800, 7200, PROPS.mount]);
174
+ assert.equal(attrs.digest, attributesOf(PROPS, live({ description: 'live says this' })).digest);
175
+ assert.notEqual(attrs.digest, attributesOf(PROPS, live()).digest);
176
+ });
177
+ });
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Props, attributes and field comparison for Bao.CloudflareRole — the resource is
3
+ * cloudflare-role.ts, the policy document rules are cloudflare-policy.ts.
4
+ *
5
+ * MEASURED from the engine (house/platform/secrets/vault/plugin/cloudflare/path_roles.go) and the
6
+ * 2026-09-14 snapshot of all 586 live roles:
7
+ * • A role is name, description, policies, ttl and max_ttl AND NOTHING ELSE (path_roles.go:14-20
8
+ * stores those; :204-210 returns them). Every snapshot read carries exactly those five keys, so
9
+ * `differences` below covers the whole server-side object — no unmanaged field can drift
10
+ * behind a green plan.
11
+ * • ttl and max_ttl are TypeDurationSecond (:73-80) and read back as integer seconds.
12
+ * • ⚠️ A WRITE MERGES, IT DOES NOT REPLACE (:117-146). The handler loads the stored role and
13
+ * overwrites only the fields present — the opposite of the proxmox engine, where an omitted
14
+ * field is zeroed. writeBody sends all four anyway, so a write always lands the whole
15
+ * declaration and can never leave an old description behind a new policy.
16
+ *
17
+ * ⛔ NO SECRET IN PROPS OR ATTRIBUTES, AND THERE IS NONE TO LEAK. A role is permission-group IDs, a
18
+ * zone or account resource, a sentence and two TTLs. The parent token lives at `<mount>/config`
19
+ * and a minted token at `<mount>/creds/<name>`; nothing in this family reads either path. Keep
20
+ * it that way — Alchemy persists attributes unencrypted (see policy.ts).
21
+ */
22
+ import {
23
+ type ResolvedPolicy,
24
+ type WirePolicy,
25
+ parseLivePolicies,
26
+ policiesJson,
27
+ samePolicies,
28
+ } from './cloudflare-policy.ts';
29
+ import type { DeclaredPolicy } from './cloudflare-roles-expand.ts';
30
+ import { sha256 } from './digest.ts';
31
+ import { mountPath, parseDuration } from './mount-form.ts';
32
+
33
+ export interface BaoCloudflareRoleProps {
34
+ /** Mount of the Cloudflare secrets engine, e.g. `cloudflare-<account>-dns`. */
35
+ mount: string;
36
+ /** Role name. ⛔ Consumers mint by it — see cloudflare-roles-expand.ts before changing one. */
37
+ name: string;
38
+ /** The description as written: a zone role's carries its ` (zone: <name>)` suffix. */
39
+ description: string;
40
+ /** Default lease TTL, e.g. `5m`. */
41
+ ttl: string;
42
+ /** Maximum lease TTL, e.g. `1h`. */
43
+ maxTtl: string;
44
+ /**
45
+ * Policy entries, groups by NAME. The provider resolves them through CloudflarePermissionGroups
46
+ * at diff and reconcile time; an ID is never a prop, so a plan can never persist a guessed one.
47
+ */
48
+ policies: DeclaredPolicy[];
49
+ }
50
+
51
+ export interface BaoCloudflareRoleAttributes {
52
+ mount: string;
53
+ name: string;
54
+ description: string;
55
+ /** Live ttl in seconds, as the engine stores it. */
56
+ ttl: number;
57
+ /** Live max_ttl in seconds. */
58
+ maxTtl: number;
59
+ /** Live `policies` JSON — group IDs and a resource, never a credential. */
60
+ policies: string;
61
+ /** SHA-256 of the fields above — safe to persist; see policy.ts. */
62
+ digest: string;
63
+ }
64
+
65
+ /** One live role, as a read returns it. */
66
+ export interface LiveCloudflareRole {
67
+ readonly name: string;
68
+ readonly description: string;
69
+ readonly ttl: number | undefined;
70
+ readonly maxTtl: number | undefined;
71
+ readonly policiesText: string;
72
+ /** Undefined when `policies` is not a policy document — see parseLivePolicies. */
73
+ readonly policies: readonly WirePolicy[] | undefined;
74
+ }
75
+
76
+ export interface RoleDifference {
77
+ readonly field: 'description' | 'max_ttl' | 'policies' | 'ttl';
78
+ readonly want: string;
79
+ readonly have: string;
80
+ }
81
+
82
+ /** `cloudflare-<account>-dns/roles/homeflare-dev-dns-read` — read, write and delete use it. */
83
+ export const rolePath = (mount: string, name: string) => `${mountPath(mount)}/roles/${name}`;
84
+
85
+ const seconds = (value: unknown) =>
86
+ typeof value === 'number' && Number.isFinite(value) ? value : undefined;
87
+
88
+ /** A read's `data`, shaped. Lenient on purpose: a malformed field compares unequal, loudly. */
89
+ export const liveRoleOf = (data: Readonly<Record<string, unknown>>): LiveCloudflareRole => ({
90
+ description: typeof data['description'] === 'string' ? data['description'] : '',
91
+ maxTtl: seconds(data['max_ttl']),
92
+ name: typeof data['name'] === 'string' ? data['name'] : '',
93
+ policies: parseLivePolicies(data['policies']),
94
+ policiesText: typeof data['policies'] === 'string' ? data['policies'] : '',
95
+ ttl: seconds(data['ttl']),
96
+ });
97
+
98
+ /**
99
+ * ⚠️ Every value is taken from `live`, the prop only standing in for a missing `name` — the same
100
+ * split the sibling families use, so a read never launders the declaration back as "current".
101
+ */
102
+ export const attributesOf = (
103
+ props: Pick<BaoCloudflareRoleProps, 'mount' | 'name'>,
104
+ live: LiveCloudflareRole,
105
+ ): BaoCloudflareRoleAttributes => {
106
+ const attrs = {
107
+ description: live.description,
108
+ maxTtl: live.maxTtl ?? 0,
109
+ mount: mountPath(props.mount),
110
+ name: live.name || props.name,
111
+ policies: live.policiesText,
112
+ ttl: live.ttl ?? 0,
113
+ };
114
+ return { ...attrs, digest: sha256(JSON.stringify(attrs)) };
115
+ };
116
+
117
+ /**
118
+ * Why reconcile must refuse this declaration, or undefined.
119
+ *
120
+ * ⛔ AN UNPARSEABLE TTL IS A REFUSAL, NOT A ZERO: it can never equal the live seconds, so it would
121
+ * plan `update` forever. ⛔ AN EMPTY POLICY LIST OR AN ENTRY WITH NO GROUPS IS A REFUSAL TOO — the
122
+ * engine rejects both (path_roles.go:147-149, :175-180), and saying so before the write names the
123
+ * declaration instead of quoting the server.
124
+ */
125
+ export const refusalOf = (props: BaoCloudflareRoleProps): string | undefined => {
126
+ const bad = [
127
+ ...(parseDuration(props.ttl) === undefined ? [`ttl=${props.ttl}`] : []),
128
+ ...(parseDuration(props.maxTtl) === undefined ? [`maxTtl=${props.maxTtl}`] : []),
129
+ ];
130
+ if (bad.length > 0) return `unparseable duration ${bad.join(', ')} (use 30s / 5m / 1h / 1d)`;
131
+ if (props.policies.length === 0) return 'no policy entries';
132
+ if (props.policies.some((policy) => policy.groups.length === 0)) return 'a policy with no groups';
133
+ return undefined;
134
+ };
135
+
136
+ /**
137
+ * The body for `PUT <mount>/roles/<name>` — the four `k=v` pairs apply-roles.py:157-162 sent.
138
+ *
139
+ * ★ THE TTLs GO AS THE DECLARED TEXT (`5m`), which is what `bao write ttl=5m` put in the JSON body,
140
+ * and TypeDurationSecond parses it server-side. `policies` is a string because the field is a
141
+ * string (path_roles.go:67-68), not because the engine wants the whole body stringly-typed.
142
+ */
143
+ export const writeBody = (props: BaoCloudflareRoleProps, policies: readonly WirePolicy[]) => ({
144
+ description: props.description,
145
+ max_ttl: props.maxTtl,
146
+ policies: policiesJson(policies),
147
+ ttl: props.ttl,
148
+ });
149
+
150
+ const sameTtl = (want: string, have: number | undefined) => {
151
+ const wantSeconds = parseDuration(want);
152
+ return wantSeconds !== undefined && wantSeconds === have;
153
+ };
154
+
155
+ /**
156
+ * Every field where live differs from the resolved declaration. Empty means noop.
157
+ *
158
+ * ⛔ apply-roles.py --verify COMPARED NONE OF THESE. It lists `<mount>/roles` and reports names
159
+ * only: MISSING when roles.yaml has a role the vault lacks, UNTRACKED for the reverse
160
+ * (apply-roles.py:136-141, :167-173, :179-185). It never read a role. So this equality is new: the
161
+ * four fields --apply WRITES (apply-roles.py:157-162), compared the way the engine reads them back
162
+ * — durations as seconds, description exactly, policies by the rules in samePolicies.
163
+ */
164
+ export const differences = (
165
+ props: Pick<BaoCloudflareRoleProps, 'description' | 'maxTtl' | 'ttl'>,
166
+ policies: readonly ResolvedPolicy[],
167
+ live: LiveCloudflareRole,
168
+ ): RoleDifference[] => {
169
+ const out: RoleDifference[] = [];
170
+ if (props.description !== live.description) {
171
+ out.push({ field: 'description', have: live.description, want: props.description });
172
+ }
173
+ if (!sameTtl(props.ttl, live.ttl)) {
174
+ out.push({ field: 'ttl', have: String(live.ttl), want: props.ttl });
175
+ }
176
+ if (!sameTtl(props.maxTtl, live.maxTtl)) {
177
+ out.push({ field: 'max_ttl', have: String(live.maxTtl), want: props.maxTtl });
178
+ }
179
+ if (live.policies === undefined || !samePolicies(policies, live.policies)) {
180
+ out.push({ field: 'policies', have: live.policiesText, want: policiesJson(policies) });
181
+ }
182
+ return out;
183
+ };
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Bao.CloudflareRole's calls against a fake that behaves like the Cloudflare engine where it matters:
3
+ * a write parses the duration text into seconds and re-marshals `policies` (path_roles.go:117-146,
4
+ * :199-210), a role never written is a 404, and a delete answers 204 whether or not it existed.
5
+ */
6
+ import assert from 'node:assert/strict';
7
+ import { describe, it } from 'node:test';
8
+ import { BaoError } from './bao-status.ts';
9
+ import { atEveryScope, resolvePolicies } from './cloudflare-policy.ts';
10
+ import { type BaoCloudflareRoleProps, differences, writeBody } from './cloudflare-role-form.ts';
11
+ import {
12
+ deleteCloudflareRole,
13
+ readCloudflareRole,
14
+ writeCloudflareRole,
15
+ } from './cloudflare-role-wire.ts';
16
+ import { type Reply, type Seen, run, runFailure, withFake } from './fake-bao.ts';
17
+ import { parseDuration } from './mount-form.ts';
18
+
19
+ const PROPS: BaoCloudflareRoleProps = {
20
+ description: 'Read DNS records on one zone. (zone: example.com)',
21
+ maxTtl: '1h',
22
+ mount: 'cloudflare-acme-dns',
23
+ name: 'example-com-dns-read',
24
+ policies: [
25
+ {
26
+ effect: 'allow',
27
+ groupOrder: 'declared',
28
+ groups: ['DNS Read'],
29
+ resource: 'com.cloudflare.api.account.zone.z1',
30
+ },
31
+ ],
32
+ ttl: '5m',
33
+ };
34
+
35
+ const PATH = '/v1/cloudflare-acme-dns/roles/example-com-dns-read';
36
+
37
+ /** The engine's role store, reduced to what these calls can observe. */
38
+ const roleStore = () => {
39
+ const roles = new Map<string, Record<string, unknown>>();
40
+ return (seen: Seen): Reply => {
41
+ if (seen.method === 'PUT') {
42
+ const body = JSON.parse(seen.body) as Record<string, string>;
43
+ roles.set(seen.path, {
44
+ description: body['description'],
45
+ max_ttl: parseDuration(body['max_ttl'] ?? ''),
46
+ name: seen.path.split('/').at(-1),
47
+ // ★ Re-marshalled, as json.Marshal of the stored structs would be.
48
+ policies: JSON.stringify(JSON.parse(body['policies'] ?? 'null')),
49
+ ttl: parseDuration(body['ttl'] ?? ''),
50
+ });
51
+ return { status: 204 };
52
+ }
53
+ if (seen.method === 'DELETE') {
54
+ roles.delete(seen.path);
55
+ return { status: 204 };
56
+ }
57
+ const role = roles.get(seen.path);
58
+ return role === undefined
59
+ ? { json: { errors: [] }, status: 404 }
60
+ : { json: { data: role }, status: 200 };
61
+ };
62
+ };
63
+
64
+ describe('Cloudflare role wire', () => {
65
+ it('reads a role that was never written as absent', async () => {
66
+ await withFake(roleStore(), async (bao) => {
67
+ const env = { BAO_ADDR: bao.address };
68
+ assert.equal(await run(env, readCloudflareRole(PROPS.mount, PROPS.name)), undefined);
69
+ assert.deepEqual([bao.seen[0]?.method, bao.seen[0]?.path], ['GET', PATH]);
70
+ });
71
+ });
72
+
73
+ it('writes the four fields and reads them back with no difference', async () => {
74
+ await withFake(roleStore(), async (bao) => {
75
+ const env = { BAO_ADDR: bao.address };
76
+ const { policies } = resolvePolicies(
77
+ PROPS.policies,
78
+ atEveryScope(new Map([['DNS Read', 'id-read']])),
79
+ );
80
+ await run(env, writeCloudflareRole(PROPS.mount, PROPS.name, writeBody(PROPS, policies)));
81
+ const put = bao.seen[0];
82
+ assert.deepEqual([put?.method, put?.path], ['PUT', PATH]);
83
+ assert.equal(put?.headers.get('content-type'), 'application/json');
84
+ assert.deepEqual(JSON.parse(put?.body ?? ''), {
85
+ description: PROPS.description,
86
+ max_ttl: '1h',
87
+ policies:
88
+ '[{"effect":"allow","resources":{"com.cloudflare.api.account.zone.z1":"*"},' +
89
+ '"permission_groups":[{"id":"id-read"}]}]',
90
+ ttl: '5m',
91
+ });
92
+ const live = await run(env, readCloudflareRole(PROPS.mount, PROPS.name));
93
+ assert.ok(live);
94
+ assert.deepEqual([live.ttl, live.maxTtl], [300, 3600]);
95
+ assert.deepEqual(differences(PROPS, policies, live), []);
96
+ });
97
+ });
98
+
99
+ it('fails a refused read (403) instead of calling the role absent', async () => {
100
+ await withFake(
101
+ () => ({ json: { errors: ['permission denied'] }, status: 403 }),
102
+ async (bao) => {
103
+ const error = await runFailure({ BAO_ADDR: bao.address }, readCloudflareRole('m', 'r'));
104
+ assert.ok(error instanceof BaoError);
105
+ assert.equal(error.status, 403);
106
+ },
107
+ );
108
+ });
109
+
110
+ it('fails a write to a mount that is not there (404), carrying OpenBao errors', async () => {
111
+ const errors = ['no handler for route "cloudflare-nope/roles/r". route entry not found.'];
112
+ await withFake(
113
+ () => ({ json: { errors }, status: 404 }),
114
+ async (bao) => {
115
+ const write = writeCloudflareRole('cloudflare-nope', 'r', { ttl: '5m' });
116
+ const error = await runFailure({ BAO_ADDR: bao.address }, write);
117
+ assert.ok(error instanceof BaoError);
118
+ assert.deepEqual(error.errors, errors);
119
+ },
120
+ );
121
+ });
122
+
123
+ it('deletes idempotently: a role already gone is success', async () => {
124
+ await withFake(
125
+ () => ({ json: { errors: [] }, status: 404 }),
126
+ async (bao) => {
127
+ await run({ BAO_ADDR: bao.address }, deleteCloudflareRole(PROPS.mount, PROPS.name));
128
+ assert.deepEqual([bao.seen[0]?.method, bao.seen[0]?.path], ['DELETE', PATH]);
129
+ },
130
+ );
131
+ });
132
+ });