@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.
Files changed (195) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +34 -38
  3. package/dist/abi1/buildinfo.d.ts +40 -0
  4. package/dist/abi1/buildinfo.d.ts.map +1 -0
  5. package/dist/abi1/buildinfo.js +83 -0
  6. package/dist/abi1/buildinfo.js.map +1 -0
  7. package/dist/abi1/calls.gen.d.ts +55 -0
  8. package/dist/abi1/calls.gen.d.ts.map +1 -0
  9. package/dist/abi1/calls.gen.js +110 -0
  10. package/dist/abi1/calls.gen.js.map +1 -0
  11. package/dist/abi1/decls.gen.d.ts +115 -0
  12. package/dist/abi1/decls.gen.d.ts.map +1 -0
  13. package/dist/abi1/decls.gen.js +1613 -0
  14. package/dist/abi1/decls.gen.js.map +1 -0
  15. package/dist/abi1/errmap.gen.d.ts +18 -0
  16. package/dist/abi1/errmap.gen.d.ts.map +1 -0
  17. package/dist/abi1/errmap.gen.js +66 -0
  18. package/dist/abi1/errmap.gen.js.map +1 -0
  19. package/dist/abi1/errors.d.ts +107 -0
  20. package/dist/abi1/errors.d.ts.map +1 -0
  21. package/dist/abi1/errors.js +134 -0
  22. package/dist/abi1/errors.js.map +1 -0
  23. package/dist/abi1/handles.d.ts +31 -0
  24. package/dist/abi1/handles.d.ts.map +1 -0
  25. package/dist/abi1/handles.js +76 -0
  26. package/dist/abi1/handles.js.map +1 -0
  27. package/dist/abi1/index.d.ts +17 -0
  28. package/dist/abi1/index.d.ts.map +1 -0
  29. package/dist/abi1/index.js +17 -0
  30. package/dist/abi1/index.js.map +1 -0
  31. package/dist/abi1/libc.d.ts +54 -0
  32. package/dist/abi1/libc.d.ts.map +1 -0
  33. package/dist/abi1/libc.gen.d.ts +12 -0
  34. package/dist/abi1/libc.gen.d.ts.map +1 -0
  35. package/dist/abi1/libc.gen.js +75 -0
  36. package/dist/abi1/libc.gen.js.map +1 -0
  37. package/dist/abi1/libc.js +109 -0
  38. package/dist/abi1/libc.js.map +1 -0
  39. package/dist/abi1/loader.d.ts +99 -0
  40. package/dist/abi1/loader.d.ts.map +1 -0
  41. package/dist/abi1/loader.js +261 -0
  42. package/dist/abi1/loader.js.map +1 -0
  43. package/dist/abi1/raw.d.ts +104 -0
  44. package/dist/abi1/raw.d.ts.map +1 -0
  45. package/dist/abi1/raw.js +296 -0
  46. package/dist/abi1/raw.js.map +1 -0
  47. package/dist/abi1/strictjson.d.ts +19 -0
  48. package/dist/abi1/strictjson.d.ts.map +1 -0
  49. package/dist/abi1/strictjson.js +78 -0
  50. package/dist/abi1/strictjson.js.map +1 -0
  51. package/dist/abi1/vocab.gen.d.ts +169 -0
  52. package/dist/abi1/vocab.gen.d.ts.map +1 -0
  53. package/dist/abi1/vocab.gen.js +306 -0
  54. package/dist/abi1/vocab.gen.js.map +1 -0
  55. package/dist/cli.d.ts +25 -26
  56. package/dist/cli.d.ts.map +1 -1
  57. package/dist/cli.js +206 -218
  58. package/dist/cli.js.map +1 -1
  59. package/dist/documents.d.ts +195 -0
  60. package/dist/documents.d.ts.map +1 -0
  61. package/dist/documents.js +389 -0
  62. package/dist/documents.js.map +1 -0
  63. package/dist/env.d.ts +16 -0
  64. package/dist/env.d.ts.map +1 -0
  65. package/dist/env.js +57 -0
  66. package/dist/env.js.map +1 -0
  67. package/dist/index.d.ts +19 -24
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +17 -23
  70. package/dist/index.js.map +1 -1
  71. package/dist/json.d.ts +11 -89
  72. package/dist/json.d.ts.map +1 -1
  73. package/dist/json.js +16 -180
  74. package/dist/json.js.map +1 -1
  75. package/dist/library.d.ts +53 -309
  76. package/dist/library.d.ts.map +1 -1
  77. package/dist/library.js +77 -315
  78. package/dist/library.js.map +1 -1
  79. package/dist/ocifetch/constants.gen.d.ts +84 -0
  80. package/dist/ocifetch/constants.gen.d.ts.map +1 -0
  81. package/dist/ocifetch/constants.gen.js +92 -0
  82. package/dist/ocifetch/constants.gen.js.map +1 -0
  83. package/dist/ocifetch/dsse.d.ts +114 -0
  84. package/dist/ocifetch/dsse.d.ts.map +1 -0
  85. package/dist/ocifetch/dsse.js +331 -0
  86. package/dist/ocifetch/dsse.js.map +1 -0
  87. package/dist/ocifetch/ensure.d.ts +65 -0
  88. package/dist/ocifetch/ensure.d.ts.map +1 -0
  89. package/dist/ocifetch/ensure.js +775 -0
  90. package/dist/ocifetch/ensure.js.map +1 -0
  91. package/dist/ocifetch/errors.d.ts +92 -0
  92. package/dist/ocifetch/errors.d.ts.map +1 -0
  93. package/dist/ocifetch/errors.js +101 -0
  94. package/dist/ocifetch/errors.js.map +1 -0
  95. package/dist/ocifetch/goldens.d.ts +44 -0
  96. package/dist/ocifetch/goldens.d.ts.map +1 -0
  97. package/dist/ocifetch/goldens.js +62 -0
  98. package/dist/ocifetch/goldens.js.map +1 -0
  99. package/dist/ocifetch/http.d.ts +124 -0
  100. package/dist/ocifetch/http.d.ts.map +1 -0
  101. package/dist/ocifetch/http.js +549 -0
  102. package/dist/ocifetch/http.js.map +1 -0
  103. package/dist/ocifetch/index.d.ts +17 -0
  104. package/dist/ocifetch/index.d.ts.map +1 -0
  105. package/dist/ocifetch/index.js +14 -0
  106. package/dist/ocifetch/index.js.map +1 -0
  107. package/dist/ocifetch/layout.d.ts +126 -0
  108. package/dist/ocifetch/layout.d.ts.map +1 -0
  109. package/dist/ocifetch/layout.js +375 -0
  110. package/dist/ocifetch/layout.js.map +1 -0
  111. package/dist/ocifetch/localverify.d.ts +43 -0
  112. package/dist/ocifetch/localverify.d.ts.map +1 -0
  113. package/dist/ocifetch/localverify.js +164 -0
  114. package/dist/ocifetch/localverify.js.map +1 -0
  115. package/dist/ocifetch/lock.d.ts +39 -0
  116. package/dist/ocifetch/lock.d.ts.map +1 -0
  117. package/dist/ocifetch/lock.js +140 -0
  118. package/dist/ocifetch/lock.js.map +1 -0
  119. package/dist/ocifetch/oci.d.ts +100 -0
  120. package/dist/ocifetch/oci.d.ts.map +1 -0
  121. package/dist/ocifetch/oci.js +323 -0
  122. package/dist/ocifetch/oci.js.map +1 -0
  123. package/dist/ocifetch/referrers.d.ts +55 -0
  124. package/dist/ocifetch/referrers.d.ts.map +1 -0
  125. package/dist/ocifetch/referrers.js +132 -0
  126. package/dist/ocifetch/referrers.js.map +1 -0
  127. package/dist/ocifetch/types.d.ts +167 -0
  128. package/dist/ocifetch/types.d.ts.map +1 -0
  129. package/dist/ocifetch/types.js +88 -0
  130. package/dist/ocifetch/types.js.map +1 -0
  131. package/dist/ocifetch/unpack.d.ts +55 -0
  132. package/dist/ocifetch/unpack.d.ts.map +1 -0
  133. package/dist/ocifetch/unpack.js +205 -0
  134. package/dist/ocifetch/unpack.js.map +1 -0
  135. package/dist/registry.d.ts +38 -387
  136. package/dist/registry.d.ts.map +1 -1
  137. package/dist/registry.js +94 -819
  138. package/dist/registry.js.map +1 -1
  139. package/dist/schema.d.ts +77 -588
  140. package/dist/schema.d.ts.map +1 -1
  141. package/dist/schema.js +87 -549
  142. package/dist/schema.js.map +1 -1
  143. package/dist/settings.d.ts +27 -36
  144. package/dist/settings.d.ts.map +1 -1
  145. package/dist/settings.js +75 -25
  146. package/dist/settings.js.map +1 -1
  147. package/dist/setup.d.ts +37 -0
  148. package/dist/setup.d.ts.map +1 -0
  149. package/dist/setup.js +60 -0
  150. package/dist/setup.js.map +1 -0
  151. package/dist/tar.d.ts +27 -11
  152. package/dist/tar.d.ts.map +1 -1
  153. package/dist/tar.js +54 -32
  154. package/dist/tar.js.map +1 -1
  155. package/package.json +2 -2
  156. package/dist/discover.d.ts +0 -142
  157. package/dist/discover.d.ts.map +0 -1
  158. package/dist/discover.js +0 -288
  159. package/dist/discover.js.map +0 -1
  160. package/dist/error-codes.d.ts +0 -80
  161. package/dist/error-codes.d.ts.map +0 -1
  162. package/dist/error-codes.js +0 -139
  163. package/dist/error-codes.js.map +0 -1
  164. package/dist/errors.d.ts +0 -284
  165. package/dist/errors.d.ts.map +0 -1
  166. package/dist/errors.js +0 -321
  167. package/dist/errors.js.map +0 -1
  168. package/dist/fetch.d.ts +0 -382
  169. package/dist/fetch.d.ts.map +0 -1
  170. package/dist/fetch.js +0 -1498
  171. package/dist/fetch.js.map +0 -1
  172. package/dist/ffi.d.ts +0 -410
  173. package/dist/ffi.d.ts.map +0 -1
  174. package/dist/ffi.js +0 -1154
  175. package/dist/ffi.js.map +0 -1
  176. package/dist/format.d.ts +0 -128
  177. package/dist/format.d.ts.map +0 -1
  178. package/dist/format.js +0 -132
  179. package/dist/format.js.map +0 -1
  180. package/dist/numeric.d.ts +0 -20
  181. package/dist/numeric.d.ts.map +0 -1
  182. package/dist/numeric.js +0 -107
  183. package/dist/numeric.js.map +0 -1
  184. package/dist/paths.d.ts +0 -55
  185. package/dist/paths.d.ts.map +0 -1
  186. package/dist/paths.js +0 -87
  187. package/dist/paths.js.map +0 -1
  188. package/dist/results.d.ts +0 -495
  189. package/dist/results.d.ts.map +0 -1
  190. package/dist/results.js +0 -406
  191. package/dist/results.js.map +0 -1
  192. package/dist/transform.d.ts +0 -73
  193. package/dist/transform.d.ts.map +0 -1
  194. package/dist/transform.js +0 -533
  195. 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