@homeflare/alchemy 0.37.0 → 0.37.2

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 (73) hide show
  1. package/dist/proxmox/ceph-daemon-wire.d.ts +5 -7
  2. package/dist/proxmox/ceph-daemon-wire.d.ts.map +1 -1
  3. package/dist/proxmox/ceph-daemon.d.ts +7 -8
  4. package/dist/proxmox/ceph-daemon.d.ts.map +1 -1
  5. package/dist/proxmox/ceph-fs-distilled.d.ts +31 -5
  6. package/dist/proxmox/ceph-fs-distilled.d.ts.map +1 -1
  7. package/dist/proxmox/ceph-fs-wire.d.ts +5 -22
  8. package/dist/proxmox/ceph-fs-wire.d.ts.map +1 -1
  9. package/dist/proxmox/ceph-fs.d.ts +5 -6
  10. package/dist/proxmox/ceph-fs.d.ts.map +1 -1
  11. package/dist/proxmox/ceph-pool-settle.d.ts +9 -8
  12. package/dist/proxmox/ceph-pool-settle.d.ts.map +1 -1
  13. package/dist/proxmox/ceph-pool-wire.d.ts +11 -20
  14. package/dist/proxmox/ceph-pool-wire.d.ts.map +1 -1
  15. package/dist/proxmox/group-wire.d.ts +8 -13
  16. package/dist/proxmox/group-wire.d.ts.map +1 -1
  17. package/dist/proxmox/index.js +296 -326
  18. package/dist/proxmox/index.js.map +14 -14
  19. package/dist/proxmox/node-network-wire.d.ts +1 -1
  20. package/dist/proxmox/node-network-wire.d.ts.map +1 -1
  21. package/dist/proxmox/storage-wire.d.ts +8 -11
  22. package/dist/proxmox/storage-wire.d.ts.map +1 -1
  23. package/dist/proxmox/unreadable-read.d.ts +7 -7
  24. package/dist/proxmox/user-wire.d.ts +8 -11
  25. package/dist/proxmox/user-wire.d.ts.map +1 -1
  26. package/dist/proxmox/user.d.ts +5 -5
  27. package/dist/proxmox/zfs-pool-wire.d.ts +1 -1
  28. package/dist/proxmox/zfs-pool-wire.d.ts.map +1 -1
  29. package/docs/changelog/README.md +22 -0
  30. package/docs/changelog/archive-01.md +176 -0
  31. package/docs/changelog/archive-02.md +180 -0
  32. package/docs/changelog/archive-03.md +179 -0
  33. package/docs/changelog/archive-04.md +178 -0
  34. package/docs/changelog/archive-05.md +177 -0
  35. package/docs/changelog/archive-06.md +180 -0
  36. package/docs/changelog/archive-07.md +180 -0
  37. package/docs/changelog/archive-08.md +178 -0
  38. package/docs/changelog/archive-09.md +181 -0
  39. package/docs/changelog/archive-10.md +182 -0
  40. package/docs/changelog/archive-11.md +182 -0
  41. package/docs/changelog/archive-12.md +183 -0
  42. package/docs/changelog/archive-13.md +180 -0
  43. package/docs/changelog/archive-14.md +178 -0
  44. package/docs/changelog/archive-15.md +181 -0
  45. package/docs/changelog/archive-16.md +182 -0
  46. package/docs/changelog/archive-17.md +183 -0
  47. package/docs/changelog/archive-18.md +55 -0
  48. package/docs/distilled-interim.md +15 -3
  49. package/docs/provider-standard.md +24 -6
  50. package/docs/proxmox.md +26 -0
  51. package/docs/upstream-conformance-bun.md +67 -0
  52. package/docs/upstream-conformance-conforms.md +55 -0
  53. package/docs/upstream-conformance-families.md +95 -0
  54. package/docs/upstream-conformance-grafana.md +106 -0
  55. package/docs/upstream-conformance.md +20 -298
  56. package/package.json +3 -3
  57. package/src/proxmox/ceph-daemon-wire.ts +4 -7
  58. package/src/proxmox/ceph-daemon.ts +7 -8
  59. package/src/proxmox/ceph-fs-delete.test.ts +5 -8
  60. package/src/proxmox/ceph-fs-destroy-sdk.test.ts +98 -0
  61. package/src/proxmox/ceph-fs-distilled.ts +45 -19
  62. package/src/proxmox/ceph-fs-wire.ts +5 -93
  63. package/src/proxmox/ceph-fs.ts +13 -15
  64. package/src/proxmox/ceph-pool-settle.ts +11 -10
  65. package/src/proxmox/ceph-pool-wire.ts +11 -20
  66. package/src/proxmox/ceph-pool-write.test.ts +9 -8
  67. package/src/proxmox/group-wire.ts +7 -12
  68. package/src/proxmox/read-failure-propagation.test.ts +5 -5
  69. package/src/proxmox/storage-wire.ts +9 -10
  70. package/src/proxmox/typed-absence.test.ts +146 -0
  71. package/src/proxmox/unreadable-read.ts +7 -7
  72. package/src/proxmox/user-wire.ts +7 -10
  73. package/src/proxmox/user.ts +5 -5
