@homeflare/alchemy 0.38.0 → 0.39.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 (165) hide show
  1. package/README.md +22 -0
  2. package/dist/forgejo/index.js +4 -2
  3. package/dist/forgejo/index.js.map +3 -3
  4. package/dist/forgejo/team-member.d.ts +25 -0
  5. package/dist/forgejo/team-member.d.ts.map +1 -1
  6. package/dist/proxmox/generated/constraints/index.d.ts +1 -1
  7. package/dist/proxmox/generated/constraints/index.d.ts.map +1 -1
  8. package/dist/proxmox/generated/constraints/pve-access.d.ts +1 -1
  9. package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts +1 -1
  10. package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts +1 -1
  11. package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts +1 -1
  12. package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts +1 -1
  13. package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts +1 -1
  14. package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts +1 -1
  15. package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts +1 -1
  16. package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts +1 -1
  17. package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts +1 -1
  18. package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts +1 -1
  19. package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts +1 -1
  20. package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts +1 -1
  21. package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts +1 -1
  22. package/dist/proxmox/generated/constraints/pve-nodes-storage.d.ts +23 -0
  23. package/dist/proxmox/generated/constraints/pve-nodes-storage.d.ts.map +1 -0
  24. package/dist/proxmox/generated/constraints/pve-pools.d.ts +1 -1
  25. package/dist/proxmox/generated/constraints/pve-storage.d.ts +1 -1
  26. package/dist/proxmox/index.d.ts +14 -3
  27. package/dist/proxmox/index.d.ts.map +1 -1
  28. package/dist/proxmox/index.js +565 -133
  29. package/dist/proxmox/index.js.map +33 -23
  30. package/dist/proxmox/qemu-form.d.ts +27 -18
  31. package/dist/proxmox/qemu-form.d.ts.map +1 -1
  32. package/dist/proxmox/qemu-judge.d.ts +30 -0
  33. package/dist/proxmox/qemu-judge.d.ts.map +1 -0
  34. package/dist/proxmox/qemu-lifecycle.d.ts +16 -2
  35. package/dist/proxmox/qemu-lifecycle.d.ts.map +1 -1
  36. package/dist/proxmox/qemu-net.d.ts +9 -0
  37. package/dist/proxmox/qemu-net.d.ts.map +1 -0
  38. package/dist/proxmox/qemu-props.d.ts +112 -0
  39. package/dist/proxmox/qemu-props.d.ts.map +1 -0
  40. package/dist/proxmox/qemu-volume.d.ts +17 -0
  41. package/dist/proxmox/qemu-volume.d.ts.map +1 -0
  42. package/dist/proxmox/qemu.d.ts +14 -27
  43. package/dist/proxmox/qemu.d.ts.map +1 -1
  44. package/dist/proxmox/storage-download-errors.d.ts +5 -0
  45. package/dist/proxmox/storage-download-errors.d.ts.map +1 -0
  46. package/dist/proxmox/storage-download-identity.d.ts +27 -0
  47. package/dist/proxmox/storage-download-identity.d.ts.map +1 -0
  48. package/dist/proxmox/storage-download-lifecycle.d.ts +97 -0
  49. package/dist/proxmox/storage-download-lifecycle.d.ts.map +1 -0
  50. package/dist/proxmox/storage-download-props.d.ts +36 -0
  51. package/dist/proxmox/storage-download-props.d.ts.map +1 -0
  52. package/dist/proxmox/storage-download-task.d.ts +17 -0
  53. package/dist/proxmox/storage-download-task.d.ts.map +1 -0
  54. package/dist/proxmox/storage-download.d.ts +44 -0
  55. package/dist/proxmox/storage-download.d.ts.map +1 -0
  56. package/dist/telemetry/fake-collector.d.ts +36 -0
  57. package/dist/telemetry/fake-collector.d.ts.map +1 -0
  58. package/dist/telemetry/index.d.ts +11 -0
  59. package/dist/telemetry/index.d.ts.map +1 -0
  60. package/dist/telemetry/index.js +223 -0
  61. package/dist/telemetry/index.js.map +12 -0
  62. package/dist/telemetry/layer.d.ts +86 -0
  63. package/dist/telemetry/layer.d.ts.map +1 -0
  64. package/dist/telemetry/redact-serialization.d.ts +21 -0
  65. package/dist/telemetry/redact-serialization.d.ts.map +1 -0
  66. package/dist/telemetry/redact.d.ts +46 -0
  67. package/dist/telemetry/redact.d.ts.map +1 -0
  68. package/dist/unifi/drift.d.ts +27 -0
  69. package/dist/unifi/drift.d.ts.map +1 -0
  70. package/dist/unifi/fake-unifi.d.ts +5 -1
  71. package/dist/unifi/fake-unifi.d.ts.map +1 -1
  72. package/dist/unifi/firewall-zone-form.d.ts +6 -0
  73. package/dist/unifi/firewall-zone-form.d.ts.map +1 -1
  74. package/dist/unifi/firewall-zone.d.ts +1 -1
  75. package/dist/unifi/firewall-zone.d.ts.map +1 -1
  76. package/dist/unifi/index.d.ts +4 -3
  77. package/dist/unifi/index.d.ts.map +1 -1
  78. package/dist/unifi/index.js +138 -58
  79. package/dist/unifi/index.js.map +10 -8
  80. package/dist/unifi/network-drift.d.ts +26 -0
  81. package/dist/unifi/network-drift.d.ts.map +1 -0
  82. package/dist/unifi/network-form.d.ts +40 -7
  83. package/dist/unifi/network-form.d.ts.map +1 -1
  84. package/dist/unifi/network.d.ts +1 -0
  85. package/dist/unifi/network.d.ts.map +1 -1
  86. package/dist/unifi/paginate.d.ts +36 -0
  87. package/dist/unifi/paginate.d.ts.map +1 -0
  88. package/dist/unifi/resource.d.ts +36 -1
  89. package/dist/unifi/resource.d.ts.map +1 -1
  90. package/docs/telemetry-spike.md +94 -0
  91. package/docs/telemetry.md +109 -0
  92. package/docs/unifi.md +56 -0
  93. package/package.json +6 -2
  94. package/src/forgejo/team-member.test.ts +14 -0
  95. package/src/forgejo/team-member.ts +28 -1
  96. package/src/proxmox/constraints-required.test.ts +1 -0
  97. package/src/proxmox/generated/constraints/index.ts +3 -1
  98. package/src/proxmox/generated/constraints/pve-access.ts +1 -1
  99. package/src/proxmox/generated/constraints/pve-cluster-backup.ts +1 -1
  100. package/src/proxmox/generated/constraints/pve-cluster-ceph.ts +1 -1
  101. package/src/proxmox/generated/constraints/pve-cluster-firewall.ts +1 -1
  102. package/src/proxmox/generated/constraints/pve-cluster-ha.ts +1 -1
  103. package/src/proxmox/generated/constraints/pve-cluster-metrics.ts +1 -1
  104. package/src/proxmox/generated/constraints/pve-cluster-notifications.ts +1 -1
  105. package/src/proxmox/generated/constraints/pve-cluster-replication.ts +1 -1
  106. package/src/proxmox/generated/constraints/pve-cluster-sdn.ts +1 -1
  107. package/src/proxmox/generated/constraints/pve-nodes-ceph.ts +1 -1
  108. package/src/proxmox/generated/constraints/pve-nodes-disks.ts +1 -1
  109. package/src/proxmox/generated/constraints/pve-nodes-lxc.ts +1 -1
  110. package/src/proxmox/generated/constraints/pve-nodes-network.ts +1 -1
  111. package/src/proxmox/generated/constraints/pve-nodes-qemu.ts +1 -1
  112. package/src/proxmox/generated/constraints/pve-nodes-storage.ts +30 -0
  113. package/src/proxmox/generated/constraints/pve-pools.ts +1 -1
  114. package/src/proxmox/generated/constraints/pve-storage.ts +1 -1
  115. package/src/proxmox/index.test.ts +11 -6
  116. package/src/proxmox/index.ts +19 -3
  117. package/src/proxmox/qemu-form.ts +49 -13
  118. package/src/proxmox/qemu-identity.test.ts +30 -0
  119. package/src/proxmox/qemu-judge.test.ts +65 -0
  120. package/src/proxmox/qemu-judge.ts +84 -0
  121. package/src/proxmox/qemu-lifecycle.test.ts +78 -19
  122. package/src/proxmox/qemu-lifecycle.ts +131 -32
  123. package/src/proxmox/qemu-net.test.ts +48 -0
  124. package/src/proxmox/qemu-net.ts +78 -0
  125. package/src/proxmox/qemu-ownership.test.ts +44 -0
  126. package/src/proxmox/qemu-props.test.ts +87 -0
  127. package/src/proxmox/qemu-props.ts +157 -0
  128. package/src/proxmox/qemu-volume.test.ts +80 -0
  129. package/src/proxmox/qemu-volume.ts +121 -0
  130. package/src/proxmox/qemu.ts +15 -29
  131. package/src/proxmox/storage-download-errors.ts +7 -0
  132. package/src/proxmox/storage-download-identity.test.ts +54 -0
  133. package/src/proxmox/storage-download-identity.ts +50 -0
  134. package/src/proxmox/storage-download-lifecycle.ts +226 -0
  135. package/src/proxmox/storage-download-ownership.test.ts +50 -0
  136. package/src/proxmox/storage-download-props.ts +38 -0
  137. package/src/proxmox/storage-download-task.ts +72 -0
  138. package/src/proxmox/storage-download.test.ts +216 -0
  139. package/src/proxmox/storage-download.ts +58 -0
  140. package/src/telemetry/fake-collector.ts +59 -0
  141. package/src/telemetry/http-client-sealing.test.ts +82 -0
  142. package/src/telemetry/index.ts +10 -0
  143. package/src/telemetry/layer.test.ts +204 -0
  144. package/src/telemetry/layer.ts +152 -0
  145. package/src/telemetry/redact-serialization.ts +178 -0
  146. package/src/telemetry/redact.test.ts +124 -0
  147. package/src/telemetry/redact.ts +147 -0
  148. package/src/unifi/drift.test.ts +78 -0
  149. package/src/unifi/drift.ts +56 -0
  150. package/src/unifi/errors-and-secrets.test.ts +127 -0
  151. package/src/unifi/fake-unifi.ts +5 -1
  152. package/src/unifi/firewall-zone-form.ts +31 -2
  153. package/src/unifi/firewall-zone.test.ts +21 -1
  154. package/src/unifi/firewall-zone.ts +1 -1
  155. package/src/unifi/get-only-guard.test.ts +165 -0
  156. package/src/unifi/index.ts +15 -1
  157. package/src/unifi/network-drift.test.ts +207 -0
  158. package/src/unifi/network-drift.ts +80 -0
  159. package/src/unifi/network-form.ts +11 -39
  160. package/src/unifi/network.test.ts +7 -82
  161. package/src/unifi/network.ts +6 -6
  162. package/src/unifi/paginate.test.ts +228 -0
  163. package/src/unifi/paginate.ts +129 -0
  164. package/src/unifi/resource.ts +73 -3
  165. package/src/unifi/write-op-reference.test.ts +236 -0
