@homeflare/alchemy 0.20.0 → 0.21.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 (172) hide show
  1. package/README.md +25 -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.js +173 -120
  24. package/dist/launchd/index.js.map +12 -10
  25. package/dist/launchd/local-runner.d.ts.map +1 -1
  26. package/dist/launchd/runner.d.ts +17 -2
  27. package/dist/launchd/runner.d.ts.map +1 -1
  28. package/dist/launchd/sudo-runner.d.ts +11 -0
  29. package/dist/launchd/sudo-runner.d.ts.map +1 -1
  30. package/dist/linux/index.js.map +3 -3
  31. package/dist/linux/sudo-listing.d.ts +47 -0
  32. package/dist/linux/sudo-listing.d.ts.map +1 -0
  33. package/dist/proxmox/credential-literals.d.ts +1 -1
  34. package/dist/proxmox/credential-literals.d.ts.map +1 -1
  35. package/dist/proxmox/generated/pbs/nodes-node.d.ts +1 -1
  36. package/dist/proxmox/generated/pbs/nodes-node.d.ts.map +1 -1
  37. package/dist/proxmox/generated/pbs.d.ts +7 -0
  38. package/dist/proxmox/generated/pbs.d.ts.map +1 -1
  39. package/dist/proxmox/generated/pve.d.ts +7 -0
  40. package/dist/proxmox/generated/pve.d.ts.map +1 -1
  41. package/dist/proxmox/index.d.ts +1 -1
  42. package/dist/proxmox/index.d.ts.map +1 -1
  43. package/dist/proxmox/index.js +2 -2
  44. package/dist/proxmox/index.js.map +9 -9
  45. package/dist/proxmox/pbs-notification-target-form.d.ts +1 -1
  46. package/dist/proxmox/pbs-notification-target-form.d.ts.map +1 -1
  47. package/dist/proxmox/pbs-notification-target.d.ts +1 -1
  48. package/dist/proxmox/pbs-notification-target.d.ts.map +1 -1
  49. package/dist/release/archive.d.ts +11 -0
  50. package/dist/release/archive.d.ts.map +1 -0
  51. package/dist/release/binary-claim.d.ts +53 -0
  52. package/dist/release/binary-claim.d.ts.map +1 -0
  53. package/dist/release/binary-diff.d.ts +38 -0
  54. package/dist/release/binary-diff.d.ts.map +1 -0
  55. package/dist/release/binary-form.d.ts +118 -0
  56. package/dist/release/binary-form.d.ts.map +1 -0
  57. package/dist/release/binary-lifecycle.d.ts +55 -0
  58. package/dist/release/binary-lifecycle.d.ts.map +1 -0
  59. package/dist/release/binary-read.d.ts +11 -0
  60. package/dist/release/binary-read.d.ts.map +1 -0
  61. package/dist/release/binary.d.ts +68 -0
  62. package/dist/release/binary.d.ts.map +1 -0
  63. package/dist/release/catalog.d.ts +76 -0
  64. package/dist/release/catalog.d.ts.map +1 -0
  65. package/dist/release/checksums.d.ts +22 -0
  66. package/dist/release/checksums.d.ts.map +1 -0
  67. package/dist/release/declared-pins.d.ts +4 -0
  68. package/dist/release/declared-pins.d.ts.map +1 -0
  69. package/dist/release/download.d.ts +44 -0
  70. package/dist/release/download.d.ts.map +1 -0
  71. package/dist/release/fake-deploy.d.ts +48 -0
  72. package/dist/release/fake-deploy.d.ts.map +1 -0
  73. package/dist/release/fake-engine.d.ts +8 -0
  74. package/dist/release/fake-engine.d.ts.map +1 -0
  75. package/dist/release/fake-install.d.ts +39 -0
  76. package/dist/release/fake-install.d.ts.map +1 -0
  77. package/dist/release/fake-provider.d.ts +58 -0
  78. package/dist/release/fake-provider.d.ts.map +1 -0
  79. package/dist/release/fake-release.d.ts +95 -0
  80. package/dist/release/fake-release.d.ts.map +1 -0
  81. package/dist/release/index.d.ts +24 -0
  82. package/dist/release/index.d.ts.map +1 -0
  83. package/dist/release/index.js +1365 -0
  84. package/dist/release/index.js.map +35 -0
  85. package/dist/release/providers.d.ts +19 -0
  86. package/dist/release/providers.d.ts.map +1 -0
  87. package/dist/release/refused.d.ts +39 -0
  88. package/dist/release/refused.d.ts.map +1 -0
  89. package/dist/release/tar.d.ts +14 -0
  90. package/dist/release/tar.d.ts.map +1 -0
  91. package/dist/release/victoria.d.ts +36 -0
  92. package/dist/release/victoria.d.ts.map +1 -0
  93. package/dist/{proxmox → secrets}/write-only.d.ts.map +1 -1
  94. package/docs/release-binary-catalogs.md +113 -0
  95. package/docs/release-binary-measured.md +163 -0
  96. package/docs/release-binary-refusals.md +57 -0
  97. package/docs/release-binary-upstream.md +163 -0
  98. package/docs/release-binary.md +154 -0
  99. package/package.json +5 -1
  100. package/src/caddy/access-forward-auth.test.ts +63 -0
  101. package/src/caddy/access-forward-auth.ts +120 -0
  102. package/src/caddy/index.ts +10 -0
  103. package/src/caddy/providers.test.ts +4 -0
  104. package/src/cloudflare/mcp-server-api.ts +294 -0
  105. package/src/cloudflare/mcp-server-form.ts +219 -0
  106. package/src/index.ts +1 -0
  107. package/src/launchd/file-converge.ts +233 -0
  108. package/src/launchd/file-identity.ts +69 -0
  109. package/src/launchd/host-file-alias.test.ts +65 -0
  110. package/src/launchd/host-file-form.ts +2 -1
  111. package/src/launchd/host-file-lifecycle.ts +25 -144
  112. package/src/launchd/host-file.ts +8 -2
  113. package/src/launchd/local-runner.test.ts +12 -1
  114. package/src/launchd/local-runner.ts +2 -0
  115. package/src/launchd/runner.ts +17 -2
  116. package/src/launchd/sudo-runner.ts +11 -0
  117. package/src/linux/sudo-listing.ts +122 -0
  118. package/src/proxmox/credential-literals.ts +1 -1
  119. package/src/proxmox/generated/pbs/nodes-node.ts +1 -1
  120. package/src/proxmox/generated/pbs.ts +7 -0
  121. package/src/proxmox/generated/pve.ts +7 -0
  122. package/src/proxmox/index.ts +1 -1
  123. package/src/proxmox/pbs-notification-target-form.ts +1 -1
  124. package/src/proxmox/pbs-notification-target-lifecycle.ts +1 -1
  125. package/src/proxmox/pbs-notification-target-wire.ts +1 -1
  126. package/src/proxmox/pbs-notification-target.ts +1 -1
  127. package/src/release/adopt-parity.test.ts +106 -0
  128. package/src/release/apply-pins.test.ts +128 -0
  129. package/src/release/archive.ts +94 -0
  130. package/src/release/binary-alias.test.ts +109 -0
  131. package/src/release/binary-claim.ts +80 -0
  132. package/src/release/binary-claims.test.ts +47 -0
  133. package/src/release/binary-diff.ts +73 -0
  134. package/src/release/binary-form.ts +229 -0
  135. package/src/release/binary-lifecycle.test.ts +90 -0
  136. package/src/release/binary-lifecycle.ts +202 -0
  137. package/src/release/binary-read.ts +55 -0
  138. package/src/release/binary-refusals.test.ts +192 -0
  139. package/src/release/binary-sudo.test.ts +93 -0
  140. package/src/release/binary.ts +181 -0
  141. package/src/release/catalog.test.ts +96 -0
  142. package/src/release/catalog.ts +175 -0
  143. package/src/release/checksums.test.ts +35 -0
  144. package/src/release/checksums.ts +43 -0
  145. package/src/release/declared-pins.ts +39 -0
  146. package/src/release/download.test.ts +138 -0
  147. package/src/release/download.ts +110 -0
  148. package/src/release/error-channel.test.ts +69 -0
  149. package/src/release/fake-deploy.ts +87 -0
  150. package/src/release/fake-engine.ts +50 -0
  151. package/src/release/fake-install.ts +59 -0
  152. package/src/release/fake-provider.ts +68 -0
  153. package/src/release/fake-release.ts +185 -0
  154. package/src/release/fixtures/victoria/darwin-arm64-assets.txt +22 -0
  155. package/src/release/fixtures/victoria/victoria-logs-darwin-arm64-v1.52.0_checksums.txt +2 -0
  156. package/src/release/fixtures/victoria/victoria-metrics-darwin-arm64-v1.151.0_checksums.txt +2 -0
  157. package/src/release/fixtures/victoria/victoria-traces-darwin-arm64-v0.10.0_checksums.txt +2 -0
  158. package/src/release/fixtures/victoria/vmutils-darwin-arm64-v1.151.0_checksums.txt +8 -0
  159. package/src/release/index.ts +41 -0
  160. package/src/release/plan.test.ts +189 -0
  161. package/src/release/provider-refusals.test.ts +48 -0
  162. package/src/release/providers.test.ts +174 -0
  163. package/src/release/providers.ts +24 -0
  164. package/src/release/recovery-read.test.ts +44 -0
  165. package/src/release/refused.ts +40 -0
  166. package/src/release/tar.test.ts +142 -0
  167. package/src/release/tar.ts +195 -0
  168. package/src/release/victoria.test.ts +108 -0
  169. package/src/release/victoria.ts +154 -0
  170. package/src/{proxmox → secrets}/write-only.ts +6 -0
  171. /package/dist/{proxmox → secrets}/write-only.d.ts +0 -0
  172. /package/src/{proxmox → secrets}/write-only.test.ts +0 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Synthetic release archives for the tests beside it: a tar writer, gzip, a catalog pinned to what
