@homeflare/alchemy 0.38.0 → 0.40.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 (353) hide show
  1. package/README.md +32 -8
  2. package/dist/ceph/auth-entity.d.ts +34 -0
  3. package/dist/ceph/auth-entity.d.ts.map +1 -0
  4. package/dist/ceph/ceph-argv-config.d.ts +10 -0
  5. package/dist/ceph/ceph-argv-config.d.ts.map +1 -0
  6. package/dist/ceph/ceph-argv.d.ts +19 -0
  7. package/dist/ceph/ceph-argv.d.ts.map +1 -0
  8. package/dist/ceph/ceph-auth-form.d.ts +54 -0
  9. package/dist/ceph/ceph-auth-form.d.ts.map +1 -0
  10. package/dist/ceph/ceph-auth-parse.d.ts +56 -0
  11. package/dist/ceph/ceph-auth-parse.d.ts.map +1 -0
  12. package/dist/ceph/ceph-auth-reconcile.d.ts +35 -0
  13. package/dist/ceph/ceph-auth-reconcile.d.ts.map +1 -0
  14. package/dist/ceph/ceph-config-options.d.ts +23 -0
  15. package/dist/ceph/ceph-config-options.d.ts.map +1 -0
  16. package/dist/ceph/ceph-key-leak-assert.d.ts +25 -0
  17. package/dist/ceph/ceph-key-leak-assert.d.ts.map +1 -0
  18. package/dist/ceph/ceph-transport.d.ts +37 -0
  19. package/dist/ceph/ceph-transport.d.ts.map +1 -0
  20. package/dist/ceph/fake-ceph-dial.d.ts +35 -0
  21. package/dist/ceph/fake-ceph-dial.d.ts.map +1 -0
  22. package/dist/ceph/index.d.ts +13 -0
  23. package/dist/ceph/index.d.ts.map +1 -0
  24. package/dist/ceph/index.js +1609 -0
  25. package/dist/ceph/index.js.map +38 -0
  26. package/dist/forgejo/index.js +4 -2
  27. package/dist/forgejo/index.js.map +3 -3
  28. package/dist/forgejo/team-member.d.ts +25 -0
  29. package/dist/forgejo/team-member.d.ts.map +1 -1
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/proxmox/generated/constraints/index.d.ts +1 -1
  33. package/dist/proxmox/generated/constraints/index.d.ts.map +1 -1
  34. package/dist/proxmox/generated/constraints/pve-access.d.ts +1 -1
  35. package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts +1 -1
  36. package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts +1 -1
  37. package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts +1 -1
  38. package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts +1 -1
  39. package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts +1 -1
  40. package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts +1 -1
  41. package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts +1 -1
  42. package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts +1 -1
  43. package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts +1 -1
  44. package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts +1 -1
  45. package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts +1 -1
  46. package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts +1 -1
  47. package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts +1 -1
  48. package/dist/proxmox/generated/constraints/pve-nodes-storage.d.ts +23 -0
  49. package/dist/proxmox/generated/constraints/pve-nodes-storage.d.ts.map +1 -0
  50. package/dist/proxmox/generated/constraints/pve-pools.d.ts +1 -1
  51. package/dist/proxmox/generated/constraints/pve-storage.d.ts +1 -1
  52. package/dist/proxmox/index.d.ts +14 -3
  53. package/dist/proxmox/index.d.ts.map +1 -1
  54. package/dist/proxmox/index.js +574 -133
  55. package/dist/proxmox/index.js.map +33 -23
  56. package/dist/proxmox/qemu-form.d.ts +27 -18
  57. package/dist/proxmox/qemu-form.d.ts.map +1 -1
  58. package/dist/proxmox/qemu-judge.d.ts +30 -0
  59. package/dist/proxmox/qemu-judge.d.ts.map +1 -0
  60. package/dist/proxmox/qemu-lifecycle.d.ts +16 -2
  61. package/dist/proxmox/qemu-lifecycle.d.ts.map +1 -1
  62. package/dist/proxmox/qemu-net.d.ts +9 -0
  63. package/dist/proxmox/qemu-net.d.ts.map +1 -0
  64. package/dist/proxmox/qemu-props.d.ts +112 -0
  65. package/dist/proxmox/qemu-props.d.ts.map +1 -0
  66. package/dist/proxmox/qemu-volume.d.ts +27 -0
  67. package/dist/proxmox/qemu-volume.d.ts.map +1 -0
  68. package/dist/proxmox/qemu.d.ts +14 -27
  69. package/dist/proxmox/qemu.d.ts.map +1 -1
  70. package/dist/proxmox/storage-download-errors.d.ts +5 -0
  71. package/dist/proxmox/storage-download-errors.d.ts.map +1 -0
  72. package/dist/proxmox/storage-download-identity.d.ts +27 -0
  73. package/dist/proxmox/storage-download-identity.d.ts.map +1 -0
  74. package/dist/proxmox/storage-download-lifecycle.d.ts +97 -0
  75. package/dist/proxmox/storage-download-lifecycle.d.ts.map +1 -0
  76. package/dist/proxmox/storage-download-props.d.ts +36 -0
  77. package/dist/proxmox/storage-download-props.d.ts.map +1 -0
  78. package/dist/proxmox/storage-download-task.d.ts +17 -0
  79. package/dist/proxmox/storage-download-task.d.ts.map +1 -0
  80. package/dist/proxmox/storage-download.d.ts +44 -0
  81. package/dist/proxmox/storage-download.d.ts.map +1 -0
  82. package/dist/talos/credentials-write.d.ts +46 -0
  83. package/dist/talos/credentials-write.d.ts.map +1 -0
  84. package/dist/talos/credentials.d.ts +51 -9
  85. package/dist/talos/credentials.d.ts.map +1 -1
  86. package/dist/talos/fake-process.d.ts +56 -0
  87. package/dist/talos/fake-process.d.ts.map +1 -0
  88. package/dist/talos/index.d.ts +19 -4
  89. package/dist/talos/index.d.ts.map +1 -1
  90. package/dist/talos/index.js +357 -173
  91. package/dist/talos/index.js.map +16 -10
  92. package/dist/talos/kubeconfig-attrs.d.ts +4 -0
  93. package/dist/talos/kubeconfig-attrs.d.ts.map +1 -0
  94. package/dist/talos/kubeconfig.d.ts +41 -23
  95. package/dist/talos/kubeconfig.d.ts.map +1 -1
  96. package/dist/talos/machine-config-fixtures.d.ts +60 -0
  97. package/dist/talos/machine-config-fixtures.d.ts.map +1 -0
  98. package/dist/talos/machine-config-poll.d.ts +43 -0
  99. package/dist/talos/machine-config-poll.d.ts.map +1 -0
  100. package/dist/talos/machine-config-read.d.ts +49 -0
  101. package/dist/talos/machine-config-read.d.ts.map +1 -0
  102. package/dist/talos/resource.d.ts +18 -0
  103. package/dist/talos/resource.d.ts.map +1 -1
  104. package/dist/talos/talos-bootstrap-etcd.d.ts +38 -0
  105. package/dist/talos/talos-bootstrap-etcd.d.ts.map +1 -0
  106. package/dist/talos/talos-bootstrap.d.ts +87 -0
  107. package/dist/talos/talos-bootstrap.d.ts.map +1 -1
  108. package/dist/talos/talos-cluster-health.d.ts +43 -1
  109. package/dist/talos/talos-cluster-health.d.ts.map +1 -1
  110. package/dist/talos/talos-errors.d.ts +68 -0
  111. package/dist/talos/talos-errors.d.ts.map +1 -0
  112. package/dist/talos/talos-machine-config.d.ts +51 -15
  113. package/dist/talos/talos-machine-config.d.ts.map +1 -1
  114. package/dist/talos/talosctl.d.ts +7 -0
  115. package/dist/talos/talosctl.d.ts.map +1 -1
  116. package/dist/talos/values.d.ts +23 -3
  117. package/dist/talos/values.d.ts.map +1 -1
  118. package/dist/telemetry/fake-collector.d.ts +36 -0
  119. package/dist/telemetry/fake-collector.d.ts.map +1 -0
  120. package/dist/telemetry/index.d.ts +11 -0
  121. package/dist/telemetry/index.d.ts.map +1 -0
  122. package/dist/telemetry/index.js +223 -0
  123. package/dist/telemetry/index.js.map +12 -0
  124. package/dist/telemetry/layer.d.ts +86 -0
  125. package/dist/telemetry/layer.d.ts.map +1 -0
  126. package/dist/telemetry/redact-serialization.d.ts +21 -0
  127. package/dist/telemetry/redact-serialization.d.ts.map +1 -0
  128. package/dist/telemetry/redact.d.ts +46 -0
  129. package/dist/telemetry/redact.d.ts.map +1 -0
  130. package/dist/unifi/acl-rule-drift.d.ts +22 -0
  131. package/dist/unifi/acl-rule-drift.d.ts.map +1 -0
  132. package/dist/unifi/acl-rule-form.d.ts +90 -0
  133. package/dist/unifi/acl-rule-form.d.ts.map +1 -0
  134. package/dist/unifi/acl-rule-ordering-form.d.ts +49 -0
  135. package/dist/unifi/acl-rule-ordering-form.d.ts.map +1 -0
  136. package/dist/unifi/acl-rule-ordering.d.ts +54 -0
  137. package/dist/unifi/acl-rule-ordering.d.ts.map +1 -0
  138. package/dist/unifi/acl-rule.d.ts +49 -0
  139. package/dist/unifi/acl-rule.d.ts.map +1 -0
  140. package/dist/unifi/dns-policy-drift.d.ts +21 -0
  141. package/dist/unifi/dns-policy-drift.d.ts.map +1 -0
  142. package/dist/unifi/dns-policy-form.d.ts +97 -0
  143. package/dist/unifi/dns-policy-form.d.ts.map +1 -0
  144. package/dist/unifi/dns-policy.d.ts +55 -0
  145. package/dist/unifi/dns-policy.d.ts.map +1 -0
  146. package/dist/unifi/drift.d.ts +27 -0
  147. package/dist/unifi/drift.d.ts.map +1 -0
  148. package/dist/unifi/fake-unifi.d.ts +5 -1
  149. package/dist/unifi/fake-unifi.d.ts.map +1 -1
  150. package/dist/unifi/firewall-policy-drift.d.ts +26 -0
  151. package/dist/unifi/firewall-policy-drift.d.ts.map +1 -0
  152. package/dist/unifi/firewall-policy-form.d.ts +102 -0
  153. package/dist/unifi/firewall-policy-form.d.ts.map +1 -0
  154. package/dist/unifi/firewall-policy-ordering-form.d.ts +66 -0
  155. package/dist/unifi/firewall-policy-ordering-form.d.ts.map +1 -0
  156. package/dist/unifi/firewall-policy-ordering.d.ts +57 -0
  157. package/dist/unifi/firewall-policy-ordering.d.ts.map +1 -0
  158. package/dist/unifi/firewall-policy.d.ts +57 -0
  159. package/dist/unifi/firewall-policy.d.ts.map +1 -0
  160. package/dist/unifi/firewall-zone-form.d.ts +6 -0
  161. package/dist/unifi/firewall-zone-form.d.ts.map +1 -1
  162. package/dist/unifi/firewall-zone.d.ts +1 -1
  163. package/dist/unifi/firewall-zone.d.ts.map +1 -1
  164. package/dist/unifi/index.d.ts +11 -3
  165. package/dist/unifi/index.d.ts.map +1 -1
  166. package/dist/unifi/index.js +725 -60
  167. package/dist/unifi/index.js.map +31 -9
  168. package/dist/unifi/network-drift.d.ts +26 -0
  169. package/dist/unifi/network-drift.d.ts.map +1 -0
  170. package/dist/unifi/network-form.d.ts +40 -7
  171. package/dist/unifi/network-form.d.ts.map +1 -1
  172. package/dist/unifi/network.d.ts +1 -0
  173. package/dist/unifi/network.d.ts.map +1 -1
  174. package/dist/unifi/paginate.d.ts +24 -0
  175. package/dist/unifi/paginate.d.ts.map +1 -0
  176. package/dist/unifi/providers.d.ts +1 -1
  177. package/dist/unifi/providers.d.ts.map +1 -1
  178. package/dist/unifi/resource.d.ts +36 -1
  179. package/dist/unifi/resource.d.ts.map +1 -1
  180. package/dist/unifi/scan-source.d.ts +11 -0
  181. package/dist/unifi/scan-source.d.ts.map +1 -0
  182. package/dist/unifi/traffic-matching-list-drift.d.ts +22 -0
  183. package/dist/unifi/traffic-matching-list-drift.d.ts.map +1 -0
  184. package/dist/unifi/traffic-matching-list-form.d.ts +56 -0
  185. package/dist/unifi/traffic-matching-list-form.d.ts.map +1 -0
  186. package/dist/unifi/traffic-matching-list.d.ts +51 -0
  187. package/dist/unifi/traffic-matching-list.d.ts.map +1 -0
  188. package/dist/unifi/wifi-broadcast-drift.d.ts +22 -0
  189. package/dist/unifi/wifi-broadcast-drift.d.ts.map +1 -0
  190. package/dist/unifi/wifi-broadcast-form.d.ts +127 -0
  191. package/dist/unifi/wifi-broadcast-form.d.ts.map +1 -0
  192. package/dist/unifi/wifi-broadcast.d.ts +73 -0
  193. package/dist/unifi/wifi-broadcast.d.ts.map +1 -0
  194. package/docs/telemetry-spike.md +94 -0
  195. package/docs/telemetry.md +109 -0
  196. package/docs/unifi-acl-rule.md +122 -0
  197. package/docs/unifi-dns-policy.md +75 -0
  198. package/docs/unifi-firewall-policy.md +109 -0
  199. package/docs/unifi-traffic-matching-list.md +82 -0
  200. package/docs/unifi-wifi.md +120 -0
  201. package/docs/unifi.md +64 -3
  202. package/package.json +10 -2
  203. package/src/ceph/auth-entity.ts +74 -0
  204. package/src/ceph/ceph-argv-config.ts +78 -0
  205. package/src/ceph/ceph-argv.test.ts +226 -0
  206. package/src/ceph/ceph-argv.ts +189 -0
  207. package/src/ceph/ceph-auth-form.test.ts +103 -0
  208. package/src/ceph/ceph-auth-form.ts +107 -0
  209. package/src/ceph/ceph-auth-parse.test.ts +155 -0
  210. package/src/ceph/ceph-auth-parse.ts +121 -0
  211. package/src/ceph/ceph-auth-reconcile-secrecy.test.ts +82 -0
  212. package/src/ceph/ceph-auth-reconcile.test.ts +233 -0
  213. package/src/ceph/ceph-auth-reconcile.ts +119 -0
  214. package/src/ceph/ceph-config-options.test.ts +44 -0
  215. package/src/ceph/ceph-config-options.ts +36 -0
  216. package/src/ceph/ceph-key-leak-assert.ts +35 -0
  217. package/src/ceph/ceph-no-output-logging.test.ts +52 -0
  218. package/src/ceph/ceph-transport.test.ts +125 -0
  219. package/src/ceph/ceph-transport.ts +117 -0
  220. package/src/ceph/fake-ceph-dial.ts +55 -0
  221. package/src/ceph/index.ts +12 -0
  222. package/src/forgejo/team-member.test.ts +14 -0
  223. package/src/forgejo/team-member.ts +28 -1
  224. package/src/index.ts +1 -0
  225. package/src/proxmox/constraints-required.test.ts +1 -0
  226. package/src/proxmox/generated/constraints/index.ts +3 -1
  227. package/src/proxmox/generated/constraints/pve-access.ts +1 -1
  228. package/src/proxmox/generated/constraints/pve-cluster-backup.ts +1 -1
  229. package/src/proxmox/generated/constraints/pve-cluster-ceph.ts +1 -1
  230. package/src/proxmox/generated/constraints/pve-cluster-firewall.ts +1 -1
  231. package/src/proxmox/generated/constraints/pve-cluster-ha.ts +1 -1
  232. package/src/proxmox/generated/constraints/pve-cluster-metrics.ts +1 -1
  233. package/src/proxmox/generated/constraints/pve-cluster-notifications.ts +1 -1
  234. package/src/proxmox/generated/constraints/pve-cluster-replication.ts +1 -1
  235. package/src/proxmox/generated/constraints/pve-cluster-sdn.ts +1 -1
  236. package/src/proxmox/generated/constraints/pve-nodes-ceph.ts +1 -1
  237. package/src/proxmox/generated/constraints/pve-nodes-disks.ts +1 -1
  238. package/src/proxmox/generated/constraints/pve-nodes-lxc.ts +1 -1
  239. package/src/proxmox/generated/constraints/pve-nodes-network.ts +1 -1
  240. package/src/proxmox/generated/constraints/pve-nodes-qemu.ts +1 -1
  241. package/src/proxmox/generated/constraints/pve-nodes-storage.ts +30 -0
  242. package/src/proxmox/generated/constraints/pve-pools.ts +1 -1
  243. package/src/proxmox/generated/constraints/pve-storage.ts +1 -1
  244. package/src/proxmox/index.test.ts +11 -6
  245. package/src/proxmox/index.ts +19 -3
  246. package/src/proxmox/qemu-form.ts +49 -13
  247. package/src/proxmox/qemu-identity.test.ts +30 -0
  248. package/src/proxmox/qemu-judge.test.ts +65 -0
  249. package/src/proxmox/qemu-judge.ts +84 -0
  250. package/src/proxmox/qemu-lifecycle.test.ts +78 -19
  251. package/src/proxmox/qemu-lifecycle.ts +131 -32
  252. package/src/proxmox/qemu-net.test.ts +48 -0
  253. package/src/proxmox/qemu-net.ts +78 -0
  254. package/src/proxmox/qemu-ownership.test.ts +44 -0
  255. package/src/proxmox/qemu-props.test.ts +87 -0
  256. package/src/proxmox/qemu-props.ts +157 -0
  257. package/src/proxmox/qemu-volume.test.ts +229 -0
  258. package/src/proxmox/qemu-volume.ts +178 -0
  259. package/src/proxmox/qemu.ts +15 -29
  260. package/src/proxmox/storage-download-errors.ts +7 -0
  261. package/src/proxmox/storage-download-identity.test.ts +54 -0
  262. package/src/proxmox/storage-download-identity.ts +50 -0
  263. package/src/proxmox/storage-download-lifecycle.ts +226 -0
  264. package/src/proxmox/storage-download-ownership.test.ts +50 -0
  265. package/src/proxmox/storage-download-props.ts +38 -0
  266. package/src/proxmox/storage-download-task.ts +72 -0
  267. package/src/proxmox/storage-download.test.ts +216 -0
  268. package/src/proxmox/storage-download.ts +58 -0
  269. package/src/talos/credentials-write.test.ts +81 -0
  270. package/src/talos/credentials-write.ts +95 -0
  271. package/src/talos/credentials.test.ts +157 -0
  272. package/src/talos/credentials.ts +160 -90
  273. package/src/talos/fake-process.ts +94 -0
  274. package/src/talos/index.ts +39 -4
  275. package/src/talos/kubeconfig-attrs.ts +55 -0
  276. package/src/talos/kubeconfig.test.ts +256 -0
  277. package/src/talos/kubeconfig.ts +170 -90
  278. package/src/talos/machine-config-fixtures.ts +86 -0
  279. package/src/talos/machine-config-poll.test.ts +57 -0
  280. package/src/talos/machine-config-poll.ts +82 -0
  281. package/src/talos/machine-config-read.ts +118 -0
  282. package/src/talos/resource.ts +19 -0
  283. package/src/talos/secrets-gate.test.ts +63 -0
  284. package/src/talos/talos-bootstrap-etcd.ts +71 -0
  285. package/src/talos/talos-bootstrap.test.ts +218 -0
  286. package/src/talos/talos-bootstrap.ts +151 -45
  287. package/src/talos/talos-cluster-health.test.ts +143 -0
  288. package/src/talos/talos-cluster-health.ts +91 -28
  289. package/src/talos/talos-errors.ts +85 -0
  290. package/src/talos/talos-machine-config.test.ts +189 -0
  291. package/src/talos/talos-machine-config.ts +102 -88
  292. package/src/talos/talosctl.ts +8 -0
  293. package/src/talos/values.test.ts +34 -6
  294. package/src/talos/values.ts +41 -7
  295. package/src/telemetry/fake-collector.ts +59 -0
  296. package/src/telemetry/http-client-sealing.test.ts +82 -0
  297. package/src/telemetry/index.ts +10 -0
  298. package/src/telemetry/layer.test.ts +204 -0
  299. package/src/telemetry/layer.ts +152 -0
  300. package/src/telemetry/redact-serialization.ts +178 -0
  301. package/src/telemetry/redact.test.ts +124 -0
  302. package/src/telemetry/redact.ts +147 -0
  303. package/src/unifi/acl-rule-drift.ts +57 -0
  304. package/src/unifi/acl-rule-form.ts +135 -0
  305. package/src/unifi/acl-rule-ordering-form.ts +79 -0
  306. package/src/unifi/acl-rule-ordering.test.ts +141 -0
  307. package/src/unifi/acl-rule-ordering.ts +70 -0
  308. package/src/unifi/acl-rule.test.ts +183 -0
  309. package/src/unifi/acl-rule.ts +61 -0
  310. package/src/unifi/discriminated-filter-decode.test.ts +134 -0
  311. package/src/unifi/dns-policy-drift.ts +48 -0
  312. package/src/unifi/dns-policy-form.ts +142 -0
  313. package/src/unifi/dns-policy.test.ts +154 -0
  314. package/src/unifi/dns-policy.ts +67 -0
  315. package/src/unifi/drift.test.ts +78 -0
  316. package/src/unifi/drift.ts +56 -0
  317. package/src/unifi/errors-and-secrets.test.ts +127 -0
  318. package/src/unifi/fake-unifi.ts +5 -1
  319. package/src/unifi/firewall-policy-drift.test.ts +151 -0
  320. package/src/unifi/firewall-policy-drift.ts +55 -0
  321. package/src/unifi/firewall-policy-filter-roundtrip.test.ts +143 -0
  322. package/src/unifi/firewall-policy-form.ts +146 -0
  323. package/src/unifi/firewall-policy-ordering-form.ts +111 -0
  324. package/src/unifi/firewall-policy-ordering.test.ts +185 -0
  325. package/src/unifi/firewall-policy-ordering.ts +87 -0
  326. package/src/unifi/firewall-policy.test.ts +183 -0
  327. package/src/unifi/firewall-policy.ts +73 -0
  328. package/src/unifi/firewall-zone-form.ts +31 -2
  329. package/src/unifi/firewall-zone.test.ts +21 -1
  330. package/src/unifi/firewall-zone.ts +1 -1
  331. package/src/unifi/get-only-guard.test.ts +165 -0
  332. package/src/unifi/index.ts +78 -1
  333. package/src/unifi/network-drift.test.ts +207 -0
  334. package/src/unifi/network-drift.ts +80 -0
  335. package/src/unifi/network-form.ts +11 -39
  336. package/src/unifi/network.test.ts +7 -82
  337. package/src/unifi/network.ts +6 -6
  338. package/src/unifi/paginate.test.ts +252 -0
  339. package/src/unifi/paginate.ts +141 -0
  340. package/src/unifi/providers.ts +19 -1
  341. package/src/unifi/resource.ts +73 -3
  342. package/src/unifi/scan-source.ts +64 -0
  343. package/src/unifi/traffic-matching-list-drift.ts +40 -0
  344. package/src/unifi/traffic-matching-list-form.ts +77 -0
  345. package/src/unifi/traffic-matching-list.test.ts +202 -0
  346. package/src/unifi/traffic-matching-list.ts +78 -0
  347. package/src/unifi/wifi-broadcast-details-call.test.ts +92 -0
  348. package/src/unifi/wifi-broadcast-drift.ts +69 -0
  349. package/src/unifi/wifi-broadcast-form.ts +211 -0
  350. package/src/unifi/wifi-broadcast-secrets.test.ts +145 -0
  351. package/src/unifi/wifi-broadcast.test.ts +155 -0
  352. package/src/unifi/wifi-broadcast.ts +90 -0
  353. package/src/unifi/write-op-reference.test.ts +190 -0
