@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.
Files changed (191) hide show
  1. package/README.md +27 -0
  2. package/dist/caddy/access-forward-auth.d.ts +56 -0
  3. package/dist/caddy/access-forward-auth.d.ts.map +1 -0
  4. package/dist/caddy/index.d.ts +6 -0
  5. package/dist/caddy/index.d.ts.map +1 -1
  6. package/dist/caddy/index.js +169 -75
  7. package/dist/caddy/index.js.map +10 -7
  8. package/dist/cloudflare/mcp-server-api.d.ts +135 -0
  9. package/dist/cloudflare/mcp-server-api.d.ts.map +1 -0
  10. package/dist/cloudflare/mcp-server-form.d.ts +116 -0
  11. package/dist/cloudflare/mcp-server-form.d.ts.map +1 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/launchd/file-converge.d.ts +70 -0
  15. package/dist/launchd/file-converge.d.ts.map +1 -0
  16. package/dist/launchd/file-identity.d.ts +32 -0
  17. package/dist/launchd/file-identity.d.ts.map +1 -0
  18. package/dist/launchd/host-file-form.d.ts +2 -0
  19. package/dist/launchd/host-file-form.d.ts.map +1 -1
  20. package/dist/launchd/host-file-lifecycle.d.ts +11 -24
  21. package/dist/launchd/host-file-lifecycle.d.ts.map +1 -1
  22. package/dist/launchd/host-file.d.ts.map +1 -1
  23. package/dist/launchd/index.d.ts +2 -0
  24. package/dist/launchd/index.d.ts.map +1 -1
  25. package/dist/launchd/index.js +225 -120
  26. package/dist/launchd/index.js.map +13 -10
  27. package/dist/launchd/local-runner.d.ts.map +1 -1
  28. package/dist/launchd/port-claims.d.ts +31 -0
  29. package/dist/launchd/port-claims.d.ts.map +1 -0
  30. package/dist/launchd/runner.d.ts +17 -2
  31. package/dist/launchd/runner.d.ts.map +1 -1
  32. package/dist/launchd/sudo-runner.d.ts +11 -0
  33. package/dist/launchd/sudo-runner.d.ts.map +1 -1
  34. package/dist/linux/index.js.map +3 -3
  35. package/dist/linux/sudo-listing.d.ts +47 -0
  36. package/dist/linux/sudo-listing.d.ts.map +1 -0
  37. package/dist/proxmox/credential-literals.d.ts +1 -1
  38. package/dist/proxmox/credential-literals.d.ts.map +1 -1
  39. package/dist/proxmox/generated/pbs/nodes-node.d.ts +1 -1
  40. package/dist/proxmox/generated/pbs/nodes-node.d.ts.map +1 -1
  41. package/dist/proxmox/generated/pbs.d.ts +7 -0
  42. package/dist/proxmox/generated/pbs.d.ts.map +1 -1
  43. package/dist/proxmox/generated/pve.d.ts +7 -0
  44. package/dist/proxmox/generated/pve.d.ts.map +1 -1
  45. package/dist/proxmox/index.d.ts +1 -1
  46. package/dist/proxmox/index.d.ts.map +1 -1
  47. package/dist/proxmox/index.js +2 -2
  48. package/dist/proxmox/index.js.map +9 -9
  49. package/dist/proxmox/pbs-notification-target-form.d.ts +1 -1
  50. package/dist/proxmox/pbs-notification-target-form.d.ts.map +1 -1
  51. package/dist/proxmox/pbs-notification-target.d.ts +1 -1
  52. package/dist/proxmox/pbs-notification-target.d.ts.map +1 -1
  53. package/dist/release/archive.d.ts +11 -0
  54. package/dist/release/archive.d.ts.map +1 -0
  55. package/dist/release/binary-claim.d.ts +53 -0
  56. package/dist/release/binary-claim.d.ts.map +1 -0
  57. package/dist/release/binary-diff.d.ts +38 -0
  58. package/dist/release/binary-diff.d.ts.map +1 -0
  59. package/dist/release/binary-form.d.ts +118 -0
  60. package/dist/release/binary-form.d.ts.map +1 -0
  61. package/dist/release/binary-lifecycle.d.ts +55 -0
  62. package/dist/release/binary-lifecycle.d.ts.map +1 -0
  63. package/dist/release/binary-read.d.ts +11 -0
  64. package/dist/release/binary-read.d.ts.map +1 -0
  65. package/dist/release/binary.d.ts +68 -0
  66. package/dist/release/binary.d.ts.map +1 -0
  67. package/dist/release/catalog.d.ts +113 -0
  68. package/dist/release/catalog.d.ts.map +1 -0
  69. package/dist/release/checksums.d.ts +22 -0
  70. package/dist/release/checksums.d.ts.map +1 -0
  71. package/dist/release/declared-pins.d.ts +4 -0
  72. package/dist/release/declared-pins.d.ts.map +1 -0
  73. package/dist/release/download.d.ts +44 -0
  74. package/dist/release/download.d.ts.map +1 -0
  75. package/dist/release/fake-deploy.d.ts +48 -0
  76. package/dist/release/fake-deploy.d.ts.map +1 -0
  77. package/dist/release/fake-engine.d.ts +8 -0
  78. package/dist/release/fake-engine.d.ts.map +1 -0
  79. package/dist/release/fake-install.d.ts +39 -0
  80. package/dist/release/fake-install.d.ts.map +1 -0
  81. package/dist/release/fake-provider.d.ts +58 -0
  82. package/dist/release/fake-provider.d.ts.map +1 -0
  83. package/dist/release/fake-release.d.ts +95 -0
  84. package/dist/release/fake-release.d.ts.map +1 -0
  85. package/dist/release/index.d.ts +25 -0
  86. package/dist/release/index.d.ts.map +1 -0
  87. package/dist/release/index.js +1425 -0
  88. package/dist/release/index.js.map +36 -0
  89. package/dist/release/openbao.d.ts +35 -0
  90. package/dist/release/openbao.d.ts.map +1 -0
  91. package/dist/release/providers.d.ts +19 -0
  92. package/dist/release/providers.d.ts.map +1 -0
  93. package/dist/release/refused.d.ts +39 -0
  94. package/dist/release/refused.d.ts.map +1 -0
  95. package/dist/release/tar.d.ts +14 -0
  96. package/dist/release/tar.d.ts.map +1 -0
  97. package/dist/release/victoria.d.ts +36 -0
  98. package/dist/release/victoria.d.ts.map +1 -0
  99. package/dist/{proxmox → secrets}/write-only.d.ts.map +1 -1
  100. package/docs/launchd-ports.md +109 -0
  101. package/docs/launchd.md +5 -0
  102. package/docs/release-binary-catalogs.md +138 -0
  103. package/docs/release-binary-measured.md +163 -0
  104. package/docs/release-binary-openbao.md +151 -0
  105. package/docs/release-binary-refusals.md +57 -0
  106. package/docs/release-binary-upstream.md +163 -0
  107. package/docs/release-binary.md +165 -0
  108. package/package.json +5 -1
  109. package/src/caddy/access-forward-auth.test.ts +63 -0
  110. package/src/caddy/access-forward-auth.ts +120 -0
  111. package/src/caddy/index.ts +10 -0
  112. package/src/caddy/providers.test.ts +4 -0
  113. package/src/cloudflare/mcp-server-api.ts +294 -0
  114. package/src/cloudflare/mcp-server-form.ts +219 -0
  115. package/src/index.ts +1 -0
  116. package/src/launchd/file-converge.ts +233 -0
  117. package/src/launchd/file-identity.ts +69 -0
  118. package/src/launchd/host-file-alias.test.ts +65 -0
  119. package/src/launchd/host-file-form.ts +2 -1
  120. package/src/launchd/host-file-lifecycle.ts +25 -144
  121. package/src/launchd/host-file.ts +8 -2
  122. package/src/launchd/index.ts +2 -0
  123. package/src/launchd/local-runner.test.ts +12 -1
  124. package/src/launchd/local-runner.ts +2 -0
  125. package/src/launchd/port-claims.test.ts +124 -0
  126. package/src/launchd/port-claims.ts +102 -0
  127. package/src/launchd/providers.test.ts +3 -0
  128. package/src/launchd/runner.ts +17 -2
  129. package/src/launchd/sudo-runner.ts +11 -0
  130. package/src/linux/sudo-listing.ts +122 -0
  131. package/src/proxmox/credential-literals.ts +1 -1
  132. package/src/proxmox/generated/pbs/nodes-node.ts +1 -1
  133. package/src/proxmox/generated/pbs.ts +7 -0
  134. package/src/proxmox/generated/pve.ts +7 -0
  135. package/src/proxmox/index.ts +1 -1
  136. package/src/proxmox/pbs-notification-target-form.ts +1 -1
  137. package/src/proxmox/pbs-notification-target-lifecycle.ts +1 -1
  138. package/src/proxmox/pbs-notification-target-wire.ts +1 -1
  139. package/src/proxmox/pbs-notification-target.ts +1 -1
  140. package/src/release/adopt-parity.test.ts +106 -0
  141. package/src/release/apply-pins.test.ts +128 -0
  142. package/src/release/archive.ts +94 -0
  143. package/src/release/binary-alias.test.ts +109 -0
  144. package/src/release/binary-claim.ts +80 -0
  145. package/src/release/binary-claims.test.ts +47 -0
  146. package/src/release/binary-diff.ts +73 -0
  147. package/src/release/binary-form.ts +229 -0
  148. package/src/release/binary-lifecycle.test.ts +90 -0
  149. package/src/release/binary-lifecycle.ts +202 -0
  150. package/src/release/binary-read.ts +55 -0
  151. package/src/release/binary-refusals.test.ts +192 -0
  152. package/src/release/binary-sudo.test.ts +93 -0
  153. package/src/release/binary.ts +181 -0
  154. package/src/release/catalog.test.ts +167 -0
  155. package/src/release/catalog.ts +238 -0
  156. package/src/release/checksums.test.ts +35 -0
  157. package/src/release/checksums.ts +43 -0
  158. package/src/release/declared-pins.ts +39 -0
  159. package/src/release/download.test.ts +138 -0
  160. package/src/release/download.ts +110 -0
  161. package/src/release/error-channel.test.ts +69 -0
  162. package/src/release/fake-deploy.ts +87 -0
  163. package/src/release/fake-engine.ts +50 -0
  164. package/src/release/fake-install.ts +59 -0
  165. package/src/release/fake-provider.ts +68 -0
  166. package/src/release/fake-release.ts +185 -0
  167. package/src/release/fixtures/openbao/checksums.txt +83 -0
  168. package/src/release/fixtures/openbao/checksums.txt.gpgsig +0 -0
  169. package/src/release/fixtures/openbao/darwin-assets.txt +12 -0
  170. package/src/release/fixtures/openbao/openbao-gpg-pub-20240618.asc +109 -0
  171. package/src/release/fixtures/victoria/darwin-arm64-assets.txt +22 -0
  172. package/src/release/fixtures/victoria/victoria-logs-darwin-arm64-v1.52.0_checksums.txt +2 -0
  173. package/src/release/fixtures/victoria/victoria-metrics-darwin-arm64-v1.151.0_checksums.txt +2 -0
  174. package/src/release/fixtures/victoria/victoria-traces-darwin-arm64-v0.10.0_checksums.txt +2 -0
  175. package/src/release/fixtures/victoria/vmutils-darwin-arm64-v1.151.0_checksums.txt +8 -0
  176. package/src/release/index.ts +44 -0
  177. package/src/release/openbao.test.ts +120 -0
  178. package/src/release/openbao.ts +86 -0
  179. package/src/release/plan.test.ts +189 -0
  180. package/src/release/provider-refusals.test.ts +48 -0
  181. package/src/release/providers.test.ts +175 -0
  182. package/src/release/providers.ts +24 -0
  183. package/src/release/recovery-read.test.ts +44 -0
  184. package/src/release/refused.ts +40 -0
  185. package/src/release/tar.test.ts +142 -0
  186. package/src/release/tar.ts +195 -0
  187. package/src/release/victoria.test.ts +108 -0
  188. package/src/release/victoria.ts +154 -0
  189. package/src/{proxmox → secrets}/write-only.ts +6 -0
  190. /package/dist/{proxmox → secrets}/write-only.d.ts +0 -0
  191. /package/src/{proxmox → secrets}/write-only.test.ts +0 -0
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Every refusal of the install lifecycle, each asserted by what it LEFT (fake-install.ts): the
3
+ * transport's request log, the host's write log, and the host's paths before and after.
4
+ */
5
+ import { describe, expect, test } from 'bun:test';
6
+ import { sha256Hex } from '../launchd/job-form.ts';
7
+ import type { ReleaseBinaryProps } from './binary-form.ts';
8
+ import { reconcileBinary } from './binary-lifecycle.ts';
9
+ import {
10
+ BINARY,
11
+ VMUTILS_DIR,
12
+ VMUTILS_ENTRIES,
13
+ VMUTILS_URL,
14
+ bytesOf,
15
+ fakeTransport,
16
+ gzip,
17
+ pathsOn,
18
+ tarOf,
19
+ } from './fake-release.ts';
20
+ import { PATH, refused, setup } from './fake-install.ts';
21
+ import { ArchiveRefused, BinaryRefused, ChecksumMismatch, DownloadFailed } from './refused.ts';
22
+
23
+ describe('refusals leave the host exactly as it was', () => {
24
+ test('a wrong archive hash', async () => {
25
+ const s = setup();
26
+ const tampered = gzip(tarOf(VMUTILS_ENTRIES.map((e) => ({ ...e, bytes: bytesOf('evil') }))));
27
+ const transport = fakeTransport({ [VMUTILS_URL]: tampered });
28
+ const error = await refused(s, () =>
29
+ reconcileBinary(s.fake.runner, transport.fetch, s.props()),
30
+ );
31
+ expect(error).toBeInstanceOf(ChecksumMismatch);
32
+ expect(error.message).toContain(`Release.Binary ${PATH}: ${VMUTILS_URL} hashes to`);
33
+ expect(error.message).toContain('Nothing was written.');
34
+ });
35
+
36
+ test('the right archive with a wrong binary pin', async () => {
37
+ const s = setup();
38
+ const error = await refused(s, () => s.install(s.props({ sha256: 'f'.repeat(64) })));
39
+ expect(error).toBeInstanceOf(ChecksumMismatch);
40
+ expect(error.message).toContain(`member "vmalert-prod" hashes to ${sha256Hex(BINARY.vmalert)}`);
41
+ });
42
+
43
+ test('a tar-slip entry beside the declared member, in a correctly pinned archive', async () => {
44
+ const s = setup([
45
+ ...VMUTILS_ENTRIES,
46
+ { bytes: bytesOf('pwn'), name: '../../../etc/sudoers.d/x' },
47
+ ]);
48
+ const error = await refused(s, () => s.install());
49
+ expect(error).toBeInstanceOf(ArchiveRefused);
50
+ expect(error.message).toContain('climbs out');
51
+ });
52
+
53
+ test('the declared member as a symlink', async () => {
54
+ const s = setup([{ name: 'vmalert-prod', type: '2' }]);
55
+ expect(await refused(s, () => s.install())).toBeInstanceOf(ArchiveRefused);
56
+ });
57
+
58
+ test('the declared member missing from a correctly pinned archive', async () => {
59
+ const s = setup([{ bytes: BINARY.vmagent, name: 'vmagent-prod' }]);
60
+ const error = await refused(s, () => s.install());
61
+ expect(error.message).toContain(
62
+ 'no member named exactly "vmalert-prod" (it has "vmagent-prod")',
63
+ );
64
+ });
65
+
66
+ test('a failed download', async () => {
67
+ const s = setup();
68
+ const error = await refused(s, () =>
69
+ reconcileBinary(s.fake.runner, fakeTransport({}).fetch, s.props()),
70
+ );
71
+ expect(error).toBeInstanceOf(DownloadFailed);
72
+ expect(error.message).toContain(`Release.Binary ${PATH}:`);
73
+ });
74
+
75
+ const archive = (more: object) => (p: ReleaseBinaryProps) => ({
76
+ archive: { ...p.archive, ...more },
77
+ });
78
+ test.each([
79
+ ['an archive digest that is not hex', archive({ sha256: 'abc' })],
80
+ ['an asset given as a glob', archive({ asset: 'vmutils-darwin-arm64-*.tar.gz' })],
81
+ ['an asset that is not a tarball', archive({ asset: 'vmutils-darwin-arm64-v1.151.0.zip' })],
82
+ ['a repo that climbs', archive({ repo: '../VictoriaMetrics' })],
83
+ ['a tag with a slash', archive({ tag: 'v1.151.0/../x' })],
84
+ ['no size', archive({ size: 0 })],
85
+ ['a size past the one-gibibyte ceiling', archive({ size: 2 ** 30 + 1 })],
86
+ ['a repo named ..', archive({ repo: 'VictoriaMetrics/..' })],
87
+ ['an upper-case member digest', () => ({ sha256: 'A'.repeat(64) })],
88
+ ['an absolute member', () => ({ member: '/vmalert-prod' })],
89
+ ['a member that climbs', () => ({ member: '../vmalert-prod' })],
90
+ ['a name with a slash', () => ({ name: 'bin/vmalert' })],
91
+ ['a relative directory', () => ({ directory: 'bin/vmutils-1.151.0' })],
92
+ ['a setuid mode', () => ({ mode: 0o4755 })],
93
+ ['a group-writable mode', () => ({ mode: 0o775 })],
94
+ // Measured 2026-09-22 before the fix: written, then EACCES on the read-back, left with no state.
95
+ ['an execute-only mode its owner cannot re-hash', () => ({ mode: 0o111 })],
96
+ ])('%s: refused before any host call or download', async (_, change) => {
97
+ const s = setup();
98
+ s.fake.calls.length = 0;
99
+ const base = s.props();
100
+ const error = await refused(s, () =>
101
+ s.install({ ...base, ...change(base) } as ReleaseBinaryProps),
102
+ );
103
+ expect(error).toBeInstanceOf(BinaryRefused);
104
+ expect(s.transport.requests).toEqual([]);
105
+ expect(s.fake.calls).toEqual([]);
106
+ });
107
+
108
+ test('a new pin at the same path: refused, the installed binary untouched', async () => {
109
+ const s = setup();
110
+ const olds = s.props();
111
+ const output = await s.install(olds);
112
+ s.transport.requests.length = 0;
113
+ s.fake.calls.length = 0;
114
+ const bumped = s.props({
115
+ archive: { ...olds.archive, tag: 'v1.152.0' },
116
+ sha256: 'e'.repeat(64),
117
+ });
118
+ const error = await refused(s, () => s.install(bumped, { olds, output }));
119
+ expect(error.message).toContain('would overwrite it in place');
120
+ expect(s.transport.requests).toEqual([]);
121
+ expect(s.fake.files.get(PATH)?.bytes).toEqual(BINARY.vmalert);
122
+ });
123
+
124
+ test('a missing directory: refused before the download', async () => {
125
+ const s = setup();
126
+ s.fake.dirs.delete(VMUTILS_DIR);
127
+ const error = await refused(s, () => s.install());
128
+ expect(error.message).toContain(`directory ${VMUTILS_DIR} does not exist`);
129
+ expect(s.transport.requests).toEqual([]);
130
+ });
131
+
132
+ test('⛔ a directory others may write: refused before the download', async () => {
133
+ const s = setup();
134
+ const stat = s.fake.runner.stat;
135
+ s.fake.runner.stat = async (path) => {
136
+ const found = await stat(path);
137
+ return path === VMUTILS_DIR && found !== undefined ? { ...found, mode: 0o777 } : found;
138
+ };
139
+ const error = await refused(s, () => s.install());
140
+ expect(error.message).toContain('is writable by group or other (mode 0777)');
141
+ expect(s.transport.requests).toEqual([]);
142
+ });
143
+
144
+ test('a file this resource does not own, without --adopt: refused before the download', async () => {
145
+ const s = setup();
146
+ s.fake.files.set(PATH, { bytes: bytesOf('theirs'), gid: 0, kind: 'file', mode: 0o755, uid: 0 });
147
+ const error = await refused(s, () => s.install());
148
+ expect(error.message).toContain('already exists and is not this resource');
149
+ expect(s.transport.requests).toEqual([]);
150
+ });
151
+
152
+ // ⛔ binary-claim.ts: with no state, only the pinned binary, and only under --adopt.
153
+ test('the pinned bytes at the path with no state, without --adopt: refused, not claimed', async () => {
154
+ const s = setup();
155
+ s.fake.files.set(PATH, { bytes: BINARY.vmalert, gid: 0, kind: 'file', mode: 0o755, uid: 0 });
156
+ const error = await refused(s, () => s.install());
157
+ expect(error.message).toContain('it holds the pinned binary, but this stack holds no state');
158
+ expect(s.transport.requests).toEqual([]);
159
+ });
160
+
161
+ test('other bytes at the path under --adopt: refused before the download, their file kept', async () => {
162
+ const s = setup();
163
+ const theirs = bytesOf('theirs');
164
+ s.fake.files.set(PATH, { bytes: theirs, gid: 0, kind: 'file', mode: 0o755, uid: 0 });
165
+ const error = await refused(s, () => s.install(s.props(), { adopt: true }));
166
+ expect(error.message).toContain(`a file hashing to ${sha256Hex(theirs)} is there`);
167
+ expect(s.transport.requests).toEqual([]);
168
+ expect(s.fake.files.get(PATH)?.bytes).toEqual(theirs);
169
+ });
170
+
171
+ test('a write that does not read back as declared: the create is rolled back', async () => {
172
+ const s = setup();
173
+ const write = s.fake.runner.writeFileAtomic;
174
+ s.fake.runner.writeFileAtomic = (path, bytes, options) =>
175
+ write(path, bytes, { ...options, mode: 0o700 });
176
+ const before = pathsOn(s.fake);
177
+ await expect(s.install()).rejects.toThrow('does not match the declaration');
178
+ expect(pathsOn(s.fake)).toEqual(before);
179
+ });
180
+
181
+ test('⛔ a read-back that THROWS rolls the create back too, as a refusal', async () => {
182
+ const s = setup();
183
+ const read = s.fake.runner.readFile;
184
+ s.fake.runner.readFile = async (path) => {
185
+ if (s.fake.files.has(path)) throw Object.assign(new Error('EACCES'), { code: 'EACCES' });
186
+ return read(path);
187
+ };
188
+ const before = pathsOn(s.fake);
189
+ await expect(s.install()).rejects.toThrow(`Release.Binary ${PATH}: the write returned but`);
190
+ expect(pathsOn(s.fake)).toEqual(before);
191
+ });
192
+ });
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The install through the kit's REAL sudoRunner over its fake host (launchd/fake-sudo.ts), so what
3
+ * is asserted is the runner's own staging, not a fake's: the verified bytes reach root's directory
4
+ * as one `install -S` of one staged file, and no failure — before the write or during it — leaves a
5
+ * staged file behind.
6
+ *
7
+ * ★ WHY THIS IS THE STAGING TEST. Release.Binary stages nothing itself: the archive and the member
8
+ * live in memory (archive.ts). The only staged file in an install is the runner's — local-runner's
9
+ * same-directory temp file (local-runner.test.ts: "leaves no temp file behind") or, here,
10
+ * sudoRunner's 0600 file in a 0700 directory.
11
+ */
12
+ import { describe, expect, test } from 'bun:test';
13
+ import { fakeSudoHost } from '../launchd/fake-sudo.ts';
14
+ import { INSTALL } from '../launchd/sudo-allowlist.ts';
15
+ import type { ReleaseBinaryProps } from './binary-form.ts';
16
+ import { reconcileBinary } from './binary-lifecycle.ts';
17
+ import {
18
+ BINARY,
19
+ type TarEntry,
20
+ VMUTILS_ENTRIES,
21
+ VMUTILS_URL,
22
+ bytesOf,
23
+ fakeTransport,
24
+ gzip,
25
+ syntheticRelease,
26
+ tarOf,
27
+ vmalertProps,
28
+ } from './fake-release.ts';
29
+
30
+ const DIR = '/opt/example/app/vmutils-1.151.0';
31
+ const PATH = `${DIR}/vmalert`;
32
+
33
+ type Setup = {
34
+ /** What the transport serves at the vmutils URL; undefined serves nothing (a 404). */
35
+ readonly served?: (release: ReturnType<typeof syntheticRelease>) => Uint8Array | undefined;
36
+ readonly entries?: readonly TarEntry[];
37
+ readonly declare?: Partial<ReleaseBinaryProps>;
38
+ };
39
+
40
+ const setup = ({ declare = {}, entries, served = (r) => r.vmutils }: Setup = {}) => {
41
+ const release = syntheticRelease(entries);
42
+ const host = fakeSudoHost();
43
+ host.fake.dirs.set(DIR, 0);
44
+ const bytes = served(release);
45
+ const transport = fakeTransport(bytes === undefined ? {} : { [VMUTILS_URL]: bytes });
46
+ const props = vmalertProps(release.catalog, { directory: DIR, group: 0, owner: 0, ...declare });
47
+ const install = () => reconcileBinary(host.runner, transport.fetch, props);
48
+ return { host, install, transport };
49
+ };
50
+
51
+ describe('Release.Binary through sudoRunner', () => {
52
+ test('one staged file, one exact install, and the staged file removed', async () => {
53
+ const { host, install } = setup();
54
+ const attrs = await install();
55
+ expect(host.privileged()).toEqual([
56
+ [INSTALL, '-S', '-m', '0755', '-o', '0', '-g', '0', expect.any(String), PATH],
57
+ ]);
58
+ expect(host.fake.files.get(PATH)?.bytes).toEqual(BINARY.vmalert);
59
+ expect(attrs).toMatchObject({ gid: 0, mode: 0o755, uid: 0 });
60
+ expect(host.state.stagedCount).toBe(1);
61
+ expect(host.staged.size).toBe(0);
62
+ });
63
+
64
+ test('a failed install throws and leaves no staged file and no binary', async () => {
65
+ const { host, install } = setup();
66
+ host.state.installFailure = { exitCode: 71, stderr: 'install: disk full', stdout: '' };
67
+ await expect(install()).rejects.toThrow('disk full');
68
+ expect(host.state.stagedCount).toBe(1);
69
+ expect(host.staged.size).toBe(0);
70
+ expect(host.fake.files.has(PATH)).toBe(false);
71
+ });
72
+
73
+ // ⛔ EVERY failure before the write: nothing is staged at all, sudo is never asked, no binary.
74
+ const tampered = () =>
75
+ gzip(tarOf(VMUTILS_ENTRIES.map((e) => ({ ...e, bytes: bytesOf('evil') }))));
76
+ test.each<[string, Setup]>([
77
+ ['a wrong archive hash', { served: tampered }],
78
+ ['a wrong binary hash', { declare: { sha256: 'f'.repeat(64) } }],
79
+ ['a tar-slip entry', { entries: [...VMUTILS_ENTRIES, { bytes: bytesOf('x'), name: '../x' }] }],
80
+ ['the member as a symlink', { entries: [{ name: 'vmalert-prod', type: '2' }] }],
81
+ ['the member missing', { entries: [{ bytes: BINARY.vmagent, name: 'vmagent-prod' }] }],
82
+ ['a failed download', { served: () => undefined }],
83
+ ['a malformed pin', { declare: { sha256: 'not-a-digest' } }],
84
+ ['an unwritable mode', { declare: { mode: 0o777 } }],
85
+ ])('%s stages nothing, asks sudo nothing, writes nothing', async (_, how) => {
86
+ const { host, install } = setup(how);
87
+ await expect(install()).rejects.toThrow();
88
+ expect(host.state.stagedCount).toBe(0);
89
+ expect(host.staged.size).toBe(0);
90
+ expect(host.sudoCalls).toEqual([]);
91
+ expect(host.fake.files.has(PATH)).toBe(false);
92
+ });
93
+ });
@@ -0,0 +1,181 @@
1
+ /**
2
+ * `Release.Binary` — one binary out of a pinned release archive, written onto a host and verified
3
+ * twice: the archive against its pinned SHA-256, then the binary against its own.
4
+ *
5
+ * - reconcile — validate every prop; download the archive by its exact URL; verify it; unpack ONLY
6
+ * the declared member; verify that; write it atomically through the HostRunner; read it back. A
7
+ * file already holding the pinned bytes is never downloaded again.
8
+ * - read — lstat plus the SHA-256 of the bytes; never runs the binary.
9
+ * - diff — the pins, the path, then mode and owner against the live file (binary-diff.ts).
10
+ * - replace — when the path moves (a new version is a new directory): create-before-delete.
11
+ * - delete — removes the file, never the directory.
12
+ *
13
+ * ★ VENDOR-NEUTRAL. The pinned archive arrives as props, usually from a data set beside this
14
+ * (victoria.ts) through `catalogBinary()`; nothing here knows a vendor, a package or a version.
15
+ * ⛔ IT INSTALLS; IT NEVER STARTS. No launchctl, no systemctl, no restart: the job that runs the
16
+ * binary is the stack's own `LaunchdJob`, which puts this resource's `path` in its argv so the
17
+ * engine installs first and restarts the job when the path moves.
18
+ * ⚠️ NOTHING IS ADOPTED WITHOUT `--adopt`, AT PLAN OR AT APPLY: the probe reads a file already at
19
+ * the path as `Unowned` — even one holding exactly the pinned bytes (docs/ownership.md) — and
20
+ * where the probe never ran (a prop was an Output) reconcile refuses it the same way. With
21
+ * `--adopt`, a file holding the pinned bytes is recognised by its digest and taken over without
22
+ * a download. ⛔ A file with OTHER bytes is never adopted, `--adopt` or not: that would be an
23
+ * overwrite in place that the plan prints as `adopted` (binary-claim.ts, both halves).
24
+ */
25
+ import { Resource } from 'alchemy';
26
+ import * as Provider from 'alchemy/Provider';
27
+ import * as Effect from 'effect/Effect';
28
+ import * as HttpClient from 'effect/unstable/http/HttpClient';
29
+ import { lift } from '../launchd/host-effect.ts';
30
+ import { HostRunnerService } from '../launchd/runner.ts';
31
+ import { adoptsAtApply } from '../ownership/adopt.ts';
32
+ import { noteUnfinished } from '../ownership/resume.ts';
33
+ import { diffBinary } from './binary-diff.ts';
34
+ import {
35
+ type ReleaseBinaryAttributes,
36
+ type ReleaseBinaryProps,
37
+ releaseBinaryPath,
38
+ } from './binary-form.ts';
39
+ import {
40
+ NOTHING,
41
+ deleteBinary,
42
+ reconcileBinary,
43
+ refreshBinary,
44
+ refuse,
45
+ } from './binary-lifecycle.ts';
46
+ import { readWithoutState } from './binary-read.ts';
47
+ import { declaredPinProblems } from './declared-pins.ts';
48
+ import { type DownloadPolicy, httpFetchArchive, sharingInFlight } from './download.ts';
49
+
50
+ export type { ReleaseBinaryAttributes, ReleaseBinaryProps } from './binary-form.ts';
51
+
52
+ export interface ReleaseBinary extends Resource<
53
+ 'Release.Binary',
54
+ ReleaseBinaryProps,
55
+ ReleaseBinaryAttributes
56
+ > {}
57
+
58
+ /**
59
+ * One binary from a pinned release archive, installed onto a host and verified against digests
60
+ * the stack pins in code — the archive's, then the binary's own.
61
+ *
62
+ * ### Installing a binary
63
+ *
64
+ * The directory is declared first and passed by its `path`, which orders the two; the job that
65
+ * runs the binary references the binary's `path`, which orders the job after it.
66
+ *
67
+ * **Example:**
68
+ *
69
+ * ```ts
70
+ * const vmalertOf = { package: 'vmutils', version: '1.151.0', platform: 'darwin-arm64', binary: 'vmalert' };
71
+ * const dir = yield* HostDirectory('vmutils-1.151.0', {
72
+ * path: catalogDirectory('/opt/example/bin', vmalertOf),
73
+ * mode: 0o755,
74
+ * });
75
+ * const vmalert = yield* ReleaseBinary('vmalert', {
76
+ * ...catalogBinary(VICTORIA_RELEASES, vmalertOf),
77
+ * directory: dir.path,
78
+ * });
79
+ * // LaunchdJob(…, { programArguments: [vmalert.path, '--httpListenAddr=127.0.0.1:8880'] })
80
+ * ```
81
+ *
82
+ * @resource
83
+ */
84
+ export const ReleaseBinary = Resource<ReleaseBinary>('Release.Binary');
85
+
86
+ /** Test seams — NOT on the barrel. Every caller outside tests uses the defaults. */
87
+ export type ReleaseBinaryInternals = { readonly download?: DownloadPolicy };
88
+
89
+ export const makeReleaseBinaryProvider = (internals: ReleaseBinaryInternals = {}) =>
90
+ Provider.effect(
91
+ ReleaseBinary,
92
+ Effect.gen(function* () {
93
+ const runner = yield* HostRunnerService;
94
+ const client = yield* HttpClient.HttpClient;
95
+ const fetch = sharingInFlight(httpFetchArchive(client, internals.download));
96
+ /**
97
+ * ⛔ ONE PATH, ONE DECLARATION. MEASURED 2026-09-22 (binary-claims.test.ts, before this): two
98
+ * resources installing one path in one deploy both planned `create`, shared one download,
99
+ * and both recorded the file as theirs. Dropping either later — gating vmalert-logs off
100
+ * along with its own ReleaseBinary, say — deleted the file the other still declared, and
101
+ * that deploy reported the survivor `noop`. So a second resource claiming a path in the
102
+ * same run is refused before it touches anything. Two jobs that run one binary share ONE
103
+ * ReleaseBinary. ⚠️ Keyed by spelling: one file under two spellings is not caught here.
104
+ */
105
+ const claims = new Map<string, string>();
106
+ const claim = (fqn: string, props: ReleaseBinaryProps): void => {
107
+ const path = releaseBinaryPath(props);
108
+ const holder = claims.get(path);
109
+ if (holder !== undefined && holder !== fqn) {
110
+ throw refuse(
111
+ path,
112
+ `is already installed by ${holder} in this deploy; declare each binary path once, ` +
113
+ `and let every job that runs it share that one resource. ${NOTHING}`,
114
+ );
115
+ }
116
+ claims.set(path, fqn);
117
+ };
118
+ return ReleaseBinary.Provider.of({
119
+ list: () => Effect.succeed([]),
120
+
121
+ // ⛔ The probe (no `output`) validates every prop first, so a malformed pin fails the plan
122
+ // here — before any host call, let alone a download. A recovery read never fails its
123
+ // caller (binary-read.ts): its props are the failed deploy's.
124
+ read: ({ fqn, instanceId, olds, output }) =>
125
+ output === undefined
126
+ ? readWithoutState(runner, { fqn, instanceId, olds })
127
+ : lift(() => refreshBinary(runner, output)),
128
+
129
+ diff: ({ instanceId, news, olds, output }) =>
130
+ output === undefined
131
+ ? noteUnfinished(instanceId)
132
+ : lift(() => diffBinary(runner, news, olds, output)),
133
+
134
+ // ★ `--adopt` reaches the apply too: the probe never ran for a create with an Output prop.
135
+ // ⛔ So do the pins AS DECLARED: `news` is resolved by now, and a first deploy is never
136
+ // diffed, so only the stack's own record can show a digest that was an Output.
137
+ reconcile: ({ fqn, instanceId, news, olds, output, session }) =>
138
+ Effect.gen(function* () {
139
+ const declared = yield* declaredPinProblems(fqn);
140
+ const adopt = yield* adoptsAtApply({ fqn, instanceId, output });
141
+ return yield* lift(async () => {
142
+ claim(fqn, news);
143
+ return reconcileBinary(runner, fetch, news, {
144
+ adopt,
145
+ declared,
146
+ note: (message) => Effect.runPromise(session.note(message)),
147
+ olds,
148
+ output,
149
+ });
150
+ });
151
+ }),
152
+
153
+ /**
154
+ * ⛔ NEVER REMOVE A FILE ANOTHER DECLARATION INSTALLED IN THIS DEPLOY. Alchemy runs every
155
+ * delete (Apply.ts collectGarbage, Phase 2) only after every reconcile has succeeded, so a
156
+ * claim by another name means the file is that resource's now, verified as its pin.
157
+ * 🔴 MEASURED 2026-09-22 (adopt-parity.test.ts): a rename under --adopt — the new name
158
+ * claimed the file, the old name's orphan delete removed it, and the deploy succeeded
159
+ * with the binary gone. ★ Left in place rather than removed: a stray file is a tidy-up, a
160
+ * missing binary is an outage. ⚠️ Keyed by spelling, like the claims themselves.
161
+ * ★ A deleted resource gives its claim back, so a provider that outlives one deploy never
162
+ * refuses the next resource declared at that path.
163
+ */
164
+ delete: ({ fqn, output, session }) =>
165
+ lift(async () => {
166
+ const holder = claims.get(output.path);
167
+ if (holder !== undefined && holder !== fqn) {
168
+ await Effect.runPromise(
169
+ session.note(`${output.path} was installed by ${holder} in this deploy; kept`),
170
+ );
171
+ return;
172
+ }
173
+ await deleteBinary(runner, output);
174
+ if (holder === fqn) claims.delete(output.path);
175
+ }),
176
+ });
177
+ }),
178
+ );
179
+
180
+ /** The provider, as a stack uses it. It needs a HostRunnerService and an HttpClient. */
181
+ export const ReleaseBinaryProvider = () => makeReleaseBinaryProvider();
@@ -0,0 +1,167 @@
1
+ /**
2
+ * A catalog request → pinned props, or a refusal — pure, over the REAL Victoria data set. Every
3
+ * refusal here happens in the stack's own program, so it fails the plan before any resource runs
4
+ * (plan.test.ts proves that through the engine).
5
+ */
6
+ import { describe, expect, test } from 'bun:test';
7
+ import { binaryProblems } from './binary-form.ts';
8
+ import type { ReleaseCatalog } from './catalog.ts';
9
+ import { catalogBinary, catalogDirectory, catalogProblems, identifyBinary } from './catalog.ts';
10
+ import { OPENBAO_RELEASES } from './openbao.ts';
11
+ import { BinaryRefused } from './refused.ts';
12
+ import { VICTORIA_RELEASES } from './victoria.ts';
13
+
14
+ const VMALERT = {
15
+ binary: 'vmalert',
16
+ package: 'vmutils',
17
+ platform: 'darwin-arm64',
18
+ version: '1.151.0',
19
+ };
20
+
21
+ describe('refusing what the catalog does not pin', () => {
22
+ test.each([
23
+ [
24
+ 'a version the catalog does not pin',
25
+ { version: '1.152.0' },
26
+ 'vmutils 1.152.0 is not in the catalog',
27
+ ],
28
+ ['the tag spelling', { version: 'v1.151.0' }, 'is not in the catalog'],
29
+ ['an enterprise version', { version: '1.151.0-enterprise' }, 'is not in the catalog'],
30
+ [
31
+ 'a cluster package',
32
+ { package: 'victoria-metrics-cluster' },
33
+ 'package "victoria-metrics-cluster"',
34
+ ],
35
+ ['the vendor member name', { binary: 'vmalert-prod' }, 'that is the archive member'],
36
+ ['a binary vmutils does not ship', { binary: 'vminsert' }, 'has no binary "vminsert"'],
37
+ ['another platform', { platform: 'linux-amd64' }, 'not pinned for platform "linux-amd64"'],
38
+ ['an inherited key', { package: 'constructor' }, 'package "constructor"'],
39
+ ])('refuses %s', (_, change, message) => {
40
+ const request = { ...VMALERT, ...change };
41
+ expect(catalogProblems(VICTORIA_RELEASES, request).join('; ')).toContain(message);
42
+ expect(() => catalogBinary(VICTORIA_RELEASES, request)).toThrow(message);
43
+ });
44
+
45
+ test('the refusal is typed, names the binary, and says nothing was fetched', () => {
46
+ let caught: unknown;
47
+ try {
48
+ catalogBinary(VICTORIA_RELEASES, { ...VMALERT, version: '1.152.0' });
49
+ } catch (cause) {
50
+ caught = cause;
51
+ }
52
+ expect(caught).toBeInstanceOf(BinaryRefused);
53
+ expect((caught as Error).message).toMatch(
54
+ /^Release\.Binary vmalert: .* Nothing was fetched\.$/,
55
+ );
56
+ });
57
+ });
58
+
59
+ describe('what a pinned request becomes', () => {
60
+ test('props that validate once the stack adds a directory', () => {
61
+ const pinned = catalogBinary(VICTORIA_RELEASES, VMALERT);
62
+ const directory = catalogDirectory('/opt/example/bin', VMALERT);
63
+ expect(directory).toBe('/opt/example/bin/vmutils-1.151.0');
64
+ expect(binaryProblems({ ...pinned, directory })).toEqual([]);
65
+ });
66
+
67
+ test('every binary of every pinned archive resolves to valid props', () => {
68
+ for (const [pkg, entry] of Object.entries(VICTORIA_RELEASES.packages)) {
69
+ for (const [version, platforms] of Object.entries(entry.versions)) {
70
+ for (const platform of Object.keys(platforms)) {
71
+ for (const binary of Object.keys(entry.binaries)) {
72
+ const request = { binary, package: pkg, platform, version };
73
+ const props = { ...catalogBinary(VICTORIA_RELEASES, request), directory: '/x/y' };
74
+ expect(binaryProblems(props)).toEqual([]);
75
+ }
76
+ }
77
+ }
78
+ }
79
+ });
80
+ });
81
+
82
+ describe('identifying an installed file', () => {
83
+ test('by its digest alone', () => {
84
+ const pin = '4759ec89a466129b9eefc22244a111e1f489739340747805fd93b58fd0f7b2dd';
85
+ expect(identifyBinary([VICTORIA_RELEASES], pin)).toEqual({
86
+ binary: 'victoria-traces',
87
+ package: 'victoria-traces',
88
+ platform: 'darwin-arm64',
89
+ vendor: 'VictoriaMetrics',
90
+ version: '0.10.0',
91
+ });
92
+ });
93
+
94
+ test("an archive's digest is not a binary's: identity is by the member's own line", () => {
95
+ const archive = 'cbe83f1d409cbb85fbf1c890c4a9b7fd34c1d74acec4dce2e0ebe46fcb260e38';
96
+ expect(identifyBinary([VICTORIA_RELEASES], archive)).toBeUndefined();
97
+ });
98
+
99
+ test('by a COMPUTED digest, same as a vendor one — OpenBao pins bao only there', () => {
100
+ const computed = 'd476d17e81a35e6d70dd7e86a8ab2a3664313525118f1f1cfe0130e7a2b95f3a';
101
+ expect(identifyBinary([VICTORIA_RELEASES, OPENBAO_RELEASES], computed)).toEqual({
102
+ binary: 'bao',
103
+ package: 'openbao',
104
+ platform: 'darwin_arm64',
105
+ vendor: 'OpenBao',
106
+ version: '2.6.2',
107
+ });
108
+ });
109
+ });
110
+
111
+ /** A fabricated one-entry catalog, only to exercise the `computed`/`members` overlap refusal. */
112
+ const doubled = (extra: {
113
+ members?: Record<string, string>;
114
+ computed?: Record<string, { sha256: string; recorded: string }>;
115
+ }): ReleaseCatalog => ({
116
+ packages: {
117
+ thing: {
118
+ binaries: { thing: 'thing-bin' },
119
+ versions: {
120
+ '1.0.0': {
121
+ any: {
122
+ asset: 'thing-1.0.0.tar.gz',
123
+ checksums: {
124
+ recorded: '2026-09-23',
125
+ sha256: 'a'.repeat(64),
126
+ url: 'https://example.invalid/checksums.txt',
127
+ },
128
+ // ⚠️ Spread, not a plain key: `exactOptionalPropertyTypes` treats an explicit
129
+ // `computed: undefined` as different from the key being absent.
130
+ ...(extra.computed !== undefined ? { computed: extra.computed } : {}),
131
+ members: extra.members ?? {},
132
+ repo: 'example/thing',
133
+ sha256: 'b'.repeat(64),
134
+ size: 1,
135
+ tag: 'v1.0.0',
136
+ },
137
+ },
138
+ },
139
+ },
140
+ },
141
+ vendor: 'Example',
142
+ });
143
+
144
+ const THING = { binary: 'thing', package: 'thing', platform: 'any', version: '1.0.0' };
145
+
146
+ describe('a member pinned twice, once vendor and once computed', () => {
147
+ test('catalogProblems names it, rather than silently preferring one', () => {
148
+ const catalog = doubled({
149
+ computed: { 'thing-bin': { recorded: '2026-09-23', sha256: 'c'.repeat(64) } },
150
+ members: { 'thing-bin': 'c'.repeat(64) },
151
+ });
152
+ const problems = catalogProblems(catalog, THING);
153
+ expect(problems.join('; ')).toContain('pinned twice');
154
+ expect(() => catalogBinary(catalog, THING)).toThrow('pinned twice');
155
+ });
156
+
157
+ test('computed alone resolves; vendor alone resolves; neither refuses "no digest"', () => {
158
+ const computedOnly = doubled({
159
+ computed: { 'thing-bin': { recorded: '2026-09-23', sha256: 'c'.repeat(64) } },
160
+ });
161
+ expect(catalogBinary(computedOnly, THING).sha256).toBe('c'.repeat(64));
162
+ const vendorOnly = doubled({ members: { 'thing-bin': 'd'.repeat(64) } });
163
+ expect(catalogBinary(vendorOnly, THING).sha256).toBe('d'.repeat(64));
164
+ const neither = doubled({});
165
+ expect(() => catalogBinary(neither, THING)).toThrow('pins no digest');
166
+ });
167
+ });