@pnpm/error 1100.1.2 → 1100.1.4
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 +15 -0
- package/lib/index.d.ts +7 -13
- package/lib/index.js +57 -8
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @pnpm/error
|
|
2
2
|
|
|
3
|
+
## 1100.1.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Fetch and tarball errors no longer print the secrets of the URL they name. Inline `user:pass@` credentials and the query string or fragment of a signed URL are hidden, so a failed install or `pnpm add <url>` cannot leak them into terminal scrollback or CI logs.
|
|
8
|
+
|
|
9
|
+
## 1100.1.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Added `fetchWarnTimeoutMs` and `fetchMinSpeedKiBps` to the Rust pnpm CLI and its N-API bindings. Slow registry metadata requests and tarball downloads now emit pnpm-compatible warnings without exposing URL credentials, query parameters, fragments, or control characters [pnpm/pnpm#12042](https://github.com/pnpm/pnpm/issues/12042).
|
|
14
|
+
|
|
15
|
+
- Updated dependencies:
|
|
16
|
+
- @pnpm/constants@1102.0.0
|
|
17
|
+
|
|
3
18
|
## 1100.1.2
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/lib/index.d.ts
CHANGED
|
@@ -27,19 +27,6 @@ export declare class FetchError extends PnpmError {
|
|
|
27
27
|
readonly request: FetchErrorRequest;
|
|
28
28
|
constructor(request: FetchErrorRequest, response: FetchErrorResponse, hint?: string);
|
|
29
29
|
}
|
|
30
|
-
/**
|
|
31
|
-
* Strip `user:pass@` (or `user@`) userinfo that follows a URL scheme in any
|
|
32
|
-
* text, e.g. `GET https://user:pass@host/pkg: …` → `GET https://host/pkg: …`.
|
|
33
|
-
* A registry configured as `https://user:pass@host/` would otherwise leak its
|
|
34
|
-
* embedded basic-auth credentials into every error message that interpolates
|
|
35
|
-
* the request URL (terminal output, CI logs). `FetchError` already hides the
|
|
36
|
-
* auth *header*; this covers credentials carried in the URL itself.
|
|
37
|
-
*
|
|
38
|
-
* Implemented as a single forward scan rather than a regex: `text` is
|
|
39
|
-
* uncontrolled (it interpolates the request URL), so a backtracking pattern is
|
|
40
|
-
* a ReDoS vector, and the scan strips up to the **last** `@` in the authority
|
|
41
|
-
* so a raw `@` inside the password (`user:p@ss@host`) doesn't leak its tail.
|
|
42
|
-
*/
|
|
43
30
|
export declare function redactUrlCredentials(text: string): string;
|
|
44
31
|
/**
|
|
45
32
|
* Make untrusted, URL-bearing text safe to print or log: redact inline
|
|
@@ -49,6 +36,13 @@ export declare function redactUrlCredentials(text: string): string;
|
|
|
49
36
|
* or inject terminal output via raw escapes / `\r` / `\n`.
|
|
50
37
|
*/
|
|
51
38
|
export declare function redactAndSanitize(text: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* Make a URL safe for user-visible output without exposing credentials,
|
|
41
|
+
* query parameters, fragments, or terminal control characters. Malformed
|
|
42
|
+
* URLs are replaced entirely because their authority cannot be redacted
|
|
43
|
+
* reliably.
|
|
44
|
+
*/
|
|
45
|
+
export declare function redactUrlForDisplay(url: string): string;
|
|
52
46
|
/**
|
|
53
47
|
* {@link redactAndSanitize} for text whose line breaks are worth keeping, such
|
|
54
48
|
* as a subprocess's multi-line stderr.
|
package/lib/index.js
CHANGED
|
@@ -22,7 +22,7 @@ export class FetchError extends PnpmError {
|
|
|
22
22
|
if (request.authHeaderValue) {
|
|
23
23
|
_request.authHeaderValue = hideAuthInformation(request.authHeaderValue);
|
|
24
24
|
}
|
|
25
|
-
const message = `GET ${
|
|
25
|
+
const message = `GET ${redactUrlSecrets(request.url)}: ${response.statusText} - ${response.status}`;
|
|
26
26
|
// NOTE: For security reasons, some registries respond with 404 on authentication errors as well.
|
|
27
27
|
// So we print authorization info on 404 errors as well.
|
|
28
28
|
if (response.status === 401 || response.status === 403 || response.status === 404) {
|
|
@@ -52,6 +52,32 @@ export class FetchError extends PnpmError {
|
|
|
52
52
|
* a ReDoS vector, and the scan strips up to the **last** `@` in the authority
|
|
53
53
|
* so a raw `@` inside the password (`user:p@ss@host`) doesn't leak its tail.
|
|
54
54
|
*/
|
|
55
|
+
/**
|
|
56
|
+
* A request URL made safe to print: its `user:pass@` userinfo and control
|
|
57
|
+
* characters redacted ({@link redactAndSanitize}), then everything from the
|
|
58
|
+
* query or fragment onwards dropped — a signed tarball or registry URL
|
|
59
|
+
* carries a reusable token there, which credential redaction alone keeps.
|
|
60
|
+
*
|
|
61
|
+
* `[hidden]` when the cut would leave credential material behind. A password
|
|
62
|
+
* containing `?` or `#` defeats the userinfo scan — it reads the `?` as the
|
|
63
|
+
* end of the authority and leaves the whole `user:pa?ss@host` in place — so
|
|
64
|
+
* cutting there would publish the password's prefix. The authority is
|
|
65
|
+
* therefore checked before the cut, not after: any `@` still in front of the
|
|
66
|
+
* path means the userinfo survived.
|
|
67
|
+
*
|
|
68
|
+
* Cuts rather than re-rendering through `URL`, unlike
|
|
69
|
+
* {@link redactUrlForDisplay}: that round-trip normalizes the spelling
|
|
70
|
+
* (`https://host` gains a trailing slash, percent-escapes change case), and
|
|
71
|
+
* an error message should echo the URL the request was given.
|
|
72
|
+
*/
|
|
73
|
+
function redactUrlSecrets(url) {
|
|
74
|
+
const sanitized = redactAndSanitize(url);
|
|
75
|
+
const schemeEnd = sanitized.indexOf('://');
|
|
76
|
+
const afterScheme = schemeEnd === -1 ? sanitized : sanitized.slice(schemeEnd + '://'.length);
|
|
77
|
+
if (afterScheme.split('/')[0].includes('@'))
|
|
78
|
+
return '[hidden]';
|
|
79
|
+
return sanitized.split(/[?#]/)[0];
|
|
80
|
+
}
|
|
55
81
|
export function redactUrlCredentials(text) {
|
|
56
82
|
let result = '';
|
|
57
83
|
let cursor = 0;
|
|
@@ -94,14 +120,27 @@ export function redactAndSanitize(text) {
|
|
|
94
120
|
// userinfo (`user:pass\r@host`) would otherwise split the authority across
|
|
95
121
|
// the redaction scan, and removing it afterwards would rejoin the
|
|
96
122
|
// credentials into the output.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
123
|
+
return redactUrlCredentials(sanitizeControlCharacters(text));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Make a URL safe for user-visible output without exposing credentials,
|
|
127
|
+
* query parameters, fragments, or terminal control characters. Malformed
|
|
128
|
+
* URLs are replaced entirely because their authority cannot be redacted
|
|
129
|
+
* reliably.
|
|
130
|
+
*/
|
|
131
|
+
export function redactUrlForDisplay(url) {
|
|
132
|
+
let display;
|
|
133
|
+
try {
|
|
134
|
+
display = new URL(sanitizeControlCharacters(url));
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return '[hidden]';
|
|
103
138
|
}
|
|
104
|
-
|
|
139
|
+
display.username = '';
|
|
140
|
+
display.password = '';
|
|
141
|
+
display.search = '';
|
|
142
|
+
display.hash = '';
|
|
143
|
+
return display.href;
|
|
105
144
|
}
|
|
106
145
|
/**
|
|
107
146
|
* {@link redactAndSanitize} for text whose line breaks are worth keeping, such
|
|
@@ -124,6 +163,16 @@ function isSchemeTailChar(code) {
|
|
|
124
163
|
function isAsciiWhitespace(code) {
|
|
125
164
|
return code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0b || code === 0x0c || code === 0x0d;
|
|
126
165
|
}
|
|
166
|
+
function sanitizeControlCharacters(text) {
|
|
167
|
+
let sanitized = '';
|
|
168
|
+
for (const char of text) {
|
|
169
|
+
const code = char.codePointAt(0);
|
|
170
|
+
if (code <= 0x1f || (code >= 0x7f && code <= 0x9f))
|
|
171
|
+
continue;
|
|
172
|
+
sanitized += char;
|
|
173
|
+
}
|
|
174
|
+
return sanitized;
|
|
175
|
+
}
|
|
127
176
|
function hideAuthInformation(authHeaderValue) {
|
|
128
177
|
const [authType, token] = authHeaderValue.split(' ');
|
|
129
178
|
if (token == null)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/error",
|
|
3
|
-
"version": "1100.1.
|
|
3
|
+
"version": "1100.1.4",
|
|
4
4
|
"description": "An error class for pnpm errors",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
"!*.map"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@pnpm/constants": "
|
|
31
|
+
"@pnpm/constants": "1102.0.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@jest/globals": "30.4.1",
|
|
35
|
-
"@pnpm/error": "1100.1.
|
|
35
|
+
"@pnpm/error": "1100.1.4"
|
|
36
36
|
},
|
|
37
37
|
"engines": {
|
|
38
38
|
"node": ">=22.13"
|