From 7ab89343a2184eb6805002f5547ecfe7fc055ae4 Mon Sep 17 00:00:00 2001 From: Michael Sverdlov Date: Wed, 29 Jul 2026 12:18:19 +0300 Subject: [PATCH] fix: pass npm auth token when fetching from the registry getRegistry() returned only the registry URL, and every caller then used fetch() without auth headers, so any registry that requires authentication answered 401 - breaking SWC binary downloads, lockfile patching and next info on private/proxied registries. getRegistry() now also reads the registry's _authToken from .npmrc and returns { registry, authToken }; the three callers send it as a Bearer header when present. Same execSync-based npm config lookup already used for the URL, no new dependencies. Closes #59081 Co-Authored-By: Claude Opus 5 --- packages/next/src/cli/next-info.ts | 7 +- packages/next/src/lib/download-swc.ts | 8 +- packages/next/src/lib/helpers/get-registry.ts | 33 +++++++- .../next/src/lib/patch-incorrect-lockfile.ts | 10 ++- test/unit/get-registry.test.ts | 81 +++++++++++++++++++ 5 files changed, 130 insertions(+), 9 deletions(-) create mode 100644 test/unit/get-registry.test.ts diff --git a/packages/next/src/cli/next-info.ts b/packages/next/src/cli/next-info.ts index b04a51370cfc..a79ab66d6b73 100755 --- a/packages/next/src/cli/next-info.ts +++ b/packages/next/src/cli/next-info.ts @@ -102,8 +102,11 @@ async function printInfo() { let versionInfo try { - const registry = getRegistry() - const res = await fetch(`${registry}-/package/next/dist-tags`) + const { registry, authToken } = getRegistry() + const headers: HeadersInit = authToken + ? { Authorization: `Bearer ${authToken}` } + : {} + const res = await fetch(`${registry}-/package/next/dist-tags`, { headers }) const tags = await res.json() versionInfo = parseVersionInfo({ diff --git a/packages/next/src/lib/download-swc.ts b/packages/next/src/lib/download-swc.ts index d42d73832f01..ca253abf2aab 100644 --- a/packages/next/src/lib/download-swc.ts +++ b/packages/next/src/lib/download-swc.ts @@ -34,11 +34,15 @@ async function extractBinary( `${tarFileName}.temp-${Date.now()}` ) - const registry = getRegistry() + const { registry, authToken } = getRegistry() const downloadUrl = `${registry}${pkgName}/-/${tarFileName}` - await fetch(downloadUrl).then((res) => { + const headers: HeadersInit = authToken + ? { Authorization: `Bearer ${authToken}` } + : {} + + await fetch(downloadUrl, { headers }).then((res) => { const { ok, body } = res if (!ok || !body) { Log.error(`Failed to download swc package from ${downloadUrl}`) diff --git a/packages/next/src/lib/helpers/get-registry.ts b/packages/next/src/lib/helpers/get-registry.ts index 629e9b0d265b..6c039efa2037 100644 --- a/packages/next/src/lib/helpers/get-registry.ts +++ b/packages/next/src/lib/helpers/get-registry.ts @@ -7,7 +7,10 @@ import { getFormattedNodeOptionsWithoutInspect } from '../../server/lib/utils' * The URL will have a trailing slash. * @default https://registry.npmjs.org/ */ -export function getRegistry(baseDir: string = process.cwd()) { +export function getRegistry(baseDir: string = process.cwd()): { + registry: string + authToken?: string +} { const pkgManager = getPkgManager(baseDir) // Since `npm config` command fails in npm workspace to prevent workspace config conflicts, // add `--no-workspaces` flag to run under the context of the root project only. @@ -39,5 +42,31 @@ export function getRegistry(baseDir: string = process.cwd()) { }) } - return registry + // Read auth token for this registry from .npmrc, if configured. + let authToken: string | undefined + try { + const parsed = new URL(registry) + let scope = `//${parsed.host}${parsed.pathname}` + if (!scope.endsWith('/')) scope += '/' + + const output = execSync( + `${pkgManager} config get "${scope}:_authToken" ${resolvedFlags}`, + { + env: { + ...process.env, + NODE_OPTIONS: getFormattedNodeOptionsWithoutInspect(), + }, + } + ) + .toString() + .trim() + + if (output && output !== 'undefined') { + authToken = output + } + } catch { + // Auth token is not required — proceed without it + } + + return { registry, authToken } } diff --git a/packages/next/src/lib/patch-incorrect-lockfile.ts b/packages/next/src/lib/patch-incorrect-lockfile.ts index faf49a0a53a3..e175a5389e8e 100644 --- a/packages/next/src/lib/patch-incorrect-lockfile.ts +++ b/packages/next/src/lib/patch-incorrect-lockfile.ts @@ -7,11 +7,15 @@ import type { UnwrapPromise } from './coalesced-function' import { isCI } from '../server/ci-info' import { getRegistry } from './helpers/get-registry' -let registry: string | undefined +let registryConfig: ReturnType | undefined async function fetchPkgInfo(pkg: string) { - if (!registry) registry = getRegistry() - const res = await fetch(`${registry}${pkg}`) + if (!registryConfig) registryConfig = getRegistry() + const { registry, authToken } = registryConfig + const headers: HeadersInit = authToken + ? { Authorization: `Bearer ${authToken}` } + : {} + const res = await fetch(`${registry}${pkg}`, { headers }) if (!res.ok) { throw new Error( diff --git a/test/unit/get-registry.test.ts b/test/unit/get-registry.test.ts new file mode 100644 index 000000000000..b70feaf6f0a3 --- /dev/null +++ b/test/unit/get-registry.test.ts @@ -0,0 +1,81 @@ +/* eslint-env jest */ +import { execSync } from 'child_process' +import { getRegistry } from 'next/dist/lib/helpers/get-registry' + +jest.mock('child_process', () => ({ + execSync: jest.fn(), +})) + +jest.mock('next/dist/lib/helpers/get-pkg-manager', () => ({ + getPkgManager: jest.fn(() => 'npm'), +})) + +jest.mock('next/dist/server/lib/utils', () => ({ + getFormattedNodeOptionsWithoutInspect: jest.fn(() => ''), +})) + +const mockedExecSync = execSync as jest.MockedFunction + +describe('getRegistry', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should return registry URL and auth token', () => { + mockedExecSync + .mockReturnValueOnce(Buffer.from('https://registry.example.com\n')) + .mockReturnValueOnce(Buffer.from('my-secret-token\n')) + + const result = getRegistry() + expect(result.registry).toBe('https://registry.example.com/') + expect(result.authToken).toBe('my-secret-token') + }) + + it('should return undefined authToken when npm returns "undefined"', () => { + mockedExecSync + .mockReturnValueOnce(Buffer.from('https://registry.npmjs.org/\n')) + .mockReturnValueOnce(Buffer.from('undefined\n')) + + const result = getRegistry() + expect(result.registry).toBe('https://registry.npmjs.org/') + expect(result.authToken).toBeUndefined() + }) + + it('should return undefined authToken when config get throws', () => { + mockedExecSync + .mockReturnValueOnce(Buffer.from('https://registry.npmjs.org/\n')) + .mockImplementationOnce(() => { + throw new Error('not found') + }) + + const result = getRegistry() + expect(result.registry).toBe('https://registry.npmjs.org/') + expect(result.authToken).toBeUndefined() + }) + + it('should query the correct npmrc key for a registry with a path', () => { + mockedExecSync + .mockReturnValueOnce( + Buffer.from('https://my.jfrog.io/artifactory/api/npm/npm/\n') + ) + .mockReturnValueOnce(Buffer.from('my-token\n')) + + const result = getRegistry() + expect(result.authToken).toBe('my-token') + expect(mockedExecSync).toHaveBeenCalledWith( + expect.stringContaining( + '//my.jfrog.io/artifactory/api/npm/npm/:_authToken' + ), + expect.any(Object) + ) + }) + + it('should fall back to default registry when config get returns non-URL', () => { + mockedExecSync + .mockReturnValueOnce(Buffer.from('WARN something\n')) + .mockReturnValueOnce(Buffer.from('undefined\n')) + + const result = getRegistry() + expect(result.registry).toBe('https://registry.npmjs.org/') + }) +})