diff --git a/.changeset/gentle-jars-check.md b/.changeset/gentle-jars-check.md new file mode 100644 index 0000000000..5bd8691dfc --- /dev/null +++ b/.changeset/gentle-jars-check.md @@ -0,0 +1,5 @@ +--- +"@pnpm/run-npm": minor +--- + +Allow to set custom env. diff --git a/.changeset/hot-lies-invite.md b/.changeset/hot-lies-invite.md new file mode 100644 index 0000000000..1da1cc622f --- /dev/null +++ b/.changeset/hot-lies-invite.md @@ -0,0 +1,5 @@ +--- +"@pnpm/network.auth-header": minor +--- + +Export the loadToken function. diff --git a/.changeset/shiny-coins-walk.md b/.changeset/shiny-coins-walk.md new file mode 100644 index 0000000000..7da9e3513f --- /dev/null +++ b/.changeset/shiny-coins-walk.md @@ -0,0 +1,7 @@ +--- +"@pnpm/plugin-commands-publishing": minor +"@pnpm/run-npm": minor +"pnpm": patch +--- + +Allow using token helpers in `pnpm publish` [#7316](https://github.com/pnpm/pnpm/issues/7316). diff --git a/exec/run-npm/src/index.ts b/exec/run-npm/src/index.ts index 9b15a2f9c4..fb023ccbf5 100644 --- a/exec/run-npm/src/index.ts +++ b/exec/run-npm/src/index.ts @@ -5,6 +5,7 @@ import PATH from 'path-name' export interface RunNPMOptions { cwd?: string + env?: Record } export function runNpm (npmPath: string | undefined, args: string[], options?: RunNPMOptions) { @@ -13,6 +14,7 @@ export function runNpm (npmPath: string | undefined, args: string[], options?: R cwd: options?.cwd ?? process.cwd(), stdio: 'inherit', userAgent: undefined, + env: options?.env ?? {}, }) } @@ -23,12 +25,17 @@ export function runScriptSync ( cwd: string stdio: childProcess.StdioOptions userAgent?: string + env: Record } ) { - opts = Object.assign({}, opts) - const result = spawn.sync(command, args, Object.assign({}, opts, { - env: createEnv(opts), - })) + const env = { + ...createEnv(opts), + ...opts.env, + } + const result = spawn.sync(command, args, { + ...opts, + env, + }) if (result.error) throw result.error return result } @@ -39,7 +46,7 @@ function createEnv ( userAgent?: string } ) { - const env = Object.create(process.env) + const env = { ...process.env } env[PATH] = [ path.join(opts.cwd, 'node_modules', '.bin'), diff --git a/network/auth-header/src/getAuthHeadersFromConfig.ts b/network/auth-header/src/getAuthHeadersFromConfig.ts index 64a5871bd6..f0db7e0a32 100644 --- a/network/auth-header/src/getAuthHeadersFromConfig.ts +++ b/network/auth-header/src/getAuthHeadersFromConfig.ts @@ -57,7 +57,7 @@ function splitKey (key: string) { return [key.slice(0, index), key.slice(index + 1)] } -function loadToken (helperPath: string, settingName: string) { +export function loadToken (helperPath: string, settingName: string) { if (!path.isAbsolute(helperPath) || !fs.existsSync(helperPath)) { throw new PnpmError('BAD_TOKEN_HELPER_PATH', `${settingName} must be an absolute path, without arguments`) } diff --git a/network/auth-header/src/index.ts b/network/auth-header/src/index.ts index 3ed56239f4..d77cfac717 100644 --- a/network/auth-header/src/index.ts +++ b/network/auth-header/src/index.ts @@ -1,7 +1,11 @@ import nerfDart from 'nerf-dart' -import { getAuthHeadersFromConfig } from './getAuthHeadersFromConfig' +import { getAuthHeadersFromConfig, loadToken } from './getAuthHeadersFromConfig' import { removePort } from './helpers/removePort' +export { + loadToken, +} + export function createGetAuthHeaderByURI ( opts: { allSettings: Record diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ffa12716b..9945f30451 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4856,6 +4856,9 @@ importers: '@pnpm/filter-workspace-packages': specifier: workspace:* version: link:../../workspace/filter-workspace-packages + '@pnpm/network.auth-header': + specifier: workspace:* + version: link:../../network/auth-header '@pnpm/plugin-commands-publishing': specifier: workspace:* version: 'link:' diff --git a/releasing/plugin-commands-publishing/package.json b/releasing/plugin-commands-publishing/package.json index 893dfc7833..ad9afd6548 100644 --- a/releasing/plugin-commands-publishing/package.json +++ b/releasing/plugin-commands-publishing/package.json @@ -34,6 +34,7 @@ "devDependencies": { "@pnpm/filter-workspace-packages": "workspace:*", "@pnpm/plugin-commands-publishing": "workspace:*", + "@pnpm/network.auth-header": "workspace:*", "@pnpm/prepare": "workspace:*", "@pnpm/registry-mock": "3.17.1", "@pnpm/test-ipc-server": "workspace:*", diff --git a/releasing/plugin-commands-publishing/src/publish.ts b/releasing/plugin-commands-publishing/src/publish.ts index 2a600e61ab..ffd10e5222 100644 --- a/releasing/plugin-commands-publishing/src/publish.ts +++ b/releasing/plugin-commands-publishing/src/publish.ts @@ -8,6 +8,7 @@ import { runLifecycleHook, type RunLifecycleHookOptions } from '@pnpm/lifecycle' import { runNpm } from '@pnpm/run-npm' import { type ProjectManifest } from '@pnpm/types' import { getCurrentBranch, isGitRepo, isRemoteHistoryClean, isWorkingTreeClean } from '@pnpm/git-utils' +import { loadToken } from '@pnpm/network.auth-header' import { prompt } from 'enquirer' import rimraf from '@zkochan/rimraf' import pick from 'ramda/src/pick' @@ -235,6 +236,7 @@ Do you want to continue?`, await copyNpmrc({ dir, workspaceDir: opts.workspaceDir, packDestination }) const { status } = runNpm(opts.npmPath, ['publish', '--ignore-scripts', path.basename(tarballName), ...args], { cwd: packDestination, + env: getEnvWithTokens(opts), }) await rimraf(packDestination) @@ -250,6 +252,31 @@ Do you want to continue?`, return { manifest } } +/** + * The npm CLI doesn't support token helpers, so we transform the token helper settings + * to regular auth token settings that the npm CLI can understand. + */ +function getEnvWithTokens (opts: Pick) { + const tokenHelpers = Object.entries(opts.rawConfig).filter(([key]) => key.endsWith(':tokenHelper')) + const tokenHelpersFromArgs = opts.argv.original + .filter(arg => arg.includes(':tokenHelper=')) + .map(arg => arg.split('=', 2) as [string, string]) + + const env: Record = {} + for (const [key, helperPath] of tokenHelpers.concat(tokenHelpersFromArgs)) { + const authHeader = loadToken(helperPath, key) + const authType = authHeader.startsWith('Bearer') + ? '_authToken' + : '_auth' + + const registry = key.replace(/:tokenHelper$/, '') + env[`NPM_CONFIG_${registry}:${authType}`] = authType === '_authToken' + ? authHeader.slice('Bearer '.length) + : authHeader.replace(/Basic /i, '') + } + return env +} + async function copyNpmrc ( { dir, workspaceDir, packDestination }: { dir: string diff --git a/releasing/plugin-commands-publishing/test/publish.ts b/releasing/plugin-commands-publishing/test/publish.ts index 42fc689333..004eb7929a 100644 --- a/releasing/plugin-commands-publishing/test/publish.ts +++ b/releasing/plugin-commands-publishing/test/publish.ts @@ -1,4 +1,4 @@ -import { existsSync, promises as fs } from 'fs' +import { chmodSync, existsSync, promises as fs } from 'fs' import path from 'path' import execa from 'execa' import { isCI } from 'ci-info' @@ -734,3 +734,60 @@ test('publish: provenance', async () => { dir: process.cwd(), }, []) }) + +test('publish: use basic token helper for authentication', async () => { + prepare({ + name: 'test-publish-helper-token-basic.json', + version: '0.0.2', + }) + + const os = process.platform + const file = os === 'win32' + ? 'tokenHelperBasic.bat' + : 'tokenHelperBasic.js' + + const tokenHelper = path.join(__dirname, 'utils', file) + + chmodSync(tokenHelper, 0o755) + + await publish.handler({ + ...DEFAULT_OPTS, + argv: { + original: [ + 'publish', + CREDENTIALS[0], + `--//localhost:${REGISTRY_MOCK_PORT}/:tokenHelper=${tokenHelper}`, + ], + }, + dir: process.cwd(), + gitChecks: false, + }, []) +}) + +test('publish: use bearer token helper for authentication', async () => { + prepare({ + name: 'test-publish-helper-token-bearer.json', + version: '0.0.2', + }) + + const os = process.platform + const file = os === 'win32' + ? 'tokenHelperBearer.bat' + : 'tokenHelperBearer.js' + const tokenHelper = path.join(__dirname, 'utils', file) + + chmodSync(tokenHelper, 0o755) + + await publish.handler({ + ...DEFAULT_OPTS, + argv: { + original: [ + 'publish', + CREDENTIALS[0], + `--//localhost:${REGISTRY_MOCK_PORT}/:tokenHelper=${tokenHelper}`, + ], + }, + dir: process.cwd(), + gitChecks: false, + }, []) +}) diff --git a/releasing/plugin-commands-publishing/test/utils/tokenHelperBasic.bat b/releasing/plugin-commands-publishing/test/utils/tokenHelperBasic.bat new file mode 100644 index 0000000000..9c23de654f --- /dev/null +++ b/releasing/plugin-commands-publishing/test/utils/tokenHelperBasic.bat @@ -0,0 +1,8 @@ +@echo off +setlocal enabledelayedexpansion + +set "PASSWORD=password" + +for /f "delims=" %%i in ('powershell -Command "[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes('!PASSWORD!'))"') do set ENCODED=%%i + +echo Basic %ENCODED% diff --git a/releasing/plugin-commands-publishing/test/utils/tokenHelperBasic.js b/releasing/plugin-commands-publishing/test/utils/tokenHelperBasic.js new file mode 100755 index 0000000000..b79887af40 --- /dev/null +++ b/releasing/plugin-commands-publishing/test/utils/tokenHelperBasic.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +console.log("Basic " + Buffer.from("password").toString("base64")); diff --git a/releasing/plugin-commands-publishing/test/utils/tokenHelperBearer.bat b/releasing/plugin-commands-publishing/test/utils/tokenHelperBearer.bat new file mode 100644 index 0000000000..ed89a678b6 --- /dev/null +++ b/releasing/plugin-commands-publishing/test/utils/tokenHelperBearer.bat @@ -0,0 +1,8 @@ +@echo off +setlocal enabledelayedexpansion + +set "PASSWORD=password" + +for /f "delims=" %%i in ('powershell -Command "[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes('!PASSWORD!'))"') do set ENCODED=%%i + +echo Bearer %ENCODED% diff --git a/releasing/plugin-commands-publishing/test/utils/tokenHelperBearer.js b/releasing/plugin-commands-publishing/test/utils/tokenHelperBearer.js new file mode 100755 index 0000000000..a342a236ed --- /dev/null +++ b/releasing/plugin-commands-publishing/test/utils/tokenHelperBearer.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +console.log("Bearer " + Buffer.from("password").toString("base64")); diff --git a/releasing/plugin-commands-publishing/tsconfig.json b/releasing/plugin-commands-publishing/tsconfig.json index e4a4658d6e..b2d762e41c 100644 --- a/releasing/plugin-commands-publishing/tsconfig.json +++ b/releasing/plugin-commands-publishing/tsconfig.json @@ -36,6 +36,9 @@ { "path": "../../fs/packlist" }, + { + "path": "../../network/auth-header" + }, { "path": "../../packages/error" },