@wavehouse/chtypes 0.5.2 → 1.0.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/CHANGELOG.md +26 -0
- package/README.md +34 -38
- package/dist/abi1/buildinfo.d.ts +40 -0
- package/dist/abi1/buildinfo.d.ts.map +1 -0
- package/dist/abi1/buildinfo.js +83 -0
- package/dist/abi1/buildinfo.js.map +1 -0
- package/dist/abi1/calls.gen.d.ts +55 -0
- package/dist/abi1/calls.gen.d.ts.map +1 -0
- package/dist/abi1/calls.gen.js +110 -0
- package/dist/abi1/calls.gen.js.map +1 -0
- package/dist/abi1/decls.gen.d.ts +115 -0
- package/dist/abi1/decls.gen.d.ts.map +1 -0
- package/dist/abi1/decls.gen.js +1613 -0
- package/dist/abi1/decls.gen.js.map +1 -0
- package/dist/abi1/errmap.gen.d.ts +18 -0
- package/dist/abi1/errmap.gen.d.ts.map +1 -0
- package/dist/abi1/errmap.gen.js +66 -0
- package/dist/abi1/errmap.gen.js.map +1 -0
- package/dist/abi1/errors.d.ts +107 -0
- package/dist/abi1/errors.d.ts.map +1 -0
- package/dist/abi1/errors.js +134 -0
- package/dist/abi1/errors.js.map +1 -0
- package/dist/abi1/handles.d.ts +31 -0
- package/dist/abi1/handles.d.ts.map +1 -0
- package/dist/abi1/handles.js +76 -0
- package/dist/abi1/handles.js.map +1 -0
- package/dist/abi1/index.d.ts +17 -0
- package/dist/abi1/index.d.ts.map +1 -0
- package/dist/abi1/index.js +17 -0
- package/dist/abi1/index.js.map +1 -0
- package/dist/abi1/libc.d.ts +54 -0
- package/dist/abi1/libc.d.ts.map +1 -0
- package/dist/abi1/libc.gen.d.ts +12 -0
- package/dist/abi1/libc.gen.d.ts.map +1 -0
- package/dist/abi1/libc.gen.js +75 -0
- package/dist/abi1/libc.gen.js.map +1 -0
- package/dist/abi1/libc.js +109 -0
- package/dist/abi1/libc.js.map +1 -0
- package/dist/abi1/loader.d.ts +99 -0
- package/dist/abi1/loader.d.ts.map +1 -0
- package/dist/abi1/loader.js +261 -0
- package/dist/abi1/loader.js.map +1 -0
- package/dist/abi1/raw.d.ts +104 -0
- package/dist/abi1/raw.d.ts.map +1 -0
- package/dist/abi1/raw.js +296 -0
- package/dist/abi1/raw.js.map +1 -0
- package/dist/abi1/strictjson.d.ts +19 -0
- package/dist/abi1/strictjson.d.ts.map +1 -0
- package/dist/abi1/strictjson.js +78 -0
- package/dist/abi1/strictjson.js.map +1 -0
- package/dist/abi1/vocab.gen.d.ts +169 -0
- package/dist/abi1/vocab.gen.d.ts.map +1 -0
- package/dist/abi1/vocab.gen.js +306 -0
- package/dist/abi1/vocab.gen.js.map +1 -0
- package/dist/cli.d.ts +25 -26
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +206 -218
- package/dist/cli.js.map +1 -1
- package/dist/documents.d.ts +195 -0
- package/dist/documents.d.ts.map +1 -0
- package/dist/documents.js +389 -0
- package/dist/documents.js.map +1 -0
- package/dist/env.d.ts +16 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +57 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +19 -24
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -23
- package/dist/index.js.map +1 -1
- package/dist/json.d.ts +11 -89
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +16 -180
- package/dist/json.js.map +1 -1
- package/dist/library.d.ts +53 -309
- package/dist/library.d.ts.map +1 -1
- package/dist/library.js +77 -315
- package/dist/library.js.map +1 -1
- package/dist/ocifetch/constants.gen.d.ts +84 -0
- package/dist/ocifetch/constants.gen.d.ts.map +1 -0
- package/dist/ocifetch/constants.gen.js +92 -0
- package/dist/ocifetch/constants.gen.js.map +1 -0
- package/dist/ocifetch/dsse.d.ts +114 -0
- package/dist/ocifetch/dsse.d.ts.map +1 -0
- package/dist/ocifetch/dsse.js +331 -0
- package/dist/ocifetch/dsse.js.map +1 -0
- package/dist/ocifetch/ensure.d.ts +65 -0
- package/dist/ocifetch/ensure.d.ts.map +1 -0
- package/dist/ocifetch/ensure.js +775 -0
- package/dist/ocifetch/ensure.js.map +1 -0
- package/dist/ocifetch/errors.d.ts +92 -0
- package/dist/ocifetch/errors.d.ts.map +1 -0
- package/dist/ocifetch/errors.js +101 -0
- package/dist/ocifetch/errors.js.map +1 -0
- package/dist/ocifetch/goldens.d.ts +44 -0
- package/dist/ocifetch/goldens.d.ts.map +1 -0
- package/dist/ocifetch/goldens.js +62 -0
- package/dist/ocifetch/goldens.js.map +1 -0
- package/dist/ocifetch/http.d.ts +124 -0
- package/dist/ocifetch/http.d.ts.map +1 -0
- package/dist/ocifetch/http.js +549 -0
- package/dist/ocifetch/http.js.map +1 -0
- package/dist/ocifetch/index.d.ts +17 -0
- package/dist/ocifetch/index.d.ts.map +1 -0
- package/dist/ocifetch/index.js +14 -0
- package/dist/ocifetch/index.js.map +1 -0
- package/dist/ocifetch/layout.d.ts +126 -0
- package/dist/ocifetch/layout.d.ts.map +1 -0
- package/dist/ocifetch/layout.js +375 -0
- package/dist/ocifetch/layout.js.map +1 -0
- package/dist/ocifetch/localverify.d.ts +43 -0
- package/dist/ocifetch/localverify.d.ts.map +1 -0
- package/dist/ocifetch/localverify.js +164 -0
- package/dist/ocifetch/localverify.js.map +1 -0
- package/dist/ocifetch/lock.d.ts +39 -0
- package/dist/ocifetch/lock.d.ts.map +1 -0
- package/dist/ocifetch/lock.js +140 -0
- package/dist/ocifetch/lock.js.map +1 -0
- package/dist/ocifetch/oci.d.ts +100 -0
- package/dist/ocifetch/oci.d.ts.map +1 -0
- package/dist/ocifetch/oci.js +323 -0
- package/dist/ocifetch/oci.js.map +1 -0
- package/dist/ocifetch/referrers.d.ts +55 -0
- package/dist/ocifetch/referrers.d.ts.map +1 -0
- package/dist/ocifetch/referrers.js +132 -0
- package/dist/ocifetch/referrers.js.map +1 -0
- package/dist/ocifetch/types.d.ts +167 -0
- package/dist/ocifetch/types.d.ts.map +1 -0
- package/dist/ocifetch/types.js +88 -0
- package/dist/ocifetch/types.js.map +1 -0
- package/dist/ocifetch/unpack.d.ts +55 -0
- package/dist/ocifetch/unpack.d.ts.map +1 -0
- package/dist/ocifetch/unpack.js +205 -0
- package/dist/ocifetch/unpack.js.map +1 -0
- package/dist/registry.d.ts +38 -387
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +94 -819
- package/dist/registry.js.map +1 -1
- package/dist/schema.d.ts +77 -588
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +87 -549
- package/dist/schema.js.map +1 -1
- package/dist/settings.d.ts +27 -36
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +75 -25
- package/dist/settings.js.map +1 -1
- package/dist/setup.d.ts +37 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +60 -0
- package/dist/setup.js.map +1 -0
- package/dist/tar.d.ts +27 -11
- package/dist/tar.d.ts.map +1 -1
- package/dist/tar.js +54 -32
- package/dist/tar.js.map +1 -1
- package/package.json +2 -2
- package/dist/discover.d.ts +0 -142
- package/dist/discover.d.ts.map +0 -1
- package/dist/discover.js +0 -288
- package/dist/discover.js.map +0 -1
- package/dist/error-codes.d.ts +0 -80
- package/dist/error-codes.d.ts.map +0 -1
- package/dist/error-codes.js +0 -139
- package/dist/error-codes.js.map +0 -1
- package/dist/errors.d.ts +0 -284
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -321
- package/dist/errors.js.map +0 -1
- package/dist/fetch.d.ts +0 -382
- package/dist/fetch.d.ts.map +0 -1
- package/dist/fetch.js +0 -1498
- package/dist/fetch.js.map +0 -1
- package/dist/ffi.d.ts +0 -410
- package/dist/ffi.d.ts.map +0 -1
- package/dist/ffi.js +0 -1154
- package/dist/ffi.js.map +0 -1
- package/dist/format.d.ts +0 -128
- package/dist/format.d.ts.map +0 -1
- package/dist/format.js +0 -132
- package/dist/format.js.map +0 -1
- package/dist/numeric.d.ts +0 -20
- package/dist/numeric.d.ts.map +0 -1
- package/dist/numeric.js +0 -107
- package/dist/numeric.js.map +0 -1
- package/dist/paths.d.ts +0 -55
- package/dist/paths.d.ts.map +0 -1
- package/dist/paths.js +0 -87
- package/dist/paths.js.map +0 -1
- package/dist/results.d.ts +0 -495
- package/dist/results.d.ts.map +0 -1
- package/dist/results.js +0 -406
- package/dist/results.js.map +0 -1
- package/dist/transform.d.ts +0 -73
- package/dist/transform.d.ts.map +0 -1
- package/dist/transform.js +0 -533
- package/dist/transform.js.map +0 -1
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seam (`docs/guides/fetch-v1.md` §9): `ensure`, `resolveInstalled`,
|
|
3
|
+
* `listInstalled`, `verifyInstalled`, `fetchSigned`. This is the only file
|
|
4
|
+
* that wires resolve (`oci.ts`), trust (`dsse.ts`), bytes (`unpack.ts`), the
|
|
5
|
+
* cache (`layout.ts`) and the lock (`lock.ts`) together; nothing outside
|
|
6
|
+
* `index.ts` calls into those modules directly.
|
|
7
|
+
*/
|
|
8
|
+
import { randomBytes } from 'node:crypto';
|
|
9
|
+
import { unlink } from 'node:fs/promises';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { asString, field, items, parseJsonValue } from '../json.js';
|
|
13
|
+
import { ABI_GENERATION, BASE_SEPARATOR, CACHE_ANNOTATION_PREFIX, DEFAULT_BASES, ENV_BASES_NAME, GOLDENS_ARTIFACT_TYPE, MEDIA_TYPE_BUNDLE, MEDIA_TYPE_MANIFEST, PREDICATE_TYPE_ARTIFACT, PREDICATE_TYPE_GOLDENS, RELEASE_KEYS, SPELLING_REGEX, TAGS_LIST_MAX_BYTES, } from './constants.gen.js';
|
|
14
|
+
import { checkArtifactStatement, checkGenericStatement, parseStatement, verifyAnyReferrerBundle, verifyBundleSignature } from './dsse.js';
|
|
15
|
+
import { ArtifactCorruptError, ArtifactMissingError, ArtifactPinnedError, ArtifactUnpublishedError, ArtifactUntrustedError, FetchV1Error, SourceUnreachableError, } from './errors.js';
|
|
16
|
+
import { readFileUrl, requestBuffered } from './http.js';
|
|
17
|
+
import { buildRecord, cacheRoot, commitStaging, ensureLayout, freshStagingDir, installBlob, listVerified, readIndexEntries, readVerifiedRecord, removeStaging, systemDirs, unpackedDir, upsertIndexEntry, writeVerifiedRecord, } from './layout.js';
|
|
18
|
+
import { selectGoldens, statementRevision } from './goldens.js';
|
|
19
|
+
import { discoverSignatureCandidates } from './referrers.js';
|
|
20
|
+
import { verifyAndInstallFromLocalBlobs } from './localverify.js';
|
|
21
|
+
import { fetchBlobBytesByDigest, fetchManifestByDigest, resolveTag } from './oci.js';
|
|
22
|
+
import { emptyLock, getPin, readLock, withPin, writeLock } from './lock.js';
|
|
23
|
+
import { digestOfHex, endpointUrl, hexOfDigest, platformInfo, realClock, resolvePlatformOption } from './types.js';
|
|
24
|
+
import { fetchVerifyAndUnpackLayer, measureLibrary, verifyInstalledLibrary } from './unpack.js';
|
|
25
|
+
function defaultTrustedKeys(options) {
|
|
26
|
+
if (options.trustedKeys !== undefined)
|
|
27
|
+
return options.trustedKeys;
|
|
28
|
+
return RELEASE_KEYS.map((k) => ({ keyid: k.keyid, ed25519Hex: k.ed25519Hex }));
|
|
29
|
+
}
|
|
30
|
+
function resolveBases(options) {
|
|
31
|
+
if (options.bases !== undefined && options.bases.length > 0)
|
|
32
|
+
return options.bases;
|
|
33
|
+
const env = process.env[ENV_BASES_NAME];
|
|
34
|
+
if (env !== undefined && env !== '') {
|
|
35
|
+
return env
|
|
36
|
+
.split(BASE_SEPARATOR)
|
|
37
|
+
.map((b) => b.trim())
|
|
38
|
+
.filter((b) => b !== '');
|
|
39
|
+
}
|
|
40
|
+
return DEFAULT_BASES;
|
|
41
|
+
}
|
|
42
|
+
function tokenHostsFor(bases) {
|
|
43
|
+
const hosts = [];
|
|
44
|
+
for (const b of bases) {
|
|
45
|
+
if (b.startsWith('file:'))
|
|
46
|
+
continue;
|
|
47
|
+
try {
|
|
48
|
+
hosts.push(new URL(b).host);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Not a URL this binding can parse as a host — never a token target.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return hosts;
|
|
55
|
+
}
|
|
56
|
+
function requestOptionsFor(options, bases) {
|
|
57
|
+
return {
|
|
58
|
+
clock: options.clock ?? realClock(),
|
|
59
|
+
...(options.token !== undefined ? { token: options.token } : {}),
|
|
60
|
+
tokenHosts: tokenHostsFor(bases),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Builds the `Resolved` the seam promises, from a `verified.json` record plus where it came from. */
|
|
64
|
+
function resolvedFromRecord(record, platform, request, dir, source, alreadyInstalled, warnings) {
|
|
65
|
+
return {
|
|
66
|
+
abiGeneration: ABI_GENERATION,
|
|
67
|
+
platform,
|
|
68
|
+
request,
|
|
69
|
+
version: record.version,
|
|
70
|
+
...(record.channel !== null ? { channel: record.channel } : {}),
|
|
71
|
+
build: record.build,
|
|
72
|
+
libraryPath: path.join(dir, record.library),
|
|
73
|
+
dir,
|
|
74
|
+
digests: {
|
|
75
|
+
...(record.indexDigest !== null ? { index: record.indexDigest } : {}),
|
|
76
|
+
manifest: record.manifestDigest,
|
|
77
|
+
layer: record.layerDigest,
|
|
78
|
+
bundle: record.bundleDigest ?? '',
|
|
79
|
+
...(record.bundleManifestDigest !== null ? { bundleManifest: record.bundleManifestDigest } : {}),
|
|
80
|
+
},
|
|
81
|
+
predicate: record.predicate,
|
|
82
|
+
signedBy: record.signedBy ?? '',
|
|
83
|
+
source,
|
|
84
|
+
alreadyInstalled,
|
|
85
|
+
warnings,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// ------------------------------------------------------------- resolveInstalled
|
|
89
|
+
/**
|
|
90
|
+
* Never touches the network (guide §9). The source of truth is the
|
|
91
|
+
* immutable `unpacked/sha256/*\/verified.json` records (guide §6) — never
|
|
92
|
+
* `index.json` directly, though a pre-seeded `index.json` entry with no
|
|
93
|
+
* `verified.json` of its own yet is verified and unpacked from local blobs
|
|
94
|
+
* first (guide §1), exactly once, so every call after the first reads the
|
|
95
|
+
* same way `verified.json`-backed entries always have. Picks the newest
|
|
96
|
+
* match (version, then build, both compared as fixed-width strings per the
|
|
97
|
+
* guide's monotonicity rule) among records whose platform matches and whose
|
|
98
|
+
* `clickhouse_version` lies within `request`.
|
|
99
|
+
*/
|
|
100
|
+
export async function resolveInstalled(request, platform, options = {}) {
|
|
101
|
+
const root = cacheRoot(options.cacheDir);
|
|
102
|
+
const trustedKeys = defaultTrustedKeys(options);
|
|
103
|
+
await verifyPreseededEntries(root, root, platform, trustedKeys);
|
|
104
|
+
for (const sysDir of systemDirs(options.systemDirs)) {
|
|
105
|
+
await verifyPreseededEntries(sysDir, root, platform, trustedKeys);
|
|
106
|
+
}
|
|
107
|
+
const candidates = [];
|
|
108
|
+
for (const { dir, record } of await listVerified(root)) {
|
|
109
|
+
if (record.platform !== platform)
|
|
110
|
+
continue;
|
|
111
|
+
if (!withinRequest(record.version, request))
|
|
112
|
+
continue;
|
|
113
|
+
candidates.push({ record, dir, source: 'cache' });
|
|
114
|
+
}
|
|
115
|
+
if (candidates.length === 0)
|
|
116
|
+
return undefined;
|
|
117
|
+
candidates.sort((a, b) => compareVersionThenBuild(b.record.predicate, a.record.predicate));
|
|
118
|
+
const best = candidates[0];
|
|
119
|
+
return resolvedFromRecord(best.record, platform, request, best.dir, best.source, true, []);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* For every `index.json` entry in `sourceDir` matching `platform` that has
|
|
123
|
+
* no `verified.json` under `writeRoot` yet, attempts a local-blob
|
|
124
|
+
* verify-and-install (`localverify.ts`), reading from `sourceDir` and
|
|
125
|
+
* writing into `writeRoot` — the same directory for the user's own cache,
|
|
126
|
+
* always `writeRoot` (never `sourceDir`) for a read-only system directory
|
|
127
|
+
* (`system-dir-readonly`: "the system dir is never written"). Every
|
|
128
|
+
* platform-matching entry is verified unconditionally (not pre-filtered by
|
|
129
|
+
* the index's own unverified `org.opencontainers.image.ref.name`
|
|
130
|
+
* annotation) — a real cache's pre-seeded set is small, and verifying one
|
|
131
|
+
* extra irrelevant entry is cheap next to the alternative of trusting
|
|
132
|
+
* unverified metadata to decide what is even worth checking.
|
|
133
|
+
*/
|
|
134
|
+
async function verifyPreseededEntries(sourceDir, writeRoot, platform, trustedKeys) {
|
|
135
|
+
const info = platformInfo(platform);
|
|
136
|
+
let entries;
|
|
137
|
+
try {
|
|
138
|
+
entries = await readIndexEntries(sourceDir);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
if (entry.platform !== undefined && (entry.platform.os !== info.os || entry.platform.architecture !== info.architecture))
|
|
145
|
+
continue;
|
|
146
|
+
const already = await readVerifiedRecord(unpackedDir(writeRoot, hexOfDigest(entry.digest)));
|
|
147
|
+
if (already !== undefined)
|
|
148
|
+
continue;
|
|
149
|
+
await verifyAndInstallFromLocalBlobs(sourceDir, writeRoot, entry.digest, platform, trustedKeys);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function withinRequest(actualVersion, requestedSpelling) {
|
|
153
|
+
const actual = actualVersion.split('.');
|
|
154
|
+
const requested = requestedSpelling.split('.');
|
|
155
|
+
if (requested.length > actual.length)
|
|
156
|
+
return false;
|
|
157
|
+
for (let i = 0; i < requested.length; i++)
|
|
158
|
+
if (requested[i] !== actual[i])
|
|
159
|
+
return false;
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
/** Version first, then build — both as fixed-width strings, per the guide's monotonicity rule. */
|
|
163
|
+
function compareVersionThenBuild(a, b) {
|
|
164
|
+
const av = a.clickhouse_version.split('.').map(Number);
|
|
165
|
+
const bv = b.clickhouse_version.split('.').map(Number);
|
|
166
|
+
for (let i = 0; i < Math.max(av.length, bv.length); i++) {
|
|
167
|
+
const diff = (av[i] ?? 0) - (bv[i] ?? 0);
|
|
168
|
+
if (diff !== 0)
|
|
169
|
+
return diff;
|
|
170
|
+
}
|
|
171
|
+
return a.build.localeCompare(b.build);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* `monotonic-warning`: installing a version/build **lower** than one
|
|
175
|
+
* already installed for the same platform is not an error (requests float
|
|
176
|
+
* to a specific registry answer, and a mirror or a rollback can legitimately
|
|
177
|
+
* offer an older build) — but it is surprising enough to warn about loudly,
|
|
178
|
+
* once, naming nothing more specific than that it happened.
|
|
179
|
+
*/
|
|
180
|
+
async function checkMonotonic(root, platform, incoming) {
|
|
181
|
+
const info = platformInfo(platform);
|
|
182
|
+
for (const { dir, record } of await listVerified(root)) {
|
|
183
|
+
if (record.predicate.os !== info.os || record.predicate.arch !== info.architecture)
|
|
184
|
+
continue;
|
|
185
|
+
if (compareVersionThenBuild(incoming, record.predicate) < 0) {
|
|
186
|
+
return {
|
|
187
|
+
warning: 'chtypes: the registry offered a version/build older than one already installed for this platform (monotonic warning); keeping the newer install',
|
|
188
|
+
existing: record,
|
|
189
|
+
dir,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
// ------------------------------------------------------------------ listInstalled
|
|
196
|
+
export async function listInstalled(options = {}) {
|
|
197
|
+
const root = cacheRoot(options.cacheDir);
|
|
198
|
+
const out = [];
|
|
199
|
+
for (const { dir, record } of await listVerified(root)) {
|
|
200
|
+
const platform = record.platform;
|
|
201
|
+
out.push(resolvedFromRecord(record, platform, record.version, dir, 'cache', true, []));
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
// ----------------------------------------------------------------- verifyInstalled
|
|
206
|
+
/** Re-checks every installed library's bytes against its own `verified.json` record (guide §9). */
|
|
207
|
+
export async function verifyInstalled(options = {}) {
|
|
208
|
+
const root = cacheRoot(options.cacheDir);
|
|
209
|
+
const out = [];
|
|
210
|
+
for (const { dir, record } of await listVerified(root)) {
|
|
211
|
+
const platform = record.platform;
|
|
212
|
+
try {
|
|
213
|
+
await verifyInstalledLibrary(path.join(dir, record.library), record.librarySha256, record.libraryBytes);
|
|
214
|
+
out.push({ platform, dir, ok: true, detail: '' });
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
out.push({ platform, dir, ok: false, detail: err instanceof Error ? err.message : String(err) });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
// ------------------------------------------------------------------------ ensure
|
|
223
|
+
/**
|
|
224
|
+
* `ensure(request, options)`: resolve unless `frozen` or `offline`, verify
|
|
225
|
+
* trust, download and unpack bytes, install into the cache, optionally write
|
|
226
|
+
* a lock entry. See `docs/guides/fetch-v1.md` §9 for the full contract.
|
|
227
|
+
*/
|
|
228
|
+
export async function ensure(request, options = {}) {
|
|
229
|
+
const platform = resolvePlatformOption(options.platform, os.platform(), os.arch());
|
|
230
|
+
const root = cacheRoot(options.cacheDir);
|
|
231
|
+
await ensureLayout(root);
|
|
232
|
+
if (options.offline === true) {
|
|
233
|
+
const hit = await resolveInstalled(request, platform, options);
|
|
234
|
+
if (hit === undefined) {
|
|
235
|
+
throw new ArtifactMissingError(`chtypes: ${request} (${platform}) is not installed, and --offline forbids fetching it`);
|
|
236
|
+
}
|
|
237
|
+
return hit;
|
|
238
|
+
}
|
|
239
|
+
if (options.frozen === true) {
|
|
240
|
+
return ensureFrozen(request, platform, options);
|
|
241
|
+
}
|
|
242
|
+
const bases = resolveBases(options);
|
|
243
|
+
const baseReqOptions = requestOptionsFor(options, bases);
|
|
244
|
+
const trustedKeys = defaultTrustedKeys(options);
|
|
245
|
+
const resolveResult = await resolveTag(bases, request, platform, { ...baseReqOptions, maxBytes: 0 });
|
|
246
|
+
const manifestHex = hexOfDigest(resolveResult.manifest.digest);
|
|
247
|
+
const finalDir = unpackedDir(root, manifestHex);
|
|
248
|
+
const existing = await readVerifiedRecord(finalDir);
|
|
249
|
+
let record;
|
|
250
|
+
let recordDir = finalDir;
|
|
251
|
+
let warnings = [];
|
|
252
|
+
let alreadyInstalled;
|
|
253
|
+
if (existing !== undefined) {
|
|
254
|
+
record = existing;
|
|
255
|
+
alreadyInstalled = true;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
const info = platformInfo(platform);
|
|
259
|
+
const trust = await verifyAnyReferrerBundle(resolveResult.repositoryRoot, resolveResult.manifest.digest, MEDIA_TYPE_BUNDLE, trustedKeys, (statement) => checkArtifactStatement(statement, PREDICATE_TYPE_ARTIFACT, resolveResult.manifest.layer.digest, {
|
|
260
|
+
os: info.os,
|
|
261
|
+
arch: info.architecture,
|
|
262
|
+
requestedSpelling: request,
|
|
263
|
+
}), { ...baseReqOptions, maxBytes: 0 });
|
|
264
|
+
let predicate;
|
|
265
|
+
let signedBy;
|
|
266
|
+
if (trust === undefined) {
|
|
267
|
+
if (options.allowUnsigned !== true) {
|
|
268
|
+
throw new ArtifactUntrustedError(`chtypes: no referrer of ${resolveResult.manifest.digest} verified under a trusted key (CHTYPES_ALLOW_UNSIGNED=1 to proceed anyway)`);
|
|
269
|
+
}
|
|
270
|
+
predicate = await unsignedPredicateFallback(resolveResult.repositoryRoot, resolveResult.manifest, request, info, baseReqOptions);
|
|
271
|
+
signedBy = '';
|
|
272
|
+
warnings = [...warnings, 'chtypes: proceeding with an unsigned artifact (CHTYPES_ALLOW_UNSIGNED)'];
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
predicate = trust.statement.predicate;
|
|
276
|
+
signedBy = trust.signedBy;
|
|
277
|
+
}
|
|
278
|
+
const monotonic = await checkMonotonic(root, platform, predicate);
|
|
279
|
+
if (monotonic !== undefined) {
|
|
280
|
+
// The registry is offering something OLDER than what is already
|
|
281
|
+
// installed for this platform (`monotonic-warning`): warn loudly, but
|
|
282
|
+
// never replace a newer install with an older one it was not asked to
|
|
283
|
+
// roll back to — hand back the existing, newer record instead of
|
|
284
|
+
// downloading and installing the one just resolved.
|
|
285
|
+
record = monotonic.existing;
|
|
286
|
+
recordDir = monotonic.dir;
|
|
287
|
+
alreadyInstalled = true;
|
|
288
|
+
warnings = [...warnings, monotonic.warning];
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
const staging = await freshStagingDir(root);
|
|
292
|
+
const tempLayerPath = path.join(root, `.tmp-layer-${process.pid}-${randomBytes(6).toString('hex')}`);
|
|
293
|
+
try {
|
|
294
|
+
const entries = await fetchVerifyAndUnpackLayer([resolveResult.repositoryRoot], resolveResult.manifest.layer, tempLayerPath, staging, { ...baseReqOptions, maxBytes: 0 });
|
|
295
|
+
if (!entries.some((e) => e.name === predicate.library)) {
|
|
296
|
+
throw new ArtifactUnpublishedError(`chtypes: the layer does not contain its own predicate's library ${JSON.stringify(predicate.library)}`);
|
|
297
|
+
}
|
|
298
|
+
// Nothing signed names the library's hash or size on the unsigned
|
|
299
|
+
// path (`CHTYPES_ALLOW_UNSIGNED`), so there is nothing to compare to.
|
|
300
|
+
if (trust !== undefined) {
|
|
301
|
+
await verifyInstalledLibrary(path.join(staging, predicate.library), predicate.library_sha256, predicate.library_bytes);
|
|
302
|
+
}
|
|
303
|
+
const newRecord = buildRecord({
|
|
304
|
+
platform,
|
|
305
|
+
predicate,
|
|
306
|
+
library: await measureLibrary(path.join(staging, predicate.library)),
|
|
307
|
+
indexDigest: resolveResult.indexDigest,
|
|
308
|
+
manifestDigest: resolveResult.manifest.digest,
|
|
309
|
+
layerDigest: resolveResult.manifest.layer.digest,
|
|
310
|
+
bundleDigest: trust === undefined ? null : trust.bundleDigest,
|
|
311
|
+
bundleManifestDigest: trust === undefined ? null : trust.bundleManifestDigest,
|
|
312
|
+
signedBy: trust === undefined ? null : signedBy,
|
|
313
|
+
});
|
|
314
|
+
await writeVerifiedRecord(staging, newRecord);
|
|
315
|
+
await commitStaging(staging, finalDir);
|
|
316
|
+
record = newRecord;
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
await removeStaging(staging);
|
|
320
|
+
throw err;
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
await unlink(tempLayerPath).catch(() => { });
|
|
324
|
+
}
|
|
325
|
+
const manifestDescriptor = { mediaType: MEDIA_TYPE_MANIFEST, digest: resolveResult.manifest.digest };
|
|
326
|
+
await upsertIndexEntry(root, { ...manifestDescriptor, size: resolveResult.manifest.bytes.length, annotations: { [`${CACHE_ANNOTATION_PREFIX}request`]: request } }, options.beforeIndexRename);
|
|
327
|
+
alreadyInstalled = false;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (options.lockWrite === true) {
|
|
331
|
+
await writeLockEntry(request, platform, record, options, resolveResult.repositoryRoot, resolveResult.indexBytes, baseReqOptions);
|
|
332
|
+
}
|
|
333
|
+
if (options.update === true) {
|
|
334
|
+
// `update` re-resolves every locked request and rewrites the lock from
|
|
335
|
+
// scratch (guide §6: it never merges a stale entry with a fresh one), in
|
|
336
|
+
// addition to resolving the request above.
|
|
337
|
+
await updateLock(options, bases, baseReqOptions, trustedKeys);
|
|
338
|
+
}
|
|
339
|
+
return resolvedFromRecord(record, platform, request, recordDir, resolveResult.repositoryRoot, alreadyInstalled, warnings);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* `CHTYPES_ALLOW_UNSIGNED`'s fallback metadata source: the tarball's own
|
|
343
|
+
* `manifest.json`, which is also the OCI **config** blob and (per
|
|
344
|
+
* `layout-v2.md` §4.1) carries the same fields as the predicate, value-equal
|
|
345
|
+
* — read here unverified, since there is nothing to verify it against once
|
|
346
|
+
* signing itself is being skipped. Missing or unreadable: the fields fall
|
|
347
|
+
* back to the request itself, and `library` to `libchtypes.so`/`.dylib` by
|
|
348
|
+
* platform, which is what every fixture library is actually named.
|
|
349
|
+
*/
|
|
350
|
+
async function unsignedPredicateFallback(repositoryRoot, manifest, request, info, baseReqOptions) {
|
|
351
|
+
const fallbackLibrary = info.os === 'darwin' ? 'libchtypes.dylib' : 'libchtypes.so';
|
|
352
|
+
const fallback = {
|
|
353
|
+
abi: ABI_GENERATION,
|
|
354
|
+
abi_fingerprint: '',
|
|
355
|
+
clickhouse_version: request,
|
|
356
|
+
channel: '',
|
|
357
|
+
clickhouse_minor: '',
|
|
358
|
+
clickhouse_commit: '',
|
|
359
|
+
os: info.os,
|
|
360
|
+
arch: info.architecture,
|
|
361
|
+
build: '',
|
|
362
|
+
core_commit: '',
|
|
363
|
+
inputs_sha256: '',
|
|
364
|
+
library: fallbackLibrary,
|
|
365
|
+
library_sha256: '',
|
|
366
|
+
library_bytes: 0,
|
|
367
|
+
};
|
|
368
|
+
if (manifest.config === undefined)
|
|
369
|
+
return fallback;
|
|
370
|
+
try {
|
|
371
|
+
const bytes = await fetchBlobBytesByDigest([repositoryRoot], manifest.config, { ...baseReqOptions, maxBytes: 0 });
|
|
372
|
+
const json = parseJsonValue(bytes);
|
|
373
|
+
if (json === null)
|
|
374
|
+
return fallback;
|
|
375
|
+
return {
|
|
376
|
+
...fallback,
|
|
377
|
+
clickhouse_version: asString(field(json, 'clickhouse_version')) || fallback.clickhouse_version,
|
|
378
|
+
channel: asString(field(json, 'channel')),
|
|
379
|
+
build: asString(field(json, 'build')),
|
|
380
|
+
library: asString(field(json, 'library')) || fallback.library,
|
|
381
|
+
library_sha256: asString(field(json, 'library_sha256')),
|
|
382
|
+
library_bytes: Number(asString(field(json, 'library_bytes'))) || 0,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
return fallback;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async function ensureFrozen(request, platform, options) {
|
|
390
|
+
const root = cacheRoot(options.cacheDir);
|
|
391
|
+
const lockPath = options.lockPath ?? path.join(process.cwd(), 'chtypes.lock');
|
|
392
|
+
const lock = await readLock(lockPath);
|
|
393
|
+
if (lock === undefined) {
|
|
394
|
+
throw new ArtifactPinnedError(`chtypes: --frozen with no lock file at ${lockPath}; run \`fetch --lock\` first`);
|
|
395
|
+
}
|
|
396
|
+
const pin = getPin(lock, request, platform);
|
|
397
|
+
if (pin === undefined) {
|
|
398
|
+
throw new ArtifactPinnedError(`chtypes: ${lockPath} names no entry for ${request}/${platform}; re-lock with \`fetch --lock\``);
|
|
399
|
+
}
|
|
400
|
+
const bases = resolveBases(options);
|
|
401
|
+
const baseReqOptions = requestOptionsFor(options, bases);
|
|
402
|
+
const trustedKeys = defaultTrustedKeys(options);
|
|
403
|
+
const info = platformInfo(platform);
|
|
404
|
+
// `--frozen` skips resolution and discovery entirely (guide §6): every
|
|
405
|
+
// request is by digest — the manifest, the layer and the bundle blob all
|
|
406
|
+
// come from the lock's own pin, from any configured base.
|
|
407
|
+
const manifest = await fetchManifestByDigest(bases, pin.manifest, { ...baseReqOptions, maxBytes: 0 });
|
|
408
|
+
if (manifest.layer.digest !== pin.layer) {
|
|
409
|
+
throw new ArtifactCorruptError(`chtypes: manifest ${manifest.digest}'s layer does not match the locked digest ${pin.layer}`);
|
|
410
|
+
}
|
|
411
|
+
const manifestHex = hexOfDigest(manifest.digest);
|
|
412
|
+
const finalDir = unpackedDir(root, manifestHex);
|
|
413
|
+
const existing = await readVerifiedRecord(finalDir);
|
|
414
|
+
if (existing !== undefined) {
|
|
415
|
+
return resolvedFromRecord(existing, platform, request, finalDir, 'cache', true, []);
|
|
416
|
+
}
|
|
417
|
+
const bundleBytes = await fetchBlobBytesByDigest(bases, { mediaType: MEDIA_TYPE_BUNDLE, digest: pin.bundle }, { ...baseReqOptions, maxBytes: 0 });
|
|
418
|
+
const verified = verifyBundleSignature(bundleBytes, trustedKeys);
|
|
419
|
+
let predicate;
|
|
420
|
+
let signedBy;
|
|
421
|
+
let warnings = [];
|
|
422
|
+
if (verified === undefined) {
|
|
423
|
+
if (options.allowUnsigned !== true) {
|
|
424
|
+
throw new ArtifactUntrustedError(`chtypes: the locked bundle ${pin.bundle} does not verify under a trusted key`);
|
|
425
|
+
}
|
|
426
|
+
predicate = await unsignedPredicateFallback(bases[0], manifest, request, info, baseReqOptions);
|
|
427
|
+
signedBy = '';
|
|
428
|
+
warnings = ['chtypes: proceeding with an unsigned artifact (CHTYPES_ALLOW_UNSIGNED)'];
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
predicate = checkArtifactStatement(parseStatement(verified.payload), PREDICATE_TYPE_ARTIFACT, manifest.layer.digest, {
|
|
432
|
+
os: info.os,
|
|
433
|
+
arch: info.architecture,
|
|
434
|
+
requestedSpelling: request,
|
|
435
|
+
});
|
|
436
|
+
signedBy = verified.signedBy;
|
|
437
|
+
}
|
|
438
|
+
const staging = await freshStagingDir(root);
|
|
439
|
+
const tempLayerPath = path.join(root, `.tmp-layer-${process.pid}-${randomBytes(6).toString('hex')}`);
|
|
440
|
+
try {
|
|
441
|
+
const entries = await fetchVerifyAndUnpackLayer(bases, manifest.layer, tempLayerPath, staging, { ...baseReqOptions, maxBytes: 0 });
|
|
442
|
+
if (!entries.some((e) => e.name === predicate.library)) {
|
|
443
|
+
throw new ArtifactUnpublishedError(`chtypes: the layer does not contain its own predicate's library ${JSON.stringify(predicate.library)}`);
|
|
444
|
+
}
|
|
445
|
+
if (verified !== undefined) {
|
|
446
|
+
await verifyInstalledLibrary(path.join(staging, predicate.library), predicate.library_sha256, predicate.library_bytes);
|
|
447
|
+
}
|
|
448
|
+
const record = buildRecord({
|
|
449
|
+
platform,
|
|
450
|
+
predicate,
|
|
451
|
+
library: await measureLibrary(path.join(staging, predicate.library)),
|
|
452
|
+
indexDigest: null,
|
|
453
|
+
manifestDigest: manifest.digest,
|
|
454
|
+
layerDigest: manifest.layer.digest,
|
|
455
|
+
bundleDigest: verified === undefined ? null : pin.bundle,
|
|
456
|
+
bundleManifestDigest: null,
|
|
457
|
+
signedBy: verified === undefined ? null : signedBy,
|
|
458
|
+
});
|
|
459
|
+
await writeVerifiedRecord(staging, record);
|
|
460
|
+
await commitStaging(staging, finalDir);
|
|
461
|
+
return resolvedFromRecord(record, platform, request, finalDir, bases[0], false, warnings);
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
await removeStaging(staging);
|
|
465
|
+
throw err;
|
|
466
|
+
}
|
|
467
|
+
finally {
|
|
468
|
+
await unlink(tempLayerPath).catch(() => { });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* `update`: re-resolve every (request, platform) pair the existing lock
|
|
473
|
+
* names against the current index and rewrite the lock from that fresh set
|
|
474
|
+
* alone — never a merge with a stale entry (guide §6). A request with no
|
|
475
|
+
* platform that verifies is dropped rather than carried over stale.
|
|
476
|
+
*/
|
|
477
|
+
async function updateLock(options, bases, baseReqOptions, trustedKeys) {
|
|
478
|
+
const lockPath = options.lockPath ?? path.join(process.cwd(), 'chtypes.lock');
|
|
479
|
+
const existing = await readLock(lockPath);
|
|
480
|
+
if (existing === undefined)
|
|
481
|
+
throw new ArtifactPinnedError(`chtypes: update with no lock file at ${lockPath}`);
|
|
482
|
+
let fresh = emptyLock();
|
|
483
|
+
for (const [spelling, byPlatform] of Object.entries(existing.requests)) {
|
|
484
|
+
for (const key of Object.keys(byPlatform)) {
|
|
485
|
+
const platformKey = key;
|
|
486
|
+
const info = platformInfo(platformKey);
|
|
487
|
+
const resolved = await resolveTag(bases, spelling, platformKey, { ...baseReqOptions, maxBytes: 0 });
|
|
488
|
+
const trust = await verifyAnyReferrerBundle(resolved.repositoryRoot, resolved.manifest.digest, MEDIA_TYPE_BUNDLE, trustedKeys, (statement) => checkArtifactStatement(statement, PREDICATE_TYPE_ARTIFACT, resolved.manifest.layer.digest, {
|
|
489
|
+
os: info.os,
|
|
490
|
+
arch: info.architecture,
|
|
491
|
+
requestedSpelling: spelling,
|
|
492
|
+
}), { ...baseReqOptions, maxBytes: 0 });
|
|
493
|
+
if (trust === undefined)
|
|
494
|
+
continue;
|
|
495
|
+
const predicate = trust.statement.predicate;
|
|
496
|
+
fresh = withPin(fresh, spelling, platformKey, {
|
|
497
|
+
version: predicate.clickhouse_version,
|
|
498
|
+
build: predicate.build,
|
|
499
|
+
manifest: resolved.manifest.digest,
|
|
500
|
+
layer: resolved.manifest.layer.digest,
|
|
501
|
+
bundle: trust.bundleDigest,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
await writeLock(lockPath, fresh);
|
|
506
|
+
}
|
|
507
|
+
async function writeLockEntry(request, platform, record, options, repositoryRoot, indexBytes, baseReqOptions) {
|
|
508
|
+
const lockPath = options.lockPath ?? path.join(process.cwd(), 'chtypes.lock');
|
|
509
|
+
let lock = (await readLock(lockPath)) ?? emptyLock();
|
|
510
|
+
lock = withPin(lock, request, platform, {
|
|
511
|
+
version: record.version,
|
|
512
|
+
build: record.build,
|
|
513
|
+
manifest: record.manifestDigest,
|
|
514
|
+
layer: record.layerDigest,
|
|
515
|
+
bundle: record.bundleDigest ?? '',
|
|
516
|
+
});
|
|
517
|
+
if (options.lockAllPlatforms === true) {
|
|
518
|
+
const indexJson = parseJsonValue(indexBytes);
|
|
519
|
+
const manifests = items(field(indexJson ?? undefined, 'manifests'));
|
|
520
|
+
const trustedKeys = defaultTrustedKeys(options);
|
|
521
|
+
for (const m of manifests) {
|
|
522
|
+
const p = field(m, 'platform');
|
|
523
|
+
const key = `${asString(field(p, 'os'))}-${asString(field(p, 'architecture'))}`;
|
|
524
|
+
if (key === platform)
|
|
525
|
+
continue; // already pinned above, with its layer downloaded
|
|
526
|
+
if (!isLikelyPlatformKey(key))
|
|
527
|
+
continue;
|
|
528
|
+
const digest = asString(field(m, 'digest'));
|
|
529
|
+
if (digest === '')
|
|
530
|
+
continue;
|
|
531
|
+
try {
|
|
532
|
+
const otherManifest = await fetchManifestByDigest([repositoryRoot], digest, { ...baseReqOptions, maxBytes: 0 });
|
|
533
|
+
const otherInfo = platformInfo(key);
|
|
534
|
+
const trust = await verifyAnyReferrerBundle(repositoryRoot, otherManifest.digest, MEDIA_TYPE_BUNDLE, trustedKeys, (statement) => checkArtifactStatement(statement, PREDICATE_TYPE_ARTIFACT, otherManifest.layer.digest, {
|
|
535
|
+
os: otherInfo.os,
|
|
536
|
+
arch: otherInfo.architecture,
|
|
537
|
+
requestedSpelling: request,
|
|
538
|
+
}), { ...baseReqOptions, maxBytes: 0 });
|
|
539
|
+
if (trust === undefined)
|
|
540
|
+
continue;
|
|
541
|
+
const otherPredicate = trust.statement.predicate;
|
|
542
|
+
lock = withPin(lock, request, key, {
|
|
543
|
+
version: otherPredicate.clickhouse_version,
|
|
544
|
+
build: otherPredicate.build,
|
|
545
|
+
manifest: otherManifest.digest,
|
|
546
|
+
layer: otherManifest.layer.digest,
|
|
547
|
+
bundle: trust.bundleDigest,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
catch {
|
|
551
|
+
// Best-effort: a platform whose bundle cannot be fetched/verified is
|
|
552
|
+
// left out of the lock rather than failing the whole write.
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
await writeLock(lockPath, lock);
|
|
557
|
+
}
|
|
558
|
+
function isLikelyPlatformKey(key) {
|
|
559
|
+
return key === 'linux-amd64' || key === 'linux-arm64' || key === 'darwin-arm64';
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* The generic signed-artifact fetch (guide §9): `repository` is a full
|
|
563
|
+
* repository root (like a base — see `types.ts`'s `endpointUrl`), `ref` a
|
|
564
|
+
* tag or digest. Resolves the manifest, downloads and verifies its single
|
|
565
|
+
* layer, and verifies a referrer bundle of `predicateType` against it —
|
|
566
|
+
* `predicateType` and subject digest only (`checkGenericStatement`): these
|
|
567
|
+
* objects' predicate shapes are not yet specified (blocked on the producer,
|
|
568
|
+
* plan §1.2), so the caller reads `statement.predicate` itself. Returns the
|
|
569
|
+
* verified blob's path on disk (written into this layout's `blobs/`, not
|
|
570
|
+
* unpacked — unpacking is this fetch's caller's job, since goldens and
|
|
571
|
+
* fixtures are not necessarily tarballs).
|
|
572
|
+
*
|
|
573
|
+
* For `predicateType` = `PREDICATE_TYPE_GOLDENS`, `ref` is instead a
|
|
574
|
+
* PLATFORM manifest digest: the registry is append-only, so a corrected
|
|
575
|
+
* goldens set is a second goldens referrer of it, and the result is the
|
|
576
|
+
* highest-`revision` verified one (`goldens.ts`, guide §9).
|
|
577
|
+
*/
|
|
578
|
+
export async function fetchSigned(repository, ref, predicateType, options = {}) {
|
|
579
|
+
const root = cacheRoot(options.cacheDir);
|
|
580
|
+
await ensureLayout(root);
|
|
581
|
+
const baseReqOptions = requestOptionsFor(options, [repository]);
|
|
582
|
+
const trustedKeys = defaultTrustedKeys(options);
|
|
583
|
+
if (predicateType === PREDICATE_TYPE_GOLDENS) {
|
|
584
|
+
return fetchGoldens(repository, ref, root, trustedKeys, options.allowUnsigned === true, { ...baseReqOptions, maxBytes: 0 });
|
|
585
|
+
}
|
|
586
|
+
const manifest = await fetchManifestByDigest([repository], ref, { ...baseReqOptions, maxBytes: 0 });
|
|
587
|
+
const layerBytes = await fetchBlobBytesByDigest([repository], manifest.layer, { ...baseReqOptions, maxBytes: 0 });
|
|
588
|
+
const trust = await verifyAnyReferrerBundle(repository, manifest.digest, MEDIA_TYPE_BUNDLE, trustedKeys, (statement) => checkGenericStatement(statement, predicateType, manifest.layer.digest), { ...baseReqOptions, maxBytes: 0 });
|
|
589
|
+
if (trust === undefined && options.allowUnsigned !== true) {
|
|
590
|
+
throw new ArtifactUntrustedError(`chtypes: no referrer of ${manifest.digest} verified under a trusted key`);
|
|
591
|
+
}
|
|
592
|
+
const dest = await installBlob(root, hexOfDigest(manifest.layer.digest), layerBytes);
|
|
593
|
+
return {
|
|
594
|
+
path: dest,
|
|
595
|
+
statement: trust?.statement,
|
|
596
|
+
digests: { manifest: manifest.digest, layer: manifest.layer.digest },
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* `fetchSigned` for the goldens predicate type: the highest-revision verified
|
|
601
|
+
* goldens referrer of the platform manifest `subject`.
|
|
602
|
+
*
|
|
603
|
+
* Every candidate is verified on its own (its blob against its descriptor,
|
|
604
|
+
* its own signature referrer, the statement's subject against the blob
|
|
605
|
+
* digest, the predicateType). A candidate that fails is skipped: never
|
|
606
|
+
* chosen, never a tie. If none verifies, the first failure is thrown. A
|
|
607
|
+
* VERIFIED candidate whose `revision` is unusable is not skipped: it could be
|
|
608
|
+
* the newest document, and quietly choosing an older set is the stale pick
|
|
609
|
+
* this rule exists to prevent.
|
|
610
|
+
*/
|
|
611
|
+
async function fetchGoldens(repository, subject, root, trustedKeys, allowUnsigned, reqOptions) {
|
|
612
|
+
if (!/^sha256:[0-9a-f]{64}$/.test(subject)) {
|
|
613
|
+
throw new ArtifactCorruptError(`chtypes: a goldens fetch names a platform manifest by digest, not ${JSON.stringify(subject)}`);
|
|
614
|
+
}
|
|
615
|
+
const listed = await discoverSignatureCandidates(repository, subject, GOLDENS_ARTIFACT_TYPE, reqOptions);
|
|
616
|
+
if (listed.length === 0)
|
|
617
|
+
throw new ArtifactUnpublishedError(`chtypes: no goldens referrer for ${subject}`);
|
|
618
|
+
const verified = [];
|
|
619
|
+
const unsigned = [];
|
|
620
|
+
let firstFailure;
|
|
621
|
+
const note = (err) => {
|
|
622
|
+
firstFailure ??= err;
|
|
623
|
+
};
|
|
624
|
+
const seen = new Set();
|
|
625
|
+
for (const referrer of listed) {
|
|
626
|
+
if (seen.has(referrer.digest))
|
|
627
|
+
continue;
|
|
628
|
+
seen.add(referrer.digest);
|
|
629
|
+
let blob;
|
|
630
|
+
let blobDigest;
|
|
631
|
+
try {
|
|
632
|
+
const manifest = await fetchManifestByDigest([repository], referrer.digest, reqOptions);
|
|
633
|
+
blobDigest = manifest.layer.digest;
|
|
634
|
+
blob = await fetchBlobBytesByDigest([repository], manifest.layer, reqOptions);
|
|
635
|
+
}
|
|
636
|
+
catch (err) {
|
|
637
|
+
if (!(err instanceof FetchV1Error))
|
|
638
|
+
throw err;
|
|
639
|
+
note(err);
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
unsigned.push({ manifest: referrer.digest, blob: blobDigest, bytes: blob });
|
|
643
|
+
let found;
|
|
644
|
+
const bundles = await discoverSignatureCandidates(repository, referrer.digest, MEDIA_TYPE_BUNDLE, reqOptions);
|
|
645
|
+
for (const bundleRef of bundles) {
|
|
646
|
+
let bundleBytes;
|
|
647
|
+
let bundleDigest;
|
|
648
|
+
try {
|
|
649
|
+
const bundleManifest = await fetchManifestByDigest([repository], bundleRef.digest, reqOptions);
|
|
650
|
+
bundleDigest = bundleManifest.layer.digest;
|
|
651
|
+
bundleBytes = await fetchBlobBytesByDigest([repository], bundleManifest.layer, reqOptions);
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
const signed = verifyBundleSignature(bundleBytes, trustedKeys);
|
|
657
|
+
if (signed === undefined) {
|
|
658
|
+
note(new ArtifactUntrustedError(`chtypes: no goldens signature of ${referrer.digest} verifies under a trusted key`));
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
let statement;
|
|
662
|
+
try {
|
|
663
|
+
statement = parseStatement(signed.payload);
|
|
664
|
+
checkGenericStatement(statement, PREDICATE_TYPE_GOLDENS, blobDigest);
|
|
665
|
+
}
|
|
666
|
+
catch (err) {
|
|
667
|
+
if (!(err instanceof ArtifactCorruptError))
|
|
668
|
+
throw err;
|
|
669
|
+
note(err);
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
found = {
|
|
673
|
+
manifest: referrer.digest,
|
|
674
|
+
blob: blobDigest,
|
|
675
|
+
revision: statementRevision(signed.payload),
|
|
676
|
+
bytes: blob,
|
|
677
|
+
predicate: statement.predicate,
|
|
678
|
+
signedBy: signed.signedBy,
|
|
679
|
+
bundle: bundleDigest,
|
|
680
|
+
};
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
if (found !== undefined)
|
|
684
|
+
verified.push(found);
|
|
685
|
+
}
|
|
686
|
+
let chosen;
|
|
687
|
+
if (verified.length > 0) {
|
|
688
|
+
chosen = selectGoldens(verified);
|
|
689
|
+
}
|
|
690
|
+
else if (allowUnsigned && unsigned.length === 1) {
|
|
691
|
+
chosen = { ...unsigned[0], predicate: undefined };
|
|
692
|
+
}
|
|
693
|
+
else if (allowUnsigned && unsigned.length > 1) {
|
|
694
|
+
throw new ArtifactCorruptError(`chtypes: ${unsigned.length} unsigned goldens documents of ${subject}: no signature to read a revision from, so none can be chosen`);
|
|
695
|
+
}
|
|
696
|
+
else if (firstFailure !== undefined) {
|
|
697
|
+
throw firstFailure;
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
throw new ArtifactUntrustedError(`chtypes: no goldens referrer of ${subject} verifies under a trusted key`);
|
|
701
|
+
}
|
|
702
|
+
const dest = await installBlob(root, hexOfDigest(chosen.blob), chosen.bytes);
|
|
703
|
+
return { path: dest, statement: chosen.predicate, digests: { manifest: chosen.manifest, layer: chosen.blob } };
|
|
704
|
+
}
|
|
705
|
+
// ---------------------------------------------------------------------- listTags
|
|
706
|
+
/** Compares two version spellings component by component, numerically. */
|
|
707
|
+
function compareSpellings(a, b) {
|
|
708
|
+
const pa = a.split('.').map(Number);
|
|
709
|
+
const pb = b.split('.').map(Number);
|
|
710
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
711
|
+
const d = (pa[i] ?? -1) - (pb[i] ?? -1);
|
|
712
|
+
if (d !== 0)
|
|
713
|
+
return d;
|
|
714
|
+
}
|
|
715
|
+
return 0;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* The version spellings a repository publishes: `GET tags/list` from the first
|
|
719
|
+
* base that has the repository, every tag that is not a version spelling
|
|
720
|
+
* (`SPELLING_REGEX`: the `sha256-<hex>` referrers fallback tags, symbolic tags)
|
|
721
|
+
* dropped, the rest in ascending numeric order. A tag names a version, not a
|
|
722
|
+
* platform: whether a platform is offered is that tag's index, which only a
|
|
723
|
+
* resolve reads. A next page is followed through the `Link` header.
|
|
724
|
+
*/
|
|
725
|
+
export async function listTags(options = {}) {
|
|
726
|
+
const bases = resolveBases(options);
|
|
727
|
+
const reqOptions = { ...requestOptionsFor(options, bases), maxBytes: TAGS_LIST_MAX_BYTES };
|
|
728
|
+
const spelling = new RegExp(SPELLING_REGEX);
|
|
729
|
+
let lastUnreachable;
|
|
730
|
+
for (let i = 0; i < bases.length; i++) {
|
|
731
|
+
const base = bases[i];
|
|
732
|
+
const isLast = i === bases.length - 1;
|
|
733
|
+
const tags = new Set();
|
|
734
|
+
let url = endpointUrl(base, '/tags/list');
|
|
735
|
+
let found = false;
|
|
736
|
+
for (let page = 0; url !== undefined && page < 64; page++) {
|
|
737
|
+
let res;
|
|
738
|
+
try {
|
|
739
|
+
res = url.startsWith('file:') ? await readFileUrl(url, TAGS_LIST_MAX_BYTES) : await requestBuffered(url, reqOptions);
|
|
740
|
+
}
|
|
741
|
+
catch (err) {
|
|
742
|
+
if (err instanceof SourceUnreachableError && !isLast) {
|
|
743
|
+
lastUnreachable = err;
|
|
744
|
+
url = undefined;
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
throw err;
|
|
748
|
+
}
|
|
749
|
+
if (res.status === 404) {
|
|
750
|
+
url = undefined;
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
if (res.status !== 200)
|
|
754
|
+
throw new SourceUnreachableError(`chtypes: ${url} returned status ${res.status}`);
|
|
755
|
+
const json = parseJsonValue(res.body);
|
|
756
|
+
if (json === null)
|
|
757
|
+
throw new ArtifactCorruptError(`chtypes: ${url} did not return valid JSON`);
|
|
758
|
+
found = true;
|
|
759
|
+
for (const t of items(field(json, 'tags'))) {
|
|
760
|
+
const tag = asString(t);
|
|
761
|
+
if (spelling.test(tag))
|
|
762
|
+
tags.add(tag);
|
|
763
|
+
}
|
|
764
|
+
const link = res.headers['link'];
|
|
765
|
+
const next = link === undefined ? undefined : /<([^>]+)>\s*;\s*rel="?next"?/.exec(link)?.[1];
|
|
766
|
+
url = next === undefined ? undefined : new URL(next, url).toString();
|
|
767
|
+
}
|
|
768
|
+
if (found)
|
|
769
|
+
return [...tags].sort(compareSpellings);
|
|
770
|
+
}
|
|
771
|
+
if (lastUnreachable !== undefined)
|
|
772
|
+
throw lastUnreachable;
|
|
773
|
+
throw new ArtifactUnpublishedError(`chtypes: no base serves a tag list (${bases.join(', ')})`);
|
|
774
|
+
}
|
|
775
|
+
//# sourceMappingURL=ensure.js.map
|