@vltpkg/package-info 1.2.0 → 1.3.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/dist/index.d.ts CHANGED
@@ -67,6 +67,12 @@ export type PackageInfoClientOptions = RegistryClientOptions & SpecOptions & {
67
67
  * tarball, anything else goes through the global store first.
68
68
  */
69
69
  'store-linker'?: StoreLinker;
70
+ /**
71
+ * Resolve to the registry's Brotli (`.tar.br`) tarball when it
72
+ * advertises one in `dist.alternates`. Defaults to true; set false to
73
+ * always resolve to the gzip `.tgz`.
74
+ */
75
+ 'brotli-tarballs'?: boolean;
70
76
  };
71
77
  export type PackageInfoClientRequestOptions = PickManifestOptions & RegistryClientRequestOptions & {
72
78
  /** dir to resolve `file://` specifiers against. Defaults to projectRoot. */
@@ -84,8 +90,8 @@ export type PackageInfoClientExtractOptions = PackageInfoClientRequestOptions &
84
90
  /**
85
91
  * When true, indicates that integrity + resolved came from a
86
92
  * lockfile (i.e. they were already verified on first install).
87
- * Skips the client-side tarball integrity check.
88
- * Defaults to false — fresh installs always verify integrity.
93
+ * Skips re-hashing a refetched body of an already verified url.
94
+ * Other network bodies are always checked.
89
95
  */
90
96
  fromLockfile?: boolean;
91
97
  /**
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { pickManifest } from '@vltpkg/pick-manifest';
8
8
  import { registryErrorMessage, tokenRefusalAdvice, } from '@vltpkg/registry-client/registry-error';
9
9
  import { storeRoot } from '@vltpkg/registry-client/store-root';
10
10
  import { Spec } from '@vltpkg/spec';
11
- import { asPackument, integrityHex } from '@vltpkg/types';
11
+ import { asPackument, brotliTarballUrl, integrityHex, tarballFormat, } from '@vltpkg/types';
12
12
  import ssri from 'ssri';
13
13
  import { Monorepo } from '@vltpkg/workspaces';
14
14
  import { XDG } from '@vltpkg/xdg';
@@ -178,6 +178,18 @@ export class PackageInfoClient {
178
178
  });
179
179
  return this.#tarPoolPromise;
180
180
  }
181
+ /**
182
+ * The absolute URL of a version's Brotli (`.tar.br`) tarball, or
183
+ * undefined when the registry advertised none, advertised one this
184
+ * client does not use (see {@link brotliTarballUrl}), or
185
+ * `--no-brotli-tarballs` is set. `dist.tarball` is already absolute
186
+ * here -- see `absolutizeTarballs`.
187
+ */
188
+ #brotliTarball(tarball, alternates) {
189
+ if (this.options['brotli-tarballs'] === false)
190
+ return undefined;
191
+ return brotliTarballUrl(tarball, alternates);
192
+ }
181
193
  constructor(options = {}) {
182
194
  this.options = options;
183
195
  this.#projectRoot = options.projectRoot || process.cwd();
@@ -210,10 +222,19 @@ export class PackageInfoClient {
210
222
  const { from = this.#projectRoot, integrity, resolved, fromLockfile = false, installScripts = false, } = options;
211
223
  const f = spec.final;
212
224
  // If the caller already provides both integrity and resolved
213
- // (from lockfile or prior resolution), skip re-resolving.
214
- const alreadyResolved = !!(integrity && resolved);
215
- const r = alreadyResolved ?
216
- { resolved, integrity, spec }
225
+ // (from lockfile or prior resolution), skip re-resolving. A
226
+ // `.tar.br` needs only the URL: its hash is not knowable before the
227
+ // download, so the graph can never hand one over, and `Repr-Digest`
228
+ // is what pins it -- `required`, so a registry that serves the
229
+ // alternate unlabelled is rejected rather than trusted.
230
+ const brotli = !!resolved && tarballFormat(resolved) === 'brotli';
231
+ const r = resolved && (integrity || brotli) ?
232
+ {
233
+ resolved,
234
+ integrity,
235
+ spec,
236
+ ...(brotli && !integrity ? { digestRequired: true } : {}),
237
+ }
217
238
  : await this.resolve(spec, options);
218
239
  switch (f.type) {
219
240
  case 'git': {
@@ -244,6 +265,9 @@ export class PackageInfoClient {
244
265
  }
245
266
  case 'registry': {
246
267
  const pool = await this.getTarPool();
268
+ // brotli bytes carry no signature of their own, so every unpack
269
+ // below has to be told; gzip and raw tar are sniffed.
270
+ const format = tarballFormat(r.resolved);
247
271
  // git tarballs keep the unpack path
248
272
  const hex = f.type === 'registry' && this.#storeLinker !== 'unpack' ?
249
273
  integrityHex(r.integrity)
@@ -281,12 +305,14 @@ export class PackageInfoClient {
281
305
  });
282
306
  if (cached) {
283
307
  try {
284
- await pool.unpack(cached.body, target);
308
+ await pool.unpack(cached.body, target, format);
285
309
  logRequest(r.resolved, 'cache');
286
310
  r.integrity ??= cached.integrity;
287
311
  // a warm install writes nothing to the cache, so queue the
288
312
  // store miss here or an existing cache never converges,
289
- // and a gzipped body with no store link, to unzip it
313
+ // and a gzipped body with no store link, to unzip it.
314
+ // (a brotli body is never rewritten: cache-unzip only
315
+ // un-gzips, and leaving it compressed keeps the cache small.)
290
316
  if (hex || cached.gzip) {
291
317
  rc.queueForStore(cached.key, r.integrity);
292
318
  }
@@ -316,24 +342,21 @@ export class PackageInfoClient {
316
342
  response,
317
343
  });
318
344
  }
345
+ // checkIntegrity() hashes the body unless its url is trusted
346
+ const verified = !trustIntegrity &&
347
+ response.checkIntegrity({ spec, url: resolved });
319
348
  // if it's not trusted already, but valid, start trusting
320
- if (!trustIntegrity &&
321
- response.checkIntegrity({ spec, url: resolved })) {
349
+ if (verified) {
322
350
  this.#trustedIntegrities.set(r.resolved, response.integrity);
323
351
  }
324
352
  const buf = response.buffer();
325
353
  if (r.integrity) {
326
- // Verify network-delivered tarball bytes against dist.integrity.
327
- // Skip cache-served bodies: they were verified on the fetch that
328
- // populated the cache, and cache-unzip rewrites them un-gzipped
329
- // so the gzip-hash can never match. Skip lockfile-sourced
330
- // integrity: it was verified on first install.
331
- if (!fromLockfile && !response.fromCache) {
354
+ // hash only network bytes checkIntegrity() skipped (trusted
355
+ // url). cache bodies were verified when cached.
356
+ if (!verified && !fromLockfile && !response.fromCache) {
332
357
  const hash = createHash('sha512');
333
358
  hash.update(buf);
334
359
  const computed = `sha512-${hash.digest('base64')}`;
335
- /* c8 ignore start - defense-in-depth: registry client's
336
- * checkIntegrity() usually catches mismatches first. */
337
360
  if (computed !== r.integrity) {
338
361
  throw error('Tarball integrity check failed', {
339
362
  code: 'EINTEGRITY',
@@ -343,7 +366,6 @@ export class PackageInfoClient {
343
366
  found: computed,
344
367
  });
345
368
  }
346
- /* c8 ignore stop */
347
369
  }
348
370
  }
349
371
  else if (response.fromCache) {
@@ -379,7 +401,7 @@ export class PackageInfoClient {
379
401
  }
380
402
  }
381
403
  try {
382
- await (await this.getTarPool()).unpack(buf, target);
404
+ await pool.unpack(buf, target, format);
383
405
  }
384
406
  catch (er) {
385
407
  throw this.#resolveError(spec, options, 'tar unpack failed', { cause: er });
@@ -568,19 +590,19 @@ export class PackageInfoClient {
568
590
  `version may have been unpublished, or the registry may ` +
569
591
  `be misconfigured or unreachable.`, { response, url: tarball });
570
592
  }
593
+ const verified = !trustIntegrity &&
594
+ response.checkIntegrity({ spec, url: tarball });
571
595
  // if we don't already trust it, but it's valid, start
572
596
  // trusting it
573
- if (!trustIntegrity &&
574
- response.checkIntegrity({ spec, url: tarball })) {
597
+ if (verified) {
575
598
  this.#trustedIntegrities.set(tarball, response.integrity);
576
599
  }
577
600
  const buf = response.buffer();
578
- // Same as extract(): only hash network-delivered bodies.
579
- if (integrity && !response.fromCache) {
601
+ // Same as extract()
602
+ if (integrity && !verified && !response.fromCache) {
580
603
  const hash = createHash('sha512');
581
604
  hash.update(buf);
582
605
  const computed = `sha512-${hash.digest('base64')}`;
583
- /* c8 ignore start - defense-in-depth (see extract) */
584
606
  if (computed !== integrity) {
585
607
  throw error('Tarball integrity check failed', {
586
608
  code: 'EINTEGRITY',
@@ -590,7 +612,6 @@ export class PackageInfoClient {
590
612
  found: computed,
591
613
  });
592
614
  }
593
- /* c8 ignore stop */
594
615
  }
595
616
  return buf;
596
617
  };
@@ -977,15 +998,19 @@ export class PackageInfoClient {
977
998
  case 'registry': {
978
999
  const mani = await this.manifest(spec, options);
979
1000
  if (mani.dist) {
980
- const { integrity, tarball, signatures } = mani.dist;
1001
+ const { integrity, tarball, signatures, alternates } = mani.dist;
981
1002
  if (tarball) {
982
- const r = {
983
- resolved: tarball,
984
- integrity,
985
- signatures,
986
- spec,
987
- };
988
- if (!integrity &&
1003
+ // A `.tar.br` is a distinct artifact, not a re-encoding of the
1004
+ // `.tgz`: it hashes differently, so `dist.integrity` and the
1005
+ // signatures over it describe the wrong bytes and are dropped.
1006
+ // What pins it instead is its own `Repr-Digest`, which the
1007
+ // registry that advertised it always sends -- hence `required`.
1008
+ const brotli = this.#brotliTarball(tarball, alternates);
1009
+ const r = brotli ?
1010
+ { resolved: brotli, spec, digestRequired: true }
1011
+ : { resolved: tarball, integrity, signatures, spec };
1012
+ if (!brotli &&
1013
+ !integrity &&
989
1014
  this.#vltPackuments.has(`${f.registry}${f.name}`)) {
990
1015
  r.digestRequired = true;
991
1016
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vltpkg/package-info",
3
3
  "description": "Resolve and fetch package metadata and tarballs",
4
- "version": "1.2.0",
4
+ "version": "1.3.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vltpkg/vltpkg.git",
@@ -13,17 +13,17 @@
13
13
  "url": "http://vlt.sh"
14
14
  },
15
15
  "dependencies": {
16
- "@vltpkg/error-cause": "1.2.0",
17
- "@vltpkg/git": "1.2.0",
18
- "@vltpkg/output": "1.2.0",
19
- "@vltpkg/package-json": "1.2.0",
20
- "@vltpkg/pick-manifest": "1.2.0",
21
- "@vltpkg/registry-client": "1.2.0",
22
- "@vltpkg/spec": "1.2.0",
23
- "@vltpkg/tar": "1.2.0",
24
- "@vltpkg/types": "1.2.0",
25
- "@vltpkg/workspaces": "1.2.0",
26
- "@vltpkg/xdg": "1.2.0",
16
+ "@vltpkg/error-cause": "1.3.0",
17
+ "@vltpkg/git": "1.3.0",
18
+ "@vltpkg/output": "1.3.0",
19
+ "@vltpkg/package-json": "1.3.0",
20
+ "@vltpkg/pick-manifest": "1.3.0",
21
+ "@vltpkg/registry-client": "1.3.0",
22
+ "@vltpkg/spec": "1.3.0",
23
+ "@vltpkg/tar": "1.3.0",
24
+ "@vltpkg/types": "1.3.0",
25
+ "@vltpkg/workspaces": "1.3.0",
26
+ "@vltpkg/xdg": "1.3.0",
27
27
  "ssri": "^13.0.0",
28
28
  "tar": "^7.5.2"
29
29
  },
@@ -32,8 +32,8 @@
32
32
  "@types/node": "^22.19.2",
33
33
  "@types/pacote": "^11.1.8",
34
34
  "@vltpkg/benchmark": "0.0.0",
35
- "@vltpkg/cache-unzip": "1.2.0",
36
- "@vltpkg/vlt-json": "1.2.0",
35
+ "@vltpkg/cache-unzip": "1.3.0",
36
+ "@vltpkg/vlt-json": "1.3.0",
37
37
  "eslint": "^9.39.1",
38
38
  "pacote": "^21.0.4",
39
39
  "prettier": "^3.7.4",