feat: allow using token helpers in pnpm publish (#7443)

Close #7316

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
Nacho Aldama
2024-01-08 01:24:57 +01:00
committed by GitHub
parent ff10acadec
commit 5a5e42551e
15 changed files with 147 additions and 8 deletions

View File

@@ -0,0 +1,5 @@
---
"@pnpm/run-npm": minor
---
Allow to set custom env.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/network.auth-header": minor
---
Export the loadToken function.

View File

@@ -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).

View File

@@ -5,6 +5,7 @@ import PATH from 'path-name'
export interface RunNPMOptions {
cwd?: string
env?: Record<string, string>
}
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<string, string>
}
) {
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'),

View File

@@ -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`)
}

View File

@@ -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<string, string>

3
pnpm-lock.yaml generated
View File

@@ -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:'

View File

@@ -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:*",

View File

@@ -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<PublishRecursiveOpts, 'rawConfig' | 'argv'>) {
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<string, string> = {}
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

View File

@@ -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,
}, [])
})

View File

@@ -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%

View File

@@ -0,0 +1,2 @@
#!/usr/bin/env node
console.log("Basic " + Buffer.from("password").toString("base64"));

View File

@@ -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%

View File

@@ -0,0 +1,2 @@
#!/usr/bin/env node
console.log("Bearer " + Buffer.from("password").toString("base64"));

View File

@@ -36,6 +36,9 @@
{
"path": "../../fs/packlist"
},
{
"path": "../../network/auth-header"
},
{
"path": "../../packages/error"
},