@pnpm/network.web-auth 1101.2.0 → 1101.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # @pnpm/network.web-auth
2
+
3
+ ## 1101.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - `pnpm login` no longer requires an interactive terminal when the registry supports web-based login: without a TTY it prints the authentication URL (skipping the QR code and the "Press ENTER to open the URL in your browser" prompt) and polls the registry until the browser approval completes. Only the classic username/password login still fails with `ERR_PNPM_LOGIN_NON_INTERACTIVE` in a non-interactive terminal.
8
+
9
+ ## 1101.3.0
10
+
11
+ ### Minor Changes
12
+
13
+ - The token poll for web-based authentication no longer reads the body of non-OK or still-pending (HTTP 202) responses, and caps the token response body it does read at 64 KiB, so a malicious or compromised registry cannot exhaust memory through the poll [pnpm/pnpm#12721](https://github.com/pnpm/pnpm/issues/12721).
14
+
15
+ - When the authentication URL cannot be rendered as a QR code (for example when it exceeds the maximum QR data capacity), web-based login now displays the URL alone with a warning instead of aborting authentication [pnpm/pnpm#12721](https://github.com/pnpm/pnpm/issues/12721).
16
+
17
+ ### Patch Changes
18
+
19
+ - Republished every package: the tarballs published by the v11.13.1 through v11.16.0 releases were missing most of their compiled files due to a packing bug [#13164](https://github.com/pnpm/pnpm/issues/13164).
20
+
21
+ - Updated dependencies:
22
+ - @pnpm/error@1100.1.0
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Formats the "Authenticate your account at" message for an authentication
3
+ * URL, appending a QR code rendering of it when one can be generated.
4
+ *
5
+ * The URL itself is the authentication mechanism and the QR code only a
6
+ * convenience, so a QR generation failure (e.g. a URL exceeding the maximum
7
+ * QR data capacity) downgrades to a `globalWarn` and a URL-only message
8
+ * instead of aborting the authentication flow.
9
+ */
10
+ export declare function formatAuthUrlMessage(authUrl: string, globalWarn: (message: string) => void): string;
11
+ /**
12
+ * Formats the "Authenticate your account at" message without a QR code — for
13
+ * output that is not a terminal and cannot render one.
14
+ */
15
+ export declare function formatAuthUrlOnlyMessage(authUrl: string): string;
@@ -0,0 +1,29 @@
1
+ import { generateQrCode } from './generateQrCode.js';
2
+ /**
3
+ * Formats the "Authenticate your account at" message for an authentication
4
+ * URL, appending a QR code rendering of it when one can be generated.
5
+ *
6
+ * The URL itself is the authentication mechanism and the QR code only a
7
+ * convenience, so a QR generation failure (e.g. a URL exceeding the maximum
8
+ * QR data capacity) downgrades to a `globalWarn` and a URL-only message
9
+ * instead of aborting the authentication flow.
10
+ */
11
+ export function formatAuthUrlMessage(authUrl, globalWarn) {
12
+ let qrCode;
13
+ try {
14
+ qrCode = generateQrCode(authUrl);
15
+ }
16
+ catch (err) {
17
+ globalWarn(`Could not generate a QR code: ${String(err)}`);
18
+ return formatAuthUrlOnlyMessage(authUrl);
19
+ }
20
+ return `${formatAuthUrlOnlyMessage(authUrl)}\n\n${qrCode}`;
21
+ }
22
+ /**
23
+ * Formats the "Authenticate your account at" message without a QR code — for
24
+ * output that is not a terminal and cannot render one.
25
+ */
26
+ export function formatAuthUrlOnlyMessage(authUrl) {
27
+ return `Authenticate your account at:\n${authUrl}`;
28
+ }
29
+ //# sourceMappingURL=formatAuthUrlMessage.js.map
package/lib/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
+ export { formatAuthUrlMessage, formatAuthUrlOnlyMessage } from './formatAuthUrlMessage.js';
1
2
  export { generateQrCode } from './generateQrCode.js';
2
- export { pollForWebAuthToken, type PollForWebAuthTokenParams, type WebAuthContext, type WebAuthFetchOptions, type WebAuthFetchResponse, type WebAuthFetchResponseHeaders, } from './pollForWebAuthToken.js';
3
+ export { pollForWebAuthToken, type PollForWebAuthTokenParams, type WebAuthContext, type WebAuthFetchOptions, type WebAuthFetchResponse, type WebAuthFetchResponseBody, type WebAuthFetchResponseBodyReader, type WebAuthFetchResponseHeaders, } from './pollForWebAuthToken.js';
3
4
  export { promptBrowserOpen, type PromptBrowserOpenContext, type PromptBrowserOpenParams, type PromptBrowserOpenReadlineInterface, } from './promptBrowserOpen.js';
4
5
  export { WebAuthTimeoutError } from './WebAuthTimeoutError.js';
5
6
  export { canonicalHttpUrl, isOtpError, type OtpContext, type OtpEnquirer, type OtpHandlingParams, OtpNonInteractiveError, type OtpProcess, OtpSecondChallengeError, SyntheticOtpError, withOtpHandling, } from './withOtpHandling.js';
package/lib/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { formatAuthUrlMessage, formatAuthUrlOnlyMessage } from './formatAuthUrlMessage.js';
1
2
  export { generateQrCode } from './generateQrCode.js';
2
3
  export { pollForWebAuthToken, } from './pollForWebAuthToken.js';
3
4
  export { promptBrowserOpen, } from './promptBrowserOpen.js';
@@ -12,7 +12,19 @@ export interface WebAuthFetchOptions {
12
12
  export interface WebAuthFetchResponseHeaders {
13
13
  get: (name: string) => string | null;
14
14
  }
15
+ export interface WebAuthFetchResponseBodyReader {
16
+ read: () => Promise<{
17
+ done: boolean;
18
+ value?: Uint8Array;
19
+ }>;
20
+ cancel: () => Promise<void>;
21
+ }
22
+ export interface WebAuthFetchResponseBody {
23
+ cancel: () => Promise<void>;
24
+ getReader: () => WebAuthFetchResponseBodyReader;
25
+ }
15
26
  export interface WebAuthFetchResponse {
27
+ readonly body?: WebAuthFetchResponseBody | null;
16
28
  readonly headers: WebAuthFetchResponseHeaders;
17
29
  readonly json: () => Promise<unknown>;
18
30
  readonly ok: boolean;
@@ -1,4 +1,11 @@
1
1
  import { WebAuthTimeoutError } from './WebAuthTimeoutError.js';
2
+ /**
3
+ * The most bytes of a poll response body that are read. The expected body is
4
+ * a small JSON object carrying the token, and the URL it comes from is
5
+ * registry-controlled, so an unbounded read on every poll tick would let a
6
+ * malicious or compromised registry grow memory at will.
7
+ */
8
+ const TOKEN_BODY_LIMIT = 64 * 1024;
2
9
  /**
3
10
  * Polls a registry's "done" URL until an authentication token is returned.
4
11
  *
@@ -27,9 +34,12 @@ export async function pollForWebAuthToken({ context: { Date, fetch, setTimeout }
27
34
  catch {
28
35
  continue;
29
36
  }
30
- if (!response.ok)
37
+ if (!response.ok) {
38
+ discardBody(response);
31
39
  continue;
40
+ }
32
41
  if (response.status === 202) {
42
+ discardBody(response);
33
43
  // Registry is still waiting for authentication.
34
44
  // Respect Retry-After header if present by waiting the additional time
35
45
  // beyond the default poll interval already elapsed above, but do not
@@ -50,17 +60,86 @@ export async function pollForWebAuthToken({ context: { Date, fetch, setTimeout }
50
60
  }
51
61
  continue;
52
62
  }
53
- let body;
63
+ // eslint-disable-next-line no-await-in-loop
64
+ const body = await readTokenBody(response);
65
+ if (body?.token) {
66
+ return body.token;
67
+ }
68
+ }
69
+ }
70
+ /**
71
+ * Reads and parses a poll response body, applying {@link TOKEN_BODY_LIMIT}
72
+ * when the raw stream is exposed. Returns `undefined` — which the poll loop
73
+ * treats the same as an unparsable body, retrying on the next tick — for an
74
+ * oversized or truncated body.
75
+ */
76
+ async function readTokenBody(response) {
77
+ // A response that exposes no readable body stream can only be read whole via
78
+ // json(), which the size cap cannot bound. The production undici-backed fetch
79
+ // always exposes `body` as a ReadableStream, so this uncapped path is reached
80
+ // only by stream-less WebAuthFetch stand-ins (the json()-based test mocks);
81
+ // every real transport goes through the capped stream read below.
82
+ if (response.body === undefined) {
54
83
  try {
55
- // eslint-disable-next-line no-await-in-loop
56
- body = await response.json();
84
+ return await response.json();
57
85
  }
58
86
  catch {
59
- continue;
87
+ return undefined;
60
88
  }
61
- if (body.token) {
62
- return body.token;
89
+ }
90
+ const contentLength = Number(response.headers.get('content-length'));
91
+ if (Number.isFinite(contentLength) && contentLength > TOKEN_BODY_LIMIT) {
92
+ discardBody(response);
93
+ return undefined;
94
+ }
95
+ if (response.body === null)
96
+ return undefined;
97
+ let reader;
98
+ try {
99
+ reader = response.body.getReader();
100
+ }
101
+ catch {
102
+ return undefined;
103
+ }
104
+ const chunks = [];
105
+ let total = 0;
106
+ try {
107
+ while (true) {
108
+ // eslint-disable-next-line no-await-in-loop
109
+ const { done, value } = await reader.read();
110
+ if (done)
111
+ break;
112
+ if (value == null)
113
+ continue;
114
+ total += value.length;
115
+ if (total > TOKEN_BODY_LIMIT) {
116
+ reader.cancel().catch(() => { });
117
+ return undefined;
118
+ }
119
+ chunks.push(value);
63
120
  }
64
121
  }
122
+ catch {
123
+ return undefined;
124
+ }
125
+ try {
126
+ return JSON.parse(new TextDecoder().decode(Buffer.concat(chunks)));
127
+ }
128
+ catch {
129
+ return undefined;
130
+ }
131
+ }
132
+ /**
133
+ * Cancels a response body the poll loop will not read (non-ok and 202
134
+ * responses), so its payload is not transferred on every poll tick.
135
+ */
136
+ function discardBody(response) {
137
+ try {
138
+ response.body?.cancel().catch(() => { });
139
+ }
140
+ catch {
141
+ // Cancellation is best-effort: a body that cannot be cancelled is simply
142
+ // left unread.
143
+ }
65
144
  }
66
145
  //# sourceMappingURL=pollForWebAuthToken.js.map
@@ -1,5 +1,5 @@
1
1
  import { PnpmError } from '@pnpm/error';
2
- import { generateQrCode } from './generateQrCode.js';
2
+ import { formatAuthUrlMessage } from './formatAuthUrlMessage.js';
3
3
  import { pollForWebAuthToken } from './pollForWebAuthToken.js';
4
4
  import { promptBrowserOpen } from './promptBrowserOpen.js';
5
5
  export const isOtpError = (error) => error != null &&
@@ -22,7 +22,7 @@ export const isOtpError = (error) => error != null &&
22
22
  * @see https://github.com/npm/cli/blob/7d900c46/lib/utils/otplease.js for npm's implementation.
23
23
  */
24
24
  export async function withOtpHandling({ context, fetchOptions, operation, }) {
25
- const { enquirer, globalInfo, process, } = context;
25
+ const { enquirer, globalInfo, globalWarn, process, } = context;
26
26
  try {
27
27
  return await operation();
28
28
  }
@@ -36,8 +36,7 @@ export async function withOtpHandling({ context, fetchOptions, operation, }) {
36
36
  const authUrl = canonicalHttpUrl(error.body?.authUrl);
37
37
  const doneUrl = canonicalHttpUrl(error.body?.doneUrl);
38
38
  if (authUrl != null && doneUrl != null) {
39
- const qrCode = generateQrCode(authUrl);
40
- globalInfo(`Authenticate your account at:\n${authUrl}\n\n${qrCode}`);
39
+ globalInfo(formatAuthUrlMessage(authUrl, globalWarn));
41
40
  const pollPromise = pollForWebAuthToken({
42
41
  context,
43
42
  doneUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/network.web-auth",
3
- "version": "1101.2.0",
3
+ "version": "1101.4.0",
4
4
  "description": "Web-based authentication flow with QR code display and token polling",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -28,14 +28,14 @@
28
28
  "!*.map"
29
29
  ],
30
30
  "dependencies": {
31
+ "@pnpm/error": "1100.1.0",
31
32
  "open": "^11.0.0",
32
- "qrcode-terminal": "^0.12.0",
33
- "@pnpm/error": "1100.0.1"
33
+ "qrcode-terminal": "^0.12.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@jest/globals": "30.4.1",
37
- "@types/qrcode-terminal": "^0.12.2",
38
- "@pnpm/network.web-auth": "1101.2.0"
37
+ "@pnpm/network.web-auth": "1101.4.0",
38
+ "@types/qrcode-terminal": "^0.12.2"
39
39
  },
40
40
  "engines": {
41
41
  "node": ">=22.13"