@homeflare/alchemy 0.37.1 → 0.37.3

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 (195) hide show
  1. package/dist/proxmox/backup-job-config.d.ts +95 -0
  2. package/dist/proxmox/backup-job-config.d.ts.map +1 -0
  3. package/dist/proxmox/backup-job-distilled.d.ts +46 -0
  4. package/dist/proxmox/backup-job-distilled.d.ts.map +1 -0
  5. package/dist/proxmox/backup-job.d.ts +9 -26
  6. package/dist/proxmox/backup-job.d.ts.map +1 -1
  7. package/dist/proxmox/ceph-fs-distilled.d.ts +17 -10
  8. package/dist/proxmox/ceph-fs-distilled.d.ts.map +1 -1
  9. package/dist/proxmox/ceph-fs-wire.d.ts +5 -22
  10. package/dist/proxmox/ceph-fs-wire.d.ts.map +1 -1
  11. package/dist/proxmox/ceph-fs.d.ts +3 -3
  12. package/dist/proxmox/distilled-pbs.d.ts +12 -0
  13. package/dist/proxmox/distilled-pbs.d.ts.map +1 -0
  14. package/dist/proxmox/fake-pbs-jobs.d.ts +23 -0
  15. package/dist/proxmox/fake-pbs-jobs.d.ts.map +1 -0
  16. package/dist/proxmox/fake-pbs-notify.d.ts.map +1 -1
  17. package/dist/proxmox/fake-pve.d.ts +1 -1
  18. package/dist/proxmox/fake-pve.d.ts.map +1 -1
  19. package/dist/proxmox/group-wire.d.ts +2 -2
  20. package/dist/proxmox/group-wire.d.ts.map +1 -1
  21. package/dist/proxmox/index.js +1382 -506
  22. package/dist/proxmox/index.js.map +51 -27
  23. package/dist/proxmox/live-core-fixtures.d.ts +40 -0
  24. package/dist/proxmox/live-core-fixtures.d.ts.map +1 -0
  25. package/dist/proxmox/metric-server-config.d.ts +49 -0
  26. package/dist/proxmox/metric-server-config.d.ts.map +1 -0
  27. package/dist/proxmox/metric-server-distilled.d.ts +43 -0
  28. package/dist/proxmox/metric-server-distilled.d.ts.map +1 -0
  29. package/dist/proxmox/metric-server.d.ts +2 -10
  30. package/dist/proxmox/metric-server.d.ts.map +1 -1
  31. package/dist/proxmox/node-network-wire.d.ts +1 -1
  32. package/dist/proxmox/node-network-wire.d.ts.map +1 -1
  33. package/dist/proxmox/notification-matcher-distilled.d.ts +9 -0
  34. package/dist/proxmox/notification-matcher-distilled.d.ts.map +1 -0
  35. package/dist/proxmox/notification-matcher-lifecycle.d.ts +27 -0
  36. package/dist/proxmox/notification-matcher-lifecycle.d.ts.map +1 -0
  37. package/dist/proxmox/notification-matcher.d.ts +5 -5
  38. package/dist/proxmox/notification-matcher.d.ts.map +1 -1
  39. package/dist/proxmox/notification-sdk-input.d.ts +22 -0
  40. package/dist/proxmox/notification-sdk-input.d.ts.map +1 -0
  41. package/dist/proxmox/notification-target-distilled.d.ts +12 -0
  42. package/dist/proxmox/notification-target-distilled.d.ts.map +1 -0
  43. package/dist/proxmox/notification-target-form.d.ts +2 -0
  44. package/dist/proxmox/notification-target-form.d.ts.map +1 -1
  45. package/dist/proxmox/notification-target-lifecycle.d.ts +29 -0
  46. package/dist/proxmox/notification-target-lifecycle.d.ts.map +1 -0
  47. package/dist/proxmox/notification-target-spec.d.ts +5 -0
  48. package/dist/proxmox/notification-target-spec.d.ts.map +1 -0
  49. package/dist/proxmox/notification-target.d.ts +4 -5
  50. package/dist/proxmox/notification-target.d.ts.map +1 -1
  51. package/dist/proxmox/pbs-datastore-endpoint.d.ts +1 -1
  52. package/dist/proxmox/pbs-datastore-form.d.ts +18 -42
  53. package/dist/proxmox/pbs-datastore-form.d.ts.map +1 -1
  54. package/dist/proxmox/pbs-datastore-guard.d.ts +7 -4
  55. package/dist/proxmox/pbs-datastore-guard.d.ts.map +1 -1
  56. package/dist/proxmox/pbs-datastore-lifecycle.d.ts +56 -0
  57. package/dist/proxmox/pbs-datastore-lifecycle.d.ts.map +1 -0
  58. package/dist/proxmox/pbs-datastore-wire.d.ts +21 -0
  59. package/dist/proxmox/pbs-datastore-wire.d.ts.map +1 -0
  60. package/dist/proxmox/pbs-datastore.d.ts +5 -6
  61. package/dist/proxmox/pbs-datastore.d.ts.map +1 -1
  62. package/dist/proxmox/pbs-notification-input.d.ts +18 -0
  63. package/dist/proxmox/pbs-notification-input.d.ts.map +1 -0
  64. package/dist/proxmox/pbs-notification-matcher-distilled.d.ts +11 -0
  65. package/dist/proxmox/pbs-notification-matcher-distilled.d.ts.map +1 -0
  66. package/dist/proxmox/pbs-notification-matcher.d.ts +4 -3
  67. package/dist/proxmox/pbs-notification-matcher.d.ts.map +1 -1
  68. package/dist/proxmox/pbs-notification-target-distilled.d.ts +20 -0
  69. package/dist/proxmox/pbs-notification-target-distilled.d.ts.map +1 -0
  70. package/dist/proxmox/pbs-notification-target-lifecycle.d.ts +4 -4
  71. package/dist/proxmox/pbs-notification-target-lifecycle.d.ts.map +1 -1
  72. package/dist/proxmox/pbs-notification-target.d.ts +1 -1
  73. package/dist/proxmox/pbs-prune-job-lifecycle.d.ts +22 -0
  74. package/dist/proxmox/pbs-prune-job-lifecycle.d.ts.map +1 -0
  75. package/dist/proxmox/pbs-prune-job-spec.d.ts +5 -0
  76. package/dist/proxmox/pbs-prune-job-spec.d.ts.map +1 -0
  77. package/dist/proxmox/pbs-prune-job-wire.d.ts +11 -0
  78. package/dist/proxmox/pbs-prune-job-wire.d.ts.map +1 -0
  79. package/dist/proxmox/pbs-prune-job.d.ts +9 -8
  80. package/dist/proxmox/pbs-prune-job.d.ts.map +1 -1
  81. package/dist/proxmox/pbs-sync-job-attributes.d.ts +34 -0
  82. package/dist/proxmox/pbs-sync-job-attributes.d.ts.map +1 -0
  83. package/dist/proxmox/pbs-sync-job-form.d.ts +21 -22
  84. package/dist/proxmox/pbs-sync-job-form.d.ts.map +1 -1
  85. package/dist/proxmox/pbs-sync-job-lifecycle.d.ts +22 -0
  86. package/dist/proxmox/pbs-sync-job-lifecycle.d.ts.map +1 -0
  87. package/dist/proxmox/pbs-sync-job-wire.d.ts +11 -0
  88. package/dist/proxmox/pbs-sync-job-wire.d.ts.map +1 -0
  89. package/dist/proxmox/pbs-sync-job.d.ts +9 -39
  90. package/dist/proxmox/pbs-sync-job.d.ts.map +1 -1
  91. package/dist/proxmox/pbs-verify-job-lifecycle.d.ts +22 -0
  92. package/dist/proxmox/pbs-verify-job-lifecycle.d.ts.map +1 -0
  93. package/dist/proxmox/pbs-verify-job-spec.d.ts +16 -0
  94. package/dist/proxmox/pbs-verify-job-spec.d.ts.map +1 -0
  95. package/dist/proxmox/pbs-verify-job-wire.d.ts +11 -0
  96. package/dist/proxmox/pbs-verify-job-wire.d.ts.map +1 -0
  97. package/dist/proxmox/pbs-verify-job.d.ts +12 -12
  98. package/dist/proxmox/pbs-verify-job.d.ts.map +1 -1
  99. package/dist/proxmox/pool.d.ts +10 -1
  100. package/dist/proxmox/pool.d.ts.map +1 -1
  101. package/dist/proxmox/storage-wire.d.ts +2 -2
  102. package/dist/proxmox/storage-wire.d.ts.map +1 -1
  103. package/dist/proxmox/user-wire.d.ts +2 -2
  104. package/dist/proxmox/user-wire.d.ts.map +1 -1
  105. package/dist/proxmox/zfs-pool-wire.d.ts +1 -1
  106. package/dist/proxmox/zfs-pool-wire.d.ts.map +1 -1
  107. package/docs/changelog/README.md +22 -0
  108. package/docs/changelog/archive-01.md +176 -0
  109. package/docs/changelog/archive-02.md +180 -0
  110. package/docs/changelog/archive-03.md +179 -0
  111. package/docs/changelog/archive-04.md +178 -0
  112. package/docs/changelog/archive-05.md +177 -0
  113. package/docs/changelog/archive-06.md +180 -0
  114. package/docs/changelog/archive-07.md +180 -0
  115. package/docs/changelog/archive-08.md +178 -0
  116. package/docs/changelog/archive-09.md +181 -0
  117. package/docs/changelog/archive-10.md +182 -0
  118. package/docs/changelog/archive-11.md +182 -0
  119. package/docs/changelog/archive-12.md +183 -0
  120. package/docs/changelog/archive-13.md +180 -0
  121. package/docs/changelog/archive-14.md +178 -0
  122. package/docs/changelog/archive-15.md +181 -0
  123. package/docs/changelog/archive-16.md +182 -0
  124. package/docs/changelog/archive-17.md +183 -0
  125. package/docs/changelog/archive-18.md +55 -0
  126. package/docs/pbs-distilled-jobs.md +31 -0
  127. package/docs/pbs-notifications.md +8 -2
  128. package/docs/proxmox-notification-sdk.md +28 -0
  129. package/docs/proxmox-sdk-migration.md +60 -0
  130. package/docs/proxmox.md +9 -3
  131. package/docs/upstream-conformance-families.md +6 -2
  132. package/package.json +3 -3
  133. package/src/proxmox/adopt-noop.test.ts +4 -5
  134. package/src/proxmox/backup-job-config.ts +95 -0
  135. package/src/proxmox/backup-job-distilled.ts +86 -0
  136. package/src/proxmox/backup-job.ts +78 -116
  137. package/src/proxmox/ceph-fs-delete.test.ts +5 -8
  138. package/src/proxmox/ceph-fs-destroy-sdk.test.ts +98 -0
  139. package/src/proxmox/ceph-fs-distilled.ts +41 -13
  140. package/src/proxmox/ceph-fs-wire.ts +5 -93
  141. package/src/proxmox/ceph-fs.ts +7 -7
  142. package/src/proxmox/constraints.test.ts +9 -8
  143. package/src/proxmox/distilled-pbs.test.ts +87 -0
  144. package/src/proxmox/distilled-pbs.ts +50 -0
  145. package/src/proxmox/fake-pbs-jobs.ts +99 -0
  146. package/src/proxmox/fake-pbs-notify.ts +4 -1
  147. package/src/proxmox/fake-pve.ts +3 -2
  148. package/src/proxmox/live-core-fixtures.ts +126 -0
  149. package/src/proxmox/live-core-lifecycle.test.ts +115 -0
  150. package/src/proxmox/live-core-wire.test.ts +190 -0
  151. package/src/proxmox/metric-server-config.ts +87 -0
  152. package/src/proxmox/metric-server-distilled.ts +81 -0
  153. package/src/proxmox/metric-server.ts +77 -88
  154. package/src/proxmox/notification-distilled.test.ts +162 -0
  155. package/src/proxmox/notification-matcher-distilled.ts +60 -0
  156. package/src/proxmox/notification-matcher-lifecycle.ts +71 -0
  157. package/src/proxmox/notification-matcher.ts +9 -26
  158. package/src/proxmox/notification-replacement.test.ts +190 -0
  159. package/src/proxmox/notification-sdk-input.ts +39 -0
  160. package/src/proxmox/notification-target-distilled.ts +160 -0
  161. package/src/proxmox/notification-target-form.ts +2 -0
  162. package/src/proxmox/notification-target-lifecycle.ts +75 -0
  163. package/src/proxmox/notification-target-spec.ts +68 -0
  164. package/src/proxmox/notification-target.ts +6 -68
  165. package/src/proxmox/pbs-datastore-endpoint.ts +1 -1
  166. package/src/proxmox/pbs-datastore-form.ts +18 -49
  167. package/src/proxmox/pbs-datastore-guard.ts +7 -4
  168. package/src/proxmox/pbs-datastore-lifecycle.ts +114 -0
  169. package/src/proxmox/pbs-datastore-wire.ts +94 -0
  170. package/src/proxmox/pbs-datastore.ts +7 -133
  171. package/src/proxmox/pbs-jobs-delete.test.ts +40 -0
  172. package/src/proxmox/pbs-jobs-sdk.test.ts +215 -0
  173. package/src/proxmox/pbs-notification-errors.test.ts +93 -0
  174. package/src/proxmox/pbs-notification-input.ts +25 -0
  175. package/src/proxmox/pbs-notification-mail.test.ts +64 -0
  176. package/src/proxmox/pbs-notification-matcher-distilled.ts +65 -0
  177. package/src/proxmox/pbs-notification-matcher.ts +70 -17
  178. package/src/proxmox/pbs-notification-target-distilled.ts +124 -0
  179. package/src/proxmox/pbs-notification-target-lifecycle.ts +26 -17
  180. package/src/proxmox/pbs-notification-target.ts +1 -1
  181. package/src/proxmox/pbs-notifications-stack.test.ts +25 -0
  182. package/src/proxmox/pbs-prune-job-lifecycle.ts +52 -0
  183. package/src/proxmox/pbs-prune-job-spec.ts +53 -0
  184. package/src/proxmox/pbs-prune-job-wire.ts +68 -0
  185. package/src/proxmox/pbs-prune-job.ts +10 -58
  186. package/src/proxmox/pbs-sync-job-attributes.ts +33 -0
  187. package/src/proxmox/pbs-sync-job-form.ts +7 -6
  188. package/src/proxmox/pbs-sync-job-lifecycle.ts +46 -0
  189. package/src/proxmox/pbs-sync-job-wire.ts +71 -0
  190. package/src/proxmox/pbs-sync-job.ts +10 -41
  191. package/src/proxmox/pbs-verify-job-lifecycle.ts +52 -0
  192. package/src/proxmox/pbs-verify-job-spec.ts +76 -0
  193. package/src/proxmox/pbs-verify-job-wire.ts +58 -0
  194. package/src/proxmox/pbs-verify-job.ts +13 -85
  195. package/src/proxmox/pool.ts +92 -10
