@pnpm/network.web-auth 1101.2.0 → 1101.3.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 +16 -0
- package/lib/formatAuthUrlMessage.d.ts +10 -0
- package/lib/formatAuthUrlMessage.js +22 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.js +1 -0
- package/lib/pollForWebAuthToken.d.ts +12 -0
- package/lib/pollForWebAuthToken.js +86 -7
- package/lib/withOtpHandling.js +3 -4
- package/package.json +5 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# @pnpm/network.web-auth
|
|
2
|
+
|
|
3
|
+
## 1101.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 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).
|
|
8
|
+
|
|
9
|
+
- 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).
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 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).
|
|
14
|
+
|
|
15
|
+
- Updated dependencies:
|
|
16
|
+
- @pnpm/error@1100.1.0
|
|
@@ -0,0 +1,10 @@
|
|
|
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;
|
|
@@ -0,0 +1,22 @@
|
|
|
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 `Authenticate your account at:\n${authUrl}`;
|
|
19
|
+
}
|
|
20
|
+
return `Authenticate your account at:\n${authUrl}\n\n${qrCode}`;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=formatAuthUrlMessage.js.map
|
package/lib/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
export { formatAuthUrlMessage } 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
|
@@ -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
|
-
|
|
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
|
-
|
|
56
|
-
body = await response.json();
|
|
84
|
+
return await response.json();
|
|
57
85
|
}
|
|
58
86
|
catch {
|
|
59
|
-
|
|
87
|
+
return undefined;
|
|
60
88
|
}
|
|
61
|
-
|
|
62
|
-
|
|
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
|
package/lib/withOtpHandling.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PnpmError } from '@pnpm/error';
|
|
2
|
-
import {
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "1101.3.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
|
-
"@
|
|
38
|
-
"@
|
|
37
|
+
"@pnpm/network.web-auth": "1101.3.0",
|
|
38
|
+
"@types/qrcode-terminal": "^0.12.2"
|
|
39
39
|
},
|
|
40
40
|
"engines": {
|
|
41
41
|
"node": ">=22.13"
|