@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.
- package/dist/proxmox/backup-job-config.d.ts +95 -0
- package/dist/proxmox/backup-job-config.d.ts.map +1 -0
- package/dist/proxmox/backup-job-distilled.d.ts +46 -0
- package/dist/proxmox/backup-job-distilled.d.ts.map +1 -0
- package/dist/proxmox/backup-job.d.ts +9 -26
- package/dist/proxmox/backup-job.d.ts.map +1 -1
- package/dist/proxmox/ceph-fs-distilled.d.ts +17 -10
- package/dist/proxmox/ceph-fs-distilled.d.ts.map +1 -1
- package/dist/proxmox/ceph-fs-wire.d.ts +5 -22
- package/dist/proxmox/ceph-fs-wire.d.ts.map +1 -1
- package/dist/proxmox/ceph-fs.d.ts +3 -3
- package/dist/proxmox/distilled-pbs.d.ts +12 -0
- package/dist/proxmox/distilled-pbs.d.ts.map +1 -0
- package/dist/proxmox/fake-pbs-jobs.d.ts +23 -0
- package/dist/proxmox/fake-pbs-jobs.d.ts.map +1 -0
- package/dist/proxmox/fake-pbs-notify.d.ts.map +1 -1
- package/dist/proxmox/fake-pve.d.ts +1 -1
- package/dist/proxmox/fake-pve.d.ts.map +1 -1
- package/dist/proxmox/group-wire.d.ts +2 -2
- package/dist/proxmox/group-wire.d.ts.map +1 -1
- package/dist/proxmox/index.js +1382 -506
- package/dist/proxmox/index.js.map +51 -27
- package/dist/proxmox/live-core-fixtures.d.ts +40 -0
- package/dist/proxmox/live-core-fixtures.d.ts.map +1 -0
- package/dist/proxmox/metric-server-config.d.ts +49 -0
- package/dist/proxmox/metric-server-config.d.ts.map +1 -0
- package/dist/proxmox/metric-server-distilled.d.ts +43 -0
- package/dist/proxmox/metric-server-distilled.d.ts.map +1 -0
- package/dist/proxmox/metric-server.d.ts +2 -10
- package/dist/proxmox/metric-server.d.ts.map +1 -1
- package/dist/proxmox/node-network-wire.d.ts +1 -1
- package/dist/proxmox/node-network-wire.d.ts.map +1 -1
- package/dist/proxmox/notification-matcher-distilled.d.ts +9 -0
- package/dist/proxmox/notification-matcher-distilled.d.ts.map +1 -0
- package/dist/proxmox/notification-matcher-lifecycle.d.ts +27 -0
- package/dist/proxmox/notification-matcher-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/notification-matcher.d.ts +5 -5
- package/dist/proxmox/notification-matcher.d.ts.map +1 -1
- package/dist/proxmox/notification-sdk-input.d.ts +22 -0
- package/dist/proxmox/notification-sdk-input.d.ts.map +1 -0
- package/dist/proxmox/notification-target-distilled.d.ts +12 -0
- package/dist/proxmox/notification-target-distilled.d.ts.map +1 -0
- package/dist/proxmox/notification-target-form.d.ts +2 -0
- package/dist/proxmox/notification-target-form.d.ts.map +1 -1
- package/dist/proxmox/notification-target-lifecycle.d.ts +29 -0
- package/dist/proxmox/notification-target-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/notification-target-spec.d.ts +5 -0
- package/dist/proxmox/notification-target-spec.d.ts.map +1 -0
- package/dist/proxmox/notification-target.d.ts +4 -5
- package/dist/proxmox/notification-target.d.ts.map +1 -1
- package/dist/proxmox/pbs-datastore-endpoint.d.ts +1 -1
- package/dist/proxmox/pbs-datastore-form.d.ts +18 -42
- package/dist/proxmox/pbs-datastore-form.d.ts.map +1 -1
- package/dist/proxmox/pbs-datastore-guard.d.ts +7 -4
- package/dist/proxmox/pbs-datastore-guard.d.ts.map +1 -1
- package/dist/proxmox/pbs-datastore-lifecycle.d.ts +56 -0
- package/dist/proxmox/pbs-datastore-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/pbs-datastore-wire.d.ts +21 -0
- package/dist/proxmox/pbs-datastore-wire.d.ts.map +1 -0
- package/dist/proxmox/pbs-datastore.d.ts +5 -6
- package/dist/proxmox/pbs-datastore.d.ts.map +1 -1
- package/dist/proxmox/pbs-notification-input.d.ts +18 -0
- package/dist/proxmox/pbs-notification-input.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-matcher-distilled.d.ts +11 -0
- package/dist/proxmox/pbs-notification-matcher-distilled.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-matcher.d.ts +4 -3
- package/dist/proxmox/pbs-notification-matcher.d.ts.map +1 -1
- package/dist/proxmox/pbs-notification-target-distilled.d.ts +20 -0
- package/dist/proxmox/pbs-notification-target-distilled.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-target-lifecycle.d.ts +4 -4
- package/dist/proxmox/pbs-notification-target-lifecycle.d.ts.map +1 -1
- package/dist/proxmox/pbs-notification-target.d.ts +1 -1
- package/dist/proxmox/pbs-prune-job-lifecycle.d.ts +22 -0
- package/dist/proxmox/pbs-prune-job-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/pbs-prune-job-spec.d.ts +5 -0
- package/dist/proxmox/pbs-prune-job-spec.d.ts.map +1 -0
- package/dist/proxmox/pbs-prune-job-wire.d.ts +11 -0
- package/dist/proxmox/pbs-prune-job-wire.d.ts.map +1 -0
- package/dist/proxmox/pbs-prune-job.d.ts +9 -8
- package/dist/proxmox/pbs-prune-job.d.ts.map +1 -1
- package/dist/proxmox/pbs-sync-job-attributes.d.ts +34 -0
- package/dist/proxmox/pbs-sync-job-attributes.d.ts.map +1 -0
- package/dist/proxmox/pbs-sync-job-form.d.ts +21 -22
- package/dist/proxmox/pbs-sync-job-form.d.ts.map +1 -1
- package/dist/proxmox/pbs-sync-job-lifecycle.d.ts +22 -0
- package/dist/proxmox/pbs-sync-job-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/pbs-sync-job-wire.d.ts +11 -0
- package/dist/proxmox/pbs-sync-job-wire.d.ts.map +1 -0
- package/dist/proxmox/pbs-sync-job.d.ts +9 -39
- package/dist/proxmox/pbs-sync-job.d.ts.map +1 -1
- package/dist/proxmox/pbs-verify-job-lifecycle.d.ts +22 -0
- package/dist/proxmox/pbs-verify-job-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/pbs-verify-job-spec.d.ts +16 -0
- package/dist/proxmox/pbs-verify-job-spec.d.ts.map +1 -0
- package/dist/proxmox/pbs-verify-job-wire.d.ts +11 -0
- package/dist/proxmox/pbs-verify-job-wire.d.ts.map +1 -0
- package/dist/proxmox/pbs-verify-job.d.ts +12 -12
- package/dist/proxmox/pbs-verify-job.d.ts.map +1 -1
- package/dist/proxmox/pool.d.ts +10 -1
- package/dist/proxmox/pool.d.ts.map +1 -1
- package/dist/proxmox/storage-wire.d.ts +2 -2
- package/dist/proxmox/storage-wire.d.ts.map +1 -1
- package/dist/proxmox/user-wire.d.ts +2 -2
- package/dist/proxmox/user-wire.d.ts.map +1 -1
- package/dist/proxmox/zfs-pool-wire.d.ts +1 -1
- package/dist/proxmox/zfs-pool-wire.d.ts.map +1 -1
- package/docs/changelog/README.md +22 -0
- package/docs/changelog/archive-01.md +176 -0
- package/docs/changelog/archive-02.md +180 -0
- package/docs/changelog/archive-03.md +179 -0
- package/docs/changelog/archive-04.md +178 -0
- package/docs/changelog/archive-05.md +177 -0
- package/docs/changelog/archive-06.md +180 -0
- package/docs/changelog/archive-07.md +180 -0
- package/docs/changelog/archive-08.md +178 -0
- package/docs/changelog/archive-09.md +181 -0
- package/docs/changelog/archive-10.md +182 -0
- package/docs/changelog/archive-11.md +182 -0
- package/docs/changelog/archive-12.md +183 -0
- package/docs/changelog/archive-13.md +180 -0
- package/docs/changelog/archive-14.md +178 -0
- package/docs/changelog/archive-15.md +181 -0
- package/docs/changelog/archive-16.md +182 -0
- package/docs/changelog/archive-17.md +183 -0
- package/docs/changelog/archive-18.md +55 -0
- package/docs/pbs-distilled-jobs.md +31 -0
- package/docs/pbs-notifications.md +8 -2
- package/docs/proxmox-notification-sdk.md +28 -0
- package/docs/proxmox-sdk-migration.md +60 -0
- package/docs/proxmox.md +9 -3
- package/docs/upstream-conformance-families.md +6 -2
- package/package.json +3 -3
- package/src/proxmox/adopt-noop.test.ts +4 -5
- package/src/proxmox/backup-job-config.ts +95 -0
- package/src/proxmox/backup-job-distilled.ts +86 -0
- package/src/proxmox/backup-job.ts +78 -116
- package/src/proxmox/ceph-fs-delete.test.ts +5 -8
- package/src/proxmox/ceph-fs-destroy-sdk.test.ts +98 -0
- package/src/proxmox/ceph-fs-distilled.ts +41 -13
- package/src/proxmox/ceph-fs-wire.ts +5 -93
- package/src/proxmox/ceph-fs.ts +7 -7
- package/src/proxmox/constraints.test.ts +9 -8
- package/src/proxmox/distilled-pbs.test.ts +87 -0
- package/src/proxmox/distilled-pbs.ts +50 -0
- package/src/proxmox/fake-pbs-jobs.ts +99 -0
- package/src/proxmox/fake-pbs-notify.ts +4 -1
- package/src/proxmox/fake-pve.ts +3 -2
- package/src/proxmox/live-core-fixtures.ts +126 -0
- package/src/proxmox/live-core-lifecycle.test.ts +115 -0
- package/src/proxmox/live-core-wire.test.ts +190 -0
- package/src/proxmox/metric-server-config.ts +87 -0
- package/src/proxmox/metric-server-distilled.ts +81 -0
- package/src/proxmox/metric-server.ts +77 -88
- package/src/proxmox/notification-distilled.test.ts +162 -0
- package/src/proxmox/notification-matcher-distilled.ts +60 -0
- package/src/proxmox/notification-matcher-lifecycle.ts +71 -0
- package/src/proxmox/notification-matcher.ts +9 -26
- package/src/proxmox/notification-replacement.test.ts +190 -0
- package/src/proxmox/notification-sdk-input.ts +39 -0
- package/src/proxmox/notification-target-distilled.ts +160 -0
- package/src/proxmox/notification-target-form.ts +2 -0
- package/src/proxmox/notification-target-lifecycle.ts +75 -0
- package/src/proxmox/notification-target-spec.ts +68 -0
- package/src/proxmox/notification-target.ts +6 -68
- package/src/proxmox/pbs-datastore-endpoint.ts +1 -1
- package/src/proxmox/pbs-datastore-form.ts +18 -49
- package/src/proxmox/pbs-datastore-guard.ts +7 -4
- package/src/proxmox/pbs-datastore-lifecycle.ts +114 -0
- package/src/proxmox/pbs-datastore-wire.ts +94 -0
- package/src/proxmox/pbs-datastore.ts +7 -133
- package/src/proxmox/pbs-jobs-delete.test.ts +40 -0
- package/src/proxmox/pbs-jobs-sdk.test.ts +215 -0
- package/src/proxmox/pbs-notification-errors.test.ts +93 -0
- package/src/proxmox/pbs-notification-input.ts +25 -0
- package/src/proxmox/pbs-notification-mail.test.ts +64 -0
- package/src/proxmox/pbs-notification-matcher-distilled.ts +65 -0
- package/src/proxmox/pbs-notification-matcher.ts +70 -17
- package/src/proxmox/pbs-notification-target-distilled.ts +124 -0
- package/src/proxmox/pbs-notification-target-lifecycle.ts +26 -17
- package/src/proxmox/pbs-notification-target.ts +1 -1
- package/src/proxmox/pbs-notifications-stack.test.ts +25 -0
- package/src/proxmox/pbs-prune-job-lifecycle.ts +52 -0
- package/src/proxmox/pbs-prune-job-spec.ts +53 -0
- package/src/proxmox/pbs-prune-job-wire.ts +68 -0
- package/src/proxmox/pbs-prune-job.ts +10 -58
- package/src/proxmox/pbs-sync-job-attributes.ts +33 -0
- package/src/proxmox/pbs-sync-job-form.ts +7 -6
- package/src/proxmox/pbs-sync-job-lifecycle.ts +46 -0
- package/src/proxmox/pbs-sync-job-wire.ts +71 -0
- package/src/proxmox/pbs-sync-job.ts +10 -41
- package/src/proxmox/pbs-verify-job-lifecycle.ts +52 -0
- package/src/proxmox/pbs-verify-job-spec.ts +76 -0
- package/src/proxmox/pbs-verify-job-wire.ts +58 -0
- package/src/proxmox/pbs-verify-job.ts +13 -85
- 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.
|