@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,231 @@
1
+ /**
2
+ * The roles.yaml expansion — surfaces × accounts × zones into concrete roles — ported from
3
+ * cfroles.py `expand` and cfhf.py `policy_for`, with permission groups still by NAME.
4
+ *
5
+ * ★ PURE, AND NAMES STAY NAMES. The Python resolved names to IDs in the same pass
6
+ * (apply-roles.py:144-150), with a Cloudflare credential in hand. Agents may not hold that
7
+ * credential, so resolution is a separate step behind one interface
8
+ * (cloudflare-permission-groups.ts) and nothing here knows where IDs come from.
9
+ *
10
+ * ⛔ THE NAMING AND THE ZONE RULES ARE CONTRACTS WITH EVERY CONSUMER. apply-consumers.py grants
11
+ * `creds/<slug(zone)>-*` by zone (apply-consumers.py:117-120) and agents mint by role name, so a
12
+ * role a port renames is a role nothing can mint. Each rule cites the line it came from, and
13
+ * cloudflare/parity.ts proves the whole expansion against production.
14
+ */
15
+ import { accountResource, bucketResource, zoneResource } from './cloudflare-group-scope.ts';
16
+ import type { AccountSpec, RoleTemplate, RolesConfig } from './cloudflare-roles-config.ts';
17
+
18
+ /** A zone as the expansion needs it — what cfhf.py load_account listed (cfhf.py:134-146). */
19
+ export interface CloudflareZone {
20
+ readonly name: string;
21
+ readonly id: string;
22
+ /** From the `type=internal` listing. ⚠️ Shares its NAME with the public zone, never its id. */
23
+ readonly internal: boolean;
24
+ }
25
+
26
+ /**
27
+ * One policy entry, groups by name.
28
+ *
29
+ * ★ `groupOrder` CARRIES A PYTHON RULE THAT ONLY EXISTS AFTER RESOLUTION. cfhf.py:180 sorts the
30
+ * account entry's group IDS — not its names — so that order cannot be computed here, only
31
+ * promised. A `declared` entry keeps roles.yaml order, exactly as cfhf.py:175 emitted it.
32
+ */
33
+ export interface DeclaredPolicy {
34
+ readonly effect: 'allow';
35
+ readonly resource: string;
36
+ readonly groups: readonly string[];
37
+ readonly groupOrder: 'declared' | 'by-id';
38
+ /**
39
+ * The scope these NAMES resolve at, when it is not this entry's own resource namespace —
40
+ * roles.yaml `groups_scope:`, already a GROUP_SCOPES value. Undefined on every entry that does
41
+ * not say otherwise, which is 639 of the estate's 643 group references.
42
+ *
43
+ * ⛔ IT MOVES THE LOOKUP, NEVER THE RESOURCE. The entry still binds the groups to the resource
44
+ * above; only the id chosen for each name changes. That is the shape Cloudflare calls "all
45
+ * zones from an account" — the ACCOUNT resource carrying ZONE-scoped groups
46
+ * (house/nix/homeflare-config/scripts/cf-mint-analytics-tokens.py:155-158, beside a working
47
+ * token). Without this field the resource is the only witness, and it says `account`.
48
+ */
49
+ readonly scope?: string;
50
+ }
51
+
52
+ /** What apply-roles.py wrote to the KV catalog for one role (apply-roles.py:58-62). */
53
+ export interface CatalogView {
54
+ readonly description: string;
55
+ readonly zone: string | null;
56
+ readonly ttl: string;
57
+ readonly maxTtl: string;
58
+ readonly permissions: readonly string[];
59
+ }
60
+
61
+ export interface ExpandedRole {
62
+ readonly account: string;
63
+ readonly accountId: string;
64
+ readonly mount: string;
65
+ readonly surface: string;
66
+ readonly name: string;
67
+ /** Exactly what apply-roles.py:160-161 wrote — the zone suffix included. */
68
+ readonly description: string;
69
+ readonly ttl: string;
70
+ readonly maxTtl: string;
71
+ readonly policies: readonly DeclaredPolicy[];
72
+ readonly catalog: CatalogView;
73
+ }
74
+
75
+ /** cfroles.py:18-20 — `example.com` → `homeflare-dev`. */
76
+ export const slug = (name: string) =>
77
+ name
78
+ .toLowerCase()
79
+ .replace(/[^a-z0-9]+/g, '-')
80
+ .replace(/^-+|-+$/g, '');
81
+
82
+ /**
83
+ * cfroles.py:23-38. ⛔ THE `-internal` SUFFIX IS WHAT KEEPS TWO ZONES OF ONE NAME APART. Slugging
84
+ * both example.com zones alike would leave two roles fighting for one name, and whichever
85
+ * wrote last would silently own it.
86
+ */
87
+ export const zoneSlug = (zone: CloudflareZone) =>
88
+ `${slug(zone.name)}${zone.internal ? '-internal' : ''}`;
89
+
90
+ /** cfroles.py:46-47 — one mount, and so one parent token, per (account, surface). */
91
+ export const mountName = (account: string, surface: string) => `cloudflare-${account}-${surface}`;
92
+
93
+ /**
94
+ * cfhf.py:151-181. ⛔ ONE ENTRY PER SCOPE, NEVER MERGED: Cloudflare gates a permission at one
95
+ * scope only, so a zone role that also needs account groups gets a SECOND entry on the account
96
+ * resource. ⚠️ `if account_group_ids:` — an empty list emits no second entry (cfhf.py:176).
97
+ */
98
+ const policiesFor = (
99
+ tpl: RoleTemplate,
100
+ accountId: string,
101
+ zone: CloudflareZone | null,
102
+ accountGroups: readonly string[],
103
+ ): DeclaredPolicy[] => {
104
+ /**
105
+ * ⛔ A BUCKET NARROWS THE ACCOUNT RESOURCE; IT NEVER NARROWS A ZONE. A zone role scoped to an R2
106
+ * bucket is not a narrower grant, it is an incoherent one — the groups a zone role carries are
107
+ * gated at the zone and would be refused against a bucket resource. Refusing here is the only
108
+ * place that can tell the difference; roles.yaml cannot express the contradiction as a type.
109
+ */
110
+ if (tpl.bucket !== undefined && zone !== null) {
111
+ throw new Error(`${tpl.name}: a zone role cannot be scoped to R2 bucket ${tpl.bucket}`);
112
+ }
113
+ const accountScoped =
114
+ tpl.bucket === undefined
115
+ ? accountResource(accountId)
116
+ : bucketResource(accountId, tpl.bucketJurisdiction, tpl.bucket);
117
+ const resource = zone === null ? accountScoped : zoneResource(zone.id);
118
+ const first: DeclaredPolicy = {
119
+ effect: 'allow',
120
+ groupOrder: 'declared',
121
+ groups: tpl.groups,
122
+ resource,
123
+ /**
124
+ * ⚠️ `groups_scope:` GOVERNS `groups:` AND NOTHING ELSE. The second entry below exists because
125
+ * `account_groups` are account-scoped BY CONSTRUCTION (cfhf.py:176-180) — overriding its
126
+ * scope from the same key would make one word mean two things in one template.
127
+ * ★ OMITTED, NOT SET TO THE DEFAULT, WHEN THE yaml IS SILENT: an always-present field would
128
+ * rewrite the stored props of all 594 declared roles to say what the resource already said.
129
+ */
130
+ ...(tpl.groupsScope === undefined ? {} : { scope: tpl.groupsScope }),
131
+ };
132
+ if (accountGroups.length === 0) return [first];
133
+ const second: DeclaredPolicy = {
134
+ effect: 'allow',
135
+ groupOrder: 'by-id',
136
+ groups: accountGroups,
137
+ resource: accountResource(accountId),
138
+ };
139
+ return [first, second];
140
+ };
141
+
142
+ const build = (
143
+ account: AccountSpec,
144
+ surface: string,
145
+ tpl: RoleTemplate,
146
+ zone: CloudflareZone | null,
147
+ accountGroups: readonly string[],
148
+ ): ExpandedRole => ({
149
+ account: account.key,
150
+ accountId: account.id,
151
+ catalog: {
152
+ description: tpl.desc,
153
+ maxTtl: tpl.maxTtl,
154
+ // apply-roles.py:61 — groups then account_groups, names, in file order.
155
+ permissions: [...tpl.groups, ...accountGroups],
156
+ ttl: tpl.ttl,
157
+ zone: zone === null ? null : zone.name,
158
+ },
159
+ /**
160
+ * apply-roles.py:160-161. ⚠️ THE ZONE NAME, NOT ITS SLUG, so a public zone and its internal
161
+ * twin describe alike, and an empty `desc` still gains the suffix after a leading space.
162
+ */
163
+ description: zone === null ? tpl.desc : `${tpl.desc} (zone: ${zone.name})`,
164
+ maxTtl: tpl.maxTtl,
165
+ mount: mountName(account.key, surface),
166
+ // cfroles.py:84 for zone roles, :98 for account roles.
167
+ name: zone === null ? tpl.name : `${zoneSlug(zone)}-${tpl.name}`,
168
+ policies: policiesFor(tpl, account.id, zone, accountGroups),
169
+ surface,
170
+ ttl: tpl.ttl,
171
+ });
172
+
173
+ /** cfroles.py:50-104 for one account, in the Python's order: per surface, zone roles first. */
174
+ export const expandAccount = (
175
+ config: RolesConfig,
176
+ account: AccountSpec,
177
+ zones: readonly CloudflareZone[],
178
+ ): ExpandedRole[] => {
179
+ const roles: ExpandedRole[] = [];
180
+ for (const surface of account.surfaces) {
181
+ const spec = config.surfaces[surface];
182
+ if (spec === undefined) throw new Error(`surface ${surface} is not defined`);
183
+ for (const tpl of spec.zoneRoles) {
184
+ const only = tpl.zones;
185
+ /**
186
+ * ⛔ INTERNAL ZONES ARE OPT-IN PER TEMPLATE AND THE DEFAULT IS NO (cfroles.py:68-74). Only the
187
+ * word `internal` in zone_types matters; `full` and `secondary` are never checked, so a
188
+ * public zone of any type expands.
189
+ */
190
+ const internalOk = tpl.zoneTypes.includes('internal');
191
+ for (const zone of zones) {
192
+ if (account.zones !== 'all' && !account.zones.includes(zone.name)) continue; // :76
193
+ if (only !== undefined && only.length > 0 && !only.includes(zone.name)) continue; // :78
194
+ if (zone.internal && !internalOk) continue; // :80
195
+ roles.push(build(account, surface, tpl, zone, tpl.accountGroups));
196
+ }
197
+ }
198
+ // cfroles.py:96-103 — `account_groups` is [] on an account role, whatever the yaml says.
199
+ for (const tpl of spec.accountRoles) roles.push(build(account, surface, tpl, null, []));
200
+ }
201
+ return roles;
202
+ };
203
+
204
+ /**
205
+ * Every account's roles, grouped by mount.
206
+ *
207
+ * ⛔ A DUPLICATE ROLE ON ONE MOUNT IS AN ERROR HERE. apply-roles.py wrote both and the last write
208
+ * won in silence (apply-roles.py:157-162); declared, they are two resources fighting over one
209
+ * object. ⚠️ A MISSING ZONE INVENTORY IS AN ERROR TOO — an empty one is not, because an account
210
+ * with no zones is real, and the Python expanded it to account roles only.
211
+ */
212
+ export const expandAll = (
213
+ config: RolesConfig,
214
+ zonesByAccount: Readonly<Record<string, readonly CloudflareZone[]>>,
215
+ ): ReadonlyMap<string, readonly ExpandedRole[]> => {
216
+ const byMount = new Map<string, ExpandedRole[]>();
217
+ const seen = new Set<string>();
218
+ for (const account of config.accounts) {
219
+ const zones = zonesByAccount[account.key];
220
+ if (zones === undefined) throw new Error(`no zone inventory for account ${account.key}`);
221
+ for (const role of expandAccount(config, account, zones)) {
222
+ const key = `${role.mount}/roles/${role.name}`;
223
+ if (seen.has(key)) throw new Error(`roles.yaml expands ${key} twice`);
224
+ seen.add(key);
225
+ const mount = byMount.get(role.mount) ?? [];
226
+ mount.push(role);
227
+ byMount.set(role.mount, mount);
228
+ }
229
+ }
230
+ return byMount;
231
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The digest every Bao.* family persists instead of a value — moved here from the retired
3
+ * bao-shell.ts, unchanged, when the package stopped shelling out to `bao`.
4
+ */
5
+
6
+ /** ⚠️ Bun's hasher, not node:crypto — this runs under `bun --bun alchemy`. */
7
+ /**
8
+ * The only form a policy body may be hashed or compared in.
9
+ *
10
+ * ⛔ OPENBAO STRIPS THE TRAILING NEWLINE AND THAT ALONE WAS A FOREVER-DIFF. MEASURED on the live
11
+ * server: the assembler emits each fragment followed by a newline, so `homeflare-llm` goes in as
12
+ * 41 lines and `bao policy read` hands back 40 — identical text, one `\n` apart, two different
13
+ * digests. Every plan therefore reported `update`, every deploy rewrote the same policy, and the
14
+ * stack could never settle. It was invisible until the stack planned for the first time today.
15
+ * ★ THE HTTP API HANDS BACK THE SAME STRIPPED TEXT. `bao policy read` printed `data.policy` of
16
+ * `GET /v1/sys/policies/acl/<name>` (openbao v2.6.2 api/sys_policy.go:60-81), which is what
17
+ * policy-wire.ts now reads directly — so the canonicalisation is still load-bearing.
18
+ *
19
+ * ⚠️ `trimEnd` ONLY. Leading and interior whitespace are part of the HCL and must not be touched —
20
+ * a policy that differs by an indent really is a different document, and quietly folding that
21
+ * away would hide an edit rather than a formatting artifact.
22
+ */
23
+ export const canonical = (text: string) => text.trimEnd();
24
+
25
+ export const sha256 = (text: string) =>
26
+ new Bun.CryptoHasher('sha256').update(canonical(text)).digest('hex');
@@ -0,0 +1,84 @@
1
+ /**
2
+ * A fake OpenBao for the tests beside it — `Bun.serve` on an ephemeral 127.0.0.1 port, or on a unix
3
+ * socket the way an agent listener can be.
4
+ *
5
+ * ⛔ TEST-ONLY, AND NEVER A REAL VAULT. No provider imports this file, it forwards nowhere, and every
6
+ * helper below runs with an EXPLICIT environment rather than `process.env`, so a BAO_ADDR or
7
+ * BAO_TOKEN in the shell running the tests can never be read or contacted.
8
+ */
9
+ import * as Effect from 'effect/Effect';
10
+ import * as FetchHttpClient from 'effect/unstable/http/FetchHttpClient';
11
+ import type * as HttpClient from 'effect/unstable/http/HttpClient';
12
+ import type { BaoEnvironment } from './bao-address.ts';
13
+ import { BaoEnv } from './bao-http.ts';
14
+
15
+ /** One request the fake received. */
16
+ export type Seen = {
17
+ readonly method: string;
18
+ readonly path: string;
19
+ readonly headers: Headers;
20
+ readonly body: string;
21
+ };
22
+
23
+ /** What the fake answers: a status and a JSON value, or raw text for a body that is not JSON. */
24
+ export type Reply = { readonly status: number; readonly json?: unknown; readonly text?: string };
25
+
26
+ export type Fake = { readonly address: string; readonly seen: Seen[]; stop(): void };
27
+
28
+ /** ★ An answer may be async, so a test can HOLD requests open and count how many overlap. */
29
+ type Answer = (seen: Seen) => Reply | Promise<Reply>;
30
+
31
+ export const fakeBao = (answer: Answer, unix?: string): Fake => {
32
+ const seen: Seen[] = [];
33
+ const fetch = async (request: Request) => {
34
+ const url = new URL(request.url);
35
+ const entry = {
36
+ body: await request.text(),
37
+ headers: request.headers,
38
+ method: request.method,
39
+ path: `${url.pathname}${url.search}`,
40
+ };
41
+ seen.push(entry);
42
+ const reply = await answer(entry);
43
+ const text = reply.text ?? (reply.json === undefined ? null : JSON.stringify(reply.json));
44
+ return new Response(reply.status === 204 ? null : text, {
45
+ headers: { 'content-type': 'application/json' },
46
+ status: reply.status,
47
+ });
48
+ };
49
+ if (unix !== undefined) {
50
+ const server = Bun.serve({ fetch, unix });
51
+ return { address: `unix://${unix}`, seen, stop: () => void server.stop(true) };
52
+ }
53
+ const server = Bun.serve({ fetch, hostname: '127.0.0.1', port: 0 });
54
+ return { address: server.url.origin, seen, stop: () => void server.stop(true) };
55
+ };
56
+
57
+ /** Run `body` against a fresh fake, and stop it whatever happens. */
58
+ export const withFake = async (
59
+ answer: Answer,
60
+ body: (bao: Fake) => Promise<void>,
61
+ unix?: string,
62
+ ) => {
63
+ const bao = fakeBao(answer, unix);
64
+ try {
65
+ await body(bao);
66
+ } finally {
67
+ bao.stop();
68
+ }
69
+ };
70
+
71
+ const provided = <A, E>(env: BaoEnvironment, effect: Effect.Effect<A, E, HttpClient.HttpClient>) =>
72
+ effect.pipe(Effect.provideService(BaoEnv, env), Effect.provide(FetchHttpClient.layer));
73
+
74
+ /** The effect's success, through the real FetchHttpClient, under `env` only. */
75
+ export const run = <A, E>(
76
+ env: BaoEnvironment,
77
+ effect: Effect.Effect<A, E, HttpClient.HttpClient>,
78
+ ) => Effect.runPromise(provided(env, effect));
79
+
80
+ /** The effect's failure — rejects if it succeeded. */
81
+ export const runFailure = <A, E>(
82
+ env: BaoEnvironment,
83
+ effect: Effect.Effect<A, E, HttpClient.HttpClient>,
84
+ ) => Effect.runPromise(provided(env, Effect.flip(effect)));
@@ -0,0 +1,76 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import {
3
+ readCreateOutput,
4
+ runningForgejoBinary,
5
+ usernamesFrom,
6
+ } from './forgejo-bootstrap-parse.ts';
7
+
8
+ const NIX = '/nix/store/2wagmmlb09s2r0gv839iwmdvrc18kg8h-forgejo-16.0.3/bin/forgejo';
9
+
10
+ describe('runningForgejoBinary', () => {
11
+ test('finds the web process the daemon runs, as ps prints it', () => {
12
+ const ps = [
13
+ 'COMMAND',
14
+ '/usr/sbin/cfprefsd agent',
15
+ `${NIX} web --work-path /opt/homeflare/forgejo`,
16
+ `${NIX} admin user list --work-path /opt/homeflare/forgejo`,
17
+ ].join('\n');
18
+ expect(runningForgejoBinary(ps)).toBe(NIX);
19
+ });
20
+
21
+ test('ignores a forgejo that is not serving, and one off PATH', () => {
22
+ expect(runningForgejoBinary(`${NIX} admin user list`)).toBeUndefined();
23
+ expect(runningForgejoBinary('/opt/homebrew/bin/forgejo web')).toBeUndefined();
24
+ expect(runningForgejoBinary('')).toBeUndefined();
25
+ });
26
+ });
27
+
28
+ describe('usernamesFrom', () => {
29
+ test('reads the Username column of the measured table', () => {
30
+ const table = [
31
+ 'ID Username Email IsActive IsAdmin 2FA',
32
+ '1 tim tim@example.invalid true true false',
33
+ '7 forgejo-provision forgejo-provision@noreply.example true false false',
34
+ '',
35
+ ].join('\n');
36
+ expect(usernamesFrom(table)).toEqual(['tim', 'forgejo-provision']);
37
+ });
38
+
39
+ test('refuses a table it does not recognise rather than reporting nobody', () => {
40
+ expect(() => usernamesFrom('something else entirely')).toThrow(/unexpected header/);
41
+ expect(() => usernamesFrom('')).toThrow(/unexpected header/);
42
+ });
43
+ });
44
+
45
+ describe('readCreateOutput', () => {
46
+ const user = 'forgejo-provision';
47
+
48
+ test('captures the password and keeps it out of the lines it echoes', () => {
49
+ const stdout = [
50
+ "generated random password is 'aB3dE5gH7jK9'",
51
+ "New user 'forgejo-provision' has been successfully created!",
52
+ '',
53
+ ].join('\n');
54
+ const out = readCreateOutput(stdout, user);
55
+ expect(out.password).toBe('aB3dE5gH7jK9');
56
+ expect(out.created).toBe(true);
57
+ expect(out.rest.join('\n')).not.toContain('aB3dE5gH7jK9');
58
+ });
59
+
60
+ test('keeps a quote inside the password instead of truncating at it', () => {
61
+ const out = readCreateOutput("generated random password is 'ab'c\"d e'", user);
62
+ expect(out.password).toBe('ab\'c"d e');
63
+ });
64
+
65
+ test('reports not created when the success line names someone else or is missing', () => {
66
+ expect(readCreateOutput("New user 'tim' has been successfully created!", user).created).toBe(
67
+ false,
68
+ );
69
+ const noPassword = readCreateOutput(
70
+ "New user 'forgejo-provision' has been successfully created!",
71
+ user,
72
+ );
73
+ expect(noPassword.created).toBe(true);
74
+ expect(noPassword.password).toBeUndefined();
75
+ });
76
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Reading what the Forgejo CLI prints, for the forgejo-provision bootstrap (forgejo-bootstrap.ts).
3
+ *
4
+ * ★ PURE, SO THE ONE PART THAT TOUCHES THE PASSWORD IS TESTED WITHOUT ONE. Every format below was
5
+ * read off the running 16.0.3 daemon or its source on 2026-09-14, not assumed.
6
+ */
7
+
8
+ /** Where the mini's Forgejo keeps its state (hosts/macmini/modules/forgejo.nix:235, app.ini:14). */
9
+ export const FORGEJO_WORK_PATH = '/opt/homeflare/forgejo';
10
+ /** The file the launchd runner renders at every start (forgejo.nix:78). Readable by tim, who runs Forgejo. */
11
+ export const FORGEJO_CONFIG = `${FORGEJO_WORK_PATH}/custom/conf/app.ini`;
12
+
13
+ /**
14
+ * The binary of the RUNNING server, so the CLI writes the same schema as the daemon serving it.
15
+ *
16
+ * ★ MEASURED: `ps -axo command` shows
17
+ * `/nix/store/<hash>-forgejo-16.0.3/bin/forgejo web --work-path /opt/homeflare/forgejo`. A
18
+ * `forgejo` found on PATH could be a different version from the one holding the database.
19
+ */
20
+ export const runningForgejoBinary = (psCommands: string): string | undefined => {
21
+ for (const line of psCommands.split('\n')) {
22
+ const [binary, subcommand] = line.trim().split(/\s+/);
23
+ if (
24
+ subcommand === 'web' &&
25
+ binary !== undefined &&
26
+ /^\/nix\/store\/[^/]+-forgejo-[\d.]+\/bin\/forgejo$/.test(binary)
27
+ ) {
28
+ return binary;
29
+ }
30
+ }
31
+ return undefined;
32
+ };
33
+
34
+ /**
35
+ * Usernames from `forgejo admin user list`: a tabwriter table whose header is
36
+ * `ID Username Email IsActive IsAdmin 2FA` (measured, six columns).
37
+ *
38
+ * ⛔ AN UNEXPECTED HEADER THROWS. Parsing a changed table as "no such user" would let the bootstrap
39
+ * create over an account it failed to see.
40
+ */
41
+ export const usernamesFrom = (table: string): string[] => {
42
+ const rows = table
43
+ .split('\n')
44
+ .map((row) => row.trim())
45
+ .filter((row) => row !== '');
46
+ const header = rows[0]?.split(/\s+/) ?? [];
47
+ if (header[0] !== 'ID' || header[1] !== 'Username') {
48
+ throw new Error(
49
+ `forgejo admin user list printed an unexpected header: ${rows[0] ?? '(nothing)'}`,
50
+ );
51
+ }
52
+ return rows.slice(1).map((row) => row.split(/\s+/)[1] ?? '');
53
+ };
54
+
55
+ /** What `admin user create --random-password` printed on stdout. */
56
+ export type CreateOutput = {
57
+ readonly created: boolean;
58
+ /** ⛔ Never logged, never returned past the one OpenBao write. */
59
+ readonly password: string | undefined;
60
+ /** Every other non-empty stdout line, safe to echo. */
61
+ readonly rest: readonly string[];
62
+ };
63
+
64
+ /**
65
+ * ★ THE TWO LINES, FROM forgejo v16.0 cmd/admin_user_create.go:
66
+ * generated random password is '%s'
67
+ * New user '%s' has been successfully created!
68
+ * ⚠️ THE PATTERN IS GREEDY TO THE LAST QUOTE ON PURPOSE. Forgejo's generator can draw a `'` when
69
+ * PASSWORD_COMPLEXITY includes `spec`, and a lazy match would silently store a truncated password
70
+ * that no mint could ever use. The generator never draws a newline, so one line is one password.
71
+ */
72
+ export const readCreateOutput = (stdout: string, username: string): CreateOutput => {
73
+ let password: string | undefined;
74
+ let created = false;
75
+ const rest: string[] = [];
76
+ for (const line of stdout.split('\n')) {
77
+ const generated = /^generated random password is '(.*)'$/.exec(line);
78
+ if (generated !== null) {
79
+ password = generated[1];
80
+ continue;
81
+ }
82
+ if (line === `New user '${username}' has been successfully created!`) created = true;
83
+ if (line.trim() !== '') rest.push(line);
84
+ }
85
+ return { created, password, rest };
86
+ };
87
+
88
+ /** The last few lines of a CLI's stderr, for a failure message. Forgejo prints no secret there. */
89
+ export const tailOf = (text: string, lines = 5): string =>
90
+ text.trim().split('\n').slice(-lines).join(' | ') || '(no output)';