@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.
package/lib/install/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
281
|
-
|
|
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 (
|
|
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
|
-
|
|
806
|
-
if (!parsed.name || !parsed.version)
|
|
808
|
+
if (currentLockfile.packages?.[ignoredPkg] == null)
|
|
807
809
|
continue;
|
|
808
|
-
|
|
809
|
-
if (allowed === true) {
|
|
810
|
+
if (allowBuild(ignoredPkg) === true) {
|
|
810
811
|
// Package is explicitly allowed - rebuild it
|
|
811
|
-
pkgsToBuild.push(
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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 {
|
|
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
|
-
//
|
|
1845
|
-
//
|
|
1846
|
-
//
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
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
|
|
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
|
-
//
|
|
1917
|
-
//
|
|
1918
|
-
//
|
|
1919
|
-
//
|
|
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
|
package/lib/install/link.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
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/
|
|
69
|
-
"@pnpm/
|
|
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.
|
|
75
|
-
"@pnpm/
|
|
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/
|
|
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.
|
|
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/
|
|
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/
|
|
90
|
-
"@pnpm/
|
|
91
|
-
"@pnpm/
|
|
92
|
-
"@pnpm/installing.deps-
|
|
93
|
-
"@pnpm/
|
|
94
|
-
"@pnpm/installing.
|
|
95
|
-
"@pnpm/installing.
|
|
96
|
-
"@pnpm/installing.
|
|
97
|
-
"@pnpm/
|
|
98
|
-
"@pnpm/
|
|
99
|
-
"@pnpm/
|
|
100
|
-
"@pnpm/
|
|
101
|
-
"@pnpm/
|
|
102
|
-
"@pnpm/lockfile.
|
|
103
|
-
"@pnpm/
|
|
104
|
-
"@pnpm/
|
|
105
|
-
"@pnpm/
|
|
106
|
-
"@pnpm/
|
|
107
|
-
"@pnpm/lockfile.
|
|
108
|
-
"@pnpm/
|
|
109
|
-
"@pnpm/
|
|
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/
|
|
112
|
-
"@pnpm/
|
|
113
|
-
"@pnpm/
|
|
114
|
-
"@pnpm/
|
|
115
|
-
"@pnpm/
|
|
116
|
-
"@pnpm/
|
|
117
|
-
"@pnpm/
|
|
118
|
-
"@pnpm/
|
|
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.
|
|
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.
|
|
145
|
-
"@pnpm/lockfile.types": "1100.0.
|
|
146
|
-
"@pnpm/installing.deps-installer": "1101.
|
|
147
|
-
"@pnpm/
|
|
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/
|
|
152
|
-
"@pnpm/
|
|
153
|
-
"@pnpm/
|
|
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/
|
|
157
|
-
"@pnpm/testing.registry-mock": "1100.0.
|
|
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"
|