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
175 lines
4.5 KiB
TypeScript
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("'"))
|
|
})
|
|
})
|