@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,177 @@
1
+ # Alchemy changelog archive 5
2
+
3
+ [Current changelog](../../CHANGELOG.md) · [Archive index](./README.md)
4
+
5
+ `zfs-pool-adopt.test.ts` (the existing coverage — adopt-only noop/adopted, retain-vs-destroy, the
6
+ `ZfsRaidLevel` compile-time refusal) passes unchanged against the migrated code — `fake-pve.ts`'s
7
+ stub is transport-agnostic — plus one new case for finding 2 above (confirmed to reach a live
8
+ `POST` on the fake cluster without the fix, before timing out at the settle-poll). `zfs-pool-read-
9
+ failure.test.ts` (new) pins the cries-wolf fix, the transient-failure propagation, and the
10
+ `alchemy drift` fix, mirroring PR 243's `storage-read-failure.test.ts` — each assertion confirmed
11
+ to fail against the pre-fix code before landing.
12
+
13
+ **Scope note, not fixed by this PR:** `Proxmox.NodeNetwork` and `Proxmox.NetworkApply` (the rest
14
+ of the "ZFS pools + node network" sub-area) are deferred to a follow-up PR — see that PR's own
15
+ description for why they don't travel with this one.
16
+
17
+ **Expected after this releases and the consumer bumps:** no live plan change is expected — no
18
+ `Proxmox.ZfsPool` row was among the false-update rows PR 239/243 already fixed (this family's
19
+ `read`-role lease could always read it), so this PR is a transport migration plus a defensive fix
20
+ for a bug class not yet measured live for this specific family.
21
+
22
+ ## 0.32.0
23
+
24
+ ### Minor Changes
25
+
26
+ - [#236](https://github.com/taslabs-net/homeflare-kit/pull/236) [`3b4a5eb`](https://github.com/taslabs-net/homeflare-kit/commit/3b4a5eb37b1d136d1f899c48bccf50824c13f9bc) Thanks [@taslabs-net](https://github.com/taslabs-net)! - New `@homeflare/alchemy/opnsense` subpath, built on `@distilled.cloud/opnsense` (aliased onto
27
+ `@homeflare/distilled-opnsense@0.2.0`, generated against `opnsense/core`/`opnsense/plugins`
28
+ `26.7.2` — **measured live 2026-09-24** over the HTTPS API only, no SSH: the edge reports
29
+ `26.7.2_2`, one patch ahead of the generation tag, same series; see `docs/opnsense.md`).
30
+ `Opnsense.Firewall.Alias`, `.Category` and `.Group` (interface groups) are **READ-ONLY BY
31
+ DESIGN**: `reconcile` and `delete` always fail with a typed `OpnsenseWriteRefused`, naming the
32
+ policy ("read-only by Tim's rule, 2026-09-24; lifting it is a kit change") — neither handler
33
+ imports an `add`/`set`/`del`/`toggle` operation from the SDK, and `write-refusal.test.ts` proves
34
+ no write is reachable from any handler against a fake that fails on any non-GET request.
35
+
36
+ `read` follows upstream's `Snippet.ts` reference for a marker-less API exactly: a cold match is
37
+ `Unowned`, never a silent adopt, and each resource's convenience constructor
38
+ (`firewallAlias`/`firewallCategory`/`firewallGroup`) pipes `adopt(true)` on by default. All three
39
+ read through `get()` (the whole model tree, keyed by uuid) rather than a per-item endpoint —
40
+ `firewall_alias` has none, and `firewall_category`/`firewall_group`'s own `getCategory`/`getGroup`
41
+ carry no `uuid` in their generated request schema, a measured SDK generation gap this family works
42
+ around uniformly rather than relies on. Each resource also exports a pure declaration renderer
43
+ (`aliasPropsFromLive`/`categoryPropsFromLive`/`groupPropsFromLive`): given one live item, it
44
+ returns exactly the props a declaration needs to plan noop against it — what a later import
45
+ script will use to generate `alchemy.run.ts` rows from a live read.
46
+
47
+ This first import pass covers the three SDK modules that export both a `get` and a `search<Item>`
48
+ operation and are stable configuration (not runtime state): `firewall_alias`, `firewall_category`,
49
+ `firewall_group`. Skipped, and why, in `docs/opnsense.md`: `firewall_filter` (rules) and
50
+ `routing_settings` (gateways) have `get` but no `search*`; `quagga_general` is a settings
51
+ singleton with no uuid; `quagga_bgp` mixes several item types with no `search*` for any of them;
52
+ `quagga_service` is pure runtime control (start/stop/restart/status).
53
+
54
+ Credentials are `OPNSENSE_URL`/`OPNSENSE_API_KEY`/`OPNSENSE_API_SECRET`, read at call time through
55
+ the SDK's own `CredentialsFromEnv` (HTTP Basic, the vendor's own scheme), never a prop — today the
56
+ only key that exists is ROOT-level, one more reason writes stay refused unconditionally. This
57
+ family's own test suite makes no live call — every test runs against `fake-opnsense.ts`, a
58
+ loopback fake; the SDK's `26.7.2` version pin was measured separately, live and read-only
59
+ (`docs/opnsense.md`).
60
+
61
+ - [#238](https://github.com/taslabs-net/homeflare-kit/pull/238) [`d28861f`](https://github.com/taslabs-net/homeflare-kit/commit/d28861f00dce0d9dd17e5857e95332f8d009fa2c) Thanks [@taslabs-net](https://github.com/taslabs-net)! - New `Podman.Container` resource in `@homeflare/alchemy/linux` — one Podman Quadlet `.container`
62
+ file, declared, plus the systemd unit Podman's own generator turns it into. Built for Tim's
63
+ 2026-09-24 decision that CT100's containers (the two Grafanas, shared Postgres, shared Valkey,
64
+ later TeslaMate and NetBox) are owned by Alchemy through Quadlet rather than upstream Alchemy's
65
+ `Docker.Container`, which was measured and rejected: no host-network prop (every container here
66
+ runs `--network host`), `environment` puts `Redacted` secrets in Alchemy state, and systemd
67
+ already supervises the containers.
68
+
69
+ Reuses the `Systemd.Unit` family's lifecycle machinery directly (`renderUnit`, `isUnitRunning`,
70
+ `UNIT_WRITE`, `daemonReload`/`showUnit`/`startUnit`/`stopUnit`/`restartUnit` from `systemctl.ts`,
71
+ which also gains `SourcePath` to `SHOW_PROPERTIES` — additive for `Systemd.Unit`). `[Container]`
72
+ is typed and checked against `podman-systemd.unit(5)`, Podman 5.4 (CT100 runs 5.4.2): `Image`,
73
+ `ContainerName`, `Network` (supports `'host'`), `Volume`, `EnvironmentFile`, `Environment`
74
+ (never a secret — refused by name and value shape if it looks like one, `container-secrets.ts`),
75
+ `PublishPort`, `Exec`, `User`, `AutoUpdate`, `PodmanArgs`; `[Unit]`/`[Service]`/`[Install]` mirror
76
+ `Systemd.Unit`'s verbatim-lines approach with typed conveniences for `Description`/`Documentation`,
77
+ `Restart`/`RestartSec` and `WantedBy`/`RequiredBy`/`Alias` (the only three `[Install]` keys
78
+ Quadlet honours for a `.container` file, doc-confirmed).
79
+
80
+ The lifecycle differs from `Systemd.Unit` in the one place the generator forces it to: there is
81
+ no `enabled` prop and the resource never calls `systemctl enable`/`disable` — MEASURED on CT100
82
+ (`caddy.container`) that a generated unit is transient and doc-confirmed
83
+ (`podman-systemd.unit(5)`) that `systemctl enable` does not work on one; Quadlet applies
84
+ `[Install]` itself at every `daemon-reload`. A generator failure (a `.container` file Quadlet
85
+ refuses) surfaces as a typed `QuadletGeneratorError`, never "absent" — reasoned from
86
+ `systemd.generator(7)` (`daemon-reload` deletes and regenerates ALL generator output): an UPDATE
87
+ that fails verification restores the LAST-KNOWN-GOOD file and reloads again, so a container this
88
+ resource already promised running never loses its systemd unit to a bad update, rather than
89
+ leaving the new file for the next deploy to retry the way `Systemd.Unit` does.
90
+
91
+ `container-fixture.test.ts` renders the same directives as CT100's live `caddy.container`
92
+ (read read-only over `ssh ct100`), and documents why byte-for-byte is not the right bar for a
93
+ typed `[Container]` section (CT100's file interleaves prose comments between directives, which a
94
+ typed prop has nowhere to attach). Full guide: `docs/quadlet-container.md`.
95
+
96
+ Two rounds of review before opening/landing this PR — one foreground Sonnet adversarial pass
97
+ before opening it, aimed at restart-safety and secret leakage, and one inline coordinator
98
+ review after. All three findings were real and are fixed:
99
+
100
+ - **Restart-safety, interrupted UPDATE (high).** `reconcileContainer`'s reload gate (`wrote ||
101
+ preStatus.needDaemonReload`) could not see that the SOURCE `.container` file had already been
102
+ written by an apply that crashed before its `daemon-reload` ran: on retry, the file already held
103
+ the new content (`wrote: false`) and the still-stale GENERATED unit hadn't changed either
104
+ (`needDaemonReload: false`), so no reload ever happened, `settle` restarted the container onto
105
+ its OLD definition, and state recorded the new digest as if it had taken effect — permanently and
106
+ silently. Fixed by gating the reload on `stale` (state's digest disagreeing with what was just
107
+ rendered) too, the same signal `changed` already used to gate the restart.
108
+ - **Restart-safety, interrupted CREATE (high, found on the coordinator's inline re-review).** The
109
+ same bug for a resource with NO prior state: a create whose write landed but crashed before
110
+ `daemon-reload` retried with `wrote: false` and no `stale` to disagree with either, so
111
+ `verifyGenerated` threw on the still-missing generated unit forever — loud, but permanently
112
+ stuck, since nothing about a plain retry ever changed any of those signals. Fixed with
113
+ `needsVerificationReload` (`container-generator.ts`), sharing its predicate with
114
+ `verifyGenerated` itself so the reload gate and the check it is gating can never disagree; an
115
+ ordinary adoption whose generation was already proven fine still skips the extra reload.
116
+ - **Secret leakage (high).** `containerProblems` only ran `secretLikeEnvironment` over
117
+ `container.environment`; `exec`, `podmanArgs` (rendered verbatim) and the `unit.lines`/
118
+ `service.lines` escape hatches (which can spell a raw systemd `Environment=` outside the typed
119
+ map entirely) were unchecked. Fixed with `secretLikeLines` (the same value-shape and embedded
120
+ `KEY=VALUE` heuristics, run over every other rendered line) and an outright refusal of `-e`/
121
+ `--env` in `podmanArgs` (`podmanArgsProblems`), both in container-secrets.ts and wired into
122
+ `containerProblems`.
123
+
124
+ Regression tests for all three live in container-generator.test.ts ("an interrupted apply that
125
+ crashed before daemon-reload", "an interrupted CREATE that crashed before daemon-reload") and
126
+ container-secrets.test.ts (one per affected field); each was confirmed to fail without its fix.
127
+
128
+ - [#237](https://github.com/taslabs-net/homeflare-kit/pull/237) [`1c90063`](https://github.com/taslabs-net/homeflare-kit/commit/1c9006369dd623c522a0a1c0e747582800a4d981) Thanks [@taslabs-net](https://github.com/taslabs-net)! - New `@homeflare/alchemy/unifi` subpath: `Unifi.Network` and `Unifi.FirewallZone`,
129
+ generated from Ubiquiti's own UniFi Network Integration API **10.4.57** (OpenAPI 3.1.0)
130
+ via `@distilled.cloud/unifi-network` (aliased onto `@homeflare/distilled-unifi-network@0.2.0`,
131
+ `docs/distilled-interim.md`).
132
+
133
+ ⛔ **READ-ONLY, by Tim's rule (2026-09-24).** Neither resource has a create, an update body
134
+ or a working delete — `reconcile` and `delete` both fail with a typed `UnifiWriteRefused`
135
+ naming the policy, and no handler ever calls an SDK write operation (proved by fakes in
136
+ `resource.test.ts`, `network.test.ts` and `firewall-zone.test.ts` that record every request
137
+ sent and assert none is anything but `GET`). `read` answers `Unowned` on every match — never
138
+ a silent adopt — with `adopt(true)` piped on by default via the `network`/`firewallZone`
139
+ convenience constructors, so a first deploy against an existing object binds without a stack
140
+ needing `--adopt`. `list` answers `[]`; adoption stays explicit.
141
+
142
+ `declareNetwork(live, siteId)` and `declareFirewallZone(live, siteId)` are the declaration
143
+ renderers this PR ships alongside the resources: pure functions from one live read
144
+ (`getNetworkDetails`/`getFirewallZone`'s own response shape) to the `Props` a declaration
145
+ needs so its plan is a no-op — what a later import script will call to generate
146
+ `alchemy.run.ts` rows from a live site.
147
+
148
+ `Unifi.Network`/`Unifi.FirewallZone` were chosen as the first import set because both have a
149
+ list+get pair over genuine, stable configuration (not runtime state like a connected client,
150
+ a device statistic or a hotspot voucher) and a simple, non-discriminated wire shape. `Site`
151
+ (list-only, no `getSite`) and `Unifi.FirewallPolicy` (several converter-flattened
152
+ discriminator variants, and an ordering endpoint that replaces the whole rule list) are
153
+ deliberately out of scope for this PR — see `docs/unifi.md`.
154
+
155
+ `Unifi.Network`'s `matches` now normalizes the three fields the vendor document never says are
156
+ ordered — `dhcpGuarding.trustedDhcpServerIpAddresses`, `ipv6Configuration.additionalHostIpSubnets`,
157
+ `ipv6Configuration.dnsServerIpAddressesOverride` — before comparing, the same `sortedSet`
158
+ (dedupe + sort) fix `Unifi.FirewallZone` already applied to `networkIds`: alchemy's `deepEqual`
159
+ sorts object keys but not array elements, so an unchanged network whose console answered the same
160
+ set in a different order would otherwise plan a spurious `update` that the read-only reconcile
161
+ then refuses.
162
+
163
+ Auth was measured live, read-only, on 2026-09-24: `X-API-KEY` against the estate's local console
164
+ returns HTTP 200 on `GET /v1/info` and `GET /v1/sites`; the same key against the `api.ui.com`
165
+ cloud connector returns 401 (the wrong door, not a broken header) — see `docs/unifi.md`'s
166
+ "Credentials" section. This PR's own code still never calls the vendor API live.
167
+
168
+ ### Patch Changes
169
+
170
+ - [#243](https://github.com/taslabs-net/homeflare-kit/pull/243) [`823c1a8`](https://github.com/taslabs-net/homeflare-kit/commit/823c1a88fe1f8d34b90acb576231a18b7b04f0c3) Thanks [@taslabs-net](https://github.com/taslabs-net)! - The `proxmox/*` family's `nodes/storage` sub-area (decision 43's serial proxmox walk-down, PR 3
171
+ after `access` in PR 231/239) migrates `Proxmox.Storage` off `client.ts`'s hand-rolled `pve()`
172
+ (`resource.ts`'s `pveHandlers`) onto `@distilled.cloud/proxmox`'s typed `storage.getStorage`/
173
+ `createStorage`/`putStorage`/`deleteStorage` — the first resource of this sub-area; the rest of
174
+ `nodes`/`storage` (ZFS pools, Ceph, node network) follows in later PRs, split out because each is
175
+ independently substantial (see this PR's own description for why). `storage-wire.ts` (the read
176
+ side) and `storage-form.ts` (the write side) hold the pure wire-shape functions, the
177
+ api-token.ts/api-token-form.ts seam.
@@ -0,0 +1,180 @@
1
+ # Alchemy changelog archive 6
2
+
3
+ [Current changelog](../../CHANGELOG.md) · [Archive index](./README.md)
4
+
5
+ **Clears this family's own cries-wolf false updates**, MEASURED live 2026-09-24: on the agent
6
+ (`read`-role) lane, `bun run plan` showed all 5 storages (`cephfs-tb4`, `cephtb4`, `local`,
7
+ `local-zfs`, `pbs`) as `update` with no warning, because this family reads with `provision`
8
+ (`Datastore.Allocate` on `/storage` — `Datastore.Audit` is not enough) and the pre-migration
9
+ `pveOperations.read` folded a REFUSED `provision` mint into "absent" exactly like every family
10
+ before PR 231/239's fix — `unreadable-read.ts`'s own header already named this family as one of
11
+ the two the fix was written for. Now wired the same way: a refused mint reports `noop` with a
12
+ logged warning.
13
+
14
+ **A missing storage is a 500, not a 404** — MEASURED against the live cluster (TB4, admin lane,
15
+ read-only `GET /storage/hf-measure-nonexistent-probe`): `{"message":"storage '...' does not
16
+ exist\n"}` at HTTP 500, the same shape user.ts/group.ts measured for a missing user/group. So
17
+ `storage-wire.ts` carries the same dual-path read PR 239 established for those two families:
18
+ `readStorageOrFail` (non-folding, used by `diff`, so a genuine transient failure propagates and
19
+ fails the plan loudly) and `readStorage` (folding, used by `read`/`reconcile`'s create-detection).
20
+ `read`'s own provider hook also branches on `output` from the start (PR 239's own follow-up
21
+ finding, applied here proactively): `output === undefined` (Plan.ts's adoption probe, Apply.ts's
22
+ delete recovery — nothing confirmed exists yet) keeps the fold; `output !== undefined`
23
+ (`Drift.ts`'s already-confirmed row) uses the non-folding read instead, so `alchemy drift` never
24
+ reports a transient failure as a silent `{action: 'missing'}` either.
25
+
26
+ **Two measured gaps in `@distilled.cloud/proxmox`'s generated schema, handled rather than
27
+ papered over:**
28
+
29
+ - No `maxfiles` field at all (grepped across the whole package). Silently dropping a declared
30
+ value would be the exact class of bug this migration exists to close, so `StorageProps.maxfiles`
31
+ is now `never` — a compile error, the same treatment `StorageLocator`'s `password`/`keyring`/
32
+ `encryption-key` already get — rather than a silent no-op. None of this cluster's 5 live
33
+ storages set it; a consumer that genuinely needs it should file the gap upstream in the SDK's
34
+ generator, not work around it here.
35
+ - Every hyphenated PVE field name becomes an underscore in distilled's generated TypeScript
36
+ (`prune-backups` -> `prune_backups`, `fs-name` -> `fs_name`, and so on for every plugin field
37
+ `StorageLocator`'s free-form bag can carry — `cephfs-tb4`'s own live locator uses `fs-name`).
38
+ No other migrated family has a hyphenated field, so this is new here. `storage-form.ts`'s
39
+ `underscored` translates a form built with PVE's own names into distilled's shape before the
40
+ actual SDK call, never before the form the generated vendor-constraint tables check
41
+ (`guardWrite`, keyed by PVE's own hyphenated names), which would otherwise silently stop
42
+ inspecting the very fields it exists to catch. MEASURED both ways (a `storage-read-failure.test.ts`
43
+ case run once with the translation and once without it): the wire itself carries PVE's own
44
+ hyphenated name either way — distilled's own "unknown key" passthrough happens to re-encode an
45
+ untranslated key correctly for a body-bearing POST/PUT (unlike the GET case that caused PR 239's
46
+ regression, where no body should exist at all). The translation is still the right call: it
47
+ routes every field through distilled's own declared, typed property rather than its passthrough
48
+ fallback, so whatever per-field wire logic a future plugin field carries beyond a plain rename
49
+ runs correctly rather than silently not running.
50
+
51
+ `storage-read-failure.test.ts` pins all of this against real-shaped fixtures (live TB4 response
52
+ bodies, measured 2026-09-24, nothing secret): the cries-wolf fix, `diff`/`verify()` rejecting on a
53
+ transient failure instead of a false `update`, `alchemy drift` rejecting instead of a silent
54
+ `missing`, and — added after an adversarial review found every test above only ever adopted an
55
+ EXISTING storage, never exercising `reconcile`'s actual create path — a brand-new storage's create
56
+ translating a hyphenated locator field for the real SDK call, and a vendor-constraint violation on
57
+ a hyphenated locator field refused before any write. Each assertion confirmed to fail against the
58
+ pre-fix code (or, for the translation claim, against the untranslated code) before landing.
59
+ `verify/fake-engine.ts` gained a `drift` capability (driving the real `Alchemy.Drift.detect`) in
60
+ PR 239, reused here; that PR's `read-failure-propagation.test.ts` already established the
61
+ transient-failure/drift test pattern for User/Group.
62
+
63
+ **Expected after this releases and the consumer bumps:** on the agent lane, `bun run plan`'s
64
+ 5 storage rows return to `noop`; ACLs and users/groups (PR 231/239) stay unaffected.
65
+
66
+ ## 0.31.2
67
+
68
+ ### Patch Changes
69
+
70
+ - [#239](https://github.com/taslabs-net/homeflare-kit/pull/239) [`1e8985f`](https://github.com/taslabs-net/homeflare-kit/commit/1e8985fc8359304b2c6d03df0fba00781e5e32a0) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Fixes a live regression from kit 0.31.1 (PR 231, the `proxmox/access`
71
+ distilled migration): on the agent (`read`-role) lane, `bun run plan` showed
72
+ every `Proxmox.User`/`Proxmox.Group` row as `update` with no warning — a
73
+ silent false diff, not the loud "read was refused" case that migration was
74
+ meant to fix.
75
+
76
+ **Root cause, measured against the live cluster.** `user.ts`/`group.ts`
77
+ called `access.getAccessUser(props)`/`access.getAccessGroup(props)` with the
78
+ whole declared props object, not just the schema's own `{userid}`/
79
+ `{groupid}` label field. distilled's `buildRequest` treats any OTHER key on
80
+ that object (`target`, `comment`, every other prop this family carries) as
81
+ an "unknown key" and JSON-encodes it onto the request as a body — on what
82
+ must stay a bodyless GET. A permissive client tolerates this; Node/Bun's own
83
+ `fetch` refuses it outright
84
+ (`TypeError [ERR_INVALID_ARG_VALUE]: fetch() request with GET/HEAD method
85
+ cannot have body`), which `runPveWith` retried across every cluster member
86
+ and exhausted identically (`PveClusterExhausted`). The old
87
+ `Effect.orElseSucceed(() => undefined)` then folded that failure into
88
+ "absent", forcing a false `update` on every account and every group — the
89
+ same cries-wolf bug PR 231 had just fixed for a refused credential, now
90
+ triggered by a genuine transport failure instead. Fixed by calling both
91
+ operations with only their declared label field.
92
+
93
+ **The fold itself was too broad, independent of the request-shape bug.**
94
+ `orElseSucceed` turning ANY read failure into "absent" is unsound at `diff`
95
+ time: only a genuine not-found may mean absent, every other failure must
96
+ fail the plan loudly rather than silently forcing a write. `acl.ts` (already
97
+ migrated pre-231) and `role.ts` never needed a fold at all — an ACL/role's
98
+ absence is always a successful list read that just doesn't contain the row,
99
+ so any thrown failure was already a bug; the fold there is now removed
100
+ outright. `api-token.ts`'s absence signal is likewise a measured
101
+ success-path check (`expire`/`privsep` both undefined on a 200), so its
102
+ fold is also removed outright.
103
+
104
+ `user.ts`/`group.ts` are different: a genuinely missing user or group is a
105
+ thrown 500 on this cluster (`"no such user"`/`"no such group"`, not a clean
106
+ 404), MEASURED live — so `reconcile`'s create workflow still needs "absent"
107
+ derived from exactly that failure, or a brand-new declaration could never be
108
+ created. Each now has a dual-path read in its `*-wire.ts` sibling: a
109
+ non-folding `read...OrFail` (used by `diff`, so a genuine transient failure
110
+ propagates and fails the plan loudly) and a folding wrapper kept only for
111
+ `read`/`reconcile` (where folding costs at most a redundant, loudly-refused
112
+ create, never a silent wrong write).
113
+
114
+ **A second, deeper instance of the same bug class, found by adversarial
115
+ review of this fix rather than measured live.** The engine calls a
116
+ provider's `read` hook from four places — an adoption probe and
117
+ interrupted-create recovery in `Plan.ts`, delete recovery in `Apply.ts`, and
118
+ `Drift.ts` (`alchemy drift`/`sync`/`deploy --detect-drift`) — and only the
119
+ first three are "nothing confirmed exists yet" cases where `read`'s own
120
+ fold is safe. `Drift.ts` calls `read` on an ALREADY-CONFIRMED row (its own
121
+ persisted `output`), the same situation `diff` handles, and a folded
122
+ failure there is reported as `{action: 'missing'}` with no error anywhere —
123
+ worse than the original bug, since nothing even logs a warning. Fixed by
124
+ having `user.ts`/`group.ts`'s `read` hook branch on the input's own
125
+ `output` field (documented on `Provider.read`'s own type as "current state
126
+ -> synced state"): `output === undefined` still uses the folding read
127
+ (unchanged, for the three recovery/adoption cases), `output !== undefined`
128
+ now uses the same non-folding `read...OrFail` `diff` already calls.
129
+
130
+ `read-failure-propagation.test.ts` pins all three fixes end to end against
131
+ real-shaped PVE fixtures (live response bodies, nothing secret): a GET that
132
+ would carry a body now fails the assertion outright; a transient 500 on an
133
+ already-adopted `Proxmox.User`/`Proxmox.Group` now rejects `verify()`
134
+ instead of resolving with a false `update`; and (a new `drift` capability
135
+ on the shared `fake-engine.ts` test harness, driving the real
136
+ `Alchemy.Drift.detect`) the same transient failure now rejects `alchemy
137
+ drift` instead of silently reporting the resource missing — each of the
138
+ three new/changed tests was confirmed to fail against the pre-fix code
139
+ before this change landed.
140
+
141
+ **Expected after this releases and the consumer bumps:** on the agent
142
+ lane, `bun run plan`'s users/groups return to `noop`, ACLs stay
143
+ `noop`-with-warning, and the five pre-existing storage rows (a separate,
144
+ not-yet-migrated family) are unaffected.
145
+
146
+ - [#234](https://github.com/taslabs-net/homeflare-kit/pull/234) [`3357caa`](https://github.com/taslabs-net/homeflare-kit/commit/3357caa7c9a412365d3959bbb306628329eba4f8) Thanks [@taslabs-net](https://github.com/taslabs-net)! - `@distilled.cloud/grafana` is not published upstream yet, so this package
147
+ now aliases it onto `@homeflare/distilled-grafana` (0.1.0 pre-release,
148
+ built the distilled way and shipped from this monorepo — see
149
+ `docs/distilled-interim.md`) as a plain `dependencies` entry instead of a
150
+ `1.0.0-rc.12` peer — nothing changes for a consumer's install (the peer
151
+ line is simply gone from the README and the smoke install, the same way
152
+ `/netbox` and `/litellm` already read). `Grafana.Datasource`'s props,
153
+ attributes and generated calls are unchanged — its existing tests pass
154
+ unmodified.
155
+
156
+ The alias also picks up `@distilled.cloud/grafana`'s newly-added folder,
157
+ dashboard and alerting-provisioning operations (see the
158
+ `@homeflare/distilled-grafana` changeset), which fixes the SDK-level gap
159
+ `docs/grafana.md` and `docs/upstream-conformance.md` recorded. No new house
160
+ `Resource` is added here — `Grafana.Folder`/`Dashboard`/`AlertRule`/etc. on
161
+ top of these operations is follow-up work, not part of this change.
162
+
163
+ ⚠️ **Deliberate deviation from `docs/distilled-interim.md`'s step 5.** That
164
+ doc has the interim package publish and get confirmed live
165
+ (`npm view @homeflare/distilled-grafana version`) in its own PR _before_ a
166
+ second PR adds the alias — exactly to dodge the propagation window
167
+ `scripts/publish.ts`'s own comments document twice (2026-09-15,
168
+ `kit@0.1.1`/`cloudflare@0.1.1`: "Your package is being processed and may
169
+ take a few minutes to become available"). This PR does both at once, on
170
+ purpose, so the next teams building `Grafana.Folder`/`Dashboard`/`AlertRule`
171
+ aren't blocked on a second release cycle. `scripts/publish.ts` has no
172
+ dependency-aware ordering, so **after this releases, confirm
173
+ `npm view @homeflare/distilled-grafana version` resolves before anyone
174
+ depends on the new `@homeflare/alchemy` version** — a few minutes' wait,
175
+ not a code change, and self-healing either way (the publish script is
176
+ idempotent and a stuck install just needs a retry).
177
+
178
+ ## 0.31.1
179
+
180
+ ### Patch Changes
@@ -0,0 +1,180 @@
1
+ # Alchemy changelog archive 7
2
+
3
+ [Current changelog](../../CHANGELOG.md) · [Archive index](./README.md)
4
+
5
+ - [#231](https://github.com/taslabs-net/homeflare-kit/pull/231) [`11705f2`](https://github.com/taslabs-net/homeflare-kit/commit/11705f2b88d19335be5a115e06f7f6ca883b63b3) Thanks [@taslabs-net](https://github.com/taslabs-net)! - The `proxmox/*` family's `access` sub-area (decision 43's serial proxmox
6
+ walk-down, PR 2 after `Proxmox.Acl` in PR 209/220) now calls
7
+ `@distilled.cloud/proxmox`'s typed `access.*` operations instead of
8
+ `client.ts`'s hand-rolled, generic `pve()` call: `Proxmox.User`, `Proxmox.Group`,
9
+ `Proxmox.Role` and `Proxmox.ApiToken`. Each gets its own hand-written
10
+ reconcile (read/diff/reconcile/delete), the way `acl.ts` established, rather
11
+ than the shared `pveHandlers`/`pveOperations` factory — a `*-wire.ts` sibling
12
+ per family holds the pure wire-shape functions (create/update forms,
13
+ attributes, `matches`), keeping every file under the house's 250-line cap.
14
+ `client.ts`/`pveOperations` are untouched and still serve every other PVE
15
+ family; nothing in `nodes`, `storage`, `notifications` or PBS has moved yet.
16
+
17
+ `Proxmox.Role` reads via `GET /access/roles` (the list), not the item
18
+ `GET /access/roles/{roleid}` this family used before: distilled's generated
19
+ schema for the item response enumerates a FIXED set of ~47 known privilege
20
+ field names, and a privilege outside that set would silently vanish from
21
+ state on every read. The list's `privs` field is the same plain comma string
22
+ PVE's item read also disagreed with the index about pre-migration, with no
23
+ fixed enumeration to fall behind — `role-wire.ts`'s `find` does the item
24
+ read's old client-side job. Every other family in this PR still reads the
25
+ item it read before.
26
+
27
+ A `distilled-guard.ts` module (`asForm`/`guardWrite`) is the generalized
28
+ form of `acl.ts`'s own local `guardWrite`, needed because distilled's
29
+ generated request interfaces have no index signature and TypeScript refuses
30
+ `Record<string, string | undefined>` for them directly — `body: object`
31
+ plus one internal cast is the fix, applied once here rather than at every
32
+ call site.
33
+
34
+ **The cries-wolf fix** (measured 2026-09-24: `bun run plan` on the agent lane
35
+ showed "19 to update" — 14 ACLs and 5 storages — because a REFUSED read
36
+ folded into "absent" and `diff` forced `update` without comparing a field).
37
+ `credentials.ts`'s `mint` now fails a 403 from OpenBao (a denied
38
+ role — "this identity's AppRole has no grant") with a new typed
39
+ `PveCredentialDenied` (`credential-errors.ts`), split out from the newly
40
+ `mint.ts`-housed `mint`/`authorization` to keep `credentials.ts` under the
41
+ line cap. A new `unreadable-read.ts` module's `readOrUnreadable` `catchTag`s
42
+ exactly that one tag — never any other read failure — turning it into an
43
+ `UNREADABLE` sentinel `diff` can tell apart from a genuine absence. Upstream's
44
+ `Diff` type is exactly `NoopDiff | UpdateDiff | ReplaceDiff`
45
+ (`packages/alchemy/src/Diff.ts@v2.0.0-beta.79`) with no fourth action, and
46
+ `Plan.ts`'s resource pass fails the WHOLE plan if even one resource's `diff`
47
+ throws — so `diff` now reports `{action: 'noop'}` plus a logged warning for
48
+ an unreadable row, never a failed plan and never a forced write. Wired into
49
+ `acl.ts` (the already-migrated family) and every family in this PR;
50
+ `unreadable-read.test.ts` proves it end to end through Alchemy's real Plan
51
+ and Apply, with a fake OpenBao that denies `provision` mid-test. `storage.ts`
52
+ and the other five false updates stay open until `nodes`/`storage` migrates.
53
+
54
+ Distilled's `{userid}`/`{tokenid}` label substitution percent-encodes them
55
+ (`iac@pve` -> `iac%40pve`), where `client.ts`'s plain string concatenation
56
+ sent the `@` literally — a real PVE decodes both the same way, so this is
57
+ not a behaviour change a stack observes, but every fake cluster with an `@`
58
+ in a userid (`user.test.ts`, `api-token-adopt.test.ts`,
59
+ `provision-declare.test.ts`) now decodes the path before matching it.
60
+
61
+ State did not move: every Props/Attributes interface is unchanged, and the
62
+ existing cross-family `adopt-noop.test.ts` cases (now extended with `Group`
63
+ and `User` rows for the new hand-written reconciles) and
64
+ `api-token-adopt.test.ts` pass with the same assertions the pre-migration
65
+ code made. New tests (`group.test.ts`, `user.test.ts`, `role.test.ts`)
66
+ drive a fake PVE server through the real distilled protocol, covering
67
+ create, drift-correction, delete, retain-by-default and the read-back guard.
68
+
69
+ `codegen/constraints.ts` regenerated: `Proxmox.ApiToken` no longer names its
70
+ (always-unreachable) create endpoint anywhere in source, so the generated
71
+ tables and the hand-written ownership ledger
72
+ (`scripts/proxmox-ownership-pve.ts`) both drop that one claim — 74 tabled
73
+ endpoints, not 75.
74
+
75
+ ## 0.31.0
76
+
77
+ ### Minor Changes
78
+
79
+ - [#228](https://github.com/taslabs-net/homeflare-kit/pull/228) [`25610e8`](https://github.com/taslabs-net/homeflare-kit/commit/25610e890ac360456c318dd8dbefd0d54ccf0d3e) Thanks [@taslabs-net](https://github.com/taslabs-net)! - New `@homeflare/alchemy/discord` subpath, built directly on `@distilled.cloud/discord`
80
+ (1.0.0-rc.12) — no hand-rolled client ever existed for this vendor to retire.
81
+ `Discord.ApplicationCommand` (global) and `Discord.GuildApplicationCommand` (guild-scoped)
82
+ declare a slash/user/message command; `reconcile` is one upsert call, matching Discord's own
83
+ create-endpoint semantics (a command with the same name overwrites the old one), the same shape
84
+ `Cloudflare.Snippets.Snippet`'s `putSnippet` uses upstream. `read` follows that same upstream
85
+ reference for a marker-less API exactly: a cold match is `Unowned`, never a silent adopt, and
86
+ both convenience constructors (`applicationCommand`, `guildApplicationCommand`) pipe `adopt(true)`
87
+ by default. Rate limits are handled entirely by the SDK's own bounded default retry policy
88
+ (`Schedule.recurs(8)`, honoring a `429`'s `Retry-After`); this family adds no retry logic of its
89
+ own. Credentials are `DISCORD_BOT_TOKEN`/`DISCORD_TOKEN`, read at call time through the SDK's own
90
+ `CredentialsFromEnv`, never a prop.
91
+
92
+ Walked read-only against the live target, `hf-discord-halibut.service` on CT100: the bot
93
+ self-registers nine guild-scoped commands at every startup (Sapphire's `BulkOverwrite`), so
94
+ `docs/discord.md` documents the ownership conflict and the handover sequence rather than
95
+ declaring `Discord.GuildApplicationCommand` against Halibut's own guilds — that would fight the
96
+ bot's own registration, not replace it. Guild roles, channels and webhooks are not built: Halibut
97
+ uses none of them (measured on CT100), and the engine in `resource.ts` generalizes cleanly to a
98
+ future spec file if a stack ever needs one.
99
+
100
+ SDK gaps recorded in `docs/discord.md`: every generated operation's typed error union is narrower
101
+ than the protocol's own status map (`Forbidden`/`NotFound`/`BadRequest`/`Conflict` are built at
102
+ runtime but not in the exported type — measured via `tsc`, not assumed); `options` is passed
103
+ through opaquely rather than re-modeled from the ~40-type generated union; there is no vendor
104
+ constraint table (unlike NetBox/Paperless); global command propagation (up to an hour, Discord's
105
+ own docs) is not polled.
106
+
107
+ ## 0.30.0
108
+
109
+ ### Minor Changes
110
+
111
+ - [#229](https://github.com/taslabs-net/homeflare-kit/pull/229) [`b4d714e`](https://github.com/taslabs-net/homeflare-kit/commit/b4d714eff527c8c875afca2cb529d5f312bd1efa) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Added `caddy/formatCaddyfile(text)`: pipes a Caddyfile through the LOCAL
112
+ `caddy fmt -` binary (stdin in, formatted text out) — the same formatter the
113
+ `caddy fmt` CLI command runs. There is no admin API endpoint for this
114
+ (`cmd/commandfuncs.go` is CLI-only), so it shells out with
115
+ `ChildProcessSpawner` (S19) rather than going through `@distilled.cloud/caddy`,
116
+ and it never reimplements the formatter in TypeScript: a missing or failing
117
+ binary fails the Effect with a typed `CaddyFmtNotFound` / `CaddyFmtFailed`,
118
+ it never silently returns the input unformatted.
119
+
120
+ Also: `Caddy.Config`'s plan (`diffConfig`) and deploy (`CaddyConfigProvider`'s
121
+ `reconcile`) now surface the adapter's "Caddyfile input is not formatted"
122
+ warning as its own clear line naming the fix, separately from any other
123
+ adapter warnings — it used to be silent at plan time, and just another line
124
+ in the pile at deploy time. Nothing here changes what gets loaded onto Caddy:
125
+ formatting should never change the adapted JSON digest.ts compares (reasoned
126
+ from the Caddyfile grammar; not measured against a real Caddy in this
127
+ package — see docs/caddy-fmt.md).
128
+
129
+ - [#226](https://github.com/taslabs-net/homeflare-kit/pull/226) [`bfc71a3`](https://github.com/taslabs-net/homeflare-kit/commit/bfc71a31e811aab0ccdbbc5ce82ee6803c7ad277) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Add the Google Workspace provider family (`@homeflare/alchemy/google-workspace`): `Group`,
130
+ `GroupMember`, `DomainAlias` and `OrgUnit` over the Admin SDK Directory API, built on
131
+ `@distilled.cloud/google-workspace@1.0.0-rc.12`'s typed `admin_directory_v1` operations
132
+ (S23 — no hand-rolled client). Adopt-first by get-by-key, `retain` on removal for everything
133
+ but membership, and no `User` resource (Google's own `User` schema carries a `password` field).
134
+ Credential setup — domain-wide delegation, the least OAuth scopes each resource needs, and
135
+ where the service-account key lives in OpenBao — is in
136
+ `packages/alchemy/docs/google-workspace.md`.
137
+
138
+ - [#225](https://github.com/taslabs-net/homeflare-kit/pull/225) [`a68c021`](https://github.com/taslabs-net/homeflare-kit/commit/a68c0212f951d3492e5fde981f7f334f777d21fe) Thanks [@taslabs-net](https://github.com/taslabs-net)! - New family: `@homeflare/alchemy/grafana`. `Grafana.Datasource` declares one data source, keyed by
139
+ `uid`, calling `@distilled.cloud/grafana`'s typed `addDataSource`/`getDataSourceByUID`/
140
+ `updateDataSourceByUID`/`deleteDataSourceByUID` operations, `catchTag('NotFound', ...)` in place of
141
+ a status check. Credentials are a `GrafanaTarget` (instance origin + a token env var NAME, never a
142
+ literal value) resolved lazily per call, parameterized per instance rather than fixed like
143
+ Forgejo's — this estate runs more than one Grafana. `grafanaProviders(target)` composes the
144
+ provider with its credentials, mirroring `litellmProviders`.
145
+
146
+ Only `Datasource` ships: `@distilled.cloud/grafana@1.0.0-rc.12` has no create/update/delete
147
+ operations for folders, dashboards, alert rules or contact points, measured against its published
148
+ types — recorded in `docs/grafana.md` and `docs/upstream-conformance.md` rather than worked
149
+ around with a hand-rolled client for the missing pieces.
150
+
151
+ Built and measured, read-only, against the live target `teslamate-grafana.service` on CT100
152
+ (Grafana 13.1.3, `teslamate/grafana:4.2.0`): one file-provisioned datasource, image-baked
153
+ dashboards, no folders, alert rules or contact points. No stack yet imports this subpath.
154
+
155
+ ## 0.29.1
156
+
157
+ ### Patch Changes
158
+
159
+ - [#223](https://github.com/taslabs-net/homeflare-kit/pull/223) [`dc37600`](https://github.com/taslabs-net/homeflare-kit/commit/dc37600d0bb1c72ed2711351e498949af81271c6) Thanks [@taslabs-net](https://github.com/taslabs-net)! - `declareRepoBaseline` and `declareRepoPolicy` declared `GitHub.Repository` and their
160
+ `RepositoryRuleset` as two independent resources with no dependency edge between them (K5,
161
+ kit PR 216). `repoBaselineSettings`/`repoPolicy` already gate `allowAutoMerge` on
162
+ `checks.length > 0`, but with nothing ordering the two resources, a deploy that moves a repo
163
+ from `checks: []` to a non-empty list could apply the repository (turning auto-merge on) before
164
+ the ruleset (adding the required check) — or the ruleset apply could fail outright, leaving
165
+ auto-merge on with nothing required. `gh pr merge --auto` merges a CLEAN pull request the instant
166
+ nothing is outstanding, so that window is exactly the fail-open state K5 exists to prevent.
167
+
168
+ Both callers now thread the ruleset's `rulesetId` through `allowAutoMerge`'s own value
169
+ (`repo-auto-merge-gate.ts`'s `gateAutoMergeOnRuleset`, via `alchemy/Output`'s `map`) whenever a
170
+ declaration turns auto-merge on, instead of writing it as a literal. Alchemy orders resources by
171
+ Output references in props (`Plan.ts`'s `Output.upstreamAny`, `Apply.ts`'s `waitForDeps` —
172
+ see the kit's own `alchemy-output-refs-order-resources` memory), so this makes the engine apply
173
+ the ruleset first, with no change to the value actually sent (`allowAutoMerge` is still exactly
174
+ `true`). A ruleset apply failure now leaves the repository's `reconcile` — and `allowAutoMerge`
175
+ — untouched, proved by a new engine-level ordering test and failure test for each caller.
176
+
177
+ The reverse transition (checks/approvals removed) needs no matching edge: neither caller ever
178
+ declares a rule's removal explicitly, only omits it, and `repository-ruleset-guards.ts`'s
179
+ `requiredChecksOmissionRefusal`/`undeclaredLiveRuleRefusal` already refuse — regardless of apply
180
+ order — to drop a still-live required rule by omission.