@pnpm/error 1100.1.3 → 1100.2.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @pnpm/error
2
2
 
3
+ ## 1100.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - When the registry stops sending data for longer than `fetchTimeout`, pnpm now reports that the metadata or tarball request timed out. Previously the error did not mention the timeout [#3646](https://github.com/pnpm/pnpm/issues/3646).
8
+
9
+ ## 1100.1.4
10
+
11
+ ### Patch Changes
12
+
13
+ - 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.
14
+
3
15
  ## 1100.1.3
4
16
 
5
17
  ### Patch Changes
package/lib/index.d.ts CHANGED
@@ -28,18 +28,16 @@ export declare class FetchError extends PnpmError {
28
28
  constructor(request: FetchErrorRequest, response: FetchErrorResponse, hint?: string);
29
29
  }
30
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.
31
+ * undici fails a timed-out request with a bare `fetch failed` or `terminated`
32
+ * and keeps the timeout only in the error's `cause` chain.
42
33
  */
34
+ export declare function isFetchTimeoutError(error: unknown): boolean;
35
+ export declare class FetchTimeoutError extends PnpmError {
36
+ constructor(code: string, url: string, timeout: number | undefined, opts: {
37
+ attempts?: number;
38
+ cause: unknown;
39
+ });
40
+ }
43
41
  export declare function redactUrlCredentials(text: string): string;
44
42
  /**
45
43
  * Make untrusted, URL-bearing text safe to print or log: redact inline
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 ${redactUrlCredentials(request.url)}: ${response.statusText} - ${response.status}`;
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) {
@@ -39,6 +39,34 @@ export class FetchError extends PnpmError {
39
39
  this.response = response;
40
40
  }
41
41
  }
42
+ /**
43
+ * undici codes for a request that made no progress for `fetchTimeout`: no
44
+ * response head, or a body that stopped arriving.
45
+ */
46
+ const FETCH_TIMEOUT_ERROR_CODES = new Set(['UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT']);
47
+ /**
48
+ * undici fails a timed-out request with a bare `fetch failed` or `terminated`
49
+ * and keeps the timeout only in the error's `cause` chain.
50
+ */
51
+ export function isFetchTimeoutError(error) {
52
+ let current = error;
53
+ for (let depth = 0; depth < 8 && current != null && typeof current === 'object'; depth++) {
54
+ const { code, cause } = current;
55
+ if (typeof code === 'string' && FETCH_TIMEOUT_ERROR_CODES.has(code))
56
+ return true;
57
+ current = cause;
58
+ }
59
+ return false;
60
+ }
61
+ export class FetchTimeoutError extends PnpmError {
62
+ constructor(code, url, timeout, opts) {
63
+ const reason = timeout == null ? 'waiting for data' : `no data received for ${timeout}ms`;
64
+ super(code, `GET ${redactUrlForDisplay(url)}: timed out, ${reason}`, {
65
+ ...opts,
66
+ hint: 'The registry stopped responding. If it is just slow, increase the fetchTimeout setting.',
67
+ });
68
+ }
69
+ }
42
70
  /**
43
71
  * Strip `user:pass@` (or `user@`) userinfo that follows a URL scheme in any
44
72
  * text, e.g. `GET https://user:pass@host/pkg: …` → `GET https://host/pkg: …`.
@@ -52,6 +80,32 @@ export class FetchError extends PnpmError {
52
80
  * a ReDoS vector, and the scan strips up to the **last** `@` in the authority
53
81
  * so a raw `@` inside the password (`user:p@ss@host`) doesn't leak its tail.
54
82
  */
83
+ /**
84
+ * A request URL made safe to print: its `user:pass@` userinfo and control
85
+ * characters redacted ({@link redactAndSanitize}), then everything from the
86
+ * query or fragment onwards dropped — a signed tarball or registry URL
87
+ * carries a reusable token there, which credential redaction alone keeps.
88
+ *
89
+ * `[hidden]` when the cut would leave credential material behind. A password
90
+ * containing `?` or `#` defeats the userinfo scan — it reads the `?` as the
91
+ * end of the authority and leaves the whole `user:pa?ss@host` in place — so
92
+ * cutting there would publish the password's prefix. The authority is
93
+ * therefore checked before the cut, not after: any `@` still in front of the
94
+ * path means the userinfo survived.
95
+ *
96
+ * Cuts rather than re-rendering through `URL`, unlike
97
+ * {@link redactUrlForDisplay}: that round-trip normalizes the spelling
98
+ * (`https://host` gains a trailing slash, percent-escapes change case), and
99
+ * an error message should echo the URL the request was given.
100
+ */
101
+ function redactUrlSecrets(url) {
102
+ const sanitized = redactAndSanitize(url);
103
+ const schemeEnd = sanitized.indexOf('://');
104
+ const afterScheme = schemeEnd === -1 ? sanitized : sanitized.slice(schemeEnd + '://'.length);
105
+ if (afterScheme.split('/')[0].includes('@'))
106
+ return '[hidden]';
107
+ return sanitized.split(/[?#]/)[0];
108
+ }
55
109
  export function redactUrlCredentials(text) {
56
110
  let result = '';
57
111
  let cursor = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/error",
3
- "version": "1100.1.3",
3
+ "version": "1100.2.0",
4
4
  "description": "An error class for pnpm errors",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "devDependencies": {
34
34
  "@jest/globals": "30.4.1",
35
- "@pnpm/error": "1100.1.3"
35
+ "@pnpm/error": "1100.2.0"
36
36
  },
37
37
  "engines": {
38
38
  "node": ">=22.13"