@aexol/spectral 0.9.177 → 0.9.178

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.
@@ -7,7 +7,9 @@
7
7
  *
8
8
  * This module converts any caught error into a short, safe, user-facing
9
9
  * string. Raw errors are still logged server-side at the catch sites; this
10
- * helper only controls the user-facing message.
10
+ * helper only controls the user-facing message. Unknown, non-transient errors
11
+ * keep a sanitized version of their underlying cause so the UI can show what
12
+ * actually happened instead of only a generic fallback.
11
13
  */
12
14
  /**
13
15
  * Classification regex reused from agent-loop.ts (retry classification).
@@ -22,10 +24,13 @@ export declare const PROVIDER_ERROR_PATTERNS: RegExp;
22
24
  * 1. Rate limit / 429 / overloaded / 529 / "too many requests" /
23
25
  * "temporarily rate-limited" → "Rate limit reached. Please try again in a moment."
24
26
  * 2. timeout / econnreset / "fetch failed" / network → "Network error. Please try again."
25
- * 3. Any other Error or non-Error value "Something went wrong. Please try again."
27
+ * 3. Any other Error / stringa sanitized, concise version of the underlying
28
+ * cause. Non-string/non-Error values (or values whose cause is empty after
29
+ * sanitization) → "Something went wrong. Please try again."
26
30
  *
27
- * Never leaks raw messages, JSON blobs, or internal identifiers (org_...,
28
- * user_id, provider_name, is_byok, upstream model names).
31
+ * The sanitizer still strips JSON blobs, stack frames, URLs, absolute paths,
32
+ * and internal identifiers (org_..., user_id, provider_name, is_byok,
33
+ * upstream model names).
29
34
  */
30
35
  export declare function humanizeProviderError(err: unknown): string;
31
36
  //# sourceMappingURL=error-humanizer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"error-humanizer.d.ts","sourceRoot":"","sources":["../../src/server/error-humanizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,QACsN,CAAC;AAoC3P;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAa1D"}
1
+ {"version":3,"file":"error-humanizer.d.ts","sourceRoot":"","sources":["../../src/server/error-humanizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,QACsN,CAAC;AAqC3P;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAa1D"}
@@ -7,7 +7,9 @@
7
7
  *
8
8
  * This module converts any caught error into a short, safe, user-facing
9
9
  * string. Raw errors are still logged server-side at the catch sites; this
10
- * helper only controls the user-facing message.
10
+ * helper only controls the user-facing message. Unknown, non-transient errors
11
+ * keep a sanitized version of their underlying cause so the UI can show what
12
+ * actually happened instead of only a generic fallback.
11
13
  */
12
14
  /**
13
15
  * Classification regex reused from agent-loop.ts (retry classification).
@@ -18,6 +20,7 @@ export const PROVIDER_ERROR_PATTERNS = /rate.?limit|429|overloaded|529|timeout|n
18
20
  const RATE_LIMIT_MESSAGE = "Rate limit reached. Please try again in a moment.";
19
21
  const NETWORK_ERROR_MESSAGE = "Network error. Please try again.";
20
22
  const GENERIC_MESSAGE = "Something went wrong. Please try again.";
23
+ const MAX_USER_ERROR_LENGTH = 240;
21
24
  /**
22
25
  * Coerce any caught value into a single string suitable for regex matching,
23
26
  * without ever returning it to the user. This is only used internally for
@@ -57,10 +60,13 @@ function toErrorString(err) {
57
60
  * 1. Rate limit / 429 / overloaded / 529 / "too many requests" /
58
61
  * "temporarily rate-limited" → "Rate limit reached. Please try again in a moment."
59
62
  * 2. timeout / econnreset / "fetch failed" / network → "Network error. Please try again."
60
- * 3. Any other Error or non-Error value "Something went wrong. Please try again."
63
+ * 3. Any other Error / stringa sanitized, concise version of the underlying
64
+ * cause. Non-string/non-Error values (or values whose cause is empty after
65
+ * sanitization) → "Something went wrong. Please try again."
61
66
  *
62
- * Never leaks raw messages, JSON blobs, or internal identifiers (org_...,
63
- * user_id, provider_name, is_byok, upstream model names).
67
+ * The sanitizer still strips JSON blobs, stack frames, URLs, absolute paths,
68
+ * and internal identifiers (org_..., user_id, provider_name, is_byok,
69
+ * upstream model names).
64
70
  */
65
71
  export function humanizeProviderError(err) {
66
72
  const text = toErrorString(err);
@@ -73,5 +79,69 @@ export function humanizeProviderError(err) {
73
79
  if (PROVIDER_ERROR_PATTERNS.test(text)) {
74
80
  return NETWORK_ERROR_MESSAGE;
75
81
  }
76
- return GENERIC_MESSAGE;
82
+ return extractSafeErrorCause(err) || GENERIC_MESSAGE;
83
+ }
84
+ /**
85
+ * Extract the deepest, non-empty, human-readable cause from an Error/string
86
+ * and remove anything that could leak internal identifiers, credentials,
87
+ * JSON payloads, stack frames, URLs, or absolute filesystem paths.
88
+ *
89
+ * Non-Error, non-string values intentionally have no safe textual cause and
90
+ * fall back to the generic message.
91
+ */
92
+ function extractSafeErrorCause(err) {
93
+ if (err instanceof Error) {
94
+ const parts = [];
95
+ let current = err;
96
+ while (current instanceof Error) {
97
+ const sanitized = sanitizeErrorCause(current.message);
98
+ if (sanitized)
99
+ parts.push(sanitized);
100
+ current = current.cause;
101
+ }
102
+ // Some SDKs attach a non-Error cause (usually a string or parsed JSON).
103
+ if (typeof current === "string") {
104
+ const sanitized = sanitizeErrorCause(current);
105
+ if (sanitized)
106
+ parts.push(sanitized);
107
+ }
108
+ return parts.length > 0 ? parts[parts.length - 1] : "";
109
+ }
110
+ if (typeof err === "string")
111
+ return sanitizeErrorCause(err);
112
+ return "";
113
+ }
114
+ function sanitizeErrorCause(raw) {
115
+ if (!raw)
116
+ return "";
117
+ let text = raw;
118
+ // Remove whole JSON payloads. Raw provider errors often wrap the cause in a
119
+ // JSON blob carrying org ids, provider names, and model names.
120
+ text = text.replace(/\{[\s\S]*\}/g, " ");
121
+ text = text.replace(/\[[\s\S]*\]/g, " ");
122
+ // Remove URLs (may embed signed tokens).
123
+ text = text.replace(/\bhttps?:\/\/\S+/gi, " ");
124
+ // Remove absolute filesystem paths (POSIX and Windows).
125
+ text = text.replace(/(?:^|[\s("'])\/[\w.@-]+(?:\/[\w.@-]+)*/g, " ");
126
+ text = text.replace(/(?:^|[\s("'])[A-Za-z]:[\\/][^\s]*/g, " ");
127
+ // Remove internal identifiers and sensitive key/value metadata. Key/value
128
+ // metadata is stripped first so an identifier inside a value is consumed
129
+ // with the key rather than leaving a dangling key that then eats the next
130
+ // word in the message.
131
+ text = text.replace(/\b(?:provider_name|user_id|is_byok|api[_-]?key|authorization|secret|token)\s*[:=]\s*[^\s,;]+/gi, " ");
132
+ text = text.replace(/\bmodel\s*[:=]\s*[^\s,;]+/gi, " ");
133
+ text = text.replace(/\borg_[A-Za-z0-9_-]+\b/gi, " ");
134
+ // Remove stack-trace frames (multi-line Error messages).
135
+ text = text.replace(/^\s+at\s+[^\n]*/gm, " ");
136
+ // Remove ANSI escape sequences and normalize whitespace/punctuation noise.
137
+ text = text
138
+ .replace(/\u001b\[[0-9;]*m/g, " ")
139
+ .replace(/\s+/g, " ")
140
+ .replace(/\s*([,;:])\s*(?=[,;:.])/g, "")
141
+ .trim();
142
+ if (!text)
143
+ return "";
144
+ if (text.length <= MAX_USER_ERROR_LENGTH)
145
+ return text;
146
+ return `${text.slice(0, MAX_USER_ERROR_LENGTH - 1).trimEnd()}…`;
77
147
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.9.177",
3
+ "version": "0.9.178",
4
4
  "description": "AI coding agent for Aexol with relay-based browser access.",
5
5
  "type": "module",
6
6
  "private": false,