@sayknow-cli/utils 0.7.10 → 0.7.12
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/dist/types/broken-pipe.d.ts +4 -3
- package/dist/types/canonical-log-dir.d.ts +20 -0
- package/dist/types/cli-metadata.d.ts +33 -0
- package/dist/types/cli.d.ts +12 -0
- package/dist/types/crash-fingerprint.d.ts +82 -0
- package/dist/types/crash-journal.d.ts +84 -0
- package/dist/types/crash-redaction.d.ts +20 -0
- package/dist/types/dirs.d.ts +127 -36
- package/dist/types/env-file.d.ts +59 -0
- package/dist/types/env.d.ts +19 -1
- package/dist/types/error-classification.d.ts +12 -0
- package/dist/types/fetch-retry.d.ts +6 -3
- package/dist/types/header-value.d.ts +10 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/logger.d.ts +22 -8
- package/dist/types/native-process.d.ts +12 -0
- package/dist/types/procmgr.d.ts +1 -35
- package/dist/types/safe-error.d.ts +20 -0
- package/dist/types/sanitize-text.d.ts +10 -0
- package/dist/types/shell-config.d.ts +58 -0
- package/dist/types/stream.d.ts +2 -1
- package/dist/types/which.d.ts +1 -1
- package/package.json +2 -2
- package/src/broken-pipe.ts +16 -4
- package/src/canonical-log-dir.ts +59 -0
- package/src/cli-metadata.ts +74 -0
- package/src/cli.ts +30 -2
- package/src/crash-fingerprint.ts +294 -0
- package/src/crash-journal.ts +292 -0
- package/src/crash-redaction.ts +558 -0
- package/src/dirs.ts +583 -165
- package/src/env-file.ts +326 -20
- package/src/env.ts +321 -35
- package/src/error-classification.ts +31 -0
- package/src/fetch-retry.ts +23 -10
- package/src/frontmatter.ts +28 -2
- package/src/glob.ts +13 -10
- package/src/header-value.ts +15 -0
- package/src/index.ts +6 -0
- package/src/logger.ts +252 -97
- package/src/native-process.ts +19 -0
- package/src/postmortem.ts +1 -55
- package/src/procmgr.ts +16 -212
- package/src/ptree.ts +3 -2
- package/src/safe-error.ts +40 -0
- package/src/sanitize-text.ts +14 -0
- package/src/shell-config.ts +329 -0
- package/src/stream.ts +32 -1
- package/src/which.ts +6 -5
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort credential scrubbing for durable crash records.
|
|
3
|
+
*
|
|
4
|
+
* This is a *persistence-time* scrub: it keeps obvious credential shapes out of
|
|
5
|
+
* a file SKC keeps indefinitely. It is explicitly NOT a privacy guarantee and
|
|
6
|
+
* must never be treated as one for data that leaves the machine — outbound
|
|
7
|
+
* text goes through `sanitizeExternalCrashV1` instead.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const CREDENTIAL_KEY_SEPARATOR = "[._\\t -]*";
|
|
11
|
+
const CREDENTIAL_KEY_LABELS = `api${CREDENTIAL_KEY_SEPARATOR}key|apikey|account${CREDENTIAL_KEY_SEPARATOR}key|shared${CREDENTIAL_KEY_SEPARATOR}access${CREDENTIAL_KEY_SEPARATOR}key|shared${CREDENTIAL_KEY_SEPARATOR}access${CREDENTIAL_KEY_SEPARATOR}signature|access${CREDENTIAL_KEY_SEPARATOR}token|refresh${CREDENTIAL_KEY_SEPARATOR}token|id${CREDENTIAL_KEY_SEPARATOR}token|session${CREDENTIAL_KEY_SEPARATOR}key|session${CREDENTIAL_KEY_SEPARATOR}token|client${CREDENTIAL_KEY_SEPARATOR}secret|aws${CREDENTIAL_KEY_SEPARATOR}secret${CREDENTIAL_KEY_SEPARATOR}access${CREDENTIAL_KEY_SEPARATOR}key|secret${CREDENTIAL_KEY_SEPARATOR}key${CREDENTIAL_KEY_SEPARATOR}base|secret${CREDENTIAL_KEY_SEPARATOR}key|secret${CREDENTIAL_KEY_SEPARATOR}access${CREDENTIAL_KEY_SEPARATOR}key|encryption${CREDENTIAL_KEY_SEPARATOR}key|signing${CREDENTIAL_KEY_SEPARATOR}key|password${CREDENTIAL_KEY_SEPARATOR}(?:confirmation|confirm)|password|passwd|pwd|passphrase|auth|authorization|bearer|private${CREDENTIAL_KEY_SEPARATOR}token|token|secrets?|credentials?|private${CREDENTIAL_KEY_SEPARATOR}key|cookies?`;
|
|
12
|
+
const CREDENTIAL_KEY_PREFIX = `(?:[A-Za-z0-9][A-Za-z0-9_.-]*[_.-])?`;
|
|
13
|
+
const CREDENTIAL_KEY_PATTERN = `${CREDENTIAL_KEY_PREFIX}(?:${CREDENTIAL_KEY_LABELS})`;
|
|
14
|
+
const CREDENTIAL_KEY_SUFFIX = new RegExp(`^${CREDENTIAL_KEY_PATTERN}$`, "i");
|
|
15
|
+
/**
|
|
16
|
+
* A complete value that an earlier redaction pass (this module or a caller's) already
|
|
17
|
+
* replaced. The closing `]` is optional because unquoted values stop before `]`; the
|
|
18
|
+
* container rule only leaves a bracketed placeholder in place when a field boundary
|
|
19
|
+
* follows it, so the unbracketed form never hides a trailing secret.
|
|
20
|
+
*/
|
|
21
|
+
const REDACTION_PLACEHOLDER_PATTERN = /^(?:«redacted(?:-[a-z]+(?:-[a-z]+)*)?»|\[REDACTED_[A-Z]+(?:_[A-Z]+)*\]?)$/;
|
|
22
|
+
const REDACTION_PLACEHOLDER_BOUNDARY_PATTERN = /^(?:$|[\s,;})"'`\\])/;
|
|
23
|
+
const CREDENTIAL_CAMEL_COMPOUND_SUFFIXES =
|
|
24
|
+
"AccessToken|RefreshToken|IdToken|SessionToken|ClientSecret|EncryptionKey|SigningKey";
|
|
25
|
+
const CREDENTIAL_CAMEL_SIMPLE_SUFFIXES =
|
|
26
|
+
"SecretAccessKey|Authorization|Credentials|Credential|AccountKey|ApiKey|SecretKey|PrivateKey|SessionKey|Passphrase|Password|Passwd|Pwd|Secrets|Secret|Bearer|Cookies|Cookie|Token|Auth";
|
|
27
|
+
const CREDENTIAL_CAMEL_PREFIX = "(?:[a-z0-9]+(?:[A-Z]{2,})?|[A-Z][a-z0-9]+(?:[A-Z]{2,})?|[A-Z]{2,})";
|
|
28
|
+
const CREDENTIAL_CAMEL_COMPOUND_PREFIX = "(?:[a-z0-9]{2,}(?:[A-Z]{2,})?|[A-Z][a-z0-9]+(?:[A-Z]{2,})?|[A-Z]{2,})";
|
|
29
|
+
const CREDENTIAL_CAMEL_MULTI_PREFIX_SEGMENT = "(?:[A-Z][a-z0-9]+|[A-Z]{2,}(?=[A-Z][a-z0-9]))";
|
|
30
|
+
const CREDENTIAL_CAMEL_MULTI_PREFIX = `(?:[a-z0-9]{2,}|[A-Z][a-z0-9]+|[A-Z]{2,})(?:${CREDENTIAL_CAMEL_MULTI_PREFIX_SEGMENT})+(?:[A-Z]{2,})?`;
|
|
31
|
+
const CREDENTIAL_CAMEL_KEY_PATTERN = `(?:(?:${CREDENTIAL_CAMEL_COMPOUND_PREFIX}|${CREDENTIAL_CAMEL_MULTI_PREFIX})(?:${CREDENTIAL_CAMEL_COMPOUND_SUFFIXES})|(?:${CREDENTIAL_CAMEL_PREFIX}|${CREDENTIAL_CAMEL_MULTI_PREFIX})(?:${CREDENTIAL_CAMEL_SIMPLE_SUFFIXES}))`;
|
|
32
|
+
const CREDENTIAL_CAMEL_KEY_SUFFIX = new RegExp(`^${CREDENTIAL_CAMEL_KEY_PATTERN}$`);
|
|
33
|
+
const CREDENTIAL_CAMEL_ASSIGNMENT_CANDIDATE_PATTERN = new RegExp(
|
|
34
|
+
String.raw`(?:${CREDENTIAL_CAMEL_COMPOUND_SUFFIXES}|${CREDENTIAL_CAMEL_SIMPLE_SUFFIXES})(?:\\*["'\x60]?\s*[=:]\s*)`,
|
|
35
|
+
);
|
|
36
|
+
const CREDENTIAL_ASSIGNMENT_PREFIX = String.raw`(?<![A-Za-z0-9_.-])((?:\\*["'\x60]?-*)(?:${CREDENTIAL_KEY_PATTERN})(?:\\*["'\x60]?\s*[=:]\s*))`;
|
|
37
|
+
const CREDENTIAL_ASSIGNMENT_CANDIDATE_PATTERN = new RegExp(
|
|
38
|
+
String.raw`(?:${CREDENTIAL_KEY_LABELS})(?:\\*["'\x60]?\s*[=:]\s*)`,
|
|
39
|
+
"i",
|
|
40
|
+
);
|
|
41
|
+
const CREDENTIAL_CONTAINER_CANDIDATE_PATTERN = new RegExp(
|
|
42
|
+
String.raw`(?:${CREDENTIAL_KEY_LABELS})(?:\\*["'\x60]?\s*[=:]\s*[\[{])`,
|
|
43
|
+
"i",
|
|
44
|
+
);
|
|
45
|
+
const CREDENTIAL_CAMEL_ASSIGNMENT_PREFIX = String.raw`(?<![A-Za-z0-9_-])((?:\\*["'\x60]?-*)(?:${CREDENTIAL_CAMEL_KEY_PATTERN})(?:\\*["'\x60]?\s*[=:]\s*))`;
|
|
46
|
+
const AUTHORIZATION_HEADER_PREFIX = String.raw`(?<![A-Za-z0-9_-])((?:\\*["'\x60]?-*)(?:[A-Za-z0-9][A-Za-z0-9_-]*[_-])?(?:auth|authorization)(?:\\*["'\x60]?\s*:\s*))`;
|
|
47
|
+
// Avoid splitting recognized multiword labels at their final component (e.g. `API Key`).
|
|
48
|
+
const CREDENTIAL_COMPOUND_FIELD_PREFIX =
|
|
49
|
+
"(?:api|account|access|refresh|id|session|client|aws|secret|private|encryption|signing|key)";
|
|
50
|
+
const CREDENTIAL_WHITESPACE_FOLLOWING_FIELD_PATTERN = String.raw`[ \t]+(?:${CREDENTIAL_KEY_LABELS}|(?<!(?:^|[^A-Za-z0-9_.=:-])${CREDENTIAL_COMPOUND_FIELD_PREFIX}[ \t])[A-Za-z][A-Za-z0-9_.-]{0,127})[ \t]*[=:]`;
|
|
51
|
+
const CREDENTIAL_FOLLOWING_FIELD_PATTERN = String.raw`(?:[,;][ \t]*[A-Za-z][A-Za-z0-9_. \t-]{0,127}[ \t]*[=:]|${CREDENTIAL_WHITESPACE_FOLLOWING_FIELD_PATTERN})`;
|
|
52
|
+
const CREDENTIAL_UNQUOTED_VALUE_PATTERN_BODY = String.raw`(?:(?!\\+n|\\+r\\+n|${CREDENTIAL_FOLLOWING_FIELD_PATTERN})(?:\\[^\r\n]|[^\s"'\x60}\]]))(?:(?!\\+n|\\+r\\+n|${CREDENTIAL_FOLLOWING_FIELD_PATTERN})(?:\\[^\r\n]|[^\r\n"'}\]]))*`;
|
|
53
|
+
const CREDENTIAL_VALUE_QUOTED_BOUNDARY = String.raw`(?=$|[\s,;}):.\]]|/?>)`;
|
|
54
|
+
const CREDENTIAL_QUOTED_ADJACENT_SUFFIX_PATTERN = String.raw`[^\s,;}\]]*`;
|
|
55
|
+
const CREDENTIAL_AUTH_VALUE_PATTERN = new RegExp(
|
|
56
|
+
String.raw`${CREDENTIAL_ASSIGNMENT_PREFIX}(?:Bearer|Basic|Token)\s+${CREDENTIAL_UNQUOTED_VALUE_PATTERN_BODY}`,
|
|
57
|
+
"gi",
|
|
58
|
+
);
|
|
59
|
+
const CREDENTIAL_AUTHORIZATION_VALUE_PATTERN = new RegExp(
|
|
60
|
+
String.raw`${AUTHORIZATION_HEADER_PREFIX}(?!\s*(?:["'«\x60]|\\+["'\x60]))(?:(?!\\+n|\\+r\\+n)[^\r\n])+`,
|
|
61
|
+
"gi",
|
|
62
|
+
);
|
|
63
|
+
const CREDENTIAL_VALUE_PATTERN_BODY = String.raw`(?:(\\+")((?:\\[^\r\n]|[^"\\\r\n])*?)(\\+")${CREDENTIAL_QUOTED_ADJACENT_SUFFIX_PATTERN}${CREDENTIAL_VALUE_QUOTED_BOUNDARY}|(")((?:\\[^\r\n]|[^"\\\r\n])*)(")${CREDENTIAL_QUOTED_ADJACENT_SUFFIX_PATTERN}${CREDENTIAL_VALUE_QUOTED_BOUNDARY}|(\\+')((?:\\[^\r\n]|[^'\\\r\n])*?)(\\+')${CREDENTIAL_QUOTED_ADJACENT_SUFFIX_PATTERN}${CREDENTIAL_VALUE_QUOTED_BOUNDARY}|(')((?:\\[^\r\n]|[^'\\\r\n])*)(')${CREDENTIAL_QUOTED_ADJACENT_SUFFIX_PATTERN}${CREDENTIAL_VALUE_QUOTED_BOUNDARY}|(\x60)((?:\\[^\r\n]|[^\x60\\\r\n])*)(\x60)${CREDENTIAL_QUOTED_ADJACENT_SUFFIX_PATTERN}${CREDENTIAL_VALUE_QUOTED_BOUNDARY}|(\\+")([^\r\n]*)(?=\r?\n|$)|(")([^\r\n]*)(?=\r?\n|$)|(\\+')([^\r\n]*)(?=\r?\n|$)|(')([^\r\n]*)(?=\r?\n|$)|(\x60)([^\r\n]*)(?=\r?\n|$)|(${CREDENTIAL_UNQUOTED_VALUE_PATTERN_BODY}))`;
|
|
64
|
+
const CREDENTIAL_VALUE_PATTERN = new RegExp(
|
|
65
|
+
String.raw`${CREDENTIAL_ASSIGNMENT_PREFIX}(?!\\+\x60)${CREDENTIAL_VALUE_PATTERN_BODY}`,
|
|
66
|
+
"gi",
|
|
67
|
+
);
|
|
68
|
+
const CREDENTIAL_CAMEL_VALUE_PATTERN = new RegExp(
|
|
69
|
+
String.raw`${CREDENTIAL_CAMEL_ASSIGNMENT_PREFIX}(?!\\+\x60)${CREDENTIAL_VALUE_PATTERN_BODY}`,
|
|
70
|
+
"g",
|
|
71
|
+
);
|
|
72
|
+
const CREDENTIAL_ESCAPED_BACKTICK_VALUE_PATTERN = new RegExp(
|
|
73
|
+
String.raw`${CREDENTIAL_ASSIGNMENT_PREFIX}(\\+\x60)((?:\\[^\r\n]|[^\x60\\\r\n])*?)(\\+\x60)${CREDENTIAL_QUOTED_ADJACENT_SUFFIX_PATTERN}${CREDENTIAL_VALUE_QUOTED_BOUNDARY}`,
|
|
74
|
+
"gi",
|
|
75
|
+
);
|
|
76
|
+
const CREDENTIAL_CAMEL_ESCAPED_BACKTICK_VALUE_PATTERN = new RegExp(
|
|
77
|
+
String.raw`${CREDENTIAL_CAMEL_ASSIGNMENT_PREFIX}(\\+\x60)((?:\\[^\r\n]|[^\x60\\\r\n])*?)(\\+\x60)${CREDENTIAL_QUOTED_ADJACENT_SUFFIX_PATTERN}${CREDENTIAL_VALUE_QUOTED_BOUNDARY}`,
|
|
78
|
+
"g",
|
|
79
|
+
);
|
|
80
|
+
const CREDENTIAL_ESCAPED_BACKTICK_UNCLOSED_PATTERN = new RegExp(
|
|
81
|
+
String.raw`${CREDENTIAL_ASSIGNMENT_PREFIX}(\\+\x60)((?:(?!\\+\x60)[^\r\n])*)(?=\r?\n|$)`,
|
|
82
|
+
"gi",
|
|
83
|
+
);
|
|
84
|
+
const CREDENTIAL_CAMEL_ESCAPED_BACKTICK_UNCLOSED_PATTERN = new RegExp(
|
|
85
|
+
String.raw`${CREDENTIAL_CAMEL_ASSIGNMENT_PREFIX}(\\+\x60)((?:(?!\\+\x60)[^\r\n])*)(?=\r?\n|$)`,
|
|
86
|
+
"g",
|
|
87
|
+
);
|
|
88
|
+
const COOKIE_VALUE_PATTERN =
|
|
89
|
+
/(?:(?<![A-Za-z0-9_-])|(?<=\\n))((?:[A-Za-z0-9][A-Za-z0-9_-]*[_-])?cookies?\s*[:=]\s*)((?:(?!\\+n|\\+r\\+n)[^\r\n])*)/gi;
|
|
90
|
+
// Cap key inspection; YAML permits the chomping and indentation indicators in either order.
|
|
91
|
+
const YAML_BLOCK_SCALAR_HEADER_PATTERN =
|
|
92
|
+
/^([ \t]*(?:-[ \t]+)?)(?:"([^"\r\n]{1,128})"|'([^'\r\n]{1,128})'|([A-Za-z0-9][A-Za-z0-9_. \t-]{0,127}?))[ \t]*:[ \t]*(?:[ \t]+(?:&[^ \t\r\n]{1,128}|![^ \t\r\n]{1,128}))*[ \t]*([|>](?:[1-9][+-]?|[+-][1-9]?)?)(?:[ \t]+#.*|[ \t]*)$/;
|
|
93
|
+
|
|
94
|
+
type YamlBlockScalarHeader = { indentation: number };
|
|
95
|
+
|
|
96
|
+
function parseCredentialYamlBlockScalarHeader(line: string): YamlBlockScalarHeader | undefined {
|
|
97
|
+
const match = YAML_BLOCK_SCALAR_HEADER_PATTERN.exec(line);
|
|
98
|
+
const key = (match?.[2] ?? match?.[3] ?? match?.[4] ?? "").trim();
|
|
99
|
+
if (match === null || !isCredentialKey(key)) return undefined;
|
|
100
|
+
return { indentation: (match[1] ?? "").length };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function lineBounds(text: string, start: number): { contentEnd: number; nextLineStart: number } {
|
|
104
|
+
const newline = text.indexOf("\n", start);
|
|
105
|
+
const nextLineStart = newline < 0 ? text.length : newline + 1;
|
|
106
|
+
let contentEnd = newline < 0 ? text.length : newline;
|
|
107
|
+
if (newline > start && text[newline - 1] === "\r") contentEnd--;
|
|
108
|
+
return { contentEnd, nextLineStart };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function redactYamlBlockScalars(text: string): string {
|
|
112
|
+
const output: string[] = [];
|
|
113
|
+
let copiedUntil = 0;
|
|
114
|
+
let lineStart = 0;
|
|
115
|
+
|
|
116
|
+
while (lineStart < text.length) {
|
|
117
|
+
const { contentEnd, nextLineStart } = lineBounds(text, lineStart);
|
|
118
|
+
const header = parseCredentialYamlBlockScalarHeader(text.slice(lineStart, contentEnd));
|
|
119
|
+
if (header === undefined) {
|
|
120
|
+
lineStart = nextLineStart;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
output.push(text.slice(copiedUntil, nextLineStart));
|
|
125
|
+
let bodyLineStart = nextLineStart;
|
|
126
|
+
let copiedBodyUntil = bodyLineStart;
|
|
127
|
+
while (bodyLineStart < text.length) {
|
|
128
|
+
const bodyLine = lineBounds(text, bodyLineStart);
|
|
129
|
+
let indentation = 0;
|
|
130
|
+
while (
|
|
131
|
+
bodyLineStart + indentation < bodyLine.contentEnd &&
|
|
132
|
+
(text[bodyLineStart + indentation] === " " || text[bodyLineStart + indentation] === "\t")
|
|
133
|
+
) {
|
|
134
|
+
indentation++;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (bodyLineStart + indentation === bodyLine.contentEnd) {
|
|
138
|
+
bodyLineStart = bodyLine.nextLineStart;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (indentation <= header.indentation) break;
|
|
142
|
+
|
|
143
|
+
output.push(text.slice(copiedBodyUntil, bodyLineStart));
|
|
144
|
+
let bodyTextEnd = bodyLine.contentEnd;
|
|
145
|
+
while (
|
|
146
|
+
bodyTextEnd > bodyLineStart + indentation &&
|
|
147
|
+
(text[bodyTextEnd - 1] === " " || text[bodyTextEnd - 1] === "\t")
|
|
148
|
+
) {
|
|
149
|
+
bodyTextEnd--;
|
|
150
|
+
}
|
|
151
|
+
output.push(
|
|
152
|
+
text.slice(bodyLineStart, bodyLineStart + indentation),
|
|
153
|
+
"«redacted»",
|
|
154
|
+
text.slice(bodyTextEnd, bodyLine.nextLineStart),
|
|
155
|
+
);
|
|
156
|
+
copiedBodyUntil = bodyLine.nextLineStart;
|
|
157
|
+
bodyLineStart = bodyLine.nextLineStart;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
output.push(text.slice(copiedBodyUntil, bodyLineStart));
|
|
161
|
+
copiedUntil = bodyLineStart;
|
|
162
|
+
lineStart = bodyLineStart;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return copiedUntil === 0 ? text : output.join("") + text.slice(copiedUntil);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isCookieFieldWhitespace(character: string | undefined): boolean {
|
|
169
|
+
return character === " " || character === "\t";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isCookieFieldNameStart(character: string | undefined): boolean {
|
|
173
|
+
if (character === undefined) return false;
|
|
174
|
+
const code = character.charCodeAt(0);
|
|
175
|
+
return (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isCookieFieldNameCharacter(character: string | undefined): boolean {
|
|
179
|
+
if (character === undefined) return false;
|
|
180
|
+
const code = character.charCodeAt(0);
|
|
181
|
+
return (
|
|
182
|
+
character === "_" ||
|
|
183
|
+
character === "-" ||
|
|
184
|
+
(code >= 48 && code <= 57) ||
|
|
185
|
+
(code >= 65 && code <= 90) ||
|
|
186
|
+
(code >= 97 && code <= 122)
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function cookieDiagnosticSuffix(value: string): string {
|
|
191
|
+
let quote: string | undefined;
|
|
192
|
+
for (let index = 0; index < value.length; index++) {
|
|
193
|
+
const character = value[index];
|
|
194
|
+
if (character === "\\") {
|
|
195
|
+
index++;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (quote !== undefined) {
|
|
199
|
+
if (character === quote) quote = undefined;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (character === "'" || character === '"' || character === "`") {
|
|
203
|
+
quote = character;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (!isCookieFieldWhitespace(character)) continue;
|
|
207
|
+
|
|
208
|
+
const boundary = index;
|
|
209
|
+
while (isCookieFieldWhitespace(value[index])) index++;
|
|
210
|
+
if (!isCookieFieldNameStart(value[index])) continue;
|
|
211
|
+
while (isCookieFieldNameCharacter(value[index])) index++;
|
|
212
|
+
while (isCookieFieldWhitespace(value[index])) index++;
|
|
213
|
+
if (value[index] !== "=" && value[index] !== ":") continue;
|
|
214
|
+
|
|
215
|
+
let previous = boundary - 1;
|
|
216
|
+
while (previous >= 0 && isCookieFieldWhitespace(value[previous])) previous--;
|
|
217
|
+
if (value[previous] !== ";" && value[previous] !== ",") return value.slice(boundary);
|
|
218
|
+
}
|
|
219
|
+
return "";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Whether a field name ends in a credential label recognized by the redactor. */
|
|
223
|
+
export function isCredentialKey(key: string): boolean {
|
|
224
|
+
return CREDENTIAL_KEY_SUFFIX.test(key) || CREDENTIAL_CAMEL_KEY_SUFFIX.test(key);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Scrub credential material from crash text before it is persisted.
|
|
229
|
+
* Covers bearer/basic-style headers, key=value or JSON key forms of common
|
|
230
|
+
* credential names, and well-known vendor token shapes. Normal messages and
|
|
231
|
+
* stack frames are untouched; matches are replaced in place so surrounding
|
|
232
|
+
* diagnostic context survives.
|
|
233
|
+
*/
|
|
234
|
+
export function redactCrashSecrets(text: string): string {
|
|
235
|
+
let redacted = redactCredentialContainers(text);
|
|
236
|
+
// Block scalar bodies span lines and must be masked before line-bounded rules handle the header.
|
|
237
|
+
redacted = redactYamlBlockScalars(redacted);
|
|
238
|
+
redacted = redacted.replace(
|
|
239
|
+
/(?<![A-Za-z0-9_-])(?:Bearer|Basic|Token)\s+[A-Za-z0-9._~+/=-]{8,}/gi,
|
|
240
|
+
"«redacted-auth»",
|
|
241
|
+
);
|
|
242
|
+
redacted = redacted.replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, "«redacted-jwt»");
|
|
243
|
+
redacted = redacted.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "«redacted-api-key»");
|
|
244
|
+
// `gh[opsur]_` covers the classic PAT/OAuth/server/user/refresh prefixes;
|
|
245
|
+
// fine-grained PATs use an entirely different `github_pat_` prefix and would
|
|
246
|
+
// otherwise survive into a log the module keeps indefinitely.
|
|
247
|
+
redacted = redacted.replace(/\bgh[opsur]_[A-Za-z0-9]{16,}\b/g, "«redacted-github-token»");
|
|
248
|
+
redacted = redacted.replace(/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, "«redacted-github-token»");
|
|
249
|
+
redacted = redacted.replace(/\bxox[baprs]-[A-Za-z0-9-]{8,}\b/g, "«redacted-slack-token»");
|
|
250
|
+
// The five shapes below are the ones `crash/upstream/envelope.ts` already
|
|
251
|
+
// classifies as credential-like and refuses to transmit. That refusal only
|
|
252
|
+
// guards the Sentry frame fields; the persisted crash log and the
|
|
253
|
+
// `skc crash report` body — which the user files as a public issue — reach
|
|
254
|
+
// egress through this function alone, so the same shapes have to be named here.
|
|
255
|
+
redacted = redacted.replace(/\bnpm_[A-Za-z0-9]{20,}\b/g, "«redacted-npm-token»");
|
|
256
|
+
redacted = redacted.replace(/\bglpat-[A-Za-z0-9_-]{20,}\b/g, "«redacted-gitlab-token»");
|
|
257
|
+
// Stripe separates with `_`, so the `sk-` rule above never matched one.
|
|
258
|
+
redacted = redacted.replace(/\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b/g, "«redacted-api-key»");
|
|
259
|
+
redacted = redacted.replace(/\bhf_[A-Za-z0-9]{20,}\b/g, "«redacted-api-key»");
|
|
260
|
+
// A PEM block carries the key material itself, so it is redacted whole rather
|
|
261
|
+
// than line by line. It runs before the narrower rules because they would
|
|
262
|
+
// otherwise chew on the base64 body and leave a truncated key behind.
|
|
263
|
+
redacted = redacted.replace(
|
|
264
|
+
/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g,
|
|
265
|
+
"«redacted-private-key»",
|
|
266
|
+
);
|
|
267
|
+
// Google API keys are a fixed 39-character shape that carries no label of its
|
|
268
|
+
// own, so the labeled-value rule below never sees one.
|
|
269
|
+
redacted = redacted.replace(
|
|
270
|
+
/(?<![A-Za-z0-9_-])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9_-])/g,
|
|
271
|
+
"«redacted-google-api-key»",
|
|
272
|
+
);
|
|
273
|
+
// Basic-auth credentials embedded in a URL. Scheme and host stay readable
|
|
274
|
+
// because they are the diagnostic value; only the userinfo is dropped. The
|
|
275
|
+
// boundary is consumed so the unbounded scheme scan starts only at the start
|
|
276
|
+
// of a scheme-character run, avoiding quadratic retries while accepting long
|
|
277
|
+
// standards-compliant schemes. The greedy authority scan consumes raw `@`
|
|
278
|
+
// characters in userinfo through its delimiter. Leading digits and scheme
|
|
279
|
+
// punctuation stay in the scheme capture so `9https://user:pass@` still redacts.
|
|
280
|
+
redacted = redacted.replace(
|
|
281
|
+
/(^|[^A-Za-z0-9+.-])([0-9+.-]*[a-z][a-z0-9+.-]*:\/\/)[^/\s?#]*@/gi,
|
|
282
|
+
"$1$2«redacted-url-credential»@",
|
|
283
|
+
);
|
|
284
|
+
// AKIA is the long-term access key id; ASIA is the temporary/STS one, which is
|
|
285
|
+
// the shape that actually shows up in a crashed request. ABIA (bearer) and
|
|
286
|
+
// ACCA (context) complete the set the session-import scrubber already lists.
|
|
287
|
+
// The id alone is not
|
|
288
|
+
// the credential: an STS payload carries `SecretAccessKey` and `SessionToken`
|
|
289
|
+
// alongside it, so the labeled-value rule below must name both. `secret_key`
|
|
290
|
+
// does not match `SecretAccessKey` (the canonical field has `Access` in the
|
|
291
|
+
// middle), and `access_token` does not match `SessionToken`.
|
|
292
|
+
redacted = redacted.replace(/\b(?:AKIA|ASIA|ABIA|ACCA)[0-9A-Z]{16}\b/g, "«redacted-aws-key»");
|
|
293
|
+
// Cookie values may contain semicolon- or comma-separated pairs. Preserve a later,
|
|
294
|
+
// whitespace-separated diagnostic assignment so the narrower rules can scrub
|
|
295
|
+
// it independently without losing useful context after the cookie.
|
|
296
|
+
redacted = redacted.replace(COOKIE_VALUE_PATTERN, (_match: string, prefix: string, value: string) =>
|
|
297
|
+
parseCredentialYamlBlockScalarHeader(_match) === undefined
|
|
298
|
+
? `${prefix}«redacted»${cookieDiagnosticSuffix(value)}`
|
|
299
|
+
: _match,
|
|
300
|
+
);
|
|
301
|
+
redacted = redacted.replace(CREDENTIAL_AUTH_VALUE_PATTERN, (_match, prefix: string) => `${prefix}«redacted»`);
|
|
302
|
+
redacted = redacted.replace(CREDENTIAL_AUTHORIZATION_VALUE_PATTERN, (_match: string, prefix: string) =>
|
|
303
|
+
parseCredentialYamlBlockScalarHeader(_match) === undefined ? `${prefix}«redacted»` : _match,
|
|
304
|
+
);
|
|
305
|
+
// Escaped delimiters support JSON nested in diagnostic strings. Quoted scalar
|
|
306
|
+
// values stay within one physical line; closed values also require a field
|
|
307
|
+
// boundary after the closing delimiter, while unterminated values redact
|
|
308
|
+
// through the end of their line. Capture each complete key once and inspect
|
|
309
|
+
// its suffix; repeated compound-prefix groups backtrack quadratically on long
|
|
310
|
+
// attacker-controlled identifiers.
|
|
311
|
+
if (CREDENTIAL_ASSIGNMENT_CANDIDATE_PATTERN.test(redacted)) {
|
|
312
|
+
redacted = redacted.replace(
|
|
313
|
+
CREDENTIAL_ESCAPED_BACKTICK_VALUE_PATTERN,
|
|
314
|
+
(_match: string, prefix: string, open: string, _value: string, close: string) =>
|
|
315
|
+
`${prefix}${open}«redacted»${close}`,
|
|
316
|
+
);
|
|
317
|
+
redacted = redacted.replace(
|
|
318
|
+
CREDENTIAL_ESCAPED_BACKTICK_UNCLOSED_PATTERN,
|
|
319
|
+
(_match: string, prefix: string, open: string) => `${prefix}${open}«redacted»`,
|
|
320
|
+
);
|
|
321
|
+
redacted = redacted.replace(CREDENTIAL_VALUE_PATTERN, redactCredentialValue);
|
|
322
|
+
}
|
|
323
|
+
if (CREDENTIAL_CAMEL_ASSIGNMENT_CANDIDATE_PATTERN.test(redacted)) {
|
|
324
|
+
redacted = redacted.replace(
|
|
325
|
+
CREDENTIAL_CAMEL_ESCAPED_BACKTICK_VALUE_PATTERN,
|
|
326
|
+
(_match: string, prefix: string, open: string, _value: string, close: string) =>
|
|
327
|
+
`${prefix}${open}«redacted»${close}`,
|
|
328
|
+
);
|
|
329
|
+
redacted = redacted.replace(
|
|
330
|
+
CREDENTIAL_CAMEL_ESCAPED_BACKTICK_UNCLOSED_PATTERN,
|
|
331
|
+
(_match: string, prefix: string, open: string) => `${prefix}${open}«redacted»`,
|
|
332
|
+
);
|
|
333
|
+
redacted = redacted.replace(CREDENTIAL_CAMEL_VALUE_PATTERN, redactCredentialValue);
|
|
334
|
+
}
|
|
335
|
+
// Keep dev's literal session-id cookie variants, which are not credential labels.
|
|
336
|
+
redacted = redacted.replace(
|
|
337
|
+
/(?<![A-Za-z0-9_])(["']?session(?:id|[_-]id)["']?\s*[=:]\s*["']?)[^\s"',;}\]]{8,}/gi,
|
|
338
|
+
"$1«redacted»",
|
|
339
|
+
);
|
|
340
|
+
redacted = redacted.replace(/(?<![A-Za-z0-9_%])(session(?:id|[_-]id)%3[Dd])[A-Za-z0-9._~%-]{8,}/gi, "$1«redacted»");
|
|
341
|
+
// Bare AWS secret access keys have no label; require `/` or `+` to avoid
|
|
342
|
+
// masking a 40-character alphanumeric diagnostic token.
|
|
343
|
+
redacted = redacted.replace(
|
|
344
|
+
/(?<![A-Za-z0-9/+])(?=[A-Za-z0-9/+]{0,39}[/+])[A-Za-z0-9/+]{40}(?![A-Za-z0-9/+])/g,
|
|
345
|
+
"«redacted-aws-key»",
|
|
346
|
+
);
|
|
347
|
+
return redacted;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function redactCredentialValue(
|
|
351
|
+
_match: string,
|
|
352
|
+
prefix: string,
|
|
353
|
+
escapedDoubleOpen: string | undefined,
|
|
354
|
+
escapedDoubleQuoted: string | undefined,
|
|
355
|
+
escapedDoubleClose: string | undefined,
|
|
356
|
+
doubleOpen: string | undefined,
|
|
357
|
+
doubleQuoted: string | undefined,
|
|
358
|
+
doubleClose: string | undefined,
|
|
359
|
+
escapedSingleOpen: string | undefined,
|
|
360
|
+
escapedSingleQuoted: string | undefined,
|
|
361
|
+
escapedSingleClose: string | undefined,
|
|
362
|
+
singleOpen: string | undefined,
|
|
363
|
+
singleQuoted: string | undefined,
|
|
364
|
+
singleClose: string | undefined,
|
|
365
|
+
backtickOpen: string | undefined,
|
|
366
|
+
backtickQuoted: string | undefined,
|
|
367
|
+
backtickClose: string | undefined,
|
|
368
|
+
unclosedEscapedDoubleOpen: string | undefined,
|
|
369
|
+
unclosedEscapedDoubleQuoted: string | undefined,
|
|
370
|
+
unclosedDoubleOpen: string | undefined,
|
|
371
|
+
unclosedDoubleQuoted: string | undefined,
|
|
372
|
+
unclosedEscapedSingleOpen: string | undefined,
|
|
373
|
+
unclosedEscapedSingleQuoted: string | undefined,
|
|
374
|
+
unclosedSingleOpen: string | undefined,
|
|
375
|
+
unclosedSingleQuoted: string | undefined,
|
|
376
|
+
unclosedBacktickOpen: string | undefined,
|
|
377
|
+
unclosedBacktickQuoted: string | undefined,
|
|
378
|
+
): string {
|
|
379
|
+
if (parseCredentialYamlBlockScalarHeader(_match) !== undefined) return _match;
|
|
380
|
+
// A value that is already a redaction placeholder carries no secret; re-masking it
|
|
381
|
+
// would only erase which credential shape an earlier pass identified.
|
|
382
|
+
const value =
|
|
383
|
+
escapedDoubleQuoted ??
|
|
384
|
+
doubleQuoted ??
|
|
385
|
+
escapedSingleQuoted ??
|
|
386
|
+
singleQuoted ??
|
|
387
|
+
backtickQuoted ??
|
|
388
|
+
unclosedEscapedDoubleQuoted ??
|
|
389
|
+
unclosedDoubleQuoted ??
|
|
390
|
+
unclosedEscapedSingleQuoted ??
|
|
391
|
+
unclosedSingleQuoted ??
|
|
392
|
+
unclosedBacktickQuoted ??
|
|
393
|
+
_match.slice(prefix.length);
|
|
394
|
+
if (REDACTION_PLACEHOLDER_PATTERN.test(value.trim())) return _match;
|
|
395
|
+
if (escapedDoubleQuoted !== undefined && escapedDoubleOpen !== undefined && escapedDoubleClose !== undefined) {
|
|
396
|
+
return `${prefix}${escapedDoubleOpen}«redacted»${escapedDoubleClose}`;
|
|
397
|
+
}
|
|
398
|
+
if (doubleQuoted !== undefined && doubleOpen !== undefined && doubleClose !== undefined) {
|
|
399
|
+
return `${prefix}${doubleOpen}«redacted»${doubleClose}`;
|
|
400
|
+
}
|
|
401
|
+
if (escapedSingleQuoted !== undefined && escapedSingleOpen !== undefined && escapedSingleClose !== undefined) {
|
|
402
|
+
return `${prefix}${escapedSingleOpen}«redacted»${escapedSingleClose}`;
|
|
403
|
+
}
|
|
404
|
+
if (singleQuoted !== undefined && singleOpen !== undefined && singleClose !== undefined) {
|
|
405
|
+
return `${prefix}${singleOpen}«redacted»${singleClose}`;
|
|
406
|
+
}
|
|
407
|
+
if (backtickQuoted !== undefined && backtickOpen !== undefined && backtickClose !== undefined) {
|
|
408
|
+
return `${prefix}${backtickOpen}«redacted»${backtickClose}`;
|
|
409
|
+
}
|
|
410
|
+
if (unclosedEscapedDoubleQuoted !== undefined && unclosedEscapedDoubleOpen !== undefined) {
|
|
411
|
+
return `${prefix}${unclosedEscapedDoubleOpen}«redacted»`;
|
|
412
|
+
}
|
|
413
|
+
if (unclosedDoubleQuoted !== undefined && unclosedDoubleOpen !== undefined) {
|
|
414
|
+
return `${prefix}${unclosedDoubleOpen}«redacted»`;
|
|
415
|
+
}
|
|
416
|
+
if (unclosedEscapedSingleQuoted !== undefined && unclosedEscapedSingleOpen !== undefined) {
|
|
417
|
+
return `${prefix}${unclosedEscapedSingleOpen}«redacted»`;
|
|
418
|
+
}
|
|
419
|
+
if (unclosedSingleQuoted !== undefined && unclosedSingleOpen !== undefined) {
|
|
420
|
+
return `${prefix}${unclosedSingleOpen}«redacted»`;
|
|
421
|
+
}
|
|
422
|
+
if (unclosedBacktickQuoted !== undefined && unclosedBacktickOpen !== undefined) {
|
|
423
|
+
return `${prefix}${unclosedBacktickOpen}«redacted»`;
|
|
424
|
+
}
|
|
425
|
+
return `${prefix}«redacted»`;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function redactCredentialContainers(text: string): string {
|
|
429
|
+
if (!CREDENTIAL_CONTAINER_CANDIDATE_PATTERN.test(text)) return text;
|
|
430
|
+
const assignment = new RegExp(String.raw`${CREDENTIAL_ASSIGNMENT_PREFIX}(?=[\[{])`, "gi");
|
|
431
|
+
const camelAssignment = new RegExp(String.raw`${CREDENTIAL_CAMEL_ASSIGNMENT_PREFIX}(?=[\[{])`, "g");
|
|
432
|
+
return redactCredentialContainersWithPattern(
|
|
433
|
+
redactCredentialContainersWithPattern(text, assignment),
|
|
434
|
+
camelAssignment,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function redactCredentialContainersWithPattern(text: string, assignment: RegExp): string {
|
|
439
|
+
let output = "";
|
|
440
|
+
let copiedUntil = 0;
|
|
441
|
+
for (let match = assignment.exec(text); match !== null; match = assignment.exec(text)) {
|
|
442
|
+
const containerStart = assignment.lastIndex;
|
|
443
|
+
if (containerStart < copiedUntil) continue;
|
|
444
|
+
const containerEnd = findCredentialContainerEnd(text, containerStart);
|
|
445
|
+
// `[REDACTED_TOKEN]` from an earlier pass is bracketed but is a placeholder, not a
|
|
446
|
+
// container — only when a field boundary follows, so nothing can trail it unscrubbed.
|
|
447
|
+
const container = text.slice(containerStart, containerEnd);
|
|
448
|
+
if (
|
|
449
|
+
container.endsWith("]") &&
|
|
450
|
+
REDACTION_PLACEHOLDER_PATTERN.test(container) &&
|
|
451
|
+
REDACTION_PLACEHOLDER_BOUNDARY_PATTERN.test(text.slice(containerEnd))
|
|
452
|
+
) {
|
|
453
|
+
assignment.lastIndex = containerEnd;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
output += `${text.slice(copiedUntil, containerStart)}«redacted»`;
|
|
457
|
+
copiedUntil = containerEnd;
|
|
458
|
+
assignment.lastIndex = containerEnd;
|
|
459
|
+
}
|
|
460
|
+
return copiedUntil === 0 ? text : output + text.slice(copiedUntil);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
type EscapedQuoteDelimiter = { delimiter: "'" | '"' | "`"; backslashCount: number };
|
|
464
|
+
|
|
465
|
+
function findCredentialContainerEnd(text: string, start: number): number {
|
|
466
|
+
const closing = [text[start] === "[" ? "]" : "}"];
|
|
467
|
+
const escapedQuoteDelimiter = findEscapedQuoteDelimiter(text, start);
|
|
468
|
+
const lineFeedIndex = text.indexOf("\n", start);
|
|
469
|
+
const carriageReturnIndex = text.indexOf("\r", start);
|
|
470
|
+
const unclosedEnd =
|
|
471
|
+
lineFeedIndex < 0
|
|
472
|
+
? carriageReturnIndex < 0
|
|
473
|
+
? text.length
|
|
474
|
+
: carriageReturnIndex
|
|
475
|
+
: carriageReturnIndex < 0
|
|
476
|
+
? lineFeedIndex
|
|
477
|
+
: Math.min(lineFeedIndex, carriageReturnIndex);
|
|
478
|
+
let quote: "'" | '"' | "`" | undefined;
|
|
479
|
+
for (let index = start + 1; index < text.length; index++) {
|
|
480
|
+
const character = text[index] ?? "";
|
|
481
|
+
if (character === "\\") {
|
|
482
|
+
let backslashEnd = index;
|
|
483
|
+
while (text[backslashEnd + 1] === "\\") backslashEnd++;
|
|
484
|
+
const backslashCount = backslashEnd - index + 1;
|
|
485
|
+
const nextCharacter = text[backslashEnd + 1];
|
|
486
|
+
// The opening escape run identifies this serialization layer; matching runs delimit strings.
|
|
487
|
+
if (
|
|
488
|
+
escapedQuoteDelimiter !== undefined &&
|
|
489
|
+
nextCharacter === escapedQuoteDelimiter.delimiter &&
|
|
490
|
+
backslashCount === escapedQuoteDelimiter.backslashCount
|
|
491
|
+
) {
|
|
492
|
+
if (quote === undefined) quote = escapedQuoteDelimiter.delimiter;
|
|
493
|
+
else if (quote === escapedQuoteDelimiter.delimiter) quote = undefined;
|
|
494
|
+
index = backslashEnd + 1;
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
index = nextCharacter !== undefined && backslashCount % 2 === 1 ? backslashEnd + 1 : backslashEnd;
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
if (quote) {
|
|
501
|
+
if (character === quote) quote = undefined;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (character === '"' || character === "'" || character === "`") {
|
|
505
|
+
quote = character;
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (character === "[") closing.push("]");
|
|
509
|
+
else if (character === "{") closing.push("}");
|
|
510
|
+
else if (character === closing[closing.length - 1]) {
|
|
511
|
+
closing.pop();
|
|
512
|
+
if (closing.length === 0) return index + 1;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return unclosedEnd;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function findEscapedQuoteDelimiter(text: string, start: number): EscapedQuoteDelimiter | undefined {
|
|
519
|
+
const closing = [text[start] === "[" ? "]" : "}"];
|
|
520
|
+
for (let index = start + 1; index < text.length; index++) {
|
|
521
|
+
const character = text[index] ?? "";
|
|
522
|
+
if (character === "\\") {
|
|
523
|
+
let backslashEnd = index;
|
|
524
|
+
while (text[backslashEnd + 1] === "\\") backslashEnd++;
|
|
525
|
+
const backslashCount = backslashEnd - index + 1;
|
|
526
|
+
const nextCharacter = text[backslashEnd + 1];
|
|
527
|
+
if ((nextCharacter === '"' || nextCharacter === "'" || nextCharacter === "`") && backslashCount % 2 === 1) {
|
|
528
|
+
return { delimiter: nextCharacter, backslashCount };
|
|
529
|
+
}
|
|
530
|
+
index = nextCharacter !== undefined && backslashCount % 2 === 1 ? backslashEnd + 1 : backslashEnd;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
if (character === '"' || character === "'" || character === "`") return undefined;
|
|
534
|
+
if (character === "[") closing.push("]");
|
|
535
|
+
else if (character === "{") closing.push("}");
|
|
536
|
+
else if (character === closing[closing.length - 1]) {
|
|
537
|
+
closing.pop();
|
|
538
|
+
if (closing.length === 0) return undefined;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** Every placeholder `redactCrashSecrets` can emit. Used by downstream normalizers. */
|
|
545
|
+
export const CRASH_REDACTION_MARKERS: readonly string[] = [
|
|
546
|
+
"«redacted-auth»",
|
|
547
|
+
"«redacted-jwt»",
|
|
548
|
+
"«redacted-api-key»",
|
|
549
|
+
"«redacted-github-token»",
|
|
550
|
+
"«redacted-slack-token»",
|
|
551
|
+
"«redacted-npm-token»",
|
|
552
|
+
"«redacted-gitlab-token»",
|
|
553
|
+
"«redacted-aws-key»",
|
|
554
|
+
"«redacted-private-key»",
|
|
555
|
+
"«redacted-google-api-key»",
|
|
556
|
+
"«redacted-url-credential»",
|
|
557
|
+
"«redacted»",
|
|
558
|
+
];
|