@pnpm/engine.runtime.node-resolver 1101.2.3 → 1101.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @pnpm/node.resolver
2
2
 
3
+ ## 1101.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Authenticate Node.js runtime downloads from `nodeDownloadMirrors` with URL-scoped npm registry credentials, including bearer tokens, basic auth, and `tokenHelper` [pnpm/pnpm#14334](https://github.com/pnpm/pnpm/issues/14334).
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies:
12
+ - @pnpm/config.reader@1102.1.1
13
+ - @pnpm/crypto.shasums-file@1100.2.3
14
+ - @pnpm/error@1100.1.4
15
+
3
16
  ## 1101.2.3
4
17
 
5
18
  ### Patch Changes
package/lib/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { FetchFromRegistry } from '@pnpm/fetching.types';
1
+ import type { FetchFromRegistry, GetAuthHeader } from '@pnpm/fetching.types';
2
2
  import type { LatestInfo, LatestQuery, ResolveOptions, ResolveResult, VariationsResolution, WantedDependency } from '@pnpm/resolving.resolver-base';
3
3
  import { getNodeArtifactAddress } from './getNodeArtifactAddress.js';
4
4
  import { getNodeMirror } from './getNodeMirror.js';
@@ -13,13 +13,19 @@ export interface NodeRuntimeResolveResult extends ResolveResult {
13
13
  }
14
14
  export declare function resolveNodeRuntime(ctx: {
15
15
  fetchFromRegistry: FetchFromRegistry;
16
+ getAuthHeader?: GetAuthHeader;
16
17
  nodeDownloadMirrors?: Record<string, string>;
17
18
  offline?: boolean;
18
19
  cacheDir?: string;
19
20
  }, wantedDependency: WantedDependency, opts?: Partial<ResolveOptions>): Promise<NodeRuntimeResolveResult | null>;
20
21
  export declare function resolveLatestNodeRuntime(ctx: {
21
22
  fetchFromRegistry: FetchFromRegistry;
23
+ getAuthHeader?: GetAuthHeader;
22
24
  nodeDownloadMirrors?: Record<string, string>;
23
25
  }, query: LatestQuery, _opts: ResolveOptions): Promise<LatestInfo | undefined>;
24
- export declare function resolveNodeVersion(fetch: FetchFromRegistry, versionSpec: string, nodeMirrorBaseUrl?: string): Promise<string | null>;
25
- export declare function resolveNodeVersions(fetch: FetchFromRegistry, versionSpec?: string, nodeMirrorBaseUrl?: string): Promise<string[]>;
26
+ export declare function resolveNodeVersion(fetch: FetchFromRegistry, versionSpec: string, opts?: string | NodeVersionFetchOptions): Promise<string | null>;
27
+ export declare function resolveNodeVersions(fetch: FetchFromRegistry, versionSpec?: string, opts?: string | NodeVersionFetchOptions): Promise<string[]>;
28
+ export interface NodeVersionFetchOptions {
29
+ nodeMirrorBaseUrl?: string;
30
+ getAuthHeader?: GetAuthHeader;
31
+ }
package/lib/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { isIP } from 'node:net';
1
2
  import { fetchShasumsFileCached, fetchVerifiedNodeShasumsFileCached } from '@pnpm/crypto.shasums-file';
2
3
  import { PnpmError } from '@pnpm/error';
3
4
  import semver from 'semver';
@@ -25,6 +26,7 @@ export async function resolveNodeRuntime(ctx, wantedDependency, opts) {
25
26
  }
26
27
  if (ctx.offline)
27
28
  throw new PnpmError('NO_OFFLINE_NODEJS_RESOLUTION', 'Offline Node.js resolution is not supported');
29
+ const fetch = createAuthenticatedFetch(ctx.fetchFromRegistry, ctx.getAuthHeader);
28
30
  const versionSpec = normalizeRuntimeSpec(wantedDependency.bareSpecifier.substring('runtime:'.length));
29
31
  const { releaseChannel, versionSpecifier } = parseNodeSpecifier(versionSpec);
30
32
  const nodeMirrorBaseUrl = getNodeMirror(ctx.nodeDownloadMirrors, releaseChannel);
