Files
pnpm/pnpm11/config/reader/test/parseCreds.test.ts
Zoltan Kochan fc0c282b71 fix(config): decode and re-encode _auth credentials (#14261)
pacquet forwarded an `.npmrc` `_auth` value verbatim as the `Basic` header, so a
value written without its `=` padding (or with redundant padding or embedded
whitespace) reached the registry as an unusable header and the request failed
with a 401. Decode it, require the `:` that separates the username from the
password, and re-encode canonically — matching `parseBasicAuth` in the
TypeScript stack, which has decoded `_auth` since the setting was added. The
decode/re-encode is byte-exact for a canonically encoded value, so a working
configuration sends the same header as before.

An `_auth` that is not base64 at all, or whose decoded form has no separator,
now fails the config load with ERR_PNPM_AUTH_INVALID_BASE64 /
ERR_PNPM_AUTH_MISSING_SEPARATOR, the codes the TypeScript stack raises, instead
of silently producing a header no server can read.

The `_password` decoder keeps its raw-value fallback: that tolerance is a
deliberate divergence from the TypeScript stack, and this change is about the
`_auth` path.

Closes pnpm/pnpm#14257
2026-08-28 02:19:17 +02:00

175 lines
4.5 KiB
TypeScript

import { describe, expect, test } from '@jest/globals'
import {
AuthBase64DecodeError,
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('authPairBase64 allows redundant trailing padding', () => {
expect(parseCreds({
authPairBase64: `${btoa('foo:bar')}=`,
})).toStrictEqual({
basicAuth: {
username: 'foo',
password: 'bar',
},
} as Creds)
})
test('authPairBase64 allows missing trailing padding', () => {
expect(parseCreds({
authPairBase64: btoa('foo:bar').replaceAll('=', ''),
})).toStrictEqual({
basicAuth: {
username: 'foo',
password: 'bar',
},
} as Creds)
})
test('authPairBase64 of only padding is not base64', () => {
expect(() => parseCreds({
authPairBase64: '====',
})).toThrow(new AuthBase64DecodeError('_auth'))
})
test('an empty authPairBase64 supplies no credentials', () => {
expect(parseCreds({ authPairBase64: '' })).toBeUndefined()
})
test('authPairBase64 must be base64', () => {
expect(() => parseCreds({
authPairBase64: 'foo*bar',
})).toThrow(new AuthBase64DecodeError('_auth'))
})
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('authPassword allows redundant trailing padding', () => {
expect(parseCreds({
authUsername: 'foo',
authPassword: `${btoa('bar')}=`,
})).toStrictEqual({
basicAuth: {
username: 'foo',
password: 'bar',
},
} as Creds)
})
test('authPassword must be base64', () => {
expect(() => parseCreds({
authUsername: 'foo',
authPassword: 'bar*baz',
})).toThrow(new AuthBase64DecodeError('_password'))
})
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("'"))
})
})