@homeflare/alchemy 0.19.1 → 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.
- package/README.md +25 -0
- package/dist/caddy/access-forward-auth.d.ts +56 -0
- package/dist/caddy/access-forward-auth.d.ts.map +1 -0
- package/dist/caddy/index.d.ts +6 -0
- package/dist/caddy/index.d.ts.map +1 -1
- package/dist/caddy/index.js +169 -75
- package/dist/caddy/index.js.map +10 -7
- package/dist/cloudflare/mcp-server-api.d.ts +135 -0
- package/dist/cloudflare/mcp-server-api.d.ts.map +1 -0
- package/dist/cloudflare/mcp-server-form.d.ts +116 -0
- package/dist/cloudflare/mcp-server-form.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/launchd/file-converge.d.ts +70 -0
- package/dist/launchd/file-converge.d.ts.map +1 -0
- package/dist/launchd/file-identity.d.ts +32 -0
- package/dist/launchd/file-identity.d.ts.map +1 -0
- package/dist/launchd/host-file-form.d.ts +2 -0
- package/dist/launchd/host-file-form.d.ts.map +1 -1
- package/dist/launchd/host-file-lifecycle.d.ts +11 -24
- package/dist/launchd/host-file-lifecycle.d.ts.map +1 -1
- package/dist/launchd/host-file.d.ts.map +1 -1
- package/dist/launchd/index.js +173 -120
- package/dist/launchd/index.js.map +12 -10
- package/dist/launchd/local-runner.d.ts.map +1 -1
- package/dist/launchd/runner.d.ts +17 -2
- package/dist/launchd/runner.d.ts.map +1 -1
- package/dist/launchd/sudo-runner.d.ts +11 -0
- package/dist/launchd/sudo-runner.d.ts.map +1 -1
- package/dist/linux/index.js +37 -11
- package/dist/linux/index.js.map +10 -10
- package/dist/linux/region.d.ts +11 -0
- package/dist/linux/region.d.ts.map +1 -1
- package/dist/linux/remote-file-lifecycle.d.ts.map +1 -1
- package/dist/linux/remote-file-plan.d.ts +22 -0
- package/dist/linux/remote-file-plan.d.ts.map +1 -1
- package/dist/linux/remote-file.d.ts +4 -1
- package/dist/linux/remote-file.d.ts.map +1 -1
- package/dist/linux/sudo-listing.d.ts +47 -0
- package/dist/linux/sudo-listing.d.ts.map +1 -0
- package/dist/linux/unit-handlers.d.ts.map +1 -1
- package/dist/linux/unit-preflight.d.ts +10 -1
- package/dist/linux/unit-preflight.d.ts.map +1 -1
- package/dist/proxmox/credential-literals.d.ts +1 -1
- package/dist/proxmox/credential-literals.d.ts.map +1 -1
- package/dist/proxmox/generated/pbs/nodes-node.d.ts +1 -1
- package/dist/proxmox/generated/pbs/nodes-node.d.ts.map +1 -1
- package/dist/proxmox/generated/pbs.d.ts +7 -0
- package/dist/proxmox/generated/pbs.d.ts.map +1 -1
- package/dist/proxmox/generated/pve.d.ts +7 -0
- package/dist/proxmox/generated/pve.d.ts.map +1 -1
- package/dist/proxmox/index.d.ts +16 -4
- package/dist/proxmox/index.d.ts.map +1 -1
- package/dist/proxmox/index.js +5 -2
- package/dist/proxmox/index.js.map +9 -9
- package/dist/proxmox/pbs-notification-target-form.d.ts +1 -1
- package/dist/proxmox/pbs-notification-target-form.d.ts.map +1 -1
- package/dist/proxmox/pbs-notification-target.d.ts +1 -1
- package/dist/proxmox/pbs-notification-target.d.ts.map +1 -1
- package/dist/release/archive.d.ts +11 -0
- package/dist/release/archive.d.ts.map +1 -0
- package/dist/release/binary-claim.d.ts +53 -0
- package/dist/release/binary-claim.d.ts.map +1 -0
- package/dist/release/binary-diff.d.ts +38 -0
- package/dist/release/binary-diff.d.ts.map +1 -0
- package/dist/release/binary-form.d.ts +118 -0
- package/dist/release/binary-form.d.ts.map +1 -0
- package/dist/release/binary-lifecycle.d.ts +55 -0
- package/dist/release/binary-lifecycle.d.ts.map +1 -0
- package/dist/release/binary-read.d.ts +11 -0
- package/dist/release/binary-read.d.ts.map +1 -0
- package/dist/release/binary.d.ts +68 -0
- package/dist/release/binary.d.ts.map +1 -0
- package/dist/release/catalog.d.ts +76 -0
- package/dist/release/catalog.d.ts.map +1 -0
- package/dist/release/checksums.d.ts +22 -0
- package/dist/release/checksums.d.ts.map +1 -0
- package/dist/release/declared-pins.d.ts +4 -0
- package/dist/release/declared-pins.d.ts.map +1 -0
- package/dist/release/download.d.ts +44 -0
- package/dist/release/download.d.ts.map +1 -0
- package/dist/release/fake-deploy.d.ts +48 -0
- package/dist/release/fake-deploy.d.ts.map +1 -0
- package/dist/release/fake-engine.d.ts +8 -0
- package/dist/release/fake-engine.d.ts.map +1 -0
- package/dist/release/fake-install.d.ts +39 -0
- package/dist/release/fake-install.d.ts.map +1 -0
- package/dist/release/fake-provider.d.ts +58 -0
- package/dist/release/fake-provider.d.ts.map +1 -0
- package/dist/release/fake-release.d.ts +95 -0
- package/dist/release/fake-release.d.ts.map +1 -0
- package/dist/release/index.d.ts +24 -0
- package/dist/release/index.d.ts.map +1 -0
- package/dist/release/index.js +1365 -0
- package/dist/release/index.js.map +35 -0
- package/dist/release/providers.d.ts +19 -0
- package/dist/release/providers.d.ts.map +1 -0
- package/dist/release/refused.d.ts +39 -0
- package/dist/release/refused.d.ts.map +1 -0
- package/dist/release/tar.d.ts +14 -0
- package/dist/release/tar.d.ts.map +1 -0
- package/dist/release/victoria.d.ts +36 -0
- package/dist/release/victoria.d.ts.map +1 -0
- package/dist/{proxmox → secrets}/write-only.d.ts.map +1 -1
- package/docs/adopted-deploys.md +13 -0
- package/docs/linux-host.md +5 -1
- package/docs/provider-standard.md +138 -0
- package/docs/release-binary-catalogs.md +113 -0
- package/docs/release-binary-measured.md +163 -0
- package/docs/release-binary-refusals.md +57 -0
- package/docs/release-binary-upstream.md +163 -0
- package/docs/release-binary.md +154 -0
- package/docs/upstream-conformance.md +164 -0
- package/package.json +5 -1
- package/src/caddy/access-forward-auth.test.ts +63 -0
- package/src/caddy/access-forward-auth.ts +120 -0
- package/src/caddy/index.ts +10 -0
- package/src/caddy/providers.test.ts +4 -0
- package/src/cloudflare/mcp-server-api.ts +294 -0
- package/src/cloudflare/mcp-server-form.ts +219 -0
- package/src/index.ts +1 -0
- package/src/launchd/file-converge.ts +233 -0
- package/src/launchd/file-identity.ts +69 -0
- package/src/launchd/host-file-alias.test.ts +65 -0
- package/src/launchd/host-file-form.ts +2 -1
- package/src/launchd/host-file-lifecycle.ts +25 -144
- package/src/launchd/host-file.ts +8 -2
- package/src/launchd/local-runner.test.ts +12 -1
- package/src/launchd/local-runner.ts +2 -0
- package/src/launchd/runner.ts +17 -2
- package/src/launchd/sudo-runner.ts +11 -0
- package/src/linux/region.ts +15 -0
- package/src/linux/remote-file-lifecycle.test.ts +0 -88
- package/src/linux/remote-file-lifecycle.ts +25 -7
- package/src/linux/remote-file-plan.ts +37 -1
- package/src/linux/remote-file-region.test.ts +181 -0
- package/src/linux/remote-file.ts +4 -1
- package/src/linux/sudo-listing.ts +122 -0
- package/src/linux/unit-handlers.ts +12 -5
- package/src/linux/unit-preflight.ts +20 -4
- package/src/linux/unit-rename.test.ts +76 -0
- package/src/proxmox/ceph-adopt.test.ts +210 -0
- package/src/proxmox/credential-literals.ts +1 -1
- package/src/proxmox/generated/pbs/nodes-node.ts +1 -1
- package/src/proxmox/generated/pbs.ts +7 -0
- package/src/proxmox/generated/pve.ts +7 -0
- package/src/proxmox/index.test.ts +7 -4
- package/src/proxmox/index.ts +16 -4
- package/src/proxmox/pbs-notification-target-form.ts +1 -1
- package/src/proxmox/pbs-notification-target-lifecycle.ts +1 -1
- package/src/proxmox/pbs-notification-target-wire.ts +1 -1
- package/src/proxmox/pbs-notification-target.ts +1 -1
- package/src/release/adopt-parity.test.ts +106 -0
- package/src/release/apply-pins.test.ts +128 -0
- package/src/release/archive.ts +94 -0
- package/src/release/binary-alias.test.ts +109 -0
- package/src/release/binary-claim.ts +80 -0
- package/src/release/binary-claims.test.ts +47 -0
- package/src/release/binary-diff.ts +73 -0
- package/src/release/binary-form.ts +229 -0
- package/src/release/binary-lifecycle.test.ts +90 -0
- package/src/release/binary-lifecycle.ts +202 -0
- package/src/release/binary-read.ts +55 -0
- package/src/release/binary-refusals.test.ts +192 -0
- package/src/release/binary-sudo.test.ts +93 -0
- package/src/release/binary.ts +181 -0
- package/src/release/catalog.test.ts +96 -0
- package/src/release/catalog.ts +175 -0
- package/src/release/checksums.test.ts +35 -0
- package/src/release/checksums.ts +43 -0
- package/src/release/declared-pins.ts +39 -0
- package/src/release/download.test.ts +138 -0
- package/src/release/download.ts +110 -0
- package/src/release/error-channel.test.ts +69 -0
- package/src/release/fake-deploy.ts +87 -0
- package/src/release/fake-engine.ts +50 -0
- package/src/release/fake-install.ts +59 -0
- package/src/release/fake-provider.ts +68 -0
- package/src/release/fake-release.ts +185 -0
- package/src/release/fixtures/victoria/darwin-arm64-assets.txt +22 -0
- package/src/release/fixtures/victoria/victoria-logs-darwin-arm64-v1.52.0_checksums.txt +2 -0
- package/src/release/fixtures/victoria/victoria-metrics-darwin-arm64-v1.151.0_checksums.txt +2 -0
- package/src/release/fixtures/victoria/victoria-traces-darwin-arm64-v0.10.0_checksums.txt +2 -0
- package/src/release/fixtures/victoria/vmutils-darwin-arm64-v1.151.0_checksums.txt +8 -0
- package/src/release/index.ts +41 -0
- package/src/release/plan.test.ts +189 -0
- package/src/release/provider-refusals.test.ts +48 -0
- package/src/release/providers.test.ts +174 -0
- package/src/release/providers.ts +24 -0
- package/src/release/recovery-read.test.ts +44 -0
- package/src/release/refused.ts +40 -0
- package/src/release/tar.test.ts +142 -0
- package/src/release/tar.ts +195 -0
- package/src/release/victoria.test.ts +108 -0
- package/src/release/victoria.ts +154 -0
- package/src/{proxmox → secrets}/write-only.ts +6 -0
- /package/dist/{proxmox → secrets}/write-only.d.ts +0 -0
- /package/src/{proxmox → secrets}/write-only.test.ts +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tar reader with one job: hand back the NAMED regular files of a vendor archive, and refuse the
|
|
3
|
+
* whole archive if any entry could make a name mean something other than what it says.
|
|
4
|
+
*
|
|
5
|
+
* ★ IN PROCESS, NOT `tar` ON THE HOST. An exec'd tar writes through its own path — past the
|
|
6
|
+
* HostRunner seam, past sudoRunner's allowlist, and past the member digest check, which has to
|
|
7
|
+
* see the bytes BEFORE they exist on any disk. Nothing this file reads is ever written by it.
|
|
8
|
+
* ★ STREAMING. It is fed the gunzip output chunk by chunk; an entry nobody asked for is counted
|
|
9
|
+
* past, never held. vmutils unpacks to 264 MB across seven binaries (measured 2026-09-22) and a
|
|
10
|
+
* stack wants two, so the unpacked archive never exists whole in memory.
|
|
11
|
+
* ⛔ REFUSED, WHOLE ARCHIVE, whichever entry it is and whether or not it was asked for:
|
|
12
|
+
* an absolute name; a `..` segment; a hard link, symlink, directory, device, FIFO or contiguous
|
|
13
|
+
* file (only a regular file is accepted); a name given twice; a header whose checksum fails; a
|
|
14
|
+
* size in GNU base-256; a header that is neither ustar nor GNU; a lone zero block between entries;
|
|
15
|
+
* non-zero bytes after the end marker; an archive that stops before it.
|
|
16
|
+
* ★ GNU LONG NAMES ('L', 'K') AND PAX HEADERS ('x', 'g') ARE REFUSED, NOT INTERPRETED. Each renames
|
|
17
|
+
* the entry after it: a reader that honours them must get them exactly right to match exactly,
|
|
18
|
+
* and one that skips them matches under the wrong name. The four pinned archives use neither
|
|
19
|
+
* (measured 2026-09-22 with `tar -tvzf` on each: every entry a root-level regular file with a
|
|
20
|
+
* short name), so refusing costs nothing today, and a vendor format change becomes a refusal that
|
|
21
|
+
* says what changed instead of a silent mis-read.
|
|
22
|
+
*/
|
|
23
|
+
import { ArchiveRefused } from './refused.ts';
|
|
24
|
+
|
|
25
|
+
const BLOCK = 512;
|
|
26
|
+
|
|
27
|
+
const KINDS: Readonly<Record<string, string>> = {
|
|
28
|
+
'1': 'a hard link',
|
|
29
|
+
'2': 'a symlink',
|
|
30
|
+
'3': 'a character device',
|
|
31
|
+
'4': 'a block device',
|
|
32
|
+
'5': 'a directory',
|
|
33
|
+
'6': 'a FIFO',
|
|
34
|
+
'7': 'a contiguous file',
|
|
35
|
+
K: 'a GNU long-link header',
|
|
36
|
+
L: 'a GNU long-name header',
|
|
37
|
+
g: 'a PAX global header',
|
|
38
|
+
x: 'a PAX extended header',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const refuse = (message: string): ArchiveRefused => new ArchiveRefused({ message });
|
|
42
|
+
const strict = new TextDecoder('utf-8', { fatal: true });
|
|
43
|
+
const ascii = new TextDecoder('latin1');
|
|
44
|
+
|
|
45
|
+
/** One NUL-terminated header field. */
|
|
46
|
+
const field = (block: Uint8Array, start: number, length: number): Uint8Array => {
|
|
47
|
+
const slice = block.subarray(start, start + length);
|
|
48
|
+
const end = slice.indexOf(0);
|
|
49
|
+
return end === -1 ? slice : slice.subarray(0, end);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const text = (block: Uint8Array, start: number, length: number): string => {
|
|
53
|
+
try {
|
|
54
|
+
return strict.decode(field(block, start, length));
|
|
55
|
+
} catch {
|
|
56
|
+
throw refuse('an entry name is not UTF-8');
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const octal = (block: Uint8Array, start: number, length: number, what: string): number => {
|
|
61
|
+
const raw = block.subarray(start, start + length);
|
|
62
|
+
if (((raw[0] ?? 0) & 0x80) !== 0) throw refuse(`${what} is base-256 encoded`);
|
|
63
|
+
const digits = ascii
|
|
64
|
+
.decode(raw)
|
|
65
|
+
.replace(/[\0 ]+$/, '')
|
|
66
|
+
.replace(/^ +/, '');
|
|
67
|
+
if (!/^[0-7]+$/.test(digits)) throw refuse(`${what} is not octal: ${JSON.stringify(digits)}`);
|
|
68
|
+
return Number.parseInt(digits, 8);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const checksumHolds = (block: Uint8Array): boolean => {
|
|
72
|
+
let sum = 0;
|
|
73
|
+
// The checksum field itself counts as eight spaces (POSIX ustar).
|
|
74
|
+
for (let at = 0; at < BLOCK; at += 1) sum += at >= 148 && at < 156 ? 0x20 : (block[at] ?? 0);
|
|
75
|
+
return sum === octal(block, 148, 8, 'a header checksum');
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Why an entry name is unsafe to trust, or undefined. */
|
|
79
|
+
const nameProblem = (name: string): string | undefined => {
|
|
80
|
+
if (name === '') return 'has an empty name';
|
|
81
|
+
if (name.startsWith('/')) return 'is absolute';
|
|
82
|
+
if (name.split('/').includes('..')) return 'climbs out of the archive with ".."';
|
|
83
|
+
return undefined;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** Concatenate chunks into one buffer. */
|
|
87
|
+
export const concatBytes = (parts: readonly Uint8Array[]): Uint8Array => {
|
|
88
|
+
const out = new Uint8Array(parts.reduce((total, part) => total + part.length, 0));
|
|
89
|
+
let at = 0;
|
|
90
|
+
for (const part of parts) {
|
|
91
|
+
out.set(part, at);
|
|
92
|
+
at += part.length;
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type TarContents = {
|
|
98
|
+
/** The asked-for regular files, by exact name. An asked-for name that is absent is absent here. */
|
|
99
|
+
readonly members: ReadonlyMap<string, Uint8Array>;
|
|
100
|
+
/** Every entry name, in archive order — so a refusal can say what the archive did hold. */
|
|
101
|
+
readonly names: readonly string[];
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** A push parser: `push` each chunk of the unzipped stream, then `finish`. Either may throw. */
|
|
105
|
+
export const tarReader = (wanted: ReadonlySet<string>) => {
|
|
106
|
+
const header = new Uint8Array(BLOCK);
|
|
107
|
+
const members = new Map<string, Uint8Array>();
|
|
108
|
+
const names: string[] = [];
|
|
109
|
+
const seen = new Set<string>();
|
|
110
|
+
let phase: 'header' | 'data' | 'pad' | 'end' = 'header';
|
|
111
|
+
let filled = 0;
|
|
112
|
+
let remaining = 0;
|
|
113
|
+
let padding = 0;
|
|
114
|
+
let zeros = 0;
|
|
115
|
+
let name = '';
|
|
116
|
+
let parts: Uint8Array[] | undefined;
|
|
117
|
+
|
|
118
|
+
const endEntry = () => {
|
|
119
|
+
if (parts !== undefined) members.set(name, concatBytes(parts));
|
|
120
|
+
parts = undefined;
|
|
121
|
+
phase = padding > 0 ? 'pad' : 'header';
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const startEntry = () => {
|
|
125
|
+
if (header.every((byte) => byte === 0)) {
|
|
126
|
+
zeros += 1;
|
|
127
|
+
if (zeros === 2) phase = 'end';
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (zeros > 0) throw refuse('a zero block sits between entries (truncated or spliced)');
|
|
131
|
+
if (!checksumHolds(header)) throw refuse('an entry header fails its checksum');
|
|
132
|
+
const magic = ascii.decode(header.subarray(257, 265));
|
|
133
|
+
const posix = magic === 'ustar\u000000';
|
|
134
|
+
if (!posix && magic !== 'ustar \u0000')
|
|
135
|
+
throw refuse('an entry header is neither ustar nor GNU');
|
|
136
|
+
const prefix = posix ? text(header, 345, 155) : '';
|
|
137
|
+
name = prefix === '' ? text(header, 0, 100) : `${prefix}/${text(header, 0, 100)}`;
|
|
138
|
+
const problem = nameProblem(name);
|
|
139
|
+
if (problem !== undefined) throw refuse(`entry ${JSON.stringify(name)} ${problem}`);
|
|
140
|
+
const type = String.fromCharCode(header[156] ?? 0);
|
|
141
|
+
if (type !== '0' && type !== '\0') {
|
|
142
|
+
const kind = KINDS[type] ?? `of unknown type ${JSON.stringify(type)}`;
|
|
143
|
+
throw refuse(`entry "${name}" is ${kind}; only regular files are accepted`);
|
|
144
|
+
}
|
|
145
|
+
if (seen.has(name)) throw refuse(`entry "${name}" appears twice`);
|
|
146
|
+
seen.add(name);
|
|
147
|
+
names.push(name);
|
|
148
|
+
remaining = octal(header, 124, 12, `entry "${name}"'s size`);
|
|
149
|
+
padding = (BLOCK - (remaining % BLOCK)) % BLOCK;
|
|
150
|
+
parts = wanted.has(name) ? [] : undefined;
|
|
151
|
+
phase = 'data';
|
|
152
|
+
if (remaining === 0) endEntry();
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const push = (chunk: Uint8Array): void => {
|
|
156
|
+
let at = 0;
|
|
157
|
+
while (at < chunk.length) {
|
|
158
|
+
if (phase === 'end') {
|
|
159
|
+
if (chunk.subarray(at).some((byte) => byte !== 0)) {
|
|
160
|
+
throw refuse('data follows the end-of-archive marker');
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (phase === 'header') {
|
|
165
|
+
const take = Math.min(BLOCK - filled, chunk.length - at);
|
|
166
|
+
header.set(chunk.subarray(at, at + take), filled);
|
|
167
|
+
filled += take;
|
|
168
|
+
at += take;
|
|
169
|
+
if (filled === BLOCK) {
|
|
170
|
+
filled = 0;
|
|
171
|
+
startEntry();
|
|
172
|
+
}
|
|
173
|
+
} else if (phase === 'data') {
|
|
174
|
+
const take = Math.min(remaining, chunk.length - at);
|
|
175
|
+
// ⚠️ slice, not subarray: a chunk's buffer may be reused by the stream after this returns.
|
|
176
|
+
parts?.push(chunk.slice(at, at + take));
|
|
177
|
+
remaining -= take;
|
|
178
|
+
at += take;
|
|
179
|
+
if (remaining === 0) endEntry();
|
|
180
|
+
} else {
|
|
181
|
+
const take = Math.min(padding, chunk.length - at);
|
|
182
|
+
padding -= take;
|
|
183
|
+
at += take;
|
|
184
|
+
if (padding === 0) phase = 'header';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const finish = (): TarContents => {
|
|
190
|
+
if (phase !== 'end') throw refuse('the archive stops before its end-of-archive marker');
|
|
191
|
+
return { members, names };
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
return { finish, push };
|
|
195
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The REAL Victoria data set against the vendor's own files — no network: fixtures/victoria/ holds
|
|
3
|
+
* the four checksum files byte-for-byte as fetched on 2026-09-22 (each one's SHA-256 equalled
|
|
4
|
+
* GitHub's asset digest for it that day; the data set records it), and the darwin-arm64 asset
|
|
5
|
+
* names the release API listed.
|
|
6
|
+
*
|
|
7
|
+
* ★ THE CHAIN THIS PROVES: fixture bytes = the recorded digest of the vendor file → the parser
|
|
8
|
+
* reads exactly its lines → the data set equals those lines. A pin typed by hand, a member missed
|
|
9
|
+
* or a line the vendor added fails here, not on a host.
|
|
10
|
+
* ⚠️ CONSISTENCY, NOT PROVENANCE. No network here: that the recorded digest is GITHUB'S was measured
|
|
11
|
+
* once, on the recorded date. A change that edits a fixture and its pins together passes; review
|
|
12
|
+
* is the gate for that.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, expect, test } from 'bun:test';
|
|
15
|
+
import { sha256Hex } from '../launchd/job-form.ts';
|
|
16
|
+
import { releaseUrl } from './binary-form.ts';
|
|
17
|
+
import { catalogBinary } from './catalog.ts';
|
|
18
|
+
import { parseChecksums } from './checksums.ts';
|
|
19
|
+
import { VICTORIA_RELEASES } from './victoria.ts';
|
|
20
|
+
|
|
21
|
+
const fixture = (name: string) => Bun.file(new URL(`fixtures/victoria/${name}`, import.meta.url));
|
|
22
|
+
const assets = (await fixture('darwin-arm64-assets.txt').text()).trim().split('\n');
|
|
23
|
+
const REPOS: Readonly<Record<string, string>> = {
|
|
24
|
+
'victoria-logs': 'VictoriaMetrics/VictoriaLogs',
|
|
25
|
+
'victoria-metrics': 'VictoriaMetrics/VictoriaMetrics',
|
|
26
|
+
'victoria-traces': 'VictoriaMetrics/VictoriaTraces',
|
|
27
|
+
vmutils: 'VictoriaMetrics/VictoriaMetrics',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const archives = Object.entries(VICTORIA_RELEASES.packages).flatMap(([pkg, entry]) =>
|
|
31
|
+
Object.entries(entry.versions).flatMap(([version, platforms]) =>
|
|
32
|
+
Object.entries(platforms).map(([platform, archive]) => ({
|
|
33
|
+
archive,
|
|
34
|
+
entry,
|
|
35
|
+
pkg,
|
|
36
|
+
platform,
|
|
37
|
+
version,
|
|
38
|
+
})),
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
describe('the Victoria data set is the vendor checksum files', () => {
|
|
43
|
+
test('it pins the four archives the Mac host runs, and nothing else', () => {
|
|
44
|
+
expect(archives.map((a) => `${a.pkg} ${a.version} ${a.platform}`).sort()).toEqual([
|
|
45
|
+
'victoria-logs 1.52.0 darwin-arm64',
|
|
46
|
+
'victoria-metrics 1.151.0 darwin-arm64',
|
|
47
|
+
'victoria-traces 0.10.0 darwin-arm64',
|
|
48
|
+
'vmutils 1.151.0 darwin-arm64',
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test.each(archives.map((a) => [`${a.pkg} ${a.version}`, a] as const))('%s', async (_, a) => {
|
|
53
|
+
const asset = `${a.pkg}-${a.platform}-v${a.version}.tar.gz`;
|
|
54
|
+
const sums = `${a.pkg}-${a.platform}-v${a.version}_checksums.txt`;
|
|
55
|
+
// ⛔ Exact, whole names — and the names the release API actually lists.
|
|
56
|
+
expect([a.archive.repo, a.archive.tag, a.archive.asset]).toEqual([
|
|
57
|
+
REPOS[a.pkg] ?? '',
|
|
58
|
+
`v${a.version}`,
|
|
59
|
+
asset,
|
|
60
|
+
]);
|
|
61
|
+
const base = `https://github.com/${REPOS[a.pkg] ?? ''}/releases/download/v${a.version}`;
|
|
62
|
+
expect(releaseUrl(a.archive.repo, a.archive.tag, a.archive.asset)).toBe(`${base}/${asset}`);
|
|
63
|
+
expect(a.archive.checksums.url).toBe(`${base}/${sums}`);
|
|
64
|
+
expect(a.archive.checksums.recorded).toBe('2026-09-22');
|
|
65
|
+
expect(assets).toContain(asset);
|
|
66
|
+
const bytes = new Uint8Array(await fixture(sums).arrayBuffer());
|
|
67
|
+
expect(sha256Hex(bytes)).toBe(a.archive.checksums.sha256);
|
|
68
|
+
const lines = parseChecksums(new TextDecoder().decode(bytes));
|
|
69
|
+
const [first, ...members] = [...lines.entries()];
|
|
70
|
+
expect(first).toEqual([asset, a.archive.sha256]);
|
|
71
|
+
expect(Object.fromEntries(members)).toEqual(a.archive.members);
|
|
72
|
+
// ★ Every installed name maps to `<name>-prod`, deliberately, and every one is pinned.
|
|
73
|
+
for (const [binary, member] of Object.entries(a.entry.binaries)) {
|
|
74
|
+
expect(member).toBe(`${binary}-prod`);
|
|
75
|
+
expect(a.archive.members[member]).toMatch(/^[0-9a-f]{64}$/);
|
|
76
|
+
}
|
|
77
|
+
expect(Object.keys(a.entry.binaries).length).toBe(members.length);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('no pinned asset is an enterprise, cluster or other sibling, though each one is listed', () => {
|
|
81
|
+
const siblings = assets.filter((name) => /-(enterprise|cluster)[._]/.test(name));
|
|
82
|
+
expect(siblings.length).toBe(12);
|
|
83
|
+
const names = archives.flatMap((a) => [a.archive.asset, a.archive.checksums.url]);
|
|
84
|
+
for (const name of names) expect(name).not.toMatch(/enterprise|cluster|vlutils/);
|
|
85
|
+
for (const sibling of siblings) expect(names.some((n) => n.endsWith(sibling))).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('vmalert out of the data set: the member vmalert-prod of the plain vmutils archive', () => {
|
|
89
|
+
const request = {
|
|
90
|
+
binary: 'vmalert',
|
|
91
|
+
package: 'vmutils',
|
|
92
|
+
platform: 'darwin-arm64',
|
|
93
|
+
version: '1.151.0',
|
|
94
|
+
};
|
|
95
|
+
expect(catalogBinary(VICTORIA_RELEASES, request)).toEqual({
|
|
96
|
+
archive: {
|
|
97
|
+
asset: 'vmutils-darwin-arm64-v1.151.0.tar.gz',
|
|
98
|
+
repo: 'VictoriaMetrics/VictoriaMetrics',
|
|
99
|
+
sha256: '27d68bac90e28929214091ed9d27f2e9fef25a080407e2c8661ea9b52dd6b183',
|
|
100
|
+
size: 123_584_800,
|
|
101
|
+
tag: 'v1.151.0',
|
|
102
|
+
},
|
|
103
|
+
member: 'vmalert-prod',
|
|
104
|
+
name: 'vmalert',
|
|
105
|
+
sha256: '62845795167e9ff47890e83ac4ac5934693c105767db88591c95a08f3c4fb354',
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VictoriaMetrics' release archives, pinned — the first data set for Release.Binary, and vendor
|
|
3
|
+
* facts only: every digest copied from the vendor's own checksum file for that archive and
|
|
4
|
+
* cross-checked against GitHub's asset digest ON THE RECORDED DATE. Nothing here names a host, a
|
|
5
|
+
* path, a user or an estate. victoria.test.ts holds every pin to the committed checksum files
|
|
6
|
+
* (fixtures/victoria/) — ⚠️ consistency, not provenance: it cannot re-read GitHub, so a fixture
|
|
7
|
+
* edited together with its pins passes, and review is the gate for that.
|
|
8
|
+
*
|
|
9
|
+
* ★ PINNED IN CODE, NOT FETCHED AT APPLY. The checksum file is published by the same account, on
|
|
10
|
+
* the same release, over the same channel as the archive it describes — and these releases are
|
|
11
|
+
* MUTABLE (`"immutable": false` on v1.151.0, v1.52.0 and v0.10.0, measured 2026-09-22; the
|
|
12
|
+
* vendor's own release Makefile says to delete an asset in the UI to re-upload it). Whoever can
|
|
13
|
+
* swap an archive can swap its checksum file in the same minute, and a provider that trusts
|
|
14
|
+
* whatever checksum file it finds would install the new bytes and call them verified. A pin
|
|
15
|
+
* committed here changes only through a reviewed kit release, so a swapped asset is a REFUSAL
|
|
16
|
+
* that names the digest it expected. It also means an apply makes one request, not two, and a
|
|
17
|
+
* plan knows exactly which bytes it will install without touching the network.
|
|
18
|
+
* ⚠️ TRUST ROOT. At apply, these pins alone: the transport (TLS, the redirect, the CDN) can fail
|
|
19
|
+
* a download, never pass other bytes. The pins' own root is the GitHub release over TLS as read
|
|
20
|
+
* on RECORDED (trust on first use), uploaded from a maintainer's personal account.
|
|
21
|
+
* VictoriaMetrics publishes no signature, cosign bundle, SLSA provenance or GitHub artifact
|
|
22
|
+
* attestation for these archives (measured 2026-09-22: no .sig/.asc/.pem/.bundle assets; the
|
|
23
|
+
* attestations API answers 404 for all four digests, next to a positive control that answers
|
|
24
|
+
* 200). A checksum file proves the download is not corrupt — integrity — not who built it.
|
|
25
|
+
* ⛔ A NEW VERSION IS A NEW ENTRY, WALKED DOWN THE SAME WAY — docs/release-binary-catalogs.md.
|
|
26
|
+
* Never `latest`, never sorted by publish date: VictoriaLogs v1.51.1 was published AFTER v1.52.0.
|
|
27
|
+
* ⛔ EXACT NAMES. `-enterprise`, `-cluster` and `-enterprise-cluster` archives sit beside each of
|
|
28
|
+
* these with every prefix in common, so each asset name below is written out whole — never
|
|
29
|
+
* composed from a package and a version, which is how a template drifts onto a sibling.
|
|
30
|
+
* ★ THE VENDOR'S `-prod` SUFFIX IS DROPPED ON PURPOSE: every member is `<binary>-prod`, and
|
|
31
|
+
* installing it as `<binary>` keeps a job's argv[0], `ps` and `pgrep` names identical to the
|
|
32
|
+
* builds it replaces. The map is written out per package, never derived.
|
|
33
|
+
*/
|
|
34
|
+
import type { ReleaseCatalog } from './catalog.ts';
|
|
35
|
+
|
|
36
|
+
const RECORDED = '2026-09-22';
|
|
37
|
+
const DOWNLOAD = 'https://github.com/VictoriaMetrics';
|
|
38
|
+
|
|
39
|
+
export const VICTORIA_RELEASES: ReleaseCatalog = {
|
|
40
|
+
packages: {
|
|
41
|
+
'victoria-logs': {
|
|
42
|
+
binaries: { 'victoria-logs': 'victoria-logs-prod' },
|
|
43
|
+
versions: {
|
|
44
|
+
'1.52.0': {
|
|
45
|
+
'darwin-arm64': {
|
|
46
|
+
asset: 'victoria-logs-darwin-arm64-v1.52.0.tar.gz',
|
|
47
|
+
checksums: {
|
|
48
|
+
recorded: RECORDED,
|
|
49
|
+
sha256: 'd4ca686c3cceec78a0f4e733ef9ab3f857549131ea48cd6f89f701cda406ac4b',
|
|
50
|
+
url: `${DOWNLOAD}/VictoriaLogs/releases/download/v1.52.0/victoria-logs-darwin-arm64-v1.52.0_checksums.txt`,
|
|
51
|
+
},
|
|
52
|
+
members: {
|
|
53
|
+
'victoria-logs-prod':
|
|
54
|
+
'9e48809e314902782959b3a4e683a3087476759d0293791a0564921c676ca7be',
|
|
55
|
+
},
|
|
56
|
+
repo: 'VictoriaMetrics/VictoriaLogs',
|
|
57
|
+
sha256: '3157d4b6181d8a7e3e30918e2cbfcd4cc4cb66263e3ef21ea91e4f20f8980883',
|
|
58
|
+
size: 10_943_697,
|
|
59
|
+
tag: 'v1.52.0',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
'victoria-metrics': {
|
|
65
|
+
// ⛔ The SINGLE-NODE archive. `…-cluster.tar.gz` holds vminsert/vmselect/vmstorage instead.
|
|
66
|
+
binaries: { 'victoria-metrics': 'victoria-metrics-prod' },
|
|
67
|
+
versions: {
|
|
68
|
+
'1.151.0': {
|
|
69
|
+
'darwin-arm64': {
|
|
70
|
+
asset: 'victoria-metrics-darwin-arm64-v1.151.0.tar.gz',
|
|
71
|
+
checksums: {
|
|
72
|
+
recorded: RECORDED,
|
|
73
|
+
sha256: '735745ee0e7b2d5b8118883f2d0ed124c2a12d70261c16380128f5e362566d1e',
|
|
74
|
+
url: `${DOWNLOAD}/VictoriaMetrics/releases/download/v1.151.0/victoria-metrics-darwin-arm64-v1.151.0_checksums.txt`,
|
|
75
|
+
},
|
|
76
|
+
members: {
|
|
77
|
+
'victoria-metrics-prod':
|
|
78
|
+
'3c493a95ac1e9034a3261b230c4d62f3b186a04aafcdbacc99bfc110dc5de6a1',
|
|
79
|
+
},
|
|
80
|
+
repo: 'VictoriaMetrics/VictoriaMetrics',
|
|
81
|
+
sha256: '8792437c4c0b63719fed7b44ad18d4b4167bbe6861ab3e84c7bb010c922e7582',
|
|
82
|
+
size: 12_739_105,
|
|
83
|
+
tag: 'v1.151.0',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
'victoria-traces': {
|
|
89
|
+
binaries: { 'victoria-traces': 'victoria-traces-prod' },
|
|
90
|
+
versions: {
|
|
91
|
+
'0.10.0': {
|
|
92
|
+
'darwin-arm64': {
|
|
93
|
+
asset: 'victoria-traces-darwin-arm64-v0.10.0.tar.gz',
|
|
94
|
+
checksums: {
|
|
95
|
+
recorded: RECORDED,
|
|
96
|
+
sha256: 'cd32bfc21900b48ba543124e43827a1d13d735e45d50dbfd742c0ca6640e1aae',
|
|
97
|
+
url: `${DOWNLOAD}/VictoriaTraces/releases/download/v0.10.0/victoria-traces-darwin-arm64-v0.10.0_checksums.txt`,
|
|
98
|
+
},
|
|
99
|
+
members: {
|
|
100
|
+
'victoria-traces-prod':
|
|
101
|
+
'4759ec89a466129b9eefc22244a111e1f489739340747805fd93b58fd0f7b2dd',
|
|
102
|
+
},
|
|
103
|
+
repo: 'VictoriaMetrics/VictoriaTraces',
|
|
104
|
+
sha256: 'cbe83f1d409cbb85fbf1c890c4a9b7fd34c1d74acec4dce2e0ebe46fcb260e38',
|
|
105
|
+
size: 11_126_077,
|
|
106
|
+
tag: 'v0.10.0',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
vmutils: {
|
|
112
|
+
// ★ ONE ARCHIVE, SEVEN TOOLS (measured: 264 MB unpacked). Only a declared member is extracted.
|
|
113
|
+
// vmalert-logs is not a tool of its own: it is vmalert run with `--rule.defaultRuleType=vlogs`.
|
|
114
|
+
binaries: {
|
|
115
|
+
vmagent: 'vmagent-prod',
|
|
116
|
+
vmalert: 'vmalert-prod',
|
|
117
|
+
'vmalert-tool': 'vmalert-tool-prod',
|
|
118
|
+
vmauth: 'vmauth-prod',
|
|
119
|
+
vmbackup: 'vmbackup-prod',
|
|
120
|
+
vmctl: 'vmctl-prod',
|
|
121
|
+
vmrestore: 'vmrestore-prod',
|
|
122
|
+
},
|
|
123
|
+
versions: {
|
|
124
|
+
'1.151.0': {
|
|
125
|
+
'darwin-arm64': {
|
|
126
|
+
asset: 'vmutils-darwin-arm64-v1.151.0.tar.gz',
|
|
127
|
+
checksums: {
|
|
128
|
+
recorded: RECORDED,
|
|
129
|
+
sha256: 'fda5a64fce9022bee7614c3fda27271c0826f1d7b90f8b162e1f48c77c0df04b',
|
|
130
|
+
url: `${DOWNLOAD}/VictoriaMetrics/releases/download/v1.151.0/vmutils-darwin-arm64-v1.151.0_checksums.txt`,
|
|
131
|
+
},
|
|
132
|
+
members: {
|
|
133
|
+
'vmagent-prod': 'd3b6fa1f71ef48beac619f15f8adfa7610e76605caf37db1ebfbb379b6692329',
|
|
134
|
+
'vmalert-prod': '62845795167e9ff47890e83ac4ac5934693c105767db88591c95a08f3c4fb354',
|
|
135
|
+
'vmalert-tool-prod':
|
|
136
|
+
'cbffb9495e4bb1457c9d45e0a7064b3a3a29b2cfd1ae24bfe8f00ec327c26998',
|
|
137
|
+
// ⚠️ gitleaks' generic-api-key rule reads this one public digest as a key (its
|
|
138
|
+
// entropy; measured 2026-09-22). It is the vendor's published SHA-256 of vmauth-prod.
|
|
139
|
+
'vmauth-prod': '398df9e0f5b7035d037e39ee9ff66a10ddde5333d832602463aa4d2865fdb508', // gitleaks:allow
|
|
140
|
+
'vmbackup-prod': '1974a2ee211b49bf581287808e010846f6c47ff135276214c6da628567777b9e',
|
|
141
|
+
'vmctl-prod': '998b111eee206b98e6d25fdf2fbaa455ecc17c7f9e7c3b2373e127cef762459f',
|
|
142
|
+
'vmrestore-prod': 'fc824df7215b01b57921ed46afde7fea6b6698adf13d9ea8e3fe36e194ef8190',
|
|
143
|
+
},
|
|
144
|
+
repo: 'VictoriaMetrics/VictoriaMetrics',
|
|
145
|
+
sha256: '27d68bac90e28929214091ed9d27f2e9fef25a080407e2c8661ea9b52dd6b183',
|
|
146
|
+
size: 123_584_800,
|
|
147
|
+
tag: 'v1.151.0',
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
vendor: 'VictoriaMetrics',
|
|
154
|
+
};
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* Values a declaration may SEND and must never STORE — a webhook secret, a header carrying a
|
|
3
3
|
* credential, an smtp password — and the one form in which a plan may still remember them.
|
|
4
4
|
*
|
|
5
|
+
* ★ FAMILY-NEUTRAL ON PURPOSE, MOVED HERE FROM `proxmox/` 2026-09-22. The rule it encodes is not
|
|
6
|
+
* about Proxmox: Alchemy's state store is the same unencrypted store for every provider, so the
|
|
7
|
+
* Cloudflare MCP server entry's `auth_credentials` needs exactly this shape. A second copy under
|
|
8
|
+
* `cloudflare/` would be two seal formats for one rule; a cross-family import would make
|
|
9
|
+
* Cloudflare depend on Proxmox. Neither; it lives on its own.
|
|
10
|
+
*
|
|
5
11
|
* ⛔ A SECRET PROP IS A SECRET IN THE STATE STORE, WHATEVER TYPE IT HAS. Alchemy persists props as
|
|
6
12
|
* well as attributes — `delete` is handed `olds`, which can only have come from the store — and
|
|
7
13
|
* it does not encrypt either: `StateEncoding.ts` TAGS a `Redacted` value and writes the inner
|
|
File without changes
|
|
File without changes
|