@pnpm/installing.deps-installer 1101.7.0 → 1101.8.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.
@@ -58,7 +58,7 @@ export async function install(manifest, opts) {
58
58
  // When a pnpr server is configured, use server-side resolution
59
59
  // instead of the normal resolution flow.
60
60
  if (opts.pnprServer) {
61
- return installFromPnpmRegistry(manifest, rootDir, opts);
61
+ return installViaPnprServer(manifest, rootDir, opts);
62
62
  }
63
63
  const { updatedCatalogs, updatedProjects: projects, ignoredBuilds, resolutionPolicyViolations } = await mutateModules([
64
64
  {
@@ -262,7 +262,7 @@ export async function mutateModules(projects, maybeOpts) {
262
262
  }
263
263
  let ignoredBuilds = result.ignoredBuilds;
264
264
  if (!opts.ignoreScripts && ignoredBuilds?.size) {
265
- ignoredBuilds = await runUnignoredDependencyBuilds(opts, ignoredBuilds, allowBuild);
265
+ ignoredBuilds = await runUnignoredDependencyBuilds(opts, ignoredBuilds, ctx.wantedLockfile, allowBuild);
266
266
  }
267
267
  // Detect packages whose build approval was revoked between the previous
268
268
  // and current install. A package is considered revoked when it was
@@ -277,10 +277,13 @@ export async function mutateModules(projects, maybeOpts) {
277
277
  for (const depPath of Object.keys(ctx.wantedLockfile.packages)) {
278
278
  if (ignoredBuilds?.has(depPath))
279
279
  continue;
280
- const { name, version } = dp.parse(depPath);
281
- if (!name || !version)
280
+ // The old policy is evaluated with identity trust overridden so that
281
+ // package-name approvals count as they did when they were granted,
282
+ // even for git/tarball artifacts that the current policy no longer
283
+ // approves by name.
284
+ if (oldAllowBuild(depPath, { trustPackageIdentity: true }) !== true)
282
285
  continue;
283
- if (oldAllowBuild(name, version) === true && allowBuild?.(name, version) === undefined) {
286
+ if (allowBuild?.(depPath) === undefined) {
284
287
  ignoredBuilds ??= new Set();
285
288
  ignoredBuilds.add(depPath);
286
289
  }
@@ -796,19 +799,17 @@ Note that in CI environments, this setting is enabled by default.`,
796
799
  }
797
800
  }
798
801
  }
799
- async function runUnignoredDependencyBuilds(opts, previousIgnoredBuilds, allowBuild) {
802
+ async function runUnignoredDependencyBuilds(opts, previousIgnoredBuilds, currentLockfile, allowBuild) {
800
803
  if (!allowBuild) {
801
804
  return previousIgnoredBuilds;
802
805
  }
803
806
  const pkgsToBuild = [];
804
807
  for (const ignoredPkg of previousIgnoredBuilds) {
805
- const parsed = dp.parse(ignoredPkg);
806
- if (!parsed.name || !parsed.version)
808
+ if (currentLockfile.packages?.[ignoredPkg] == null)
807
809
  continue;
808
- const allowed = allowBuild(parsed.name, parsed.version);
809
- if (allowed === true) {
810
+ if (allowBuild(ignoredPkg) === true) {
810
811
  // Package is explicitly allowed - rebuild it
811
- pkgsToBuild.push(`${parsed.name}@${parsed.version}`);
812
+ pkgsToBuild.push(dp.getPkgIdWithPatchHash(ignoredPkg));
812
813
  }
813
814
  }
814
815
  if (pkgsToBuild.length) {
@@ -1572,7 +1573,7 @@ export class IgnoredBuildsError extends PnpmError {
1572
1573
  }
1573
1574
  }
1574
1575
  function dedupePackageNamesFromIgnoredBuilds(ignoredBuilds) {
1575
- return Array.from(new Set(Array.from(ignoredBuilds ?? []).map(dp.removeSuffix))).sort(lexCompare);
1576
+ return Array.from(new Set(Array.from(ignoredBuilds ?? []).map(depPath => dp.getPkgIdWithPatchHash(depPath)))).sort(lexCompare);
1576
1577
  }
1577
1578
  /**
1578
1579
  * Build injectionTargetsByDepPath from the dependenciesGraph for injected workspace packages
@@ -1777,10 +1778,10 @@ async function mutateModulesViaPnpr(projects, opts) {
1777
1778
  const pnprProjects = await preparePnprProjects(projects, opts);
1778
1779
  if (!pnprProjects)
1779
1780
  return null;
1780
- // installFromPnpmRegistry runs the headless install for the first
1781
+ // installViaPnprServer runs the headless install for the first
1781
1782
  // project's root and the workspace path for the rest. Pass the
1782
1783
  // pre-processed manifests so resolution sees the post-mutation state.
1783
- const result = await installFromPnpmRegistry(pnprProjects[0].manifest, pnprProjects[0].rootDir, opts, pnprProjects.map((p) => ({ rootDir: p.rootDir, manifest: p.manifest })));
1784
+ const result = await installViaPnprServer(pnprProjects[0].manifest, pnprProjects[0].rootDir, opts, pnprProjects.map((p) => ({ rootDir: p.rootDir, manifest: p.manifest })));
1784
1785
  // For installSome projects, copy resolved specs from the lockfile importer
1785
1786
  // entries back into the client manifest so save-prefix/catalog/etc. take
1786
1787
  // effect (the server applies these during its resolution step).
@@ -1805,11 +1806,11 @@ async function mutateModulesViaPnpr(projects, opts) {
1805
1806
  };
1806
1807
  }
1807
1808
  /**
1808
- * When a pnpr server is configured, resolve dependencies server-side
1809
- * and download only the missing files. Then run a headless install to link
1810
- * packages into node_modules.
1809
+ * When a pnpr server is configured, resolve dependencies server-side,
1810
+ * then run a headless install that fetches tarballs from the registries
1811
+ * and links packages into node_modules — like a normal install.
1811
1812
  */
1812
- async function installFromPnpmRegistry(manifest, rootDir, opts, allInstallProjects) {
1813
+ async function installViaPnprServer(manifest, rootDir, opts, allInstallProjects) {
1813
1814
  // The pnpr server path skips client-side resolution, so resolver-side policies
1814
1815
  // can't be enforced locally. `minimumReleaseAge` is forwarded to the
1815
1816
  // pnpr server and enforced server-side. `trustPolicy` has no server-side
@@ -1818,20 +1819,14 @@ async function installFromPnpmRegistry(manifest, rootDir, opts, allInstallProjec
1818
1819
  if (opts.trustPolicy === 'no-downgrade') {
1819
1820
  throw new PnpmError('TRUST_POLICY_INCOMPATIBLE_WITH_PNPR', 'The pnpr server does not yet enforce `trustPolicy: no-downgrade`, so running an install through it under this policy would produce a lockfile that the local verifier rejects.', { hint: 'Unset `trustPolicy` for this install, or disable the pnpr server (unset `--pnpr-server` / `pnprServer` in pnpm-workspace.yaml) so resolution runs locally and the trust check applies.' });
1820
1821
  }
1821
- const { fetchFromPnpmRegistry } = await import('@pnpm/pnpr.client');
1822
+ const { resolveViaPnprServer } = await import('@pnpm/pnpr.client');
1822
1823
  const { createGetAuthHeaderByURI, getAuthHeadersFromCreds } = await import('@pnpm/network.auth-header');
1823
- const { StoreIndex } = await import('@pnpm/store.index');
1824
- const { setImportConcurrency } = await import('@pnpm/worker');
1825
1824
  // Forward the whole credential map (the registries a graph touches
1826
1825
  // aren't known up front), so the server attaches the right token per
1827
1826
  // URL. `authorization` also identifies the caller to pnpr's gate.
1828
1827
  const configByUri = opts.configByUri ?? {};
1829
1828
  const forwardedAuthHeaders = getAuthHeadersFromCreds(configByUri);
1830
1829
  const pnprAuthorization = createGetAuthHeaderByURI(configByUri)(opts.pnprServer);
1831
- // Raise import concurrency for this install only — the pnpr server path has no
1832
- // concurrent fetching competing for workers. Restore afterwards so we
1833
- // don't leak a process-wide mutation to other installs (e.g. tests).
1834
- const restoreImportConcurrency = setImportConcurrency(6);
1835
1830
  try {
1836
1831
  const lockfileDir = opts.lockfileDir ?? rootDir;
1837
1832
  // Read the existing lockfile (if any) in its on-disk shape — that's
@@ -1841,49 +1836,32 @@ async function installFromPnpmRegistry(manifest, rootDir, opts, allInstallProjec
1841
1836
  ignoreIncompatible: true,
1842
1837
  }).catch(() => null);
1843
1838
  logger.info({ message: 'Resolving dependencies via the pnpr server', prefix: rootDir });
1844
- // Open the store index to read integrities and write new entries.
1845
- // Close it in a finally so a failure in fetchFromPnpmRegistry doesn't
1846
- // leak an open SQLite handle (on Windows that also blocks store cleanup).
1847
- const storeIndex = new StoreIndex(opts.storeDir);
1848
- let lockfile, pnprStats, fileDownloads, indexEntries;
1849
- try {
1850
- // Build projects list for workspace support.
1851
- // Normalize separators to POSIX — on Windows `path.relative` returns
1852
- // backslashes, which the pnpr server rejects (it treats `\` as an
1853
- // unsafe/YAML-injection character and normalizes paths as POSIX).
1854
- const projectsList = allInstallProjects && allInstallProjects.length > 1
1855
- ? allInstallProjects.map(p => ({
1856
- dir: (path.relative(lockfileDir, p.rootDir) || '.').split(path.sep).join('/'),
1857
- dependencies: p.manifest.dependencies,
1858
- devDependencies: p.manifest.devDependencies,
1859
- optionalDependencies: p.manifest.optionalDependencies,
1860
- }))
1861
- : undefined;
1862
- ({ lockfile, stats: pnprStats, fileDownloads, indexEntries } = await fetchFromPnpmRegistry({
1863
- registryUrl: opts.pnprServer,
1864
- storeDir: opts.storeDir,
1865
- storeIndex,
1866
- dependencies: projectsList ? undefined : manifest.dependencies,
1867
- devDependencies: projectsList ? undefined : manifest.devDependencies,
1868
- optionalDependencies: projectsList ? undefined : manifest.optionalDependencies,
1869
- projects: projectsList,
1870
- registry: opts.registries?.default,
1871
- namedRegistries: opts.namedRegistries,
1872
- authHeaders: forwardedAuthHeaders,
1873
- authorization: pnprAuthorization,
1874
- overrides: opts.overrides,
1875
- minimumReleaseAge: opts.minimumReleaseAge,
1876
- lockfile: existingLockfile ?? undefined,
1877
- lockfileOnly: opts.lockfileOnly,
1878
- }));
1879
- // Write store index entries so headless install finds them.
1880
- const { writeRawIndexEntries } = await import('@pnpm/pnpr.client');
1881
- writeRawIndexEntries(indexEntries, storeIndex);
1882
- storeIndex.checkpoint();
1883
- }
1884
- finally {
1885
- storeIndex.close();
1886
- }
1839
+ // Build projects list for workspace support.
1840
+ // Normalize separators to POSIX on Windows `path.relative` returns
1841
+ // backslashes, which the pnpr server rejects (it treats `\` as an
1842
+ // unsafe/YAML-injection character and normalizes paths as POSIX).
1843
+ const projectsList = allInstallProjects && allInstallProjects.length > 1
1844
+ ? allInstallProjects.map(p => ({
1845
+ dir: (path.relative(lockfileDir, p.rootDir) || '.').split(path.sep).join('/'),
1846
+ dependencies: p.manifest.dependencies,
1847
+ devDependencies: p.manifest.devDependencies,
1848
+ optionalDependencies: p.manifest.optionalDependencies,
1849
+ }))
1850
+ : undefined;
1851
+ const { lockfile, stats: pnprStats } = await resolveViaPnprServer({
1852
+ registryUrl: opts.pnprServer,
1853
+ dependencies: projectsList ? undefined : manifest.dependencies,
1854
+ devDependencies: projectsList ? undefined : manifest.devDependencies,
1855
+ optionalDependencies: projectsList ? undefined : manifest.optionalDependencies,
1856
+ projects: projectsList,
1857
+ registry: opts.registries?.default,
1858
+ namedRegistries: opts.namedRegistries,
1859
+ authHeaders: forwardedAuthHeaders,
1860
+ authorization: pnprAuthorization,
1861
+ overrides: opts.overrides,
1862
+ minimumReleaseAge: opts.minimumReleaseAge,
1863
+ lockfile: existingLockfile ?? undefined,
1864
+ });
1887
1865
  await writeWantedLockfileAndRecordVerified({
1888
1866
  lockfileDir,
1889
1867
  lockfile,
@@ -1893,17 +1871,13 @@ async function installFromPnpmRegistry(manifest, rootDir, opts, allInstallProjec
1893
1871
  mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
1894
1872
  });
1895
1873
  logger.info({
1896
- message: `Resolved ${pnprStats.totalPackages} packages: ${pnprStats.alreadyInStore} cached, ${pnprStats.filesToDownload} files to download`,
1874
+ message: `Resolved ${pnprStats.totalPackages} packages`,
1897
1875
  prefix: rootDir,
1898
1876
  });
1899
1877
  // `--lockfile-only`: the pnpr server resolved and we wrote the lockfile, but
1900
1878
  // pnpm fetches nothing and links nothing in this mode — stop before the
1901
1879
  // headless install. See https://github.com/pnpm/pnpm/issues/12146.
1902
1880
  if (opts.lockfileOnly) {
1903
- // Nothing is downloaded in this mode, but the lockfile arrives before
1904
- // the stream closes — observe `fileDownloads` so a stream error after
1905
- // the `L` frame doesn't surface as an unhandled rejection.
1906
- void fileDownloads.catch(() => { });
1907
1881
  return {
1908
1882
  updatedCatalogs: undefined,
1909
1883
  updatedManifest: manifest,
@@ -1913,43 +1887,12 @@ async function installFromPnpmRegistry(manifest, rootDir, opts, allInstallProjec
1913
1887
  resolutionPolicyViolations: [],
1914
1888
  };
1915
1889
  }
1916
- // Wrap fetchPackage to:
1917
- // 1. Wait for pnpr server file downloads before checking the store
1918
- // 2. Skip integrity verification files just written from the pnpr server
1919
- // are guaranteed correct (server verified, no rehashing needed)
1920
- const { readPkgFromCafs } = await import('@pnpm/worker');
1921
- const { storeIndexKey: _storeIndexKey } = await import('@pnpm/store.index');
1922
- const wrappedStoreController = {
1923
- ...opts.storeController,
1924
- fetchPackage: async (fetchOpts) => {
1925
- await fileDownloads;
1926
- const resolution = fetchOpts.pkg.resolution;
1927
- const integrity = resolution?.integrity;
1928
- // Fall through to the regular store controller for git-hosted tarballs.
1929
- // Their cached entry lives under gitHostedStoreIndexKey (preserves the
1930
- // built/not-built dimension), not the integrity-keyed path the pnpr server
1931
- // uses for npm tarballs. See @pnpm/store.pkg-finder for the rationale.
1932
- if (integrity && !resolution?.gitHosted) {
1933
- const filesIndexFile = _storeIndexKey(integrity, fetchOpts.pkg.id);
1934
- const result = await readPkgFromCafs({ storeDir: opts.storeDir, verifyStoreIntegrity: false }, filesIndexFile, { readManifest: true, expectedPkg: { name: fetchOpts.pkg.name, version: fetchOpts.pkg.version } });
1935
- return {
1936
- fetching: () => Promise.resolve({
1937
- files: result.files,
1938
- bundledManifest: result.bundledManifest,
1939
- integrity,
1940
- }),
1941
- filesIndexFile,
1942
- };
1943
- }
1944
- return opts.storeController.fetchPackage(fetchOpts);
1945
- },
1946
- };
1890
+ // The pnpr server only resolves; it serves no file content. Fetch every
1891
+ // tarball from the registries with the regular store controller, in
1892
+ // parallel, exactly like a normal install. See
1893
+ // https://github.com/pnpm/pnpm/issues/12230.
1947
1894
  const headlessOpts = {
1948
1895
  ...opts,
1949
- // Skip re-verifying files just written from the pnpr server — they're
1950
- // guaranteed correct (server verified, no rehashing needed).
1951
- verifyStoreIntegrity: false,
1952
- storeController: wrappedStoreController,
1953
1896
  dir: rootDir,
1954
1897
  lockfileDir,
1955
1898
  engineStrict: opts.engineStrict ?? false,
@@ -2006,7 +1949,6 @@ async function installFromPnpmRegistry(manifest, rootDir, opts, allInstallProjec
2006
1949
  // normal install path does the same; skipping it here would leave
2007
1950
  // pending writes on disk and diverge from lifecycle expectations.
2008
1951
  await opts.storeController.close();
2009
- restoreImportConcurrency();
2010
1952
  }
2011
1953
  }
2012
1954
  //# sourceMappingURL=index.js.map
@@ -328,7 +328,7 @@ async function linkAllPkgs(storeController, depNodes, opts) {
328
328
  depNode.requiresBuild = files.requiresBuild;
329
329
  let sideEffectsCacheKey;
330
330
  if (opts.sideEffectsCacheRead && files.sideEffectsMaps && !isEmpty(files.sideEffectsMaps)) {
331
- if (opts.allowBuild?.(depNode.name, depNode.version) === true) {
331
+ if (opts.allowBuild?.(depNode.depPath) === true) {
332
332
  sideEffectsCacheKey = calcDepState(opts.depGraph, opts.depsStateCache, depNode.depPath, {
333
333
  includeDepGraphHash: !opts.ignoreScripts && depNode.requiresBuild, // true when is built
334
334
  patchFileHash: depNode.patch?.hash,
@@ -1,4 +1,5 @@
1
1
  import { hashObject } from '@pnpm/crypto.object-hasher';
2
+ import { withResolutionShapeCacheIdentity } from './verifyLockfileResolutions.js';
2
3
  import { recordVerification } from './verifyLockfileResolutionsCache.js';
3
4
  /**
4
5
  * Records the post-resolution lockfile as verified so the next install
@@ -17,7 +18,7 @@ export function recordLockfileVerified(opts) {
17
18
  return;
18
19
  recordVerification(opts.cacheDir, {
19
20
  lockfilePath: opts.lockfilePath,
20
- verifiers: opts.resolutionVerifiers,
21
+ verifiers: withResolutionShapeCacheIdentity(opts.resolutionVerifiers),
21
22
  hashLockfile: () => hashObject(opts.lockfile),
22
23
  });
23
24
  }
@@ -1,6 +1,17 @@
1
1
  import type { LockfileObject } from '@pnpm/lockfile.fs';
2
2
  import type { ResolutionPolicyViolation, ResolutionVerifier } from '@pnpm/resolving.resolver-base';
3
+ import { type VerifierCacheIdentity } from './verifyLockfileResolutionsCache.js';
3
4
  export type { ResolutionPolicyViolation };
5
+ export declare const RESOLUTION_SHAPE_MISMATCH_VIOLATION_CODE = "RESOLUTION_SHAPE_MISMATCH";
6
+ /**
7
+ * Every verifier list that flows into the verification cache must carry
8
+ * the resolution-shape identity, so records written before the shape rule
9
+ * existed cannot stat-fast-path around it. Used by the gate itself and by
10
+ * {@link recordLockfileVerified}, whose freshly-resolved lockfile satisfies
11
+ * the shape invariant by construction (the writer derives every key from
12
+ * the resolution it just produced).
13
+ */
14
+ export declare function withResolutionShapeCacheIdentity(verifiers: readonly VerifierCacheIdentity[]): VerifierCacheIdentity[];
4
15
  export interface VerifyLockfileResolutionsOptions {
5
16
  concurrency?: number;
6
17
  /**
@@ -1,7 +1,7 @@
1
1
  import { lockfileVerificationLogger } from '@pnpm/core-loggers';
2
2
  import { hashObject } from '@pnpm/crypto.object-hasher';
3
3
  import { PnpmError } from '@pnpm/error';
4
- import { nameVerFromPkgSnapshot } from '@pnpm/lockfile.utils';
4
+ import { isGitHostedTarballUrl, nameVerFromPkgSnapshot } from '@pnpm/lockfile.utils';
5
5
  import pLimit from 'p-limit';
6
6
  import { recordVerification, tryLockfileVerificationCache, } from './verifyLockfileResolutionsCache.js';
7
7
  // Cap the per-entry breakdown so a verifier rejecting hundreds of entries
@@ -12,6 +12,22 @@ const MAX_VIOLATIONS_TO_PRINT = 20;
12
12
  // (Math.min(64, Math.max(workers*3, 16))); keep them aligned so the
13
13
  // verification pass doesn't push past what the rest of the install respects.
14
14
  const DEFAULT_CONCURRENCY = 16;
15
+ export const RESOLUTION_SHAPE_MISMATCH_VIOLATION_CODE = 'RESOLUTION_SHAPE_MISMATCH';
16
+ const RESOLUTION_SHAPE_CACHE_IDENTITY = {
17
+ policy: { resolutionShapeCheck: true },
18
+ canTrustPastCheck: (cached) => cached.resolutionShapeCheck === true,
19
+ };
20
+ /**
21
+ * Every verifier list that flows into the verification cache must carry
22
+ * the resolution-shape identity, so records written before the shape rule
23
+ * existed cannot stat-fast-path around it. Used by the gate itself and by
24
+ * {@link recordLockfileVerified}, whose freshly-resolved lockfile satisfies
25
+ * the shape invariant by construction (the writer derives every key from
26
+ * the resolution it just produced).
27
+ */
28
+ export function withResolutionShapeCacheIdentity(verifiers) {
29
+ return [...verifiers, RESOLUTION_SHAPE_CACHE_IDENTITY];
30
+ }
15
31
  /**
16
32
  * Policy-neutral pass that asks every resolver-supplied
17
33
  * {@link ResolutionVerifier} to check every entry in a lockfile loaded
@@ -40,8 +56,6 @@ const DEFAULT_CONCURRENCY = 16;
40
56
  * the lookup logic.
41
57
  */
42
58
  export async function verifyLockfileResolutions(lockfile, verifiers, options) {
43
- if (verifiers.length === 0)
44
- return;
45
59
  if (!lockfile.packages)
46
60
  return;
47
61
  // Caching kicks in only when the caller surfaced both a writable
@@ -51,6 +65,7 @@ export async function verifyLockfileResolutions(lockfile, verifiers, options) {
51
65
  const cache = options?.cacheDir && options?.lockfilePath
52
66
  ? { cacheDir: options.cacheDir, lockfilePath: options.lockfilePath }
53
67
  : undefined;
68
+ const cacheVerifiers = withResolutionShapeCacheIdentity(verifiers);
54
69
  let cachePrecomputed;
55
70
  // hashObject streams and is key-order-stable, unlike JSON.stringify.
56
71
  let cachedHash;
@@ -62,7 +77,7 @@ export async function verifyLockfileResolutions(lockfile, verifiers, options) {
62
77
  if (cache) {
63
78
  const result = tryLockfileVerificationCache(cache.cacheDir, {
64
79
  lockfilePath: cache.lockfilePath,
65
- verifiers,
80
+ verifiers: cacheVerifiers,
66
81
  hashLockfile,
67
82
  });
68
83
  if (result.hit)
@@ -76,12 +91,17 @@ export async function verifyLockfileResolutions(lockfile, verifiers, options) {
76
91
  // A degenerate lockfile where every snapshot fails the
77
92
  // name/version extraction (so candidates is empty) skips emission
78
93
  // entirely — no work, no noise.
79
- const candidates = collectCandidates(lockfile);
94
+ const { candidates, shapeViolations } = collectCandidates(lockfile);
95
+ if (shapeViolations.length > 0) {
96
+ throw buildVerificationError(shapeViolations);
97
+ }
98
+ if (verifiers.length === 0)
99
+ return;
80
100
  if (candidates.size === 0) {
81
101
  if (cache) {
82
102
  recordVerification(cache.cacheDir, {
83
103
  lockfilePath: cache.lockfilePath,
84
- verifiers,
104
+ verifiers: cacheVerifiers,
85
105
  hashLockfile,
86
106
  }, cachePrecomputed);
87
107
  }
@@ -108,7 +128,7 @@ export async function verifyLockfileResolutions(lockfile, verifiers, options) {
108
128
  if (cache) {
109
129
  recordVerification(cache.cacheDir, {
110
130
  lockfilePath: cache.lockfilePath,
111
- verifiers,
131
+ verifiers: cacheVerifiers,
112
132
  hashLockfile,
113
133
  }, cachePrecomputed);
114
134
  }
@@ -174,7 +194,48 @@ export async function collectResolutionPolicyViolations(lockfile, verifiers, opt
174
194
  return [];
175
195
  if (!lockfile.packages)
176
196
  return [];
177
- return iterateLockfileViolations(collectCandidates(lockfile), verifiers, options?.concurrency);
197
+ // Shape violations are deliberately not collected here: they are hard
198
+ // tampering failures, not policy picks a caller may auto-exclude.
199
+ return iterateLockfileViolations(collectCandidates(lockfile).candidates, verifiers, options?.concurrency);
200
+ }
201
+ function isRegistryShapedResolution(resolution) {
202
+ if (resolution == null)
203
+ return true;
204
+ if (typeof resolution !== 'object')
205
+ return false;
206
+ const { type, gitHosted, tarball, variants } = resolution;
207
+ if (type === 'variations') {
208
+ return Array.isArray(variants) && variants.every((variant) => isRegistryShapedResolution(variant?.resolution));
209
+ }
210
+ // Custom resolver protocols (`type: 'custom:*'`) are a legitimate
211
+ // non-registry source the user opted into. They can only be materialized by
212
+ // a project-configured custom fetcher — an unrecognized custom type throws at
213
+ // fetch time (see @pnpm/fetching.pick-fetcher) — so a forged custom type
214
+ // cannot launder an artifact past this gate into a build.
215
+ if (typeof type === 'string' && type.startsWith('custom:'))
216
+ return true;
217
+ if (type != null)
218
+ return false;
219
+ // Plain tarball / registry resolution. The lockfile is parsed from YAML
220
+ // without schema validation, so the `gitHosted` flag is not trustworthy on
221
+ // its own: a tampered entry could set a non-boolean (dodging a strict
222
+ // `=== true`) or an explicit `false` on a git-host URL (the loader only
223
+ // backfills the flag when absent). Treat any non-boolean flag as git-hosted
224
+ // and gate on the URL so the verdict never depends on the flag alone.
225
+ if (gitHosted != null && (typeof gitHosted !== 'boolean' || gitHosted))
226
+ return false;
227
+ // A registry resolution reconstructs its tarball URL from name+version, so
228
+ // an absent/empty `tarball` is registry-shaped. When a URL is present it
229
+ // must be an http(s) registry artifact: the npm verifier's tarball-URL
230
+ // binding skips non-http(s) schemes (file:, etc.), so a `file:` tarball
231
+ // under a name@semver key would otherwise be trusted with no safety net.
232
+ if (typeof tarball === 'string' && tarball !== '') {
233
+ if (!/^https?:\/\//i.test(tarball))
234
+ return false;
235
+ if (isGitHostedTarballUrl(tarball))
236
+ return false;
237
+ }
238
+ return true;
178
239
  }
179
240
  // depPath can include peer-dependency and patch_hash suffixes (e.g.
180
241
  // `react@18.0.0(peer)(patch_hash=…)`); the same (name, version) pair may
@@ -191,10 +252,25 @@ export async function collectResolutionPolicyViolations(lockfile, verifiers, opt
191
252
  // never checked.
192
253
  function collectCandidates(lockfile) {
193
254
  const candidates = new Map();
255
+ const shapeViolations = [];
194
256
  for (const [depPath, snapshot] of Object.entries(lockfile.packages ?? {})) {
195
257
  const { name, version, nonSemverVersion } = nameVerFromPkgSnapshot(depPath, snapshot);
196
258
  if (!name || !version)
197
259
  continue;
260
+ // A registry-style depPath (name@semver) must be backed by a
261
+ // registry-shaped resolution: the allowBuilds policy derives a
262
+ // trusted package identity from that key shape, which is only sound
263
+ // while this invariant holds. The check is offline, so it applies
264
+ // even when no policy verifiers are active.
265
+ if (nonSemverVersion == null && !isRegistryShapedResolution(snapshot.resolution)) {
266
+ shapeViolations.push({
267
+ name,
268
+ version,
269
+ resolution: snapshot.resolution,
270
+ code: RESOLUTION_SHAPE_MISMATCH_VIOLATION_CODE,
271
+ reason: 'a registry-style dependency path is backed by a non-registry resolution',
272
+ });
273
+ }
198
274
  const key = `${name}@${version}@${nonSemverVersion ?? ''}@${JSON.stringify(snapshot.resolution)}`;
199
275
  candidates.set(key, {
200
276
  name,
@@ -203,7 +279,7 @@ function collectCandidates(lockfile) {
203
279
  resolution: snapshot.resolution,
204
280
  });
205
281
  }
206
- return candidates;
282
+ return { candidates, shapeViolations };
207
283
  }
208
284
  async function iterateLockfileViolations(candidates, verifiers, concurrency) {
209
285
  const violations = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/installing.deps-installer",
3
- "version": "1101.7.0",
3
+ "version": "1101.8.0",
4
4
  "description": "Fast, disk space efficient installation engine",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -65,61 +65,61 @@
65
65
  "ramda": "npm:@pnpm/ramda@0.28.1",
66
66
  "run-groups": "^5.0.0",
67
67
  "semver": "^7.8.1",
68
- "@pnpm/building.after-install": "1101.0.19",
69
- "@pnpm/bins.linker": "1100.0.11",
70
- "@pnpm/building.policy": "1100.0.8",
71
- "@pnpm/catalogs.resolver": "1100.0.0",
72
- "@pnpm/config.matcher": "1100.0.1",
68
+ "@pnpm/bins.linker": "1100.0.12",
69
+ "@pnpm/building.after-install": "1101.0.20",
73
70
  "@pnpm/catalogs.protocol-parser": "1100.0.0",
74
- "@pnpm/building.during-install": "1101.0.16",
75
- "@pnpm/config.normalize-registries": "1100.0.6",
76
- "@pnpm/core-loggers": "1100.1.3",
77
- "@pnpm/bins.remover": "1100.0.7",
71
+ "@pnpm/building.during-install": "1101.0.17",
72
+ "@pnpm/building.policy": "1100.0.9",
78
73
  "@pnpm/catalogs.types": "1100.0.0",
79
- "@pnpm/deps.graph-sequencer": "1100.0.0",
74
+ "@pnpm/catalogs.resolver": "1100.0.0",
75
+ "@pnpm/config.normalize-registries": "1100.0.7",
76
+ "@pnpm/config.matcher": "1100.0.1",
77
+ "@pnpm/core-loggers": "1100.1.4",
80
78
  "@pnpm/constants": "1100.0.0",
79
+ "@pnpm/crypto.hash": "1100.0.1",
80
+ "@pnpm/deps.graph-hasher": "1100.2.4",
81
81
  "@pnpm/crypto.object-hasher": "1100.0.0",
82
- "@pnpm/deps.path": "1100.0.6",
83
- "@pnpm/deps.graph-hasher": "1100.2.3",
84
- "@pnpm/error": "1100.0.0",
82
+ "@pnpm/deps.graph-sequencer": "1100.0.0",
85
83
  "@pnpm/config.parse-overrides": "1100.0.1",
86
- "@pnpm/exec.lifecycle": "1100.0.15",
87
- "@pnpm/hooks.read-package-hook": "1100.0.6",
84
+ "@pnpm/error": "1100.0.0",
88
85
  "@pnpm/fs.read-modules-dir": "1100.0.1",
89
- "@pnpm/hooks.types": "1100.0.10",
90
- "@pnpm/installing.context": "1100.0.15",
91
- "@pnpm/crypto.hash": "1100.0.1",
92
- "@pnpm/installing.deps-resolver": "1100.2.0",
93
- "@pnpm/installing.deps-restorer": "1101.1.9",
94
- "@pnpm/installing.linking.direct-dep-linker": "1100.0.7",
95
- "@pnpm/installing.linking.hoist": "1100.0.11",
96
- "@pnpm/installing.modules-yaml": "1100.0.7",
97
- "@pnpm/installing.package-requester": "1101.0.11",
98
- "@pnpm/lockfile.fs": "1100.1.3",
99
- "@pnpm/lockfile.filtering": "1100.1.4",
100
- "@pnpm/lockfile.to-pnp": "1100.0.12",
101
- "@pnpm/lockfile.preferred-versions": "1100.0.13",
102
- "@pnpm/lockfile.utils": "1100.0.11",
103
- "@pnpm/lockfile.walker": "1100.0.9",
104
- "@pnpm/network.auth-header": "1101.1.0",
105
- "@pnpm/pkg-manifest.utils": "1100.2.2",
106
- "@pnpm/patching.config": "1100.0.6",
107
- "@pnpm/lockfile.pruner": "1100.0.9",
108
- "@pnpm/store.index": "1100.1.0",
109
- "@pnpm/resolving.resolver-base": "1100.4.0",
86
+ "@pnpm/exec.lifecycle": "1100.0.16",
87
+ "@pnpm/fs.symlink-dependency": "1100.0.8",
88
+ "@pnpm/hooks.read-package-hook": "1100.0.7",
89
+ "@pnpm/installing.deps-restorer": "1101.1.10",
90
+ "@pnpm/hooks.types": "1100.0.11",
91
+ "@pnpm/installing.context": "1100.0.16",
92
+ "@pnpm/installing.deps-resolver": "1100.2.1",
93
+ "@pnpm/installing.linking.direct-dep-linker": "1100.0.8",
94
+ "@pnpm/deps.path": "1100.0.7",
95
+ "@pnpm/installing.linking.modules-cleaner": "1100.1.6",
96
+ "@pnpm/installing.linking.hoist": "1100.0.12",
97
+ "@pnpm/bins.remover": "1100.0.8",
98
+ "@pnpm/installing.modules-yaml": "1100.0.8",
99
+ "@pnpm/lockfile.fs": "1100.1.4",
100
+ "@pnpm/installing.package-requester": "1101.0.12",
101
+ "@pnpm/lockfile.pruner": "1100.0.10",
102
+ "@pnpm/lockfile.settings-checker": "1100.0.16",
103
+ "@pnpm/lockfile.preferred-versions": "1100.0.14",
104
+ "@pnpm/lockfile.filtering": "1100.1.5",
105
+ "@pnpm/lockfile.to-pnp": "1100.0.13",
106
+ "@pnpm/lockfile.verification": "1100.0.16",
107
+ "@pnpm/lockfile.utils": "1100.0.12",
108
+ "@pnpm/lockfile.walker": "1100.0.10",
109
+ "@pnpm/network.auth-header": "1101.1.1",
110
110
  "@pnpm/resolving.parse-wanted-dependency": "1100.0.1",
111
- "@pnpm/workspace.project-manifest-reader": "1100.0.10",
112
- "@pnpm/pnpr.client": "1.1.0",
113
- "@pnpm/types": "1101.3.0",
114
- "@pnpm/lockfile.settings-checker": "1100.0.15",
115
- "@pnpm/installing.linking.modules-cleaner": "1100.1.5",
116
- "@pnpm/store.controller-types": "1100.1.3",
117
- "@pnpm/lockfile.verification": "1100.0.15",
118
- "@pnpm/fs.symlink-dependency": "1100.0.7"
111
+ "@pnpm/pkg-manifest.utils": "1100.2.3",
112
+ "@pnpm/patching.config": "1100.0.7",
113
+ "@pnpm/pnpr.client": "1.2.0",
114
+ "@pnpm/resolving.resolver-base": "1100.4.1",
115
+ "@pnpm/store.controller-types": "1100.1.4",
116
+ "@pnpm/workspace.project-manifest-reader": "1100.0.11",
117
+ "@pnpm/store.index": "1100.1.0",
118
+ "@pnpm/types": "1101.3.1"
119
119
  },
120
120
  "peerDependencies": {
121
121
  "@pnpm/logger": "^1001.0.1",
122
- "@pnpm/worker": "^1100.1.9"
122
+ "@pnpm/worker": "^1100.1.10"
123
123
  },
124
124
  "devDependencies": {
125
125
  "@jest/globals": "30.3.0",
@@ -141,22 +141,22 @@
141
141
  "symlink-dir": "^10.0.1",
142
142
  "write-json-file": "^7.0.0",
143
143
  "write-yaml-file": "^6.0.0",
144
- "@pnpm/assert-project": "1100.0.13",
145
- "@pnpm/lockfile.types": "1100.0.9",
146
- "@pnpm/installing.deps-installer": "1101.7.0",
147
- "@pnpm/prepare": "1100.0.13",
148
- "@pnpm/pkg-manifest.reader": "1100.0.6",
144
+ "@pnpm/assert-project": "1100.0.14",
145
+ "@pnpm/lockfile.types": "1100.0.10",
146
+ "@pnpm/installing.deps-installer": "1101.8.0",
147
+ "@pnpm/assert-store": "1100.0.14",
149
148
  "@pnpm/network.git-utils": "1100.0.1",
150
149
  "@pnpm/logger": "1100.0.0",
151
- "@pnpm/resolving.registry.types": "1100.1.1",
152
- "@pnpm/store.cafs": "1100.1.8",
153
- "@pnpm/testing.mock-agent": "1101.0.0",
150
+ "@pnpm/pkg-manifest.reader": "1100.0.7",
151
+ "@pnpm/resolving.registry.types": "1100.1.2",
152
+ "@pnpm/prepare": "1100.0.14",
153
+ "@pnpm/test-fixtures": "1100.0.0",
154
+ "@pnpm/store.cafs": "1100.1.9",
155
+ "@pnpm/testing.mock-agent": "1101.0.1",
154
156
  "@pnpm/test-ipc-server": "1100.0.0",
155
157
  "@pnpm/store.path": "1100.0.1",
156
- "@pnpm/test-fixtures": "1100.0.0",
157
- "@pnpm/testing.registry-mock": "1100.0.3",
158
- "@pnpm/testing.temp-store": "1100.1.6",
159
- "@pnpm/assert-store": "1100.0.13"
158
+ "@pnpm/testing.temp-store": "1100.1.7",
159
+ "@pnpm/testing.registry-mock": "1100.0.4"
160
160
  },
161
161
  "engines": {
162
162
  "node": ">=22.13"