Skip to content

Commit

Permalink
feat: allow using token helpers in pnpm publish (#7443)
Browse files Browse the repository at this point in the history
Close #7316

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
  • Loading branch information
nachoaldamav and zkochan committed Jan 8, 2024
1 parent ff10aca commit 5a5e425
Show file tree
Hide file tree
Showing 15 changed files with 147 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/gentle-jars-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pnpm/run-npm": minor
---

Allow to set custom env.
5 changes: 5 additions & 0 deletions .changeset/hot-lies-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pnpm/network.auth-header": minor
---

Export the loadToken function.
7 changes: 7 additions & 0 deletions .changeset/shiny-coins-walk.md
Original file line number Diff line number Diff line change
@@ -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).
17 changes: 12 additions & 5 deletions exec/run-npm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 ?? {},
})
}

Expand All @@ -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
}
Expand All @@ -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'),
Expand Down
2 changes: 1 addition & 1 deletion network/auth-header/src/getAuthHeadersFromConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
}
Expand Down
6 changes: 5 additions & 1 deletion network/auth-header/src/index.ts
Original file line number Diff line number Diff line change
@@ -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>
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions releasing/plugin-commands-publishing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
27 changes: 27 additions & 0 deletions releasing/plugin-commands-publishing/src/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down
59 changes: 58 additions & 1 deletion releasing/plugin-commands-publishing/test/publish.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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,
}, [])
})
Original file line number Diff line number Diff line change
@@ -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%
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env node
console.log("Basic " + Buffer.from("password").toString("base64"));
Original file line number Diff line number Diff line change
@@ -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%
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env node
console.log("Bearer " + Buffer.from("password").toString("base64"));
3 changes: 3 additions & 0 deletions releasing/plugin-commands-publishing/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
{
"path": "../../fs/packlist"
},
{
"path": "../../network/auth-header"
},
{
"path": "../../packages/error"
},
Expand Down

0 comments on commit 5a5e425

Please sign in to comment.