@pnpm/installing.deps-installer 1101.7.0 → 1101.9.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 +51 -109
- package/lib/install/link.js +1 -1
- package/lib/install/recordLockfileVerified.js +2 -1
- package/lib/install/verifyLockfileResolutions.d.ts +11 -0
- package/lib/install/verifyLockfileResolutions.js +104 -15
- package/lib/install/verifyLockfileResolutionsCache.d.ts +7 -0
- package/lib/install/verifyLockfileResolutionsCache.js +4 -2
- package/package.json +59 -59
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,17 +1,33 @@
|
|
|
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
|
|
8
8
|
// (e.g. a poisoned lockfile) doesn't flood the terminal / CI log; the full
|
|
9
9
|
// count is in the header and the remainder is summarized at the end.
|
|
10
10
|
const MAX_VIOLATIONS_TO_PRINT = 20;
|
|
11
|
-
//
|
|
12
|
-
// (Math.min(
|
|
11
|
+
// 64 mirrors the floor of pnpm's package-requester network-concurrency
|
|
12
|
+
// (Math.min(96, Math.max(workers*3, 64))); keep them aligned so the
|
|
13
13
|
// verification pass doesn't push past what the rest of the install respects.
|
|
14
|
-
const DEFAULT_CONCURRENCY =
|
|
14
|
+
const DEFAULT_CONCURRENCY = 64;
|
|
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,26 +77,44 @@ 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
|
-
if (result.hit)
|
|
83
|
+
if (result.hit) {
|
|
84
|
+
// A silent short-circuit looks like the policy gate never ran
|
|
85
|
+
// (pnpm/pnpm#12324), so surface the reused verdict — but only
|
|
86
|
+
// when policy verifiers are active; the shape-only run that
|
|
87
|
+
// every install performs stays quiet.
|
|
88
|
+
if (verifiers.length > 0) {
|
|
89
|
+
lockfileVerificationLogger.debug({
|
|
90
|
+
status: 'cached',
|
|
91
|
+
verifiedAt: result.verifiedAt,
|
|
92
|
+
lockfilePath: options?.lockfilePath,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
69
95
|
return;
|
|
96
|
+
}
|
|
70
97
|
cachePrecomputed = result.precomputed;
|
|
71
98
|
}
|
|
72
99
|
// Emit started/done around the actual verification pass — the
|
|
73
100
|
// round-trip can be slow on a cold registry cache, and the cached
|
|
74
|
-
// short-circuit above
|
|
75
|
-
// sees these messages on installs that are doing
|
|
101
|
+
// short-circuit above announces itself with its own `cached` event,
|
|
102
|
+
// so a user only sees these messages on installs that are doing
|
|
103
|
+
// real work.
|
|
76
104
|
// A degenerate lockfile where every snapshot fails the
|
|
77
105
|
// name/version extraction (so candidates is empty) skips emission
|
|
78
106
|
// entirely — no work, no noise.
|
|
79
|
-
const candidates = collectCandidates(lockfile);
|
|
107
|
+
const { candidates, shapeViolations } = collectCandidates(lockfile);
|
|
108
|
+
if (shapeViolations.length > 0) {
|
|
109
|
+
throw buildVerificationError(shapeViolations);
|
|
110
|
+
}
|
|
111
|
+
if (verifiers.length === 0)
|
|
112
|
+
return;
|
|
80
113
|
if (candidates.size === 0) {
|
|
81
114
|
if (cache) {
|
|
82
115
|
recordVerification(cache.cacheDir, {
|
|
83
116
|
lockfilePath: cache.lockfilePath,
|
|
84
|
-
verifiers,
|
|
117
|
+
verifiers: cacheVerifiers,
|
|
85
118
|
hashLockfile,
|
|
86
119
|
}, cachePrecomputed);
|
|
87
120
|
}
|
|
@@ -108,7 +141,7 @@ export async function verifyLockfileResolutions(lockfile, verifiers, options) {
|
|
|
108
141
|
if (cache) {
|
|
109
142
|
recordVerification(cache.cacheDir, {
|
|
110
143
|
lockfilePath: cache.lockfilePath,
|
|
111
|
-
verifiers,
|
|
144
|
+
verifiers: cacheVerifiers,
|
|
112
145
|
hashLockfile,
|
|
113
146
|
}, cachePrecomputed);
|
|
114
147
|
}
|
|
@@ -174,7 +207,48 @@ export async function collectResolutionPolicyViolations(lockfile, verifiers, opt
|
|
|
174
207
|
return [];
|
|
175
208
|
if (!lockfile.packages)
|
|
176
209
|
return [];
|
|
177
|
-
|
|
210
|
+
// Shape violations are deliberately not collected here: they are hard
|
|
211
|
+
// tampering failures, not policy picks a caller may auto-exclude.
|
|
212
|
+
return iterateLockfileViolations(collectCandidates(lockfile).candidates, verifiers, options?.concurrency);
|
|
213
|
+
}
|
|
214
|
+
function isRegistryShapedResolution(resolution) {
|
|
215
|
+
if (resolution == null)
|
|
216
|
+
return true;
|
|
217
|
+
if (typeof resolution !== 'object')
|
|
218
|
+
return false;
|
|
219
|
+
const { type, gitHosted, tarball, variants } = resolution;
|
|
220
|
+
if (type === 'variations') {
|
|
221
|
+
return Array.isArray(variants) && variants.every((variant) => isRegistryShapedResolution(variant?.resolution));
|
|
222
|
+
}
|
|
223
|
+
// Custom resolver protocols (`type: 'custom:*'`) are a legitimate
|
|
224
|
+
// non-registry source the user opted into. They can only be materialized by
|
|
225
|
+
// a project-configured custom fetcher — an unrecognized custom type throws at
|
|
226
|
+
// fetch time (see @pnpm/fetching.pick-fetcher) — so a forged custom type
|
|
227
|
+
// cannot launder an artifact past this gate into a build.
|
|
228
|
+
if (typeof type === 'string' && type.startsWith('custom:'))
|
|
229
|
+
return true;
|
|
230
|
+
if (type != null)
|
|
231
|
+
return false;
|
|
232
|
+
// Plain tarball / registry resolution. The lockfile is parsed from YAML
|
|
233
|
+
// without schema validation, so the `gitHosted` flag is not trustworthy on
|
|
234
|
+
// its own: a tampered entry could set a non-boolean (dodging a strict
|
|
235
|
+
// `=== true`) or an explicit `false` on a git-host URL (the loader only
|
|
236
|
+
// backfills the flag when absent). Treat any non-boolean flag as git-hosted
|
|
237
|
+
// and gate on the URL so the verdict never depends on the flag alone.
|
|
238
|
+
if (gitHosted != null && (typeof gitHosted !== 'boolean' || gitHosted))
|
|
239
|
+
return false;
|
|
240
|
+
// A registry resolution reconstructs its tarball URL from name+version, so
|
|
241
|
+
// an absent/empty `tarball` is registry-shaped. When a URL is present it
|
|
242
|
+
// must be an http(s) registry artifact: the npm verifier's tarball-URL
|
|
243
|
+
// binding skips non-http(s) schemes (file:, etc.), so a `file:` tarball
|
|
244
|
+
// under a name@semver key would otherwise be trusted with no safety net.
|
|
245
|
+
if (typeof tarball === 'string' && tarball !== '') {
|
|
246
|
+
if (!/^https?:\/\//i.test(tarball))
|
|
247
|
+
return false;
|
|
248
|
+
if (isGitHostedTarballUrl(tarball))
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
return true;
|
|
178
252
|
}
|
|
179
253
|
// depPath can include peer-dependency and patch_hash suffixes (e.g.
|
|
180
254
|
// `react@18.0.0(peer)(patch_hash=…)`); the same (name, version) pair may
|
|
@@ -191,10 +265,25 @@ export async function collectResolutionPolicyViolations(lockfile, verifiers, opt
|
|
|
191
265
|
// never checked.
|
|
192
266
|
function collectCandidates(lockfile) {
|
|
193
267
|
const candidates = new Map();
|
|
268
|
+
const shapeViolations = [];
|
|
194
269
|
for (const [depPath, snapshot] of Object.entries(lockfile.packages ?? {})) {
|
|
195
270
|
const { name, version, nonSemverVersion } = nameVerFromPkgSnapshot(depPath, snapshot);
|
|
196
271
|
if (!name || !version)
|
|
197
272
|
continue;
|
|
273
|
+
// A registry-style depPath (name@semver) must be backed by a
|
|
274
|
+
// registry-shaped resolution: the allowBuilds policy derives a
|
|
275
|
+
// trusted package identity from that key shape, which is only sound
|
|
276
|
+
// while this invariant holds. The check is offline, so it applies
|
|
277
|
+
// even when no policy verifiers are active.
|
|
278
|
+
if (nonSemverVersion == null && !isRegistryShapedResolution(snapshot.resolution)) {
|
|
279
|
+
shapeViolations.push({
|
|
280
|
+
name,
|
|
281
|
+
version,
|
|
282
|
+
resolution: snapshot.resolution,
|
|
283
|
+
code: RESOLUTION_SHAPE_MISMATCH_VIOLATION_CODE,
|
|
284
|
+
reason: 'a registry-style dependency path is backed by a non-registry resolution',
|
|
285
|
+
});
|
|
286
|
+
}
|
|
198
287
|
const key = `${name}@${version}@${nonSemverVersion ?? ''}@${JSON.stringify(snapshot.resolution)}`;
|
|
199
288
|
candidates.set(key, {
|
|
200
289
|
name,
|
|
@@ -203,7 +292,7 @@ function collectCandidates(lockfile) {
|
|
|
203
292
|
resolution: snapshot.resolution,
|
|
204
293
|
});
|
|
205
294
|
}
|
|
206
|
-
return candidates;
|
|
295
|
+
return { candidates, shapeViolations };
|
|
207
296
|
}
|
|
208
297
|
async function iterateLockfileViolations(candidates, verifiers, concurrency) {
|
|
209
298
|
const violations = [];
|
|
@@ -9,6 +9,13 @@ import type { ResolutionVerifier } from '@pnpm/resolving.resolver-base';
|
|
|
9
9
|
export type VerifierCacheIdentity = Pick<ResolutionVerifier, 'policy' | 'canTrustPastCheck'>;
|
|
10
10
|
export interface CacheLookupResult {
|
|
11
11
|
hit: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* ISO-8601 timestamp of the verification run the hit is reusing.
|
|
14
|
+
* Set only on a hit, and only when the record carries a usable
|
|
15
|
+
* timestamp (records written before the field existed normalize to
|
|
16
|
+
* an empty string and surface as `undefined` here).
|
|
17
|
+
*/
|
|
18
|
+
verifiedAt?: string;
|
|
12
19
|
/**
|
|
13
20
|
* stat + hash already computed during the lookup. When the caller
|
|
14
21
|
* follows up with {@link recordVerification} after running the gate,
|
|
@@ -159,8 +159,10 @@ export function tryLockfileVerificationCache(cacheDir, key) {
|
|
|
159
159
|
// hash without reading the file. Microseconds.
|
|
160
160
|
const byPathRecord = indexes.byPath.get(key.lockfilePath);
|
|
161
161
|
if (byPathRecord && statMatches(stat, byPathRecord.lockfile)) {
|
|
162
|
+
const hit = everyVerifierTrustsCachedRun(byPathRecord, key.verifiers);
|
|
162
163
|
return {
|
|
163
|
-
hit
|
|
164
|
+
hit,
|
|
165
|
+
verifiedAt: hit ? byPathRecord.verifiedAt || undefined : undefined,
|
|
164
166
|
// The stat-match implies the file content is unchanged since the
|
|
165
167
|
// cached record was written, so its hash is still correct. Pass
|
|
166
168
|
// it through to skip hashing on the miss-then-record path.
|
|
@@ -190,7 +192,7 @@ export function tryLockfileVerificationCache(cacheDir, key) {
|
|
|
190
192
|
...byHashRecord,
|
|
191
193
|
lockfile: { ...byHashRecord.lockfile, path: key.lockfilePath, size: stat.size, mtimeNs: stat.mtimeNs, inode: stat.inode },
|
|
192
194
|
});
|
|
193
|
-
return { hit: true, precomputed: { stat, hash } };
|
|
195
|
+
return { hit: true, verifiedAt: byHashRecord.verifiedAt || undefined, precomputed: { stat, hash } };
|
|
194
196
|
}
|
|
195
197
|
function everyVerifierTrustsCachedRun(record, verifiers) {
|
|
196
198
|
for (const verifier of verifiers) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/installing.deps-installer",
|
|
3
|
-
"version": "1101.
|
|
3
|
+
"version": "1101.9.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.
|
|
68
|
+
"@pnpm/bins.linker": "1100.0.13",
|
|
69
|
+
"@pnpm/building.after-install": "1101.0.21",
|
|
70
|
+
"@pnpm/building.during-install": "1101.0.18",
|
|
71
|
+
"@pnpm/building.policy": "1100.0.9",
|
|
71
72
|
"@pnpm/catalogs.resolver": "1100.0.0",
|
|
72
|
-
"@pnpm/config.matcher": "1100.0.1",
|
|
73
|
-
"@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",
|
|
78
73
|
"@pnpm/catalogs.types": "1100.0.0",
|
|
79
|
-
"@pnpm/
|
|
74
|
+
"@pnpm/catalogs.protocol-parser": "1100.0.0",
|
|
75
|
+
"@pnpm/config.parse-overrides": "1100.0.1",
|
|
80
76
|
"@pnpm/constants": "1100.0.0",
|
|
81
|
-
"@pnpm/
|
|
82
|
-
"@pnpm/
|
|
83
|
-
"@pnpm/
|
|
77
|
+
"@pnpm/config.matcher": "1100.0.1",
|
|
78
|
+
"@pnpm/config.normalize-registries": "1100.0.7",
|
|
79
|
+
"@pnpm/core-loggers": "1100.2.0",
|
|
80
|
+
"@pnpm/deps.graph-hasher": "1100.2.4",
|
|
81
|
+
"@pnpm/crypto.hash": "1100.0.1",
|
|
82
|
+
"@pnpm/deps.graph-sequencer": "1100.0.0",
|
|
84
83
|
"@pnpm/error": "1100.0.0",
|
|
85
|
-
"@pnpm/
|
|
86
|
-
"@pnpm/exec.lifecycle": "1100.0.
|
|
87
|
-
"@pnpm/
|
|
84
|
+
"@pnpm/deps.path": "1100.0.7",
|
|
85
|
+
"@pnpm/exec.lifecycle": "1100.0.17",
|
|
86
|
+
"@pnpm/fs.symlink-dependency": "1100.0.9",
|
|
87
|
+
"@pnpm/crypto.object-hasher": "1100.0.0",
|
|
88
|
+
"@pnpm/hooks.read-package-hook": "1100.0.7",
|
|
89
|
+
"@pnpm/hooks.types": "1100.0.11",
|
|
90
|
+
"@pnpm/installing.deps-resolver": "1100.2.2",
|
|
91
|
+
"@pnpm/bins.remover": "1100.0.9",
|
|
92
|
+
"@pnpm/installing.context": "1100.0.17",
|
|
93
|
+
"@pnpm/installing.linking.hoist": "1100.0.13",
|
|
94
|
+
"@pnpm/installing.deps-restorer": "1101.1.11",
|
|
95
|
+
"@pnpm/installing.linking.modules-cleaner": "1100.1.7",
|
|
96
|
+
"@pnpm/installing.linking.direct-dep-linker": "1100.0.9",
|
|
97
|
+
"@pnpm/installing.modules-yaml": "1100.0.8",
|
|
98
|
+
"@pnpm/lockfile.filtering": "1100.1.6",
|
|
99
|
+
"@pnpm/lockfile.fs": "1100.1.4",
|
|
100
|
+
"@pnpm/installing.package-requester": "1101.1.0",
|
|
101
|
+
"@pnpm/lockfile.preferred-versions": "1100.0.15",
|
|
88
102
|
"@pnpm/fs.read-modules-dir": "1100.0.1",
|
|
89
|
-
"@pnpm/
|
|
90
|
-
"@pnpm/
|
|
91
|
-
"@pnpm/
|
|
92
|
-
"@pnpm/
|
|
93
|
-
"@pnpm/
|
|
94
|
-
"@pnpm/
|
|
95
|
-
"@pnpm/
|
|
96
|
-
"@pnpm/
|
|
97
|
-
"@pnpm/
|
|
98
|
-
"@pnpm/
|
|
99
|
-
"@pnpm/
|
|
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",
|
|
103
|
+
"@pnpm/lockfile.settings-checker": "1100.0.17",
|
|
104
|
+
"@pnpm/lockfile.to-pnp": "1100.0.13",
|
|
105
|
+
"@pnpm/lockfile.pruner": "1100.0.10",
|
|
106
|
+
"@pnpm/lockfile.utils": "1100.0.12",
|
|
107
|
+
"@pnpm/lockfile.walker": "1100.0.10",
|
|
108
|
+
"@pnpm/network.auth-header": "1101.1.1",
|
|
109
|
+
"@pnpm/lockfile.verification": "1100.0.17",
|
|
110
|
+
"@pnpm/patching.config": "1100.0.7",
|
|
111
|
+
"@pnpm/pnpr.client": "1.2.0",
|
|
112
|
+
"@pnpm/pkg-manifest.utils": "1100.2.4",
|
|
113
|
+
"@pnpm/store.controller-types": "1100.1.4",
|
|
108
114
|
"@pnpm/store.index": "1100.1.0",
|
|
109
|
-
"@pnpm/resolving.resolver-base": "1100.4.
|
|
115
|
+
"@pnpm/resolving.resolver-base": "1100.4.1",
|
|
116
|
+
"@pnpm/types": "1101.3.1",
|
|
110
117
|
"@pnpm/resolving.parse-wanted-dependency": "1100.0.1",
|
|
111
|
-
"@pnpm/workspace.project-manifest-reader": "1100.0.
|
|
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"
|
|
118
|
+
"@pnpm/workspace.project-manifest-reader": "1100.0.12"
|
|
119
119
|
},
|
|
120
120
|
"peerDependencies": {
|
|
121
121
|
"@pnpm/logger": "^1001.0.1",
|
|
122
|
-
"@pnpm/worker": "^1100.1.
|
|
122
|
+
"@pnpm/worker": "^1100.1.11"
|
|
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/
|
|
146
|
-
"@pnpm/installing.deps-installer": "1101.
|
|
147
|
-
"@pnpm/prepare": "1100.0.13",
|
|
148
|
-
"@pnpm/pkg-manifest.reader": "1100.0.6",
|
|
149
|
-
"@pnpm/network.git-utils": "1100.0.1",
|
|
144
|
+
"@pnpm/assert-project": "1100.0.15",
|
|
145
|
+
"@pnpm/assert-store": "1100.0.15",
|
|
146
|
+
"@pnpm/installing.deps-installer": "1101.9.0",
|
|
150
147
|
"@pnpm/logger": "1100.0.0",
|
|
151
|
-
"@pnpm/
|
|
152
|
-
"@pnpm/
|
|
153
|
-
"@pnpm/
|
|
154
|
-
"@pnpm/
|
|
148
|
+
"@pnpm/network.git-utils": "1100.0.1",
|
|
149
|
+
"@pnpm/lockfile.types": "1100.0.10",
|
|
150
|
+
"@pnpm/resolving.registry.types": "1100.1.2",
|
|
151
|
+
"@pnpm/pkg-manifest.reader": "1100.0.7",
|
|
152
|
+
"@pnpm/store.cafs": "1100.1.9",
|
|
155
153
|
"@pnpm/store.path": "1100.0.1",
|
|
154
|
+
"@pnpm/prepare": "1100.0.15",
|
|
155
|
+
"@pnpm/test-ipc-server": "1100.0.0",
|
|
156
156
|
"@pnpm/test-fixtures": "1100.0.0",
|
|
157
|
-
"@pnpm/testing.
|
|
158
|
-
"@pnpm/testing.temp-store": "1100.1.
|
|
159
|
-
"@pnpm/
|
|
157
|
+
"@pnpm/testing.mock-agent": "1101.0.2",
|
|
158
|
+
"@pnpm/testing.temp-store": "1100.1.8",
|
|
159
|
+
"@pnpm/testing.registry-mock": "1100.0.5"
|
|
160
160
|
},
|
|
161
161
|
"engines": {
|
|
162
162
|
"node": ">=22.13"
|