@@ -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.';
@@ -5,7 +5,7 @@ import * as Effect from 'effect/Effect';
5
5
  import * as Layer from 'effect/Layer';
6
6
  import { engineOver } from '../verify/fake-engine.ts';
7
7
  import { FAKE_TARGET, type FakePve, fakePve, withoutBao } from './fake-pve.ts';
8
- import { ProxmoxVm, ProxmoxVmProvider } from './qemu.ts';
8
+ import { ProxmoxVm, ProxmoxVmProvider, type VmProps } from './qemu.ts';
9
9
 
10
10
  const NODE = 'pve1';
11
11
  const VMID = 150;
@@ -42,24 +42,27 @@ describe('Proxmox.Vm distilled transport', () => {
42
42
  });
43
43
 
44
44
  test('absence creates on the collection route, waits, then settles', async () => {
45
- let exists = false;
45
+ let posted: Record<string, string> | undefined;
46
46
  const fake = fakePve((call) => {
47
47
  if (call.path.includes('/tasks/') && call.path.endsWith('/status')) {
48
48
  return { status: 'stopped', exitstatus: 'OK' };
49
49
  }
50
50
  if (isIndex(call)) return [];
51
- if (call.method === 'GET') return exists ? live : missing();
52
- exists = true;
51
+ // ⚠️ `net0` IS NOW A MANAGED, COMPARED KEY (qemu-props.ts) — the old fixture answered the
52
+ // SAME static `live` regardless of what was posted, which only worked because the old
53
+ // `matches` never looked at `net0` at all. Echoing the create form back is what proves
54
+ // the declared value is what a real PVE config would report.
55
+ if (call.method === 'GET') return posted === undefined ? missing() : { ...live, ...posted };
56
+ posted = call.form;
53
57
  return UPID;
54
58
  });
59
+ const declared = { ...props, net0: 'virtio=AA:BB:CC:DD:EE:FF,bridge=vmbr0' };
55
60
  await withoutBao(async () => {
56
61
  const stack = engine(fake);
57
- await stack.deploy(
58
- ProxmoxVm('row', { ...props, net0: 'virtio=AA:BB:CC:DD:EE:FF,bridge=vmbr0' }),
62
+ await stack.deploy(ProxmoxVm('row', declared));
63
+ expect((await stack.verify(ProxmoxVm('row', declared), { all: true })).rows[0]).toMatchObject(
64
+ { diff: 'noop' },
59
65
  );
60
- expect((await stack.verify(ProxmoxVm('row', props), { all: true })).rows[0]).toMatchObject({
61
- diff: 'noop',
62
- });
63
66
  });
64
67
  expect(fake.writes()).toEqual([`POST nodes/${NODE}/qemu`]);
65
68
  expect(fake.calls.find((call) => call.method === 'POST')?.form['net0']).toBe(
@@ -68,6 +71,41 @@ describe('Proxmox.Vm distilled transport', () => {
68
71
  expect(fake.calls.some((call) => call.path.includes('/tasks/'))).toBe(true);
69
72
  });
70
73
 
74
+ test('C1: a "fresh" disk/cloudinit and a MAC-less NIC do not re-drift after PVE allocates them', async () => {
75
+ // ⛔ THE MEASURED REGRESSION: before qemu-judge.ts, every deploy after the create re-sent
76
+ // scsi0/ide2/net0 in their DECLARED (fresh) spelling, because a plain string compare never
77
+ // equals what PVE reports back for an allocated volume or a generated MAC -- reallocating the
78
+ // disk and handing out a new MAC on every single deploy.
79
+ const declared = {
80
+ ...props,
81
+ ide2: 'cephtb4:cloudinit',
82
+ net0: 'virtio,bridge=vmbr0,tag=20',
83
+ scsi0: 'cephtb4:32',
84
+ };
85
+ const allocated = {
86
+ ide2: 'cephtb4:vm-150-cloudinit,media=cdrom',
87
+ net0: 'virtio=AA:BB:CC:DD:EE:FF,bridge=vmbr0,tag=20',
88
+ scsi0: 'cephtb4:vm-150-disk-0,size=32G',
89
+ };
90
+ let created = false;
91
+ const fake = fakePve((call) => {
92
+ if (call.path.includes('/tasks/') && call.path.endsWith('/status')) {
93
+ return { status: 'stopped', exitstatus: 'OK' };
94
+ }
95
+ if (isIndex(call)) return [];
96
+ if (call.method === 'GET') return created ? { ...live, ...allocated } : missing();
97
+ created = true;
98
+ return UPID;
99
+ });
100
+ await withoutBao(async () => {
101
+ const stack = engine(fake);
102
+ await stack.deploy(ProxmoxVm('row', declared));
103
+ await stack.deploy(ProxmoxVm('row', declared));
104
+ await stack.deploy(ProxmoxVm('row', declared));
105
+ });
106
+ expect(fake.writes()).toEqual([`POST nodes/${NODE}/qemu`]);
107
+ });
108
+
71
109
  test('a transitional task status keeps polling instead of aborting the create', async () => {
72
110
  // ⚠️ `GetNodeTaskStatusResponseStatus` types `status` as the closed union
73
111
  // "running" | "stopped", but the runtime validator is `S.String` (distilled-proxmox's
@@ -113,24 +151,45 @@ describe('Proxmox.Vm distilled transport', () => {
113
151
  expect(fake.writes()).toEqual([]);
114
152
  });
115
153
 
116
- test('drift updates the config route and the next plan is a no-op', async () => {
117
- let current: unknown = { ...live, memory: '256' };
154
+ test('drift on a declared field updates the config route, and only that field', async () => {
155
+ // ⛔ THE "5-DEFAULT PUT" REGRESSION TEST. `name` is deliberately left UNDECLARED and deliberately
156
+ // NOT what qemu-form.ts's old default would have produced (`vm150`) -- the old `shape()` sent
157
+ // it (and `cores`/`sockets`/`onboot`) on every write regardless, silently resetting an
158
+ // undeclared field to its factory value. Only `memory` is declared here, so only `memory`
159
+ // may appear in the PUT body.
160
+ const declared = { ...props, memory: 512 };
161
+ let current: unknown = { ...live, memory: '256', name: 'a-name-nobody-declared' };
118
162
  const fake = fakePve((call) => {
119
163
  if (call.method === 'GET') return current;
120
- current = live;
164
+ current = { ...(current as Record<string, unknown>), memory: '512' };
121
165
  return null;
122
166
  });
123
167
  await withoutBao(async () => {
124
168
  const stack = engine(fake);
125
- expect((await stack.verify(ProxmoxVm('row', props), { all: true })).rows[0]).toMatchObject({
126
- diff: 'update',
127
- });
128
- await stack.deploy(ProxmoxVm('row', props));
129
- expect((await stack.verify(ProxmoxVm('row', props), { all: true })).rows[0]).toMatchObject({
130
- diff: 'noop',
131
- });
169
+ expect((await stack.verify(ProxmoxVm('row', declared), { all: true })).rows[0]).toMatchObject(
170
+ { diff: 'update' },
171
+ );
172
+ await stack.deploy(ProxmoxVm('row', declared));
173
+ expect((await stack.verify(ProxmoxVm('row', declared), { all: true })).rows[0]).toMatchObject(
174
+ { diff: 'noop' },
175
+ );
132
176
  });
133
177
  expect(fake.writes()).toEqual([`PUT nodes/${NODE}/qemu/${String(VMID)}/config`]);
178
+ const put = fake.calls.find((call) => call.method === 'PUT');
179
+ expect(put?.form).toEqual({ memory: '512' });
180
+ });
181
+
182
+ test('an unmanaged key smuggled past the types is refused before any write', async () => {
183
+ const fake = fakePve((call) => (call.method === 'GET' ? live : null));
184
+ // ⛔ `cipassword` IS TYPED `never` (qemu-props.ts): a cast is the only way in, exactly the
185
+ // scenario the type cannot guard against by itself. `formRefusals` is the runtime backstop.
186
+ const smuggled = { ...props, cipassword: 'not-a-real-secret' } as unknown as VmProps;
187
+ await withoutBao(async () => {
188
+ await expect(engine(fake).deploy(ProxmoxVm('row', smuggled))).rejects.toThrow(
189
+ /cipassword: not a config key this resource manages/,
190
+ );
191
+ });
192
+ expect(fake.writes()).toEqual([]);
134
193
  });
135
194
 
136
195
  test('unrelated read failures and a digest-less body do not write', async () => {