@homeflare/alchemy 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/dist/caddy/access-forward-auth.d.ts +56 -0
- package/dist/caddy/access-forward-auth.d.ts.map +1 -0
- package/dist/caddy/index.d.ts +6 -0
- package/dist/caddy/index.d.ts.map +1 -1
- package/dist/caddy/index.js +169 -75
- package/dist/caddy/index.js.map +10 -7
- package/dist/cloudflare/mcp-server-api.d.ts +135 -0
- package/dist/cloudflare/mcp-server-api.d.ts.map +1 -0
- package/dist/cloudflare/mcp-server-form.d.ts +116 -0
- package/dist/cloudflare/mcp-server-form.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/launchd/file-converge.d.ts +70 -0
- package/dist/launchd/file-converge.d.ts.map +1 -0
- package/dist/launchd/file-identity.d.ts +32 -0
- package/dist/launchd/file-identity.d.ts.map +1 -0
- package/dist/launchd/host-file-form.d.ts +2 -0
- package/dist/launchd/host-file-form.d.ts.map +1 -1
- package/dist/launchd/host-file-lifecycle.d.ts +11 -24
- package/dist/launchd/host-file-lifecycle.d.ts.map +1 -1
- package/dist/launchd/host-file.d.ts.map +1 -1
- package/dist/launchd/index.d.ts +2 -0
- package/dist/launchd/index.d.ts.map +1 -1
- package/dist/launchd/index.js +225 -120
- package/dist/launchd/index.js.map +13 -10
- package/dist/launchd/local-runner.d.ts.map +1 -1
- package/dist/launchd/port-claims.d.ts +31 -0
- package/dist/launchd/port-claims.d.ts.map +1 -0
- package/dist/launchd/runner.d.ts +17 -2
- package/dist/launchd/runner.d.ts.map +1 -1
- package/dist/launchd/sudo-runner.d.ts +11 -0
- package/dist/launchd/sudo-runner.d.ts.map +1 -1
- package/dist/linux/index.js.map +3 -3
- package/dist/linux/sudo-listing.d.ts +47 -0
- package/dist/linux/sudo-listing.d.ts.map +1 -0
- package/dist/proxmox/credential-literals.d.ts +1 -1
- package/dist/proxmox/credential-literals.d.ts.map +1 -1
- package/dist/proxmox/generated/pbs/nodes-node.d.ts +1 -1
- package/dist/proxmox/generated/pbs/nodes-node.d.ts.map +1 -1
- package/dist/proxmox/generated/pbs.d.ts +7 -0
- package/dist/proxmox/generated/pbs.d.ts.map +1 -1
- package/dist/proxmox/generated/pve.d.ts +7 -0
- package/dist/proxmox/generated/pve.d.ts.map +1 -1
- package/dist/proxmox/index.d.ts +1 -1
- package/dist/proxmox/index.d.ts.map +1 -1
- package/dist/proxmox/index.js +2 -2
- package/dist/proxmox/index.js.map +9 -9
- package/dist/proxmox/pbs-notification-target-form.d.ts +1 -1
- package/dist/proxmox/pbs-notification-target-form.d.ts.map +1 -1
- package/dist/proxmox/pbs-notification-target.d.ts +1 -1
- package/dist/proxmox/pbs-notification-target.d.ts.map +1 -1
- package/dist/release/archive.d.ts +11 -0
- package/dist/release/archive.d.ts.map +1 -0
- package/dist/release/binary-claim.d.ts +53 -0
- package/dist/release/binary-claim.d.ts.map +1 -0
- package/dist/release/binary-diff.d.ts +38 -0
- package/dist/release/binary-diff.d.ts.map +1 -0
- package/dist/release/binary-form.d.ts +118 -0
- package/dist/release/binary-form.d.ts.map +1 -0
- package/dist/release/binary-lifecycle.d.ts +55 -0
- package/dist/release/binary-lifecycle.d.ts.map +1 -0
- package/dist/release/binary-read.d.ts +11 -0
- package/dist/release/binary-read.d.ts.map +1 -0
- package/dist/release/binary.d.ts +68 -0
- package/dist/release/binary.d.ts.map +1 -0
- package/dist/release/catalog.d.ts +113 -0
- package/dist/release/catalog.d.ts.map +1 -0
- package/dist/release/checksums.d.ts +22 -0
- package/dist/release/checksums.d.ts.map +1 -0
- package/dist/release/declared-pins.d.ts +4 -0
- package/dist/release/declared-pins.d.ts.map +1 -0
- package/dist/release/download.d.ts +44 -0
- package/dist/release/download.d.ts.map +1 -0
- package/dist/release/fake-deploy.d.ts +48 -0
- package/dist/release/fake-deploy.d.ts.map +1 -0
- package/dist/release/fake-engine.d.ts +8 -0
- package/dist/release/fake-engine.d.ts.map +1 -0
- package/dist/release/fake-install.d.ts +39 -0
- package/dist/release/fake-install.d.ts.map +1 -0
- package/dist/release/fake-provider.d.ts +58 -0
- package/dist/release/fake-provider.d.ts.map +1 -0
- package/dist/release/fake-release.d.ts +95 -0
- package/dist/release/fake-release.d.ts.map +1 -0
- package/dist/release/index.d.ts +25 -0
- package/dist/release/index.d.ts.map +1 -0
- package/dist/release/index.js +1425 -0
- package/dist/release/index.js.map +36 -0
- package/dist/release/openbao.d.ts +35 -0
- package/dist/release/openbao.d.ts.map +1 -0
- package/dist/release/providers.d.ts +19 -0
- package/dist/release/providers.d.ts.map +1 -0
- package/dist/release/refused.d.ts +39 -0
- package/dist/release/refused.d.ts.map +1 -0
- package/dist/release/tar.d.ts +14 -0
- package/dist/release/tar.d.ts.map +1 -0
- package/dist/release/victoria.d.ts +36 -0
- package/dist/release/victoria.d.ts.map +1 -0
- package/dist/{proxmox → secrets}/write-only.d.ts.map +1 -1
- package/docs/launchd-ports.md +109 -0
- package/docs/launchd.md +5 -0
- package/docs/release-binary-catalogs.md +138 -0
- package/docs/release-binary-measured.md +163 -0
- package/docs/release-binary-openbao.md +151 -0
- package/docs/release-binary-refusals.md +57 -0
- package/docs/release-binary-upstream.md +163 -0
- package/docs/release-binary.md +165 -0
- package/package.json +5 -1
- package/src/caddy/access-forward-auth.test.ts +63 -0
- package/src/caddy/access-forward-auth.ts +120 -0
- package/src/caddy/index.ts +10 -0
- package/src/caddy/providers.test.ts +4 -0
- package/src/cloudflare/mcp-server-api.ts +294 -0
- package/src/cloudflare/mcp-server-form.ts +219 -0
- package/src/index.ts +1 -0
- package/src/launchd/file-converge.ts +233 -0
- package/src/launchd/file-identity.ts +69 -0
- package/src/launchd/host-file-alias.test.ts +65 -0
- package/src/launchd/host-file-form.ts +2 -1
- package/src/launchd/host-file-lifecycle.ts +25 -144
- package/src/launchd/host-file.ts +8 -2
- package/src/launchd/index.ts +2 -0
- package/src/launchd/local-runner.test.ts +12 -1
- package/src/launchd/local-runner.ts +2 -0
- package/src/launchd/port-claims.test.ts +124 -0
- package/src/launchd/port-claims.ts +102 -0
- package/src/launchd/providers.test.ts +3 -0
- package/src/launchd/runner.ts +17 -2
- package/src/launchd/sudo-runner.ts +11 -0
- package/src/linux/sudo-listing.ts +122 -0
- package/src/proxmox/credential-literals.ts +1 -1
- package/src/proxmox/generated/pbs/nodes-node.ts +1 -1
- package/src/proxmox/generated/pbs.ts +7 -0
- package/src/proxmox/generated/pve.ts +7 -0
- package/src/proxmox/index.ts +1 -1
- package/src/proxmox/pbs-notification-target-form.ts +1 -1
- package/src/proxmox/pbs-notification-target-lifecycle.ts +1 -1
- package/src/proxmox/pbs-notification-target-wire.ts +1 -1
- package/src/proxmox/pbs-notification-target.ts +1 -1
- package/src/release/adopt-parity.test.ts +106 -0
- package/src/release/apply-pins.test.ts +128 -0
- package/src/release/archive.ts +94 -0
- package/src/release/binary-alias.test.ts +109 -0
- package/src/release/binary-claim.ts +80 -0
- package/src/release/binary-claims.test.ts +47 -0
- package/src/release/binary-diff.ts +73 -0
- package/src/release/binary-form.ts +229 -0
- package/src/release/binary-lifecycle.test.ts +90 -0
- package/src/release/binary-lifecycle.ts +202 -0
- package/src/release/binary-read.ts +55 -0
- package/src/release/binary-refusals.test.ts +192 -0
- package/src/release/binary-sudo.test.ts +93 -0
- package/src/release/binary.ts +181 -0
- package/src/release/catalog.test.ts +167 -0
- package/src/release/catalog.ts +238 -0
- package/src/release/checksums.test.ts +35 -0
- package/src/release/checksums.ts +43 -0
- package/src/release/declared-pins.ts +39 -0
- package/src/release/download.test.ts +138 -0
- package/src/release/download.ts +110 -0
- package/src/release/error-channel.test.ts +69 -0
- package/src/release/fake-deploy.ts +87 -0
- package/src/release/fake-engine.ts +50 -0
- package/src/release/fake-install.ts +59 -0
- package/src/release/fake-provider.ts +68 -0
- package/src/release/fake-release.ts +185 -0
- package/src/release/fixtures/openbao/checksums.txt +83 -0
- package/src/release/fixtures/openbao/checksums.txt.gpgsig +0 -0
- package/src/release/fixtures/openbao/darwin-assets.txt +12 -0
- package/src/release/fixtures/openbao/openbao-gpg-pub-20240618.asc +109 -0
- package/src/release/fixtures/victoria/darwin-arm64-assets.txt +22 -0
- package/src/release/fixtures/victoria/victoria-logs-darwin-arm64-v1.52.0_checksums.txt +2 -0
- package/src/release/fixtures/victoria/victoria-metrics-darwin-arm64-v1.151.0_checksums.txt +2 -0
- package/src/release/fixtures/victoria/victoria-traces-darwin-arm64-v0.10.0_checksums.txt +2 -0
- package/src/release/fixtures/victoria/vmutils-darwin-arm64-v1.151.0_checksums.txt +8 -0
- package/src/release/index.ts +44 -0
- package/src/release/openbao.test.ts +120 -0
- package/src/release/openbao.ts +86 -0
- package/src/release/plan.test.ts +189 -0
- package/src/release/provider-refusals.test.ts +48 -0
- package/src/release/providers.test.ts +175 -0
- package/src/release/providers.ts +24 -0
- package/src/release/recovery-read.test.ts +44 -0
- package/src/release/refused.ts +40 -0
- package/src/release/tar.test.ts +142 -0
- package/src/release/tar.ts +195 -0
- package/src/release/victoria.test.ts +108 -0
- package/src/release/victoria.ts +154 -0
- package/src/{proxmox → secrets}/write-only.ts +6 -0
- /package/dist/{proxmox → secrets}/write-only.d.ts +0 -0
- /package/src/{proxmox → secrets}/write-only.test.ts +0 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Release binaries — the OpenBao walk-down
|
|
2
|
+
|
|
3
|
+
Status: active
|
|
4
|
+
Verified: 2026-09-23
|
|
5
|
+
|
|
6
|
+
`OPENBAO_RELEASES` (`src/release/openbao.ts`) is the second data set, and the
|
|
7
|
+
first whose checksum file never lists its own binary. Every command below was
|
|
8
|
+
run 2026-09-23 against `openbao/openbao` v2.6.2, from this worktree, network
|
|
9
|
+
reads only — nothing executed the vendor binary.
|
|
10
|
+
|
|
11
|
+
## The release itself
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
gh api repos/openbao/openbao/releases/tags/v2.6.2
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`published_at` 2026-08-18T16:21:00Z, `immutable: false`, 251 assets.
|
|
18
|
+
`openbao_2.6.2_darwin_arm64.tar.gz`: 76,833,570 bytes,
|
|
19
|
+
`digest: sha256:4e495376174accc0e014d31e9901f518a974f966850c839f626347eaac05fd52`.
|
|
20
|
+
|
|
21
|
+
## checksums.txt
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
curl -sL -o checksums.txt \
|
|
25
|
+
https://github.com/openbao/openbao/releases/download/v2.6.2/checksums.txt
|
|
26
|
+
shasum -a 256 checksums.txt
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
8,683 bytes, `sha256 3d4a19fdc54a86fd94ce59ea91316dbf4304180bb27ae818e78e85c7f2164645`
|
|
30
|
+
— equal to GitHub's own `digest` for the asset. 83 lines, every one matching
|
|
31
|
+
`^[0-9a-f]{64} [^\s/]+$` (`checksums.ts`'s parser), LF endings, a trailing
|
|
32
|
+
newline, no name repeated. Line 20 is the darwin_arm64 archive and equals the
|
|
33
|
+
release API's digest. **41 of the 83 lines are `.sbom.json`; the rest are
|
|
34
|
+
archives, `.deb` or `.rpm` — never `bao` itself**: `grep -n ' bao$'
|
|
35
|
+
checksums.txt` finds nothing. That is why the member digest is `computed`,
|
|
36
|
+
never `members` (checksums.ts's header explains the shape generally; this is
|
|
37
|
+
OpenBao's instance of it).
|
|
38
|
+
|
|
39
|
+
## The signature over checksums.txt
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
curl -sL -o checksums.txt.gpgsig \
|
|
43
|
+
https://github.com/openbao/openbao/releases/download/v2.6.2/checksums.txt.gpgsig
|
|
44
|
+
curl -sL -o openbao-gpg-pub-20240618.asc \
|
|
45
|
+
https://openbao.org/assets/openbao-gpg-pub-20240618.asc
|
|
46
|
+
gpg --dearmor -o openbao-pub.gpg openbao-gpg-pub-20240618.asc
|
|
47
|
+
gpgv --keyring ./openbao-pub.gpg checksums.txt.gpgsig checksums.txt
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
gpgv: Signature made Tue Aug 18 11:57:25 2026 EDT
|
|
52
|
+
gpgv: using RSA key E617DCD4065C2AFC0B2CF7A7BA8BC08C0F691F94
|
|
53
|
+
gpgv: Good signature from "OpenBao <openbao@lists.lfedge.org>"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`gpg --show-keys --with-fingerprint` on the same file: primary
|
|
57
|
+
`66D1 5FDD 8728 7219 C8E1 5478 D200 CD70 2853 E6D0`, signing subkey
|
|
58
|
+
`E617 DCD4 065C 2AFC 0B2C F7A7 BA8B C08C 0F69 1F94` — both match OpenBao's own
|
|
59
|
+
`install.mdx` at this tag. `checksums.txt.gpgsig` is 566 bytes, sha256
|
|
60
|
+
`40f294d642a2b4e9adac766ca6b8b2633e15b3dbfdf63969dedbf46d94df53e8`; the key
|
|
61
|
+
file is 6,896 bytes, sha256
|
|
62
|
+
`1862a196422947124282e026ea4e09d6f7c5d383b628d90c17906936e6e5a8e0`.
|
|
63
|
+
|
|
64
|
+
⚠️ **Trust on first use, across two channels one project runs**: the release
|
|
65
|
+
asset (GitHub) and the key (openbao.org) are both OpenBao's own
|
|
66
|
+
infrastructure. A verified signature says the two agree, not that a third
|
|
67
|
+
party attests to either. `cosign` is not installed on the mini (`which
|
|
68
|
+
cosign` — not found), so the Sigstore bundle alongside `checksums.txt` was
|
|
69
|
+
read, not verified: its certificate names
|
|
70
|
+
`https://github.com/openbao/openbao/.github/workflows/release.yml@refs/heads/release/2.6.x`,
|
|
71
|
+
issuer `https://token.actions.githubusercontent.com` — recorded in
|
|
72
|
+
`openbao.ts` as an **unverified** identity. "GPG or Sigstore" is met by the
|
|
73
|
+
GPG half.
|
|
74
|
+
|
|
75
|
+
Attestations, for completeness: `gh api
|
|
76
|
+
repos/openbao/openbao/attestations/sha256:4e49…` → 404. Positive control
|
|
77
|
+
(`gh api repos/cli/cli/attestations/sha256:<its macOS_arm64 digest>`) → 200,
|
|
78
|
+
2 attestations, same day. A 404 here means "none published", not "the API is
|
|
79
|
+
down".
|
|
80
|
+
|
|
81
|
+
## The archive: download, hash, unpack
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
curl -sL -o openbao_2.6.2_darwin_arm64.tar.gz \
|
|
85
|
+
https://github.com/openbao/openbao/releases/download/v2.6.2/openbao_2.6.2_darwin_arm64.tar.gz
|
|
86
|
+
shasum -a 256 openbao_2.6.2_darwin_arm64.tar.gz
|
|
87
|
+
tar -tvzf openbao_2.6.2_darwin_arm64.tar.gz
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
76,833,570 bytes, sha256
|
|
91
|
+
`4e495376174accc0e014d31e9901f518a974f966850c839f626347eaac05fd52` — equal to
|
|
92
|
+
the pin. `tar -tvzf` (full stream, nothing saved beyond the archive itself):
|
|
93
|
+
four root-level regular files, `ustar`, no PAX header, no directory entry —
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
-rw-r--r-- 0 runner runner 131858 CHANGELOG.md
|
|
97
|
+
-rw-r--r-- 0 runner runner 15958 LICENSE
|
|
98
|
+
-rw-r--r-- 0 runner runner 7136 README.md
|
|
99
|
+
-rwxr-xr-x 0 root root 193769394 bao
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The kit's own `extractMembers` (`archive.ts`) read the same stream without
|
|
103
|
+
refusing it and returned the same four names, in the same order — the
|
|
104
|
+
previous partial-range read (`release-binary-catalogs.md`'s queue note) is
|
|
105
|
+
superseded by this full-stream one, so **no tar-reader change is needed for
|
|
106
|
+
this vendor**.
|
|
107
|
+
|
|
108
|
+
## The member digest — computed, not the vendor's
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
tar -xzOf openbao_2.6.2_darwin_arm64.tar.gz bao | shasum -a 256
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`d476d17e81a35e6d70dd7e86a8ab2a3664313525118f1f1cfe0130e7a2b95f3a` — and the
|
|
115
|
+
kit's `extractMembers` over the same bytes produced the identical digest. Two
|
|
116
|
+
independent readers agree, which is what `computed` in `openbao.ts` records,
|
|
117
|
+
dated, next to a header explaining it is not a vendor fact.
|
|
118
|
+
|
|
119
|
+
## The extracted binary, never run
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
tar -xzf openbao_2.6.2_darwin_arm64.tar.gz bao
|
|
123
|
+
chmod -x bao # never made executable in this worktree
|
|
124
|
+
otool -L bao
|
|
125
|
+
codesign -dv --verbose=2 bao
|
|
126
|
+
xattr -l bao
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`otool -L`: `libSystem.B.dylib`, `libresolv.9.dylib`, CoreFoundation,
|
|
130
|
+
Security — all system frameworks, none from `/nix/store` — the same class as
|
|
131
|
+
Victoria's five. `codesign`: `flags=0x20002(adhoc,linker-signed)`, no
|
|
132
|
+
TeamIdentifier — a stock Go/macOS ad-hoc signature, same as the Victoria
|
|
133
|
+
binaries. `xattr -l`: `com.apple.provenance` only, no quarantine. Build
|
|
134
|
+
provenance (`goreleaser.other.yaml` at v2.6.2, read, not re-measured here):
|
|
135
|
+
`CGO_ENABLED=0`, tag `ui` — the same build class the darwin release ships for
|
|
136
|
+
every OpenBao platform.
|
|
137
|
+
|
|
138
|
+
Both scratch copies (`bao`, the `.tar.gz`) were deleted after these commands;
|
|
139
|
+
nothing was executed and nothing was written outside the scratch directory.
|
|
140
|
+
|
|
141
|
+
## What this does NOT establish
|
|
142
|
+
|
|
143
|
+
- **Sigstore identity is read, not cryptographically checked** — `cosign` was
|
|
144
|
+
not run. Treat the workflow identity above as a claim, not a verification.
|
|
145
|
+
- **The agent never ran `bao`.** Whether it starts under launchd, and whether
|
|
146
|
+
its approle auto-auth / template / exec behaviour matches the Nix build
|
|
147
|
+
the vault server runs today, is unmeasured — expected to be unaffected
|
|
148
|
+
(nothing here suggests otherwise), but nobody has watched it happen.
|
|
149
|
+
- **No adoption or install-path measurement.** This PR adds only the catalog
|
|
150
|
+
entry, its tests and this record — no `ReleaseBinary` declaration, no
|
|
151
|
+
consuming stack change.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Release binaries — every refusal
|
|
2
|
+
|
|
3
|
+
Status: active
|
|
4
|
+
Verified: 2026-09-22
|
|
5
|
+
|
|
6
|
+
Everything `Release.Binary` refuses, by where it is refused. The guide is
|
|
7
|
+
[release-binary.md](./release-binary.md). ★ One page per subject: the guide says
|
|
8
|
+
what a declaration does, this says what stops it, and a new refusal lands here.
|
|
9
|
+
|
|
10
|
+
In the stack program (`catalogBinary`): a package, version, platform or
|
|
11
|
+
binary the data set does not pin, including `v1.151.0`, `1.151.0-enterprise`,
|
|
12
|
+
`victoria-metrics-cluster`, `vmalert-prod` and inherited keys like
|
|
13
|
+
`constructor`.
|
|
14
|
+
|
|
15
|
+
At plan (the probe and `diff`), before anything is touched: a pin that is
|
|
16
|
+
missing or not a plain value (an Output pin would be a fetch; a first deploy
|
|
17
|
+
is never diffed, so reconcile refuses it there, before any host call, from the
|
|
18
|
+
props as declared); a repo that is not `owner/name`, or whose name is `.` or
|
|
19
|
+
`..`; a tag, asset or name that is not one safe path segment; an asset that is
|
|
20
|
+
not a `.tar.gz` or `.tgz` (a glob is not a name); a size that is not an
|
|
21
|
+
integer from 1 byte to 1 GiB (it is allocated up front); a digest that is not
|
|
22
|
+
64 lower-case hex; a member that is absolute or climbs. Also a relative or
|
|
23
|
+
unnormalised directory; a mode that is setuid, setgid, sticky, group- or
|
|
24
|
+
world-writable, or not owner-readable and owner-executable; an owner or group
|
|
25
|
+
that is not a valid name or id; a new pin at the same path.
|
|
26
|
+
|
|
27
|
+
⚠️ **Not on a first deploy with `directory: dir.path`.** Alchemy neither
|
|
28
|
+
probes nor diffs a create whose props hold an Output, so there every check
|
|
29
|
+
above runs at apply, after `HostDirectory` has made the directory (measured: a
|
|
30
|
+
group-writable mode planned `create`, then the apply ran `mkdir` and refused).
|
|
31
|
+
Nothing is written to the binary's path. A failed first install never bricks
|
|
32
|
+
the next plan: the recovery read of its row answers "nothing recovered"
|
|
33
|
+
(`binary-read.ts`).
|
|
34
|
+
|
|
35
|
+
At plan, only under `--adopt` (the probe then hands `diff` what it read): a
|
|
36
|
+
file at the path that is not the pinned binary, or not a regular file.
|
|
37
|
+
|
|
38
|
+
At apply, before the download: a second resource installing the same path in
|
|
39
|
+
one deploy (two jobs that run one binary share one `ReleaseBinary`; measured,
|
|
40
|
+
two owners meant dropping either deleted the other's file); a missing
|
|
41
|
+
directory, one that is a symlink, or one that group or other may write; a
|
|
42
|
+
symlink or directory at the path; a file the resource does not own (without
|
|
43
|
+
`--adopt`, even the pinned bytes; with it, anything but the pinned bytes); a
|
|
44
|
+
chown without root.
|
|
45
|
+
|
|
46
|
+
From the vendor: a status other than 200; a body past or short of the pinned
|
|
47
|
+
size; an archive whose SHA-256 is not the pin; an archive entry that is
|
|
48
|
+
absolute, has a `..` segment, is a hard link, symlink, directory, device, FIFO
|
|
49
|
+
or contiguous file, is a GNU long-name or PAX header, is named twice, fails its
|
|
50
|
+
header checksum, uses base-256, is neither ustar nor GNU, a lone zero block,
|
|
51
|
+
data after the end, a truncated archive; the declared member missing; the
|
|
52
|
+
member's SHA-256 not its pin. Every one of these ends `Nothing was written.`
|
|
53
|
+
|
|
54
|
+
After the write: a file that does not read back as declared (a create is
|
|
55
|
+
removed). Errors are tagged (`BinaryRefused`, `DownloadFailed`,
|
|
56
|
+
`ChecksumMismatch`, `ArchiveRefused`) and carry the house refusal sentence.
|
|
57
|
+
⚠️ Tagged, not typed: no caller can `catchTag` one yet (`error-channel.test.ts`).
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Release binaries — what upstream would need
|
|
2
|
+
|
|
3
|
+
Status: active
|
|
4
|
+
Verified: 2026-09-22 (against `alchemy-run/alchemy` at `v2.0.0-beta.79`, read-only:
|
|
5
|
+
AGENTS.md and `scripts/generate-api-reference.ts`, plus the installed
|
|
6
|
+
`src/Command/Exec.ts`, `src/GitHub/Release.ts`, `src/GitHub/BaseUrl.ts`,
|
|
7
|
+
`src/Util/AtomicFile.ts`, `src/Hetzner/Ssh.ts` and `src/Hetzner/Providers.ts`)
|
|
8
|
+
|
|
9
|
+
`Release.Binary` is built so it **could** be contributed to Alchemy. This page
|
|
10
|
+
is the honest gap list: what already fits, and what a pull request to
|
|
11
|
+
`alchemy-run/alchemy` would have to change first. Nothing here is a promise
|
|
12
|
+
that upstream wants it.
|
|
13
|
+
|
|
14
|
+
## Already shaped for it
|
|
15
|
+
|
|
16
|
+
- **Generic before its first release.** It began as `Victoria.Binary` with the
|
|
17
|
+
catalog inside the resource. It was generalised before any state row carried
|
|
18
|
+
that type name, because a rename after release would be a state migration.
|
|
19
|
+
The pinned archive (repo, tag, exact asset name, size, archive digest,
|
|
20
|
+
member, member digest, installed name) is props. Vendor data sets sit beside
|
|
21
|
+
it as plain modules. Alchemy could ship the resource without shipping anyone's
|
|
22
|
+
pins.
|
|
23
|
+
- **Named the upstream way.** The type is `Release.Binary` (`Namespace.Kind`).
|
|
24
|
+
Under upstream's layout it would be `src/Release/Binary.ts`, imported as
|
|
25
|
+
`import * as Release from "alchemy/Release"`, used as `Release.Binary`. The
|
|
26
|
+
house exports the prefixed `ReleaseBinary`, as it does `HostFile` and
|
|
27
|
+
`LaunchdJob`. Upstream's `GitHub.Release` already means _publishing_ a
|
|
28
|
+
release, so the GitHub namespace would be the confusing home.
|
|
29
|
+
- **The lifecycle is beta.79's**: `read`, `diff`, `reconcile`, `delete`,
|
|
30
|
+
`list` returning `[]`. No `create`/`update`. `reconcile` is one flow —
|
|
31
|
+
observe the path, ensure the bytes, sync mode and owner, read back — not a
|
|
32
|
+
create body and an update body. ⚠️ It does branch on `output` and `olds` for
|
|
33
|
+
**ownership**: only a create may take over a file under `--adopt`
|
|
34
|
+
(`file-converge.ts`), and the in-place guard compares `olds`' pins (gap 3).
|
|
35
|
+
- **Props are plain types**, never `Input<T>`, and every prop has JSDoc, with
|
|
36
|
+
`@default` where one applies. ⚠️ Not every attribute: `path`, `mode`, `uid`,
|
|
37
|
+
`gid` and `size` come from `HostFileAttributes` with no JSDoc, so the
|
|
38
|
+
generator would print them bare.
|
|
39
|
+
- **The resource JSDoc is the generator's shape**: `### Installing a binary`,
|
|
40
|
+
`**Example:**` with fenced TypeScript, and `@resource` last. (An untitled
|
|
41
|
+
example is titled "Example" by the generator.) ⚠️ The example needs
|
|
42
|
+
`HostDirectory`, `LaunchdJob` and `VICTORIA_RELEASES`, which upstream lacks.
|
|
43
|
+
- **Errors are tagged** (`Data.TaggedError`: `BinaryRefused`, `DownloadFailed`,
|
|
44
|
+
`ChecksumMismatch`, `ArchiveRefused`), and nothing casts on `_tag`.
|
|
45
|
+
⚠️ **They are not typed end to end, so no caller can `catchTag` one**
|
|
46
|
+
(measured, `error-channel.test.ts`). `catalogBinary()` throws, which in a
|
|
47
|
+
stack program is a defect that `catchTag` never sees. The provider's `lift()`
|
|
48
|
+
keeps the instance at runtime but types the channel as `Error` (gap 10).
|
|
49
|
+
- **HTTP is Effect's `HttpClient`**, and `reconcile` reports progress through
|
|
50
|
+
`session.note`. A provider layer that brings `FetchHttpClient.layer` itself
|
|
51
|
+
has upstream precedent (`Hetzner/Providers.ts`).
|
|
52
|
+
- **The props carry nothing estate-specific.** Paths, owners and modes are the
|
|
53
|
+
stack's. ⚠️ The wiring does carry the estate's name: the host service is
|
|
54
|
+
keyed `homeflare/launchd/HostRunner`, and the ownership notes are
|
|
55
|
+
`Artifacts` keys `homeflare/ownership/*` (gaps 1 and 2).
|
|
56
|
+
|
|
57
|
+
## What would have to change
|
|
58
|
+
|
|
59
|
+
1. **The host seam.** Every host touch goes through the kit's promise-based
|
|
60
|
+
`HostRunner`, a Context service under `launchd/`. Upstream would expect an
|
|
61
|
+
Effect service with a neutral key. ⚠️ `Util/AtomicFile.ts` is not that
|
|
62
|
+
service as it stands: it writes a **string** (`writeFileString`) and a mode,
|
|
63
|
+
never bytes, a uid or a gid. `FileSystem` reaches only the deploying
|
|
64
|
+
machine. Upstream's one remote-host seam is `Hetzner.Ssh` (Effect `exec` and
|
|
65
|
+
`scp`). A contribution needs a host service with a local implementation over
|
|
66
|
+
`FileSystem` and a remote one in that shape. The house seam is one file wide
|
|
67
|
+
on purpose, so the swap is local.
|
|
68
|
+
2. **Ownership semantics.** The house refuses to adopt without `--adopt`:
|
|
69
|
+
`read` returns `Unowned` even for a byte-identical file, and `reconcile`
|
|
70
|
+
resolves adoption at apply through `ownership/adopt.ts`. That reads
|
|
71
|
+
`Stack.resources[fqn].Adopt`, which upstream marks `@internal`, and state
|
|
72
|
+
rows. Upstream's default for a resource without ownership metadata is
|
|
73
|
+
**plain attributes, which means silent adoption**. A contribution would drop
|
|
74
|
+
`adoptsAtApply` / `noteUnfinished`, ask the engine to hand `adopt` to
|
|
75
|
+
`reconcile`, and settle whether a file on disk is ever adopted silently.
|
|
76
|
+
Worth keeping in any form: a file whose digest is not the pin is never
|
|
77
|
+
adopted, because adopting it is overwriting it (`binary-claim.ts`).
|
|
78
|
+
⚠️ `declared-pins.ts` reads the same `@internal` record (`.Props`) to refuse
|
|
79
|
+
a pin that was an Output on a first deploy. Upstream applies `Input<T>` to
|
|
80
|
+
every prop automatically, so "this prop must be a literal" is an engine
|
|
81
|
+
feature to propose (the declared props handed to `reconcile`, or a
|
|
82
|
+
literal-only marker), not something a provider can reach for.
|
|
83
|
+
3. **`diff` does live I/O and returns `noop`.** Upstream's guidance is that a
|
|
84
|
+
diff should almost never return `noop`. Live I/O has precedent, though:
|
|
85
|
+
`Command.Exec`'s diff hashes a directory and answers `noop` on a match, the
|
|
86
|
+
same shape as this one. Two rules are worth keeping in any form: a new pin
|
|
87
|
+
or path → `replace`, even while `directory` is an Output; and a new pin at
|
|
88
|
+
the same path is refused. The apply-time half of that refusal reads `olds`,
|
|
89
|
+
which upstream's reconciler doctrine calls "at most a hint". Restated as
|
|
90
|
+
observation it reads: the recorded path's live file is not these bytes.
|
|
91
|
+
4. **Layout and exports.** Upstream co-locates contract and provider in
|
|
92
|
+
`src/{Cloud}/{Service}/{Resource}.ts`, re-exports with `export *`, and
|
|
93
|
+
registers providers in a `Providers.ts` collection that `alchemy unsafe nuke`
|
|
94
|
+
enumerates. The house splits files under a 250-line cap, curates its barrel,
|
|
95
|
+
and exports `releaseProviders(runner)` with no collection.
|
|
96
|
+
5. **Where the data sets live.** Upstream ships no estate's pins. The resource,
|
|
97
|
+
`catalogBinary()` and the tar reader would go upstream; `victoria.ts` and its
|
|
98
|
+
fixtures would stay in the kit, or become a documented example.
|
|
99
|
+
6. **Tests.** The house tests run on fakes (`bun:test`, no network, no host),
|
|
100
|
+
plus Alchemy's real Plan and Apply over in-memory state (`plan.test.ts`).
|
|
101
|
+
Upstream tests run `pnpm test` against the real target, start and end with
|
|
102
|
+
`stack.destroy()`, and verify out of band. For a host resource the honest
|
|
103
|
+
equivalent is a real temporary directory and a real download. That was done
|
|
104
|
+
once, by hand, before the generalisation (release-binary-measured.md), and
|
|
105
|
+
CI here deliberately never does it.
|
|
106
|
+
7. **Toolchain and style.** pnpm, the alchemy-test runner, `tsc -b`, double
|
|
107
|
+
quotes; generated docs via `pnpm docs:gen`, never hand-edited.
|
|
108
|
+
8. **The pull request itself.** A conventional-commit title; no `#` or `##`
|
|
109
|
+
headings in the body (the smallest is `###`), the summary first with no
|
|
110
|
+
heading, no test plan, and code over prose.
|
|
111
|
+
9. **The directory must already exist.** `reconcile` refuses a missing one, and
|
|
112
|
+
the kit's `HostDirectory` (linux subpath) is what creates it. Upstream has no
|
|
113
|
+
such resource, so a contribution brings one, or a prop that creates the
|
|
114
|
+
directory with a declared mode and owner.
|
|
115
|
+
10. **A typed error channel.** `catalogBinary()` would return an `Effect` that
|
|
116
|
+
fails with `BinaryRefused`, and the provider would fail with the tag union
|
|
117
|
+
instead of `lift()`'s `Error`. `error-channel.test.ts` fails the moment
|
|
118
|
+
either changes, so this page moves with it.
|
|
119
|
+
11. **The tar reader accepts one measured layout.** Measured 2026-09-22 against
|
|
120
|
+
`extractMembers`: macOS's default `tar czf` (bsdtar 3.5.3) writes a PAX `x`
|
|
121
|
+
header before every entry (a sub-second `mtime` and the
|
|
122
|
+
`com.apple.provenance` xattr), so even a one-file archive is refused whole.
|
|
123
|
+
`--no-mac-metadata` still writes them; `--format ustar` passes. A wrapping
|
|
124
|
+
directory entry is refused whole. Go's `archive/tar` writing root-level
|
|
125
|
+
files passes. The house accepts this on purpose: the Victoria archives are
|
|
126
|
+
flat, and homeflare-builds writes plain ustar. ⚠️ REASONED NOT MEASURED:
|
|
127
|
+
the Prometheus family's archives (`alertmanager-0.34.1.darwin-arm64.tar.gz`)
|
|
128
|
+
wrap their files in a directory, so the queue in
|
|
129
|
+
release-binary-catalogs.md needs a reader change, not only a data set.
|
|
130
|
+
Generic replacement: skip directory entries, and links that are not the
|
|
131
|
+
declared member, because nothing is ever written from an entry name; read
|
|
132
|
+
PAX records and refuse only a `path`, `linkpath` or `size` record. The
|
|
133
|
+
member digest still guards the bytes.
|
|
134
|
+
12. **One source, one tag alphabet, other prop names.** `releaseUrl` fixes
|
|
135
|
+
`https://github.com`. Upstream's GitHub family also reaches GitHub
|
|
136
|
+
Enterprise (`normalizeGitHubBaseUrl`, `GitHub/BaseUrl.ts`). A tag must be
|
|
137
|
+
one `[A-Za-z0-9._-]` segment, so kustomize's `kustomize/v5.8.1` (measured:
|
|
138
|
+
GitHub's own download URL carries the raw `/`) and a changesets-style
|
|
139
|
+
`pkg@1.2.3` are refused at plan. Generic: validate git's ref-name rules,
|
|
140
|
+
segment by segment. Upstream's GitHub props split `owner` and `repository`
|
|
141
|
+
and call a tag `tagName`; this takes `repo: "owner/name"` and `tag`.
|
|
142
|
+
13. **No platform check.** `platform` is a catalog key, not a prop, and nothing
|
|
143
|
+
compares the archive's platform with the host. On a Linux runner, a stack
|
|
144
|
+
that asks for `darwin-arm64` gets a Mach-O written, read back and reported
|
|
145
|
+
installed; its unit then fails with "exec format error". Generic: a
|
|
146
|
+
`platform` prop, checked against the host (`uname -sm` through the host
|
|
147
|
+
service) or against the member's executable header.
|
|
148
|
+
|
|
149
|
+
## What should NOT change on the way
|
|
150
|
+
|
|
151
|
+
- ⛔ **Pins in code, never a checksum file fetched at apply.** Mutable releases
|
|
152
|
+
make a fetched checksum file a claim by the same party whose bytes it vouches
|
|
153
|
+
for. A pin that is an Output is refused for the same reason.
|
|
154
|
+
- ⛔ **Two layers.** Verify the archive before unpacking and the member before
|
|
155
|
+
writing. The member check is what still holds if a reader is ever wrong about
|
|
156
|
+
a boundary.
|
|
157
|
+
- ⛔ **Exact names.** An asset and a member are matched whole, never by a
|
|
158
|
+
prefix, a glob or "contains darwin-arm64", because the enterprise and cluster
|
|
159
|
+
siblings share every prefix.
|
|
160
|
+
- ⛔ **Refuse the whole archive** for any unsafe entry, extract only what was
|
|
161
|
+
declared, and stage nothing on disk before the one atomic write. ⚠️ Gap 11 is
|
|
162
|
+
where upstream is likeliest to push back: which entries count as unsafe when
|
|
163
|
+
nothing is written from an entry name.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Release binaries — `@homeflare/alchemy/release`
|
|
2
|
+
|
|
3
|
+
Status: active
|
|
4
|
+
Verified: 2026-09-23
|
|
5
|
+
|
|
6
|
+
`Release.Binary` (`ReleaseBinary`) installs one binary out of a pinned release
|
|
7
|
+
archive, verified twice: the archive against its pinned SHA-256 before anything
|
|
8
|
+
is unpacked, then the binary against its own before it is written. The pinned
|
|
9
|
+
archive is **props**. Each vendor's pinned versions are **data** kept beside
|
|
10
|
+
the resource, and `VICTORIA_RELEASES` is the first data set. It exists to
|
|
11
|
+
replace the Nix-built Victoria binaries on the Mac host: all five Nix builds
|
|
12
|
+
link a `/nix/store` `libresolv`, and the vendor builds link none.
|
|
13
|
+
|
|
14
|
+
⛔ **It installs; it never starts.** No `launchctl`, no `systemctl`, no restart.
|
|
15
|
+
The daemon is the stack's own `LaunchdJob` (or systemd unit), and it puts the
|
|
16
|
+
binary's `path` in its argv.
|
|
17
|
+
|
|
18
|
+
## Declaring one
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { HostDirectory, linuxProviders } from '@homeflare/alchemy/linux';
|
|
22
|
+
import { LaunchdJob, launchdProviders, localRunner } from '@homeflare/alchemy/launchd';
|
|
23
|
+
import {
|
|
24
|
+
ReleaseBinary, VICTORIA_RELEASES, catalogBinary, catalogDirectory, releaseProviders,
|
|
25
|
+
} from '@homeflare/alchemy/release';
|
|
26
|
+
|
|
27
|
+
const runner = localRunner();
|
|
28
|
+
// providers: Layer.mergeAll(launchdProviders(runner), linuxProviders(runner), releaseProviders(runner))
|
|
29
|
+
|
|
30
|
+
const vmalertOf = { package: 'vmutils', version: '1.151.0', platform: 'darwin-arm64', binary: 'vmalert' };
|
|
31
|
+
const dir = yield* HostDirectory('vmutils-1.151.0', {
|
|
32
|
+
path: catalogDirectory('/opt/example/bin', vmalertOf), // …/vmutils-1.151.0
|
|
33
|
+
mode: 0o755, owner: 0, group: 0,
|
|
34
|
+
});
|
|
35
|
+
const vmalert = yield* ReleaseBinary('vmalert', {
|
|
36
|
+
...catalogBinary(VICTORIA_RELEASES, vmalertOf), // archive, member, sha256, name
|
|
37
|
+
directory: dir.path, // ★ an Output: the engine creates the directory first
|
|
38
|
+
owner: 0, group: 0, mode: 0o555,
|
|
39
|
+
});
|
|
40
|
+
yield* LaunchdJob('vmalert-job', { programArguments: [vmalert.path, '--httpListenAddr=127.0.0.1:8880'], … });
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
⚠️ **Give every resource its own logical id, even across different types.**
|
|
44
|
+
alchemy@2.0.0-beta.79's FQN is `namespace + id` alone (`FQN.ts`) — it does
|
|
45
|
+
NOT fold in the resource type. `stack.resources[fqn]` (`Resource.ts`)
|
|
46
|
+
returns the **first** registration for a repeated id whatever type it was:
|
|
47
|
+
`ReleaseBinary('vmalert', …)` then `LaunchdJob('vmalert', …)` in the same
|
|
48
|
+
namespace silently hands the job call back the _binary_ resource, and the
|
|
49
|
+
job is never registered at all — no error, no second row in the plan, just a
|
|
50
|
+
job that does not exist. Measured against the installed alchemy package,
|
|
51
|
+
2026-09-23. Naming them `'vmalert'` and `'vmalert-job'` (as above) avoids it;
|
|
52
|
+
so does any other pair of distinct ids.
|
|
53
|
+
|
|
54
|
+
- **`catalogBinary()` runs in your stack program.** A version, package,
|
|
55
|
+
platform or binary the data set does not pin throws `BinaryRefused` there,
|
|
56
|
+
so `alchemy plan` fails before any resource is asked anything
|
|
57
|
+
(`plan.test.ts`: zero requests, zero host calls).
|
|
58
|
+
- **The directory is yours, one per version.** It must exist.
|
|
59
|
+
`catalogDirectory(root, …)` names it `<root>/<package>-<version>`. A version
|
|
60
|
+
bump is then a new path. Measured through the engine (`plan.test.ts`):
|
|
61
|
+
mkdir new, download, write new, remove old, rmdir old, in that order.
|
|
62
|
+
- ⛔ **A new pin at the same path is refused**, at plan when the path is
|
|
63
|
+
resolved and at apply otherwise, rather than overwriting a binary under the
|
|
64
|
+
daemon that runs it. 🔴 While the directory is still an Output, a re-pin of
|
|
65
|
+
the same bytes is planned as an `update`, never a `replace`: measured through
|
|
66
|
+
the engine, a `replace` onto an unmoved path took the identical file as the
|
|
67
|
+
new generation's, then deleted it with the old one, and reported success.
|
|
68
|
+
- **The job references `vmalert.path`**, not a string it built. That Output
|
|
69
|
+
orders the job after the binary. A version bump moves the path, so the plist
|
|
70
|
+
changes and the job restarts on the new binary before the old one is deleted.
|
|
71
|
+
- **Declare each path once.** vmalert-logs is vmalert with other flags: one
|
|
72
|
+
`ReleaseBinary`, referenced by both jobs.
|
|
73
|
+
- **One logical id per binary, one per versioned directory.** `plan.test.ts`
|
|
74
|
+
declares `ReleaseBinary('vmalert')` and `HostDirectory('vmutils-<version>')`:
|
|
75
|
+
a bump plans the binary as a `replace`, the new directory as a `create` and
|
|
76
|
+
the old one as a `delete`, and the engine removes the old binary before its
|
|
77
|
+
directory.
|
|
78
|
+
- **Nothing estate-specific is in the kit.** Root, owner, group and mode are
|
|
79
|
+
your props. The data sets hold vendor facts only.
|
|
80
|
+
- **One runner for all of it.** Pass the same `HostRunner` to
|
|
81
|
+
`releaseProviders()` as to the jobs. There is no default: a binary goes
|
|
82
|
+
wherever the runner reaches. It provides its own `FetchHttpClient.layer`.
|
|
83
|
+
- **Your own pins work too.** The props are plain data, so a stack may write
|
|
84
|
+
them itself. ⛔ They must be plain values in the stack program: a pin wired
|
|
85
|
+
from another resource's Output (a checksum file read at apply) is refused,
|
|
86
|
+
on a first deploy too (`apply-pins.test.ts`). ⚠️ A stack program that
|
|
87
|
+
fetches the file itself and passes plain strings cannot be told apart from
|
|
88
|
+
a reviewed pin; `catalogBinary()` over a reviewed data set is the path
|
|
89
|
+
that keeps them reviewed.
|
|
90
|
+
|
|
91
|
+
## What reconcile does, in order
|
|
92
|
+
|
|
93
|
+
1. **Validate every prop**, before any host or network call. Every pin must be a
|
|
94
|
+
plain, well-formed value, as the stack program DECLARED it: by now an Output
|
|
95
|
+
has resolved to a plausible string (`declared-pins.ts`).
|
|
96
|
+
2. **Check the directory exists**, is not a symlink, and is not writable by
|
|
97
|
+
group or other (they could rename a file over the binary), before any download.
|
|
98
|
+
3. **Observe the path.** A symlink or directory there is refused. A file this
|
|
99
|
+
resource does not own is refused without `--adopt`, even one holding the
|
|
100
|
+
pinned bytes: at plan the probe calls it `Unowned`, and at apply, when a
|
|
101
|
+
prop was an Output and the probe never ran, reconcile refuses it the same
|
|
102
|
+
way (`binary-claim.ts`). ⛔ A file with other bytes is refused under
|
|
103
|
+
`--adopt` too, at plan and at apply: adopting it would overwrite it in
|
|
104
|
+
place, and the plan would print that as `adopted`. Under `--adopt`, the
|
|
105
|
+
pinned bytes are kept: no download, and a write only to fix mode or owner.
|
|
106
|
+
⚠️ A rename is not an adoption: declare it with `renamedFrom()`. Under
|
|
107
|
+
`--adopt` the new name takes the file, and the old name's delete leaves a
|
|
108
|
+
path another declaration installed in the same deploy (`adopt-parity.test.ts`).
|
|
109
|
+
4. **Download** `https://github.com/<repo>/releases/download/<tag>/<asset>`,
|
|
110
|
+
into memory, never past the pinned size. Two binaries from one archive at
|
|
111
|
+
once share one download.
|
|
112
|
+
5. **Verify the archive** against its pinned SHA-256, before unpacking a byte.
|
|
113
|
+
6. **Unpack only the declared member**, streaming. The tar reader refuses the
|
|
114
|
+
whole archive for any unsafe entry, declared or not.
|
|
115
|
+
7. **Verify the member** against its own pinned SHA-256.
|
|
116
|
+
8. **Write** through `HostRunner.writeFileAtomic`, the one writer every file
|
|
117
|
+
resource uses: a temp file in the same directory, mode and owner set, then
|
|
118
|
+
rename. Under `sudoRunner`, one staged `install -S`, ⚠️ which sets the mode
|
|
119
|
+
just after its rename (measured: `0600` for under half a millisecond).
|
|
120
|
+
9. **Read back** and compare. A create that does not read back, or whose
|
|
121
|
+
read-back fails, is removed.
|
|
122
|
+
|
|
123
|
+
A binary with the right bytes but the wrong mode or owner is re-written from
|
|
124
|
+
the bytes already on disk, re-hashed, without a download.
|
|
125
|
+
|
|
126
|
+
⛔ A directory respelled to the same place (a symlinked parent, or case on
|
|
127
|
+
APFS) is an `update` that keeps the file, never a `replace`: the replace's
|
|
128
|
+
Phase 2 would delete the old path, which is the file (`launchd/file-identity.ts`).
|
|
129
|
+
|
|
130
|
+
## Trust: why the digests are pinned in code
|
|
131
|
+
|
|
132
|
+
★ **Pinned, not fetched at apply.** A checksum file comes from the same
|
|
133
|
+
account, release and channel as the archive, and every release checked so far
|
|
134
|
+
is mutable (`"immutable": false`). Whoever can swap an archive can swap its
|
|
135
|
+
checksum file, and a provider that trusts what it fetches would call the swap
|
|
136
|
+
verified. A pin moves only through a reviewed change, so a swapped asset is a
|
|
137
|
+
refusal. And a plan knows exactly which bytes it will install without the
|
|
138
|
+
network. Each data-set entry records the checksum file's URL, its own SHA-256
|
|
139
|
+
and the date the pins were copied.
|
|
140
|
+
|
|
141
|
+
⚠️ **The trust root.** At apply it is the pins alone: TLS, GitHub's redirect,
|
|
142
|
+
its CDN or a proxy can make a download fail, never make other bytes pass. The
|
|
143
|
+
pins rest on one read of the GitHub release over TLS on the `recorded` date
|
|
144
|
+
(trust on first use), checked against GitHub's own asset `digest`, then on
|
|
145
|
+
review of that commit and the kit release the stack's lockfile resolves.
|
|
146
|
+
VictoriaMetrics publishes no signature, cosign bundle, SLSA provenance or
|
|
147
|
+
GitHub attestation for these archives: a checksum proves integrity, not
|
|
148
|
+
authorship.
|
|
149
|
+
|
|
150
|
+
★ **What CI proves without a network** (`victoria.test.ts`) is consistency,
|
|
151
|
+
not provenance: each committed checksum file in `src/release/fixtures/victoria/`
|
|
152
|
+
hashes to the digest the data set records for it; the strict parser reads
|
|
153
|
+
them; the data set equals what it reads; every asset is in the committed
|
|
154
|
+
listing, and none is an `-enterprise`, `-cluster` or `vlutils` sibling.
|
|
155
|
+
⚠️ That those recorded digests are GitHub's was measured once, not by CI. A
|
|
156
|
+
change that edits a fixture and its pins together passes; review is the gate.
|
|
157
|
+
|
|
158
|
+
## Refusals
|
|
159
|
+
|
|
160
|
+
Every refusal, by where it is refused (the stack program, the plan, the apply,
|
|
161
|
+
the vendor's bytes, the read-back): [release-binary-refusals.md](./release-binary-refusals.md).
|
|
162
|
+
|
|
163
|
+
Data sets and adding a vendor: [release-binary-catalogs.md](./release-binary-catalogs.md).
|
|
164
|
+
Measurements and limits: [release-binary-measured.md](./release-binary-measured.md).
|
|
165
|
+
What alchemy-run/alchemy would need: [release-binary-upstream.md](./release-binary-upstream.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homeflare/alchemy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
|
|
5
5
|
"homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
|
|
6
6
|
"bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
|
|
@@ -64,6 +64,10 @@
|
|
|
64
64
|
"types": "./dist/linux/index.d.ts",
|
|
65
65
|
"default": "./dist/linux/index.js"
|
|
66
66
|
},
|
|
67
|
+
"./release": {
|
|
68
|
+
"types": "./dist/release/index.d.ts",
|
|
69
|
+
"default": "./dist/release/index.js"
|
|
70
|
+
},
|
|
67
71
|
"./caddy": {
|
|
68
72
|
"types": "./dist/caddy/index.d.ts",
|
|
69
73
|
"default": "./dist/caddy/index.js"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { accessForwardAuth, verifierProblems } from './access-forward-auth.ts';
|
|
3
|
+
|
|
4
|
+
describe('accessForwardAuth', () => {
|
|
5
|
+
test('emits a forward_auth block with the identity headers copied', () => {
|
|
6
|
+
const block = accessForwardAuth({
|
|
7
|
+
verifier: '127.0.0.1:9101',
|
|
8
|
+
body: ['reverse_proxy 127.0.0.1:6080'],
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
expect(block).toBe(
|
|
12
|
+
[
|
|
13
|
+
'\treverse_proxy 127.0.0.1:6080',
|
|
14
|
+
'\tforward_auth 127.0.0.1:9101 {',
|
|
15
|
+
'\t\turi /access/verify',
|
|
16
|
+
'\t\tcopy_headers {',
|
|
17
|
+
'\t\t\tX-Access-Email',
|
|
18
|
+
'\t\t\tX-Access-Sub',
|
|
19
|
+
'\t\t}',
|
|
20
|
+
'\t}',
|
|
21
|
+
].join('\n'),
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('copies exactly the headers the verifier sets, by default', () => {
|
|
26
|
+
// ⛔ A header the verifier emits but Caddy does not copy never reaches the upstream;
|
|
27
|
+
// a header Caddy copies but the verifier never sets is the SPOOFING case — the
|
|
28
|
+
// client's own value survives. These two lists must agree, which is why the default
|
|
29
|
+
// is not configurable-by-accident.
|
|
30
|
+
const block = accessForwardAuth({ verifier: '127.0.0.1:9101' });
|
|
31
|
+
expect(block).toContain('X-Access-Email');
|
|
32
|
+
expect(block).toContain('X-Access-Sub');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('refuses a verifier on a routable address', () => {
|
|
36
|
+
// 🔴 A verifier reachable from off-box can be answered around, and the upstream with it.
|
|
37
|
+
expect(() => accessForwardAuth({ verifier: '10.20.10.250:9101' })).toThrow(/loopback/);
|
|
38
|
+
expect(() => accessForwardAuth({ verifier: '0.0.0.0:9101' })).toThrow(/loopback/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('accepts loopback in its usual spellings, and a unix socket', () => {
|
|
42
|
+
for (const verifier of ['127.0.0.1:9101', 'localhost:9101', '[::1]:9101', 'unix//run/v.sock']) {
|
|
43
|
+
expect(verifierProblems(verifier)).toEqual([]);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('a custom uri and header names flow through', () => {
|
|
48
|
+
const block = accessForwardAuth({
|
|
49
|
+
verifier: 'unix//run/access.sock',
|
|
50
|
+
uri: '/verify/vnc',
|
|
51
|
+
copyHeaders: ['Remote-Email'],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(block).toContain('forward_auth unix//run/access.sock {');
|
|
55
|
+
expect(block).toContain('uri /verify/vnc');
|
|
56
|
+
expect(block).toContain('Remote-Email');
|
|
57
|
+
expect(block).not.toContain('X-Access-Sub');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('refuses an empty verifier rather than emitting a block that adapts to nothing', () => {
|
|
61
|
+
expect(() => accessForwardAuth({ verifier: ' ' })).toThrow(/empty/);
|
|
62
|
+
});
|
|
63
|
+
});
|