@@ -0,0 +1,183 @@
1
+ # Alchemy changelog archive 17
2
+
3
+ [Current changelog](../../CHANGELOG.md) · [Archive index](./README.md)
4
+
5
+ ### Minor Changes
6
+
7
+ - [#67](https://github.com/taslabs-net/homeflare-kit/pull/67) [`48163cf`](https://github.com/taslabs-net/homeflare-kit/commit/48163cfc83ad7deac8720be2e8fb7fe964ade7a3) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Add to `@homeflare/alchemy/openbao`:
8
+
9
+ - **`BaoJwtRole`**: roles on `jwt` and `oidc` mounts.
10
+ - **`BaoKubernetesRole`**: roles on `kubernetes` mounts. `aliasNameSource` is required, because changing it on a live role moves every pod to a new entity.
11
+ - **`BaoJwtAuthConfig`**: the non-secret config of a JWT-validating mount. It deliberately has no OIDC client secret, and it refuses a mount that has one, because its full-replace write would erase it.
12
+ - **`BaoMfaTotpMethod`** and **`BaoMfaLoginEnforcement`**: login MFA. A TOTP method is found by name. Renaming one is refused, because a rename would strand every enrolled secret. A name already held by another MFA method type is refused too, because the write would convert that method. Deleting an enforcement is refused, because in OpenBao 2.6.2 the delete comes back after a restart (openbao/openbao#4030).
13
+ - **`assertBaoIdentity`** (with `assertBaoIdentityEffect` and `BaoIdentityError`): refuses to proceed unless unauthenticated `sys/health` reports the expected `cluster_name` and the namespace is the expected one.
14
+ - **`hostAppRoles`**: a pure generator that makes one AppRole per host, named `<class>--<host>`. It refuses a secret_id TTL of 0.
15
+
16
+ Behaviour changes:
17
+
18
+ - A changed `path` on `BaoMount` or `BaoAuthMethod` now **fails the plan** unless the new `remountFrom` prop names the old path. With `remountFrom`, the mount is moved with `sys/remount`, keeping its data (leases under it are revoked). Before, the plan answered `update` and enabled an empty mount at the new path.
19
+ - A changed `name` on `BaoAuthRole`, or a changed `name` on `BaoPkiRole`, now plans `replace`. Before, it answered `update` and left the old role live with no state record.
20
+ - `BaoPkiRole` now manages `requireCn`, `enforceHostnames`, `keyUsage`, `allowedDomainsTemplate`, `noStore` and `generateLease`, defaulting to OpenBao's own values. Its full-replace write already reset these fields silently, so a role whose live values differ from those defaults now plans `update` instead of being reset unseen. `noStore` together with `generateLease` is refused.
21
+
22
+ See `src/openbao/REPLACE.md` for what every family does on a rename.
23
+
24
+ - [#68](https://github.com/taslabs-net/homeflare-kit/pull/68) [`b023bae`](https://github.com/taslabs-net/homeflare-kit/commit/b023bae8f20d6aa33239107dc0538750efc466ca) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Add the `@homeflare/alchemy/launchd` subpath, so a Mac host can be declared with Alchemy.
25
+
26
+ - `LaunchdJob` / `LaunchdJobProvider` (`Launchd.Job`): one launchd job in the `system` domain or a `gui/<uid>` domain. The provider renders the plist itself, writes it atomically to the derived path (`/Library/LaunchDaemons` or the user's `LaunchAgents`) and drives `launchctl`. Create bootstraps; update writes, boots out and bootstraps (a restart); read uses `launchctl print`; diff compares the rendered plist's SHA-256 with the stored and on-disk digests. Replace happens only on a `label` or `domain` change, delete-first — so everything the new job would be refused for is refused at plan time, while the old job still runs, and a rename is seen even while other props are unresolved. A label another job already holds is never booted out or overwritten. A failed first bootstrap removes the plist it wrote. A disabled label is refused, not re-enabled. Labels under `org.nixos.`, `com.apple.` and `homebrew.mxcl.` are refused; `docs/launchd.md` describes the nix-darwin cutover.
27
+ - `HostFile` / `HostFileProvider` (`Host.File`): a text file with mode, owner and group. It is written atomically (temp file, then rename), diffed by SHA-256, mode and owner, and refused over a symlink, a directory, or a different file at a new path.
28
+ - `HostRunner`, `localRunner()`, `hostRunnerLayer()` and `launchdProviders()`: every filesystem and `launchctl` call goes through one injectable runner. The local runner never elevates. System-domain writes are refused unless the deploy runs as root or the runner is explicitly `privileged`.
29
+ - `renderPlist`: a small, deterministic XML plist serializer, round-tripped through `plutil` in the tests.
30
+
31
+ Props are stored unencrypted in Alchemy state, so `environment`, `programArguments` and file `content` must not hold secrets. A tripwire refuses the obvious cases; secret files stay rendered by openbao-agent, and the stack declares only their path. Anything already on the host is read as `Unowned`, so it is never adopted without `--adopt`.
32
+
33
+ ## 0.5.0
34
+
35
+ ### Minor Changes
36
+
37
+ - [#64](https://github.com/taslabs-net/homeflare-kit/pull/64) [`3ed9771`](https://github.com/taslabs-net/homeflare-kit/commit/3ed977142ffe2fe6dd02359a3a87b1a0a84c9952) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Add `appRoleLogin` / `revokeSelf` (with `appRoleLoginEffect`, `revokeSelfEffect` and `BaoLoginError`) to `@homeflare/alchemy/openbao`, so wrapper scripts can log in with an AppRole and revoke on exit. They use the same address resolution and transport as the `Bao.*` families. The login never sends `BAO_TOKEN`, and error strings are redacted, because OpenBao 2.6.2 can echo a secret_id back in an error. `clientToken` is a getter over a private field, so printing or serialising the result does not show the token under Bun or Node.
38
+
39
+ Add `BaoPlugin` / `BaoPluginProvider` for the plugin catalog (`sys/plugins/catalog/<type>/<name>`). It has no `env` prop and retains on destroy. It refuses to shadow a builtin or overwrite a declarative entry, and it names the fix when an unversioned registration is filed under the binary's self-reported version.
40
+
41
+ Fix `BaoSshRole` writes. `default_extensions` and `default_critical_options` were sent as JSON strings. OpenBao's field validation rejects that with a 400, so every role write failed. They are now sent as objects.
42
+
43
+ ## 0.4.1
44
+
45
+ ### Patch Changes
46
+
47
+ - [#62](https://github.com/taslabs-net/homeflare-kit/pull/62) [`93b7107`](https://github.com/taslabs-net/homeflare-kit/commit/93b71070b1a64701cadc1a547044a3ec4da26a50) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Export TB4 control-plane Resource constructors from `@homeflare/alchemy/proxmox` (Storage, SDN, access, HA, backup, metrics, PBS). Guests and NIC apply stay Provider-only.
48
+
49
+ ## 0.4.0
50
+
51
+ ### Minor Changes
52
+
53
+ - [#60](https://github.com/taslabs-net/homeflare-kit/pull/60) [`df34d27`](https://github.com/taslabs-net/homeflare-kit/commit/df34d2750fca4a2b6b86490ddb0a819c12f9ea39) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Add `BaoAuthMethod` so a stack can enable `approle` (or jwt/oidc) instead of running `configure-engines`. Metadata only — no role ids or OIDC secrets. File audit stays out: OpenBao 2.6 file audit is config-only.
54
+
55
+ ## 0.3.2
56
+
57
+ ### Patch Changes
58
+
59
+ - [#58](https://github.com/taslabs-net/homeflare-kit/pull/58) [`e7c101f`](https://github.com/taslabs-net/homeflare-kit/commit/e7c101faada2b14466a1f0bbf1f93da46e5e06cb) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Bump Alchemy to 2.0.0-beta.79. Effect stays rc.115. 79 starts Bun with production JSX (the `jsxDEV` CLI crash on 78) and declares `mime` on cloudflare-runtime; the `mime` peer stays so existing consumer installs do not drop a required line.
60
+
61
+ ## 0.3.1
62
+
63
+ ### Patch Changes
64
+
65
+ - [#56](https://github.com/taslabs-net/homeflare-kit/pull/56) [`5f41ad7`](https://github.com/taslabs-net/homeflare-kit/commit/5f41ad7ab59375e42bb4757dd5b4b81f4a7b6cd9) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Export `BaoPkiRole` and `BaoSshRole` from `@homeflare/alchemy/openbao`. The barrel already shipped both providers; stacks constructing either role had to import the resource from internals.
66
+
67
+ ## 0.3.0
68
+
69
+ ### Minor Changes
70
+
71
+ - [#54](https://github.com/taslabs-net/homeflare-kit/pull/54) [`d526365`](https://github.com/taslabs-net/homeflare-kit/commit/d526365a51deac968e5b0076e88b2e68b866534f) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Bump Alchemy to 2.0.0-beta.78 and Effect to 4.0.0-rc.115. Consumers must move the override set with it — Alchemy 78's peer is `effect >= rc.115`. Effect rc.115 renamed `Config.redacted` to `Config.Redacted`. `mime@4.1.0` is now a required peer: Alchemy's cloudflare-runtime imports it and does not declare it.
72
+
73
+ ## 0.2.2
74
+
75
+ ### Patch Changes
76
+
77
+ - [#46](https://github.com/taslabs-net/homeflare-kit/pull/46) [`9bc37f8`](https://github.com/taslabs-net/homeflare-kit/commit/9bc37f8bd7b9af1fd5c62cc4bc48597251ef61ab) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Export `BaoMount` and `BaoAuthRole` from `@homeflare/alchemy/openbao`.
78
+
79
+ The docs already showed `import { BaoMount } from '@homeflare/alchemy/openbao'`, but the barrel only shipped the providers. Stacks constructing those resources had to import from internals.
80
+
81
+ - [#48](https://github.com/taslabs-net/homeflare-kit/pull/48) [`4967118`](https://github.com/taslabs-net/homeflare-kit/commit/4967118cc887faba34b17cd48588102736be49bb) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Export `BaoCloudflareRole` and the Cloudflare role catalog helpers from `@homeflare/alchemy/openbao`, and retry dropped OpenBao transports.
82
+
83
+ Stacks declaring mint roles need the resource constructor plus `parseRolesConfig` / `expandAll` / `permissionGroupsFromEngine`. The barrel previously shipped only `BaoCloudflareRoleProvider`. A 585-role plan against a Mesh-fronted vault died mid-diff with an empty transport error; status-0 calls now retry twice.
84
+
85
+ ## 0.2.1
86
+
87
+ ### Patch Changes
88
+
89
+ - [#40](https://github.com/taslabs-net/homeflare-kit/pull/40) [`60c90eb`](https://github.com/taslabs-net/homeflare-kit/commit/60c90eb4682d0ded2597a79033159ab71cc7a649) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Pin `rolldown` in the consumer overrides so an unlocked install cannot float a missing tarball.
90
+
91
+ Alchemy's optional peer `vite@^8` depends on `rolldown: ~1.2.6`. A consumer `bun add`
92
+ (no lockfile) resolved that to 1.2.9; npm listed the version and 404'd
93
+ `rolldown-1.2.9.tgz`. Main CI failed on that fetch after [#39](https://github.com/taslabs-net/homeflare-kit/issues/39). The override holds 1.2.8 —
94
+ the last tarball a green smoke actually installed.
95
+
96
+ ## 0.2.0
97
+
98
+ ### Minor Changes
99
+
100
+ - [#36](https://github.com/taslabs-net/homeflare-kit/pull/36) [`cff4111`](https://github.com/taslabs-net/homeflare-kit/commit/cff4111c2f4461a45be5811547125b1f5d98c6d2) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Ship the HTTP and Website SDK surfaces apps were hand-rolling.
101
+
102
+ **`@homeflare/kit/openapi`** — `createOpenApiApp()` is OpenAPIHono with one
103
+ readable validation hook. The document and the request share a Zod schema.
104
+ Subpath, not the main entry: a Node script that only wants `parseEnv` must not
105
+ resolve Hono. Peers: `hono`, `@hono/zod-openapi`, `zod` (optional). Import `z`
106
+ from `@hono/zod-openapi`.
107
+
108
+ **`astroWebsite` / `viteWebsite`** on `@homeflare/alchemy/cloudflare` — house
109
+ flags on Alchemy's own stacks. Astro gets `disable_nodejs_process_v2` (workerd
110
+ process-v2 returns `[object Object]`). Vite is TanStack Start / static Vite.
111
+ Not Nextjs: that helper hashes source and plans as create against a live Worker.
112
+
113
+ Catalog also pins `@tanstack/react-router` 1.170.35, `@tanstack/react-start`
114
+ 1.168.52, `@tanstack/react-query` 5.102.8 — match these, do not wrap them.
115
+
116
+ ## 0.1.3
117
+
118
+ ### Patch Changes
119
+
120
+ - [#32](https://github.com/taslabs-net/homeflare-kit/pull/32) [`5cc2cd6`](https://github.com/taslabs-net/homeflare-kit/commit/5cc2cd62241f926aace1646fd3a1e0057e96cddd) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Add `accessIdentity` (ctx.access) and RFC 9728 MCP discovery; fix three doc defects.
121
+
122
+ An app team reviewed the published packages before adopting and was right on every point.
123
+
124
+ **`accessIdentity(ctx)` — Access identity without parsing a JWT.** Cloudflare attaches the
125
+ authenticated identity to the execution context (shipped 2026-08-14), so a Worker behind
126
+ Access reads `ctx.access.getIdentity()` with no token handling. The kit only offered
127
+ `verifyAccessJwt`, which is the older path.
128
+
129
+ ⛔ Both stay, because they are not alternatives: `accessIdentity` for a Worker behind
130
+ Access, `verifyAccessJwt` for an origin that has no `ctx.access` — service-to-service, a
131
+ non-Worker origin, or a Worker reached by service binding, since **`ctx.access` does not
132
+ propagate through bindings**.
133
+
134
+ ⚠️ Read groups from `accessIdentity`, not from a token: Cloudflare trims the JWT's
135
+ `custom` claim at roughly 1 KB _silently_, so token-read group membership can be
136
+ incomplete — an authorization bug that only appears for users in many groups.
137
+
138
+ **`serveMcpMetadata` / `unauthorizedResponse` — RFC 9728.** The MCP spec requires a server
139
+ to publish Protected Resource Metadata _and_ a 401 naming it in `WWW-Authenticate`.
140
+ Publishing the document while answering a bare 401 leaves clients that follow the header
141
+ with nowhere to go.
142
+
143
+ **Three documentation defects, all reported and all real:**
144
+
145
+ - `@homeflare/alchemy`'s README documented `@homeflare/alchemy/providers`, which does not
146
+ exist. The real path is `/cloudflare`.
147
+ - `@homeflare/cloudflare`'s npm description advertised "typed bindings" — it exports none.
148
+ - `@homeflare/kit`'s advertised "logging" — `log` lives in `@homeflare/cloudflare`.
149
+
150
+ **Packing no longer edits a manifest on disk.** `packForPublish` stripped `scripts` and
151
+ `devDependencies` from the real `package.json`, packed, then restored it — which is a race
152
+ when two smoke tests pack the same workspace dependency in parallel. It destroyed
153
+ `@homeflare/kit`'s `scripts` block during this branch, _after_ `verify` had passed. The
154
+ strip now happens inside the packed tarball, so nothing in the repository is written to.
155
+
156
+ ## 0.1.2
157
+
158
+ ### Patch Changes
159
+
160
+ - [#29](https://github.com/taslabs-net/homeflare-kit/pull/29) [`72f0787`](https://github.com/taslabs-net/homeflare-kit/commit/72f0787952b3a71bb6573476918e79117250409e) Thanks [@taslabs-net](https://github.com/taslabs-net)! - `cloudflare` is a required peer, not an optional one.
161
+
162
+ Measured 2026-09-16 against the published 0.1.1 in a clean consumer install: importing
163
+ `@homeflare/alchemy/cloudflare` without it throws `Cannot find package 'cloudflare'`.
164
+ Marking it optional claimed the subpath would degrade gracefully; it does not load at all.
165
+ An optional peer should mean a _feature_ is absent, not that an import fails.
166
+
167
+ ⛔ **Why this got through, and what now stops it.** The README's pinned install command and
168
+ the smoke test's install command disagreed — the smoke test installed `cloudflare`, the
169
+ README never mentioned it, and nothing compared the two. So the gate proved an install no
170
+ consumer would ever perform.
171
+
172
+ `tests/peers.test.ts` now asserts the manifest, the README and the smoke script agree:
173
+ every declared peer appears in all three, peers are pinned rather than ranged, and none is
174
+ marked optional.
175
+
176
+ ## 0.1.1
177
+
178
+ ### Patch Changes
179
+
180
+ - [#27](https://github.com/taslabs-net/homeflare-kit/pull/27) [`5b73400`](https://github.com/taslabs-net/homeflare-kit/commit/5b73400e7fc2dd8e28a0368db3e5aa105ebdde9a) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Fix the peer contract: 0.1.0 installed cleanly and threw at import.
181
+
182
+ Measured 2026-09-16 against the published 0.1.0 in a clean consumer install — three
183
+ defects, none of which failed at install time:
@@ -0,0 +1,55 @@
1
+ # Alchemy changelog archive 18
2
+
3
+ [Current changelog](../../CHANGELOG.md) · [Archive index](./README.md)
4
+
5
+ 1. **`@effect/platform-node` was a devDependency**, so a consumer got
6
+ `Cannot find module '@effect/platform-node/NodeServices'`. Alchemy's module graph
7
+ reaches `Cloudflare/Workers/WorkerBridge` even when you only import `/proxmox`, so it
8
+ is a required peer, not an optional one.
9
+ 2. **The `effect` peer was ranged** `>=4.0.0-rc.112`, which resolves to rc.115 —
10
+ `TypeError: Config.string is not a function`. Effect's rc line is not
11
+ semver-compatible with itself, so a range is a promise this package cannot keep. Peers
12
+ are pinned exactly now.
13
+ 3. **`@effect/platform-node-shared` still resolves up** to rc.115 against effect rc.112
14
+ (`Cannot find module 'effect/ByteSize'`), because `platform-bun@rc.112` asks for
15
+ `^4.0.0-rc.112`. Only a consumer-side `overrides` block holds the set together, and the
16
+ README now says so with the exact block to paste.
17
+
18
+ ⛔ The smoke script was `echo '…exercised by the consuming stack'` — a check that cannot
19
+ fail, which is how all three shipped. It now packs the tarball, installs it the way the
20
+ README says, and **imports every subpath**, because each of these threw at import rather
21
+ than at install.
22
+
23
+ ## 0.1.0
24
+
25
+ ### Minor Changes
26
+
27
+ - [#25](https://github.com/taslabs-net/homeflare-kit/pull/25) [`309c644`](https://github.com/taslabs-net/homeflare-kit/commit/309c644d83c7149571f4caada135d1b8d141a484) Thanks [@taslabs-net](https://github.com/taslabs-net)! - New package: custom Alchemy providers for five systems the vendor has none for.
28
+
29
+ **`R2BucketLock`** — an R2 bucket's lock rules, declared rather than applied by hand. A
30
+ lock rule is a retention floor: while a rule covers an object, no API call, no lifecycle
31
+ rule and no credential can delete it.
32
+
33
+ ★ Alchemy 2.0.0-beta.77 has no lock property anywhere in its R2 namespace, so the
34
+ alternative was a runbook step a human runs once — and a plan can never show a missing
35
+ runbook step. Covering the gap with a resource makes the drift visible in `plan`.
36
+
37
+ ⛔ Deletion is refused by design: removing a lock removes a retention floor, which is the
38
+ one operation this resource exists to make hard.
39
+
40
+ ⚠️ `alchemy`, `cloudflare` and `effect` are **peers**, not dependencies — Alchemy's
41
+ resource registry and Effect's context both break if two copies load in one process.
42
+
43
+ **Also in this release** — the same treatment for four more systems, 140 files in all:
44
+
45
+ - **`/proxmox`** (61 source files) — ACLs, API tokens, backup jobs, Ceph, SDN, storage.
46
+ - **`/openbao`** (33) — mounts, policies, PKI/SSH/auth roles, Cloudflare role expansion.
47
+ - **`/forgejo`** (12) — branch protection, org secrets, labels, webhooks.
48
+ - **`/talos`** (8) — cluster bootstrap, machine config, health.
49
+
50
+ ⛔ **Import a subpath, never the root.** Each system carries its own client, so a root
51
+ barrel would pull Proxmox into a stack that only wanted Forgejo.
52
+
53
+ ★ **The barrels are deliberately smaller than the directories** — 70 exported symbols out
54
+ of 606 defined, chosen from what a real stack actually consumes. An `export *` would
55
+ publish every helper as API and make the next refactor a breaking change.
@@ -0,0 +1,31 @@
1
+ # PBS jobs on distilled
2
+
3
+ The datastore, prune, sync and verification providers use the typed operations in
4
+ `@distilled.cloud/proxmox-backup`, generated from **proxmox-backup-server 4.2.6-1**.
5
+ `distilled-pbs.ts` supplies the existing leased read/provision credentials to the SDK's
6
+ Credentials service. The SDK owns the `PBSAPIToken` header and request serialization.
7
+
8
+ Existing props and state remain unchanged: retention windows omitted from a declaration
9
+ stay unmanaged, `remove-vanished` still defaults to false, a verification schedule may be
10
+ explicitly null, and a datastore path or backend change is refused. No automatic clearing,
11
+ new deletion flag or live job execution is introduced.
12
+
13
+ Read-only probes on 2026-09-24 found that missing configuration sections answer plain-text
14
+ HTTP 400: `no such datastore`, `no such prune`, `no such sync`, or `no such verification`.
15
+ The SDK patches those specific GET errors as `DatastoreNotFound`, `PruneJobNotFound`,
16
+ `SyncJobNotFound` and `VerifyJobNotFound`. Providers catch those tags only. Permission
17
+ errors, unrelated 400/500 responses, malformed replies and failed transports propagate.
18
+ A failed read can no longer become a speculative create or a successful deletion.
19
+
20
+ Delete first reads the declared item. A typed absent item needs no DELETE. When it exists,
21
+ the provider calls the SDK delete operation; any unclassified error during a concurrent
22
+ removal propagates. The datastore retains its bounded readback and default retain policy;
23
+ `destroy-data` is never sent.
24
+
25
+ The pinned schema declares a null result for datastore create and a UPID for delete. The
26
+ provider verifies the resulting configuration rather than inventing a create task id.
27
+
28
+ The offline lifecycle suite uses `alchemy/Test/Bun` and a strict in-memory PBS HTTP
29
+ fixture. It covers matching adoption without writes, genuine create, drift repair followed
30
+ by a no-op plan, and refused reads. Live adoption remains a separate consumer gate against
31
+ the released package; an offline fixture is not proof of the running estate.
@@ -12,6 +12,11 @@
12
12
 
13
13
  A target that no matcher names receives nothing. Declare both.
14
14
 
15
+ PBS lifecycle calls use `@distilled.cloud/proxmox-backup` (vendor schema PBS 4.2.6-1).
16
+ The runner reuses short-lived OpenBao leases and bounds each request to 20 seconds, with SDK
17
+ retries disabled. A typed `NotFound` alone means absence or an already completed delete.
18
+ Other read failures stop the plan, so a denied read cannot silently plan a create.
19
+
15
20
  ## Paging on a failed verify, sync, prune or garbage collection
16
21
 
17
22
  ```ts
@@ -157,7 +162,8 @@ built-in reverts it to the shipped rule**, and the server reports success. The a
157
162
  | PBS | `Sys.Audit` on `/system/notifications` | `Sys.Modify` on `/system/notifications` |
158
163
  | PVE | `Mapping.Audit` or `Mapping.Modify` on `/mapping/notifications` | `Mapping.Modify` on `/mapping/notifications` |
159
164
 
160
- ⚠️ A missing read privilege looks like "absent". The plan says `create` and the server answers
161
- that the object already exists. Grant the read before changing anything else. On PVE,
165
+ ⚠️ The old shared client hid missing read privileges as "absent", planning a `create` that the
166
+ server rejected as already existing. PBS now preserves the SDK refusal and fails the plan.
167
+ Grant the required read privilege before changing infrastructure. On PVE,
162
168
  `Mapping.Use` is not enough: it opens the matcher list, not the per-matcher read this package
163
169
  makes (`get_matcher` in pve-manager's `Notifications.pm`).
@@ -0,0 +1,28 @@
1
+ # PVE notification SDK migration
2
+
3
+ PVE 9.2.11 notification targets and matchers use named distilled SDK operations
4
+ for every lifecycle call. Their existing declaration forms, comparison rules,
5
+ secret omission and vendor constraint guards remain in place.
6
+
7
+ A missing matcher or target returns HTTP 404, confirmed by read-only probes on
8
+ 2026-09-24. The provider catches the SDK's typed `NotFound` for absent reads and
9
+ idempotent deletes. Authentication, permissions, transport failures and unrelated
10
+ server errors propagate. A malformed successful read fails against the SDK's
11
+ response schema; diagnostics do not retain response contents.
12
+
13
+ SDK request schemas validate the form-name translation before sending a write.
14
+ Arrays use repeated form keys. Commas within matcher rules and webhook property
15
+ strings remain part of each item. An explicitly empty recipient/header list uses
16
+ the vendor's update `delete` field, because an empty SDK array emits no keys.
17
+ Undeclared fields remain unmanaged.
18
+
19
+ A matching adoption sends no write. A changed name replaces the resource; an
20
+ endpoint changing type under the same name replaces delete-first because the
21
+ name is unique across types. Built-in deletion still reverts the vendor default,
22
+ and a gotify create still refuses its missing write-only token: this migration
23
+ does not introduce secrets into persisted props.
24
+
25
+ `notification-distilled.test.ts` exercises create, matching adoption, list clearing,
26
+ identity replacement, malformed/refused reads and subsequent no-op plans through
27
+ the upstream Alchemy harness and real SDK protocol. Live verification follows the
28
+ published release in the consuming stack; fixtures do not prove live writes.
@@ -0,0 +1,60 @@
1
+ # PVE/PBS SDK migration
2
+
3
+ 2026-09-24. Transport completion and live adoption are separate gates. The interim
4
+ SDKs follow [the distilled route](./distilled-interim.md): generated vendor schemas,
5
+ local distilled fixes first, verbatim source copies, and published npm aliases.
6
+
7
+ ## Implemented transport
8
+
9
+ Seventeen existing PVE resource families use named distilled operations throughout:
10
+ ACL, ApiToken, BackupJob, CephDaemon, CephFs, CephOsd, CephPool, Group, MetricServer,
11
+ NodeNetwork, NotificationMatcher, NotificationTarget, Pool, Role, Storage, User and
12
+ ZfsPool. All six existing PBS families now do too: Datastore, NotificationMatcher,
13
+ NotificationTarget, PruneJob, SyncJob and VerifyJob.
14
+
15
+ The latest migration preserves credential leases, safe member failover, bounded
16
+ polling, vendor constraints, write-only secret handling and existing state shapes.
17
+ The newly migrated pool, metric, job and notification providers fold only typed
18
+ missing-resource errors to absence; their other SDK failures propagate.
19
+ Protocol fixtures exercise the generated operations; Alchemy engine tests prove
20
+ matching adoption does not write and a real update settles to a no-op.
21
+
22
+ The SDK patches record PVE 9.2.11 source and PBS 4.2.6-1 schema provenance. Read-only
23
+ probes against PVE 9.2.11 and PBS 4.2.3 distinguish notification HTTP404 responses
24
+ from PBS configuration HTTP400 responses. Resource providers do not inspect status
25
+ codes or message text to make that distinction.
26
+
27
+ ## Consumer gate
28
+
29
+ These families cover the default TB4/HF-Ops/PBS adoption declarations in
30
+ `homeflare-proxmox` at commit `91b31b0`. Its opt-in vault-door Lxc is still a later
31
+ slice. This source migration alone does not establish live adoption.
32
+ After both SDKs and the provider release publish, the consumer pins those exact
33
+ versions, runs its full check and all three adoption verifiers, then compares the
34
+ read and admin plans at stage `live`. A write requires a fresh matching proof.
35
+
36
+ ## Remaining PVE families
37
+
38
+ Twelve existing families still retain some hand-client transport: Lxc, Vm (QEMU),
39
+ NetworkApply, SdnApply, SdnZone, SdnVnet, SdnSubnet, HaResource, HaRule,
40
+ ReplicationJob, FirewallAlias and CephFlag. Their previous measured-none records
41
+ are historical inventories, not a current census or permission to skip migration.
42
+
43
+ - NetworkApply needs the `changes` sibling beside the response's `data`; generic
44
+ unwrapping discards it. Fix the distilled protocol/schema boundary before moving
45
+ this read. The network diff can contain secrets and must not enter state or logs.
46
+ - Lxc task polling must keep the credential that started the task, preserve bounded
47
+ uncertainty after connection loss, and retain its cluster-wide identity checks.
48
+ - SdnApply distinguishes an unavailable subsystem from failed reads and refuses
49
+ undiffable settings. Those semantics need vendor-backed typed SDK errors.
50
+ - Qemu's guest DELETE route differs from its configuration GET/PUT route; copying
51
+ the old generic factory path would keep an existing bug.
52
+
53
+ Complete vendor coverage additionally requires a census of families not yet modeled
54
+ as resources. The 23 migrated families above are a transport milestone, not that
55
+ larger completion claim. No upstream push or pull request was made.
56
+
57
+ Existing NodeNetwork and ZfsPool adoption/reconcile reads still contain broad failure
58
+ folds. Their strict diff reads do not remove that separate conformance gap. CephFs
59
+ task polling also tolerates temporary read failures, but uses a bounded uncertainty
60
+ policy rather than treating a resource as absent.
package/docs/proxmox.md CHANGED
@@ -37,7 +37,13 @@ CephFS index cannot produce a successful delete. Existing adoption fixtures prov
37
37
  matching objects remain no-ops. Credential-denied warnings are still not live no-op
38
38
  evidence; use the consumer's authorized read lane and `hf-adopt-verify --all`.
39
39
 
40
- This is a scoped follow-up to the transport migration. CephFS delete still uses the old
41
- client despite the SDK's now-fixed DELETE query encoding; bounded task polling and
42
- other pre-existing provider refusals retain their own policies. No live writes are part
40
+ This is a scoped follow-up to the transport migration. CephFS delete now uses SDK 0.3.0's fixed
41
+ DELETE query binding and folds only CephFsNotFound. Its existing destructive flags,
42
+ bounded task polling and final index read remain in place. Other provider families
43
+ retain their own policies. No live writes are part
43
44
  of this validation. Released-consumer plans remain a separate gate from these fixtures.
45
+
46
+ `ceph-fs-destroy-sdk.test.ts` verifies that remove-pools/remove-storages are query
47
+ parameters with an empty DELETE body, omitted flags retain the safe defaults, an
48
+ already-missing filesystem succeeds, and unrelated 401/403/500 responses fail.
49
+ The engine destroy fixture still proves the complete read/delete/poll/read-back flow.
@@ -10,6 +10,9 @@ measurements and open findings are preserved below.
10
10
  - 27 `Effect.die` sites in non-test source (defect risk, not a separate S20 ban);
11
11
  - 7 test files on `node:test`;
12
12
  - `Pbs.*` type strings (H14).
13
+ **SDK progress, 2026-09-24:** 17 PVE and all six existing PBS resource families now
14
+ call named distilled operations. The [migration record](./proxmox-sdk-migration.md)
15
+ separates that transport milestone, the published-consumer gate and remaining families.
13
16
  8. **`talos/*`.** Its `talosctl` calls go through `ChildProcessSpawner`, as upstream's Docker
14
17
  provider does, which conforms. Its file handling uses `Bun.file`, `Bun.write`,
15
18
  `Bun.YAML` and `node:fs` (S42), and it uses `Effect.promise` in 3 files (S19). The
@@ -75,8 +78,9 @@ HttpClient` client. The old status-carrying `NetboxError` and its `cause.status
75
78
  12. **Resource JSDoc is not in upstream's generator format.** Zero files use `@resource`,
76
79
  `**Example:**` or `### Section` (S31, H10). **Decision:** maintainer, because it moves
77
80
  the house's glyph rationale into `//` comments.
78
- 13. **Tests never use `alchemy/Test/Bun`.** Every lifecycle is proven against loopback
79
- fakes (S28, H12). Live suites need a place to run, and that is a maintainer decision.
81
+ 13. **Original finding: tests did not use `alchemy/Test/Bun`.** The PVE/PBS SDK migration
82
+ now exercises provider lifecycles through that upstream engine harness with fake HTTP
83
+ responses. It does not claim live write coverage; live suites still need a place to run.
80
84
  14. **`discord/*` is new (2026-09-24, task-authorized, kit PR TBD) and built directly on
81
85
  `@distilled.cloud/discord` — no hand-rolled `client.ts` ever existed to retire.**
82
86
  `Discord.ApplicationCommand` and `Discord.GuildApplicationCommand` call the SDK's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homeflare/alchemy",
3
- "version": "0.37.1",
3
+ "version": "0.37.3",
4
4
  "description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
5
5
  "homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
6
6
  "bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
@@ -124,8 +124,8 @@
124
124
  "@distilled.cloud/netbox": "npm:@homeflare/distilled-netbox@0.3.0",
125
125
  "@distilled.cloud/opnsense": "npm:@homeflare/distilled-opnsense@0.2.0",
126
126
  "@distilled.cloud/paperless-ngx": "npm:@homeflare/distilled-paperless-ngx@0.3.0",
127
- "@distilled.cloud/proxmox": "npm:@homeflare/distilled-proxmox@0.3.0",
128
- "@distilled.cloud/proxmox-backup": "npm:@homeflare/distilled-proxmox-backup@0.3.0",
127
+ "@distilled.cloud/proxmox": "npm:@homeflare/distilled-proxmox@0.3.1",
128
+ "@distilled.cloud/proxmox-backup": "npm:@homeflare/distilled-proxmox-backup@0.3.1",
129
129
  "@distilled.cloud/unifi-network": "npm:@homeflare/distilled-unifi-network@0.2.0"
130
130
  },
131
131
  "peerDependencies": {
@@ -2,10 +2,9 @@
2
2
  * Adopting an object that already matches must be read-only — measured per reconcile SHAPE, through
3
3
  * Alchemy's own Plan and Apply over a fake cluster (fake-pve.ts, ../verify/fake-engine.ts).
4
4
  *
5
- * ★ ONE ROW PER WAY A FAMILY REACHES ITS WRITE, not one per family. Every family built on
6
- * `pveHandlers` shares resource.ts's reconcile, so `Proxmox.Pool` stands for all of them; `Acl`
7
- * wraps that reconcile; `PbsDatastore` and `SdnApply` write their own. CephPool, the one that got
8
- * it wrong, has its own file. docs/adopt-verify.md carries the full per-family table.
5
+ * ★ ONE ROW PER WAY A FAMILY REACHES ITS WRITE, not one per family. Pool now has its own distilled
6
+ * lifecycle; `Acl` wraps resource.ts's shared reconcile; `PbsDatastore` and `SdnApply` write their
7
+ * own. CephPool, the one that got it wrong, has its own file. docs/adopt-verify.md carries the full per-family table.
9
8
  * ⚠️ A FIXTURE THAT DOES NOT MATCH TURNS A ROW INTO A DRIFT TEST, and the assertion on the
10
9
  * verifier's `noop` is what catches that: a wrong fixture fails loudly instead of passing for
11
10
  * the wrong reason. The Pool drift row proves the harness can see a write at all.
@@ -36,7 +35,7 @@ const cases: readonly Case[] = [
36
35
  {
37
36
  declare: () => ProxmoxPool('lab', { comment: 'x', poolid: 'lab', target: FAKE_TARGET }),
38
37
  live: { 'pools/lab': { comment: 'x', members: [] } },
39
- name: 'Proxmox.Pool (pveHandlers: every factory family)',
38
+ name: 'Proxmox.Pool (direct distilled lifecycle)',
40
39
  provider: ProxmoxPoolProvider as never,
41
40
  },
42
41
  {
@@ -0,0 +1,95 @@
1
+ /** Pure existing forms, normalization and equality, shared by plan and SDK lifecycle. */
2
+ import type { BackupJobAttributes, BackupJobProps } from './backup-job.ts';
3
+ import type { PveSpec } from './resource-spec.ts';
4
+ import { bool, guestList, propertyString, text } from './values.ts';
5
+
6
+ /**
7
+ * The form for both create and update.
8
+ *
9
+ * ⛔ A FIELD THE DECLARATION LEAVES OUT IS NOT SENT, AND THAT IS LOAD BEARING. PVE's PUT can only
10
+ * SET a key; clearing one needs `delete=<key>`, which this resource deliberately never sends.
11
+ * `updateForm` is handed the props and nothing else, so a `delete` list could only be "every
12
+ * field you did not declare" — and on a job adopted out of the UI that would silently strip
13
+ * settings this resource does not even model, `compress`, `mailto` and `bwlimit` among them, on
14
+ * the first deploy. Leaving undeclared fields alone is the smaller lie, and `matches` tells the
15
+ * same lie consistently: it does not compare what it cannot change.
16
+ *
17
+ * ⚠️ THE FIVE FIELDS PVE DOCUMENTS A DEFAULT FOR ARE ALWAYS SENT AND ALWAYS COMPARED — `enabled`
18
+ * (1), `all` (0), `mode` (snapshot), `notification-mode` (auto), `repeat-missed` (0). That is
19
+ * what makes a hand-flipped `enabled 0` show up as drift rather than as silence, and it is also
20
+ * what stops those five looping: a field compared against a default must be sent as that
21
+ * default, or every plan reports an update the PUT never performs. The cost is worth reading
22
+ * before the first deploy against an adopted job — an omitted `all` is a declaration that this
23
+ * job backs up NOTHING, and PVE accepts a job that selects nothing without a word. Read that
24
+ * first plan; do not deploy it unseen.
25
+ */
26
+ const shape = (props: BackupJobProps) => ({
27
+ all: props.all === true ? '1' : '0',
28
+ enabled: props.enabled === false ? '0' : '1',
29
+ mode: props.mode ?? 'snapshot',
30
+ 'notification-mode': props['notification-mode'] ?? 'auto',
31
+ 'repeat-missed': props['repeat-missed'] === true ? '1' : '0',
32
+ schedule: props.schedule,
33
+ ...(props.comment === undefined ? {} : { comment: props.comment }),
34
+ ...(props.exclude === undefined ? {} : { exclude: guestList(props.exclude) }),
35
+ ...(props.fleecing === undefined ? {} : { fleecing: props.fleecing }),
36
+ ...(props['notes-template'] === undefined ? {} : { 'notes-template': props['notes-template'] }),
37
+ ...(props.pool === undefined ? {} : { pool: props.pool }),
38
+ ...(props['prune-backups'] === undefined ? {} : { 'prune-backups': props['prune-backups'] }),
39
+ ...(props.storage === undefined ? {} : { storage: props.storage }),
40
+ ...(props.vmid === undefined ? {} : { vmid: guestList(props.vmid) }),
41
+ });
42
+
43
+ export const backupJobSpec = {
44
+ /**
45
+ * ⛔ NO "IS IT REALLY THERE" GUARD, ON PURPOSE. Returning undefined for a job whose JSON is
46
+ * missing some key would be a guess about which keys PVE echoes back, and a wrong guess here
47
+ * does not read as absent-and-harmless: `reconcile` would POST, and POST creates ANOTHER job.
48
+ * Absence is decided by the API declining to answer, which `pveOperations.read` already
49
+ * handles — not by a key being missing from an answer that did arrive.
50
+ */
51
+ attributes: (live, props) => ({
52
+ all: bool(live['all'], false),
53
+ comment: text(live['comment'], ''),
54
+ enabled: bool(live['enabled'], true),
55
+ exclude: guestList(live['exclude']),
56
+ fleecing: propertyString(live['fleecing'], 'enabled'),
57
+ id: props.id,
58
+ mode: text(live['mode'], 'snapshot'),
59
+ 'next-run': typeof live['next-run'] === 'number' ? live['next-run'] : 0,
60
+ 'notes-template': text(live['notes-template'], ''),
61
+ 'notification-mode': text(live['notification-mode'], 'auto'),
62
+ pool: text(live['pool'], ''),
63
+ 'prune-backups': propertyString(live['prune-backups']),
64
+ 'repeat-missed': bool(live['repeat-missed'], false),
65
+ schedule: text(live['schedule'], ''),
66
+ storage: text(live['storage'], ''),
67
+ vmid: guestList(live['vmid']),
68
+ }),
69
+ collection: () => 'cluster/backup',
70
+ /** ⛔ `id` IS SENT AND IS NOT OPTIONAL. Everything above depends on PVE not inventing one. */
71
+ createForm: (props) => ({ ...shape(props), id: props.id }),
72
+ /** Each line reads "not declared, or equal"; the five defaulted fields have no undeclared case. */
73
+ /** The vendor rules these forms are checked against at plan time — resource-spec.ts. */
74
+ endpoint: { create: 'pve:POST /cluster/backup', update: 'pve:PUT /cluster/backup/{id}' },
75
+ matches: (attributes, props) =>
76
+ attributes.schedule === props.schedule &&
77
+ attributes.enabled === (props.enabled !== false) &&
78
+ attributes.all === (props.all === true) &&
79
+ attributes.mode === (props.mode ?? 'snapshot') &&
80
+ attributes['notification-mode'] === (props['notification-mode'] ?? 'auto') &&
81
+ attributes['repeat-missed'] === (props['repeat-missed'] === true) &&
82
+ (props.storage === undefined || attributes.storage === props.storage) &&
83
+ (props.pool === undefined || attributes.pool === props.pool) &&
84
+ (props.comment === undefined || attributes.comment === props.comment) &&
85
+ (props.vmid === undefined || attributes.vmid === guestList(props.vmid)) &&
86
+ (props.exclude === undefined || attributes.exclude === guestList(props.exclude)) &&
87
+ (props['notes-template'] === undefined ||
88
+ attributes['notes-template'] === props['notes-template']) &&
89
+ (props['prune-backups'] === undefined ||
90
+ attributes['prune-backups'] === propertyString(props['prune-backups'])) &&
91
+ (props.fleecing === undefined ||
92
+ attributes.fleecing === propertyString(props.fleecing, 'enabled')),
93
+ path: (props) => `cluster/backup/${props.id}`,
94
+ updateForm: shape,
95
+ } satisfies PveSpec<BackupJobProps, BackupJobAttributes>;