@vltpkg/package-info 1.0.0-rc.26 → 1.0.0-rc.27

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
@@ -32,6 +32,13 @@ export type PackageInfoClientRequestOptions = PickManifestOptions & RegistryClie
32
32
  export type PackageInfoClientExtractOptions = PackageInfoClientRequestOptions & {
33
33
  integrity?: Integrity;
34
34
  resolved?: string;
35
+ /**
36
+ * When true, indicates that integrity + resolved came from a
37
+ * lockfile (i.e. they were already verified on first install).
38
+ * Skips the client-side tarball integrity check.
39
+ * Defaults to false — fresh installs always verify integrity.
40
+ */
41
+ fromLockfile?: boolean;
35
42
  };
36
43
  export declare class PackageInfoClient {
37
44
  #private;
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { pickManifest } from '@vltpkg/pick-manifest';
5
5
  import { RegistryClient } from '@vltpkg/registry-client';
6
6
  import { Spec } from '@vltpkg/spec';
7
7
  import { Pool } from '@vltpkg/tar';
8
- import { asPackument, isIntegrity } from '@vltpkg/types';
8
+ import { asPackument } from '@vltpkg/types';
9
9
  import ssri from 'ssri';
10
10
  import { Monorepo } from '@vltpkg/workspaces';
11
11
  import { XDG } from '@vltpkg/xdg';
@@ -65,13 +65,12 @@ export class PackageInfoClient {
65
65
  async extract(spec, target, options = {}) {
66
66
  if (typeof spec === 'string')
67
67
  spec = Spec.parse(spec, this.options);
68
- const { from = this.#projectRoot, integrity, resolved } = options;
68
+ const { from = this.#projectRoot, integrity, resolved, fromLockfile = false, } = options;
69
69
  const f = spec.final;
70
- // Track whether integrity/resolved came from the caller (e.g. lockfile)
71
- // vs freshly resolved. We only verify tarball integrity on net-new
72
- // installs (fresh resolution), not when replaying from lockfile.
73
- const fromLockfile = !!(integrity && resolved);
74
- const r = fromLockfile ?
70
+ // If the caller already provides both integrity and resolved
71
+ // (from lockfile or prior resolution), skip re-resolving.
72
+ const alreadyResolved = !!(integrity && resolved);
73
+ const r = alreadyResolved ?
75
74
  { resolved, integrity, spec }
76
75
  : await this.resolve(spec, options);
77
76
  switch (f.type) {
@@ -102,41 +101,70 @@ export class PackageInfoClient {
102
101
  // fallthrough if a remote tarball url present
103
102
  }
104
103
  case 'registry': {
105
- const trustIntegrity = this.#trustedIntegrities.get(r.resolved) === r.integrity;
106
- const response = await this.registryClient.request(r.resolved, {
107
- integrity: r.integrity,
108
- trustIntegrity,
109
- });
110
- if (response.statusCode !== 200) {
111
- throw this.#resolveError(spec, options, 'failed to fetch tarball', {
112
- url: r.resolved,
113
- response,
104
+ const fetchTarball = async (useCache) => {
105
+ const trustIntegrity = this.#trustedIntegrities.get(r.resolved) === r.integrity;
106
+ const response = await this.registryClient.request(r.resolved, {
107
+ integrity: r.integrity,
108
+ trustIntegrity,
109
+ ...(useCache === false ? { useCache } : {}),
114
110
  });
115
- }
116
- // if it's not trusted already, but valid, start trusting
117
- if (!trustIntegrity &&
118
- response.checkIntegrity({ spec, url: resolved })) {
119
- this.#trustedIntegrities.set(r.resolved, response.integrity);
120
- }
121
- const buf = response.buffer();
122
- // Verify tarball integrity against the manifest's dist.integrity.
123
- // This is a supply-chain security measure: the registry may not
124
- // validate integrity, so we do it client-side on every fresh
125
- // download. Skip when integrity came from lockfile/cache (it was
126
- // already verified on first install).
127
- if (r.integrity && !fromLockfile) {
128
- const hash = createHash('sha512');
129
- hash.update(buf);
130
- const computed = `sha512-${hash.digest('base64')}`;
131
- if (computed !== r.integrity) {
132
- throw error('Tarball integrity check failed', {
133
- code: 'EINTEGRITY',
134
- spec,
111
+ if (response.statusCode !== 200) {
112
+ throw this.#resolveError(spec, options, 'failed to fetch tarball', {
135
113
  url: r.resolved,
136
- wanted: r.integrity,
137
- found: computed,
114
+ response,
138
115
  });
139
116
  }
117
+ // if it's not trusted already, but valid, start trusting
118
+ if (!trustIntegrity &&
119
+ response.checkIntegrity({ spec, url: resolved })) {
120
+ this.#trustedIntegrities.set(r.resolved, response.integrity);
121
+ }
122
+ const buf = response.buffer();
123
+ // Verify tarball integrity against the manifest's
124
+ // dist.integrity. This is a supply-chain security measure:
125
+ // the registry may not validate integrity, so we do it
126
+ // client-side on every fresh download. Skip when integrity
127
+ // came from lockfile/cache (it was already verified on
128
+ // first install).
129
+ /* c8 ignore start - defense-in-depth: registry client's
130
+ * checkIntegrity() usually catches mismatches first since
131
+ * it checks the same gzip bytes. This fires only when the
132
+ * registry client check is skipped (trustIntegrity=true). */
133
+ if (r.integrity && !fromLockfile) {
134
+ const hash = createHash('sha512');
135
+ hash.update(buf);
136
+ const computed = `sha512-${hash.digest('base64')}`;
137
+ if (computed !== r.integrity) {
138
+ throw error('Tarball integrity check failed', {
139
+ code: 'EINTEGRITY',
140
+ spec,
141
+ url: r.resolved,
142
+ wanted: r.integrity,
143
+ found: computed,
144
+ });
145
+ }
146
+ }
147
+ /* c8 ignore stop */
148
+ return buf;
149
+ };
150
+ let buf;
151
+ try {
152
+ buf = await fetchTarball();
153
+ }
154
+ catch (er) {
155
+ // On EINTEGRITY, retry once bypassing cache. This handles
156
+ // transient issues such as corrupted downloads or CDN
157
+ // inconsistencies that cause the cached tarball to not
158
+ // match the expected integrity hash.
159
+ if (er instanceof Error &&
160
+ 'cause' in er &&
161
+ er.cause
162
+ ?.code === 'EINTEGRITY') {
163
+ buf = await fetchTarball(false);
164
+ }
165
+ else {
166
+ throw er;
167
+ }
140
168
  }
141
169
  try {
142
170
  await this.tarPool.unpack(buf, target);
@@ -302,37 +330,56 @@ export class PackageInfoClient {
302
330
  if (!tarball) {
303
331
  throw this.#resolveError(spec, options, 'no tarball found in manifest.dist');
304
332
  }
305
- const trustIntegrity = this.#trustedIntegrities.get(tarball) === integrity;
306
- const response = await this.registryClient.request(tarball, {
307
- ...options,
308
- integrity,
309
- trustIntegrity,
310
- });
311
- if (response.statusCode !== 200) {
312
- throw this.#resolveError(spec, options, 'failed to fetch tarball', { response, url: tarball });
313
- }
314
- // if we don't already trust it, but it's valid, start trusting it
315
- if (!trustIntegrity &&
316
- response.checkIntegrity({ spec, url: tarball })) {
317
- this.#trustedIntegrities.set(tarball, response.integrity);
333
+ const fetchTarball = async (useCache) => {
334
+ const trustIntegrity = this.#trustedIntegrities.get(tarball) === integrity;
335
+ const response = await this.registryClient.request(tarball, {
336
+ ...options,
337
+ integrity,
338
+ trustIntegrity,
339
+ ...(useCache === false ? { useCache } : {}),
340
+ });
341
+ if (response.statusCode !== 200) {
342
+ throw this.#resolveError(spec, options, 'failed to fetch tarball', { response, url: tarball });
343
+ }
344
+ // if we don't already trust it, but it's valid, start
345
+ // trusting it
346
+ if (!trustIntegrity &&
347
+ response.checkIntegrity({ spec, url: tarball })) {
348
+ this.#trustedIntegrities.set(tarball, response.integrity);
349
+ }
350
+ const buf = response.buffer();
351
+ // Verify tarball integrity against the manifest's
352
+ // dist.integrity.
353
+ /* c8 ignore start - defense-in-depth (see extract) */
354
+ if (integrity) {
355
+ const hash = createHash('sha512');
356
+ hash.update(buf);
357
+ const computed = `sha512-${hash.digest('base64')}`;
358
+ if (computed !== integrity) {
359
+ throw error('Tarball integrity check failed', {
360
+ code: 'EINTEGRITY',
361
+ spec,
362
+ url: tarball,
363
+ wanted: integrity,
364
+ found: computed,
365
+ });
366
+ }
367
+ }
368
+ /* c8 ignore stop */
369
+ return buf;
370
+ };
371
+ try {
372
+ return await fetchTarball();
318
373
  }
319
- const buf = response.buffer();
320
- // Verify tarball integrity against the manifest's dist.integrity.
321
- if (integrity) {
322
- const hash = createHash('sha512');
323
- hash.update(buf);
324
- const computed = `sha512-${hash.digest('base64')}`;
325
- if (computed !== integrity) {
326
- throw error('Tarball integrity check failed', {
327
- code: 'EINTEGRITY',
328
- spec,
329
- url: tarball,
330
- wanted: integrity,
331
- found: computed,
332
- });
374
+ catch (er) {
375
+ if (er instanceof Error &&
376
+ 'cause' in er &&
377
+ er.cause
378
+ ?.code === 'EINTEGRITY') {
379
+ return await fetchTarball(false);
333
380
  }
381
+ throw er;
334
382
  }
335
- return buf;
336
383
  }
337
384
  case 'git': {
338
385
  const { remoteURL, gitRemote, gitCommittish, gitSelectorParsed, } = f;
@@ -432,15 +479,6 @@ export class PackageInfoClient {
432
479
  : pickManifest(await this.packument(f, options), spec, options);
433
480
  if (!mani)
434
481
  throw this.#resolveError(spec, options);
435
- const { integrity, tarball } = mani.dist ?? /* c8 ignore next */ {};
436
- if (isIntegrity(integrity) && tarball) {
437
- const registryOrigin = new URL(String(f.registry)).origin;
438
- const tgzOrigin = new URL(tarball).origin;
439
- // if it comes from the same origin, trust the integrity
440
- if (tgzOrigin === registryOrigin) {
441
- this.#trustedIntegrities.set(tarball, integrity);
442
- }
443
- }
444
482
  // Cache the manifest data
445
483
  if (cachePath) {
446
484
  const json = JSON.stringify({
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.0.0-rc.26",
4
+ "version": "1.0.0-rc.27",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vltpkg/vltpkg.git",
@@ -13,16 +13,16 @@
13
13
  "url": "http://vlt.sh"
14
14
  },
15
15
  "dependencies": {
16
- "@vltpkg/error-cause": "1.0.0-rc.26",
17
- "@vltpkg/git": "1.0.0-rc.26",
18
- "@vltpkg/package-json": "1.0.0-rc.26",
19
- "@vltpkg/pick-manifest": "1.0.0-rc.26",
20
- "@vltpkg/registry-client": "1.0.0-rc.26",
21
- "@vltpkg/spec": "1.0.0-rc.26",
22
- "@vltpkg/tar": "1.0.0-rc.26",
23
- "@vltpkg/types": "1.0.0-rc.26",
24
- "@vltpkg/workspaces": "1.0.0-rc.26",
25
- "@vltpkg/xdg": "1.0.0-rc.26",
16
+ "@vltpkg/error-cause": "1.0.0-rc.27",
17
+ "@vltpkg/git": "1.0.0-rc.27",
18
+ "@vltpkg/package-json": "1.0.0-rc.27",
19
+ "@vltpkg/pick-manifest": "1.0.0-rc.27",
20
+ "@vltpkg/registry-client": "1.0.0-rc.27",
21
+ "@vltpkg/spec": "1.0.0-rc.27",
22
+ "@vltpkg/tar": "1.0.0-rc.27",
23
+ "@vltpkg/types": "1.0.0-rc.27",
24
+ "@vltpkg/workspaces": "1.0.0-rc.27",
25
+ "@vltpkg/xdg": "1.0.0-rc.27",
26
26
  "ssri": "^13.0.0",
27
27
  "tar": "^7.5.2"
28
28
  },
@@ -31,8 +31,8 @@
31
31
  "@types/node": "^22.19.2",
32
32
  "@types/pacote": "^11.1.8",
33
33
  "@vltpkg/benchmark": "0.0.0",
34
- "@vltpkg/cache-unzip": "1.0.0-rc.26",
35
- "@vltpkg/vlt-json": "1.0.0-rc.26",
34
+ "@vltpkg/cache-unzip": "1.0.0-rc.27",
35
+ "@vltpkg/vlt-json": "1.0.0-rc.27",
36
36
  "eslint": "^9.39.1",
37
37
  "pacote": "^21.0.4",
38
38
  "prettier": "^3.7.4",