pnpm login refused to run whenever stdin or stdout was not a TTY, even though the registry web-auth flow only prints an authentication URL and polls the done endpoint until the browser approval completes - neither needs a terminal. Agent- and CI-adjacent tooling had to wrap pnpm in a pseudo-terminal (script -q /dev/null pnpm login) to use the web flow. Move the non-interactive guard from the top of the login command into the classic username/password fallback, the only path that prompts on the terminal. Without a TTY the web flow now prints the authentication URL and polls as before; the URL is printed without the QR code (a piped stdout cannot render the block art), and the press-ENTER browser prompt was already skipped for a non-TTY stdin. A registry without web login support still fails with ERR_PNPM_LOGIN_NON_INTERACTIVE. Harden the TypeScript web-login path to match pacquet while touching it: narrow the attacker-controlled response body at runtime (a missing, empty, or non-string loginUrl/doneUrl is an invalid response), and reject URLs containing Unicode control characters with pacquet's ERR_PNPM_AUTH_COMMANDS_LOGIN_UNSAFE_URL before anything is printed or polled. The shared error message now says "authentication URL" in both stacks, since the check covers loginUrl and doneUrl alike. Implemented in both stacks: the TypeScript CLI moves the guard into classicLogin and prints a URL-only message via the new formatAuthUrlOnlyMessage export of the web-auth package; pacquet moves the same guard into classic_login and selects AuthUrlMessage::UrlOnly when stdout is not a TTY. The pacquet CLI adapter unit test and the CLI-tier integration tests now drive the guard through a 404 web-login probe so it exercises the classic fallback, and a new integration test covers the headless web flow end-to-end against a mock registry. Also acknowledge a pre-existing zizmor ref-version-mismatch finding on the winget-releaser pin in update-latest.yml with the repository's usual inline ignore: the pinned commit is no longer reachable from any named ref upstream, so no version comment can describe it accurately.
30 lines
1.3 KiB
TypeScript
30 lines
1.3 KiB
TypeScript
import { describe, expect, it } from '@jest/globals'
|
|
import { formatAuthUrlMessage, formatAuthUrlOnlyMessage, generateQrCode } from '@pnpm/network.web-auth'
|
|
|
|
describe('formatAuthUrlMessage', () => {
|
|
it('appends a QR code when one can be generated', () => {
|
|
const authUrl = 'https://example.com/auth'
|
|
const message = formatAuthUrlMessage(authUrl, msg => {
|
|
throw new Error(`Unexpected call to globalWarn: ${msg}`)
|
|
})
|
|
expect(message).toBe(`Authenticate your account at:\n${authUrl}\n\n${generateQrCode(authUrl)}`)
|
|
})
|
|
|
|
it('warns and falls back to a URL-only message when QR generation fails', () => {
|
|
// Longer than the 2953-byte maximum QR data capacity (version 40 at
|
|
// error-correction level L), which makes qrcode-terminal throw.
|
|
const longAuthUrl = `https://example.com/auth/${'a'.repeat(4000)}`
|
|
const warnings: string[] = []
|
|
const message = formatAuthUrlMessage(longAuthUrl, msg => warnings.push(msg))
|
|
expect(message).toBe(`Authenticate your account at:\n${longAuthUrl}`)
|
|
expect(warnings).toStrictEqual([expect.stringContaining('Could not generate a QR code:')])
|
|
})
|
|
})
|
|
|
|
describe('formatAuthUrlOnlyMessage', () => {
|
|
it('formats the message without a QR code', () => {
|
|
const authUrl = 'https://example.com/auth'
|
|
expect(formatAuthUrlOnlyMessage(authUrl)).toBe(`Authenticate your account at:\n${authUrl}`)
|
|
})
|
|
})
|