mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-03 04:15:12 -04:00
Closes #11513. `actions/setup-node` writes `_authToken=${NODE_AUTH_TOKEN}` to `.npmrc`. When the user relies on OIDC trusted publishing without setting `NODE_AUTH_TOKEN`, pnpm previously passed the literal placeholder through verbatim — so any time OIDC fallback failed, pnpm sent `Authorization: Bearer ${NODE_AUTH_TOKEN}` to the registry and the publish came back as a 404. This worked in v10 because `pnpm publish` shelled out to `npm publish`, whose own OIDC flow handled the case. The fix lives in `@pnpm/config.env-replace@4.1.0`, which adds an `envReplaceLossy` variant that returns `{ value, unresolved }` instead of throwing. Unresolved `${VAR}` placeholders become `''` and are reported back as a list — leaving OIDC trusted publishing as the sole auth source. Resolvable placeholders and `${VAR-default}` / `${VAR:-default}` fallbacks elsewhere in the same string still expand normally, so a value like `pre-${SET}-mid-${UNSET}-${OTHER-default}-post` now produces `pre-AAA-mid--default-post` rather than dropping every placeholder. Also treats `{ KEY: undefined }` in the env object the same as a missing key (the `Record<string, string | undefined>` contract), so a `${KEY-default}` reaches the fallback in that case. ### Changes - `@pnpm/config.env-replace` catalog bumped from `^3.0.2` → `^4.1.0` (`pnpm-workspace.yaml`, `pnpm-lock.yaml`) - `config/reader/src/loadNpmrcFiles.ts` — `substituteEnv` now calls `envReplaceLossy` and pushes one warning per unresolved placeholder - `config/reader/test/index.ts` + `parseCreds.test.ts` — regression tests covering the OIDC case, mixed resolvable/unresolved placeholders, explicit-undefined env values, and `parseCreds({ authToken: '' })` - `.changeset/oidc-unresolved-env-placeholder.md` — patch bump for `@pnpm/config.reader` and `pnpm` - `pacquet/crates/config/{env_replace.rs, npmrc_auth.rs, npmrc_auth/tests.rs}` — mirrors the lossy semantics in pacquet's local `env_replace_lossy`, with matching test coverage
117 lines
3.1 KiB
TypeScript
117 lines
3.1 KiB
TypeScript
import { describe, expect, test } from '@jest/globals'
|
|
|
|
import {
|
|
AuthMissingSeparatorError,
|
|
type Creds,
|
|
parseCreds,
|
|
TokenHelperUnsupportedCharacterError,
|
|
} from '../src/parseCreds.js'
|
|
|
|
describe('parseCreds', () => {
|
|
test('empty object', () => {
|
|
expect(parseCreds({})).toBeUndefined()
|
|
})
|
|
|
|
test('authToken', () => {
|
|
expect(parseCreds({
|
|
authToken: 'example auth token',
|
|
})).toStrictEqual({
|
|
authToken: 'example auth token',
|
|
} as Creds)
|
|
})
|
|
|
|
test('empty authToken is treated as absent', () => {
|
|
// Reachable path via loadNpmrcFiles when an unresolved `${VAR}` placeholder
|
|
// is dropped to empty. Must not surface as a usable token.
|
|
expect(parseCreds({ authToken: '' })).toBeUndefined()
|
|
})
|
|
|
|
test('authPairBase64', () => {
|
|
expect(parseCreds({
|
|
authPairBase64: btoa('foo:bar'),
|
|
})).toStrictEqual({
|
|
basicAuth: {
|
|
username: 'foo',
|
|
password: 'bar',
|
|
},
|
|
} as Creds)
|
|
|
|
expect(parseCreds({
|
|
authPairBase64: btoa('foo:bar:baz'),
|
|
})).toStrictEqual({
|
|
basicAuth: {
|
|
username: 'foo',
|
|
password: 'bar:baz',
|
|
},
|
|
} as Creds)
|
|
})
|
|
|
|
test('authPairBase64 must have a separator', () => {
|
|
expect(() => parseCreds({
|
|
authPairBase64: btoa('foo'),
|
|
})).toThrow(new AuthMissingSeparatorError())
|
|
})
|
|
|
|
test('authUsername and authPassword', () => {
|
|
expect(parseCreds({
|
|
authUsername: 'foo',
|
|
authPassword: btoa('bar'),
|
|
})).toStrictEqual({
|
|
basicAuth: {
|
|
username: 'foo',
|
|
password: 'bar',
|
|
},
|
|
} as Creds)
|
|
|
|
expect(parseCreds({
|
|
authUsername: 'foo',
|
|
})).toBeUndefined()
|
|
|
|
expect(parseCreds({
|
|
authPassword: 'bar',
|
|
})).toBeUndefined()
|
|
})
|
|
|
|
test('tokenHelper', () => {
|
|
expect(parseCreds({
|
|
tokenHelper: 'example-token-helper --foo --bar baz',
|
|
})).toStrictEqual({
|
|
tokenHelper: ['example-token-helper', '--foo', '--bar', 'baz'],
|
|
} as Creds)
|
|
|
|
expect(parseCreds({
|
|
tokenHelper: './example-token-helper.sh --foo --bar baz',
|
|
})).toStrictEqual({
|
|
tokenHelper: ['./example-token-helper.sh', '--foo', '--bar', 'baz'],
|
|
} as Creds)
|
|
|
|
expect(parseCreds({
|
|
tokenHelper: 'node ./example-token-helper.js --foo --bar baz',
|
|
})).toStrictEqual({
|
|
tokenHelper: ['node', './example-token-helper.js', '--foo', '--bar', 'baz'],
|
|
} as Creds)
|
|
|
|
expect(parseCreds({
|
|
tokenHelper: './example-token-helper.sh',
|
|
})).toStrictEqual({
|
|
tokenHelper: ['./example-token-helper.sh'],
|
|
} as Creds)
|
|
})
|
|
|
|
test('tokenHelper does not support environment variable', () => {
|
|
expect(() => parseCreds({
|
|
tokenHelper: 'example-token-helper $MY_VAR',
|
|
})).toThrow(new TokenHelperUnsupportedCharacterError('$'))
|
|
})
|
|
|
|
test('tokenHelper does not support quotations', () => {
|
|
expect(() => parseCreds({
|
|
tokenHelper: 'example-token-helper "hello world"',
|
|
})).toThrow(new TokenHelperUnsupportedCharacterError('"'))
|
|
|
|
expect(() => parseCreds({
|
|
tokenHelper: "example-token-helper 'hello world'",
|
|
})).toThrow(new TokenHelperUnsupportedCharacterError("'"))
|
|
})
|
|
})
|