@wavehouse/chtypes 2.0.0-dev.1 → 2.0.0-dev.10
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 +85 -0
- package/README.md +5 -1
- package/dist/abi2/buildinfo.d.ts +1 -1
- package/dist/abi2/buildinfo.js +3 -3
- package/dist/abi2/buildinfo.js.map +1 -1
- package/dist/abi2/calls.gen.js +1 -1
- package/dist/abi2/decls.gen.d.ts +1 -1
- package/dist/abi2/decls.gen.d.ts.map +1 -1
- package/dist/abi2/decls.gen.js +3 -2
- package/dist/abi2/decls.gen.js.map +1 -1
- package/dist/abi2/errmap.gen.js +1 -1
- package/dist/abi2/errors.d.ts +16 -38
- package/dist/abi2/errors.d.ts.map +1 -1
- package/dist/abi2/errors.js +16 -42
- package/dist/abi2/errors.js.map +1 -1
- package/dist/abi2/index.d.ts +2 -2
- package/dist/abi2/index.d.ts.map +1 -1
- package/dist/abi2/index.js +1 -1
- package/dist/abi2/index.js.map +1 -1
- package/dist/abi2/libc.gen.d.ts +2 -0
- package/dist/abi2/libc.gen.d.ts.map +1 -1
- package/dist/abi2/libc.gen.js +11 -1
- package/dist/abi2/libc.gen.js.map +1 -1
- package/dist/abi2/loader.js +2 -2
- package/dist/abi2/loader.js.map +1 -1
- package/dist/abi2/vocab.gen.d.ts +41 -0
- package/dist/abi2/vocab.gen.d.ts.map +1 -1
- package/dist/abi2/vocab.gen.js +65 -1
- package/dist/abi2/vocab.gen.js.map +1 -1
- package/dist/cli.d.ts +11 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +130 -20
- package/dist/cli.js.map +1 -1
- package/dist/documents.d.ts +63 -3
- package/dist/documents.d.ts.map +1 -1
- package/dist/documents.js +79 -4
- package/dist/documents.js.map +1 -1
- package/dist/env.d.ts +8 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +17 -8
- package/dist/env.js.map +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/library.d.ts +13 -2
- package/dist/library.d.ts.map +1 -1
- package/dist/library.js +23 -5
- package/dist/library.js.map +1 -1
- package/dist/ocifetch/channel.d.ts +84 -0
- package/dist/ocifetch/channel.d.ts.map +1 -1
- package/dist/ocifetch/channel.js +157 -2
- package/dist/ocifetch/channel.js.map +1 -1
- package/dist/ocifetch/constants.gen.d.ts +28 -0
- package/dist/ocifetch/constants.gen.d.ts.map +1 -1
- package/dist/ocifetch/constants.gen.js +30 -1
- package/dist/ocifetch/constants.gen.js.map +1 -1
- package/dist/ocifetch/dsse.d.ts +5 -1
- package/dist/ocifetch/dsse.d.ts.map +1 -1
- package/dist/ocifetch/dsse.js +10 -3
- package/dist/ocifetch/dsse.js.map +1 -1
- package/dist/ocifetch/ensure.d.ts +68 -8
- package/dist/ocifetch/ensure.d.ts.map +1 -1
- package/dist/ocifetch/ensure.js +305 -66
- package/dist/ocifetch/ensure.js.map +1 -1
- package/dist/ocifetch/errors.d.ts +101 -25
- package/dist/ocifetch/errors.d.ts.map +1 -1
- package/dist/ocifetch/errors.js +74 -36
- package/dist/ocifetch/errors.js.map +1 -1
- package/dist/ocifetch/hold.d.ts +66 -0
- package/dist/ocifetch/hold.d.ts.map +1 -0
- package/dist/ocifetch/hold.js +150 -0
- package/dist/ocifetch/hold.js.map +1 -0
- package/dist/ocifetch/http.d.ts +10 -2
- package/dist/ocifetch/http.d.ts.map +1 -1
- package/dist/ocifetch/http.js +31 -12
- package/dist/ocifetch/http.js.map +1 -1
- package/dist/ocifetch/index.d.ts +10 -5
- package/dist/ocifetch/index.d.ts.map +1 -1
- package/dist/ocifetch/index.js +8 -3
- package/dist/ocifetch/index.js.map +1 -1
- package/dist/ocifetch/localverify.d.ts +3 -1
- package/dist/ocifetch/localverify.d.ts.map +1 -1
- package/dist/ocifetch/localverify.js +6 -1
- package/dist/ocifetch/localverify.js.map +1 -1
- package/dist/ocifetch/lock.d.ts +2 -1
- package/dist/ocifetch/lock.d.ts.map +1 -1
- package/dist/ocifetch/lock.js +7 -5
- package/dist/ocifetch/lock.js.map +1 -1
- package/dist/ocifetch/oci.d.ts +45 -3
- package/dist/ocifetch/oci.d.ts.map +1 -1
- package/dist/ocifetch/oci.js +125 -58
- package/dist/ocifetch/oci.js.map +1 -1
- package/dist/ocifetch/prune.d.ts +48 -0
- package/dist/ocifetch/prune.d.ts.map +1 -0
- package/dist/ocifetch/prune.js +383 -0
- package/dist/ocifetch/prune.js.map +1 -0
- package/dist/ocifetch/referrers.d.ts +24 -0
- package/dist/ocifetch/referrers.d.ts.map +1 -1
- package/dist/ocifetch/referrers.js +92 -5
- package/dist/ocifetch/referrers.js.map +1 -1
- package/dist/ocifetch/resolve.d.ts +37 -0
- package/dist/ocifetch/resolve.d.ts.map +1 -0
- package/dist/ocifetch/resolve.js +90 -0
- package/dist/ocifetch/resolve.js.map +1 -0
- package/dist/ocifetch/retired.d.ts +61 -0
- package/dist/ocifetch/retired.d.ts.map +1 -0
- package/dist/ocifetch/retired.js +236 -0
- package/dist/ocifetch/retired.js.map +1 -0
- package/dist/ocifetch/revocation.d.ts +151 -0
- package/dist/ocifetch/revocation.d.ts.map +1 -0
- package/dist/ocifetch/revocation.js +481 -0
- package/dist/ocifetch/revocation.js.map +1 -0
- package/dist/ocifetch/types.d.ts +18 -2
- package/dist/ocifetch/types.d.ts.map +1 -1
- package/dist/ocifetch/types.js.map +1 -1
- package/dist/registry.d.ts +80 -3
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +168 -9
- package/dist/registry.js.map +1 -1
- package/dist/schema.d.ts +10 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js.map +1 -1
- package/dist/server.d.ts +27 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +35 -0
- package/dist/server.js.map +1 -0
- package/dist/settings.d.ts +21 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +35 -0
- package/dist/settings.js.map +1 -1
- package/package.json +1 -1
package/dist/ocifetch/ensure.js
CHANGED
|
@@ -3,21 +3,26 @@
|
|
|
3
3
|
* `listInstalled`, `verifyInstalled`, `fetchSigned`. This is the only file
|
|
4
4
|
* that wires resolve (`oci.ts`), trust (`dsse.ts`), bytes (`unpack.ts`), the
|
|
5
5
|
* cache (`layout.ts`) and the lock (`lock.ts`) together; nothing outside
|
|
6
|
-
* `index.ts` calls into those modules directly.
|
|
6
|
+
* `index.ts` calls into those modules directly. `resolve.ts` (`chtypes
|
|
7
|
+
* resolve`) takes the same resolve and trust steps, through
|
|
8
|
+
* `verifyManifestTrust`, and `prune.ts` (`chtypes prune`) reads and removes
|
|
9
|
+
* cache entries under the in-use hold (`hold.ts`).
|
|
7
10
|
*/
|
|
8
11
|
import { randomBytes } from 'node:crypto';
|
|
9
12
|
import { unlink } from 'node:fs/promises';
|
|
10
13
|
import os from 'node:os';
|
|
11
14
|
import path from 'node:path';
|
|
12
15
|
import { asString, field, items, parseJsonValue } from '../json.js';
|
|
13
|
-
import { activeChannel, allowUnsigned, noteIgnoredOverrides, refusePinning, warnIgnored } from './channel.js';
|
|
16
|
+
import { activeChannel, aheadOfRegistry, allowUnsigned, noteIgnoredOverrides, offlineMode, refusePinning, visibleToChannel, warnIgnored } from './channel.js';
|
|
14
17
|
import { BASE_SEPARATOR, CACHE_ANNOTATION_PREFIX, ENV_BASES_NAME, GOLDENS_ARTIFACT_TYPE, MEDIA_TYPE_BUNDLE, MEDIA_TYPE_MANIFEST, PREDICATE_TYPE_ARTIFACT, PREDICATE_TYPE_GOLDENS, SPELLING_REGEX, TAGS_LIST_MAX_BYTES, } from './constants.gen.js';
|
|
15
18
|
import { checkArtifactStatement, checkGenericStatement, parseStatement, verifyAnyReferrerBundle, verifyBundleSignature } from './dsse.js';
|
|
16
19
|
import { ArtifactCorruptError, ArtifactMissingError, ArtifactPinnedError, ArtifactUnpublishedError, ArtifactUntrustedError, FetchV1Error, SourceUnreachableError, } from './errors.js';
|
|
17
20
|
import { readFileUrl, requestBuffered } from './http.js';
|
|
18
21
|
import { buildRecord, cacheRoot, commitStaging, ensureLayout, freshStagingDir, installBlob, listVerified, readIndexEntries, readVerifiedRecord, removeStaging, systemDirs, unpackedDir, upsertIndexEntry, writeVerifiedRecord, zeroXHint, } from './layout.js';
|
|
19
22
|
import { selectGoldens, statementRevision } from './goldens.js';
|
|
20
|
-
import { discoverSignatureCandidates } from './referrers.js';
|
|
23
|
+
import { discoverSignatureCandidates, readListing } from './referrers.js';
|
|
24
|
+
import { isGone } from './retired.js';
|
|
25
|
+
import { checkCandidates, checkFrozenInstalled, checkInstalledOnce, checkInstalledStored, checkListing, revocationOn, settle, storedRevocation, storePending, subjectOfStatement, } from './revocation.js';
|
|
21
26
|
import { probeRoots, strictMode, unwritable } from './faults.js';
|
|
22
27
|
import { verifyAndInstallFromLocalBlobs } from './localverify.js';
|
|
23
28
|
import { fetchBlobBytesByDigest, fetchManifestByDigest, resolveTag } from './oci.js';
|
|
@@ -32,7 +37,7 @@ function defaultTrustedKeys(options) {
|
|
|
32
37
|
return channel.keys;
|
|
33
38
|
}
|
|
34
39
|
/** The bases: the caller's or `CHTYPES_ARTIFACTS_URL` when the active contract honors them, else the contract's own (the staging dev channel alone, rule r6). */
|
|
35
|
-
function resolveBases(options) {
|
|
40
|
+
export function resolveBases(options) {
|
|
36
41
|
const channel = activeChannel();
|
|
37
42
|
if (!channel.overridable)
|
|
38
43
|
return channel.bases;
|
|
@@ -70,13 +75,53 @@ function tokenHostsFor(bases) {
|
|
|
70
75
|
}
|
|
71
76
|
return hosts;
|
|
72
77
|
}
|
|
73
|
-
|
|
78
|
+
/** The request options every fetch over `bases` sends: the clock, and the token for the configured base hosts only. */
|
|
79
|
+
export function requestOptionsFor(options, bases) {
|
|
74
80
|
return {
|
|
75
81
|
clock: options.clock ?? realClock(),
|
|
76
82
|
...(options.token !== undefined ? { token: options.token } : {}),
|
|
77
83
|
tokenHosts: tokenHostsFor(bases),
|
|
78
84
|
};
|
|
79
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* What one revocation check reads from `options` (`revocation.ts`). The override
|
|
88
|
+
* is per call and forensic: it never lets a lock or an update pin a revoked build.
|
|
89
|
+
*/
|
|
90
|
+
export function revocationContext(options) {
|
|
91
|
+
const bases = resolveBases(options);
|
|
92
|
+
return {
|
|
93
|
+
bases,
|
|
94
|
+
trustedKeys: defaultTrustedKeys(options),
|
|
95
|
+
request: requestOptionsFor(options, bases),
|
|
96
|
+
cacheRoot: cacheRoot(options.cacheDir),
|
|
97
|
+
offline: offlineMode(options.offline),
|
|
98
|
+
allowRevoked: options.allowRevoked === true && options.lockWrite !== true && options.update !== true,
|
|
99
|
+
requireRevocationCheck: options.requireRevocationCheck === true,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** `res` with `warnings` added to its own. */
|
|
103
|
+
function withWarnings(res, warnings) {
|
|
104
|
+
return warnings.length === 0 ? res : { ...res, warnings: [...res.warnings, ...warnings] };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The installed-build revocation check a registry makes after `resolveInstalled`
|
|
108
|
+
* answers and before it loads the build (guide §9, `check_installed`): the stored
|
|
109
|
+
* record, then, unless offline, at most one check per build per process. The
|
|
110
|
+
* warnings the open prints are returned even when the build is refused, as Go
|
|
111
|
+
* returns them beside the error. Under the v1 contract it does nothing.
|
|
112
|
+
*/
|
|
113
|
+
export async function checkInstalled(res, options = {}) {
|
|
114
|
+
const warnings = [];
|
|
115
|
+
try {
|
|
116
|
+
await checkInstalledOnce(revocationContext(options), res, warnings);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
if (err instanceof FetchV1Error)
|
|
120
|
+
return { warnings, error: err };
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
return { warnings, error: undefined };
|
|
124
|
+
}
|
|
80
125
|
/** Builds the `Resolved` the seam promises, from a `verified.json` record plus where it came from. */
|
|
81
126
|
function resolvedFromRecord(record, platform, request, dir, source, alreadyInstalled, warnings) {
|
|
82
127
|
return {
|
|
@@ -134,6 +179,9 @@ export async function resolveInstalled(request, platform, options = {}) {
|
|
|
134
179
|
continue;
|
|
135
180
|
if (!withinRequest(record.version, request))
|
|
136
181
|
continue;
|
|
182
|
+
// Under the dev channel, only its own fingerprint's builds (`visibleToChannel`).
|
|
183
|
+
if (!visibleToChannel(record.predicate))
|
|
184
|
+
continue;
|
|
137
185
|
candidates.push({ record, dir, source: r.source, rank });
|
|
138
186
|
}
|
|
139
187
|
}
|
|
@@ -143,10 +191,35 @@ export async function resolveInstalled(request, platform, options = {}) {
|
|
|
143
191
|
const best = candidates[0];
|
|
144
192
|
return resolvedFromRecord(best.record, platform, request, best.dir, best.source, true, warnings);
|
|
145
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* The build id of the newest cached record of this SDK's OWN fingerprint that
|
|
196
|
+
* answers the request (`resolveInstalled`'s filter), `''` when it names none,
|
|
197
|
+
* or `undefined` when the cache holds no such record (public issue #581).
|
|
198
|
+
*/
|
|
199
|
+
async function cachedOwnBuild(options, platform, request) {
|
|
200
|
+
let best;
|
|
201
|
+
for (const r of searchRoots(options)) {
|
|
202
|
+
for (const { record } of await listVerified(r.root)) {
|
|
203
|
+
if (record.platform !== platform)
|
|
204
|
+
continue;
|
|
205
|
+
if (!withinRequest(record.version, request))
|
|
206
|
+
continue;
|
|
207
|
+
if (!visibleToChannel(record.predicate))
|
|
208
|
+
continue;
|
|
209
|
+
if (best === undefined || compareRecords(record, best) > 0)
|
|
210
|
+
best = record;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return best === undefined ? undefined : best.build;
|
|
214
|
+
}
|
|
146
215
|
/** The cache, then every system directory in order: the one search order every lookup uses. */
|
|
147
216
|
function searchRoots(options) {
|
|
148
217
|
return [{ root: cacheRoot(options.cacheDir), source: 'cache' }, ...systemDirs(options.systemDirs).map((d) => ({ root: d, source: `system:${d}` }))];
|
|
149
218
|
}
|
|
219
|
+
/** The directories every lookup reads, in order: the cache root, then each system directory (public issue #530). It reads and creates nothing. */
|
|
220
|
+
export function searchDirs(options = {}) {
|
|
221
|
+
return searchRoots(options).map((r) => r.root);
|
|
222
|
+
}
|
|
150
223
|
/** Two records by (version, build): the version numerically, part by part, then the fixed-width build. */
|
|
151
224
|
function compareRecords(a, b) {
|
|
152
225
|
const av = a.version.split('.').map(Number);
|
|
@@ -205,7 +278,7 @@ async function verifyPreseededEntries(sourceDir, writeRoot, platform, trustedKey
|
|
|
205
278
|
const already = await readVerifiedRecord(unpackedDir(writeRoot, hexOfDigest(entry.digest)));
|
|
206
279
|
if (already !== undefined)
|
|
207
280
|
continue;
|
|
208
|
-
await verifyAndInstallFromLocalBlobs(sourceDir, writeRoot, entry.digest, platform, trustedKeys);
|
|
281
|
+
await verifyAndInstallFromLocalBlobs(sourceDir, writeRoot, entry.digest, platform, trustedKeys, true);
|
|
209
282
|
}
|
|
210
283
|
}
|
|
211
284
|
/**
|
|
@@ -254,6 +327,10 @@ function compareVersionThenBuild(a, b) {
|
|
|
254
327
|
* answered only by that exact version, so for it only a newer BUILD of the
|
|
255
328
|
* same version counts. A request that is not a version spelling (an
|
|
256
329
|
* arbitrary tag) names no range, so nothing is kept for it.
|
|
330
|
+
*
|
|
331
|
+
* Under the dev channel only an install of its own fingerprint counts
|
|
332
|
+
* (`visibleToChannel`): a newer build of another fingerprint, installed by
|
|
333
|
+
* another dev SDK, is never kept over this one's own.
|
|
257
334
|
*/
|
|
258
335
|
async function checkMonotonic(root, platform, request, incoming) {
|
|
259
336
|
if (!new RegExp(SPELLING_REGEX).test(request))
|
|
@@ -264,6 +341,8 @@ async function checkMonotonic(root, platform, request, incoming) {
|
|
|
264
341
|
continue;
|
|
265
342
|
if (!withinRequest(record.version, request))
|
|
266
343
|
continue;
|
|
344
|
+
if (!visibleToChannel(record.predicate))
|
|
345
|
+
continue;
|
|
267
346
|
if (compareVersionThenBuild(incoming, record.predicate) >= 0)
|
|
268
347
|
continue;
|
|
269
348
|
if (best === undefined || compareVersionThenBuild(best.record.predicate, record.predicate) < 0)
|
|
@@ -295,17 +374,22 @@ export async function listInstalled(options = {}) {
|
|
|
295
374
|
export async function verifyInstalled(options = {}) {
|
|
296
375
|
noteIgnoredOverrides(options);
|
|
297
376
|
await probeCache(options);
|
|
377
|
+
const rctx = revocationContext(options);
|
|
298
378
|
const out = [];
|
|
299
379
|
for (const r of searchRoots(options)) {
|
|
300
380
|
for (const { dir, record } of await listVerified(r.root)) {
|
|
301
381
|
const platform = record.platform;
|
|
382
|
+
let result;
|
|
302
383
|
try {
|
|
303
384
|
await verifyInstalledLibrary(path.join(dir, record.library), record.librarySha256, record.libraryBytes);
|
|
304
|
-
|
|
385
|
+
result = { platform, version: record.version, dir, ok: true, detail: '' };
|
|
305
386
|
}
|
|
306
387
|
catch (err) {
|
|
307
|
-
|
|
388
|
+
result = { platform, version: record.version, dir, ok: false, detail: err instanceof Error ? err.message : String(err) };
|
|
308
389
|
}
|
|
390
|
+
// A build a stored revocation refuses is reported as revoked, whatever its bytes say.
|
|
391
|
+
const stored = revocationOn() ? await storedRevocation(rctx, resolvedFromRecord(record, platform, record.version, dir, r.source, true, [])) : undefined;
|
|
392
|
+
out.push(stored === undefined ? result : { ...result, detail: stored.text, revocation: stored.revocation });
|
|
309
393
|
}
|
|
310
394
|
}
|
|
311
395
|
return out;
|
|
@@ -336,12 +420,18 @@ async function ensureIn(request, options) {
|
|
|
336
420
|
const root = cacheRoot(options.cacheDir);
|
|
337
421
|
// An offline lookup is read-only: it creates nothing, so a read-only mount
|
|
338
422
|
// reads cleanly (guide §6; public issue #486). Only a fetch makes the layout.
|
|
339
|
-
if (options.offline
|
|
423
|
+
if (offlineMode(options.offline)) {
|
|
424
|
+
// Offline makes no request, so only a stored revocation can refuse it (guide,
|
|
425
|
+
// "Build-level revocation").
|
|
426
|
+
if (options.frozen === true) {
|
|
427
|
+
const frozenHit = await resolveFrozenOffline(request, platform, options);
|
|
428
|
+
return withWarnings(frozenHit, await checkInstalledStored(revocationContext(options), frozenHit));
|
|
429
|
+
}
|
|
340
430
|
const hit = await resolveInstalled(request, platform, options);
|
|
341
431
|
if (hit === undefined) {
|
|
342
432
|
throw new ArtifactMissingError(withNotes(`chtypes: ${request} (${platform}) is not installed, and --offline forbids fetching it`, await missingNotes(options)));
|
|
343
433
|
}
|
|
344
|
-
return hit;
|
|
434
|
+
return withWarnings(hit, await checkInstalledStored(revocationContext(options), hit));
|
|
345
435
|
}
|
|
346
436
|
if (strictMode(options.strictCache)) {
|
|
347
437
|
// Strict: the cache is checked before anything is fetched into it.
|
|
@@ -354,6 +444,7 @@ async function ensureIn(request, options) {
|
|
|
354
444
|
const bases = resolveBases(options);
|
|
355
445
|
const baseReqOptions = requestOptionsFor(options, bases);
|
|
356
446
|
const trustedKeys = defaultTrustedKeys(options);
|
|
447
|
+
const rctx = revocationContext(options);
|
|
357
448
|
const resolveResult = await resolveTag(bases, request, platform, { ...baseReqOptions, maxBytes: 0 });
|
|
358
449
|
const manifestHex = hexOfDigest(resolveResult.manifest.digest);
|
|
359
450
|
const finalDir = unpackedDir(root, manifestHex);
|
|
@@ -362,7 +453,16 @@ async function ensureIn(request, options) {
|
|
|
362
453
|
let recordDir = finalDir;
|
|
363
454
|
let warnings = [];
|
|
364
455
|
let alreadyInstalled;
|
|
456
|
+
// The build is answered from the cache with no download, so it gets the
|
|
457
|
+
// installed-build revocation check (guide, "Build-level revocation").
|
|
458
|
+
let usedInstalled = false;
|
|
459
|
+
// The revocation an override let this call install past, stored beside it.
|
|
460
|
+
let pending;
|
|
365
461
|
if (existing !== undefined) {
|
|
462
|
+
// An install of another fingerprint's build, reached through the tag
|
|
463
|
+
// because the registry has none at this SDK's own, is never reported
|
|
464
|
+
// installed (public issue #578): refused before the cache is touched.
|
|
465
|
+
await aheadOfRegistry(resolveResult.aliasAbsent, existing.predicate, () => cachedOwnBuild(options, platform, request));
|
|
366
466
|
record = existing;
|
|
367
467
|
alreadyInstalled = true;
|
|
368
468
|
// A NEWER build within the request, installed beside it, is still the
|
|
@@ -377,29 +477,21 @@ async function ensureIn(request, options) {
|
|
|
377
477
|
recordDir = monotonic.dir;
|
|
378
478
|
warnings = [...warnings, monotonic.warning];
|
|
379
479
|
}
|
|
480
|
+
usedInstalled = true;
|
|
380
481
|
}
|
|
381
482
|
else {
|
|
382
|
-
const
|
|
383
|
-
const trust
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
: `chtypes: no referrer of ${resolveResult.manifest.digest} verified under the staging key ${activeChannel().keys.map((k) => k.keyid).join(', ')}`);
|
|
395
|
-
}
|
|
396
|
-
predicate = await unsignedPredicateFallback(resolveResult.repositoryRoot, resolveResult.manifest, request, info, baseReqOptions);
|
|
397
|
-
signedBy = '';
|
|
398
|
-
warnings = [...warnings, 'chtypes: proceeding with an unsigned artifact (CHTYPES_ALLOW_UNSIGNED)'];
|
|
399
|
-
}
|
|
400
|
-
else {
|
|
401
|
-
predicate = trust.statement.predicate;
|
|
402
|
-
signedBy = trust.signedBy;
|
|
483
|
+
const verified = await verifyManifestTrust(resolveResult.repositoryRoot, resolveResult.manifest, resolveResult.aliasAbsent, platform, request, options, baseReqOptions);
|
|
484
|
+
const { trust, predicate, signedBy } = verified;
|
|
485
|
+
warnings = [...warnings, ...verified.warnings];
|
|
486
|
+
// A revocation of the candidate, from the trust listing just read (no extra
|
|
487
|
+
// listing request), refuses it before the monotonic rule and before its layer
|
|
488
|
+
// is requested: a revoked build is never downloaded. With the override, it is
|
|
489
|
+
// installed, and its revocation stored beside it.
|
|
490
|
+
if (revocationOn() && trust !== undefined) {
|
|
491
|
+
const sub = subjectOfStatement(trust.statement, resolveResult.manifest.digest, resolveResult.manifest.layer.digest, platform, request);
|
|
492
|
+
const out = await checkCandidates(rctx, [resolveResult.repositoryRoot], verified.listing.revocations, sub, false);
|
|
493
|
+
await settle(rctx, sub, out, false, warnings);
|
|
494
|
+
pending = out.verified;
|
|
403
495
|
}
|
|
404
496
|
const monotonic = await checkMonotonic(root, platform, request, predicate);
|
|
405
497
|
if (monotonic !== undefined) {
|
|
@@ -412,6 +504,7 @@ async function ensureIn(request, options) {
|
|
|
412
504
|
recordDir = monotonic.dir;
|
|
413
505
|
alreadyInstalled = true;
|
|
414
506
|
warnings = [...warnings, monotonic.warning];
|
|
507
|
+
usedInstalled = true;
|
|
415
508
|
}
|
|
416
509
|
else {
|
|
417
510
|
const staging = await freshStagingDir(root);
|
|
@@ -453,19 +546,60 @@ async function ensureIn(request, options) {
|
|
|
453
546
|
const manifestDescriptor = { mediaType: MEDIA_TYPE_MANIFEST, digest: resolveResult.manifest.digest };
|
|
454
547
|
await upsertIndexEntry(root, { ...manifestDescriptor, size: resolveResult.manifest.bytes.length, annotations: { [`${CACHE_ANNOTATION_PREFIX}request`]: request } }, options.beforeIndexRename);
|
|
455
548
|
alreadyInstalled = lostRace;
|
|
549
|
+
await storePending(rctx, finalDir, pending);
|
|
456
550
|
}
|
|
457
551
|
}
|
|
552
|
+
if (usedInstalled && revocationOn()) {
|
|
553
|
+
const used = resolvedFromRecord(record, platform, request, recordDir, resolveResult.repositoryRoot, true, []);
|
|
554
|
+
await checkInstalledOnce(rctx, used, warnings);
|
|
555
|
+
}
|
|
458
556
|
if (options.lockWrite === true) {
|
|
459
|
-
|
|
557
|
+
warnings = [
|
|
558
|
+
...warnings,
|
|
559
|
+
...(await writeLockEntry(request, platform, record, options, resolveResult.repositoryRoot, resolveResult.indexBytes, baseReqOptions, rctx)),
|
|
560
|
+
];
|
|
460
561
|
}
|
|
461
562
|
if (options.update === true) {
|
|
462
563
|
// `update` re-resolves every locked request and rewrites the lock from
|
|
463
564
|
// scratch (guide §6: it never merges a stale entry with a fresh one), in
|
|
464
565
|
// addition to resolving the request above.
|
|
465
|
-
await updateLock(options, bases, baseReqOptions, trustedKeys);
|
|
566
|
+
warnings = [...warnings, ...(await updateLock(options, bases, baseReqOptions, trustedKeys, rctx))];
|
|
466
567
|
}
|
|
467
568
|
return resolvedFromRecord(record, platform, request, recordDir, resolveResult.repositoryRoot, alreadyInstalled, warnings);
|
|
468
569
|
}
|
|
570
|
+
/**
|
|
571
|
+
* The trust step of a fetch for one platform manifest (guide §4), exactly as
|
|
572
|
+
* `ensure` takes it before anything is downloaded: a signature referrer of the
|
|
573
|
+
* manifest verified under the trust list, its statement checked against the
|
|
574
|
+
* manifest's layer, the platform and `request`; no signature that verifies is
|
|
575
|
+
* `ArtifactUntrustedError` unless allow-unsigned is honored, when the config
|
|
576
|
+
* blob's metadata stands in with a warning. A signed build is then checked by
|
|
577
|
+
* the dev channel's ahead-of-registry rule (public issue #578: the alias for
|
|
578
|
+
* its own fingerprint answered 404 on every base, and the tag's build is
|
|
579
|
+
* signed for another), refused from the signed statement, before any layer is
|
|
580
|
+
* requested. `chtypes resolve` takes the same step for every platform.
|
|
581
|
+
*/
|
|
582
|
+
export async function verifyManifestTrust(repositoryRoot, manifest, aliasAbsent, platform, request, options, baseReqOptions) {
|
|
583
|
+
const info = platformInfo(platform);
|
|
584
|
+
const listing = await readListing([repositoryRoot], manifest.digest, { ...baseReqOptions, maxBytes: 0 });
|
|
585
|
+
const trust = await verifyAnyReferrerBundle(repositoryRoot, manifest.digest, MEDIA_TYPE_BUNDLE, defaultTrustedKeys(options), (statement) => checkArtifactStatement(statement, PREDICATE_TYPE_ARTIFACT, manifest.layer.digest, {
|
|
586
|
+
os: info.os,
|
|
587
|
+
arch: info.architecture,
|
|
588
|
+
requestedSpelling: request,
|
|
589
|
+
}), { ...baseReqOptions, maxBytes: 0 }, listing.signatures);
|
|
590
|
+
if (trust === undefined) {
|
|
591
|
+
if (!allowUnsigned(options)) {
|
|
592
|
+
throw new ArtifactUntrustedError(activeChannel().overridable
|
|
593
|
+
? `chtypes: no referrer of ${manifest.digest} verified under a trusted key (CHTYPES_ALLOW_UNSIGNED=1 to proceed anyway)`
|
|
594
|
+
: `chtypes: no referrer of ${manifest.digest} verified under the staging key ${activeChannel().keys.map((k) => k.keyid).join(', ')}`);
|
|
595
|
+
}
|
|
596
|
+
const predicate = await unsignedPredicateFallback(repositoryRoot, manifest, request, info, baseReqOptions);
|
|
597
|
+
return { trust, predicate, signedBy: '', warnings: ['chtypes: proceeding with an unsigned artifact (CHTYPES_ALLOW_UNSIGNED)'], listing };
|
|
598
|
+
}
|
|
599
|
+
const predicate = trust.statement.predicate;
|
|
600
|
+
await aheadOfRegistry(aliasAbsent, predicate, () => cachedOwnBuild(options, platform, request));
|
|
601
|
+
return { trust, predicate, signedBy: trust.signedBy, warnings: [], listing };
|
|
602
|
+
}
|
|
469
603
|
/**
|
|
470
604
|
* `CHTYPES_ALLOW_UNSIGNED`'s fallback metadata source: the tarball's own
|
|
471
605
|
* `manifest.json`, which is also the OCI **config** blob and (per
|
|
@@ -510,12 +644,18 @@ async function unsignedPredicateFallback(repositoryRoot, manifest, request, info
|
|
|
510
644
|
library_bytes: Number(asString(field(json, 'library_bytes'))) || 0,
|
|
511
645
|
};
|
|
512
646
|
}
|
|
513
|
-
catch {
|
|
647
|
+
catch (err) {
|
|
648
|
+
// A 410 (guide §2) is the answer, never a fallback.
|
|
649
|
+
if (isGone(err))
|
|
650
|
+
throw err;
|
|
514
651
|
return fallback;
|
|
515
652
|
}
|
|
516
653
|
}
|
|
517
|
-
|
|
518
|
-
|
|
654
|
+
/**
|
|
655
|
+
* The lock's pin for `request`/`platform`: no lock file, or a lock with no
|
|
656
|
+
* entry for them, is `CHTYPES_ARTIFACT_PINNED`.
|
|
657
|
+
*/
|
|
658
|
+
async function lockedPin(request, platform, options) {
|
|
519
659
|
const lockPath = options.lockPath ?? path.join(process.cwd(), 'chtypes.lock');
|
|
520
660
|
const lock = await readLock(lockPath);
|
|
521
661
|
if (lock === undefined) {
|
|
@@ -525,6 +665,60 @@ async function ensureFrozen(request, platform, options) {
|
|
|
525
665
|
if (pin === undefined) {
|
|
526
666
|
throw new ArtifactPinnedError(`chtypes: ${lockPath} names no entry for ${request}/${platform}; re-lock with \`fetch --lock\``);
|
|
527
667
|
}
|
|
668
|
+
return pin;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* The installed build a lock pin names, with no network: an unpacked
|
|
672
|
+
* directory named by the pinned manifest digest, in the cache or a system
|
|
673
|
+
* directory, whose `verified.json` is for this platform and records exactly
|
|
674
|
+
* the pinned manifest, layer and bundle digests (public issues #414, #487).
|
|
675
|
+
* `undefined` when no installed build matches.
|
|
676
|
+
*/
|
|
677
|
+
async function installedByPin(request, platform, options, pin) {
|
|
678
|
+
return (await lookupByPin(request, platform, options, pin)).hit;
|
|
679
|
+
}
|
|
680
|
+
/** `installedByPin`, also saying whether a record exists under the pinned manifest but disagrees with the pin. */
|
|
681
|
+
async function lookupByPin(request, platform, options, pin) {
|
|
682
|
+
let mismatch = false;
|
|
683
|
+
const warnings = await probeRoots(searchRoots(options).map((r) => r.root), strictMode(options.strictCache));
|
|
684
|
+
for (const r of searchRoots(options)) {
|
|
685
|
+
const dir = unpackedDir(r.root, hexOfDigest(pin.manifest));
|
|
686
|
+
const record = await readVerifiedRecord(dir);
|
|
687
|
+
if (record === undefined)
|
|
688
|
+
continue;
|
|
689
|
+
if (record.platform === platform &&
|
|
690
|
+
record.manifestDigest === pin.manifest &&
|
|
691
|
+
record.layerDigest === pin.layer &&
|
|
692
|
+
record.bundleDigest === pin.bundle) {
|
|
693
|
+
return { hit: resolvedFromRecord(record, platform, request, dir, r.source, true, warnings), mismatch: false };
|
|
694
|
+
}
|
|
695
|
+
mismatch = true;
|
|
696
|
+
}
|
|
697
|
+
return { hit: undefined, mismatch };
|
|
698
|
+
}
|
|
699
|
+
/** `--frozen --offline`: verify the installed build against the lock, with zero network. No lock entry, or an installed record that does not match the pin, is `CHTYPES_ARTIFACT_PINNED`; a pinned build that is not installed is `CHTYPES_ARTIFACT_MISSING`. */
|
|
700
|
+
async function resolveFrozenOffline(request, platform, options) {
|
|
701
|
+
const pin = await lockedPin(request, platform, options);
|
|
702
|
+
const { hit, mismatch } = await lookupByPin(request, platform, options, pin);
|
|
703
|
+
if (hit === undefined) {
|
|
704
|
+
if (mismatch) {
|
|
705
|
+
throw new ArtifactPinnedError(`chtypes: --frozen --offline: the installed build for ${request} (${platform}) does not match the lock's pin ${pin.manifest}`);
|
|
706
|
+
}
|
|
707
|
+
throw new ArtifactMissingError(withNotes(`chtypes: --frozen --offline: the build the lock pins (${pin.manifest}) is not installed for ${request} (${platform})`, await missingNotes(options)));
|
|
708
|
+
}
|
|
709
|
+
return hit;
|
|
710
|
+
}
|
|
711
|
+
async function ensureFrozen(request, platform, options) {
|
|
712
|
+
const root = cacheRoot(options.cacheDir);
|
|
713
|
+
const pin = await lockedPin(request, platform, options);
|
|
714
|
+
// A build already installed under exactly the pinned digests is used as it
|
|
715
|
+
// is: zero requests (guide §6; public issues #414 and #487).
|
|
716
|
+
const installed = await installedByPin(request, platform, options, pin);
|
|
717
|
+
const rctx = revocationContext(options);
|
|
718
|
+
// Under generation 2, the one listing a warm --frozen fetch makes (guide §6,
|
|
719
|
+
// "Build-level revocation").
|
|
720
|
+
if (installed !== undefined)
|
|
721
|
+
return withWarnings(installed, await checkFrozenInstalled(rctx, installed));
|
|
528
722
|
const bases = resolveBases(options);
|
|
529
723
|
const baseReqOptions = requestOptionsFor(options, bases);
|
|
530
724
|
const trustedKeys = defaultTrustedKeys(options);
|
|
@@ -540,13 +734,17 @@ async function ensureFrozen(request, platform, options) {
|
|
|
540
734
|
const finalDir = unpackedDir(root, manifestHex);
|
|
541
735
|
const existing = await readVerifiedRecord(finalDir);
|
|
542
736
|
if (existing !== undefined) {
|
|
543
|
-
|
|
737
|
+
const usedFrozen = resolvedFromRecord(existing, platform, request, finalDir, 'cache', true, []);
|
|
738
|
+
return withWarnings(usedFrozen, await checkFrozenInstalled(rctx, usedFrozen));
|
|
544
739
|
}
|
|
740
|
+
// The bundle, then (generation 2) the one revocation listing, and only then the
|
|
741
|
+
// layer, so a revoked pin is never downloaded.
|
|
545
742
|
const bundleBytes = await fetchBlobBytesByDigest(bases, { mediaType: MEDIA_TYPE_BUNDLE, digest: pin.bundle }, { ...baseReqOptions, maxBytes: 0 });
|
|
546
743
|
const verified = verifyBundleSignature(bundleBytes, trustedKeys);
|
|
547
744
|
let predicate;
|
|
548
745
|
let signedBy;
|
|
549
746
|
let warnings = [];
|
|
747
|
+
let pending;
|
|
550
748
|
if (verified === undefined) {
|
|
551
749
|
if (!allowUnsigned(options)) {
|
|
552
750
|
throw new ArtifactUntrustedError(`chtypes: the locked bundle ${pin.bundle} does not verify under a trusted key`);
|
|
@@ -556,12 +754,19 @@ async function ensureFrozen(request, platform, options) {
|
|
|
556
754
|
warnings = ['chtypes: proceeding with an unsigned artifact (CHTYPES_ALLOW_UNSIGNED)'];
|
|
557
755
|
}
|
|
558
756
|
else {
|
|
559
|
-
|
|
757
|
+
const statement = parseStatement(verified.payload);
|
|
758
|
+
predicate = checkArtifactStatement(statement, PREDICATE_TYPE_ARTIFACT, manifest.layer.digest, {
|
|
560
759
|
os: info.os,
|
|
561
760
|
arch: info.architecture,
|
|
562
761
|
requestedSpelling: request,
|
|
563
762
|
});
|
|
564
763
|
signedBy = verified.signedBy;
|
|
764
|
+
if (revocationOn()) {
|
|
765
|
+
const sub = subjectOfStatement(statement, pin.manifest, manifest.layer.digest, platform, request);
|
|
766
|
+
const out = await checkListing(rctx, sub, false);
|
|
767
|
+
await settle(rctx, sub, out, false, warnings);
|
|
768
|
+
pending = out.verified;
|
|
769
|
+
}
|
|
565
770
|
}
|
|
566
771
|
const staging = await freshStagingDir(root);
|
|
567
772
|
const tempLayerPath = path.join(root, `.tmp-layer-${process.pid}-${randomBytes(6).toString('hex')}`);
|
|
@@ -586,6 +791,7 @@ async function ensureFrozen(request, platform, options) {
|
|
|
586
791
|
});
|
|
587
792
|
await writeVerifiedRecord(staging, record);
|
|
588
793
|
const lostRace = await commitStaging(staging, finalDir);
|
|
794
|
+
await storePending(rctx, finalDir, pending);
|
|
589
795
|
return resolvedFromRecord(record, platform, request, finalDir, bases[0], lostRace, warnings);
|
|
590
796
|
}
|
|
591
797
|
catch (err) {
|
|
@@ -602,24 +808,32 @@ async function ensureFrozen(request, platform, options) {
|
|
|
602
808
|
* alone — never a merge with a stale entry (guide §6). A request with no
|
|
603
809
|
* platform that verifies is dropped rather than carried over stale.
|
|
604
810
|
*/
|
|
605
|
-
async function updateLock(options, bases, baseReqOptions, trustedKeys) {
|
|
811
|
+
async function updateLock(options, bases, baseReqOptions, trustedKeys, rctx) {
|
|
606
812
|
const lockPath = options.lockPath ?? path.join(process.cwd(), 'chtypes.lock');
|
|
607
813
|
const existing = await readLock(lockPath);
|
|
608
814
|
if (existing === undefined)
|
|
609
815
|
throw new ArtifactPinnedError(`chtypes: update with no lock file at ${lockPath}`);
|
|
610
816
|
let fresh = emptyLock();
|
|
817
|
+
const warnings = [];
|
|
611
818
|
for (const [spelling, byPlatform] of Object.entries(existing.requests)) {
|
|
612
819
|
for (const key of Object.keys(byPlatform)) {
|
|
613
820
|
const platformKey = key;
|
|
614
821
|
const info = platformInfo(platformKey);
|
|
615
822
|
const resolved = await resolveTag(bases, spelling, platformKey, { ...baseReqOptions, maxBytes: 0 });
|
|
823
|
+
const listing = await readListing([resolved.repositoryRoot], resolved.manifest.digest, { ...baseReqOptions, maxBytes: 0 });
|
|
616
824
|
const trust = await verifyAnyReferrerBundle(resolved.repositoryRoot, resolved.manifest.digest, MEDIA_TYPE_BUNDLE, trustedKeys, (statement) => checkArtifactStatement(statement, PREDICATE_TYPE_ARTIFACT, resolved.manifest.layer.digest, {
|
|
617
825
|
os: info.os,
|
|
618
826
|
arch: info.architecture,
|
|
619
827
|
requestedSpelling: spelling,
|
|
620
|
-
}), { ...baseReqOptions, maxBytes: 0 });
|
|
828
|
+
}), { ...baseReqOptions, maxBytes: 0 }, listing.signatures);
|
|
621
829
|
if (trust === undefined)
|
|
622
830
|
continue;
|
|
831
|
+
// A revoked build on any platform the update would pin refuses it.
|
|
832
|
+
if (revocationOn()) {
|
|
833
|
+
const sub = subjectOfStatement(trust.statement, resolved.manifest.digest, resolved.manifest.layer.digest, platformKey, spelling);
|
|
834
|
+
const out = await checkCandidates(rctx, [resolved.repositoryRoot], listing.revocations, sub, false);
|
|
835
|
+
await settle(rctx, sub, out, false, warnings);
|
|
836
|
+
}
|
|
623
837
|
const predicate = trust.statement.predicate;
|
|
624
838
|
fresh = withPin(fresh, spelling, platformKey, {
|
|
625
839
|
version: predicate.clickhouse_version,
|
|
@@ -631,8 +845,10 @@ async function updateLock(options, bases, baseReqOptions, trustedKeys) {
|
|
|
631
845
|
}
|
|
632
846
|
}
|
|
633
847
|
await writeLock(lockPath, fresh);
|
|
848
|
+
return warnings;
|
|
634
849
|
}
|
|
635
|
-
async function writeLockEntry(request, platform, record, options, repositoryRoot, indexBytes, baseReqOptions) {
|
|
850
|
+
async function writeLockEntry(request, platform, record, options, repositoryRoot, indexBytes, baseReqOptions, rctx) {
|
|
851
|
+
const warnings = [];
|
|
636
852
|
const lockPath = options.lockPath ?? path.join(process.cwd(), 'chtypes.lock');
|
|
637
853
|
let lock = (await readLock(lockPath)) ?? emptyLock();
|
|
638
854
|
lock = withPin(lock, request, platform, {
|
|
@@ -642,7 +858,7 @@ async function writeLockEntry(request, platform, record, options, repositoryRoot
|
|
|
642
858
|
layer: record.layerDigest,
|
|
643
859
|
bundle: record.bundleDigest ?? '',
|
|
644
860
|
});
|
|
645
|
-
if (options.lockAllPlatforms
|
|
861
|
+
if (options.lockAllPlatforms !== false) {
|
|
646
862
|
const indexJson = parseJsonValue(indexBytes);
|
|
647
863
|
const manifests = items(field(indexJson ?? undefined, 'manifests'));
|
|
648
864
|
const trustedKeys = defaultTrustedKeys(options);
|
|
@@ -656,32 +872,50 @@ async function writeLockEntry(request, platform, record, options, repositoryRoot
|
|
|
656
872
|
const digest = asString(field(m, 'digest'));
|
|
657
873
|
if (digest === '')
|
|
658
874
|
continue;
|
|
875
|
+
let otherManifest;
|
|
876
|
+
let listing;
|
|
877
|
+
let trust;
|
|
659
878
|
try {
|
|
660
|
-
|
|
879
|
+
otherManifest = await fetchManifestByDigest([repositoryRoot], digest, { ...baseReqOptions, maxBytes: 0 });
|
|
661
880
|
const otherInfo = platformInfo(key);
|
|
662
|
-
const
|
|
881
|
+
const layerDigest = otherManifest.layer.digest;
|
|
882
|
+
listing = await readListing([repositoryRoot], otherManifest.digest, { ...baseReqOptions, maxBytes: 0 });
|
|
883
|
+
trust = await verifyAnyReferrerBundle(repositoryRoot, otherManifest.digest, MEDIA_TYPE_BUNDLE, trustedKeys, (statement) => checkArtifactStatement(statement, PREDICATE_TYPE_ARTIFACT, layerDigest, {
|
|
663
884
|
os: otherInfo.os,
|
|
664
885
|
arch: otherInfo.architecture,
|
|
665
886
|
requestedSpelling: request,
|
|
666
|
-
}), { ...baseReqOptions, maxBytes: 0 });
|
|
667
|
-
if (trust === undefined)
|
|
668
|
-
continue;
|
|
669
|
-
const otherPredicate = trust.statement.predicate;
|
|
670
|
-
lock = withPin(lock, request, key, {
|
|
671
|
-
version: otherPredicate.clickhouse_version,
|
|
672
|
-
build: otherPredicate.build,
|
|
673
|
-
manifest: otherManifest.digest,
|
|
674
|
-
layer: otherManifest.layer.digest,
|
|
675
|
-
bundle: trust.bundleDigest,
|
|
676
|
-
});
|
|
887
|
+
}), { ...baseReqOptions, maxBytes: 0 }, listing.signatures);
|
|
677
888
|
}
|
|
678
|
-
catch {
|
|
889
|
+
catch (err) {
|
|
890
|
+
// A 410 (guide §2) fails the write: it is permanent, never a platform to
|
|
891
|
+
// leave out.
|
|
892
|
+
if (isGone(err))
|
|
893
|
+
throw err;
|
|
679
894
|
// Best-effort: a platform whose bundle cannot be fetched/verified is
|
|
680
895
|
// left out of the lock rather than failing the whole write.
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
if (trust === undefined || otherManifest === undefined || listing === undefined)
|
|
899
|
+
continue;
|
|
900
|
+
// A revoked build on any platform the lock would pin refuses the write,
|
|
901
|
+
// whatever the override says (`revocationContext` drops it for a lock).
|
|
902
|
+
if (revocationOn()) {
|
|
903
|
+
const sub = subjectOfStatement(trust.statement, otherManifest.digest, otherManifest.layer.digest, key, request);
|
|
904
|
+
const out = await checkCandidates(rctx, [repositoryRoot], listing.revocations, sub, false);
|
|
905
|
+
await settle(rctx, sub, out, false, warnings);
|
|
681
906
|
}
|
|
907
|
+
const otherPredicate = trust.statement.predicate;
|
|
908
|
+
lock = withPin(lock, request, key, {
|
|
909
|
+
version: otherPredicate.clickhouse_version,
|
|
910
|
+
build: otherPredicate.build,
|
|
911
|
+
manifest: otherManifest.digest,
|
|
912
|
+
layer: otherManifest.layer.digest,
|
|
913
|
+
bundle: trust.bundleDigest,
|
|
914
|
+
});
|
|
682
915
|
}
|
|
683
916
|
}
|
|
684
917
|
await writeLock(lockPath, lock);
|
|
918
|
+
return warnings;
|
|
685
919
|
}
|
|
686
920
|
function isLikelyPlatformKey(key) {
|
|
687
921
|
return key === 'linux-amd64' || key === 'linux-arm64' || key === 'darwin-arm64';
|
|
@@ -779,7 +1013,8 @@ async function fetchGoldens(repository, subject, root, trustedKeys, allowUnsigne
|
|
|
779
1013
|
blob = await fetchBlobBytesByDigest([repository], manifest.layer, reqOptions);
|
|
780
1014
|
}
|
|
781
1015
|
catch (err) {
|
|
782
|
-
|
|
1016
|
+
// A 410 (guide §2) is the answer, never a candidate to pass over.
|
|
1017
|
+
if (!(err instanceof FetchV1Error) || isGone(err))
|
|
783
1018
|
throw err;
|
|
784
1019
|
note(err);
|
|
785
1020
|
continue;
|
|
@@ -795,7 +1030,9 @@ async function fetchGoldens(repository, subject, root, trustedKeys, allowUnsigne
|
|
|
795
1030
|
bundleDigest = bundleManifest.layer.digest;
|
|
796
1031
|
bundleBytes = await fetchBlobBytesByDigest([repository], bundleManifest.layer, reqOptions);
|
|
797
1032
|
}
|
|
798
|
-
catch {
|
|
1033
|
+
catch (err) {
|
|
1034
|
+
if (isGone(err))
|
|
1035
|
+
throw err;
|
|
799
1036
|
continue;
|
|
800
1037
|
}
|
|
801
1038
|
const signed = verifyBundleSignature(bundleBytes, trustedKeys);
|
|
@@ -848,6 +1085,8 @@ async function fetchGoldens(repository, subject, root, trustedKeys, allowUnsigne
|
|
|
848
1085
|
return { path: dest, statement: chosen.predicate, digests: { manifest: chosen.manifest, layer: chosen.blob } };
|
|
849
1086
|
}
|
|
850
1087
|
// ---------------------------------------------------------------------- listTags
|
|
1088
|
+
/** The published lines: exactly two numeric parts, no leading zeros (Go's `lineTag`). */
|
|
1089
|
+
const LINE_SPELLING = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/;
|
|
851
1090
|
/** Compares two version spellings component by component, numerically. */
|
|
852
1091
|
function compareSpellings(a, b) {
|
|
853
1092
|
const pa = a.split('.').map(Number);
|
|
@@ -860,18 +1099,18 @@ function compareSpellings(a, b) {
|
|
|
860
1099
|
return 0;
|
|
861
1100
|
}
|
|
862
1101
|
/**
|
|
863
|
-
* The
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
* dropped, the rest in ascending numeric order. A tag names a
|
|
867
|
-
* platform: whether a platform is offered is that tag's index,
|
|
868
|
-
* resolve reads. A next page is followed through the `Link`
|
|
1102
|
+
* The lines a repository publishes: `GET tags/list` from the first base that
|
|
1103
|
+
* has the repository, every tag that is not a two-part line (three- and
|
|
1104
|
+
* four-part versions, aliases, the `sha256-<hex>` referrers fallback tags,
|
|
1105
|
+
* symbolic tags) dropped, the rest in ascending numeric order. A tag names a
|
|
1106
|
+
* line, not a platform: whether a platform is offered is that tag's index,
|
|
1107
|
+
* which only a resolve reads. A next page is followed through the `Link`
|
|
1108
|
+
* header.
|
|
869
1109
|
*/
|
|
870
1110
|
export async function listTags(options = {}) {
|
|
871
1111
|
noteIgnoredOverrides(options);
|
|
872
1112
|
const bases = resolveBases(options);
|
|
873
1113
|
const reqOptions = { ...requestOptionsFor(options, bases), maxBytes: TAGS_LIST_MAX_BYTES };
|
|
874
|
-
const spelling = new RegExp(SPELLING_REGEX);
|
|
875
1114
|
let lastUnreachable;
|
|
876
1115
|
for (let i = 0; i < bases.length; i++) {
|
|
877
1116
|
const base = bases[i];
|
|
@@ -904,7 +1143,7 @@ export async function listTags(options = {}) {
|
|
|
904
1143
|
found = true;
|
|
905
1144
|
for (const t of items(field(json, 'tags'))) {
|
|
906
1145
|
const tag = asString(t);
|
|
907
|
-
if (
|
|
1146
|
+
if (LINE_SPELLING.test(tag))
|
|
908
1147
|
tags.add(tag);
|
|
909
1148
|
}
|
|
910
1149
|
const link = res.headers['link'];
|