@vltpkg/package-info 1.0.0-rc.25 → 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,11 +5,11 @@ 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';
12
- import { randomBytes } from 'node:crypto';
12
+ import { createHash, randomBytes } from 'node:crypto';
13
13
  import { mkdir, readFile, rm, stat, symlink, unlink, writeFile, } from 'node:fs/promises';
14
14
  import { basename, dirname, resolve as pathResolve, relative, } from 'node:path';
15
15
  import { create as tarC } from 'tar';
@@ -65,9 +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
- const r = integrity && resolved ?
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 ?
71
74
  { resolved, integrity, spec }
72
75
  : await this.resolve(spec, options);
73
76
  switch (f.type) {
@@ -98,24 +101,73 @@ export class PackageInfoClient {
98
101
  // fallthrough if a remote tarball url present
99
102
  }
100
103
  case 'registry': {
101
- const trustIntegrity = this.#trustedIntegrities.get(r.resolved) === r.integrity;
102
- const response = await this.registryClient.request(r.resolved, {
103
- integrity: r.integrity,
104
- trustIntegrity,
105
- });
106
- if (response.statusCode !== 200) {
107
- throw this.#resolveError(spec, options, 'failed to fetch tarball', {
108
- url: r.resolved,
109
- 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 } : {}),
110
110
  });
111
+ if (response.statusCode !== 200) {
112
+ throw this.#resolveError(spec, options, 'failed to fetch tarball', {
113
+ url: r.resolved,
114
+ response,
115
+ });
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();
111
153
  }
112
- // if it's not trusted already, but valid, start trusting
113
- if (!trustIntegrity &&
114
- response.checkIntegrity({ spec, url: resolved })) {
115
- this.#trustedIntegrities.set(r.resolved, response.integrity);
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
+ }
116
168
  }
117
169
  try {
118
- await this.tarPool.unpack(response.buffer(), target);
170
+ await this.tarPool.unpack(buf, target);
119
171
  }
120
172
  catch (er) {
121
173
  throw this.#resolveError(spec, options, 'tar unpack failed', { cause: er });
@@ -278,21 +330,56 @@ export class PackageInfoClient {
278
330
  if (!tarball) {
279
331
  throw this.#resolveError(spec, options, 'no tarball found in manifest.dist');
280
332
  }
281
- const trustIntegrity = this.#trustedIntegrities.get(tarball) === integrity;
282
- const response = await this.registryClient.request(tarball, {
283
- ...options,
284
- integrity,
285
- trustIntegrity,
286
- });
287
- if (response.statusCode !== 200) {
288
- throw this.#resolveError(spec, options, 'failed to fetch tarball', { response, url: tarball });
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();
289
373
  }
290
- // if we don't already trust it, but it's valid, start trusting it
291
- if (!trustIntegrity &&
292
- response.checkIntegrity({ spec, url: tarball })) {
293
- this.#trustedIntegrities.set(tarball, response.integrity);
374
+ catch (er) {
375
+ if (er instanceof Error &&
376
+ 'cause' in er &&
377
+ er.cause
378
+ ?.code === 'EINTEGRITY') {
379
+ return await fetchTarball(false);
380
+ }
381
+ throw er;
294
382
  }
295
- return response.buffer();
296
383
  }
297
384
  case 'git': {
298
385
  const { remoteURL, gitRemote, gitCommittish, gitSelectorParsed, } = f;
@@ -392,15 +479,6 @@ export class PackageInfoClient {
392
479
  : pickManifest(await this.packument(f, options), spec, options);
393
480
  if (!mani)
394
481
  throw this.#resolveError(spec, options);
395
- const { integrity, tarball } = mani.dist ?? /* c8 ignore next */ {};
396
- if (isIntegrity(integrity) && tarball) {
397
- const registryOrigin = new URL(String(f.registry)).origin;
398
- const tgzOrigin = new URL(tarball).origin;
399
- // if it comes from the same origin, trust the integrity
400
- if (tgzOrigin === registryOrigin) {
401
- this.#trustedIntegrities.set(tarball, integrity);
402
- }
403
- }
404
482
  // Cache the manifest data
405
483
  if (cachePath) {
406
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.25",
4
+ "version": "1.0.0-rc.27",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vltpkg/vltpkg.git",
@@ -9,19 +9,20 @@
9
9
  },
10
10
  "author": {
11
11
  "name": "vlt technology inc.",
12
- "email": "support@vlt.sh"
12
+ "email": "support@vlt.sh",
13
+ "url": "http://vlt.sh"
13
14
  },
14
15
  "dependencies": {
15
- "@vltpkg/error-cause": "1.0.0-rc.25",
16
- "@vltpkg/git": "1.0.0-rc.25",
17
- "@vltpkg/package-json": "1.0.0-rc.25",
18
- "@vltpkg/pick-manifest": "1.0.0-rc.25",
19
- "@vltpkg/registry-client": "1.0.0-rc.25",
20
- "@vltpkg/spec": "1.0.0-rc.25",
21
- "@vltpkg/tar": "1.0.0-rc.25",
22
- "@vltpkg/types": "1.0.0-rc.25",
23
- "@vltpkg/workspaces": "1.0.0-rc.25",
24
- "@vltpkg/xdg": "1.0.0-rc.25",
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",
25
26
  "ssri": "^13.0.0",
26
27
  "tar": "^7.5.2"
27
28
  },
@@ -30,8 +31,8 @@
30
31
  "@types/node": "^22.19.2",
31
32
  "@types/pacote": "^11.1.8",
32
33
  "@vltpkg/benchmark": "0.0.0",
33
- "@vltpkg/cache-unzip": "1.0.0-rc.25",
34
- "@vltpkg/vlt-json": "1.0.0-rc.25",
34
+ "@vltpkg/cache-unzip": "1.0.0-rc.27",
35
+ "@vltpkg/vlt-json": "1.0.0-rc.27",
35
36
  "eslint": "^9.39.1",
36
37
  "pacote": "^21.0.4",
37
38
  "prettier": "^3.7.4",