3
+ * they build, a counting transport, and a host with the versioned directories in place.
4
+ *
5
+ * ⛔ TEST-ONLY. No provider imports this file, it is not on the barrel, and nothing here reaches a
6
+ * network or a real filesystem. The catalog is synthetic because a test cannot build bytes that
7
+ * hash to the vendor's pins; victoria.test.ts holds the REAL data set to the vendor's own files.
8
+ * ★ THE ASSET NAMES AND URLS ARE THE REAL ONES, so a test that serves the `-enterprise` sibling
9
+ * next to the plain archive proves the provider asks for exactly one of them.
10
+ */
11
+ import { fakeRunner } from '../launchd/fake-runner.ts';
12
+ import { sha256Hex } from '../launchd/job-form.ts';
13
+ import { type ReleaseBinaryProps, releaseUrl } from './binary-form.ts';
14
+ import { type PinnedArchive, type ReleaseCatalog, catalogBinary } from './catalog.ts';
15
+ import type { FetchArchive } from './download.ts';
16
+ import { DownloadFailed } from './refused.ts';
17
+ import { VICTORIA_RELEASES } from './victoria.ts';
18
+
19
+ export type TarEntry = {
20
+ readonly name: string;
21
+ readonly bytes?: Uint8Array;
22
+ /** The typeflag: '0' regular (default), '1' hard link, '2' symlink, '5' directory, 'L', 'x', … */
23
+ readonly type?: string;
24
+ readonly format?: 'gnu' | 'posix';
25
+ readonly prefix?: string;
26
+ /** Size field written as GNU base-256. */
27
+ readonly base256?: boolean;
28
+ readonly corruptChecksum?: boolean;
29
+ };
30
+
31
+ const put = (block: Uint8Array, at: number, text: string) =>
32
+ block.set(new TextEncoder().encode(text), at);
33
+ const octal = (value: number, width: number) => `${value.toString(8).padStart(width - 1, '0')}\0`;
34
+
35
+ export const tarHeader = (entry: TarEntry): Uint8Array => {
36
+ const block = new Uint8Array(512);
37
+ const size = entry.bytes?.length ?? 0;
38
+ put(block, 0, entry.name);
39
+ put(block, 100, octal(0o755, 8));
40
+ put(block, 108, octal(1000, 8));
41
+ put(block, 116, octal(1000, 8));
42
+ if (entry.base256 === true) block.set([0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, size], 124);
43
+ else put(block, 124, octal(size, 12));
44
+ put(block, 136, octal(1_750_000_000, 12));
45
+ block.fill(0x20, 148, 156);
46
+ put(block, 156, entry.type ?? '0');
47
+ put(block, 257, entry.format === 'posix' ? 'ustar\u000000' : 'ustar \u0000');
48
+ if (entry.prefix !== undefined) put(block, 345, entry.prefix);
49
+ const sum = block.reduce((total, byte) => total + byte, 0) + (entry.corruptChecksum ? 1 : 0);
50
+ put(block, 148, `${sum.toString(8).padStart(6, '0')}\0 `);
51
+ return block;
52
+ };
53
+
54
+ /** A tar: each header, its data padded to 512, then the two zero blocks. */
55
+ export const tarOf = (entries: readonly TarEntry[], end = 1024): Uint8Array => {
56
+ const blocks: Uint8Array[] = [];
57
+ for (const entry of entries) {
58
+ blocks.push(tarHeader(entry));
59
+ const bytes = entry.bytes ?? new Uint8Array();
60
+ const padded = new Uint8Array(Math.ceil(bytes.length / 512) * 512);
61
+ padded.set(bytes);
62
+ blocks.push(padded);
63
+ }
64
+ blocks.push(new Uint8Array(end));
65
+ const out = new Uint8Array(blocks.reduce((total, block) => total + block.length, 0));
66
+ let at = 0;
67
+ for (const block of blocks) {
68
+ out.set(block, at);
69
+ at += block.length;
70
+ }
71
+ return out;
72
+ };
73
+
74
+ export const gzip = (bytes: Uint8Array): Uint8Array => Bun.gzipSync(bytes.slice());
75
+
76
+ export const bytesOf = (text: string): Uint8Array => new TextEncoder().encode(text);
77
+
78
+ /** Stand-ins for the vendor binaries: distinct bytes, so each digest is distinct. */
79
+ export const BINARY = {
80
+ vmagent: bytesOf('#!fake vmagent 1.151.0\n'),
81
+ vmalert: bytesOf('#!fake vmalert 1.151.0\n'),
82
+ vmauth: bytesOf('#!fake vmauth 1.151.0\n'),
83
+ 'victoria-traces': bytesOf('#!fake victoria-traces 0.10.0\n'),
84
+ };
85
+
86
+ const present = (archive: PinnedArchive | undefined): PinnedArchive => {
87
+ if (archive === undefined) throw new Error('the data set lost a pinned archive');
88
+ return archive;
89
+ };
90
+ const REAL = VICTORIA_RELEASES.packages;
91
+ export const VMUTILS = present(REAL.vmutils?.versions['1.151.0']?.['darwin-arm64']);
92
+ export const TRACES = present(REAL['victoria-traces']?.versions['0.10.0']?.['darwin-arm64']);
93
+ export const urlOf = (archive: PinnedArchive): string =>
94
+ releaseUrl(archive.repo, archive.tag, archive.asset);
95
+ export const VMUTILS_URL = urlOf(VMUTILS);
96
+ export const TRACES_URL = urlOf(TRACES);
97
+ export const ENTERPRISE_URL = VMUTILS_URL.replace('.tar.gz', '-enterprise.tar.gz');
98
+
99
+ export const VMUTILS_ENTRIES: readonly TarEntry[] = [
100
+ { bytes: BINARY.vmagent, name: 'vmagent-prod' },
101
+ { bytes: BINARY.vmalert, name: 'vmalert-prod' },
102
+ { bytes: BINARY.vmauth, name: 'vmauth-prod' },
103
+ ];
104
+
105
+ /** Pin an archive the way victoria.ts pins a vendor one, from the bytes themselves. */
106
+ export const pinned = (
107
+ real: PinnedArchive,
108
+ archive: Uint8Array,
109
+ members: Readonly<Record<string, Uint8Array>>,
110
+ ): PinnedArchive => ({
111
+ ...real,
112
+ members: Object.fromEntries(Object.entries(members).map(([n, b]) => [n, sha256Hex(b)])),
113
+ sha256: sha256Hex(archive),
114
+ size: archive.length,
115
+ });
116
+
117
+ /** The real data set with vmutils 1.151.0 and victoria-traces 0.10.0 re-pinned to synthetic bytes. */
118
+ export const syntheticRelease = (vmutilsEntries: readonly TarEntry[] = VMUTILS_ENTRIES) => {
119
+ const vmutils = gzip(tarOf(vmutilsEntries));
120
+ const traces = gzip(tarOf([{ bytes: BINARY['victoria-traces'], name: 'victoria-traces-prod' }]));
121
+ // ★ vmalert-prod is always pinned, as the vendor file lists it — so an archive built WITHOUT it
122
+ // models "the checksum file names a member the archive does not hold".
123
+ const memberBytes = {
124
+ 'vmalert-prod': BINARY.vmalert,
125
+ ...Object.fromEntries(vmutilsEntries.map((e) => [e.name, e.bytes ?? new Uint8Array()])),
126
+ };
127
+ const tracesPin = pinned(TRACES, traces, { 'victoria-traces-prod': BINARY['victoria-traces'] });
128
+ const catalog: ReleaseCatalog = {
129
+ ...VICTORIA_RELEASES,
130
+ packages: {
131
+ ...REAL,
132
+ 'victoria-traces': {
133
+ binaries: { 'victoria-traces': 'victoria-traces-prod' },
134
+ versions: { '0.10.0': { 'darwin-arm64': tracesPin } },
135
+ },
136
+ vmutils: {
137
+ binaries: REAL.vmutils?.binaries ?? {},
138
+ versions: { '1.151.0': { 'darwin-arm64': pinned(VMUTILS, vmutils, memberBytes) } },
139
+ },
140
+ },
141
+ };
142
+ return { catalog, traces, vmutils };
143
+ };
144
+
145
+ /** A transport over a URL → bytes table that records every request, in order. */
146
+ export const fakeTransport = (served: Readonly<Record<string, Uint8Array>>) => {
147
+ const requests: string[] = [];
148
+ const fetch: FetchArchive = async (url) => {
149
+ requests.push(url);
150
+ const bytes = served[url];
151
+ if (bytes === undefined)
152
+ throw new DownloadFailed({ message: `${url}: HTTP 404`, retryable: false });
153
+ return bytes;
154
+ };
155
+ return { fetch, requests };
156
+ };
157
+
158
+ export const ROOT = '/opt/example/bin';
159
+ export const VMUTILS_DIR = `${ROOT}/vmutils-1.151.0`;
160
+ export const TRACES_DIR = `${ROOT}/victoria-traces-0.10.0`;
161
+
162
+ /** A host deploying as root, with the two versioned directories already declared. */
163
+ export const releaseHost = () =>
164
+ fakeRunner({ dirs: { [ROOT]: 0, [TRACES_DIR]: 0, [VMUTILS_DIR]: 0 }, euid: 0 });
165
+
166
+ export const VMALERT_REQUEST = {
167
+ binary: 'vmalert',
168
+ package: 'vmutils',
169
+ platform: 'darwin-arm64',
170
+ version: '1.151.0',
171
+ } as const;
172
+
173
+ /** vmalert's props out of `catalog` (the synthetic one, normally), in its versioned directory. */
174
+ export const vmalertProps = (
175
+ catalog: ReleaseCatalog,
176
+ more: Partial<ReleaseBinaryProps> = {},
177
+ ): ReleaseBinaryProps => ({
178
+ ...catalogBinary(catalog, VMALERT_REQUEST),
179
+ directory: VMUTILS_DIR,
180
+ ...more,
181
+ });
182
+
183
+ /** Every path on the fake host, sorted — equal before and after means nothing was left behind. */
184
+ export const pathsOn = (fake: ReturnType<typeof fakeRunner>): string[] =>
185
+ [...fake.files.keys()].sort();
@@ -0,0 +1,22 @@
1
+ victoria-logs-darwin-arm64-v1.52.0-enterprise.tar.gz
2
+ victoria-logs-darwin-arm64-v1.52.0-enterprise_checksums.txt
3
+ victoria-logs-darwin-arm64-v1.52.0.tar.gz
4
+ victoria-logs-darwin-arm64-v1.52.0_checksums.txt
5
+ victoria-metrics-darwin-arm64-v1.151.0-cluster.tar.gz
6
+ victoria-metrics-darwin-arm64-v1.151.0-cluster_checksums.txt
7
+ victoria-metrics-darwin-arm64-v1.151.0-enterprise-cluster.tar.gz
8
+ victoria-metrics-darwin-arm64-v1.151.0-enterprise-cluster_checksums.txt
9
+ victoria-metrics-darwin-arm64-v1.151.0-enterprise.tar.gz
10
+ victoria-metrics-darwin-arm64-v1.151.0-enterprise_checksums.txt
11
+ victoria-metrics-darwin-arm64-v1.151.0.tar.gz
12
+ victoria-metrics-darwin-arm64-v1.151.0_checksums.txt
13
+ victoria-traces-darwin-arm64-v0.10.0.tar.gz
14
+ victoria-traces-darwin-arm64-v0.10.0_checksums.txt
15
+ vlutils-darwin-arm64-v1.52.0-enterprise.tar.gz
16
+ vlutils-darwin-arm64-v1.52.0-enterprise_checksums.txt
17
+ vlutils-darwin-arm64-v1.52.0.tar.gz
18
+ vlutils-darwin-arm64-v1.52.0_checksums.txt
19
+ vmutils-darwin-arm64-v1.151.0-enterprise.tar.gz
20
+ vmutils-darwin-arm64-v1.151.0-enterprise_checksums.txt
21
+ vmutils-darwin-arm64-v1.151.0.tar.gz
22
+ vmutils-darwin-arm64-v1.151.0_checksums.txt
@@ -0,0 +1,2 @@
1
+ 3157d4b6181d8a7e3e30918e2cbfcd4cc4cb66263e3ef21ea91e4f20f8980883 victoria-logs-darwin-arm64-v1.52.0.tar.gz
2
+ 9e48809e314902782959b3a4e683a3087476759d0293791a0564921c676ca7be victoria-logs-prod
@@ -0,0 +1,2 @@
1
+ 8792437c4c0b63719fed7b44ad18d4b4167bbe6861ab3e84c7bb010c922e7582 victoria-metrics-darwin-arm64-v1.151.0.tar.gz
2
+ 3c493a95ac1e9034a3261b230c4d62f3b186a04aafcdbacc99bfc110dc5de6a1 victoria-metrics-prod
@@ -0,0 +1,2 @@
1
+ cbe83f1d409cbb85fbf1c890c4a9b7fd34c1d74acec4dce2e0ebe46fcb260e38 victoria-traces-darwin-arm64-v0.10.0.tar.gz
2
+ 4759ec89a466129b9eefc22244a111e1f489739340747805fd93b58fd0f7b2dd victoria-traces-prod
@@ -0,0 +1,8 @@
1
+ 27d68bac90e28929214091ed9d27f2e9fef25a080407e2c8661ea9b52dd6b183 vmutils-darwin-arm64-v1.151.0.tar.gz
2
+ d3b6fa1f71ef48beac619f15f8adfa7610e76605caf37db1ebfbb379b6692329 vmagent-prod
3
+ 62845795167e9ff47890e83ac4ac5934693c105767db88591c95a08f3c4fb354 vmalert-prod
4
+ cbffb9495e4bb1457c9d45e0a7064b3a3a29b2cfd1ae24bfe8f00ec327c26998 vmalert-tool-prod
5
+ 398df9e0f5b7035d037e39ee9ff66a10ddde5333d832602463aa4d2865fdb508 vmauth-prod
6
+ 1974a2ee211b49bf581287808e010846f6c47ff135276214c6da628567777b9e vmbackup-prod
7
+ fc824df7215b01b57921ed46afde7fea6b6698adf13d9ea8e3fe36e194ef8190 vmrestore-prod
8
+ 998b111eee206b98e6d25fdf2fbaa455ecc17c7f9e7c3b2373e127cef762459f vmctl-prod
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Release binaries for Alchemy — one binary out of a pinned release archive, installed onto a host
3
+ * and verified against digests the stack pins in code, plus the vendor data sets those pins come
4
+ * from (VictoriaMetrics' is the first).
5
+ *
6
+ * ⛔ THIS BARREL IS THE PUBLIC API, AND IT IS DELIBERATELY SMALLER THAN THE DIRECTORY. The tar
7
+ * reader, the downloader, the checksum parser, the lifecycle and the test seams are internals; an
8
+ * `export *` would publish them as API and make the next refactor a breaking change.
9
+ * ⛔ IT INSTALLS; IT NEVER STARTS. Run the binary with `LaunchdJob` (the launchd subpath) or a
10
+ * systemd unit, putting `ReleaseBinary`'s `path` in its argv. See docs/release-binary.md.
11
+ * ★ THE SEAM IS SHARED, NOT COPIED. `HostRunner` is re-exported so a stack types its runner without
12
+ * importing from another subpath.
13
+ */
14
+ export type {
15
+ PinnedDownload,
16
+ ReleaseArchive,
17
+ ReleaseBinaryAttributes,
18
+ ReleaseBinaryProps,
19
+ } from './binary-form.ts';
20
+ export { releaseBinaryPath, releaseUrl } from './binary-form.ts';
21
+ export { ReleaseBinary, ReleaseBinaryProvider } from './binary.ts';
22
+ export type {
23
+ CatalogPackage,
24
+ CatalogRequest,
25
+ IdentifiedBinary,
26
+ PinnedArchive,
27
+ PinnedBinary,
28
+ ReleaseCatalog,
29
+ } from './catalog.ts';
30
+ export { catalogBinary, catalogDirectory, catalogProblems, identifyBinary } from './catalog.ts';
31
+ export { releaseProviders } from './providers.ts';
32
+ export { ArchiveRefused, BinaryRefused, ChecksumMismatch, DownloadFailed } from './refused.ts';
33
+ export { VICTORIA_RELEASES } from './victoria.ts';
34
+ export type {
35
+ ExecResult,
36
+ FileStat,
37
+ HostRunner,
38
+ HostUser,
39
+ WriteOptions,
40
+ } from '../launchd/runner.ts';
41
+ export { HostRunnerService, hostRunnerLayer } from '../launchd/runner.ts';
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Release.Binary through Alchemy's OWN Plan and Apply (openbao/fake-stack.ts, in-memory state), with
3
+ * `HostDirectory` declared in front of it, over a fake host that models mkdir/rmdir
4
+ * (linux/fake-linux-host.ts) and a fake HttpClient. No network, no real filesystem.
5
+ *
6
+ * ★ WHAT ONLY THE ENGINE CAN SAY. A handler test shows what `diff` answers; this shows what the
7
+ * engine then DOES with it, in which order: that `directory: dir.path` orders the directory
8
+ * first; that a version bump writes the new binary before the old one goes; that a version
9
+ * outside the catalog fails the plan with nothing fetched and nothing touched.
10
+ */
11
+ import { describe, expect, test } from 'bun:test';
12
+ import * as Effect from 'effect/Effect';
13
+ import * as Layer from 'effect/Layer';
14
+ import * as HttpClient from 'effect/unstable/http/HttpClient';
15
+ import * as HttpClientResponse from 'effect/unstable/http/HttpClientResponse';
16
+ import { sha256Hex } from '../launchd/job-form.ts';
17
+ import { type HostRunner, hostRunnerLayer } from '../launchd/runner.ts';
18
+ import { HostDirectory, HostDirectoryProvider } from '../linux/directory.ts';
19
+ import { fakeLinuxHost } from '../linux/fake-linux-host.ts';
20
+ import { fakeStack } from '../openbao/fake-stack.ts';
21
+ import { releaseUrl } from './binary-form.ts';
22
+ import { ReleaseBinary, makeReleaseBinaryProvider } from './binary.ts';
23
+ import { type PinnedBinary, catalogBinary, catalogDirectory } from './catalog.ts';
24
+ import {
25
+ BINARY,
26
+ VMALERT_REQUEST,
27
+ VMUTILS_URL,
28
+ bytesOf,
29
+ gzip,
30
+ syntheticRelease,
31
+ tarOf,
32
+ } from './fake-release.ts';
33
+ import { VICTORIA_RELEASES } from './victoria.ts';
34
+
35
+ const ROOT = '/opt/example/bin';
36
+ const NEXT = bytesOf('#!fake vmalert 1.152.0\n');
37
+
38
+ /** vmutils 1.151.0 (synthetic, at its real URL) and a synthetic 1.152.0 beside it. */
39
+ const releases = () => {
40
+ const current = syntheticRelease();
41
+ const next = gzip(tarOf([{ bytes: NEXT, name: 'vmalert-prod' }]));
42
+ const nextPins: PinnedBinary = {
43
+ archive: {
44
+ asset: 'vmutils-darwin-arm64-v1.152.0.tar.gz',
45
+ repo: 'VictoriaMetrics/VictoriaMetrics',
46
+ sha256: sha256Hex(next),
47
+ size: next.length,
48
+ tag: 'v1.152.0',
49
+ },
50
+ member: 'vmalert-prod',
51
+ name: 'vmalert',
52
+ sha256: sha256Hex(NEXT),
53
+ };
54
+ const nextUrl = releaseUrl(nextPins.archive.repo, nextPins.archive.tag, nextPins.archive.asset);
55
+ const served: Record<string, Uint8Array> = { [VMUTILS_URL]: current.vmutils, [nextUrl]: next };
56
+ return { current: catalogBinary(current.catalog, VMALERT_REQUEST), next: nextPins, served };
57
+ };
58
+
59
+ /** One fake host and one fake server; `events` is every host change and request, in order. */
60
+ const harness = () => {
61
+ const { current, next, served } = releases();
62
+ const fake = fakeLinuxHost({ dirs: { [ROOT]: 0 }, euid: 0 });
63
+ const events: string[] = [];
64
+ const base = fake.runner;
65
+ const runner: HostRunner = {
66
+ ...base,
67
+ exec: (argv) => {
68
+ events.push(argv.filter((arg) => arg !== '--').join(' '));
69
+ return base.exec(argv);
70
+ },
71
+ removeFile: (path) => {
72
+ events.push(`remove ${path}`);
73
+ return base.removeFile(path);
74
+ },
75
+ writeFileAtomic: (path, bytes, options) => {
76
+ events.push(`write ${path}`);
77
+ return base.writeFileAtomic(path, bytes, options);
78
+ },
79
+ };
80
+ const client = HttpClient.make((request, url) => {
81
+ events.push(`GET ${url.toString()}`);
82
+ const bytes = served[url.toString()];
83
+ const body = bytes === undefined ? null : new Blob([bytes.slice()]);
84
+ return Effect.succeed(
85
+ HttpClientResponse.fromWeb(request, new Response(body, { status: body ? 200 : 404 })),
86
+ );
87
+ });
88
+ const providers = Layer.mergeAll(makeReleaseBinaryProvider(), HostDirectoryProvider()).pipe(
89
+ Layer.provide(
90
+ Layer.mergeAll(hostRunnerLayer(runner), Layer.succeed(HttpClient.HttpClient, client)),
91
+ ),
92
+ );
93
+ const stack = fakeStack(providers, {}, 'ReleaseStack');
94
+ /** The stack body: one versioned directory and the binary in it, ordered by `dir.path`. */
95
+ const declare = (pins: PinnedBinary, version: string) =>
96
+ Effect.gen(function* () {
97
+ const path = catalogDirectory(ROOT, { package: 'vmutils', version });
98
+ const dir = yield* HostDirectory(`vmutils-${version}`, { mode: 0o755, path });
99
+ yield* ReleaseBinary('vmalert', { ...pins, directory: dir.path });
100
+ });
101
+ return { current, declare, events, fake, next, stack };
102
+ };
103
+
104
+ const OLD = `${ROOT}/vmutils-1.151.0`;
105
+ const NEW = `${ROOT}/vmutils-1.152.0`;
106
+
107
+ describe('through the engine', () => {
108
+ test('a version outside the catalog fails the plan: nothing fetched, nothing touched', async () => {
109
+ const h = harness();
110
+ const body = Effect.gen(function* () {
111
+ const pins = catalogBinary(VICTORIA_RELEASES, { ...VMALERT_REQUEST, version: '1.152.0' });
112
+ yield* ReleaseBinary('vmalert', { ...pins, directory: OLD });
113
+ });
114
+ await expect(h.stack.deploy(body)).rejects.toThrow('vmutils 1.152.0 is not in the catalog');
115
+ expect(h.events).toEqual([]);
116
+ });
117
+
118
+ test('the directory first, then one download, then the one write', async () => {
119
+ const h = harness();
120
+ const planned = await h.stack.deploy(h.declare(h.current, '1.151.0'));
121
+ expect(planned).toEqual({ vmalert: 'create', 'vmutils-1.151.0': 'create' });
122
+ expect(h.events).toEqual([`mkdir -m 755 ${OLD}`, `GET ${VMUTILS_URL}`, `write ${OLD}/vmalert`]);
123
+ expect(h.fake.files.get(`${OLD}/vmalert`)?.bytes).toEqual(BINARY.vmalert);
124
+ });
125
+
126
+ test('a second deploy of the same declaration changes nothing and fetches nothing', async () => {
127
+ const h = harness();
128
+ await h.stack.deploy(h.declare(h.current, '1.151.0'));
129
+ h.events.length = 0;
130
+ await h.stack.deploy(h.declare(h.current, '1.151.0'));
131
+ expect(h.events).toEqual([]);
132
+ });
133
+
134
+ test('a version bump: the new binary is written BEFORE the old one goes, then the old directory', async () => {
135
+ const h = harness();
136
+ await h.stack.deploy(h.declare(h.current, '1.151.0'));
137
+ h.events.length = 0;
138
+ const planned = await h.stack.deploy(h.declare(h.next, '1.152.0'));
139
+ expect(planned).toEqual({
140
+ vmalert: 'replace',
141
+ 'vmutils-1.151.0': 'delete',
142
+ 'vmutils-1.152.0': 'create',
143
+ });
144
+ // ★ MEASURED HERE, not reasoned from Apply.ts: create-before-delete, and the old directory
145
+ // only once it is empty — a job whose argv holds `vmalert.path` moves before its file goes.
146
+ expect(h.events).toEqual([
147
+ `mkdir -m 755 ${NEW}`,
148
+ `GET ${releaseUrl(h.next.archive.repo, h.next.archive.tag, h.next.archive.asset)}`,
149
+ `write ${NEW}/vmalert`,
150
+ `remove ${OLD}/vmalert`,
151
+ `rmdir ${OLD}`,
152
+ ]);
153
+ expect(h.fake.files.get(`${NEW}/vmalert`)?.bytes).toEqual(NEXT);
154
+ expect([...h.fake.files.keys()]).toEqual([`${NEW}/vmalert`]);
155
+ });
156
+
157
+ test('a new pin in the SAME directory fails the plan; the installed binary is untouched', async () => {
158
+ const h = harness();
159
+ await h.stack.deploy(h.declare(h.current, '1.151.0'));
160
+ h.events.length = 0;
161
+ await expect(h.stack.deploy(h.declare(h.next, '1.151.0'))).rejects.toThrow(
162
+ 'would overwrite it in place',
163
+ );
164
+ // ★ Refused by diff, at plan: the directory was already created, so its path is resolved.
165
+ expect(h.events).toEqual([]);
166
+ expect(h.fake.files.get(`${OLD}/vmalert`)?.bytes).toEqual(BINARY.vmalert);
167
+ });
168
+
169
+ test('the same bytes re-pinned while the directory is updated: refused at apply, binary kept', async () => {
170
+ const h = harness();
171
+ const body = (mode: number, tag: string) =>
172
+ Effect.gen(function* () {
173
+ const dir = yield* HostDirectory('vmutils-1.151.0', { mode, path: OLD });
174
+ const archive = { ...h.current.archive, tag };
175
+ yield* ReleaseBinary('vmalert', { ...h.current, archive, directory: dir.path });
176
+ });
177
+ await h.stack.deploy(body(0o755, 'v1.151.0'));
178
+ h.events.length = 0;
179
+ // The directory's update leaves its path unresolved at plan, so diff cannot see the path.
180
+ await expect(h.stack.deploy(body(0o750, 'v1.151.1'))).rejects.toThrow(
181
+ 'would overwrite it in place',
182
+ );
183
+ // 🔴 MEASURED 2026-09-22 with diff answering `replace` here instead of `update`: the deploy
184
+ // SUCCEEDED and the binary was gone — the new generation took the identical file as its own,
185
+ // then Phase 2 deleted the old generation at the same path (binary-diff.ts).
186
+ expect(h.events).toEqual([`chmod 750 ${OLD}`]);
187
+ expect(h.fake.files.get(`${OLD}/vmalert`)?.bytes).toEqual(BINARY.vmalert);
188
+ });
189
+ });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The provider's plan-time refusals, through the engine-facing handlers (fake-provider.ts): each
3
+ * with zero requests and zero writes. (A version outside a catalog never reaches a handler: it
4
+ * fails in the stack program — catalog.test.ts, plan.test.ts.)
5
+ */
6
+ import { describe, expect, test } from 'bun:test';
7
+ import * as Effect from 'effect/Effect';
8
+ import type { ReleaseBinaryProps } from './binary-form.ts';
9
+ import { VMUTILS_DIR } from './fake-release.ts';
10
+ import { call, harness, ids, must } from './fake-provider.ts';
11
+
12
+ describe('plan-time refusals: zero requests, zero writes', () => {
13
+ test('the adoption probe refuses a malformed pin before touching the host', async () => {
14
+ const h = harness();
15
+ const next = { ...h.VMALERT, sha256: 'v1.151.0' };
16
+ const probe = h.run((p) => must(p.read)({ ...ids, olds: next as never, output: undefined }));
17
+ await expect(probe).rejects.toThrow('sha256 must be 64 lower-case hex digits');
18
+ expect(h.requests).toEqual([]);
19
+ expect(h.fake.calls).toEqual([]);
20
+ });
21
+
22
+ test('diff refuses a pin that is not a plain value, even while the directory is an Output', async () => {
23
+ const h = harness();
24
+ const output = await h.run((p) => h.reconcile(p, h.VMALERT));
25
+ h.requests.length = 0;
26
+ // ⛔ A pin computed during the deploy is a fetch by another name.
27
+ const pending = {
28
+ ...h.VMALERT,
29
+ directory: Effect.succeed(VMUTILS_DIR),
30
+ sha256: Effect.succeed(h.VMALERT.sha256),
31
+ };
32
+ const diff = h.run((p) =>
33
+ must(p.diff)({ ...ids, ...call, news: pending as never, olds: h.VMALERT as never, output }),
34
+ );
35
+ await expect(diff).rejects.toThrow('sha256 is required, as a plain string');
36
+ expect(h.requests).toEqual([]);
37
+ });
38
+
39
+ test('diff refuses a new pin at the same path', async () => {
40
+ const h = harness();
41
+ const output = await h.run((p) => h.reconcile(p, h.VMALERT));
42
+ const bumped: ReleaseBinaryProps = { ...h.VMALERT, sha256: 'e'.repeat(64) };
43
+ const diff = h.run((p) =>
44
+ must(p.diff)({ ...ids, ...call, news: bumped as never, olds: h.VMALERT as never, output }),
45
+ );
46
+ await expect(diff).rejects.toThrow('would overwrite it in place');
47
+ });
48
+ });