@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.
- package/esm/_dnt.polyfills.d.ts +6 -6
- package/esm/_dnt.polyfills.d.ts.map +1 -1
- package/esm/deno.d.ts +18 -0
- package/esm/deno.js +30 -9
- package/esm/src/config/defaults.d.ts +31 -24
- package/esm/src/config/defaults.d.ts.map +1 -1
- package/esm/src/config/defaults.js +25 -18
- package/esm/src/errors/diagnostic-policy.d.ts +35 -0
- package/esm/src/errors/diagnostic-policy.d.ts.map +1 -0
- package/esm/src/errors/diagnostic-policy.js +100 -0
- package/esm/src/errors/error-registry/boundary.d.ts +2 -0
- package/esm/src/errors/error-registry/boundary.d.ts.map +1 -1
- package/esm/src/errors/error-registry/boundary.js +8 -0
- package/esm/src/errors/error-registry/config.d.ts.map +1 -1
- package/esm/src/errors/error-registry/config.js +3 -3
- package/esm/src/errors/error-registry/deploy.js +1 -1
- package/esm/src/errors/error-registry/server.d.ts +2 -0
- package/esm/src/errors/error-registry/server.d.ts.map +1 -1
- package/esm/src/errors/error-registry/server.js +8 -0
- package/esm/src/errors/error-registry-helpers.d.ts +11 -2
- package/esm/src/errors/error-registry-helpers.d.ts.map +1 -1
- package/esm/src/errors/error-registry-helpers.js +70 -1
- package/esm/src/errors/error-registry.d.ts +85 -73
- package/esm/src/errors/error-registry.d.ts.map +1 -1
- package/esm/src/errors/error-registry.js +2 -14
- package/esm/src/errors/types.d.ts +44 -5
- package/esm/src/errors/types.d.ts.map +1 -1
- package/esm/src/errors/types.js +148 -24
- package/esm/src/errors/veryfront-error.d.ts +25 -2
- package/esm/src/errors/veryfront-error.d.ts.map +1 -1
- package/esm/src/errors/veryfront-error.js +638 -28
- package/esm/src/observability/limits.d.ts +20 -0
- package/esm/src/observability/limits.d.ts.map +1 -0
- package/esm/src/observability/limits.js +19 -0
- package/esm/src/observability/telemetry-error.d.ts +22 -0
- package/esm/src/observability/telemetry-error.d.ts.map +1 -0
- package/esm/src/observability/telemetry-error.js +377 -0
- package/esm/src/observability/tracing/api-shim.d.ts +39 -0
- package/esm/src/observability/tracing/api-shim.d.ts.map +1 -1
- package/esm/src/observability/tracing/api-shim.js +196 -69
- package/esm/src/observability/tracing/context-callback.d.ts +14 -0
- package/esm/src/observability/tracing/context-callback.d.ts.map +1 -0
- package/esm/src/observability/tracing/context-callback.js +111 -0
- package/esm/src/observability/tracing/otlp-setup.d.ts.map +1 -1
- package/esm/src/observability/tracing/otlp-setup.js +190 -46
- package/esm/src/platform/adapters/base.d.ts +51 -7
- package/esm/src/platform/adapters/base.d.ts.map +1 -1
- package/esm/src/platform/adapters/base.js +4 -2
- package/esm/src/platform/adapters/bounded-file-read.d.ts +9 -0
- package/esm/src/platform/adapters/bounded-file-read.d.ts.map +1 -1
- package/esm/src/platform/adapters/bounded-file-read.js +11 -5
- package/esm/src/platform/adapters/file-snapshot-error.d.ts +2 -0
- package/esm/src/platform/adapters/file-snapshot-error.d.ts.map +1 -1
- package/esm/src/platform/adapters/file-snapshot-error.js +2 -0
- package/esm/src/platform/adapters/file-system-capabilities.d.ts +74 -0
- package/esm/src/platform/adapters/file-system-capabilities.d.ts.map +1 -0
- package/esm/src/platform/adapters/file-system-capabilities.js +508 -0
- package/esm/src/platform/adapters/native-file-system-provenance.d.ts +6 -0
- package/esm/src/platform/adapters/native-file-system-provenance.d.ts.map +1 -0
- package/esm/src/platform/adapters/native-file-system-provenance.js +15 -0
- package/esm/src/platform/adapters/runtime/deno/filesystem-adapter.d.ts +45 -0
- package/esm/src/platform/adapters/runtime/deno/filesystem-adapter.d.ts.map +1 -0
- package/esm/src/platform/adapters/runtime/deno/filesystem-adapter.js +291 -0
- package/esm/src/platform/adapters/runtime/shared/node-filesystem-adapter.d.ts +71 -0
- package/esm/src/platform/adapters/runtime/shared/node-filesystem-adapter.d.ts.map +1 -0
- package/esm/src/platform/adapters/runtime/shared/node-filesystem-adapter.js +471 -0
- package/esm/src/platform/adapters/runtime/shared/shared-watcher.d.ts +16 -0
- package/esm/src/platform/adapters/runtime/shared/shared-watcher.d.ts.map +1 -0
- package/esm/src/platform/adapters/runtime/shared/shared-watcher.js +140 -0
- package/esm/src/platform/adapters/runtime/shared/temp-dir.d.ts +2 -0
- package/esm/src/platform/adapters/runtime/shared/temp-dir.d.ts.map +1 -0
- package/esm/src/platform/adapters/runtime/shared/temp-dir.js +10 -0
- package/esm/src/platform/adapters/runtime/shared/watcher-queue.d.ts +5 -0
- package/esm/src/platform/adapters/runtime/shared/watcher-queue.d.ts.map +1 -0
- package/esm/src/platform/adapters/runtime/shared/watcher-queue.js +75 -0
- package/esm/src/platform/compat/abort-signal.d.ts +20 -0
- package/esm/src/platform/compat/abort-signal.d.ts.map +1 -0
- package/esm/src/platform/compat/abort-signal.js +64 -0
- package/esm/src/platform/compat/constants.d.ts +2 -2
- package/esm/src/platform/compat/constants.d.ts.map +1 -1
- package/esm/src/platform/compat/constants.js +2 -2
- package/esm/src/platform/compat/error-introspection.d.ts +37 -0
- package/esm/src/platform/compat/error-introspection.d.ts.map +1 -0
- package/esm/src/platform/compat/error-introspection.js +258 -0
- package/esm/src/platform/compat/fs.d.ts +29 -8
- package/esm/src/platform/compat/fs.d.ts.map +1 -1
- package/esm/src/platform/compat/fs.js +123 -57
- package/esm/src/platform/compat/native-brand-checks.d.ts +14 -0
- package/esm/src/platform/compat/native-brand-checks.d.ts.map +1 -0
- package/esm/src/platform/compat/native-brand-checks.js +68 -0
- package/esm/src/platform/compat/not-found-error.d.ts +8 -3
- package/esm/src/platform/compat/not-found-error.d.ts.map +1 -1
- package/esm/src/platform/compat/not-found-error.js +164 -31
- package/esm/src/platform/compat/path/security.d.ts.map +1 -1
- package/esm/src/platform/compat/path/security.js +3 -5
- package/esm/src/platform/compat/process/command.d.ts +24 -1
- package/esm/src/platform/compat/process/command.d.ts.map +1 -1
- package/esm/src/platform/compat/process/command.js +393 -78
- package/esm/src/platform/compat/process/env.d.ts +11 -0
- package/esm/src/platform/compat/process/env.d.ts.map +1 -1
- package/esm/src/platform/compat/process/env.js +28 -38
- package/esm/src/platform/compat/process/global-error.d.ts +19 -0
- package/esm/src/platform/compat/process/global-error.d.ts.map +1 -0
- package/esm/src/platform/compat/process/global-error.js +57 -0
- package/esm/src/platform/compat/process/lifecycle.d.ts +10 -3
- package/esm/src/platform/compat/process/lifecycle.d.ts.map +1 -1
- package/esm/src/platform/compat/process/lifecycle.js +74 -23
- package/esm/src/platform/compat/process/project-env-contract.d.ts +19 -0
- package/esm/src/platform/compat/process/project-env-contract.d.ts.map +1 -0
- package/esm/src/platform/compat/process/project-env-contract.js +19 -0
- package/esm/src/platform/compat/runtime.d.ts +14 -5
- package/esm/src/platform/compat/runtime.d.ts.map +1 -1
- package/esm/src/platform/compat/runtime.js +34 -9
- package/esm/src/platform/compat/std/front-matter-yaml.d.ts +12 -0
- package/esm/src/platform/compat/std/front-matter-yaml.d.ts.map +1 -1
- package/esm/src/platform/compat/std/front-matter-yaml.js +35 -3
- package/esm/src/platform/compat/temp-directory-prefix.d.ts +8 -0
- package/esm/src/platform/compat/temp-directory-prefix.d.ts.map +1 -0
- package/esm/src/platform/compat/temp-directory-prefix.js +15 -0
- package/esm/src/utils/abort.d.ts +10 -0
- package/esm/src/utils/abort.d.ts.map +1 -0
- package/esm/src/utils/abort.js +35 -0
- package/esm/src/utils/constants/build.d.ts +3 -0
- package/esm/src/utils/constants/build.d.ts.map +1 -1
- package/esm/src/utils/constants/build.js +5 -1
- package/esm/src/utils/constants/cache.d.ts +21 -0
- package/esm/src/utils/constants/cache.d.ts.map +1 -1
- package/esm/src/utils/constants/cache.js +101 -39
- package/esm/src/utils/constants/css.d.ts +20 -0
- package/esm/src/utils/constants/css.d.ts.map +1 -0
- package/esm/src/utils/constants/css.js +19 -0
- package/esm/src/utils/constants/limits.d.ts +4 -0
- package/esm/src/utils/constants/limits.d.ts.map +1 -1
- package/esm/src/utils/constants/limits.js +4 -0
- package/esm/src/utils/constants/project-identity.d.ts +5 -0
- package/esm/src/utils/constants/project-identity.d.ts.map +1 -0
- package/esm/src/utils/constants/project-identity.js +4 -0
- package/esm/src/utils/constants/retry.d.ts +18 -0
- package/esm/src/utils/constants/retry.d.ts.map +1 -1
- package/esm/src/utils/constants/retry.js +18 -0
- package/esm/src/utils/constants/security.d.ts +4 -0
- package/esm/src/utils/constants/security.d.ts.map +1 -1
- package/esm/src/utils/constants/security.js +4 -0
- package/esm/src/utils/index.d.ts +3 -1
- package/esm/src/utils/index.d.ts.map +1 -1
- package/esm/src/utils/index.js +3 -1
- package/esm/src/utils/logger/core.d.ts +5 -0
- package/esm/src/utils/logger/core.d.ts.map +1 -1
- package/esm/src/utils/logger/core.js +67 -2
- package/esm/src/utils/logger/redact.d.ts +36 -13
- package/esm/src/utils/logger/redact.d.ts.map +1 -1
- package/esm/src/utils/logger/redact.js +573 -83
- package/esm/src/utils/parallel.d.ts.map +1 -1
- package/esm/src/utils/parallel.js +11 -1
- package/esm/src/utils/project-identity.d.ts +11 -0
- package/esm/src/utils/project-identity.d.ts.map +1 -0
- package/esm/src/utils/project-identity.js +69 -0
- package/esm/src/utils/version-constant.d.ts +1 -1
- package/esm/src/utils/version-constant.js +1 -1
- package/package.json +3 -3
- package/esm/src/platform/adapters/runtime/shared/native-file-capabilities.d.ts +0 -33
- package/esm/src/platform/adapters/runtime/shared/native-file-capabilities.d.ts.map +0 -1
- 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
|
-
*
|
|
13
|
-
* in
|
|
14
|
-
* over-redaction — masking a benign `tokenCount` is
|
|
15
|
-
* token is not. The traversal fails *closed*: on a
|
|
16
|
-
* throwing getter it returns {@link REDACTED}
|
|
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
|
-
|
|
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
|
|
77
|
-
if (
|
|
78
|
-
|
|
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 (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
129
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
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+.-]
|
|
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
|
-
*
|
|
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 =
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
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
|
|
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")
|