@@ -22,6 +22,31 @@
22
22
  * cannot create a team.
23
23
  *
24
24
  * ⛔ TOKEN NEEDS `write:organization` TO ADD OR REMOVE, and an identity allowed to manage the team.
25
+ *
26
+ * ⛔ `defaultRemovalPolicy: 'retain'` — FOUND 2026-09-25 by reading the source, not by measuring
27
+ * anything (mini PR 82's red team, `homeflare-mini` 970e8be): before this, `ForgejoRepository`/
28
+ * `ForgejoOrgLabel` defaulted to retain in the kit itself but this family did not, so alchemy's
29
+ * own engine fallback (`destroy`) applied to it — every caller had to remember its own
30
+ * `.pipe(RemovalPolicy.retain())`, the way mini's `src/forgejo/declare.ts` did for
31
+ * `forgejo-provision`'s row. Dropping a `TeamMember` declaration — toggling a feature gate off,
32
+ * renaming the resource id — must not silently call `organization.orgRemoveTeamMember` and
33
+ * revoke a real membership; that is the same "removal from the stack is not removal from
34
+ * Forgejo" posture `ForgejoOrgLabel` and `ForgejoBranchProtection` already take, this family
35
+ * exists to attach a stack's identity to a team it never creates or destroys (`teamIdOf` below
36
+ * only ever locates the team, never manages it), and the same trade-off alchemy's own
37
+ * `GitHub.Collaborator` and `GitHub.TeamAccess` make for the identical reason — "preventing
38
+ * accidental lockout" (`GitHub/Collaborator.ts:127`, `GitHub/TeamAccess.ts:145` in the pinned
39
+ * alchemy source). The trade-off, stated plainly: removing a declaration no longer revokes
40
+ * access on its own — the plan shows that row as `orphaned`, not `delete`. Opt into a real
41
+ * revoke with `.pipe(RemovalPolicy.destroy())`; `destroy` below is fully implemented.
42
+ *
43
+ * ⚠️ THE DEFAULT ONLY PROTECTS A ROW ALREADY IN STATE ONCE THIS RELEASE IS DEPLOYED. Alchemy's
44
+ * plan reads `removalPolicy` off the *persisted row* (`Plan.ts`'s delete-node builder), not off
45
+ * this default; the default only reaches an existing row's state on a deploy where the resource
46
+ * plans as a no-op (`Apply.ts`'s noop path rewrites it and logs "removal policy destroy →
47
+ * retain"). A consumer that bumps to this release and drops a `TeamMember` declaration in the
48
+ * SAME deploy still gets `delete`, not `orphaned` — see the changeset for the two-deploy shape
49
+ * this implies for both adopting retain and opting back into destroy.
25
50
  */
26
51
  import { Resource } from 'alchemy';
27
52
  import * as Provider from 'alchemy/Provider';
@@ -52,7 +77,9 @@ export interface ForgejoTeamMember extends Resource<
52
77
  ForgejoRequirements
53
78
  > {}
54
79
 
55
- export const ForgejoTeamMember = Resource<ForgejoTeamMember>('Forgejo.TeamMember');
80
+ export const ForgejoTeamMember = Resource<ForgejoTeamMember>('Forgejo.TeamMember', {
81
+ defaultRemovalPolicy: 'retain',
82
+ });
56
83
 
57
84
  export class ForgejoTeamNotFoundError extends Data.TaggedError('ForgejoTeamNotFoundError')<{
58
85
  readonly message: string;
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  * import { BaoMount } from '@homeflare/alchemy/openbao';
11
11
  * import { TalosCluster } from '@homeflare/alchemy/talos';
12
12
  * import { ProxmoxAcl } from '@homeflare/alchemy/proxmox';
13
+ * import { CephAuthEntity } from '@homeflare/alchemy/ceph';
13
14
  * import { LaunchdJob } from '@homeflare/alchemy/launchd';
14
15
  * import { ReleaseBinary } from '@homeflare/alchemy/release';
15
16
  * import { CaddyConfig } from '@homeflare/alchemy/caddy';
@@ -80,6 +80,7 @@ describe('every create this package makes, and what the vendor requires of it',
80
80
  'pve:POST /nodes/{node}/lxc': ['ostemplate', 'vmid'],
81
81
  'pve:POST /nodes/{node}/network': ['iface', 'type'],
82
82
  'pve:POST /nodes/{node}/qemu': ['vmid'],
83
+ 'pve:POST /nodes/{node}/storage/{storage}/download-url': ['content', 'filename', 'url'],
83
84
  'pve:POST /pools': ['poolid'],
84
85
  'pve:POST /storage': ['storage', 'type'],
85
86
  });
@@ -22,6 +22,7 @@ import { PVE_NODES_DISKS_CONSTRAINTS } from './pve-nodes-disks.ts';
22
22
  import { PVE_NODES_LXC_CONSTRAINTS } from './pve-nodes-lxc.ts';
23
23
  import { PVE_NODES_NETWORK_CONSTRAINTS } from './pve-nodes-network.ts';
24
24
  import { PVE_NODES_QEMU_CONSTRAINTS } from './pve-nodes-qemu.ts';
25
+ import { PVE_NODES_STORAGE_CONSTRAINTS } from './pve-nodes-storage.ts';
25
26
  import { PVE_POOLS_CONSTRAINTS } from './pve-pools.ts';
26
27
  import { PVE_STORAGE_CONSTRAINTS } from './pve-storage.ts';
27
28
  import { PBS_CONFIG_CONSTRAINTS } from './pbs-config.ts';
@@ -41,6 +42,7 @@ export const PROXMOX_CONSTRAINTS: Readonly<Record<string, EndpointConstraints>>
41
42
  ...PVE_NODES_LXC_CONSTRAINTS,
42
43
  ...PVE_NODES_NETWORK_CONSTRAINTS,
43
44
  ...PVE_NODES_QEMU_CONSTRAINTS,
45
+ ...PVE_NODES_STORAGE_CONSTRAINTS,
44
46
  ...PVE_POOLS_CONSTRAINTS,
45
47
  ...PVE_STORAGE_CONSTRAINTS,
46
48
  ...PBS_CONFIG_CONSTRAINTS,
@@ -52,4 +54,4 @@ export const PROXMOX_CONSTRAINTS: Readonly<Record<string, EndpointConstraints>>
52
54
  * ★ A DIGEST OF THE DATA, NOT OF THE FILE TEXT, so reformatting is not a stale generation while
53
55
  * changing a 128 to a 129 by hand is. `tests/schema-manifest.test.ts` recomputes it.
54
56
  */
55
- export const PROXMOX_CONSTRAINTS_DIGEST = '56eba80750ddb1d2';
57
+ export const PROXMOX_CONSTRAINTS_DIGEST = '8b6745a471839974';
@@ -6,7 +6,7 @@
6
6
  * sha256 9def8f13611184ee, read on a PVE cluster node from
7
7
  * /usr/share/pve-docs/api-viewer/apidoc.js
8
8
  *
9
- * 58 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
9
+ * 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
10
10
  * this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
11
11
  * and therefore unchecked at plan time.
12
12
  *
@@ -6,7 +6,7 @@
6
6
  * sha256 9def8f13611184ee, read on a PVE cluster node from
7
7
  * /usr/share/pve-docs/api-viewer/apidoc.js
8
8
  *
9
- * 58 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
9
+ * 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
10
10
  * this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
11
11
  * and therefore unchecked at plan time.
12
12
  *
@@ -6,7 +6,7 @@
6
6
  * sha256 9def8f13611184ee, read on a PVE cluster node from
7
7
  * /usr/share/pve-docs/api-viewer/apidoc.js
8
8
  *
9
- * 58 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
9
+ * 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
10
10
  * this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
11
11
  * and therefore unchecked at plan time.
12
12
  *
@@ -6,7 +6,7 @@
6
6
  * sha256 9def8f13611184ee, read on a PVE cluster node from
7
7
  * /usr/share/pve-docs/api-viewer/apidoc.js
8
8
  *
9
- * 58 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
9
+ * 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
10
10
  * this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
11
11
  * and therefore unchecked at plan time.
12
12
  *
@@ -6,7 +6,7 @@
6
6
  * sha256 9def8f13611184ee, read on a PVE cluster node from
7
7
  * /usr/share/pve-docs/api-viewer/apidoc.js
8
8
  *
9
- * 58 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
9
+ * 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
10
10
  * this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
11
11
  * and therefore unchecked at plan time.
12
12
  *
@@ -6,7 +6,7 @@
6
6
  * sha256 9def8f13611184ee, read on a PVE cluster node from
7
7
  * /usr/share/pve-docs/api-viewer/apidoc.js
8
8
  *
9
- * 58 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
9
+ * 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
10
10
  * this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
11
11
  * and therefore unchecked at plan time.
12
12
  *
@@ -6,7 +6,7 @@
6
6
  * sha256 9def8f13611184ee, read on a PVE cluster node from
7
7
  * /usr/share/pve-docs/api-viewer/apidoc.js
8
8
  *
9
- * 58 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
9
+ * 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
10
10
  * this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
11
11
  * and therefore unchecked at plan time.
12
12
  *
@@ -6,7 +6,7 @@
6
6
  * sha256 9def8f13611184ee, read on a PVE cluster node from
7
7
  * /usr/share/pve-docs/api-viewer/apidoc.js
8
8
  *
9
- * 58 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
9
+ * 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
10
10
  * this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
11
11
  * and therefore unchecked at plan time.
12
12
  *
@@ -6,7 +6,7 @@
6
6
  * sha256 9def8f13611184ee, read on a PVE cluster node from
7
7
  * /usr/share/pve-docs/api-viewer/apidoc.js
8
8
  *
9
- * 58 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
9
+ * 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
10
10
  * this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
11
11
  * and therefore unchecked at plan time.
12
12
  *
@@ -6,7 +6,7 @@
6
6
  * sha256 9def8f13611184ee, read on a PVE cluster node from
7
7
  * /usr/share/pve-docs/api-viewer/apidoc.js
8
8
  *
9
- * 58 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
9
+ * 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
10
10
  * this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
11
11
  * and therefore unchecked at plan time.
12
12
  *
@@ -6,7 +6,7 @@
6
6
  * sha256 9def8f13611184ee, read on a PVE cluster node from
7
7
  * /usr/share/pve-docs/api-viewer/apidoc.js
8
8
  *
9
- * 58 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
9
+ * 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
10
10
  * this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
11
11
  * and therefore unchecked at plan time.
12
12
  *
@@ -6,7 +6,7 @@
6
6
  * sha256 9def8f13611184ee, read on a PVE cluster node from
7
7
  * /usr/share/pve-docs/api-viewer/apidoc.js
8
8
  *
9
- * 58 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
9
+ * 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
10
10
  * this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
11
11
  * and therefore unchecked at plan time.
12
12
  *
@@ -6,7 +6,7 @@
6
6
  * sha256 9def8f13611184ee, read on a PVE cluster node from
7
7
  * /usr/share/pve-docs/api-viewer/apidoc.js
8
8
  *
9
- * 58 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
9
+ * 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
10
10
  * this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
11
11
  * and therefore unchecked at plan time.
12
12
  *
@@ -6,7 +6,7 @@
6
6
  * sha256 9def8f13611184ee, read on a PVE cluster node from
7
7
  * /usr/share/pve-docs/api-viewer/apidoc.js
8
8
  *
9
- * 58 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
9
+ * 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
10
10
  * this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
11
11
  * and therefore unchecked at plan time.
12
12
  *
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Generated pve-manager parameter constraints for `/nodes/{node}/storage` — DO NOT EDIT BY HAND.
3
+ *
4
+ * Run: bun codegen/constraints.ts
5
+ * Manifest entry: `pve-apidoc` — pve-manager 9.2.11/f6997e698c7933ea
6
+ * sha256 9def8f13611184ee, read on a PVE cluster node from
7
+ * /usr/share/pve-docs/api-viewer/apidoc.js
8
+ *
9
+ * 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
10
+ * this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
11
+ * and therefore unchecked at plan time.
12
+ *
13
+ * ⚠️ A `patternSource` with no `pattern` beside it is a rule that could NOT be carried into a
14
+ * JavaScript RegExp faithfully (codegen/pattern.ts). It is recorded and NOT enforced.
15
+ * ⚠️ `pattern` IS NOT THE VENDOR'S SPELLING. For PVE it is anchored, because PVE applies
16
+ * `m/^$pattern$/` itself (JSONSchema.pm); for PBS it is the vendor's own, which already
17
+ * carries its anchors. `patternSource` is the spelling to quote at a human — param-rules.ts.
18
+ * ⚠️ `each: true` means the value rules describe every ELEMENT of a repeated key, because the
19
+ * parameter is an array and stated its limits on `items`.
20
+ */
21
+ import type { EndpointConstraints } from '../../constraints.ts';
22
+
23
+ export const PVE_NODES_STORAGE_CONSTRAINTS: Readonly<Record<string, EndpointConstraints>> = {
24
+ "pve:POST /nodes/{node}/storage/{storage}/download-url": {
25
+ "checksum-algorithm": {"enum":["md5","sha1","sha224","sha256","sha384","sha512"],"type":"string"},
26
+ "content": {"enum":["iso","vztmpl","import"],"format":"pve-storage-content","required":true,"type":"string"},
27
+ "filename": {"maxLength":255,"required":true,"type":"string"},
28
+ "url": {"pattern":"^https?://.*\\n?$","patternSource":"https?://.*","required":true,"type":"string"},
29
+ },
30
+ };
@@ -6,7 +6,7 @@
6
6
  * sha256 9def8f13611184ee, read on a PVE cluster node from
7
7
  * /usr/share/pve-docs/api-viewer/apidoc.js
8
8
  *
9
- * 58 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
9
+ * 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
10
10
  * this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
11
11
  * and therefore unchecked at plan time.
12
12
  *
@@ -6,7 +6,7 @@
6
6
  * sha256 9def8f13611184ee, read on a PVE cluster node from
7
7
  * /usr/share/pve-docs/api-viewer/apidoc.js
8
8
  *
9
- * 58 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
9
+ * 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
10
10
  * this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
11
11
  * and therefore unchecked at plan time.
12
12
  *
@@ -1,15 +1,19 @@
1
1
  /**
2
- * Public barrel: control-plane Resource constructors and `ProxmoxLxc`, not QEMU or NIC apply.
2
+ * Public barrel: control-plane Resource constructors and `ProxmoxLxc`, not NIC apply.
3
3
  *
4
- * ⛔ Vm stays Provider-only. NodeNetwork/NetworkApply stay off the barrel — they stage
5
- * interfaces.new. ★ Lxc is a Resource since 2026-09-21: adopt-first, refuse-not-replace,
6
- * retain on destroy (lxc.ts).
4
+ * ⛔ NodeNetwork/NetworkApply stay off the barrel — they stage interfaces.new. ★ Lxc is a
5
+ * Resource since 2026-09-21: adopt-first, refuse-not-replace, retain on destroy (lxc.ts).
7
6
  * ★ CephDaemon, CephFs and CephOsd are Resources since 2026-09-22: adopt-only by shape and retain
8
7
  * on destroy (ceph-adopt.test.ts). ⛔ CephFlag stays Provider-only: a declared flag reasserts a
9
8
  * maintenance toggle on every deploy (ceph-flag.ts).
10
9
  * ★ ApiToken and ZfsPool are Resources since 2026-09-23, decision 9: ApiToken metadata-only,
11
10
  * ZfsPool adopt-only when `devices`/`raidlevel` are left undeclared (api-token-adopt.test.ts,
12
11
  * zfs-pool-adopt.test.ts).
12
+ * ★ Vm and StorageDownload are Resources since 2026-09-26, for Talos: Vm's declared-keys model
13
+ * (qemu-props.ts) and retain-by-default; StorageDownload's checksum-pinned, create-oriented
14
+ * `download-url` (storage-download.ts). Neither was Provider-only for a design reason that
15
+ * changed — Vm's old "5-default PUT" is what kept it off this barrel until qemu-props.ts fixed
16
+ * it, per qemu-lifecycle.test.ts's own regression test.
13
17
  */
14
18
  import { expect, test } from 'bun:test';
15
19
  import { PbsDatastore, ProxmoxStorage } from './index.ts';
@@ -42,7 +46,9 @@ const resourceExports = [
42
46
  'ProxmoxSdnVnet',
43
47
  'ProxmoxSdnZone',
44
48
  'ProxmoxStorage',
49
+ 'ProxmoxStorageDownload',
45
50
  'ProxmoxUser',
51
+ 'ProxmoxVm',
46
52
  'ProxmoxZfsPool',
47
53
  ] as const;
48
54
 
@@ -61,8 +67,7 @@ test('the provisioning baseline is on the barrel: the list, the declaration, the
61
67
  expect(barrel.provisionBootstrap()).toStartWith('#!/bin/sh\n');
62
68
  });
63
69
 
64
- test('QEMU, NIC apply and the Ceph flag stay Provider-only', () => {
65
- expect(src).not.toMatch(/export \{ ProxmoxVm[, }]/);
70
+ test('NIC apply and the Ceph flag stay Provider-only', () => {
66
71
  expect(src).not.toMatch(/export \{ ProxmoxNodeNetwork[, }]/);
67
72
  expect(src).not.toMatch(/export \{ ProxmoxNetworkApply[, }]/);
68
73
  expect(src).not.toMatch(/export \{ ProxmoxCephFlag[, }]/);
@@ -6,8 +6,18 @@
6
6
  * provider needs but a consumer should not depend on. An `export *` here would publish
7
7
  * every helper as API and make the next refactor a breaking change.
8
8
  * ★ Control-plane Resource constructors (Storage, SDN, access, HA, backup,
9
- * metrics, PBS) sit next to their Providers. NIC apply and QEMU stay
10
- * Provider-only so a stack cannot declare them by accident.
9
+ * metrics, PBS) sit next to their Providers. NIC apply stays Provider-only
10
+ * so a stack cannot declare it by accident.
11
+ * ★ `ProxmoxVm` JOINED THEM 2026-09-26, FOR TALOS. Its declared-keys model (qemu-props.ts) is
12
+ * what let it join safely: an undeclared field is unmanaged, so it is never sent and never
13
+ * compared — the fix for the "5-default PUT" a Provider-only `Proxmox.Vm` never exercised.
14
+ * `cipassword` and `machine` are typed `never` and refused if smuggled past the types
15
+ * (qemu-form.ts's `formRefusals`); `defaultRemovalPolicy: 'retain'` means dropping a
16
+ * declaration never destroys a guest's disks. Node-pinned semantics (qemu-read.ts's "A
17
+ * migration is not an update") are unchanged by the export.
18
+ * ★ `Proxmox.StorageDownload` JOINED 2026-09-26 TOO, ALONGSIDE IT: `download-url` (content
19
+ * `import`, checksum-pinned) as a CREATE-oriented family — no adoption, no update, a task-poll
20
+ * failure (a checksum mismatch included) refuses rather than reports success.
11
21
  * ★ `ProxmoxLxc` JOINED THEM 2026-09-21, AND IT WAS KEPT OFF UNTIL IT COULD NOT HURT A GUEST.
12
22
  * The provider-only version diffed four fields and would have planned a create over any read
13
23
  * failure. The Resource now adopts from the live config, refuses every change PVE cannot make
@@ -102,7 +112,13 @@ export {
102
112
  } from './provision-baseline.ts';
103
113
  export { provisionBootstrap } from './provision-bootstrap.ts';
104
114
  export { type DeclareProvisionOptions, declareProvisionBaseline } from './provision-declare.ts';
105
- export { ProxmoxVmProvider } from './qemu.ts';
115
+ export { ProxmoxVm, ProxmoxVmProvider, type VmAttributes, type VmProps } from './qemu.ts';
116
+ export {
117
+ ProxmoxStorageDownload,
118
+ ProxmoxStorageDownloadProvider,
119
+ type StorageDownloadAttributes,
120
+ type StorageDownloadProps,
121
+ } from './storage-download.ts';
106
122
  export { ProxmoxReplicationJobProvider } from './replication-job.ts';
107
123
  export { ProxmoxRole, ProxmoxRoleProvider } from './role.ts';
108
124
  export { ProxmoxSdnApply, ProxmoxSdnApplyProvider } from './sdn-apply.ts';
@@ -1,15 +1,51 @@
1
- /** QEMU create and update forms. Path identity stays out of the body. */
2
- import type { VmProps } from './qemu.ts';
3
-
4
- /** Fields both create and update send. `net0` is optional and is not compared. */
5
- export const shape = (props: VmProps) => ({
6
- cores: String(props.cores ?? 1),
7
- memory: String(props.memory ?? 512),
8
- name: props.name ?? `vm${String(props.vmid)}`,
9
- onboot: props.onboot === true ? '1' : '0',
10
- sockets: String(props.sockets ?? 1),
11
- ...(props.net0 === undefined ? {} : { net0: props.net0 }),
1
+ /**
2
+ * QEMU create and update forms — declared keys only. Path identity stays out of the body.
3
+ *
4
+ * ⛔ NO DEFAULT IS EVER SUBSTITUTED FOR AN UNDECLARED FIELD. See qemu-props.ts's header for the
5
+ * "5-default PUT" this replaces: the old `shape()` sent `cores`/`memory`/`name`/`onboot`/
6
+ * `sockets` on every write, defaults included for whichever the declaration left out, so
7
+ * editing one field on an adopted VM silently reset every other to its factory value.
8
+ */
9
+ import {
10
+ type VmProps,
11
+ declaredKeys,
12
+ declaredValue,
13
+ isManagedKey,
14
+ wireValue,
15
+ } from './qemu-props.ts';
16
+
17
+ /**
18
+ * Why a declared key cannot go on the wire, checked before every create and every update.
19
+ * ★ THE SAME RULE `lxc-create-form.ts` ENFORCES FOR `Proxmox.Lxc`: a key smuggled past the types
20
+ * (a cast, a JS caller) is refused here rather than silently dropped or silently sent.
21
+ */
22
+ export const formRefusals = (props: VmProps): string[] =>
23
+ declaredKeys(props)
24
+ .filter((key) => !isManagedKey(key))
25
+ .map((key) => `${key}: not a config key this resource manages (vmid ${String(props.vmid)}).`);
26
+
27
+ /** The declared config, wire-spelled — every key `formRefusals` did not already refuse. */
28
+ export const form = (props: VmProps): Record<string, string> => {
29
+ const out: Record<string, string> = {};
30
+ for (const key of declaredKeys(props)) {
31
+ if (!isManagedKey(key)) continue;
32
+ out[key] = wireValue(declaredValue(props, key));
33
+ }
34
+ return out;
35
+ };
36
+
37
+ /**
38
+ * The create body: the declared form plus the vmid a create needs.
39
+ * ⚠️ NO RETURN-TYPE ANNOTATION, ON PURPOSE. Annotating this `Record<string, string>` erases the
40
+ * object literal's own inferred shape, and with it the one thing `nodes.createNodeQemu({
41
+ * ...createForm(news), node: news.node })` (qemu-lifecycle.ts) needs: TypeScript only accepts a
42
+ * spread as satisfying `CreateNodeQemuRequest`'s required `vmid` when the spread's inferred type
43
+ * still carries `vmid` as a literal key, which an index-signature-only type cannot prove.
44
+ */
45
+ export const createForm = (props: VmProps) => ({
46
+ ...form(props),
47
+ vmid: String(props.vmid),
12
48
  });
13
49
 
14
- /** ★ Exported so the constraint proof can run the REAL create form, not a retyped copy of it. */
15
- export const createForm = (props: VmProps) => ({ ...shape(props), vmid: String(props.vmid) });
50
+ /** The update body — identical construction to `createForm`, minus `vmid`, which PUT never sends. */
51
+ export const updateForm = form;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * `Proxmox.Vm` identity — red-team I1 on PR 297, through the real engine over a stub fetch.
3
+ * ⛔ WHAT THIS PINS: a changed `vmid` on an already-managed VM is refused as a different machine,
4
+ * never planned as an `update` (which used to POST a second VM and leave the first one running
5
+ * and untracked).
6
+ */
7
+ import { expect, test } from 'bun:test';
8
+ import * as Layer from 'effect/Layer';
9
+ import { engineOver } from '../verify/fake-engine.ts';
10
+ import { FAKE_TARGET, fakePve, withoutBao } from './fake-pve.ts';
11
+ import { ProxmoxVm, ProxmoxVmProvider } from './qemu.ts';
12
+
13
+ const NODE = 'pve1';
14
+ const props = { node: NODE, target: FAKE_TARGET, vmid: 150 };
15
+ const live = { digest: 'a'.repeat(40), memory: '512' };
16
+
17
+ const engine = (fake: ReturnType<typeof fakePve>) =>
18
+ engineOver(ProxmoxVmProvider().pipe(Layer.provideMerge(fake.layer)));
19
+
20
+ test('a changed vmid is refused as a different VM, never planned as an update', async () => {
21
+ const fake = fakePve((call) => (call.method === 'GET' ? live : null));
22
+ await withoutBao(async () => {
23
+ const stack = engine(fake);
24
+ await stack.deploy(ProxmoxVm('row', props));
25
+ await expect(stack.deploy(ProxmoxVm('row', { ...props, vmid: 151 }))).rejects.toThrow(
26
+ /vmid: 150 -> 151 is a different VM/,
27
+ );
28
+ });
29
+ expect(fake.writes()).toEqual([]);
30
+ });
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Pure unit tests for `judge`/`identityRefusal` — red-team C1/I1 on PR 297. No network, no fake PVE.
3
+ * ⛔ WHAT THESE PIN: only drifted keys are ever PUT (an unrelated field's drift does not drag the
4
+ * disk/NIC keys along, the failure mode a whole-form resend had); a vmid change is refused, never
5
+ * built as a second VM.
6
+ */
7
+ import { describe, expect, test } from 'bun:test';
8
+ import { FAKE_TARGET } from './fake-pve.ts';
9
+ import { identityRefusal, judge } from './qemu-judge.ts';
10
+ import type { VmProps } from './qemu-props.ts';
11
+
12
+ const base = { node: 'pve1', target: FAKE_TARGET, vmid: 101 } as const;
13
+
14
+ describe('judge', () => {
15
+ test('a fresh-spelled disk and a MAC-less NIC both settle against live, drifting nothing', () => {
16
+ const props: VmProps = {
17
+ ...base,
18
+ ide2: 'cephtb4:cloudinit',
19
+ net0: 'virtio,bridge=vmbr0,tag=20',
20
+ scsi0: 'cephtb4:32',
21
+ };
22
+ const config = {
23
+ ide2: 'cephtb4:vm-101-cloudinit,media=cdrom',
24
+ net0: 'virtio=AA:BB:CC:DD:EE:FF,bridge=vmbr0,tag=20',
25
+ scsi0: 'cephtb4:vm-101-disk-0,size=32G',
26
+ };
27
+ expect(judge(props, config)).toEqual({ drift: [], put: {}, refuse: [] });
28
+ });
29
+
30
+ test('an unrelated field drifting does not drag the settled disk/NIC keys into the PUT', () => {
31
+ const props: VmProps = {
32
+ ...base,
33
+ memory: 4096,
34
+ net0: 'virtio,bridge=vmbr0,tag=20',
35
+ scsi0: 'cephtb4:32',
36
+ };
37
+ const config = {
38
+ memory: '2048',
39
+ net0: 'virtio=AA:BB:CC:DD:EE:FF,bridge=vmbr0,tag=20',
40
+ scsi0: 'cephtb4:vm-101-disk-0,size=32G',
41
+ };
42
+ const change = judge(props, config);
43
+ expect(change.drift).toEqual(['memory']);
44
+ expect(change.put).toEqual({ memory: '4096' });
45
+ });
46
+
47
+ test('a scalar key not yet on the live config is put as declared', () => {
48
+ const change = judge({ ...base, agent: true }, {});
49
+ expect(change.put).toEqual({ agent: '1' });
50
+ });
51
+ });
52
+
53
+ describe('identityRefusal', () => {
54
+ test('no prior vmid: never refused (a fresh create)', () => {
55
+ expect(identityRefusal(101, undefined)).toBeUndefined();
56
+ });
57
+
58
+ test('the same vmid: never refused', () => {
59
+ expect(identityRefusal(101, 101)).toBeUndefined();
60
+ });
61
+
62
+ test('a changed vmid: refused, naming both -- a new resource, not an update', () => {
63
+ expect(identityRefusal(151, 150)).toMatch(/vmid: 150 -> 151 is a different VM/);
64
+ });
65
+ });
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Declared against live: what a `Proxmox.Vm` deploy would write, per key — the QEMU sibling of
3
+ * lxc-judge.ts, added 2026-09-26 (red-team C1/I1 on PR 297).
4
+ *
5
+ * ⛔ REPLACES A WHOLE-STRING, WHOLE-FORM COMPARISON. Before this, `matches` compared each declared
6
+ * key to live BYTE FOR BYTE and, on any drift, `formToSend` re-PUT every declared key at once
7
+ * (qemu-lifecycle.ts). A `scsi0`/`ide2`/`net0` declared in the "new" or MAC-less spelling then
8
+ * NEVER matched its own live volume/NIC (qemu-volume.ts / qemu-net.ts explain why), so every
9
+ * deploy re-sent all three -- reallocating the disk, re-importing it, and handing out a fresh MAC
10
+ * each time, even when nothing else changed. `judge` below decides per key, on the LIVE volume id
11
+ * and LIVE MAC, so an unrelated field's drift no longer drags the disk/NIC keys along with it.
12
+ */
13
+ import { type VmProps, declaredKeys, declaredValue, indexedKey, wireValue } from './qemu-props.ts';
14
+ import { judgeDisk } from './qemu-volume.ts';
15
+ import { netWrite, sameNet } from './qemu-net.ts';
16
+
17
+ export type VmChange = {
18
+ /** Keys to `PUT …/config`, wire-spelled and built on live ids where one exists. */
19
+ readonly put: Record<string, string>;
20
+ /** Why a key cannot be written as an update. Non-empty means nothing is written. */
21
+ readonly refuse: readonly string[];
22
+ /** Every declared key that differs from live, refused or not. */
23
+ readonly drift: readonly string[];
24
+ };
25
+
26
+ export const judge = (props: VmProps, config: Record<string, string>): VmChange => {
27
+ const put: Record<string, string> = {};
28
+ const refuse: string[] = [];
29
+ const drift = new Set<string>();
30
+
31
+ for (const key of declaredKeys(props)) {
32
+ const want = wireValue(declaredValue(props, key));
33
+ const have = config[key];
34
+ const family = indexedKey(key)?.[0];
35
+
36
+ if (family === 'scsi' || family === 'ide') {
37
+ if (have === undefined) {
38
+ drift.add(key);
39
+ put[key] = want;
40
+ } else {
41
+ const verdict = judgeDisk(key, want, have);
42
+ if (verdict.refuse !== undefined) {
43
+ drift.add(key);
44
+ refuse.push(verdict.refuse);
45
+ } else if (verdict.put !== undefined) {
46
+ drift.add(key);
47
+ put[key] = verdict.put;
48
+ }
49
+ }
50
+ continue;
51
+ }
52
+
53
+ if (family === 'net') {
54
+ if (have === undefined) {
55
+ drift.add(key);
56
+ put[key] = want;
57
+ } else if (!sameNet(want, have)) {
58
+ drift.add(key);
59
+ put[key] = netWrite(want, have);
60
+ }
61
+ continue;
62
+ }
63
+
64
+ // Scalars, `ipconfigN`, `serialN`: a plain string compare, as the old whole-form model did.
65
+ // ⚠️ No "new spelling" or MAC ambiguity applies to these -- see qemu-props.ts's own doc per key.
66
+ if (want !== (have ?? '')) {
67
+ drift.add(key);
68
+ put[key] = want;
69
+ }
70
+ }
71
+ return { drift: [...drift].sort(), put, refuse };
72
+ };
73
+
74
+ /**
75
+ * What makes a `Proxmox.Vm` a DIFFERENT machine: a new vmid (red-team I1). Never planned as a
76
+ * replace -- see qemu.ts's header on why nothing here replaces a running VM.
77
+ * ⚠️ `was` PREFERS THE RECORDED ATTRIBUTE, so a `node` change (HA failover, a hand `qm migrate`)
78
+ * never reads as a vmid change: only `vmid` is judged here, mirroring lxc-identity.ts.
79
+ */
80
+ export const identityRefusal = (vmid: number, was: number | undefined): string | undefined =>
81
+ was === undefined || vmid === was
82
+ ? undefined
83
+ : `vmid: ${String(was)} -> ${String(vmid)} is a different VM. Declare it as a new resource; ` +
84
+ 'this one is retained when its declaration goes.';