@veryfront/ext-content-mdx 0.1.1186 → 0.1.1189

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.
Files changed (163) hide show
  1. package/esm/_dnt.polyfills.d.ts +6 -6
  2. package/esm/_dnt.polyfills.d.ts.map +1 -1
  3. package/esm/deno.d.ts +18 -0
  4. package/esm/deno.js +30 -9
  5. package/esm/src/config/defaults.d.ts +31 -24
  6. package/esm/src/config/defaults.d.ts.map +1 -1
  7. package/esm/src/config/defaults.js +25 -18
  8. package/esm/src/errors/diagnostic-policy.d.ts +35 -0
  9. package/esm/src/errors/diagnostic-policy.d.ts.map +1 -0
  10. package/esm/src/errors/diagnostic-policy.js +100 -0
  11. package/esm/src/errors/error-registry/boundary.d.ts +2 -0
  12. package/esm/src/errors/error-registry/boundary.d.ts.map +1 -1
  13. package/esm/src/errors/error-registry/boundary.js +8 -0
  14. package/esm/src/errors/error-registry/config.d.ts.map +1 -1
  15. package/esm/src/errors/error-registry/config.js +3 -3
  16. package/esm/src/errors/error-registry/deploy.js +1 -1
  17. package/esm/src/errors/error-registry/server.d.ts +2 -0
  18. package/esm/src/errors/error-registry/server.d.ts.map +1 -1
  19. package/esm/src/errors/error-registry/server.js +8 -0
  20. package/esm/src/errors/error-registry-helpers.d.ts +11 -2
  21. package/esm/src/errors/error-registry-helpers.d.ts.map +1 -1
  22. package/esm/src/errors/error-registry-helpers.js +70 -1
  23. package/esm/src/errors/error-registry.d.ts +85 -73
  24. package/esm/src/errors/error-registry.d.ts.map +1 -1
  25. package/esm/src/errors/error-registry.js +2 -14
  26. package/esm/src/errors/types.d.ts +44 -5
  27. package/esm/src/errors/types.d.ts.map +1 -1
  28. package/esm/src/errors/types.js +148 -24
  29. package/esm/src/errors/veryfront-error.d.ts +25 -2
  30. package/esm/src/errors/veryfront-error.d.ts.map +1 -1
  31. package/esm/src/errors/veryfront-error.js +638 -28
  32. package/esm/src/observability/limits.d.ts +20 -0
  33. package/esm/src/observability/limits.d.ts.map +1 -0
  34. package/esm/src/observability/limits.js +19 -0
  35. package/esm/src/observability/telemetry-error.d.ts +22 -0
  36. package/esm/src/observability/telemetry-error.d.ts.map +1 -0
  37. package/esm/src/observability/telemetry-error.js +377 -0
  38. package/esm/src/observability/tracing/api-shim.d.ts +39 -0
  39. package/esm/src/observability/tracing/api-shim.d.ts.map +1 -1
  40. package/esm/src/observability/tracing/api-shim.js +196 -69
  41. package/esm/src/observability/tracing/context-callback.d.ts +14 -0
  42. package/esm/src/observability/tracing/context-callback.d.ts.map +1 -0
  43. package/esm/src/observability/tracing/context-callback.js +111 -0
  44. package/esm/src/observability/tracing/otlp-setup.d.ts.map +1 -1
  45. package/esm/src/observability/tracing/otlp-setup.js +190 -46
  46. package/esm/src/platform/adapters/base.d.ts +51 -7
  47. package/esm/src/platform/adapters/base.d.ts.map +1 -1
  48. package/esm/src/platform/adapters/base.js +4 -2
  49. package/esm/src/platform/adapters/bounded-file-read.d.ts +9 -0
  50. package/esm/src/platform/adapters/bounded-file-read.d.ts.map +1 -1
  51. package/esm/src/platform/adapters/bounded-file-read.js +11 -5
  52. package/esm/src/platform/adapters/file-snapshot-error.d.ts +2 -0
  53. package/esm/src/platform/adapters/file-snapshot-error.d.ts.map +1 -1
  54. package/esm/src/platform/adapters/file-snapshot-error.js +2 -0
  55. package/esm/src/platform/adapters/file-system-capabilities.d.ts +74 -0
  56. package/esm/src/platform/adapters/file-system-capabilities.d.ts.map +1 -0
  57. package/esm/src/platform/adapters/file-system-capabilities.js +508 -0
  58. package/esm/src/platform/adapters/native-file-system-provenance.d.ts +6 -0
  59. package/esm/src/platform/adapters/native-file-system-provenance.d.ts.map +1 -0
  60. package/esm/src/platform/adapters/native-file-system-provenance.js +15 -0
  61. package/esm/src/platform/adapters/runtime/deno/filesystem-adapter.d.ts +45 -0
  62. package/esm/src/platform/adapters/runtime/deno/filesystem-adapter.d.ts.map +1 -0
  63. package/esm/src/platform/adapters/runtime/deno/filesystem-adapter.js +291 -0
  64. package/esm/src/platform/adapters/runtime/shared/node-filesystem-adapter.d.ts +71 -0
  65. package/esm/src/platform/adapters/runtime/shared/node-filesystem-adapter.d.ts.map +1 -0
  66. package/esm/src/platform/adapters/runtime/shared/node-filesystem-adapter.js +471 -0
  67. package/esm/src/platform/adapters/runtime/shared/shared-watcher.d.ts +16 -0
  68. package/esm/src/platform/adapters/runtime/shared/shared-watcher.d.ts.map +1 -0
  69. package/esm/src/platform/adapters/runtime/shared/shared-watcher.js +140 -0
  70. package/esm/src/platform/adapters/runtime/shared/temp-dir.d.ts +2 -0
  71. package/esm/src/platform/adapters/runtime/shared/temp-dir.d.ts.map +1 -0
  72. package/esm/src/platform/adapters/runtime/shared/temp-dir.js +10 -0
  73. package/esm/src/platform/adapters/runtime/shared/watcher-queue.d.ts +5 -0
  74. package/esm/src/platform/adapters/runtime/shared/watcher-queue.d.ts.map +1 -0
  75. package/esm/src/platform/adapters/runtime/shared/watcher-queue.js +75 -0
  76. package/esm/src/platform/compat/abort-signal.d.ts +20 -0
  77. package/esm/src/platform/compat/abort-signal.d.ts.map +1 -0
  78. package/esm/src/platform/compat/abort-signal.js +64 -0
  79. package/esm/src/platform/compat/constants.d.ts +2 -2
  80. package/esm/src/platform/compat/constants.d.ts.map +1 -1
  81. package/esm/src/platform/compat/constants.js +2 -2
  82. package/esm/src/platform/compat/error-introspection.d.ts +37 -0
  83. package/esm/src/platform/compat/error-introspection.d.ts.map +1 -0
  84. package/esm/src/platform/compat/error-introspection.js +258 -0
  85. package/esm/src/platform/compat/fs.d.ts +29 -8
  86. package/esm/src/platform/compat/fs.d.ts.map +1 -1
  87. package/esm/src/platform/compat/fs.js +123 -57
  88. package/esm/src/platform/compat/native-brand-checks.d.ts +14 -0
  89. package/esm/src/platform/compat/native-brand-checks.d.ts.map +1 -0
  90. package/esm/src/platform/compat/native-brand-checks.js +68 -0
  91. package/esm/src/platform/compat/not-found-error.d.ts +8 -3
  92. package/esm/src/platform/compat/not-found-error.d.ts.map +1 -1
  93. package/esm/src/platform/compat/not-found-error.js +164 -31
  94. package/esm/src/platform/compat/path/security.d.ts.map +1 -1
  95. package/esm/src/platform/compat/path/security.js +3 -5
  96. package/esm/src/platform/compat/process/command.d.ts +24 -1
  97. package/esm/src/platform/compat/process/command.d.ts.map +1 -1
  98. package/esm/src/platform/compat/process/command.js +393 -78
  99. package/esm/src/platform/compat/process/env.d.ts +11 -0
  100. package/esm/src/platform/compat/process/env.d.ts.map +1 -1
  101. package/esm/src/platform/compat/process/env.js +28 -38
  102. package/esm/src/platform/compat/process/global-error.d.ts +19 -0
  103. package/esm/src/platform/compat/process/global-error.d.ts.map +1 -0
  104. package/esm/src/platform/compat/process/global-error.js +57 -0
  105. package/esm/src/platform/compat/process/lifecycle.d.ts +10 -3
  106. package/esm/src/platform/compat/process/lifecycle.d.ts.map +1 -1
  107. package/esm/src/platform/compat/process/lifecycle.js +74 -23
  108. package/esm/src/platform/compat/process/project-env-contract.d.ts +19 -0
  109. package/esm/src/platform/compat/process/project-env-contract.d.ts.map +1 -0
  110. package/esm/src/platform/compat/process/project-env-contract.js +19 -0
  111. package/esm/src/platform/compat/runtime.d.ts +14 -5
  112. package/esm/src/platform/compat/runtime.d.ts.map +1 -1
  113. package/esm/src/platform/compat/runtime.js +34 -9
  114. package/esm/src/platform/compat/std/front-matter-yaml.d.ts +12 -0
  115. package/esm/src/platform/compat/std/front-matter-yaml.d.ts.map +1 -1
  116. package/esm/src/platform/compat/std/front-matter-yaml.js +35 -3
  117. package/esm/src/platform/compat/temp-directory-prefix.d.ts +8 -0
  118. package/esm/src/platform/compat/temp-directory-prefix.d.ts.map +1 -0
  119. package/esm/src/platform/compat/temp-directory-prefix.js +15 -0
  120. package/esm/src/utils/abort.d.ts +10 -0
  121. package/esm/src/utils/abort.d.ts.map +1 -0
  122. package/esm/src/utils/abort.js +35 -0
  123. package/esm/src/utils/constants/build.d.ts +3 -0
  124. package/esm/src/utils/constants/build.d.ts.map +1 -1
  125. package/esm/src/utils/constants/build.js +5 -1
  126. package/esm/src/utils/constants/cache.d.ts +21 -0
  127. package/esm/src/utils/constants/cache.d.ts.map +1 -1
  128. package/esm/src/utils/constants/cache.js +101 -39
  129. package/esm/src/utils/constants/css.d.ts +20 -0
  130. package/esm/src/utils/constants/css.d.ts.map +1 -0
  131. package/esm/src/utils/constants/css.js +19 -0
  132. package/esm/src/utils/constants/limits.d.ts +4 -0
  133. package/esm/src/utils/constants/limits.d.ts.map +1 -1
  134. package/esm/src/utils/constants/limits.js +4 -0
  135. package/esm/src/utils/constants/project-identity.d.ts +5 -0
  136. package/esm/src/utils/constants/project-identity.d.ts.map +1 -0
  137. package/esm/src/utils/constants/project-identity.js +4 -0
  138. package/esm/src/utils/constants/retry.d.ts +18 -0
  139. package/esm/src/utils/constants/retry.d.ts.map +1 -1
  140. package/esm/src/utils/constants/retry.js +18 -0
  141. package/esm/src/utils/constants/security.d.ts +4 -0
  142. package/esm/src/utils/constants/security.d.ts.map +1 -1
  143. package/esm/src/utils/constants/security.js +4 -0
  144. package/esm/src/utils/index.d.ts +3 -1
  145. package/esm/src/utils/index.d.ts.map +1 -1
  146. package/esm/src/utils/index.js +3 -1
  147. package/esm/src/utils/logger/core.d.ts +5 -0
  148. package/esm/src/utils/logger/core.d.ts.map +1 -1
  149. package/esm/src/utils/logger/core.js +67 -2
  150. package/esm/src/utils/logger/redact.d.ts +36 -13
  151. package/esm/src/utils/logger/redact.d.ts.map +1 -1
  152. package/esm/src/utils/logger/redact.js +573 -83
  153. package/esm/src/utils/parallel.d.ts.map +1 -1
  154. package/esm/src/utils/parallel.js +11 -1
  155. package/esm/src/utils/project-identity.d.ts +11 -0
  156. package/esm/src/utils/project-identity.d.ts.map +1 -0
  157. package/esm/src/utils/project-identity.js +69 -0
  158. package/esm/src/utils/version-constant.d.ts +1 -1
  159. package/esm/src/utils/version-constant.js +1 -1
  160. package/package.json +3 -3
  161. package/esm/src/platform/adapters/runtime/shared/native-file-capabilities.d.ts +0 -33
  162. package/esm/src/platform/adapters/runtime/shared/native-file-capabilities.d.ts.map +0 -1
  163. package/esm/src/platform/adapters/runtime/shared/native-file-capabilities.js +0 -205
@@ -9,19 +9,124 @@
9
9
  * credential before serialization, so an accidental
10
10
  * `logger.info("...", { authorization: token })` cannot leak the secret.
11
11
  *
12
- * Scope is intentionally key-based: we do not attempt to find secrets embedded
13
- * in free-form message strings (too lossy). The deny-list errs toward
14
- * over-redaction — masking a benign `tokenCount` is acceptable; leaking a real
15
- * token is not. The traversal fails *closed*: on a cycle, depth overflow, or a
16
- * throwing getter it returns {@link REDACTED} rather than risk emitting an
17
- * unredacted object.
12
+ * Sensitive keys are masked and every string value is scrubbed for credentials
13
+ * embedded in URL userinfo, query parameters, or fragment parameters. The
14
+ * deny-list errs toward over-redaction — masking a benign `tokenCount` is
15
+ * acceptable; leaking a real token is not. The traversal fails *closed*: on a
16
+ * cycle, depth overflow, or a throwing getter it returns {@link REDACTED}
17
+ * rather than risk emitting an unredacted object.
18
18
  */
19
- import { isRecord } from "./core.js";
20
19
  /** Replacement value substituted for any sensitive field. */
21
20
  export const REDACTED = "[REDACTED]";
21
+ const apply = Reflect.apply;
22
+ const NativeUint32Array = Uint32Array;
23
+ const regExpExec = RegExp.prototype.exec;
24
+ const regExpReplace = RegExp.prototype[Symbol.replace];
25
+ const stringCharCodeAt = String.prototype.charCodeAt;
26
+ const stringSlice = String.prototype.slice;
27
+ const stringToLowerCase = String.prototype.toLowerCase;
28
+ const NON_ALPHANUMERIC_PATTERN = /[^a-z0-9]/g;
22
29
  /** Strip all non-alphanumeric characters and lowercase, used for key normalization. */
23
30
  function normalizeToAlphanumeric(s) {
24
- return s.toLowerCase().replace(/[^a-z0-9]/g, "");
31
+ const lowercase = apply(stringToLowerCase, s, []);
32
+ return apply(regExpReplace, NON_ALPHANUMERIC_PATTERN, [lowercase, ""]);
33
+ }
34
+ const FORWARD_SLASH_CODE_UNIT = 47;
35
+ const BACKSLASH_CODE_UNIT = 92;
36
+ const COLON_CODE_UNIT = 58;
37
+ const ASCII_UPPERCASE_A_CODE_UNIT = 65;
38
+ const ASCII_UPPERCASE_Z_CODE_UNIT = 90;
39
+ const ASCII_LOWERCASE_OFFSET = 32;
40
+ function stringCodeUnitAt(value, index) {
41
+ return apply(stringCharCodeAt, value, [index]);
42
+ }
43
+ function sliceString(value, start, end) {
44
+ return end === undefined
45
+ ? apply(stringSlice, value, [start])
46
+ : apply(stringSlice, value, [start, end]);
47
+ }
48
+ function isPathSeparatorCodeUnit(codeUnit) {
49
+ return codeUnit === FORWARD_SLASH_CODE_UNIT || codeUnit === BACKSLASH_CODE_UNIT;
50
+ }
51
+ function isAsciiLetterCodeUnit(codeUnit) {
52
+ const lowercase = codeUnit >= ASCII_UPPERCASE_A_CODE_UNIT &&
53
+ codeUnit <= ASCII_UPPERCASE_Z_CODE_UNIT
54
+ ? codeUnit + ASCII_LOWERCASE_OFFSET
55
+ : codeUnit;
56
+ return lowercase >= 97 && lowercase <= 122;
57
+ }
58
+ function isWindowsPath(path) {
59
+ if (path.length >= 2) {
60
+ const first = stringCodeUnitAt(path, 0);
61
+ const second = stringCodeUnitAt(path, 1);
62
+ if (isPathSeparatorCodeUnit(first) && isPathSeparatorCodeUnit(second))
63
+ return true;
64
+ }
65
+ return path.length >= 3 &&
66
+ isAsciiLetterCodeUnit(stringCodeUnitAt(path, 0)) &&
67
+ stringCodeUnitAt(path, 1) === COLON_CODE_UNIT &&
68
+ isPathSeparatorCodeUnit(stringCodeUnitAt(path, 2));
69
+ }
70
+ function normalizePathCodeUnit(codeUnit, foldAsciiCase) {
71
+ if (isPathSeparatorCodeUnit(codeUnit))
72
+ return FORWARD_SLASH_CODE_UNIT;
73
+ return foldAsciiCase &&
74
+ codeUnit >= ASCII_UPPERCASE_A_CODE_UNIT &&
75
+ codeUnit <= ASCII_UPPERCASE_Z_CODE_UNIT
76
+ ? codeUnit + ASCII_LOWERCASE_OFFSET
77
+ : codeUnit;
78
+ }
79
+ /**
80
+ * Replace every non-overlapping occurrence of a trusted path in untrusted text.
81
+ *
82
+ * Comparison treats slash and backslash as equivalent. Windows drive and UNC
83
+ * paths additionally use ASCII-only case folding, matching Windows path
84
+ * identity without locale-sensitive conversion. The linear-time matcher keeps
85
+ * the path literal: regex syntax in either input cannot change what matches.
86
+ */
87
+ export function redactPathFromText(input, path, replacement) {
88
+ if (path.length === 0 || input.length < path.length)
89
+ return input;
90
+ const foldAsciiCase = isWindowsPath(path);
91
+ const patternLength = path.length;
92
+ const pattern = new NativeUint32Array(patternLength);
93
+ const prefixTable = new NativeUint32Array(patternLength);
94
+ for (let index = 0; index < patternLength; index++) {
95
+ pattern[index] = normalizePathCodeUnit(stringCodeUnitAt(path, index), foldAsciiCase);
96
+ }
97
+ prefixTable[0] = 0;
98
+ for (let index = 1, prefixLength = 0; index < patternLength;) {
99
+ if (pattern[index] === pattern[prefixLength]) {
100
+ prefixTable[index] = ++prefixLength;
101
+ index++;
102
+ }
103
+ else if (prefixLength > 0) {
104
+ prefixLength = prefixTable[prefixLength - 1];
105
+ }
106
+ else {
107
+ prefixTable[index] = 0;
108
+ index++;
109
+ }
110
+ }
111
+ let result = "";
112
+ let copyStart = 0;
113
+ let matchLength = 0;
114
+ for (let index = 0; index < input.length; index++) {
115
+ const codeUnit = normalizePathCodeUnit(stringCodeUnitAt(input, index), foldAsciiCase);
116
+ while (matchLength > 0 && codeUnit !== pattern[matchLength]) {
117
+ matchLength = prefixTable[matchLength - 1];
118
+ }
119
+ if (codeUnit === pattern[matchLength])
120
+ matchLength++;
121
+ if (matchLength !== patternLength)
122
+ continue;
123
+ const matchStart = index - patternLength + 1;
124
+ result += apply(stringSlice, input, [copyStart, matchStart]);
125
+ result += replacement;
126
+ copyStart = index + 1;
127
+ matchLength = 0;
128
+ }
129
+ return copyStart === 0 ? input : result + apply(stringSlice, input, [copyStart]);
25
130
  }