@@ -0,0 +1,181 @@
1
+ # Alchemy changelog archive 15
2
+
3
+ [Current changelog](../../CHANGELOG.md) · [Archive index](./README.md)
4
+
5
+ ⚠️ **The ruleset half cannot adopt.** Alchemy's `Ruleset` reports nothing without prior state and
6
+ creates unconditionally, and GitHub allows two rulesets with one name — so a first deploy onto a
7
+ repository that already has one adds a second, both enforcing. `GitHub.Repository` does not share
8
+ the problem. Check `gh api repos/<owner>/<repo>/rulesets` first, or pass your own `rulesetName`.
9
+ See `docs/repo-policy.md`.
10
+
11
+ ### Patch Changes
12
+
13
+ - [#101](https://github.com/taslabs-net/homeflare-kit/pull/101) [`2e43257`](https://github.com/taslabs-net/homeflare-kit/commit/2e4325741b1264feb4069712e2da1dc28c7f9ac3) Thanks [@taslabs-net](https://github.com/taslabs-net)! - `repoPolicy` refuses two more ways to build a ruleset that matches no ref: a blank ref
14
+ pattern (`include: [' ']` has length 1, so the empty-array guard passed it) and an
15
+ `exclude` that cancels every `include` (exclusions win in a GitHub ruleset, so it reads
16
+ as a narrowing and acts as an off switch). Both produced an `active` ruleset over nothing
17
+ with `allowAutoMerge: true` — the end state the auto-merge guard exists to prevent.
18
+ Include and exclude patterns are now trimmed, de-duplicated and sorted like `checks`.
19
+
20
+ `docs/repo-policy.md` also records, measured against live GitHub rather than inferred,
21
+ that the ruleset half never plans a no-op, and that its `rules` and `bypass_actors` are
22
+ replaced wholesale rather than merged.
23
+
24
+ ## 0.13.0
25
+
26
+ ### Minor Changes
27
+
28
+ - [#95](https://github.com/taslabs-net/homeflare-kit/pull/95) [`4fc9a38`](https://github.com/taslabs-net/homeflare-kit/commit/4fc9a38d5f2f9513d75edad99edaf8b9005afc30) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Proxmox notifications that page. `@homeflare/alchemy/proxmox` adds three adopt-capable families and
29
+ a webhook body:
30
+
31
+ - **`PbsNotificationTarget`** — PBS `webhook`, `smtp` and `sendmail` targets. Secret fields are
32
+ **write-only**: a webhook `secret`, the smtp `password` and any credential-bearing `header` are
33
+ declared as `{ fromEnv: 'VARIABLE' }` and read by the deploying process at call time. No value
34
+ reaches Alchemy state — the store keeps names, a fixed-salt scrypt digest of the live headers, and
35
+ a random-salt seal of what the provider last wrote. A plan diffs on those (hash or presence), a
36
+ plan without the variables is presence-only, a rotated value is PUT alone, and a write that needs
37
+ a missing variable fails by the variable's name before any request.
38
+ - **`PbsNotificationMatcher`** and **`ProxmoxNotificationMatcher`** — `match-severity`,
39
+ `match-field`, `match-calendar`, `targets`, `mode`, `invert-match`, `comment`, `disable`. A matcher
40
+ is compared as its whole rule; both built-in `default-matcher`s adopt as-is with no write.
41
+ - **`alertmanagerAlertBody()`** — a PBS webhook body template that posts one Alertmanager v2 alert
42
+ (`/api/v2/alerts`; labels `alertname`, `severity`, `source`, `job_type`, `job_id`, `datastore`,
43
+ `hostname`; annotations `summary`, `description`). Every value goes through `json`, and every
44
+ optional field is guarded, so a GC failure (no `job-id`) and the UI's field-less Test notification
45
+ both render valid JSON.
46
+ - `FromEnv` is exported. The PVE and PBS generated API types now cover the notification endpoints
47
+ and matchers, and the shared PVE read treats a `{"data": null}` answer as absent instead of
48
+ throwing.
49
+
50
+ Guide: `docs/pbs-notifications.md`.
51
+
52
+ ### Patch Changes
53
+
54
+ - [#97](https://github.com/taslabs-net/homeflare-kit/pull/97) [`1c746d2`](https://github.com/taslabs-net/homeflare-kit/commit/1c746d26334444d8ef8270fcba857035391b11e0) Thanks [@taslabs-net](https://github.com/taslabs-net)! - `Pbs.NotificationTarget` now refuses a literal credential in a plain prop, and does so at plan,
55
+ before anything is stored.
56
+
57
+ - The target refuses an `Authorization`, `Proxy-Authorization` or `Cookie` header, a header or URL
58
+ query parameter named like `token`, `key`, `secret`, `password` or `signature`, and a password in
59
+ the URL's userinfo. Each must be declared `{ fromEnv }` or read `{{ secrets.<name> }}`. Before
60
+ this change, such a value planned, deployed, and stayed in the state store. A target already
61
+ deployed that way now fails its plan until the literal is moved; the next deploy then replaces
62
+ the stored props.
63
+ - Refusals now run in Alchemy's adoption probe as well as in `diff`. A new target used to be
64
+ refused only in `reconcile`, after Alchemy had already committed its props to state.
65
+ - `alertmanagerAlertBody()` refuses a `generatorURL` that is not an absolute http(s) URL.
66
+ Alertmanager rejects the whole post for one.
67
+ - Docs: `docs/pbs-alertmanager-body.md` shows the exact template text. The old block had been
68
+ reflowed by the formatter. PVE's per-matcher read needs `Mapping.Audit` or `Mapping.Modify`;
69
+ `Mapping.Use` is not enough.
70
+
71
+ ## 0.12.0
72
+
73
+ ### Minor Changes
74
+
75
+ - [#90](https://github.com/taslabs-net/homeflare-kit/pull/90) [`de65267`](https://github.com/taslabs-net/homeflare-kit/commit/de652677804581b1dd431d3930d6656ac760037f) Thanks [@taslabs-net](https://github.com/taslabs-net)! - **New: the provisioning baseline, one description for every cluster and node.**
76
+ `@homeflare/alchemy/proxmox` now exports:
77
+
78
+ - `PROVISION_PRIVILEGES`: the provision role's 27 privileges as one sorted, frozen constant. It is
79
+ the union of what every family's reconcile needs, including what the lane needs to manage the
80
+ baseline itself.
81
+ - `PROVISION_DEFAULTS` and `provisionBaseline(names)`: generic names (role `HfProvisioner`, users
82
+ `hf-provision@pve` and `hf-read@pve`, group `hf-mint`, read role `PVEAuditor`), overridable per
83
+ site. Names that are not PVE-shaped or would need shell quoting are refused.
84
+ - `declareProvisionBaseline(id, target, names?, { adopt? })`: declares the role, the mint group,
85
+ one user per lane (its group membership included) and the grant for each lane on `/`. Every
86
+ resource retains, and `adopt` is piped only when asked.
87
+ - `provisionBootstrap(names?)`: a pure generator of the one-time root commands for a new cluster
88
+ or node, as a POSIX `sh` script. It checks each object before changing it, so it is idempotent,
89
+ and it never creates a token or sets a password.
90
+
91
+ The provision lane cannot create itself, so root bootstraps it once, then the stack adopts it and
92
+ keeps it. After the bootstrap, the declaration is a clean adoption that writes nothing. See
93
+ `docs/provision-baseline.md`.
94
+
95
+ ### Patch Changes
96
+
97
+ - [#89](https://github.com/taslabs-net/homeflare-kit/pull/89) [`94fbc2f`](https://github.com/taslabs-net/homeflare-kit/commit/94fbc2f0f8471e64164b4c77418140c0fcf28cce) Thanks [@taslabs-net](https://github.com/taslabs-net)! - The adopt verifier's guide now says what `alchemy drift` does in alchemy 2.0.0-beta.79. It has no dry run and no `--yes`, although its docs page lists one: `alchemy drift --yes` fails with `Unrecognized flag`. A non-interactive run prints the repair plan and exits `0` even when something drifted, so it cannot gate a deploy. `--repair` restores the props saved at the last deploy, not what the code declares now, and it writes without a prompt, outside the deploy gate. The ownership guide now says that recovering from a wiped state store, or from `alchemy state delete`, needs `--adopt` for every family that follows the ownership rule, and for `MeshNode` and `R2BucketLock`. Alchemy's docs say objects with no ownership marker re-import without the flag, but many of Alchemy's own marker-less providers refuse them too, as the kit does. The exceptions are a `CaddyConfig` running the declared config, and the families whose `read` never answers `Unowned`: the `pveHandlers`, `forgejoHandlers` and Talos resources.
98
+
99
+ - [#94](https://github.com/taslabs-net/homeflare-kit/pull/94) [`745d941`](https://github.com/taslabs-net/homeflare-kit/commit/745d94161b5cdd63c8d8ebd40a730c8b8b931ff7) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Docs only. The changelog marks 0.9.0 as never published (no npm version, no git tag): its changes first shipped in 0.10.0, and the README, the openbao README and the ownership guide now say so where they cite 0.9.0. The `Proxmox.Storage` header no longer says the estate's provision role lacks `Datastore.Allocate`: it was widened, and the provisioning baseline carries it.
100
+
101
+ - [#93](https://github.com/taslabs-net/homeflare-kit/pull/93) [`d3c332b`](https://github.com/taslabs-net/homeflare-kit/commit/d3c332bd4f175cc3510b7ae06ff98f4b426f0c52) Thanks [@taslabs-net](https://github.com/taslabs-net)! - The published sources, docs and examples no longer name the maintainer's own infrastructure.
102
+ Node names, cluster and pool names, NICs, VLANs, addresses, hostnames, guest ids, principals and
103
+ policy names in comments, fixtures and examples are now neutral placeholders: nodes `node-a`…
104
+ `node-d`, a reference cluster `C1`, documentation addresses (RFC 5737), `bao.example.internal`.
105
+ Measured facts are unchanged; only the names are. `site.example.json` names its hosts `node-a`…
106
+ `node-c`. One runtime message changed: `forgejo-bootstrap` now says to run on "the host where
107
+ Forgejo runs". The historical CHANGELOG entries are unchanged.
108
+
109
+ ## 0.11.0
110
+
111
+ ### Minor Changes
112
+
113
+ - [#87](https://github.com/taslabs-net/homeflare-kit/pull/87) [`520926f`](https://github.com/taslabs-net/homeflare-kit/commit/520926fd81230025bb5337a2b0ea72992c0ccf7f) Thanks [@taslabs-net](https://github.com/taslabs-net)! - **Breaking: a `ProxmoxLxc` adoption never changes a guest.** When a guest is adopted — found by
114
+ the adoption probe with no state, or an interrupted create resumed under `--adopt` that the plan
115
+ could not prove its own — any key the declaration says otherwise now FAILS the plan, naming the
116
+ keys and never their values:
117
+
118
+ ```
119
+ CT 100 on pve1: adopting it would change memory, net1. An adoption never changes a guest, so
120
+ nothing is written. …
121
+ ```
122
+
123
+ Only an exact match adopts, and its deploy writes nothing. Before, such a plan said `adopted`,
124
+ only logged the keys as a warning, and `deploy --adopt --yes` wrote them. The deploy asks again
125
+ against a fresh read, so a hand edit between plan and deploy is refused rather than written back.
126
+ To change a guest, adopt it as it runs first; the change is then an ordinary `update`. The
127
+ ownership rules are unchanged: nothing is adopted without `--adopt` or `adopt(true)`, and an
128
+ interrupted create proven its own still resumes and writes. See `docs/proxmox-lxc-adopt.md`.
129
+
130
+ ## 0.10.1
131
+
132
+ ### Patch Changes
133
+
134
+ - [#85](https://github.com/taslabs-net/homeflare-kit/pull/85) [`61249dc`](https://github.com/taslabs-net/homeflare-kit/commit/61249dce57ec7a3ba319246074ba7e431a9927fc) Thanks [@taslabs-net](https://github.com/taslabs-net)! - **Fix: `hf-adopt-verify` no longer passes a drifted adoption whose provider's `diff` never looks at
135
+ the live object.** Alchemy gives an adopted row's `diff` the declaration as its recorded props, so
136
+ a diff that compares recorded props with the declaration says `noop` whatever the cloud holds.
137
+ Alchemy's own `Cloudflare.R2Bucket` diff works that way. The verifier printed `ok` and exited `0`
138
+ while listing the drift under `changed`, and the deploy then wrote it. Now an adopted `noop` with
139
+ something under `changed` gets its `diff` run a second time, with the live values of those fields
140
+ passed as the recorded props (still read-only, and write paths are still refused). A second
141
+ `noop` passes with a note that the family does not manage those fields. Any other answer fails
142
+ the row. The answer appears as `recheck` in the JSON report. No kit PVE/PBS family is affected:
143
+ each one's `diff` re-reads the cluster.
144
+
145
+ **Fix: the default report no longer hides a `create` when a rename hands its old id to a new
146
+ resource.** A row now counts as stateful only when it plans from the state row it reads, rather
147
+ than any row that happens to sit at its FQN.
148
+
149
+ ## 0.10.0
150
+
151
+ ### Minor Changes
152
+
153
+ - [#84](https://github.com/taslabs-net/homeflare-kit/pull/84) [`eb2fd8b`](https://github.com/taslabs-net/homeflare-kit/commit/eb2fd8b0a7f332731316ceedfa9af3a619616e95) Thanks [@taslabs-net](https://github.com/taslabs-net)! - ⚠️ **BEHAVIOUR CHANGE — three silent takeovers in 0.9.0's ownership rule closed, and `sudoRunner()`
154
+ refuses more.** Found by an adversarial review of 0.9.0, each measured through Alchemy's own plan
155
+ and apply before the fix, and each now refused, writing nothing. It narrows two 0.9.0 notes: crash
156
+ recovery without `--adopt` needs a row that can prove the object ours, and `--adopt` at apply
157
+ never covers a fresh replace's new generation.
158
+
159
+ - **A `Bao.*` create killed before its ownership check no longer resumes onto someone else's object.**
160
+ Apply writes the `creating` row before `reconcile` runs, and drops any prop still an `Output`
161
+ from it. With the name an Output, the next deploy "resumed" that create and wrote over another
162
+ owner's role (all nine role and MFA families, and `Bao.Mount` by path). With a knob an Output,
163
+ `Bao.Mount` and `Bao.AuthMethod` read the missing prop as "not managed", adopted another owner's
164
+ mount and tuned it. A state row now proves an object ours only when it carries every value the
165
+ declaration names, and a resume is let through only when the family's own `read` proves the
166
+ object that generation's — for every `Bao.*` family and `ProxmoxLxc`. **So a create killed
167
+ while a prop was still an Output now needs `--adopt` to resume.**
168
+ - **An interrupted `Bao.*` replace no longer writes over what another owner put at its new
169
+ identity since**, unless `--adopt`.
170
+ - **`--adopt` at apply now covers a create or an interrupted generation, never a fresh replace's
171
+ new generation**, for every `Bao.*` family, `HostFile` and `LaunchdJob`. The planner never
172
+ offers adoption there, yet a deploy-wide `--adopt` let a `HostFile` whose path changed overwrite
173
+ a file it did not own at the new path.
174
+ - **`sudoRunner()` also refuses** (⚠️ a prefix 0.9.0 accepted can now fail): a directory _above_ the
175
+ prefix, from `/` down, that root does not own alone (whoever may write the prefix's parent can
176
+ swap the prefix itself; a root-owned symlink such as `/etc` is still followed), and any ACL entry
177
+ from `/` down to the file that grants a write right (read with `ls -lden`, as the operator; deny
178
+ entries pass; unreadable ACLs refuse). Both run before sudo and, through `checkWrite`, at plan
179
+ time.
180
+
181
+ `docs/ownership.md` and `docs/launchd-sudo.md` carry the details and the limits.
@@ -0,0 +1,182 @@
1
+ # Alchemy changelog archive 16
2
+
3
+ [Current changelog](../../CHANGELOG.md) · [Archive index](./README.md)
4
+
5
+ - [#83](https://github.com/taslabs-net/homeflare-kit/pull/83) [`01c54cf`](https://github.com/taslabs-net/homeflare-kit/commit/01c54cfcf3892388369e4c01765ca5f69d843e8b) Thanks [@taslabs-net](https://github.com/taslabs-net)! - **New: `hf-adopt-verify` and `@homeflare/alchemy/verify` — prove a deploy's adoptions are no-ops
6
+ before it runs.** Alchemy prints `adopted` for an object that already matches and for one that
7
+ drifts alike (beta.79 `Plan.ts` turns the diff's `noop` into an update after the adoption probe),
8
+ and the deploy reconciles both. The verifier plans the stack with Alchemy's own planner, with
9
+ every provider watched and every write path refused. For each row without a state row it reports
10
+ the provider's `read`, its own `diff` before the engine forced it, and the declared fields that
11
+ differ (names only). It exits `0` only when all are no-ops, `1` when any is not, `2` when the plan
12
+ could not be computed.
13
+
14
+ ```sh
15
+ bunx --bun hf-adopt-verify --config alchemy.run.ts --stage live [--all] [--json]
16
+ ```
17
+
18
+ `verifyStack(target)` and `verifySession({ stack, context })` are the same thing as functions.
19
+
20
+ **Fix: adopting a `Proxmox.CephPool` that already matches no longer writes.** Its reconcile PUT
21
+ `setpool` whenever the pool existed, so every adoption forked a `cephsetpool` worker under the
22
+ provision token. On TB4 that was six tasks, one per pool, on 2026-09-13 and 2026-09-20. It now
23
+ skips the PUT when its own `matches` holds, like every other PVE/PBS family. The predicate is
24
+ shared in one place (`update-guard.ts`). `docs/adopted-deploys.md` traces what a deploy of an
25
+ adopted row does for every family the Proxmox and PBS stacks use.
26
+
27
+ - [#80](https://github.com/taslabs-net/homeflare-kit/pull/80) [`66d9374`](https://github.com/taslabs-net/homeflare-kit/commit/66d937416287b8b657da4091eecdd3824666087a) Thanks [@taslabs-net](https://github.com/taslabs-net)! - `@homeflare/alchemy/proxmox` now exports `ProxmoxLxc` as a Resource. It declares a Proxmox VE container: you can adopt one that runs today, or create one from a template.
28
+
29
+ - **The props are PVE's own keys and spellings**, taken from `/nodes/{node}/lxc/{vmid}/config`: `hostname`, `cores`, `memory`, `swap`, `rootfs`, `mpN`, `netN`, `devN`, `features`, `unprivileged`, `onboot`, `startup`, `description`, `tags`, and the rest. To adopt a guest, paste its `pvesh get` output. An undeclared key is unmanaged. A key declared as `''` is removed, where PVE allows that. `password`, `ssh-public-keys` and `env` are typed `never`.
30
+ - **Values are compared as PVE stores them.** Key order and written-out defaults are ignored. A MAC PVE generated is ignored, and a NIC write keeps the live MAC. `storage:GiB` equals the volume it allocated. An existing volume is always written back with its live volume id.
31
+ - **Changes are made in place.** Config changes use one `PUT …/config` carrying the config `digest`. A larger disk uses `PUT …/resize`. Create, resize and delete wait for their PVE task. If nothing differs, nothing is written, including on the first deploy after an adoption.
32
+ - **Nothing plans a replace.** These changes fail the plan with a sentence, and nothing is written: a new `vmid`, a `node` the guest is not on, another `ostemplate`, an `unprivileged` flip, a smaller disk, another storage, or detaching a mount point.
33
+ - **Keys only root@pam can write are refused at plan.** These are `devN`, bind or device mounts, features other than `nesting`, and any feature on a privileged guest. PVE never treats an API token as `root@pam`, so the refusal prints the `pct set` to run on the node instead.
34
+ - **A read failure is not "absent".** A config read counts as absent only when it answers 500 and the cluster lists the vmid nowhere. Any other failure fails the plan. A vmid held by another node, or by a QEMU VM, fails the plan and says where it is. After HA or `pct migrate` has moved a guest, setting `node` to where it is now is an update that writes nothing.
35
+ - **Nothing is adopted without `adopt(true)` or `--adopt`, not even a guest that matches the declaration.** This is the rule of `docs/ownership.md`, which every `Bao.*` family, `HostFile` and `LaunchdJob` follow. Matching is not proof of ownership: once state claims a guest, `RemovalPolicy.destroy()` deletes it and its volumes. Without adoption on, the plan fails with "Cannot adopt". A create interrupted after its POST still resumes without `--adopt` when the guest matches what it declared.
36
+ - **An adoption's plan always says `adopted`.** Alchemy prints no diff for it, so a warning names each key a deploy would write.
37
+ - **A create only ever allocates.** A create naming an existing volume id (rather than `storage:GiB`) is refused, because PVE would unpack the template onto that volume; so is any key the resource does not manage. A deploy that planned a create never takes over a guest it then finds at that vmid. Without adoption on, it fails and forgets its `creating` row. With it on, a matching guest is recorded with no write and any other is refused. A guest the cluster lost while state still holds it plans `update` with a warning that the deploy creates it again, or fails the plan when it cannot be created.
38
+ - **State keeps managed keys only.** The `config` attribute is an allowlist, so a key a newer PVE adds (such as `entrypoint`) is not stored.
39
+ - **It retains by default.** Dropping the declaration leaves the guest running. Only `RemovalPolicy.destroy()` deletes it, only while the guest still matches its last declaration, and it never forces the delete or stops the guest first.
40
+
41
+ Breaking, for anyone who deep-imported the old provider-only version: `storage` is gone (declare `rootfs: 'storage:GiB'`), `ostemplate` is optional, the attributes are now `{ node, vmid, config, rawKeys }`, and `hostname` no longer defaults to `ct<vmid>`. The guide is `docs/proxmox-lxc.md`.
42
+
43
+ ### Patch Changes
44
+
45
+ - [#82](https://github.com/taslabs-net/homeflare-kit/pull/82) [`462368f`](https://github.com/taslabs-net/homeflare-kit/commit/462368fa263ef541bac7c0e70070fb656b4fcda7) Thanks [@taslabs-net](https://github.com/taslabs-net)! - The ownership and `ProxmoxLxc` guides now say how to read an adoption's plan before it writes. `alchemy plan` has no `--adopt` flag in alchemy 2.0.0-beta.79, so without adoption on it stops at "Cannot adopt" before any resource can warn what taking the object over would write. Run `alchemy deploy --adopt --dry-run` instead, or declare `.pipe(adopt(true))` and run `alchemy plan`. The LXC guide also warns that a drift warning does not stop the deploy: `deploy --adopt --yes` writes a `net0` declared without the live `tag=` without it, and the guest leaves its VLAN.
46
+
47
+ ## 0.9.0
48
+
49
+ > ⚠️ Never published: no npm version and no git tag. These changes first shipped in 0.10.0.
50
+
51
+ ### Minor Changes
52
+
53
+ - [#78](https://github.com/taslabs-net/homeflare-kit/pull/78) [`8de1015`](https://github.com/taslabs-net/homeflare-kit/commit/8de1015993f147ffb2f88116204cba6efb449268) Thanks [@taslabs-net](https://github.com/taslabs-net)! - ⚠️ **BEHAVIOUR CHANGE — `@homeflare/alchemy/openbao` no longer adopts anything silently.** Until
54
+ 0.8.0, every `Bao.*` family adopted a live object it had no state for: a new declaration of a
55
+ policy, role, mount, auth method, plugin or MFA object that already existed was taken over, and
56
+ rewritten, without being asked. **A stack that relied on that must now add `adopt(true)` to those
57
+ resources, or deploy once with `--adopt`.** Otherwise its next plan fails with
58
+ `OwnedBySomeoneElse` ("Cannot adopt resource … Re-run with `--adopt`").
59
+
60
+ - **Every `Bao.*` family (all 14).** With no state row, a live object reads as `Unowned`, even when it
61
+ is identical to the declaration. Identical is not proof of ownership: under `destroy`, the old
62
+ owner's delete would remove the object the new declaration had just claimed. The plan fails
63
+ unless adoption is on. This is the rule `HostFile`, `LaunchdJob` and `CaddyConfig` already
64
+ follow. The 0.8.0 swap (a new logical id for a live name, then the old id's delete) now fails the
65
+ plan and writes nothing.
66
+ - **Crash recovery still works without `--adopt`.** Alchemy's recovery read for an interrupted
67
+ create carries that row's own instance id, and the object is ours when it also matches the row's
68
+ props. An interrupted replace resumes through a note that its `diff` leaves for the apply. A
69
+ create interrupted between two writes (a mount enabled but not tuned) is not proven ours, so it
70
+ needs `--adopt`.
71
+ - **The same check at apply.** Alchemy skips the probe while a prop is still an Output, and never
72
+ probes the new generation of a replace. Each family's `reconcile` now reads the object first and
73
+ refuses the takeover before any write, unless `--adopt` or the resource's own `adopt(…)` allows
74
+ it, resolved as the planner resolves it. A refused create also forgets the `creating` row Apply
75
+ wrote, so the next plan does not adopt what the apply refused. A `BaoMount` / `BaoAuthMethod`
76
+ create with `remountFrom` refuses to move a live mount the stack holds no state for.
77
+ - **`HostFile` and `LaunchdJob`: `--adopt` now works at apply.** Their `reconcile` refused a
78
+ foreign file or job even under `--adopt`, where the probe had been skipped. A create now takes it
79
+ over when adoption is on. `adopt(false)` still wins over the flag. A rename onto an occupied
80
+ path or label stays refused.
81
+ - **`sudoRunner()` refuses more, before sudo** (⚠️ a declaration that 0.8.0 accepted can now fail):
82
+ - a root-owned file under a prefix that would be group- or world-writable, setuid or setgid
83
+ (`mode & 0o6022`; an omitted owner is root);
84
+ - any directory between the prefix and the file that root does not own, or that group or other
85
+ may write. Before, only the prefix itself was checked.
86
+ - A `HostFile` or `LaunchdJob` plan that will write now runs these checks too, through the new
87
+ optional `HostRunner.checkWrite`, so the refusal fails the plan instead of the apply. It only
88
+ reads, as the operator: a plan still never calls sudo.
89
+
90
+ New: `docs/ownership.md` (the rule, where it is checked, recovery, limits). `docs/launchd-sudo.md`,
91
+ `docs/launchd.md` and the openbao README and REPLACE.md are updated.
92
+
93
+ ## 0.8.0
94
+
95
+ ### Minor Changes
96
+
97
+ - [#74](https://github.com/taslabs-net/homeflare-kit/pull/74) [`cd6d437`](https://github.com/taslabs-net/homeflare-kit/commit/cd6d437c134ca726a6d6ca5b28053ed01a2762e6) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Rename safety for the other nine name- or catalog-keyed families in `@homeflare/alchemy/openbao`: `BaoAuthRole`, `BaoPkiRole`, `BaoJwtRole`, `BaoKubernetesRole`, `BaoJwtAuthConfig`, `BaoMfaTotpMethod`, `BaoMfaLoginEnforcement`, `BaoSshRole` and `BaoPlugin`. They now get the same checks as `BaoPolicy`, `BaoCloudflareRole` and `BaoProxmoxRole`, through one shared helper.
98
+
99
+ Behaviour changes:
100
+
101
+ - A rename or move onto a name, path or catalog entry that already exists now fails the plan, before anything is written. Before, two roles that swapped names under `RemovalPolicy.destroy()` both planned `replace`, and a green deploy deleted both of them. This happened for `BaoAuthRole`, `BaoPkiRole`, `BaoJwtRole`, `BaoKubernetesRole`, `BaoSshRole` and `BaoPlugin`. It also applies under `retain`: a swap now takes two deploys through a free name. For `BaoPlugin` it includes a version bump onto a version already registered by hand. For `BaoJwtAuthConfig` it includes a move onto a mount whose config is already set.
102
+ - The name or mount is now checked while other props are still pending Outputs. Before, a rename in the same deploy as any pending Output planned `update`, and the old object stayed live with no state record. For `BaoMfaTotpMethod` that wrote a second method. Its rename is now refused at plan in that case too.
103
+ - When the name itself is an Output not known until apply, reconcile now refuses the `update` before writing anything. The next deploy plans `replace`, or, for `BaoMfaTotpMethod`, refuses the rename.
104
+ - `BaoKubernetesRole` and `BaoJwtRole` compare names lowercased, as OpenBao keys them, so a change of case is not a move. Both still refuse an upper-case name.
105
+ - `hostAppRoles` now lets a host sit in several classes, with one role per class. It only refuses the same host listed twice in one class. Before, any host listed twice was refused.
106
+ - `hostAppRoles` refuses a host whose class names an inherited object key such as `constructor`. Before, that host passed as a class with no policies and no `secretIdTtl`, and got a role whose secret_id never expires.
107
+
108
+ Documentation: a new logical id for a name that is already live adopts it, and under `RemovalPolicy.destroy()` the old id's delete then removes it, in a green deploy. Change a logical id with Alchemy's `renamedFrom`, and take a name another resource is leaving in the deploy after the move (REPLACE.md). Also: deleting an AppRole role does not revoke the tokens it issued. OpenBao 2.6.2 deletes the role's secret_ids and role_id, so no new login succeeds, but issued tokens live to their TTL and only fail to renew. The `BaoAuthRole` comments said the delete revoked every token.
109
+
110
+ - [#75](https://github.com/taslabs-net/homeflare-kit/pull/75) [`85dd10c`](https://github.com/taslabs-net/homeflare-kit/commit/85dd10cd3fb16e9daf48592d21340449fb381b56) Thanks [@taslabs-net](https://github.com/taslabs-net)! - `CaddyConfig` in `@homeflare/alchemy/caddy` no longer adopts a running Caddy silently. In 0.7.0, the first read adopted whatever config a Caddy was running, and the next apply loaded over it.
111
+
112
+ Behaviour changes:
113
+
114
+ - With no state, the first read is Alchemy's adoption probe. A Caddy whose running config is the declared one is adopted as-is, and nothing is loaded. A Caddy serving nothing (`null`, or no apps) plans a create. Any other config reads as `Unowned`, so the plan refuses it unless the deploy runs with `--adopt`. A Caddyfile that cannot be compared at probe time also reads as `Unowned`, with a warning saying why, never an error: the engine replays this read to recover an interrupted create, and an error would fail every later plan.
115
+ - Where Alchemy skips that probe (props holding an Output, as on `caddyWithFile()`'s first deploy), the apply refuses the same takeover before any `/load` (the HostFile is written by then). It resolves adoption as the planner does: the resource's own `adopt(…)`, else `--adopt`. So `.pipe(adopt(false))` still refuses under `--adopt`, and `.pipe(adopt(true))` takes over without it.
116
+ - The state vouches only for the Caddy it was applied to. When the transport now reaches a Caddy at another endpoint, the apply needs adoption (`--adopt`, or the resource's `adopt(true)`) unless that Caddy runs the config the state last stored, the declared one, or nothing.
117
+
118
+ Docs: `docs/caddy.md` has the adoption table, and records that managed Caddies run `caddy run --resume` with their own `XDG_CONFIG_HOME`, set in the launchd job rather than the envfile. A restart then runs the last config Caddy accepted, and after a resumed start SIGUSR1 has no file to reload. The admin endpoint section moves to `docs/caddy-admin.md`, and the README's reasons for each peer and override pin move to `docs/peers.md`.
119
+
120
+ - [#77](https://github.com/taslabs-net/homeflare-kit/pull/77) [`9ec684c`](https://github.com/taslabs-net/homeflare-kit/commit/9ec684c612c5d9c0987b4e9ca462e1f683bf954e) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Add `sudoRunner()` to `@homeflare/alchemy/launchd`, so a host stack can deploy as the operator
121
+ instead of as root.
122
+
123
+ - **Only the calls that need root use `sudo -n`, in fixed argv shapes.** These are
124
+ `launchctl bootstrap | bootout | kickstart` in the system domain, and `install` / `rm` of a file
125
+ under a prefix the stack declares. A file is written as the operator to a private `0600` temp
126
+ file, then copied into place with `install -S -m <mode> -o <uid> -g <gid>`. Nothing else runs
127
+ as root, and a plan never calls sudo.
128
+ - **It is opt-in:** `launchdProviders(sudoRunner({ prefixes }))`. `localRunner()` stays the
129
+ default and never elevates, and nothing falls back to sudo.
130
+ - **It never prompts.** A password-required `sudo -n` fails at once with `SudoRefusedError`, and
131
+ the message says to run `sudo -v` or to grant exactly these commands `NOPASSWD`.
132
+ - **Each privileged argv is logged before it runs.** The log holds the argv only, never file
133
+ content.
134
+ - **These are refused before sudo is asked, with `SudoRefusedError`:** any argv outside the
135
+ allowlist (such as a bare `bootout system`, a directory `bootstrap`, or a plist anywhere but
136
+ `/Library/LaunchDaemons/<label>.plist`), a system `bootstrap` / `bootout` without
137
+ `/Library/LaunchDaemons` among the prefixes, a prefix that is not a real directory only root may
138
+ write, a path outside every prefix that needs root, a symlink between the prefix and the file, a
139
+ file the operator could not read back, and another user's `gui/<uid>` domain.
140
+
141
+ `docs/launchd-sudo.md` has the full list, the sudoers cautions, and the limits.
142
+
143
+ ## 0.7.0
144
+
145
+ ### Minor Changes
146
+
147
+ - [#72](https://github.com/taslabs-net/homeflare-kit/pull/72) [`294518d`](https://github.com/taslabs-net/homeflare-kit/commit/294518dc205c1981bf8f48aaf087bdb3869d4123) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Rename safety for `BaoPolicy`, `BaoCloudflareRole` and `BaoProxmoxRole` in `@homeflare/alchemy/openbao`.
148
+
149
+ Behaviour changes:
150
+
151
+ - A changed `name` on `BaoPolicy`, or a changed `mount` or `name` on `BaoCloudflareRole` or `BaoProxmoxRole`, now plans `replace`. Before, it planned `update`: the new object was written and the old one stayed live with no state record, even under `RemovalPolicy.destroy()`. Now the old one is deleted after the new one is written, or kept under the default `retain`, and the apply says so.
152
+ - `BaoPolicy` compares names the way OpenBao stores them, trimmed and lowercased, so a change of case is not a rename.
153
+ - A `BaoProxmoxRole` rename that also changes `mintUser` now fails the plan unless `allowMintUserChange` names the old mint user, the same rule an in-place re-scope already had.
154
+ - A rename or move onto a name or path that already exists live now fails the plan, before anything is written. Without this, two policies or roles that swapped names under `RemovalPolicy.destroy()` both planned `replace` and ended with both deleted. The check also applies under `retain`, because a diff cannot see the removal policy: a swap, or a move back onto a retained old generation, now takes two deploys through a free name, or removing the target by hand.
155
+ - When the new name is an Output that is not known until apply, reconcile now refuses the `update` before writing anything. The next deploy plans `replace`. A `BaoProxmoxRole` rename whose `allowMintUserChange` is still an Output defers the same way instead of failing the plan.
156
+
157
+ The rename is checked even while other props are still pending Outputs. `src/openbao/REPLACE.md` has the measured engine behaviour and what `retain` leaves live for each of the three.
158
+
159
+ - [#70](https://github.com/taslabs-net/homeflare-kit/pull/70) [`8cafb48`](https://github.com/taslabs-net/homeflare-kit/commit/8cafb48d84a9734997d7562311801025f2fa4236) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Add the `@homeflare/alchemy/caddy` subpath: a running Caddy's config, declared as Caddyfile text and applied through Caddy's own admin API.
160
+
161
+ - `CaddyConfig` / `CaddyConfigProvider` (`Caddy.Config`): the whole Caddyfile as one prop. Apply is `POST /adapt` (validate), `POST /load` with `text/caddyfile` (graceful reload), then `GET /config/`, which must hash to the adapted config, or the deploy fails. When Caddy refuses a config, it keeps the old one, and the error gives Caddy's reason and confirms whether the old config is still running. This includes Caddy's refusal that arrives in a 200 response after adapter warnings. Drift compares SHA-256 digests of canonical adapted JSON: declared, live and stored. A hand edit or a restart with a different file plans an update. A plan-time `/adapt` fails the plan on a bad Caddyfile. With no state, a running Caddy is adopted. `replace` is never planned. Delete never unloads or stops Caddy, and `retain` is the default.
162
+ - `caddyWithFile()`: the same Caddyfile is also written with launchd's `HostFile` to the file Caddy starts from, so a restart keeps it. The file is written first, then `/load`, and both are retained. `sourceFile` is sent as `Caddy-Config-Source-File` so SIGUSR1 reload-from-file keeps working. `docs/caddy.md` covers the order, `--resume`/autosave and the refused-config window.
163
+ - `CaddyAdmin`, `localCaddyAdmin()`, `caddyAdminLayer()` and `caddyProviders()`: every admin call goes through one injectable transport. The local transport uses `node:http` on both Bun and Node, accepts only loopback `http://` or `unix://`, and sends `Host`/`Origin` the way the Caddy CLI does. `hostHeader` covers narrowed `origins` and SSH-forwarded ports. It retries only refused connections and then rejects with `CaddyUnreachableError`. A stopped Caddy does not fail the plan, because its launchd job may be the fix: read and diff plan the load with a warning, and the apply fails until Caddy answers.
164
+ - Refused before anything is sent: an empty Caddyfile or one that adapts to no apps; literal secrets (a PEM key, a literal after `dns <provider>`, secret-named subdirectives, literal `Authorization` headers, token and password-hash shapes, and a `{$NAME:default}` whose default is one of these); and an adapted `admin` block that would turn the API off, move it off loopback or away from the transport (another port, socket or loopback address, or no address at all when the transport is not at Caddy's default), allow no Host the transport sends, set `enforce_origin` over a unix socket, enable `remote`, or pull config. Secrets go in `{env.NAME}` or `{file./path}` placeholders.
165
+
166
+ - [#71](https://github.com/taslabs-net/homeflare-kit/pull/71) [`73736ae`](https://github.com/taslabs-net/homeflare-kit/commit/73736aec0a686af36cb4ef7ecfba42d40e543fc7) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Add `MeshNode` (`Cloudflare.MeshNode`) and `fetchMeshNodeToken` to `@homeflare/alchemy/cloudflare`.
167
+
168
+ - **`MeshNode`** declares a Cloudflare Mesh node (a `warp_connector`) with `name` and `ha`. It never reads the node token, so the token never reaches Alchemy state. Alchemy's `Cloudflare.Tunnel.WarpConnector` fetches it on every read and stores it, and it has no `ha`. The attributes are `id`, `accountId`, `name`, `status` and `ha`.
169
+ - A `name` change renames the node in place (`PATCH`), keeping its id, token and enrolled replicas.
170
+ - `ha` is required and create-only (Cloudflare: "cannot be changed afterward"). A change replaces the node: delete-first while the name stays (names are unique per account), create-first when the name changes too. An account change is a create-first replace.
171
+ - **It defaults to `RemovalPolicy.retain`**, like the kit's other resources whose deletion breaks their consumers: deleting a node cuts every enrolled replica off the Mesh, and a new one means a new token and Mesh IPs. Dropping the declaration or `alchemy destroy` leaves the node live. Opt in with `.pipe(RemovalPolicy.destroy())`.
172
+ - Under that default a same-name `ha` change **refuses and writes nothing**: the engine keeps the old node, which still holds the name, and a create never reuses a node it did not create (that would record the wrong `ha`). The sentence names the ways on: deploy once with `.pipe(RemovalPolicy.destroy())` (delete-first), delete the old node by hand, or keep it (`alchemy state rm` the row, then `adopt(true)`; reverting `ha` alone refuses again). A create-first replace leaves the old node live. When a create-first replace's new name is already held by another node, the sentence does not call it the old node and says not to delete it. Every way back from inside a replace starts with `alchemy state rm`, because `adopt(true)` alone does not act on a `replacing` row; the 1013-retry sentence says so too. All measured through Alchemy's real plan/apply against the fake.
173
+ - A door (a node with no routes) is documented as `ha: false`: HA fails over routes, and each replica has its own Mesh IP. A second door is a second `MeshNode`.
174
+ - An existing node is adopted by exact name and returned `Unowned`. A create answered code 1013 after a clean lookup (distilled retries a create whose response was lost) names the node that appeared rather than blaming another tunnel type.
175
+ - `list` is empty and `nuke` skips the type, because Alchemy's WarpConnector already lists every `warp_connector`.
176
+ - **`fetchMeshNodeToken({ accountId, id | name })`** returns the node token `Redacted`, on demand, for a one-off enrolment step. Callers write it to a root-owned `0600` file on the node and nowhere else. An empty token fails, and a 403 names the Write permission the endpoint needs. Before any request it refuses the Global API Key, an empty API token, and a set `DISTILLED_DEBUG_HTTP` (distilled would print the token to stderr).
177
+ - Built on `@distilled.cloud/cloudflare`, the SDK Alchemy's own Cloudflare providers use. The `cloudflare@4.5.0` SDK cannot create an HA node. It is a new **required peer**, pinned to the version alchemy pins (`1.0.0-rc.12`): add it to your install line.
178
+ - `providers()` now also resolves Alchemy's Cloudflare credentials and account for `MeshNode`, the same way `Cloudflare.providers()` does (a profile, or `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID`).
179
+
180
+ Guide: `docs/mesh-node.md`.
181
+
182
+ ## 0.6.0
@@ -0,0 +1,183 @@
1
+ # Alchemy changelog archive 17
2
+
3
+ [Current changelog](../../CHANGELOG.md) · [Archive index](./README.md)
4
+
5
+ ### Minor Changes
6
+
7
+ - [#67](https://github.com/taslabs-net/homeflare-kit/pull/67) [`48163cf`](https://github.com/taslabs-net/homeflare-kit/commit/48163cfc83ad7deac8720be2e8fb7fe964ade7a3) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Add to `@homeflare/alchemy/openbao`:
8
+
9
+ - **`BaoJwtRole`**: roles on `jwt` and `oidc` mounts.
10
+ - **`BaoKubernetesRole`**: roles on `kubernetes` mounts. `aliasNameSource` is required, because changing it on a live role moves every pod to a new entity.
11
+ - **`BaoJwtAuthConfig`**: the non-secret config of a JWT-validating mount. It deliberately has no OIDC client secret, and it refuses a mount that has one, because its full-replace write would erase it.
12
+ - **`BaoMfaTotpMethod`** and **`BaoMfaLoginEnforcement`**: login MFA. A TOTP method is found by name. Renaming one is refused, because a rename would strand every enrolled secret. A name already held by another MFA method type is refused too, because the write would convert that method. Deleting an enforcement is refused, because in OpenBao 2.6.2 the delete comes back after a restart (openbao/openbao#4030).
13
+ - **`assertBaoIdentity`** (with `assertBaoIdentityEffect` and `BaoIdentityError`): refuses to proceed unless unauthenticated `sys/health` reports the expected `cluster_name` and the namespace is the expected one.
14
+ - **`hostAppRoles`**: a pure generator that makes one AppRole per host, named `<class>--<host>`. It refuses a secret_id TTL of 0.
15
+
16
+ Behaviour changes:
17
+
18
+ - A changed `path` on `BaoMount` or `BaoAuthMethod` now **fails the plan** unless the new `remountFrom` prop names the old path. With `remountFrom`, the mount is moved with `sys/remount`, keeping its data (leases under it are revoked). Before, the plan answered `update` and enabled an empty mount at the new path.
19
+ - A changed `name` on `BaoAuthRole`, or a changed `name` on `BaoPkiRole`, now plans `replace`. Before, it answered `update` and left the old role live with no state record.
20
+ - `BaoPkiRole` now manages `requireCn`, `enforceHostnames`, `keyUsage`, `allowedDomainsTemplate`, `noStore` and `generateLease`, defaulting to OpenBao's own values. Its full-replace write already reset these fields silently, so a role whose live values differ from those defaults now plans `update` instead of being reset unseen. `noStore` together with `generateLease` is refused.
21
+
22
+ See `src/openbao/REPLACE.md` for what every family does on a rename.
23
+
24
+ - [#68](https://github.com/taslabs-net/homeflare-kit/pull/68) [`b023bae`](https://github.com/taslabs-net/homeflare-kit/commit/b023bae8f20d6aa33239107dc0538750efc466ca) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Add the `@homeflare/alchemy/launchd` subpath, so a Mac host can be declared with Alchemy.
25
+
26
+ - `LaunchdJob` / `LaunchdJobProvider` (`Launchd.Job`): one launchd job in the `system` domain or a `gui/<uid>` domain. The provider renders the plist itself, writes it atomically to the derived path (`/Library/LaunchDaemons` or the user's `LaunchAgents`) and drives `launchctl`. Create bootstraps; update writes, boots out and bootstraps (a restart); read uses `launchctl print`; diff compares the rendered plist's SHA-256 with the stored and on-disk digests. Replace happens only on a `label` or `domain` change, delete-first — so everything the new job would be refused for is refused at plan time, while the old job still runs, and a rename is seen even while other props are unresolved. A label another job already holds is never booted out or overwritten. A failed first bootstrap removes the plist it wrote. A disabled label is refused, not re-enabled. Labels under `org.nixos.`, `com.apple.` and `homebrew.mxcl.` are refused; `docs/launchd.md` describes the nix-darwin cutover.
27
+ - `HostFile` / `HostFileProvider` (`Host.File`): a text file with mode, owner and group. It is written atomically (temp file, then rename), diffed by SHA-256, mode and owner, and refused over a symlink, a directory, or a different file at a new path.
28
+ - `HostRunner`, `localRunner()`, `hostRunnerLayer()` and `launchdProviders()`: every filesystem and `launchctl` call goes through one injectable runner. The local runner never elevates. System-domain writes are refused unless the deploy runs as root or the runner is explicitly `privileged`.
29
+ - `renderPlist`: a small, deterministic XML plist serializer, round-tripped through `plutil` in the tests.
30
+
31
+ Props are stored unencrypted in Alchemy state, so `environment`, `programArguments` and file `content` must not hold secrets. A tripwire refuses the obvious cases; secret files stay rendered by openbao-agent, and the stack declares only their path. Anything already on the host is read as `Unowned`, so it is never adopted without `--adopt`.
32
+
33
+ ## 0.5.0
34
+
35
+ ### Minor Changes
36
+
37
+ - [#64](https://github.com/taslabs-net/homeflare-kit/pull/64) [`3ed9771`](https://github.com/taslabs-net/homeflare-kit/commit/3ed977142ffe2fe6dd02359a3a87b1a0a84c9952) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Add `appRoleLogin` / `revokeSelf` (with `appRoleLoginEffect`, `revokeSelfEffect` and `BaoLoginError`) to `@homeflare/alchemy/openbao`, so wrapper scripts can log in with an AppRole and revoke on exit. They use the same address resolution and transport as the `Bao.*` families. The login never sends `BAO_TOKEN`, and error strings are redacted, because OpenBao 2.6.2 can echo a secret_id back in an error. `clientToken` is a getter over a private field, so printing or serialising the result does not show the token under Bun or Node.
38
+
39
+ Add `BaoPlugin` / `BaoPluginProvider` for the plugin catalog (`sys/plugins/catalog/<type>/<name>`). It has no `env` prop and retains on destroy. It refuses to shadow a builtin or overwrite a declarative entry, and it names the fix when an unversioned registration is filed under the binary's self-reported version.
40
+
41
+ Fix `BaoSshRole` writes. `default_extensions` and `default_critical_options` were sent as JSON strings. OpenBao's field validation rejects that with a 400, so every role write failed. They are now sent as objects.
42
+
43
+ ## 0.4.1
44
+
45
+ ### Patch Changes
46
+
47
+ - [#62](https://github.com/taslabs-net/homeflare-kit/pull/62) [`93b7107`](https://github.com/taslabs-net/homeflare-kit/commit/93b71070b1a64701cadc1a547044a3ec4da26a50) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Export TB4 control-plane Resource constructors from `@homeflare/alchemy/proxmox` (Storage, SDN, access, HA, backup, metrics, PBS). Guests and NIC apply stay Provider-only.
48
+
49
+ ## 0.4.0
50
+
51
+ ### Minor Changes
52
+
53
+ - [#60](https://github.com/taslabs-net/homeflare-kit/pull/60) [`df34d27`](https://github.com/taslabs-net/homeflare-kit/commit/df34d2750fca4a2b6b86490ddb0a819c12f9ea39) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Add `BaoAuthMethod` so a stack can enable `approle` (or jwt/oidc) instead of running `configure-engines`. Metadata only — no role ids or OIDC secrets. File audit stays out: OpenBao 2.6 file audit is config-only.
54
+
55
+ ## 0.3.2
56
+
57
+ ### Patch Changes
58
+
59
+ - [#58](https://github.com/taslabs-net/homeflare-kit/pull/58) [`e7c101f`](https://github.com/taslabs-net/homeflare-kit/commit/e7c101faada2b14466a1f0bbf1f93da46e5e06cb) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Bump Alchemy to 2.0.0-beta.79. Effect stays rc.115. 79 starts Bun with production JSX (the `jsxDEV` CLI crash on 78) and declares `mime` on cloudflare-runtime; the `mime` peer stays so existing consumer installs do not drop a required line.
60
+
61
+ ## 0.3.1
62
+
63
+ ### Patch Changes
64
+
65
+ - [#56](https://github.com/taslabs-net/homeflare-kit/pull/56) [`5f41ad7`](https://github.com/taslabs-net/homeflare-kit/commit/5f41ad7ab59375e42bb4757dd5b4b81f4a7b6cd9) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Export `BaoPkiRole` and `BaoSshRole` from `@homeflare/alchemy/openbao`. The barrel already shipped both providers; stacks constructing either role had to import the resource from internals.
66
+
67
+ ## 0.3.0
68
+
69
+ ### Minor Changes
70
+
71
+ - [#54](https://github.com/taslabs-net/homeflare-kit/pull/54) [`d526365`](https://github.com/taslabs-net/homeflare-kit/commit/d526365a51deac968e5b0076e88b2e68b866534f) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Bump Alchemy to 2.0.0-beta.78 and Effect to 4.0.0-rc.115. Consumers must move the override set with it — Alchemy 78's peer is `effect >= rc.115`. Effect rc.115 renamed `Config.redacted` to `Config.Redacted`. `mime@4.1.0` is now a required peer: Alchemy's cloudflare-runtime imports it and does not declare it.
72
+
73
+ ## 0.2.2
74
+
75
+ ### Patch Changes
76
+
77
+ - [#46](https://github.com/taslabs-net/homeflare-kit/pull/46) [`9bc37f8`](https://github.com/taslabs-net/homeflare-kit/commit/9bc37f8bd7b9af1fd5c62cc4bc48597251ef61ab) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Export `BaoMount` and `BaoAuthRole` from `@homeflare/alchemy/openbao`.
78
+
79
+ The docs already showed `import { BaoMount } from '@homeflare/alchemy/openbao'`, but the barrel only shipped the providers. Stacks constructing those resources had to import from internals.
80
+
81
+ - [#48](https://github.com/taslabs-net/homeflare-kit/pull/48) [`4967118`](https://github.com/taslabs-net/homeflare-kit/commit/4967118cc887faba34b17cd48588102736be49bb) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Export `BaoCloudflareRole` and the Cloudflare role catalog helpers from `@homeflare/alchemy/openbao`, and retry dropped OpenBao transports.
82
+
83
+ Stacks declaring mint roles need the resource constructor plus `parseRolesConfig` / `expandAll` / `permissionGroupsFromEngine`. The barrel previously shipped only `BaoCloudflareRoleProvider`. A 585-role plan against a Mesh-fronted vault died mid-diff with an empty transport error; status-0 calls now retry twice.
84
+
85
+ ## 0.2.1
86
+
87
+ ### Patch Changes
88
+
89
+ - [#40](https://github.com/taslabs-net/homeflare-kit/pull/40) [`60c90eb`](https://github.com/taslabs-net/homeflare-kit/commit/60c90eb4682d0ded2597a79033159ab71cc7a649) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Pin `rolldown` in the consumer overrides so an unlocked install cannot float a missing tarball.
90
+
91
+ Alchemy's optional peer `vite@^8` depends on `rolldown: ~1.2.6`. A consumer `bun add`
92
+ (no lockfile) resolved that to 1.2.9; npm listed the version and 404'd
93
+ `rolldown-1.2.9.tgz`. Main CI failed on that fetch after [#39](https://github.com/taslabs-net/homeflare-kit/issues/39). The override holds 1.2.8 —
94
+ the last tarball a green smoke actually installed.
95
+
96
+ ## 0.2.0
97
+
98
+ ### Minor Changes
99
+
100
+ - [#36](https://github.com/taslabs-net/homeflare-kit/pull/36) [`cff4111`](https://github.com/taslabs-net/homeflare-kit/commit/cff4111c2f4461a45be5811547125b1f5d98c6d2) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Ship the HTTP and Website SDK surfaces apps were hand-rolling.
101
+
102
+ **`@homeflare/kit/openapi`** — `createOpenApiApp()` is OpenAPIHono with one
103
+ readable validation hook. The document and the request share a Zod schema.
104
+ Subpath, not the main entry: a Node script that only wants `parseEnv` must not
105
+ resolve Hono. Peers: `hono`, `@hono/zod-openapi`, `zod` (optional). Import `z`
106
+ from `@hono/zod-openapi`.
107
+
108
+ **`astroWebsite` / `viteWebsite`** on `@homeflare/alchemy/cloudflare` — house
109
+ flags on Alchemy's own stacks. Astro gets `disable_nodejs_process_v2` (workerd
110
+ process-v2 returns `[object Object]`). Vite is TanStack Start / static Vite.
111
+ Not Nextjs: that helper hashes source and plans as create against a live Worker.
112
+
113
+ Catalog also pins `@tanstack/react-router` 1.170.35, `@tanstack/react-start`
114
+ 1.168.52, `@tanstack/react-query` 5.102.8 — match these, do not wrap them.
115
+
116
+ ## 0.1.3
117
+
118
+ ### Patch Changes
119
+
120
+ - [#32](https://github.com/taslabs-net/homeflare-kit/pull/32) [`5cc2cd6`](https://github.com/taslabs-net/homeflare-kit/commit/5cc2cd62241f926aace1646fd3a1e0057e96cddd) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Add `accessIdentity` (ctx.access) and RFC 9728 MCP discovery; fix three doc defects.
121
+
122
+ An app team reviewed the published packages before adopting and was right on every point.
123
+
124
+ **`accessIdentity(ctx)` — Access identity without parsing a JWT.** Cloudflare attaches the
125
+ authenticated identity to the execution context (shipped 2026-08-14), so a Worker behind
126
+ Access reads `ctx.access.getIdentity()` with no token handling. The kit only offered
127
+ `verifyAccessJwt`, which is the older path.
128
+
129
+ ⛔ Both stay, because they are not alternatives: `accessIdentity` for a Worker behind
130
+ Access, `verifyAccessJwt` for an origin that has no `ctx.access` — service-to-service, a
131
+ non-Worker origin, or a Worker reached by service binding, since **`ctx.access` does not
132
+ propagate through bindings**.
133
+
134
+ ⚠️ Read groups from `accessIdentity`, not from a token: Cloudflare trims the JWT's
135
+ `custom` claim at roughly 1 KB _silently_, so token-read group membership can be
136
+ incomplete — an authorization bug that only appears for users in many groups.
137
+
138
+ **`serveMcpMetadata` / `unauthorizedResponse` — RFC 9728.** The MCP spec requires a server
139
+ to publish Protected Resource Metadata _and_ a 401 naming it in `WWW-Authenticate`.
140
+ Publishing the document while answering a bare 401 leaves clients that follow the header
141
+ with nowhere to go.
142
+
143
+ **Three documentation defects, all reported and all real:**
144
+
145
+ - `@homeflare/alchemy`'s README documented `@homeflare/alchemy/providers`, which does not
146
+ exist. The real path is `/cloudflare`.
147
+ - `@homeflare/cloudflare`'s npm description advertised "typed bindings" — it exports none.
148
+ - `@homeflare/kit`'s advertised "logging" — `log` lives in `@homeflare/cloudflare`.
149
+
150
+ **Packing no longer edits a manifest on disk.** `packForPublish` stripped `scripts` and
151
+ `devDependencies` from the real `package.json`, packed, then restored it — which is a race
152
+ when two smoke tests pack the same workspace dependency in parallel. It destroyed
153
+ `@homeflare/kit`'s `scripts` block during this branch, _after_ `verify` had passed. The
154
+ strip now happens inside the packed tarball, so nothing in the repository is written to.
155
+
156
+ ## 0.1.2
157
+
158
+ ### Patch Changes
159
+
160
+ - [#29](https://github.com/taslabs-net/homeflare-kit/pull/29) [`72f0787`](https://github.com/taslabs-net/homeflare-kit/commit/72f0787952b3a71bb6573476918e79117250409e) Thanks [@taslabs-net](https://github.com/taslabs-net)! - `cloudflare` is a required peer, not an optional one.
161
+
162
+ Measured 2026-09-16 against the published 0.1.1 in a clean consumer install: importing
163
+ `@homeflare/alchemy/cloudflare` without it throws `Cannot find package 'cloudflare'`.
164
+ Marking it optional claimed the subpath would degrade gracefully; it does not load at all.
165
+ An optional peer should mean a _feature_ is absent, not that an import fails.
166
+
167
+ ⛔ **Why this got through, and what now stops it.** The README's pinned install command and
168
+ the smoke test's install command disagreed — the smoke test installed `cloudflare`, the
169
+ README never mentioned it, and nothing compared the two. So the gate proved an install no
170
+ consumer would ever perform.
171
+
172
+ `tests/peers.test.ts` now asserts the manifest, the README and the smoke script agree:
173
+ every declared peer appears in all three, peers are pinned rather than ranged, and none is
174
+ marked optional.
175
+
176
+ ## 0.1.1
177
+
178
+ ### Patch Changes
179
+
180
+ - [#27](https://github.com/taslabs-net/homeflare-kit/pull/27) [`5b73400`](https://github.com/taslabs-net/homeflare-kit/commit/5b73400e7fc2dd8e28a0368db3e5aa105ebdde9a) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Fix the peer contract: 0.1.0 installed cleanly and threw at import.
181
+
182
+ Measured 2026-09-16 against the published 0.1.0 in a clean consumer install — three
183
+ defects, none of which failed at install time: