mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-23 23:29:17 -05:00
feat(config): add support for token helper (#4163)
* feat(config): add support for token helper Use the new interface in `pnpm/credentials-by-uri` for supporting token helpers. A token helper is an executable, set in the user's `.npmrc` which outputs an auth token. This can be used in situations where the `authToken` is not a constant value, but is something that refreshes regularly, where a script or other tool can use an existing refresh token to obtain a new access token. The configuration for the path to the helper must be an absolute path, with no arguments. In order to be secure, it is _only_ permitted to set this value in the user `.npmrc`, otherwise a project could place a value in a project local `.npmrc` and run arbitrary executables. A similar feature is available in many similar tools. The implementation in `credentials-by-uri` is modelled after the `vault` (vaultproject.io) implementation - https://github.com/hashicorp/vault/blob/main/command/token/helper_external.go * test: fix * docs: add changesets Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
committed by
GitHub
parent
6e4becd997
commit
a6cf11cb77
12
.changeset/clever-lamps-buy.md
Normal file
12
.changeset/clever-lamps-buy.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
"@pnpm/client": minor
|
||||
"@pnpm/plugin-commands-installation": minor
|
||||
"@pnpm/plugin-commands-listing": minor
|
||||
"@pnpm/plugin-commands-outdated": minor
|
||||
"@pnpm/plugin-commands-publishing": minor
|
||||
"@pnpm/plugin-commands-rebuild": minor
|
||||
"@pnpm/plugin-commands-store": minor
|
||||
"@pnpm/store-connection-manager": minor
|
||||
---
|
||||
|
||||
New optional setting added: userConfig. userConfig may contain token helpers.
|
||||
31
.changeset/honest-bears-smash.md
Normal file
31
.changeset/honest-bears-smash.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Add support for token helper, a command line tool to obtain a token.
|
||||
|
||||
A token helper is an executable, set in the user's `.npmrc` which
|
||||
outputs an auth token. This can be used in situations where the
|
||||
authToken is not a constant value, but is something that refreshes
|
||||
regularly, where a script or other tool can use an existing refresh
|
||||
token to obtain a new access token.
|
||||
|
||||
The configuration for the path to the helper must be an absolute path,
|
||||
with no arguments. In order to be secure, it is only permitted to set
|
||||
this value in the user `.npmrc`, otherwise a project could place a value
|
||||
in a project local `.npmrc` and run arbitrary executables.
|
||||
|
||||
Usage example:
|
||||
|
||||
```ini
|
||||
; Setting a token helper for the default registry
|
||||
tokenHelper=/home/ivan/token-generator
|
||||
|
||||
; Setting a token helper for the specified registry
|
||||
//registry.corp.com:tokenHelper=/home/ivan/token-generator
|
||||
```
|
||||
|
||||
Related PRs:
|
||||
|
||||
- [pnpm/credentials-by-uri#2](https://github.com/pnpm/credentials-by-uri/pull/2)
|
||||
- [#4163](https://github.com/pnpm/pnpm/pull/4163)
|
||||
5
.changeset/twenty-timers-end.md
Normal file
5
.changeset/twenty-timers-end.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/config": minor
|
||||
---
|
||||
|
||||
`userConfig` added to the config object, which contain only the settings set in the user's home config file.
|
||||
@@ -38,7 +38,7 @@
|
||||
"@pnpm/git-fetcher": "workspace:4.1.13",
|
||||
"@pnpm/resolver-base": "workspace:8.1.4",
|
||||
"@pnpm/tarball-fetcher": "workspace:9.3.14",
|
||||
"credentials-by-uri": "^2.0.0",
|
||||
"credentials-by-uri": "^2.1.0",
|
||||
"mem": "^8.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -17,11 +17,12 @@ export type ClientOptions = {
|
||||
retry?: RetryTimeoutOptions
|
||||
timeout?: number
|
||||
userAgent?: string
|
||||
userConfig?: Record<string, string>
|
||||
} & ResolverFactoryOptions & AgentOptions
|
||||
|
||||
export default function (opts: ClientOptions) {
|
||||
const fetchFromRegistry = createFetchFromRegistry(opts)
|
||||
const getCredentials = mem((registry: string) => getCredentialsByURI(opts.authConfig, registry))
|
||||
const getCredentials = mem((registry: string) => getCredentialsByURI(opts.authConfig, registry, opts.userConfig))
|
||||
return {
|
||||
fetchers: createFetchers(fetchFromRegistry, getCredentials, opts),
|
||||
resolve: createResolve(fetchFromRegistry, getCredentials, opts),
|
||||
|
||||
@@ -143,6 +143,7 @@ export interface Config {
|
||||
changedFilesIgnorePattern?: string[]
|
||||
extendNodePath?: boolean
|
||||
rootProjectManifest?: ProjectManifest
|
||||
userConfig: Record<string, string>
|
||||
}
|
||||
|
||||
export interface ConfigWithDeprecatedSettings extends Config {
|
||||
|
||||
@@ -463,6 +463,9 @@ export default async (
|
||||
pnpmConfig.noProxy = pnpmConfig['noproxy'] ?? getProcessEnv('no_proxy')
|
||||
}
|
||||
pnpmConfig.enablePnp = pnpmConfig['nodeLinker'] === 'pnp'
|
||||
if (!pnpmConfig.userConfig) {
|
||||
pnpmConfig.userConfig = npmConfig.sources.user?.data
|
||||
}
|
||||
|
||||
if (opts.checkUnknownSetting) {
|
||||
const settingKeys = Object.keys({
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from 'path'
|
||||
import getConfig from '@pnpm/config'
|
||||
import PnpmError from '@pnpm/error'
|
||||
import prepare, { prepareEmpty } from '@pnpm/prepare'
|
||||
import loadNpmConf from '@zkochan/npm-conf'
|
||||
|
||||
import symlinkDir from 'symlink-dir'
|
||||
|
||||
@@ -725,3 +726,35 @@ test('getConfig() converts noproxy to noProxy', async () => {
|
||||
})
|
||||
expect(config.noProxy).toBe('www.foo.com')
|
||||
})
|
||||
|
||||
test('getConfig() returns the userconfig', async () => {
|
||||
prepareEmpty()
|
||||
await fs.mkdir('user-home')
|
||||
await fs.writeFile(path.resolve('user-home', '.npmrc'), 'registry = https://registry.example.test', 'utf-8')
|
||||
loadNpmConf.defaults.userconfig = path.resolve('user-home', '.npmrc')
|
||||
const { config } = await getConfig({
|
||||
cliOptions: {},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
})
|
||||
expect(config.userConfig).toEqual({ registry: 'https://registry.example.test' })
|
||||
})
|
||||
|
||||
test('getConfig() returns the userconfig even when overridden locally', async () => {
|
||||
prepareEmpty()
|
||||
await fs.mkdir('user-home')
|
||||
await fs.writeFile(path.resolve('user-home', '.npmrc'), 'registry = https://registry.example.test', 'utf-8')
|
||||
loadNpmConf.defaults.userconfig = path.resolve('user-home', '.npmrc')
|
||||
await fs.writeFile('.npmrc', 'registry = https://project-local.example.test', 'utf-8')
|
||||
const { config } = await getConfig({
|
||||
cliOptions: {},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
})
|
||||
expect(config.registry).toEqual('https://project-local.example.test')
|
||||
expect(config.userConfig).toEqual({ registry: 'https://registry.example.test' })
|
||||
})
|
||||
|
||||
@@ -31,6 +31,7 @@ const DEFAULT_OPTIONS = {
|
||||
},
|
||||
sort: true,
|
||||
storeDir: path.join(tmp, 'store'),
|
||||
userConfig: {},
|
||||
workspaceConcurrency: 1,
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ const DEFAULT_OPTIONS = {
|
||||
default: REGISTRY_URL,
|
||||
},
|
||||
sort: true,
|
||||
userConfig: {},
|
||||
workspaceConcurrency: 1,
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ const DEFAULT_OPTIONS = {
|
||||
},
|
||||
sort: true,
|
||||
storeDir: path.join(tmp, 'store'),
|
||||
userConfig: {},
|
||||
workspaceConcurrency: 1,
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ const DEFAULT_OPTS = {
|
||||
storeDir: path.join(TMP, 'store'),
|
||||
strictSsl: false,
|
||||
userAgent: 'pnpm',
|
||||
userConfig: {},
|
||||
useRunningStoreServer: false,
|
||||
useStoreServer: false,
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ const DEFAULT_OPTS = {
|
||||
storeDir: path.join(TMP, 'store'),
|
||||
strictSsl: false,
|
||||
userAgent: 'pnpm',
|
||||
userConfig: {},
|
||||
useRunningStoreServer: false,
|
||||
useStoreServer: false,
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ const DEFAULT_OPTIONS = {
|
||||
},
|
||||
sort: true,
|
||||
storeDir: path.join(TMP, 'store'),
|
||||
userConfig: {},
|
||||
workspaceConcurrency: 1,
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ const DEFAULT_OPTIONS = {
|
||||
default: REGISTRY_URL,
|
||||
},
|
||||
sort: true,
|
||||
userConfig: {},
|
||||
workspaceConcurrency: 1,
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ const DEFAULT_OPTIONS = {
|
||||
default: REGISTRY_URL,
|
||||
},
|
||||
sort: true,
|
||||
userConfig: {},
|
||||
workspaceConcurrency: 1,
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ export const DEFAULT_OPTS = {
|
||||
storeDir: '../store',
|
||||
strictSsl: false,
|
||||
userAgent: 'pnpm',
|
||||
userConfig: {},
|
||||
useRunningStoreServer: false,
|
||||
useStoreServer: false,
|
||||
workspaceConcurrency: 4,
|
||||
|
||||
@@ -41,6 +41,7 @@ export const DEFAULT_OPTS = {
|
||||
storeDir: '../store',
|
||||
strictSsl: false,
|
||||
userAgent: 'pnpm',
|
||||
userConfig: {},
|
||||
useRunningStoreServer: false,
|
||||
useStoreServer: false,
|
||||
workspaceConcurrency: 4,
|
||||
|
||||
@@ -152,7 +152,7 @@ export type OutdatedCommandOptions = {
|
||||
| 'strictSsl'
|
||||
| 'tag'
|
||||
| 'userAgent'
|
||||
>
|
||||
> & Partial<Pick<Config, 'userConfig'>>
|
||||
|
||||
export async function handler (
|
||||
opts: OutdatedCommandOptions,
|
||||
|
||||
@@ -33,6 +33,7 @@ const OUTDATED_OPTIONS = {
|
||||
strictSsl: false,
|
||||
tag: 'latest',
|
||||
userAgent: '',
|
||||
userConfig: {},
|
||||
}
|
||||
|
||||
test('pnpm outdated: show details', async () => {
|
||||
|
||||
@@ -44,6 +44,7 @@ export const DEFAULT_OPTS = {
|
||||
strictSsl: false,
|
||||
tag: 'latest',
|
||||
userAgent: 'pnpm',
|
||||
userConfig: {},
|
||||
useRunningStoreServer: false,
|
||||
useStoreServer: false,
|
||||
workspaceConcurrency: 4,
|
||||
|
||||
@@ -42,6 +42,7 @@ Partial<Pick<Config,
|
||||
| 'selectedProjectsGraph'
|
||||
| 'strictSsl'
|
||||
| 'userAgent'
|
||||
| 'userConfig'
|
||||
| 'verifyStoreIntegrity'
|
||||
>> & {
|
||||
access?: 'public' | 'restricted'
|
||||
@@ -58,6 +59,7 @@ export default async function (
|
||||
const resolve = createResolver({
|
||||
...opts,
|
||||
authConfig: opts.rawConfig,
|
||||
userConfig: opts.userConfig,
|
||||
retry: {
|
||||
factor: opts.fetchRetryFactor,
|
||||
maxTimeout: opts.fetchRetryMaxtimeout,
|
||||
|
||||
@@ -41,6 +41,7 @@ export const DEFAULT_OPTS = {
|
||||
cacheDir: '../cache',
|
||||
strictSsl: false,
|
||||
userAgent: 'pnpm',
|
||||
userConfig: {},
|
||||
useRunningStoreServer: false,
|
||||
useStoreServer: false,
|
||||
workspaceConcurrency: 4,
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface StrictRebuildOptions {
|
||||
development: boolean
|
||||
optional: boolean
|
||||
rawConfig: object
|
||||
userConfig: Record<string, string>
|
||||
userAgent: string
|
||||
packageManager: {
|
||||
name: string
|
||||
|
||||
@@ -41,6 +41,7 @@ export const DEFAULT_OPTS = {
|
||||
storeDir: '../store',
|
||||
strictSsl: false,
|
||||
userAgent: 'pnpm',
|
||||
userConfig: {},
|
||||
useRunningStoreServer: false,
|
||||
useStoreServer: false,
|
||||
workspaceConcurrency: 4,
|
||||
|
||||
@@ -21,6 +21,7 @@ test('pnpm store add express@4.16.3', async () => {
|
||||
},
|
||||
registries: { default: `http://localhost:${REGISTRY_MOCK_PORT}/` },
|
||||
storeDir,
|
||||
userConfig: {},
|
||||
}, ['add', 'express@4.16.3'])
|
||||
|
||||
const { cafsHas } = assertStore(path.join(storeDir, STORE_VERSION))
|
||||
@@ -44,6 +45,7 @@ test('pnpm store add scoped package that uses not the standard registry', async
|
||||
default: 'https://registry.npmjs.org/',
|
||||
},
|
||||
storeDir,
|
||||
userConfig: {},
|
||||
}, ['add', '@foo/no-deps@1.0.0'])
|
||||
|
||||
const { cafsHas } = assertStore(path.join(storeDir, STORE_VERSION))
|
||||
@@ -70,6 +72,7 @@ test('should fail if some packages can not be added', async () => {
|
||||
default: 'https://registry.npmjs.org/',
|
||||
},
|
||||
storeDir,
|
||||
userConfig: {},
|
||||
}, ['add', '@pnpm/this-does-not-exist'])
|
||||
} catch (e: any) { // eslint-disable-line
|
||||
thrown = true
|
||||
|
||||
@@ -17,6 +17,7 @@ test('CLI prints the current store path', async () => {
|
||||
},
|
||||
registries: { default: REGISTRY },
|
||||
storeDir: '/home/example/.pnpm-store',
|
||||
userConfig: {},
|
||||
}, ['path'])
|
||||
|
||||
const expectedStorePath = os.platform() === 'win32'
|
||||
|
||||
@@ -34,6 +34,7 @@ test('remove unreferenced packages', async () => {
|
||||
registries: { default: REGISTRY },
|
||||
reporter,
|
||||
storeDir,
|
||||
userConfig: {},
|
||||
}, ['prune'])
|
||||
|
||||
expect(reporter).toBeCalledWith(
|
||||
@@ -55,6 +56,7 @@ test('remove unreferenced packages', async () => {
|
||||
registries: { default: REGISTRY },
|
||||
reporter,
|
||||
storeDir,
|
||||
userConfig: {},
|
||||
}, ['prune'])
|
||||
|
||||
expect(reporter).not.toBeCalledWith(
|
||||
@@ -87,6 +89,7 @@ test.skip('remove packages that are used by project that no longer exist', async
|
||||
registries: { default: REGISTRY },
|
||||
reporter,
|
||||
storeDir,
|
||||
userConfig: {},
|
||||
}, ['prune'])
|
||||
|
||||
expect(reporter).toBeCalledWith(
|
||||
@@ -129,6 +132,7 @@ test('keep dependencies used by others', async () => {
|
||||
},
|
||||
registries: { default: REGISTRY },
|
||||
storeDir,
|
||||
userConfig: {},
|
||||
}, ['prune'])
|
||||
|
||||
await project.storeHasNot('camelcase-keys', '3.0.0')
|
||||
@@ -151,6 +155,7 @@ test('keep dependency used by package', async () => {
|
||||
},
|
||||
registries: { default: REGISTRY },
|
||||
storeDir,
|
||||
userConfig: {},
|
||||
}, ['prune'])
|
||||
|
||||
await project.storeHas('is-positive', '3.1.0')
|
||||
@@ -171,6 +176,7 @@ test('prune will skip scanning non-directory in storeDir', async () => {
|
||||
},
|
||||
registries: { default: REGISTRY },
|
||||
storeDir,
|
||||
userConfig: {},
|
||||
}, ['prune'])
|
||||
})
|
||||
|
||||
@@ -195,6 +201,7 @@ test('prune does not fail if the store contains an unexpected directory', async
|
||||
registries: { default: REGISTRY },
|
||||
reporter,
|
||||
storeDir,
|
||||
userConfig: {},
|
||||
}, ['prune'])
|
||||
|
||||
expect(reporter).toBeCalledWith(
|
||||
|
||||
@@ -30,6 +30,7 @@ test('CLI fails when store status finds modified packages', async () => {
|
||||
},
|
||||
registries: { default: REGISTRY },
|
||||
storeDir,
|
||||
userConfig: {},
|
||||
}, ['status'])
|
||||
} catch (_err: any) { // eslint-disable-line
|
||||
err = _err
|
||||
@@ -70,5 +71,6 @@ test('CLI does not fail when store status does not find modified packages', asyn
|
||||
},
|
||||
registries: { default: REGISTRY },
|
||||
storeDir,
|
||||
userConfig: {},
|
||||
}, ['status'])
|
||||
})
|
||||
|
||||
@@ -38,12 +38,13 @@ export type CreateNewStoreControllerOptions = CreateResolverOptions & Pick<Confi
|
||||
| 'verifyStoreIntegrity'
|
||||
> & {
|
||||
ignoreFile?: (filename: string) => boolean
|
||||
}
|
||||
} & Partial<Pick<Config, 'userConfig'>>
|
||||
|
||||
export default async (
|
||||
opts: CreateNewStoreControllerOptions
|
||||
) => {
|
||||
const { resolve, fetchers } = createClient({
|
||||
userConfig: opts.userConfig,
|
||||
authConfig: opts.rawConfig,
|
||||
ca: opts.ca,
|
||||
cacheDir: opts.cacheDir,
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -296,7 +296,7 @@ importers:
|
||||
'@pnpm/logger': ^4.0.0
|
||||
'@pnpm/resolver-base': workspace:8.1.4
|
||||
'@pnpm/tarball-fetcher': workspace:9.3.14
|
||||
credentials-by-uri: ^2.0.0
|
||||
credentials-by-uri: ^2.1.0
|
||||
mem: ^8.0.0
|
||||
dependencies:
|
||||
'@pnpm/default-resolver': link:../default-resolver
|
||||
@@ -306,7 +306,7 @@ importers:
|
||||
'@pnpm/git-fetcher': link:../git-fetcher
|
||||
'@pnpm/resolver-base': link:../resolver-base
|
||||
'@pnpm/tarball-fetcher': link:../tarball-fetcher
|
||||
credentials-by-uri: 2.0.0
|
||||
credentials-by-uri: 2.1.0
|
||||
mem: 8.1.1
|
||||
devDependencies:
|
||||
'@pnpm/client': 'link:'
|
||||
@@ -4772,7 +4772,6 @@ packages:
|
||||
/@pnpm/error/2.0.0:
|
||||
resolution: {integrity: sha512-mgj4h0LWGpDPZwsEH75VFQhr2Njut3PcaCQatERIoO3zmKGqCsLfla9cWYH9+zn0fcwnKhnJ+FBzoiY2LhnCtw==}
|
||||
engines: {node: '>=12.17'}
|
||||
dev: true
|
||||
|
||||
/@pnpm/exec/2.0.0:
|
||||
resolution: {integrity: sha512-b5ALfWEOFQprWKntN7MF8XWCyslBk2c8u20GEDcDDQOs6c0HyHlWxX5lig8riQKdS000U6YyS4L4b32NOleXAQ==}
|
||||
@@ -7602,10 +7601,11 @@ packages:
|
||||
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
|
||||
dev: true
|
||||
|
||||
/credentials-by-uri/2.0.0:
|
||||
resolution: {integrity: sha512-HptfRWLKfaeewvzKPybPFsR8TRbLSsu5MvHQLc6FWqvIS1CqFHze+IteKQlpLXzx4KwSteZa0MdsN/jEYESVXA==}
|
||||
/credentials-by-uri/2.1.0:
|
||||
resolution: {integrity: sha512-Ia57VrZYcs4YTPUB4Pirjf3MYsSdc7mAYGC99lUKii0KsohmZgpPvVOeqW1NWBCRg3HVrLOtSreLqkmFIUi8WQ==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
'@pnpm/error': 2.0.0
|
||||
nerf-dart: 1.0.0
|
||||
dev: false
|
||||
|
||||
|
||||
Reference in New Issue
Block a user