@@ -34,14 +36,14 @@ export async function resolveNodeRuntime(ctx, wantedDependency, opts) {
34
36
  const exactVersion = exactReleaseVersion(releaseChannel, versionSpecifier);
35
37
  let version = exactVersion;
36
38
  if (version == null) {
37
- version = await resolveNodeVersion(ctx.fetchFromRegistry, versionSpecifier, nodeMirrorBaseUrl) ?? undefined;
39
+ version = await resolveNodeVersion(fetch, versionSpecifier, nodeMirrorBaseUrl) ?? undefined;
38
40
  if (!version) {
39
41
  throw new PnpmError('NODEJS_VERSION_NOT_FOUND', `Could not find a Node.js version that satisfies ${versionSpec}`);
40
42
  }
41
43
  }
42
44
  let variants;
43
45
  try {
44
- variants = await readNodeAssets(ctx.fetchFromRegistry, { nodeMirrorBaseUrl, version, releaseChannel, cacheDir: ctx.cacheDir });
46
+ variants = await readNodeAssets(fetch, { nodeMirrorBaseUrl, version, releaseChannel, cacheDir: ctx.cacheDir, getAuthHeader: ctx.getAuthHeader });
45
47
  }
46
48
  catch (err) {
47
49
  // The exact-specifier pick skipped the release index, so a failed asset
@@ -49,7 +51,7 @@ export async function resolveNodeRuntime(ctx, wantedDependency, opts) {
49
51
  // now, purely to raise the same NODEJS_VERSION_NOT_FOUND the index-first
50
52
  // path raises for a nonexistent version; any other outcome re-raises the
51
53
  // asset error unchanged.
52
- if (exactVersion != null && await versionMissingFromIndex(ctx.fetchFromRegistry, exactVersion, nodeMirrorBaseUrl)) {
54
+ if (exactVersion != null && await versionMissingFromIndex(fetch, exactVersion, nodeMirrorBaseUrl)) {
53
55
  throw new PnpmError('NODEJS_VERSION_NOT_FOUND', `Could not find a Node.js version that satisfies ${versionSpec}`);
54
56
  }
55
57
  throw err;
@@ -77,7 +79,10 @@ export async function resolveLatestNodeRuntime(ctx, query, _opts) {
77
79
  const versionSpec = query.compatible ? normalizeRuntimeSpec(manifestSpec.substring('runtime:'.length)) : 'latest';
78
80
  const { releaseChannel, versionSpecifier } = parseNodeSpecifier(versionSpec);
79
81
  const nodeMirrorBaseUrl = getNodeMirror(ctx.nodeDownloadMirrors, releaseChannel);
80
- const version = await resolveNodeVersion(ctx.fetchFromRegistry, versionSpecifier, nodeMirrorBaseUrl);
82
+ const version = await resolveNodeVersion(ctx.fetchFromRegistry, versionSpecifier, {
83
+ nodeMirrorBaseUrl,
84
+ getAuthHeader: ctx.getAuthHeader,
85
+ });
81
86
  if (!version)
82
87
  return {};
83
88
  return { latestManifest: { name: 'node', version } };
@@ -120,20 +125,20 @@ async function versionMissingFromIndex(fetch, version, nodeMirrorBaseUrl) {
120
125
  }
121
126
  }
122
127
  async function readNodeAssets(fetch, opts) {
123
- const { nodeMirrorBaseUrl, version, releaseChannel, cacheDir } = opts;
128
+ const { nodeMirrorBaseUrl, version, releaseChannel, cacheDir, getAuthHeader } = opts;
124
129
  // The mirror is repository-configurable, so the SHASUMS file's hashes are only
125
130
  // trustworthy once its OpenPGP signature is verified against the Node.js
126
131
  // release keys embedded in pnpm. Only the `release` channel publishes a signed
127
132
  // SHASUMS256.txt; pre-release channels (rc, nightly, …) are unsigned by Node,
128
133
  // so they cannot be verified this way.
129
- const assets = await readNodeAssetsFromMirror(fetch, { nodeMirrorBaseUrl, version, muslOnly: false, verifySignature: releaseChannel === 'release', cacheDir });
134
+ const assets = await readNodeAssetsFromMirror(fetch, { nodeMirrorBaseUrl, version, muslOnly: false, verifySignature: releaseChannel === 'release', cacheDir, getAuthHeader });
130
135
  // When using the default mirror, also fetch musl variants from unofficial-builds.nodejs.org,
131
136
  // since musl builds are not available on the official mirror. That URL is hardcoded (not
132
137
  // repository-configurable) and signed by a different (unofficial-builds) key, so it is trusted
133
138
  // over TLS rather than verified against the official release keys.
134
139
  if (nodeMirrorBaseUrl === DEFAULT_NODE_MIRROR_BASE_URL) {
135
140
  try {
136
- const muslAssets = await readNodeAssetsFromMirror(fetch, { nodeMirrorBaseUrl: UNOFFICIAL_NODE_MIRROR_BASE_URL, version, muslOnly: true, verifySignature: false, cacheDir });
141
+ const muslAssets = await readNodeAssetsFromMirror(fetch, { nodeMirrorBaseUrl: UNOFFICIAL_NODE_MIRROR_BASE_URL, version, muslOnly: true, verifySignature: false, cacheDir, getAuthHeader });
137
142
  assets.push(...muslAssets);
138
143
  }
139
144
  catch {
@@ -143,13 +148,18 @@ async function readNodeAssets(fetch, opts) {
143
148
  return assets;
144
149
  }
145
150
  async function readNodeAssetsFromMirror(fetch, opts) {
146
- const { nodeMirrorBaseUrl, version, muslOnly, verifySignature, cacheDir } = opts;
151
+ const { nodeMirrorBaseUrl, version, muslOnly, verifySignature, getAuthHeader } = opts;
147
152
  // The URL is pinned to one released version, which is what makes it
148
153
  // eligible for the SHASUMS disk cache.
149
154
  const integritiesFileUrl = `${nodeMirrorBaseUrl}v${version}/SHASUMS256.txt`;
155
+ const cacheOpts = {
156
+ cacheDir: opts.cacheDir,
157
+ skipCache: getSecureAuthHeader(getAuthHeader, integritiesFileUrl) != null ||
158
+ getSecureAuthHeader(getAuthHeader, `${integritiesFileUrl}.sig`) != null,
159
+ };
150
160
  const shasumsFileItems = verifySignature
151
- ? await fetchVerifiedNodeShasumsFileCached(fetch, integritiesFileUrl, { cacheDir })
152
- : await fetchShasumsFileCached(fetch, integritiesFileUrl, { cacheDir });
161
+ ? await fetchVerifiedNodeShasumsFileCached(fetch, integritiesFileUrl, cacheOpts)
162
+ : await fetchShasumsFileCached(fetch, integritiesFileUrl, cacheOpts);
153
163
  const escaped = version.replace(/\\/g, '\\\\').replace(/\./g, '\\.');
154
164
  // The second capture group uses [^.-]+ to stop at a dash, so that the optional
155
165
  // third group can capture the '-musl' suffix separately (e.g. 'x64' + '-musl').
@@ -201,8 +211,10 @@ const SEMVER_OPTS = {
201
211
  includePrerelease: true,
202
212
  loose: true,
203
213
  };
204
- export async function resolveNodeVersion(fetch, versionSpec, nodeMirrorBaseUrl) {
205
- const allVersions = await fetchAllVersions(fetch, nodeMirrorBaseUrl);
214
+ const MAX_NODE_MIRROR_REDIRECTS = 20;
215
+ export async function resolveNodeVersion(fetch, versionSpec, opts) {
216
+ const { nodeMirrorBaseUrl, getAuthHeader } = normalizeNodeVersionFetchOptions(opts);
217
+ const allVersions = await fetchAllVersions(createAuthenticatedFetch(fetch, getAuthHeader), nodeMirrorBaseUrl);
206
218
  versionSpec = normalizeRuntimeSpec(versionSpec);
207
219
  if (versionSpec === 'latest') {
208
220
  return allVersions[0].version;
@@ -210,8 +222,9 @@ export async function resolveNodeVersion(fetch, versionSpec, nodeMirrorBaseUrl)
210
222
  const { versions, versionRange } = filterVersions(allVersions, versionSpec);
211
223
  return semver.maxSatisfying(versions, versionRange, SEMVER_OPTS) ?? null;
212
224
  }
213
- export async function resolveNodeVersions(fetch, versionSpec, nodeMirrorBaseUrl) {
214
- const allVersions = await fetchAllVersions(fetch, nodeMirrorBaseUrl);
225
+ export async function resolveNodeVersions(fetch, versionSpec, opts) {
226
+ const { nodeMirrorBaseUrl, getAuthHeader } = normalizeNodeVersionFetchOptions(opts);
227
+ const allVersions = await fetchAllVersions(createAuthenticatedFetch(fetch, getAuthHeader), nodeMirrorBaseUrl);
215
228
  if (versionSpec == null) {
216
229
  return allVersions.map(({ version }) => version);
217
230
  }
@@ -233,6 +246,46 @@ async function fetchAllVersions(fetch, nodeMirrorBaseUrl) {
233
246
  lts,
234
247
  }));
235
248
  }
249
+ function normalizeNodeVersionFetchOptions(opts) {
250
+ return typeof opts === 'string' ? { nodeMirrorBaseUrl: opts } : opts ?? {};
251
+ }
252
+ function createAuthenticatedFetch(fetch, getAuthHeader) {
253
+ if (getAuthHeader == null)
254
+ return fetch;
255
+ return async (url, opts) => {
256
+ let currentUrl = url;
257
+ for (let redirectCount = 0;; redirectCount++) {
258
+ // eslint-disable-next-line no-await-in-loop
259
+ const response = await fetch(currentUrl, {
260
+ ...opts,
261
+ authHeaderValue: getSecureAuthHeader(getAuthHeader, currentUrl),
262
+ redirect: 'manual',
263
+ });
264
+ if (opts?.redirect === 'manual' || !isRedirectStatus(response.status) || redirectCount === MAX_NODE_MIRROR_REDIRECTS) {
265
+ return response;
266
+ }
267
+ const location = response.headers.get('location');
268
+ if (location == null)
269
+ return response;
270
+ currentUrl = new URL(location, currentUrl).toString();
271
+ }
272
+ };
273
+ }
274
+ function getSecureAuthHeader(getAuthHeader, url) {
275
+ const authHeaderValue = getAuthHeader?.(url);
276
+ if (authHeaderValue == null)
277
+ return undefined;
278
+ const parsed = new URL(url);
279
+ if (parsed.protocol === 'https:' || isLoopbackHost(parsed.hostname))
280
+ return authHeaderValue;
281
+ return undefined;
282
+ }
283
+ function isLoopbackHost(hostname) {
284
+ return hostname === 'localhost' || hostname === '[::1]' || (isIP(hostname) === 4 && hostname.startsWith('127.'));
285
+ }
286
+ function isRedirectStatus(status) {
287
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
288
+ }
236
289
  function getNodeBinsForCurrentOS(platform = process.platform) {
237
290
  if (platform === 'win32') {
238
291
  return { node: 'node.exe' };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/engine.runtime.node-resolver",
3
- "version": "1101.2.3",
3
+ "version": "1101.3.0",
4
4
  "description": "Resolves a Node.js version specifier to an exact Node.js version",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -29,9 +29,9 @@
29
29
  "!*.map"
30
30
  ],
31
31
  "dependencies": {
32
- "@pnpm/config.reader": "1102.1.0",
33
- "@pnpm/crypto.shasums-file": "1100.2.2",
34
- "@pnpm/error": "1100.1.3",
32
+ "@pnpm/config.reader": "1102.1.1",
33
+ "@pnpm/crypto.shasums-file": "1100.2.3",
34
+ "@pnpm/error": "1100.1.4",
35
35
  "@pnpm/fetching.types": "1100.0.3",
36
36
  "@pnpm/resolving.resolver-base": "1101.2.0",
37
37
  "@pnpm/types": "1102.1.0",
@@ -40,8 +40,8 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "@jest/globals": "30.4.1",
43
- "@pnpm/engine.runtime.node-resolver": "1101.2.3",
44
- "@pnpm/network.fetch": "1100.1.14",
43
+ "@pnpm/engine.runtime.node-resolver": "1101.3.0",
44
+ "@pnpm/network.fetch": "1100.1.15",
45
45
  "@types/semver": "7.8.0"
46
46
  },
47
47
  "engines": {