26
131
  /**
27
132
  * Normalized substrings that mark a key as sensitive. Matching is done against
@@ -61,9 +166,15 @@ const SENSITIVE_KEY_PATTERNS = [
61
166
  "csrf",
62
167
  ];
63
168
  const SENSITIVE_KEY_CACHE_MAX_SIZE = 512;
169
+ /** Avoid retaining attacker-controlled, oversized property names in the cache. */
170
+ const SENSITIVE_KEY_CACHE_MAX_KEY_LENGTH = 128;
64
171
  const sensitiveKeyCache = new Map();
65
172
  /** Stop traversing past this depth to keep the pass cheap and stack-safe. */
66
173
  const MAX_DEPTH = 16;
174
+ /** Bound every individual array/object before allocating a redacted copy. */
175
+ const MAX_CONTAINER_ENTRIES = 1_024;
176
+ /** Bound aggregate work across an entire redaction call, not per branch. */
177
+ const MAX_TRAVERSAL_NODES = 4_096;
67
178
  /**
68
179
  * Whether a context key names a credential and should have its value masked.
69
180
  *
@@ -73,39 +184,88 @@ const MAX_DEPTH = 16;
73
184
  * normalizes to `author`, which contains none of the patterns.
74
185
  */
75
186
  export function isSensitiveKey(key) {
76
- const cached = sensitiveKeyCache.get(key);
77
- if (cached !== undefined)
78
- return cached;
187
+ const cacheable = key.length <= SENSITIVE_KEY_CACHE_MAX_KEY_LENGTH;
188
+ if (cacheable) {
189
+ const cached = sensitiveKeyCache.get(key);
190
+ if (cached !== undefined)
191
+ return cached;
192
+ }
79
193
  const normalized = normalizeToAlphanumeric(key);
80
194
  const sensitive = SENSITIVE_KEY_PATTERNS.some((pattern) => normalized.includes(pattern));
81
- if (sensitiveKeyCache.size >= SENSITIVE_KEY_CACHE_MAX_SIZE) {
82
- const oldestKey = sensitiveKeyCache.keys().next().value;
83
- if (oldestKey !== undefined)
84
- sensitiveKeyCache.delete(oldestKey);
195
+ if (cacheable) {
196
+ if (sensitiveKeyCache.size >= SENSITIVE_KEY_CACHE_MAX_SIZE) {
197
+ const oldestKey = sensitiveKeyCache.keys().next().value;
198
+ if (oldestKey !== undefined)
199
+ sensitiveKeyCache.delete(oldestKey);
200
+ }
201
+ sensitiveKeyCache.set(key, sensitive);
85
202
  }
86
- sensitiveKeyCache.set(key, sensitive);
87
203
  return sensitive;
88
204
  }
89
205
  /**
90
- * A non-null, non-array object. {@link isRecord} covers class instances too,
91
- * whose enumerable fields `JSON.stringify` *would* serialize, so we must
92
- * traverse them to catch secrets.
206
+ * `Array.isArray` normally looks like a harmless classifier, but it throws for
207
+ * revoked proxies. Returning `null` lets every public redaction entry point
208
+ * fail closed without touching the unreadable value again.
93
209
  */
94
- function isTraversableRecord(value) {
95
- return isRecord(value);
96
- }
97
- function hasToJson(value) {
98
- return typeof value.toJSON === "function";
210
+ function classifyArray(value) {
211
+ try {
212
+ return Array.isArray(value);
213
+ }
214
+ catch {
215
+ return null;
216
+ }
99
217
  }
100
- function redactValue(value, depth, seen) {
218
+ function redactValue(value, depth, seen, mode, budget) {
219
+ if (budget.remainingNodes <= 0) {
220
+ budget.exhausted = true;
221
+ return REDACTED;
222
+ }
223
+ budget.remainingNodes--;
101
224
  if (typeof value === "string")
102
225
  return sanitizeUrlCredentials(value);
103
- if (Array.isArray(value)) {
226
+ if (typeof value === "bigint")
227
+ return mode === "serialization" ? value.toString() : value;
228
+ if (typeof value === "number") {
229
+ return mode === "serialization" && !Number.isFinite(value) ? null : value;
230
+ }
231
+ if (typeof value === "boolean" || value === null)
232
+ return value;
233
+ if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol") {
234
+ return mode === "serialization" ? REDACTED : value;
235
+ }
236
+ const arrayClassification = classifyArray(value);
237
+ if (arrayClassification === null)
238
+ return REDACTED;
239
+ if (arrayClassification) {
104
240
  if (depth >= MAX_DEPTH || seen.has(value))
105
241
  return REDACTED;
106
242
  seen.add(value);
107
243
  try {
108
- return value.map((item) => redactValue(item, depth + 1, seen));
244
+ const arrayValue = value;
245
+ const length = arrayValue.length;
246
+ if (!Number.isInteger(length) || length < 0 || length > MAX_CONTAINER_ENTRIES) {
247
+ return REDACTED;
248
+ }
249
+ const redacted = mode === "compatible" ? new Array(length) : [];
250
+ for (let index = 0; index < length; index++) {
251
+ if (mode === "compatible" && !(index in arrayValue))
252
+ continue;
253
+ const item = redactValue(arrayValue[index], depth + 1, seen, mode, budget);
254
+ if (budget.exhausted)
255
+ return REDACTED;
256
+ if (mode === "compatible") {
257
+ redacted[index] = item;
258
+ }
259
+ else {
260
+ redacted.push(item);
261
+ }
262
+ }
263
+ return redacted;
264
+ }
265
+ catch {
266
+ // Array indices can be accessors or proxy traps. A failed read makes the
267
+ // serialized contents unknowable, so the complete array fails closed.
268
+ return REDACTED;
109
269
  }
110
270
  finally {
111
271
  seen.delete(value);
@@ -116,24 +276,37 @@ function redactValue(value, depth, seen) {
116
276
  // enumerable keys. A key-based pass over the object's own properties would
117
277
  // therefore miss credentials smuggled through `toJSON`, e.g.
118
278
  // `{ toJSON: () => ({ apiKey: "sk-..." }) }` (CODEX P2). When `toJSON`
119
- // returns a non-scalar (an object/array that could carry credential keys),
120
- // redact *that* the thing actually emitted. When it returns a scalar
121
- // (Date/URL ISO string), the original object is left intact, preserving
122
- // prior behavior and identity.
123
- if (isRecord(value) && hasToJson(value)) {
124
- if (depth >= MAX_DEPTH || seen.has(value))
125
- return REDACTED;
279
+ // returns an object, array, or scalar, the serialization API redacts *that*
280
+ // snapshot. The compatibility API keeps scalar serializers such as Date and
281
+ // URL intact, preserving the established generic return contract.
282
+ if (depth >= MAX_DEPTH || seen.has(value))
283
+ return REDACTED;
284
+ let toJSON;
285
+ try {
286
+ toJSON = value.toJSON;
287
+ }
288
+ catch {
289
+ // Accessors can throw before a serializer is callable. Never inspect the
290
+ // raw object after that because its eventual serialization is unknown.
291
+ return REDACTED;
292
+ }
293
+ if (typeof toJSON === "function") {
126
294
  seen.add(value);
127
295
  try {
128
- const serialized = value.toJSON();
129
- if (isRecord(serialized) || Array.isArray(serialized)) {
130
- return redactValue(serialized, depth + 1, seen);
296
+ const serialized = apply(toJSON, value, []);
297
+ if (mode === "serialization") {
298
+ return redactValue(serialized, depth + 1, seen, mode, budget);
131
299
  }
132
300
  if (typeof serialized === "string") {
133
301
  const sanitized = sanitizeUrlCredentials(serialized);
134
302
  return sanitized === serialized ? value : sanitized;
135
303
  }
136
- // Scalar result (string/number/…): the object serializes safely as-is.
304
+ if (serialized !== null && typeof serialized === "object") {
305
+ if (classifyArray(serialized) === null)
306
+ return REDACTED;
307
+ return redactValue(serialized, depth + 1, seen, mode, budget);
308
+ }
309
+ // Other scalar results serialize safely as-is in compatibility mode.
137
310
  return value;
138
311
  }
139
312
  catch {
@@ -145,40 +318,79 @@ function redactValue(value, depth, seen) {
145
318
  seen.delete(value);
146
319
  }
147
320
  }
148
- if (isTraversableRecord(value)) {
149
- if (depth >= MAX_DEPTH || seen.has(value))
150
- return REDACTED;
151
- seen.add(value);
152
- try {
153
- const out = {};
154
- for (const [key, child] of Object.entries(value)) {
155
- out[key] = isSensitiveKey(key) ? REDACTED : redactValue(child, depth + 1, seen);
321
+ seen.add(value);
322
+ try {
323
+ const out = {};
324
+ const record = value;
325
+ let propertyCount = 0;
326
+ for (const key in record) {
327
+ if (!Object.hasOwn(record, key))
328
+ continue;
329
+ propertyCount++;
330
+ if (propertyCount > MAX_CONTAINER_ENTRIES)
331
+ return REDACTED;
332
+ if (isSensitiveKey(key)) {
333
+ Object.defineProperty(out, key, {
334
+ configurable: true,
335
+ enumerable: true,
336
+ value: REDACTED,
337
+ writable: true,
338
+ });
339
+ continue;
156
340
  }
157
- return out;
158
- }
159
- catch {
160
- // A throwing getter (or other access error) must never let an
161
- // unredacted object through: fail closed.
162
- return REDACTED;
163
- }
164
- finally {
165
- seen.delete(value);
341
+ const child = record[key];
342
+ // Match JSON object semantics only for the explicit serialization API.
343
+ // The generic compatibility API retains undefined own properties.
344
+ if (mode === "serialization" && child === undefined)
345
+ continue;
346
+ const redactedChild = redactValue(child, depth + 1, seen, mode, budget);
347
+ if (budget.exhausted)
348
+ return REDACTED;
349
+ Object.defineProperty(out, key, {
350
+ configurable: true,
351
+ enumerable: true,
352
+ value: redactedChild,
353
+ writable: true,
354
+ });
166
355
  }
356
+ return out;
357
+ }
358
+ catch {
359
+ // A throwing getter (or other access error) must never let an
360
+ // unredacted object through: fail closed.
361
+ return REDACTED;
362
+ }
363
+ finally {
364
+ seen.delete(value);
167
365
  }
168
- // Primitives and scalar-serializing objects (Date, URL, …) are returned
169
- // untouched: except for string values above, they are not key/value bags we
170
- // can safely rewrite.
171
- return value;
172
366
  }
173
367
  /**
174
- * Returns a redacted deep copy of `context`. Any property whose key is
175
- * {@link isSensitiveKey} has its value replaced with {@link REDACTED}; nested
176
- * plain objects, class instances, and arrays are traversed. The input is never
177
- * mutated, and the pass fails closed (returns {@link REDACTED}) on cycles,
178
- * depth overflow, or a throwing getter.
368
+ * Returns a redacted copy of `context` while preserving the established source
369
+ * and runtime value shapes. Any property whose key is {@link isSensitiveKey}
370
+ * is replaced with {@link REDACTED}; nested records and arrays are traversed,
371
+ * while primitives and scalar-serializing objects retain their original types.
372
+ * The input is never mutated.
373
+ *
374
+ * Use {@link redactForSerialization} at JSON/logging boundaries where BigInt,
375
+ * functions, symbols, and custom `toJSON` implementations must be normalized.
179
376
  */
180
377
  export function redactSensitive(context) {
181
- return redactValue(context, 0, new Set());
378
+ return redactValue(context, 0, new Set(), "compatible", {
379
+ remainingNodes: MAX_TRAVERSAL_NODES,
380
+ exhausted: false,
381
+ });
382
+ }
383
+ /**
384
+ * Returns a JSON-safe redacted snapshot of `context`. Sensitive keys are
385
+ * masked, nested values are traversed, BigInts become decimal strings,
386
+ * non-finite numbers become `null`, and unsupported or unreadable values fail
387
+ * closed. Objects with `toJSON` are snapshotted exactly once before redaction.
388
+ */
389
+ export function redactForSerialization(context) {
390
+ return redactValue(context, 0, new Set(), "serialization", {
391
+ remainingNodes: MAX_TRAVERSAL_NODES,
392
+ exhausted: false,
393
+ });
182
394
  }
183
395
  /**
184
396
  * Query-string parameter names that commonly carry credentials in URLs.
@@ -201,9 +413,221 @@ const SENSITIVE_URL_PARAMS = [
201
413
  "sig",
202
414
  "signature",
203
415
  "auth",
416
+ "x-amz-credential",
417
+ "x-amz-signature",
418
+ "x-amz-security-token",
419
+ "x-goog-credential",
420
+ "x-goog-signature",
204
421
  ];
205
422
  const NORMALIZED_SENSITIVE_URL_PARAMS = new Set(SENSITIVE_URL_PARAMS.map(normalizeToAlphanumeric));
206
- const URL_USERINFO_RE = /(\b[a-z][a-z0-9+.-]*:\/\/)([^/?#@\s]+)@/gi;
423
+ const URL_USERINFO_RE = /(\b[a-z][a-z0-9+.-]*:\/\/|\/\/)([^/?#\s]+)@/gi;
424
+ const HORIZONTAL_WHITESPACE_URL_USERINFO_RE = /(\b[a-z][a-z0-9+.-]*:\/\/|\/\/)([a-z0-9._~!$&'()*+,;=%-]+):([^/?#@\r\n \t]+[ \t][^/?#@\r\n]*)@/gi;
425
+ const MAX_URL_PARAMETER_DECODE_PASSES = 3;
426
+ function isHorizontalAssignmentBoundary(character) {
427
+ return (character === " " ||
428
+ character === "\t" ||
429
+ character === "," ||
430
+ character === ";" ||
431
+ character === "&" ||
432
+ character === "?" ||
433
+ character === "#");
434
+ }
435
+ function isAsciiLetter(character) {
436
+ if (!character)
437
+ return false;
438
+ const code = character.charCodeAt(0);
439
+ return (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
440
+ }
441
+ function isAssignmentKeyStartCharacter(character) {
442
+ return isAsciiLetter(character) || character === "_" || character === "$";
443
+ }
444
+ function isAssignmentKeyCharacter(character) {
445
+ if (!character)
446
+ return false;
447
+ const code = character.charCodeAt(0);
448
+ return (isAssignmentKeyStartCharacter(character) ||
449
+ (code >= 48 && code <= 57) ||
450
+ character === "." ||
451
+ character === "-");
452
+ }
453
+ /**
454
+ * Check for a syntactically recognizable next `key=value` / `"key": value`
455
+ * field without allocating substrings. Assignment values may themselves
456
+ * contain whitespace and punctuation, so only a complete next-field prefix is
457
+ * trusted as a boundary.
458
+ */
459
+ function assignmentStartsAt(input, start) {
460
+ let index = start;
461
+ const keyQuote = input[index] === `"` || input[index] === "'" ? input[index++] : "";
462
+ if (!isAssignmentKeyStartCharacter(input[index]))
463
+ return false;
464
+ index++;
465
+ while (isAssignmentKeyCharacter(input[index]))
466
+ index++;
467
+ if (keyQuote) {
468
+ if (input[index] !== keyQuote)
469
+ return false;
470
+ index++;
471
+ }
472
+ while (input[index] === " " || input[index] === "\t")
473
+ index++;
474
+ return input[index] === ":" || input[index] === "=";
475
+ }
476
+ function isAssignmentBoundaryCharacter(character) {
477
+ return (character === "\r" ||
478
+ character === "\n" ||
479
+ character === "}" ||
480
+ character === "]" ||
481
+ isHorizontalAssignmentBoundary(character));
482
+ }
483
+ function skipAssignmentBoundaryCharacters(input, start) {
484
+ let index = start;
485
+ while (index < input.length &&
486
+ isAssignmentBoundaryCharacter(input[index])) {
487
+ index++;
488
+ }
489
+ return index;
490
+ }
491
+ function assignmentValueEndsAt(input, start) {
492
+ const boundaryEnd = skipAssignmentBoundaryCharacters(input, start);
493
+ return boundaryEnd >= input.length || assignmentStartsAt(input, boundaryEnd);
494
+ }
495
+ function redactAssignmentValue(input, start) {
496
+ let scanStart = start;
497
+ let preserveValueQuote = true;
498
+ if (input.startsWith(REDACTED, start)) {
499
+ const markerEnd = start + REDACTED.length;
500
+ if (assignmentValueEndsAt(input, markerEnd)) {
501
+ return {
502
+ end: markerEnd,
503
+ replacement: REDACTED,
504
+ };
505
+ }
506
+ scanStart = markerEnd;
507
+ preserveValueQuote = false;
508
+ }
509
+ const wrapperQuote = preserveValueQuote &&
510
+ (input[scanStart] === `"` || input[scanStart] === "'" || input[scanStart] === "`")
511
+ ? input[scanStart]
512
+ : "";
513
+ let wrapperQuoteClosed = false;
514
+ const replacement = () => wrapperQuote ? `${wrapperQuote}${REDACTED}${wrapperQuoteClosed ? wrapperQuote : ""}` : REDACTED;
515
+ const expectedClosings = [];
516
+ let quote = "";
517
+ let quoteStart = -1;
518
+ for (let index = scanStart; index < input.length;) {
519
+ const character = input[index];
520
+ if (quote) {
521
+ if (character === "\\") {
522
+ index += 2;
523
+ continue;
524
+ }
525
+ if (character === quote) {
526
+ if (quoteStart === scanStart && expectedClosings.length === 0) {
527
+ wrapperQuoteClosed = true;
528
+ }
529
+ quote = "";
530
+ quoteStart = -1;
531
+ }
532
+ index++;
533
+ continue;
534
+ }
535
+ if (character === `"` || character === "'" || character === "`") {
536
+ quote = character;
537
+ quoteStart = index;
538
+ index++;
539
+ continue;
540
+ }
541
+ if (character === "{" || character === "[") {
542
+ expectedClosings.push(character === "{" ? "}" : "]");
543
+ index++;
544
+ continue;
545
+ }
546
+ if (expectedClosings.length > 0 &&
547
+ (character === "}" || character === "]")) {
548
+ if (expectedClosings.at(-1) !== character) {
549
+ return { end: input.length, replacement: replacement() };
550
+ }
551
+ expectedClosings.pop();
552
+ index++;
553
+ if (expectedClosings.length === 0 &&
554
+ assignmentValueEndsAt(input, index)) {
555
+ return { end: index, replacement: replacement() };
556
+ }
557
+ continue;
558
+ }
559
+ if (expectedClosings.length > 0 ||
560
+ !isAssignmentBoundaryCharacter(character)) {
561
+ index++;
562
+ continue;
563
+ }
564
+ const boundaryStart = index;
565
+ index = skipAssignmentBoundaryCharacters(input, index);
566
+ if (index >= input.length || assignmentStartsAt(input, index)) {
567
+ return { end: boundaryStart, replacement: replacement() };
568
+ }
569
+ }
570
+ return { end: input.length, replacement: replacement() };
571
+ }
572
+ function redactCredentialAssignments(input, prefixPattern, keyGroup, urlParameterBoundaryGroup) {
573
+ let cursor = 0;
574
+ let result = "";
575
+ for (let match = apply(regExpExec, prefixPattern, [input]); match; match = apply(regExpExec, prefixPattern, [input])) {
576
+ const key = match[keyGroup];
577
+ if (!isSensitiveKey(key))
578
+ continue;
579
+ const valueStart = prefixPattern.lastIndex;
580
+ const boundary = urlParameterBoundaryGroup === undefined
581
+ ? undefined
582
+ : match[urlParameterBoundaryGroup];
583
+ const markerEnd = valueStart + REDACTED.length;
584
+ if ((boundary === "?" || boundary === "&" || boundary === ";") &&
585
+ input.startsWith(REDACTED, valueStart) &&
586
+ input[markerEnd] === "#") {
587
+ // The URL-parameter pass already bounded this credential at the URI
588
+ // fragment delimiter. Preserve that delimiter without treating an
589
+ // arbitrary `[REDACTED]suffix` assignment as trustworthy.
590
+ continue;
591
+ }
592
+ const redactedValue = redactAssignmentValue(input, valueStart);
593
+ result += sliceString(input, cursor, match.index);
594
+ result += match[0];
595
+ result += redactedValue.replacement;
596
+ cursor = redactedValue.end;
597
+ prefixPattern.lastIndex = redactedValue.end;
598
+ }
599
+ return cursor === 0 ? input : result + sliceString(input, cursor);
600
+ }
601
+ function isStandaloneUrlAuthorityBeforeWhitespace(scheme, user, password) {
602
+ const whitespaceIndex = password.search(/[ \t]/);
603
+ if (whitespaceIndex < 0)
604
+ return false;
605
+ const authority = `${user}:${sliceString(password, 0, whitespaceIndex)}`;
606
+ const candidate = scheme === "//" ? `https://${authority}` : `${scheme}${authority}`;
607
+ try {
608
+ const url = new URL(candidate);
609
+ return url.username.length === 0 && url.password.length === 0;
610
+ }
611
+ catch {
612
+ return false;
613
+ }
614
+ }
615
+ function decodeUrlParameterName(value) {
616
+ let decoded = value;
617
+ for (let pass = 0; pass < MAX_URL_PARAMETER_DECODE_PASSES; pass++) {
618
+ let next;
619
+ try {
620
+ next = decodeURIComponent(decoded);
621
+ }
622
+ catch {
623
+ break;
624
+ }
625
+ if (next === decoded)
626
+ break;
627
+ decoded = next;
628
+ }
629
+ return decoded;
630
+ }
207
631
  /**
208
632
  * Strip credentials from URL-shaped strings so they can be safely emitted in
209
633
  * free-form text (error messages, stacks, lifted `request_url` fields). Unlike
@@ -216,27 +640,78 @@ const URL_USERINFO_RE = /(\b[a-z][a-z0-9+.-]*:\/\/)([^/?#@\s]+)@/gi;
216
640
  * It is intentionally tolerant: it operates on any string (a DSN, a Mongo URI,
217
641
  * an axios error message containing a URL) via regex rather than requiring a
218
642
  * parseable URL, so malformed or partial URLs in error text are still scrubbed.
219
- * Non-URL strings pass through unchanged.
643
+ * Strings without credential-shaped content pass through unchanged.
220
644
  */
221
645
  export function sanitizeUrlCredentials(input) {
222
646
  if (typeof input !== "string" || input.length === 0)
223
647
  return input;
224
648
  // 1) userinfo: scheme://user:pass@ → mask the password (and any bare creds).
225
- let out = input.replace(URL_USERINFO_RE, (_match, scheme, userinfo) => {
226
- const colon = userinfo.indexOf(":");
227
- if (colon === -1) {
228
- // `scheme://token@host` the whole userinfo is credential-like.
229
- return `${scheme}${REDACTED}@`;
230
- }
231
- const user = userinfo.slice(0, colon);
232
- return `${scheme}${user}:${REDACTED}@`;
233
- });
649
+ let out = apply(regExpReplace, URL_USERINFO_RE, [
650
+ input,
651
+ (_match, scheme, userinfo) => {
652
+ const colon = userinfo.indexOf(":");
653
+ if (colon === -1) {
654
+ // `scheme://token@host` — the whole userinfo is credential-like.
655
+ return `${scheme}${REDACTED}@`;
656
+ }
657
+ const user = sliceString(userinfo, 0, colon);
658
+ return `${scheme}${user}:${REDACTED}@`;
659
+ },
660
+ ]);
661
+ out = apply(regExpReplace, HORIZONTAL_WHITESPACE_URL_USERINFO_RE, [
662
+ out,
663
+ (match, scheme, user, password) => {
664
+ // Do not reinterpret a complete URL followed later by an email address
665
+ // on the same line as malformed userinfo. Raw-horizontal-whitespace
666
+ // recovery is limited to explicit `user:password` shapes whose prefix
667
+ // cannot already be parsed as a standalone authority.
668
+ if (isStandaloneUrlAuthorityBeforeWhitespace(scheme, user, password)) {
669
+ return match;
670
+ }
671
+ return `${scheme}${user}:${REDACTED}@`;
672
+ },
673
+ ]);
234
674
  // 2) sensitive query/fragment params: `key=value` → `key=[REDACTED]`.
235
- // Match `?key=`, `&key=`, `;key=` separators and stop at the next delimiter.
236
- out = out.replace(/([?&;])([a-z0-9_.\-]+)=([^&#;\s]*)/gi, (match, sep, key, _val) => {
237
- const sensitive = NORMALIZED_SENSITIVE_URL_PARAMS.has(normalizeToAlphanumeric(key));
238
- return sensitive ? `${sep}${key}=${REDACTED}` : match;
239
- });
675
+ // Match `?key=`, `#key=`, `&key=`, and `;key=` separators and stop at the
676
+ // next delimiter. OAuth implicit-flow tokens commonly appear after `#`.
677
+ out = apply(regExpReplace, /([?#&;])([-a-z0-9_.%\[\]]+)=([^&#;\s]*)/gi, [
678
+ out,
679
+ (match, sep, key, _val) => {
680
+ const decodedKey = decodeUrlParameterName(key);
681
+ const sensitive = NORMALIZED_SENSITIVE_URL_PARAMS.has(normalizeToAlphanumeric(decodedKey)) ||
682
+ isSensitiveKey(decodedKey);
683
+ return sensitive ? `${sep}${key}=${REDACTED}` : match;
684
+ },
685
+ ]);
686
+ // 3) Cookie header values.
687
+ // Cookie headers can carry multiple independent credentials separated by
688
+ // semicolons (and Set-Cookie attributes can contain commas). Mask the entire
689
+ // header line before the generic assignment scanner can stop at the first
690
+ // delimiter and expose later values.
691
+ out = apply(regExpReplace, /(^|[^a-z0-9_-])((?:set-cookie|cookie)\s*:\s*)[^\r\n]*/gi, [
692
+ out,
693
+ (_match, boundary, prefix) => `${boundary}${prefix}${REDACTED}`,
694
+ ]);
695
+ // 4) Header-shaped authorization values and standalone auth schemes.
696
+ // Authorization schemes are extensible (AWS SigV4, Digest, custom proxy
697
+ // schemes, and others), so mask the complete line instead of trying to
698
+ // enumerate schemes or parse their credential-bearing parameters.
699
+ out = apply(regExpReplace, /\b(authorization\s*[:=]\s*)[^\r\n]*/gi, [
700
+ out,
701
+ (_match, prefix) => `${prefix}${REDACTED}`,
702
+ ]);
703
+ out = apply(regExpReplace, /\b(bearer|basic)(\s+)(?:"[^"\r\n]*"|'[^'\r\n]*'|[a-z0-9._~+/=-]+)/gi, [
704
+ out,
705
+ (_match, scheme, whitespace) => `${scheme}${whitespace}${REDACTED}`,
706
+ ]);
707
+ // 5) Credential assignments embedded in free-form messages/errors. Match
708
+ // generic identifier-shaped keys and delegate classification to the same
709
+ // deny-list used for structured context. This keeps JSON snippets, header
710
+ // dumps, and ordinary `key=value` text from drifting to a weaker policy.
711
+ // Handle quoted JSON/object keys first and preserve their quoting so the
712
+ // sanitized text remains intelligible and structurally valid.
713
+ out = redactCredentialAssignments(out, /(["'])([_$a-z][a-z0-9_.$-]*)\1(\s*[:=]\s*)/gi, 2);
714
+ out = redactCredentialAssignments(out, /(^|[^a-z0-9_.$-])([_$a-z][a-z0-9_.$-]*)(\s*[:=]\s*)/gi, 2, 1);
240
715
  return out;
241
716
  }
242
717
  function firstUrlDelimiterIndex(input) {
@@ -273,21 +748,34 @@ export function sanitizeUrlForSpan(input) {
273
748
  return input;
274
749
  try {
275
750
  const url = new URL(input);
751
+ if (url.protocol === "blob:") {
752
+ try {
753
+ const embeddedUrl = new URL(url.pathname);
754
+ return embeddedUrl.origin === "null" ? "blob:" : `blob:${embeddedUrl.origin}`;
755
+ }
756
+ catch (_) {
757
+ return "blob:";
758
+ }
759
+ }
276
760
  if (url.origin !== "null")
277
761
  return `${url.origin}${url.pathname}`;
762
+ if (/^[a-z][a-z0-9+.-]*:/i.test(input))
763
+ return url.protocol;
278
764
  }
279
765
  catch (_) {
280
766
  // Relative or malformed URL-shaped strings are handled by the fallback.
281
767
  }
282
768
  const delimiterIndex = firstUrlDelimiterIndex(input);
283
- const withoutQueryOrFragment = delimiterIndex === -1 ? input : input.slice(0, delimiterIndex);
769
+ const withoutQueryOrFragment = delimiterIndex === -1
770
+ ? input
771
+ : sliceString(input, 0, delimiterIndex);
284
772
  const protocolRelativeUrl = sanitizeProtocolRelativeUrlForSpan(withoutQueryOrFragment);
285
773
  if (protocolRelativeUrl)
286
774
  return protocolRelativeUrl;
287
775
  return sanitizeUrlCredentials(withoutQueryOrFragment);
288
776
  }
289
777
  /**
290
- * Apply {@link sanitizeUrlCredentials} to the `message` and `stack` of a
778
+ * Apply {@link sanitizeUrlCredentials} to the `name`, `message`, and `stack` of a
291
779
  * serialized-error-shaped object, returning a new object. Used by the logger's
292
780
  * JSON and text paths so errors carrying DSNs, Mongo URIs, or
293
781
  * `?access_token=`-bearing URLs do not leak credentials (the serialized error
@@ -297,6 +785,8 @@ export function sanitizeSerializedError(error) {
297
785
  if (!error)
298
786
  return error;
299
787
  const out = { ...error };
788
+ if (typeof out.name === "string")
789
+ out.name = sanitizeUrlCredentials(out.name);
300
790
  if (typeof out.message === "string")
301
791
  out.message = sanitizeUrlCredentials(out.message);
302
792
  if (typeof out.stack === "string")