@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,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,96 @@
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 { catalogBinary, catalogDirectory, catalogProblems, identifyBinary } from './catalog.ts';
9
+ import { BinaryRefused } from './refused.ts';
10
+ import { VICTORIA_RELEASES } from './victoria.ts';
11
+
12
+ const VMALERT = {
13
+ binary: 'vmalert',
14
+ package: 'vmutils',
15
+ platform: 'darwin-arm64',
16
+ version: '1.151.0',
17
+ };
18
+
19
+ describe('refusing what the catalog does not pin', () => {
20
+ test.each([
21
+ [
22
+ 'a version the catalog does not pin',
23
+ { version: '1.152.0' },
24
+ 'vmutils 1.152.0 is not in the catalog',
25
+ ],
26
+ ['the tag spelling', { version: 'v1.151.0' }, 'is not in the catalog'],
27
+ ['an enterprise version', { version: '1.151.0-enterprise' }, 'is not in the catalog'],
28
+ [
29
+ 'a cluster package',
30
+ { package: 'victoria-metrics-cluster' },
31
+ 'package "victoria-metrics-cluster"',
32
+ ],
33
+ ['the vendor member name', { binary: 'vmalert-prod' }, 'that is the archive member'],
34
+ ['a binary vmutils does not ship', { binary: 'vminsert' }, 'has no binary "vminsert"'],
35
+ ['another platform', { platform: 'linux-amd64' }, 'not pinned for platform "linux-amd64"'],
36
+ ['an inherited key', { package: 'constructor' }, 'package "constructor"'],
37
+ ])('refuses %s', (_, change, message) => {
38
+ const request = { ...VMALERT, ...change };
39
+ expect(catalogProblems(VICTORIA_RELEASES, request).join('; ')).toContain(message);
40
+ expect(() => catalogBinary(VICTORIA_RELEASES, request)).toThrow(message);
41
+ });
42
+
43
+ test('the refusal is typed, names the binary, and says nothing was fetched', () => {
44
+ let caught: unknown;
45
+ try {
46
+ catalogBinary(VICTORIA_RELEASES, { ...VMALERT, version: '1.152.0' });
47
+ } catch (cause) {
48
+ caught = cause;
49
+ }
50
+ expect(caught).toBeInstanceOf(BinaryRefused);
51
+ expect((caught as Error).message).toMatch(
52
+ /^Release\.Binary vmalert: .* Nothing was fetched\.$/,
53
+ );
54
+ });
55
+ });
56
+
57
+ describe('what a pinned request becomes', () => {
58
+ test('props that validate once the stack adds a directory', () => {
59
+ const pinned = catalogBinary(VICTORIA_RELEASES, VMALERT);
60
+ const directory = catalogDirectory('/opt/example/bin', VMALERT);
61
+ expect(directory).toBe('/opt/example/bin/vmutils-1.151.0');
62
+ expect(binaryProblems({ ...pinned, directory })).toEqual([]);
63
+ });
64
+
65
+ test('every binary of every pinned archive resolves to valid props', () => {
66
+ for (const [pkg, entry] of Object.entries(VICTORIA_RELEASES.packages)) {
67
+ for (const [version, platforms] of Object.entries(entry.versions)) {
68
+ for (const platform of Object.keys(platforms)) {
69
+ for (const binary of Object.keys(entry.binaries)) {
70
+ const request = { binary, package: pkg, platform, version };
71
+ const props = { ...catalogBinary(VICTORIA_RELEASES, request), directory: '/x/y' };
72
+ expect(binaryProblems(props)).toEqual([]);
73
+ }
74
+ }
75
+ }
76
+ }
77
+ });
78
+ });
79
+
80
+ describe('identifying an installed file', () => {
81
+ test('by its digest alone', () => {
82
+ const pin = '4759ec89a466129b9eefc22244a111e1f489739340747805fd93b58fd0f7b2dd';
83
+ expect(identifyBinary([VICTORIA_RELEASES], pin)).toEqual({
84
+ binary: 'victoria-traces',
85
+ package: 'victoria-traces',
86
+ platform: 'darwin-arm64',
87
+ vendor: 'VictoriaMetrics',
88
+ version: '0.10.0',
89
+ });
90
+ });
91
+
92
+ test("an archive's digest is not a binary's: identity is by the member's own line", () => {
93
+ const archive = 'cbe83f1d409cbb85fbf1c890c4a9b7fd34c1d74acec4dce2e0ebe46fcb260e38';
94
+ expect(identifyBinary([VICTORIA_RELEASES], archive)).toBeUndefined();
95
+ });
96
+ });
@@ -0,0 +1,175 @@
1
+ /**
2
+ * A vendor's pinned releases as DATA, and the one way to turn an entry into Release.Binary props —
3
+ * or into the reasons there is none. Pure: no network, no host.
4
+ *
5
+ * ★ THE DATA SITS BESIDE THE RESOURCE, NOT INSIDE IT. A catalog (victoria.ts is the first) is the
6
+ * reviewed record of what one vendor published: exact asset names, sizes, both digest layers and
7
+ * where each was copied from. The resource takes one pinned archive as props and never reads a
8
+ * catalog, so adding a vendor is a data module with its own walk-down, never a resource change.
9
+ * ★ A VERSION THE CATALOG DOES NOT LIST IS A REFUSAL, NOT A DOWNLOAD ATTEMPT. `catalogBinary()` runs
10
+ * in the stack's own program — which `alchemy plan` evaluates — so an unknown version, a
11
+ * `-enterprise` spelling or a `v` prefix fails the plan before any resource is asked anything,
12
+ * let alone fetches a byte.
13
+ * ⛔ ADD A VENDOR ONLY AFTER ITS OWN WALK-DOWN (docs/release-binary-catalogs.md): its checksum
14
+ * format read byte-exact, and its binary's linkage measured (`otool -L`). Vendor binaries were
15
+ * measured self-contained ONLY for the five Victoria ones; that is not a property of "Go".
16
+ */
17
+ import type { ReleaseArchive, ReleaseBinaryProps } from './binary-form.ts';
18
+ import { BinaryRefused } from './refused.ts';
19
+
20
+ /** One release archive as a catalog records it: the pin, plus where every digest came from. */
21
+ export interface PinnedArchive extends ReleaseArchive {
22
+ /** Archive member → its SHA-256. Every member the vendor's checksum file lists, not just ours. */
23
+ readonly members: Readonly<Record<string, string>>;
24
+ /** The checksum file the digests were copied from, its own SHA-256, and when. Never fetched. */
25
+ readonly checksums: { readonly url: string; readonly sha256: string; readonly recorded: string };
26
+ }
27
+
28
+ /** One package (one archive per version and platform) of a vendor. */
29
+ export interface CatalogPackage {
30
+ /**
31
+ * Installed name → archive member, written out, never derived: a member the vendor adds is
32
+ * installable only once somebody names it here.
33
+ */
34
+ readonly binaries: Readonly<Record<string, string>>;
35
+ /** Version (as the catalog spells it) → platform (the vendor's spelling) → archive. */
36
+ readonly versions: Readonly<Record<string, Readonly<Record<string, PinnedArchive>>>>;
37
+ }
38
+
39
+ /** A vendor's pinned releases. */
40
+ export interface ReleaseCatalog {
41
+ /** Who publishes these, for refusals: `'VictoriaMetrics'`. */
42
+ readonly vendor: string;
43
+ readonly packages: Readonly<Record<string, CatalogPackage>>;
44
+ }
45
+
46
+ /** Which binary a stack asks a catalog for. */
47
+ export type CatalogRequest = {
48
+ readonly package: string;
49
+ readonly version: string;
50
+ readonly platform: string;
51
+ /** The installed name — `vmalert`, not the member `vmalert-prod`. */
52
+ readonly binary: string;
53
+ };
54
+
55
+ /** The pinned half of Release.Binary's props; the stack adds `directory`, `mode` and owner. */
56
+ export type PinnedBinary = Pick<ReleaseBinaryProps, 'archive' | 'member' | 'sha256' | 'name'>;
57
+
58
+ const quoted = (names: Iterable<string>): string => [...names].map((n) => `"${n}"`).join(', ');
59
+
60
+ // ★ A plain boolean, not a type guard: on a `Record<string, …>` a guard narrows the miss to `never`.
61
+ const has = (record: object, key: string): boolean => Object.hasOwn(record, key);
62
+
63
+ /** Everything wrong with a request against a catalog; empty when it resolves. */
64
+ export const catalogProblems = (catalog: ReleaseCatalog, request: CatalogRequest): string[] => {
65
+ const entry = has(catalog.packages, request.package)
66
+ ? catalog.packages[request.package]
67
+ : undefined;
68
+ if (entry === undefined) {
69
+ return [
70
+ `${catalog.vendor} package "${request.package}" is not in the catalog (${quoted(Object.keys(catalog.packages))})`,
71
+ ];
72
+ }
73
+ const found: string[] = [];
74
+ if (!has(entry.binaries, request.binary)) {
75
+ // ⚠️ The vendor spelling is the likeliest mistake: the member is `vmalert-prod`, the binary
76
+ // this installs is `vmalert`.
77
+ const member = Object.values(entry.binaries).includes(request.binary);
78
+ const hint = member ? ' (that is the archive member; name the installed binary)' : '';
79
+ found.push(
80
+ `${request.package} has no binary "${request.binary}"${hint}; it has ${quoted(Object.keys(entry.binaries))}`,
81
+ );
82
+ }
83
+ const platforms = has(entry.versions, request.version)
84
+ ? entry.versions[request.version]
85
+ : undefined;
86
+ if (platforms === undefined) {
87
+ found.push(
88
+ `${request.package} ${request.version} is not in the catalog (it pins ${quoted(Object.keys(entry.versions))}); ` +
89
+ 'a new version is a reviewed change that pins its digests, never a download of whatever is there',
90
+ );
91
+ return found;
92
+ }
93
+ const archive = has(platforms, request.platform) ? platforms[request.platform] : undefined;
94
+ if (archive === undefined) {
95
+ found.push(
96
+ `${request.package} ${request.version} is not pinned for platform "${request.platform}" (${quoted(Object.keys(platforms))})`,
97
+ );
98
+ } else if (found.length === 0) {
99
+ const member = entry.binaries[request.binary] ?? '';
100
+ if (!has(archive.members, member)) {
101
+ found.push(
102
+ `${request.package} ${request.version}'s checksum file lists no member "${member}"`,
103
+ );
104
+ }
105
+ }
106
+ return found;
107
+ };
108
+
109
+ /**
110
+ * The pinned props for one binary out of a catalog. ⛔ Throws BinaryRefused for anything the
111
+ * catalog does not pin — in a stack program, that fails the plan before anything is fetched.
112
+ */
113
+ export const catalogBinary = (catalog: ReleaseCatalog, request: CatalogRequest): PinnedBinary => {
114
+ const problems = catalogProblems(catalog, request);
115
+ // ⚠️ Own keys only: `packages.constructor` is Object's, and would read as a package.
116
+ const own = <V>(record: Readonly<Record<string, V>> | undefined, key: string): V | undefined =>
117
+ record !== undefined && has(record, key) ? record[key] : undefined;
118
+ const entry = own(catalog.packages, request.package);
119
+ const archive = own(own(entry?.versions, request.version), request.platform);
120
+ const member = own(entry?.binaries, request.binary);
121
+ const sha256 = member === undefined ? undefined : own(archive?.members, member);
122
+ if (
123
+ problems.length > 0 ||
124
+ archive === undefined ||
125
+ member === undefined ||
126
+ sha256 === undefined
127
+ ) {
128
+ throw new BinaryRefused({
129
+ message: `Release.Binary ${request.binary}: ${problems.join('; ')}. Nothing was fetched.`,
130
+ });
131
+ }
132
+ const { asset, repo, sha256: archiveSha256, size, tag } = archive;
133
+ return {
134
+ archive: { asset, repo, sha256: archiveSha256, size, tag },
135
+ member,
136
+ name: request.binary,
137
+ sha256,
138
+ };
139
+ };
140
+
141
+ /** `<root>/<package>-<version>`: one directory per version, so an upgrade is a new path. */
142
+ export const catalogDirectory = (
143
+ root: string,
144
+ request: Pick<CatalogRequest, 'package' | 'version'>,
145
+ ): string => `${root}/${request.package}-${request.version}`;
146
+
147
+ /** What a catalog says an installed binary is, found by its SHA-256 alone. */
148
+ export type IdentifiedBinary = CatalogRequest & { readonly vendor: string };
149
+
150
+ /**
151
+ * Name an installed file by its digest — never by running it. ★ IDENTITY BY SHA-256: the running
152
+ * Nix victoria-traces reports an empty version (`vm_app_version{version=""}` on /metrics, measured
153
+ * 2026-09-22 — its build sets no buildinfo), a stamped build prints `-version` to STDERR, not
154
+ * stdout (lib/buildinfo, read, not run), and executing a binary to learn what it is runs the very
155
+ * bytes whose provenance is the question. A digest needs a read, nothing more.
156
+ */
157
+ export const identifyBinary = (
158
+ catalogs: readonly ReleaseCatalog[],
159
+ sha256: string,
160
+ ): IdentifiedBinary | undefined => {
161
+ for (const catalog of catalogs) {
162
+ for (const [pkg, entry] of Object.entries(catalog.packages)) {
163
+ for (const [version, platforms] of Object.entries(entry.versions)) {
164
+ for (const [platform, archive] of Object.entries(platforms)) {
165
+ for (const [binary, member] of Object.entries(entry.binaries)) {
166
+ if (archive.members[member] === sha256) {
167
+ return { binary, package: pkg, platform, vendor: catalog.vendor, version };
168
+ }
169
+ }
170
+ }
171
+ }
172
+ }
173
+ }
174
+ return undefined;
175
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The checksum-file parser, held to the one format it was measured on (`sha256sum` text mode,
3
+ * 2026-09-22): anything else is a vendor change to read before a pin moves, so it throws.
4
+ */
5
+ import { describe, expect, test } from 'bun:test';
6
+ import { parseChecksums } from './checksums.ts';
7
+
8
+ const line = `${'a'.repeat(64)} vmagent-prod`;
9
+
10
+ describe('parseChecksums is strict', () => {
11
+ test('the measured shape: name → digest, in file order', () => {
12
+ const text = `${'b'.repeat(64)} vmutils.tar.gz\n${line}\n`;
13
+ expect([...parseChecksums(text).entries()]).toEqual([
14
+ ['vmutils.tar.gz', 'b'.repeat(64)],
15
+ ['vmagent-prod', 'a'.repeat(64)],
16
+ ]);
17
+ });
18
+
19
+ test.each([
20
+ ['CRLF endings', `${line}\r\n`],
21
+ ['a binary-mode marker', `${'a'.repeat(64)} *vmagent-prod\n`],
22
+ ['upper-case hex', `${'A'.repeat(64)} vmagent-prod\n`],
23
+ ['a blank line', `${line}\n\n`],
24
+ ['no trailing newline', line],
25
+ ['a name listed twice', `${line}\n${line}\n`],
26
+ ['a path in a name', `${'a'.repeat(64)} bin/vmagent-prod\n`],
27
+ ['an empty file', '\n'],
28
+ ['a tab for the two spaces', `${'a'.repeat(64)}\tvmagent-prod\n`],
29
+ ['one space', `${'a'.repeat(64)} vmagent-prod\n`],
30
+ ['a trailing space (a name that merely starts with another)', `${line} \n`],
31
+ ['a no-break space in the name', `${line}\u00a0-enterprise\n`],
32
+ ])('refuses %s', (_, text) => {
33
+ expect(() => parseChecksums(text)).toThrow();
34
+ });
35
+ });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A vendor checksum file in `sha256sum` text mode, parsed strictly — the shape every pin in
3
+ * victoria.ts was copied from.
4
+ *
5
+ * ★ WHY PARSE A FILE THE PROVIDER NEVER FETCHES. The pins are the ground truth at apply; this is
6
+ * the ground truth when a pin is WRITTEN. victoria.test.ts feeds it the exact bytes of all four
7
+ * recorded files (fixtures/victoria/, each matching GitHub's digest of that asset AS READ on the
8
+ * recorded date — no test re-reads it) and requires the catalog to equal what it reads, so a pin
9
+ * typed by hand, a line missed, or a member added by the vendor is a failing test rather than a
10
+ * trusted typo.
11
+ * ⛔ STRICT, BECAUSE THE FORMAT IS KNOWN. Measured 2026-09-22 on all four files: `sha256sum` text
12
+ * mode — `<64 lowercase hex><two spaces><name>` per line, LF endings, a trailing newline, the
13
+ * archive on line 1 and one `-prod` member per line after it. Anything else (a `*` binary-mode
14
+ * marker, CRLF, upper-case hex, a blank line, a repeated name) is a refusal: a file that no longer
15
+ * has this shape is a vendor change to read before any pin moves, not something to guess past.
16
+ * ⚠️ ONE FORMAT, MEASURED FOR ONE VENDOR. Other vendors list archives only (Prometheus, vector,
17
+ * pyroscope, OpenBao — the estate census, 2026-09-22) or publish digests in JSON (grafana.com). A
18
+ * catalog for one of them reads ITS format byte-exact first; nothing here is assumed to fit it.
19
+ */
20
+
21
+ const LINE = /^([0-9a-f]{64}) {2}([^\s/]+)$/;
22
+
23
+ /** Name → SHA-256, in file order. Throws, naming the line, on anything but the measured shape. */
24
+ export const parseChecksums = (text: string): ReadonlyMap<string, string> => {
25
+ if (!text.endsWith('\n')) throw new Error('checksum file: no trailing newline');
26
+ const lines = text.slice(0, -1).split('\n');
27
+ const digests = new Map<string, string>();
28
+ for (const [index, line] of lines.entries()) {
29
+ const match = LINE.exec(line);
30
+ if (match === null) {
31
+ throw new Error(
32
+ `checksum file line ${String(index + 1)}: not "<sha256> <name>": ${JSON.stringify(line)}`,
33
+ );
34
+ }
35
+ const [, digest = '', name = ''] = match;
36
+ if (digests.has(name)) {
37
+ throw new Error(`checksum file line ${String(index + 1)}: "${name}" is listed twice`);
38
+ }
39
+ digests.set(name, digest);
40
+ }
41
+ if (digests.size === 0) throw new Error('checksum file: no lines');
42
+ return digests;
43
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The pins as the STACK PROGRAM declared them — before the engine resolved a single Output — checked
3
+ * at apply. The half of "a pin is never computed during the deploy" that a FIRST deploy needs.
4
+ *
5
+ * ⛔ WHY APPLY HAS TO ASK THE STACK, NOT `news`. By the time `reconcile` runs, the engine has
6
+ * resolved every Output in `news`, so a digest wired from another resource (a checksum file read
7
+ * at apply, a lookup, a computed value) arrives as a well-formed plain string, indistinguishable
8
+ * from a reviewed pin. And on a first deploy nothing else looked: Alchemy never diffs a create,
9
+ * and skips the adoption probe while `news` holds an Output (beta.79 Plan.ts). 🔴 MEASURED
10
+ * 2026-09-22 (apply-pins.test.ts): a swapped archive plus the swapped digests, wired in as an
11
+ * Output, installed the swapped bytes and the deploy reported success. binary-diff.ts refused the
12
+ * same declaration only from the SECOND plan on, after the wrong binary was on the host.
13
+ * ★ `Stack.resources[fqn].Props` IS WHAT THE PROGRAM PASSED, Outputs and all: Alchemy's
14
+ * `Resource` records it at registration (beta.79 Resource.ts, `Props: props`), and the same Stack
15
+ * reaches the apply (ownership/adopt.ts reads `Adopt` from the same record for the same reason).
16
+ * pinProblems then sees the Output itself — a function-typed proxy, an Effect or a Config — and
17
+ * refuses it as "not a plain string".
18
+ * ⚠️ NO STACK, NO ANSWER. A handler called outside the engine (the fake-provider.ts harness) has no
19
+ * Stack service and no registration to read, so this says nothing there; the engine always has
20
+ * one. apply-pins.test.ts runs the real Plan and Apply, so an Alchemy upgrade that stops
21
+ * recording `Props` there makes the swapped bytes install again, and fails that test by name.
22
+ */
23
+ import { Stack } from 'alchemy/Stack';
24
+ import * as Effect from 'effect/Effect';
25
+ import * as Option from 'effect/Option';
26
+ import { pinProblems } from './binary-form.ts';
27
+
28
+ /** Everything wrong with the pins of the resource at `fqn` as its stack program declared them. */
29
+ export const declaredPinProblems = (fqn: string): Effect.Effect<string[]> =>
30
+ Effect.gen(function* () {
31
+ const stack = yield* Effect.serviceOption(Stack);
32
+ if (Option.isNone(stack)) return [];
33
+ const declared: unknown = stack.value.resources[fqn]?.Props;
34
+ if (typeof declared !== 'object' && typeof declared !== 'function') return [];
35
+ if (declared === null) return [];
36
+ return pinProblems(declared as Parameters<typeof pinProblems>[0]).map(
37
+ (problem) => `as declared in the stack program, ${problem}`,
38
+ );
39
+ });