@evomap/evolver-webui 2.0.0-beta.19 → 2.0.0-beta.22
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/diagnosticSanitize.d.ts +1 -0
- package/dist/diagnosticSanitize.js +743 -4
- package/dist/eventSnapshot.d.ts +1 -0
- package/dist/eventSnapshot.js +10 -2
- package/dist/jsoncScannerShim.d.ts +7 -0
- package/dist/jsoncScannerShim.js +10 -0
- package/dist/logDiagnostics.js +27 -5
- package/dist/server.js +33 -0
- package/package.json +3 -2
|
@@ -1,28 +1,767 @@
|
|
|
1
|
+
import { hub } from '@evomap/evolver-core';
|
|
2
|
+
// The package main entry is a UMD wrapper whose shadowed `require` leaves
|
|
3
|
+
// './impl/format' unresolved inside bun standalone binaries. Import the scanner
|
|
4
|
+
// impl via the shim so bundlers inline it statically.
|
|
5
|
+
import { createScanner } from './jsoncScannerShim.js';
|
|
1
6
|
const MAX_TEXT = 500;
|
|
2
7
|
const MAX_ARRAY = 50;
|
|
3
8
|
const MAX_KEYS = 50;
|
|
4
9
|
const MAX_DEPTH = 5;
|
|
5
|
-
const
|
|
10
|
+
const MAX_SERIALIZED_JSON_DEPTH = 10;
|
|
11
|
+
const MAX_JSON_CONTAINER_DEPTH = 10_000;
|
|
12
|
+
const SECRET_KEY = /(?:authorization|proxy[_-]?authorization|cookie|set[_-]?cookie|password|passwd|secret|api[_-]?key|account[_-]?key|instrumentation[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|token|private[_-]?key)/i;
|
|
13
|
+
const SENSITIVE_HTTP_HEADER_NAME = /\b(proxy[-_]?authorization|authorization|set[-_]?cookie|cookie)\b/gi;
|
|
14
|
+
const SENSITIVE_HTTP_HEADER_CANDIDATE = /\b(?:proxy[-_]?authorization|authorization|set[-_]?cookie|cookie)\b/i;
|
|
15
|
+
const SENSITIVE_HTTP_HEADER_KEY = /^(?:(?:proxy[-_]?)?authorization|(?:set[-_]?)?cookie)$/i;
|
|
16
|
+
const REDACTED_HEADER_VALUE = '[redacted]';
|
|
17
|
+
const REDACTED_JSON_VALUE = JSON.stringify(REDACTED_HEADER_VALUE);
|
|
18
|
+
// jsonc-parser declares SyntaxKind as an ambient const enum, which cannot be imported with verbatimModuleSyntax.
|
|
19
|
+
const JSON_TOKEN = {
|
|
20
|
+
openBrace: 1,
|
|
21
|
+
closeBrace: 2,
|
|
22
|
+
openBracket: 3,
|
|
23
|
+
closeBracket: 4,
|
|
24
|
+
comma: 5,
|
|
25
|
+
colon: 6,
|
|
26
|
+
null: 7,
|
|
27
|
+
true: 8,
|
|
28
|
+
false: 9,
|
|
29
|
+
string: 10,
|
|
30
|
+
number: 11,
|
|
31
|
+
unknown: 16,
|
|
32
|
+
eof: 17,
|
|
33
|
+
};
|
|
34
|
+
function isHeaderFraming(char) {
|
|
35
|
+
return char === ' ' || char === '\t';
|
|
36
|
+
}
|
|
37
|
+
function physicalLineEnd(value, start) {
|
|
38
|
+
for (let index = start; index < value.length; index += 1) {
|
|
39
|
+
const code = value.charCodeAt(index);
|
|
40
|
+
if (code === 0x0d || code === 0x0a)
|
|
41
|
+
return index;
|
|
42
|
+
}
|
|
43
|
+
return value.length;
|
|
44
|
+
}
|
|
45
|
+
function isPhysicalHeaderNamePosition(value, lineStart, nameStart) {
|
|
46
|
+
for (let index = lineStart; index < nameStart; index += 1) {
|
|
47
|
+
if (value[index] !== ' ' && value[index] !== '\t')
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
function physicalHeaderEnd(value, start) {
|
|
53
|
+
return physicalHeaderEndFromLineEnd(value, physicalLineEnd(value, start));
|
|
54
|
+
}
|
|
55
|
+
function physicalHeaderEndFromLineEnd(value, firstLineEnd, physicalHeaderEndCache) {
|
|
56
|
+
const cached = physicalHeaderEndCache?.get(firstLineEnd);
|
|
57
|
+
if (cached !== undefined)
|
|
58
|
+
return cached;
|
|
59
|
+
const traversedLineEnds = [];
|
|
60
|
+
let end = firstLineEnd;
|
|
61
|
+
while (end < value.length) {
|
|
62
|
+
traversedLineEnds.push(end);
|
|
63
|
+
const nextLine = value[end] === '\r' && value[end + 1] === '\n' ? end + 2 : end + 1;
|
|
64
|
+
if (value[nextLine] !== ' ' && value[nextLine] !== '\t')
|
|
65
|
+
break;
|
|
66
|
+
end = physicalLineEnd(value, nextLine);
|
|
67
|
+
}
|
|
68
|
+
for (const lineEnd of traversedLineEnds)
|
|
69
|
+
physicalHeaderEndCache?.set(lineEnd, end);
|
|
70
|
+
return end;
|
|
71
|
+
}
|
|
72
|
+
function looksLikeSensitiveHttpHeaderValue(headerName, value, start, end) {
|
|
73
|
+
const rawCandidate = value.slice(start, Math.min(end, start + 512)).trim();
|
|
74
|
+
const candidate = rawCandidate[0] === '\x22' || rawCandidate[0] === '\x27'
|
|
75
|
+
? rawCandidate.slice(1)
|
|
76
|
+
: rawCandidate;
|
|
77
|
+
const normalizedName = headerName.toLowerCase().replace(/[-_]/g, '');
|
|
78
|
+
if (normalizedName.endsWith('authorization')) {
|
|
79
|
+
const auth = /^([!#$%&'*+.^_`|~0-9A-Za-z-]+)[ \t]+(.+)$/.exec(candidate);
|
|
80
|
+
if (!auth)
|
|
81
|
+
return false;
|
|
82
|
+
const credentials = (auth[2] ?? '').trim();
|
|
83
|
+
return /^[A-Za-z0-9._~+/-]+={0,}(?:[ \t]*(?:[,;)\]}>\x27\x22]|$))/.test(credentials)
|
|
84
|
+
|| /\b[!#$%&'*+.^_`|~0-9A-Za-z-]+\s*=/.test(credentials);
|
|
85
|
+
}
|
|
86
|
+
return /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\s*=\s*(?:\x22[^\x22]*\x22|[^\s;,\x22\x27]*)(?:[ \t]*(?:[,;)\]}>;\x27\x22]|$))/.test(candidate);
|
|
87
|
+
}
|
|
88
|
+
function needsFoldedHeaderInspection(headerName, value, start, lineEnd) {
|
|
89
|
+
const boundedEnd = Math.min(lineEnd, start + 512);
|
|
90
|
+
if (looksLikeSensitiveHttpHeaderValue(headerName, value, start, boundedEnd))
|
|
91
|
+
return true;
|
|
92
|
+
const rawCandidate = value.slice(start, boundedEnd).trim();
|
|
93
|
+
if (rawCandidate.length === 0)
|
|
94
|
+
return true;
|
|
95
|
+
const candidate = rawCandidate[0] === '\x22' || rawCandidate[0] === '\x27'
|
|
96
|
+
? rawCandidate.slice(1)
|
|
97
|
+
: rawCandidate;
|
|
98
|
+
return candidate.startsWith('[')
|
|
99
|
+
|| /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(candidate)
|
|
100
|
+
|| /[,;]$/.test(candidate)
|
|
101
|
+
|| rawCandidate[0] === '\x22'
|
|
102
|
+
|| rawCandidate[0] === '\x27';
|
|
103
|
+
}
|
|
104
|
+
function quotedValueEnd(value, start, lineEnd) {
|
|
105
|
+
const quote = value[start];
|
|
106
|
+
if (quote !== '\x22' && quote !== '\x27')
|
|
107
|
+
return undefined;
|
|
108
|
+
let ambiguous = false;
|
|
109
|
+
for (let index = start + 1; index < lineEnd; index += 1) {
|
|
110
|
+
if (value[index] === '\\') {
|
|
111
|
+
index += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (value[index] !== quote)
|
|
115
|
+
continue;
|
|
116
|
+
const next = value[index + 1];
|
|
117
|
+
if (next === undefined || isHeaderFraming(next) || /[,;)\]}>/]/.test(next)) {
|
|
118
|
+
return { end: index + 1, ambiguous };
|
|
119
|
+
}
|
|
120
|
+
ambiguous = true;
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
function token68ValueEnd(headerName, value, start, lineEnd) {
|
|
125
|
+
const normalizedName = headerName.toLowerCase().replace(/[-_]/g, '');
|
|
126
|
+
if (!normalizedName.endsWith('authorization'))
|
|
127
|
+
return undefined;
|
|
128
|
+
const match = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+[ \t]+[A-Za-z0-9._~+/-]+={0,}/.exec(value.slice(start, Math.min(lineEnd, start + 512)));
|
|
129
|
+
if (!match)
|
|
130
|
+
return undefined;
|
|
131
|
+
const end = start + match[0].length;
|
|
132
|
+
if (match[0].endsWith('=') && (value[end] === '\x22' || value[end] === '\x27'))
|
|
133
|
+
return undefined;
|
|
134
|
+
let boundary = end;
|
|
135
|
+
while (isHeaderFraming(value[boundary]))
|
|
136
|
+
boundary += 1;
|
|
137
|
+
return boundary >= lineEnd || /[,;)\]}>\x27\x22]/.test(value[boundary] ?? '') ? end : undefined;
|
|
138
|
+
}
|
|
139
|
+
function cookieValueEnd(headerName, value, start, lineEnd) {
|
|
140
|
+
const normalizedName = headerName.toLowerCase().replace(/[-_]/g, '');
|
|
141
|
+
if (normalizedName !== 'cookie')
|
|
142
|
+
return undefined;
|
|
143
|
+
let quoted;
|
|
144
|
+
for (let index = start; index < lineEnd; index += 1) {
|
|
145
|
+
const char = value[index];
|
|
146
|
+
if (char === '\\' && quoted) {
|
|
147
|
+
index += 1;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (char === '\x22' || char === '\x27') {
|
|
151
|
+
quoted = quoted === char ? undefined : (quoted ?? char);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!quoted && char === ',')
|
|
155
|
+
return index;
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
function inspectArrayValueEnd(headerName, value, start, end, budget) {
|
|
160
|
+
if (value[start] !== '[')
|
|
161
|
+
return undefined;
|
|
162
|
+
let depth = 0;
|
|
163
|
+
let quote;
|
|
164
|
+
let quoteStart = -1;
|
|
165
|
+
let credentialFound = false;
|
|
166
|
+
for (let index = start; index < end; index += 1) {
|
|
167
|
+
budget.remaining -= 1;
|
|
168
|
+
if (budget.remaining < 0)
|
|
169
|
+
return { end };
|
|
170
|
+
const char = value[index];
|
|
171
|
+
if (quote) {
|
|
172
|
+
if (char === '\\') {
|
|
173
|
+
index += 1;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (char !== quote)
|
|
177
|
+
continue;
|
|
178
|
+
if (depth === 1) {
|
|
179
|
+
let before = quoteStart - 1;
|
|
180
|
+
while (isHeaderFraming(value[before]))
|
|
181
|
+
before -= 1;
|
|
182
|
+
let after = index + 1;
|
|
183
|
+
while (isHeaderFraming(value[after]))
|
|
184
|
+
after += 1;
|
|
185
|
+
if ((value[before] === '[' || value[before] === ',')
|
|
186
|
+
&& (value[after] === ',' || value[after] === ']')) {
|
|
187
|
+
const decoded = value.slice(quoteStart + 1, index).replace(/\\([\\'"])/g, '$1');
|
|
188
|
+
if (looksLikeSensitiveHttpHeaderValue(headerName, decoded, 0, decoded.length)) {
|
|
189
|
+
credentialFound = true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
quote = undefined;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (char === '\x22' || char === '\x27') {
|
|
197
|
+
quote = char;
|
|
198
|
+
quoteStart = index;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (char === '[') {
|
|
202
|
+
depth += 1;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (char !== ']')
|
|
206
|
+
continue;
|
|
207
|
+
depth -= 1;
|
|
208
|
+
if (depth === 0)
|
|
209
|
+
return credentialFound ? { end: index + 1 } : undefined;
|
|
210
|
+
if (depth < 0)
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
return depth > 0 ? { end } : undefined;
|
|
214
|
+
}
|
|
215
|
+
function collectFoldedCandidate(value, start, end, budget) {
|
|
216
|
+
let cursor = start;
|
|
217
|
+
const spendFraming = () => {
|
|
218
|
+
budget.remaining -= 1;
|
|
219
|
+
return budget.remaining >= 0;
|
|
220
|
+
};
|
|
221
|
+
while (cursor < end && isHeaderFraming(value[cursor])) {
|
|
222
|
+
if (!spendFraming())
|
|
223
|
+
return { text: '', failClosed: true };
|
|
224
|
+
cursor += 1;
|
|
225
|
+
}
|
|
226
|
+
let text = '';
|
|
227
|
+
while (cursor < end && text.length < 512) {
|
|
228
|
+
if (value[cursor] !== '\r' && value[cursor] !== '\n') {
|
|
229
|
+
text += value[cursor];
|
|
230
|
+
cursor += 1;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (!spendFraming())
|
|
234
|
+
return { text, failClosed: true };
|
|
235
|
+
cursor += value[cursor] === '\r' && value[cursor + 1] === '\n' ? 2 : 1;
|
|
236
|
+
let skipped = false;
|
|
237
|
+
while (cursor < end && isHeaderFraming(value[cursor])) {
|
|
238
|
+
if (!spendFraming())
|
|
239
|
+
return { text, failClosed: true };
|
|
240
|
+
cursor += 1;
|
|
241
|
+
skipped = true;
|
|
242
|
+
}
|
|
243
|
+
if (!skipped)
|
|
244
|
+
break;
|
|
245
|
+
if (text.length > 0 && text[text.length - 1] !== ' ')
|
|
246
|
+
text += ' ';
|
|
247
|
+
}
|
|
248
|
+
return { text, failClosed: false };
|
|
249
|
+
}
|
|
250
|
+
function foldedValueStart(value, start, end, budget) {
|
|
251
|
+
let cursor = start;
|
|
252
|
+
while (cursor < end) {
|
|
253
|
+
if (isHeaderFraming(value[cursor])) {
|
|
254
|
+
budget.remaining -= 1;
|
|
255
|
+
if (budget.remaining < 0)
|
|
256
|
+
return undefined;
|
|
257
|
+
cursor += 1;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (value[cursor] !== '\r' && value[cursor] !== '\n')
|
|
261
|
+
break;
|
|
262
|
+
const width = value[cursor] === '\r' && value[cursor + 1] === '\n' ? 2 : 1;
|
|
263
|
+
budget.remaining -= width;
|
|
264
|
+
if (budget.remaining < 0)
|
|
265
|
+
return undefined;
|
|
266
|
+
cursor += width;
|
|
267
|
+
}
|
|
268
|
+
return cursor;
|
|
269
|
+
}
|
|
270
|
+
function rawHeaderSpan(value, headerName, lineStart, lineEnd, physicalEnd, nameStart, nameEnd, embedded) {
|
|
271
|
+
if (!isPhysicalHeaderNamePosition(value, lineStart, nameStart))
|
|
272
|
+
return undefined;
|
|
273
|
+
let cursor = nameEnd;
|
|
274
|
+
while (isHeaderFraming(value[cursor]))
|
|
275
|
+
cursor += 1;
|
|
276
|
+
if (value[cursor] !== ':')
|
|
277
|
+
return undefined;
|
|
278
|
+
let separatorEnd = cursor + 1;
|
|
279
|
+
while (value[separatorEnd] === ' ' || value[separatorEnd] === '\t')
|
|
280
|
+
separatorEnd += 1;
|
|
281
|
+
if (embedded
|
|
282
|
+
&& !looksLikeSensitiveHttpHeaderValue(headerName, value, separatorEnd, lineEnd))
|
|
283
|
+
return undefined;
|
|
284
|
+
return {
|
|
285
|
+
start: nameStart,
|
|
286
|
+
end: physicalEnd,
|
|
287
|
+
replacement: value.slice(nameStart, separatorEnd) + REDACTED_HEADER_VALUE,
|
|
288
|
+
resumeAt: physicalEnd,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function inlineHeaderSpan(value, headerName, lineEnd, physicalEnd, nameStart, nameEnd, budget) {
|
|
292
|
+
const labelQuote = value[nameStart - 1] === '\x22' || value[nameStart - 1] === '\x27'
|
|
293
|
+
? value[nameStart - 1]
|
|
294
|
+
: undefined;
|
|
295
|
+
let cursor = nameEnd;
|
|
296
|
+
if (labelQuote && value[cursor] === labelQuote)
|
|
297
|
+
cursor += 1;
|
|
298
|
+
while (value[cursor] === ' ' || value[cursor] === '\t')
|
|
299
|
+
cursor += 1;
|
|
300
|
+
if (labelQuote && value[cursor] === ']') {
|
|
301
|
+
cursor += 1;
|
|
302
|
+
while (value[cursor] === ' ' || value[cursor] === '\t')
|
|
303
|
+
cursor += 1;
|
|
304
|
+
}
|
|
305
|
+
let separatorEnd;
|
|
306
|
+
if (value[cursor] === ':' || value[cursor] === '=') {
|
|
307
|
+
separatorEnd = cursor + 1;
|
|
308
|
+
if (value[cursor] === '=' && value[separatorEnd] === '>')
|
|
309
|
+
separatorEnd += 1;
|
|
310
|
+
}
|
|
311
|
+
else if (labelQuote && value[cursor] === ',') {
|
|
312
|
+
separatorEnd = cursor + 1;
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
while (value[separatorEnd] === ' ' || value[separatorEnd] === '\t')
|
|
318
|
+
separatorEnd += 1;
|
|
319
|
+
let shapeStart = separatorEnd;
|
|
320
|
+
const folded = physicalEnd > lineEnd;
|
|
321
|
+
if (separatorEnd >= lineEnd && physicalEnd > lineEnd) {
|
|
322
|
+
shapeStart = value[lineEnd] === '\r' && value[lineEnd + 1] === '\n'
|
|
323
|
+
? lineEnd + 2
|
|
324
|
+
: lineEnd + 1;
|
|
325
|
+
}
|
|
326
|
+
if (folded
|
|
327
|
+
&& separatorEnd < lineEnd
|
|
328
|
+
&& !needsFoldedHeaderInspection(headerName, value, separatorEnd, lineEnd))
|
|
329
|
+
return undefined;
|
|
330
|
+
const advancedFoldedStart = folded
|
|
331
|
+
? foldedValueStart(value, shapeStart, physicalEnd, budget)
|
|
332
|
+
: shapeStart;
|
|
333
|
+
const foldedFramingExhausted = advancedFoldedStart === undefined;
|
|
334
|
+
if (advancedFoldedStart !== undefined)
|
|
335
|
+
shapeStart = advancedFoldedStart;
|
|
336
|
+
const inspectArray = foldedFramingExhausted
|
|
337
|
+
? undefined
|
|
338
|
+
: inspectArrayValueEnd(headerName, value, shapeStart, physicalEnd, budget);
|
|
339
|
+
const unfoldedCandidate = folded && !foldedFramingExhausted
|
|
340
|
+
? collectFoldedCandidate(value, shapeStart, physicalEnd, budget)
|
|
341
|
+
: (foldedFramingExhausted ? { text: '', failClosed: true } : undefined);
|
|
342
|
+
if (!inspectArray
|
|
343
|
+
&& !unfoldedCandidate?.failClosed
|
|
344
|
+
&& !(unfoldedCandidate
|
|
345
|
+
? looksLikeSensitiveHttpHeaderValue(headerName, unfoldedCandidate.text, 0, unfoldedCandidate.text.length)
|
|
346
|
+
: looksLikeSensitiveHttpHeaderValue(headerName, value, shapeStart, physicalEnd)))
|
|
347
|
+
return undefined;
|
|
348
|
+
const candidateQuotedValue = folded
|
|
349
|
+
? undefined
|
|
350
|
+
: quotedValueEnd(value, separatorEnd, physicalEnd);
|
|
351
|
+
const quotedEnd = candidateQuotedValue && !candidateQuotedValue.ambiguous
|
|
352
|
+
? candidateQuotedValue.end
|
|
353
|
+
: undefined;
|
|
354
|
+
const preciseEnd = (folded ? undefined : inspectArray?.end)
|
|
355
|
+
?? quotedEnd
|
|
356
|
+
?? (folded ? undefined : token68ValueEnd(headerName, value, separatorEnd, lineEnd))
|
|
357
|
+
?? (folded ? undefined : cookieValueEnd(headerName, value, separatorEnd, lineEnd));
|
|
358
|
+
const end = preciseEnd ?? physicalEnd;
|
|
359
|
+
const quote = quotedEnd
|
|
360
|
+
? value[separatorEnd]
|
|
361
|
+
: (inspectArray ? (labelQuote ?? '\x22') : undefined);
|
|
362
|
+
return {
|
|
363
|
+
start: separatorEnd,
|
|
364
|
+
end,
|
|
365
|
+
replacement: quote ? quote + REDACTED_HEADER_VALUE + quote : REDACTED_HEADER_VALUE,
|
|
366
|
+
resumeAt: end,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function jsonContainerForOpen(kind) {
|
|
370
|
+
if (kind === JSON_TOKEN.openBracket)
|
|
371
|
+
return 'array';
|
|
372
|
+
if (kind === JSON_TOKEN.openBrace)
|
|
373
|
+
return 'object';
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
function jsonContainerForClose(kind) {
|
|
377
|
+
if (kind === JSON_TOKEN.closeBracket)
|
|
378
|
+
return 'array';
|
|
379
|
+
if (kind === JSON_TOKEN.closeBrace)
|
|
380
|
+
return 'object';
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
function isJsonScalar(kind) {
|
|
384
|
+
return kind === JSON_TOKEN.string
|
|
385
|
+
|| kind === JSON_TOKEN.number
|
|
386
|
+
|| kind === JSON_TOKEN.null
|
|
387
|
+
|| kind === JSON_TOKEN.true
|
|
388
|
+
|| kind === JSON_TOKEN.false;
|
|
389
|
+
}
|
|
390
|
+
function mightContainStructuredHeader(value) {
|
|
391
|
+
return /['"]/.test(value)
|
|
392
|
+
&& (SENSITIVE_HTTP_HEADER_CANDIDATE.test(value) || /\\u[0-9a-f]{4}/i.test(value));
|
|
393
|
+
}
|
|
394
|
+
function applyRedactionSpans(value, spans) {
|
|
395
|
+
if (spans.length === 0)
|
|
396
|
+
return value;
|
|
397
|
+
spans.sort((left, right) => left.start - right.start || right.end - left.end);
|
|
398
|
+
let cursor = 0;
|
|
399
|
+
let output = '';
|
|
400
|
+
for (const span of spans) {
|
|
401
|
+
if (span.start < cursor)
|
|
402
|
+
continue;
|
|
403
|
+
output += value.slice(cursor, span.start) + span.replacement;
|
|
404
|
+
cursor = span.end;
|
|
405
|
+
}
|
|
406
|
+
return output + value.slice(cursor);
|
|
407
|
+
}
|
|
408
|
+
function redactStructuredJsonText(value, serializedDepth = 0) {
|
|
409
|
+
if (serializedDepth >= MAX_SERIALIZED_JSON_DEPTH)
|
|
410
|
+
return REDACTED_HEADER_VALUE;
|
|
411
|
+
if (!mightContainStructuredHeader(value))
|
|
412
|
+
return value;
|
|
413
|
+
const scanner = createScanner(value, true);
|
|
414
|
+
const containers = [];
|
|
415
|
+
const spans = [];
|
|
416
|
+
let headerLabel;
|
|
417
|
+
let awaitingHeaderValue;
|
|
418
|
+
let activeComposite;
|
|
419
|
+
let depthOverflow = false;
|
|
420
|
+
let structuralMismatch = false;
|
|
421
|
+
let structuredCoveredUntil = 0;
|
|
422
|
+
const pushContainer = (container) => {
|
|
423
|
+
if (containers.length >= MAX_JSON_CONTAINER_DEPTH) {
|
|
424
|
+
depthOverflow = true;
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
containers.push(container);
|
|
428
|
+
return true;
|
|
429
|
+
};
|
|
430
|
+
const popContainer = (container) => {
|
|
431
|
+
if (containers.at(-1) !== container) {
|
|
432
|
+
structuralMismatch = true;
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
containers.pop();
|
|
436
|
+
};
|
|
437
|
+
const redactValue = (kind, offset, length, headerName, requireCredentialShape) => {
|
|
438
|
+
if (requireCredentialShape && kind === JSON_TOKEN.string) {
|
|
439
|
+
const decoded = scanner.getTokenValue();
|
|
440
|
+
if (!looksLikeSensitiveHttpHeaderValue(headerName, decoded, 0, decoded.length))
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
const container = jsonContainerForOpen(kind);
|
|
444
|
+
if (container) {
|
|
445
|
+
if (requireCredentialShape && container !== 'array')
|
|
446
|
+
return false;
|
|
447
|
+
activeComposite = {
|
|
448
|
+
start: offset,
|
|
449
|
+
baseDepth: containers.length,
|
|
450
|
+
...(requireCredentialShape
|
|
451
|
+
? { credentialHeaderName: headerName, credentialFound: false }
|
|
452
|
+
: {}),
|
|
453
|
+
};
|
|
454
|
+
pushContainer(container);
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
if (requireCredentialShape && kind !== JSON_TOKEN.string)
|
|
458
|
+
return false;
|
|
459
|
+
if (isJsonScalar(kind)) {
|
|
460
|
+
spans.push({
|
|
461
|
+
start: offset,
|
|
462
|
+
end: offset + length,
|
|
463
|
+
replacement: REDACTED_JSON_VALUE,
|
|
464
|
+
resumeAt: offset + length,
|
|
465
|
+
});
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
if (kind === JSON_TOKEN.unknown) {
|
|
469
|
+
if (offset < structuredCoveredUntil)
|
|
470
|
+
return true;
|
|
471
|
+
const end = physicalHeaderEnd(value, offset);
|
|
472
|
+
spans.push({ start: offset, end, replacement: REDACTED_JSON_VALUE, resumeAt: end });
|
|
473
|
+
structuredCoveredUntil = end;
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
return false;
|
|
477
|
+
};
|
|
478
|
+
for (let kind = scanner.scan(); kind !== JSON_TOKEN.eof; kind = scanner.scan()) {
|
|
479
|
+
const offset = scanner.getTokenOffset();
|
|
480
|
+
const length = scanner.getTokenLength();
|
|
481
|
+
const openedContainer = jsonContainerForOpen(kind);
|
|
482
|
+
const closedContainer = jsonContainerForClose(kind);
|
|
483
|
+
if (activeComposite) {
|
|
484
|
+
if (activeComposite.credentialHeaderName
|
|
485
|
+
&& containers.length === activeComposite.baseDepth + 1
|
|
486
|
+
&& kind === JSON_TOKEN.string) {
|
|
487
|
+
const decoded = stripUnsafeControls(stripTerminalEscapes(scanner.getTokenValue()));
|
|
488
|
+
if (looksLikeSensitiveHttpHeaderValue(activeComposite.credentialHeaderName, decoded, 0, decoded.length))
|
|
489
|
+
activeComposite.credentialFound = true;
|
|
490
|
+
}
|
|
491
|
+
if (openedContainer)
|
|
492
|
+
pushContainer(openedContainer);
|
|
493
|
+
if (closedContainer) {
|
|
494
|
+
popContainer(closedContainer);
|
|
495
|
+
if (containers.length <= activeComposite.baseDepth) {
|
|
496
|
+
const completed = activeComposite;
|
|
497
|
+
if (!completed.credentialHeaderName || completed.credentialFound) {
|
|
498
|
+
spans.push({
|
|
499
|
+
start: completed.start,
|
|
500
|
+
end: offset + length,
|
|
501
|
+
replacement: REDACTED_JSON_VALUE,
|
|
502
|
+
resumeAt: offset + length,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
const original = value.slice(completed.start, offset + length);
|
|
507
|
+
const nested = redactStructuredJsonText(original, serializedDepth + 1);
|
|
508
|
+
if (nested !== original) {
|
|
509
|
+
spans.push({
|
|
510
|
+
start: completed.start,
|
|
511
|
+
end: offset + length,
|
|
512
|
+
replacement: nested === REDACTED_HEADER_VALUE ? REDACTED_JSON_VALUE : nested,
|
|
513
|
+
resumeAt: offset + length,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
activeComposite = undefined;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (depthOverflow || structuralMismatch)
|
|
521
|
+
break;
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (awaitingHeaderValue) {
|
|
525
|
+
const pending = awaitingHeaderValue;
|
|
526
|
+
awaitingHeaderValue = undefined;
|
|
527
|
+
if (redactValue(kind, offset, length, pending.name, pending.requireCredentialShape)) {
|
|
528
|
+
if (depthOverflow)
|
|
529
|
+
break;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
else if (headerLabel) {
|
|
534
|
+
const isSeparator = (headerLabel.container === 'object' && kind === JSON_TOKEN.colon)
|
|
535
|
+
|| (headerLabel.container === 'array' && kind === JSON_TOKEN.comma);
|
|
536
|
+
const pending = headerLabel;
|
|
537
|
+
headerLabel = undefined;
|
|
538
|
+
if (isSeparator) {
|
|
539
|
+
awaitingHeaderValue = {
|
|
540
|
+
name: pending.name,
|
|
541
|
+
requireCredentialShape: kind === JSON_TOKEN.comma,
|
|
542
|
+
};
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (openedContainer) {
|
|
547
|
+
pushContainer(openedContainer);
|
|
548
|
+
if (depthOverflow)
|
|
549
|
+
break;
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (closedContainer) {
|
|
553
|
+
popContainer(closedContainer);
|
|
554
|
+
if (structuralMismatch)
|
|
555
|
+
break;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (kind !== JSON_TOKEN.string)
|
|
559
|
+
continue;
|
|
560
|
+
const decoded = scanner.getTokenValue();
|
|
561
|
+
const normalizedDecoded = stripUnsafeControls(stripTerminalEscapes(decoded));
|
|
562
|
+
const currentContainer = containers.at(-1);
|
|
563
|
+
if (currentContainer && SENSITIVE_HTTP_HEADER_KEY.test(normalizedDecoded)) {
|
|
564
|
+
headerLabel = { name: normalizedDecoded, container: currentContainer };
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
if (!SENSITIVE_HTTP_HEADER_CANDIDATE.test(normalizedDecoded)
|
|
568
|
+
&& !/\\u[0-9a-f]{4}/i.test(normalizedDecoded)) {
|
|
569
|
+
if (normalizedDecoded !== decoded) {
|
|
570
|
+
spans.push({
|
|
571
|
+
start: offset,
|
|
572
|
+
end: offset + length,
|
|
573
|
+
replacement: JSON.stringify(normalizedDecoded),
|
|
574
|
+
resumeAt: offset + length,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
const structured = redactStructuredJsonText(normalizedDecoded, serializedDepth + 1);
|
|
580
|
+
const redacted = redactUnstructuredHttpHeaders(structured, true);
|
|
581
|
+
if (redacted === decoded)
|
|
582
|
+
continue;
|
|
583
|
+
spans.push({
|
|
584
|
+
start: offset,
|
|
585
|
+
end: offset + length,
|
|
586
|
+
replacement: JSON.stringify(redacted),
|
|
587
|
+
resumeAt: offset + length,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
if (depthOverflow || structuralMismatch)
|
|
591
|
+
return REDACTED_HEADER_VALUE;
|
|
592
|
+
if (activeComposite && (!activeComposite.credentialHeaderName || activeComposite.credentialFound)) {
|
|
593
|
+
spans.push({
|
|
594
|
+
start: activeComposite.start,
|
|
595
|
+
end: value.length,
|
|
596
|
+
replacement: REDACTED_JSON_VALUE,
|
|
597
|
+
resumeAt: value.length,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
return applyRedactionSpans(value, spans);
|
|
601
|
+
}
|
|
6
602
|
function stripUnsafeControls(value) {
|
|
7
603
|
let out = '';
|
|
8
604
|
for (const char of value) {
|
|
9
605
|
const code = char.codePointAt(0) ?? 0;
|
|
10
|
-
if (code >= 0x20
|
|
606
|
+
if ((code >= 0x20 && (code < 0x7f || code > 0x9f))
|
|
607
|
+
|| code === 0x09 || code === 0x0a || code === 0x0d)
|
|
11
608
|
out += char;
|
|
12
609
|
}
|
|
13
610
|
return out;
|
|
14
611
|
}
|
|
612
|
+
function stripTerminalEscapes(value) {
|
|
613
|
+
const skipCsi = (start) => {
|
|
614
|
+
let cursor = start;
|
|
615
|
+
while (cursor < value.length) {
|
|
616
|
+
const code = value.charCodeAt(cursor);
|
|
617
|
+
cursor += 1;
|
|
618
|
+
if (code >= 0x40 && code <= 0x7e)
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
return cursor;
|
|
622
|
+
};
|
|
623
|
+
const skipControlString = (start, allowBell) => {
|
|
624
|
+
let cursor = start;
|
|
625
|
+
while (cursor < value.length) {
|
|
626
|
+
const code = value.charCodeAt(cursor);
|
|
627
|
+
if ((allowBell && code === 0x07) || code === 0x9c)
|
|
628
|
+
return cursor + 1;
|
|
629
|
+
if (code === 0x1b && value.charCodeAt(cursor + 1) === 0x5c)
|
|
630
|
+
return cursor + 2;
|
|
631
|
+
cursor += 1;
|
|
632
|
+
}
|
|
633
|
+
return cursor;
|
|
634
|
+
};
|
|
635
|
+
const skipEscapeSequence = (start) => {
|
|
636
|
+
let cursor = start;
|
|
637
|
+
while (cursor < value.length) {
|
|
638
|
+
const code = value.charCodeAt(cursor);
|
|
639
|
+
if (code < 0x20 || code > 0x2f)
|
|
640
|
+
break;
|
|
641
|
+
cursor += 1;
|
|
642
|
+
}
|
|
643
|
+
const final = value.charCodeAt(cursor);
|
|
644
|
+
if (final >= 0x30 && final <= 0x7e)
|
|
645
|
+
return cursor + 1;
|
|
646
|
+
return cursor > start ? value.length : start;
|
|
647
|
+
};
|
|
648
|
+
let output = '';
|
|
649
|
+
for (let index = 0; index < value.length;) {
|
|
650
|
+
const code = value.charCodeAt(index);
|
|
651
|
+
if (code === 0x9b) {
|
|
652
|
+
index = skipCsi(index + 1);
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (code === 0x9d) {
|
|
656
|
+
index = skipControlString(index + 1, true);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (code === 0x90 || code === 0x98 || code === 0x9e || code === 0x9f) {
|
|
660
|
+
index = skipControlString(index + 1, false);
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
if (code === 0x1b) {
|
|
664
|
+
const next = value.charCodeAt(index + 1);
|
|
665
|
+
if (next === 0x5b) {
|
|
666
|
+
index = skipCsi(index + 2);
|
|
667
|
+
}
|
|
668
|
+
else if (next === 0x5d) {
|
|
669
|
+
index = skipControlString(index + 2, true);
|
|
670
|
+
}
|
|
671
|
+
else if (next === 0x50 || next === 0x58 || next === 0x5e || next === 0x5f) {
|
|
672
|
+
index = skipControlString(index + 2, false);
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
index = skipEscapeSequence(index + 1);
|
|
676
|
+
}
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
output += value[index];
|
|
680
|
+
index += 1;
|
|
681
|
+
}
|
|
682
|
+
return output;
|
|
683
|
+
}
|
|
684
|
+
function redactUnstructuredHttpHeaders(value, embedded) {
|
|
685
|
+
const spans = [];
|
|
686
|
+
const budget = { remaining: Math.max(value.length * 2, 1_024) };
|
|
687
|
+
SENSITIVE_HTTP_HEADER_NAME.lastIndex = 0;
|
|
688
|
+
let lineStart = 0;
|
|
689
|
+
let lineEnd = physicalLineEnd(value, 0);
|
|
690
|
+
let cachedLineEnd = -1;
|
|
691
|
+
let cachedPhysicalEnd = lineEnd;
|
|
692
|
+
const physicalHeaderEndCache = new Map();
|
|
693
|
+
let rawPositionAvailable = true;
|
|
694
|
+
let match;
|
|
695
|
+
while ((match = SENSITIVE_HTTP_HEADER_NAME.exec(value)) !== null) {
|
|
696
|
+
const headerName = match[1];
|
|
697
|
+
if (!headerName)
|
|
698
|
+
continue;
|
|
699
|
+
const nameStart = match.index;
|
|
700
|
+
const nameEnd = nameStart + headerName.length;
|
|
701
|
+
while (nameStart > lineEnd && lineEnd < value.length) {
|
|
702
|
+
lineStart = value[lineEnd] === '\r' && value[lineEnd + 1] === '\n'
|
|
703
|
+
? lineEnd + 2
|
|
704
|
+
: lineEnd + 1;
|
|
705
|
+
lineEnd = physicalLineEnd(value, lineStart);
|
|
706
|
+
rawPositionAvailable = true;
|
|
707
|
+
}
|
|
708
|
+
if (cachedLineEnd !== lineEnd) {
|
|
709
|
+
cachedLineEnd = lineEnd;
|
|
710
|
+
cachedPhysicalEnd = physicalHeaderEndFromLineEnd(value, lineEnd, physicalHeaderEndCache);
|
|
711
|
+
}
|
|
712
|
+
const span = (rawPositionAvailable
|
|
713
|
+
? rawHeaderSpan(value, headerName, lineStart, lineEnd, cachedPhysicalEnd, nameStart, nameEnd, embedded)
|
|
714
|
+
: undefined)
|
|
715
|
+
?? inlineHeaderSpan(value, headerName, lineEnd, cachedPhysicalEnd, nameStart, nameEnd, budget);
|
|
716
|
+
rawPositionAvailable = false;
|
|
717
|
+
if (!span) {
|
|
718
|
+
SENSITIVE_HTTP_HEADER_NAME.lastIndex = nameEnd;
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
spans.push(span);
|
|
722
|
+
SENSITIVE_HTTP_HEADER_NAME.lastIndex = Math.max(span.resumeAt, nameEnd);
|
|
723
|
+
}
|
|
724
|
+
SENSITIVE_HTTP_HEADER_NAME.lastIndex = 0;
|
|
725
|
+
if (spans.length === 0)
|
|
726
|
+
return value;
|
|
727
|
+
let cursor = 0;
|
|
728
|
+
let output = '';
|
|
729
|
+
for (const span of spans) {
|
|
730
|
+
output += value.slice(cursor, span.start) + span.replacement;
|
|
731
|
+
cursor = span.end;
|
|
732
|
+
}
|
|
733
|
+
return output + value.slice(cursor);
|
|
734
|
+
}
|
|
735
|
+
export function redactSensitiveHttpHeaders(value) {
|
|
736
|
+
const couldBeStructured = value.includes('{')
|
|
737
|
+
|| value.includes('[')
|
|
738
|
+
|| value.includes('\x22')
|
|
739
|
+
|| value.includes('\x27');
|
|
740
|
+
const structured = couldBeStructured
|
|
741
|
+
? redactStructuredJsonText(value)
|
|
742
|
+
: value;
|
|
743
|
+
const normalized = stripUnsafeControls(stripTerminalEscapes(structured));
|
|
744
|
+
return redactUnstructuredHttpHeaders(normalized, false);
|
|
745
|
+
}
|
|
15
746
|
export function redactDiagnosticText(value, maxChars = MAX_TEXT) {
|
|
16
747
|
const raw = typeof value === 'string' ? value : value == null ? '' : String(value);
|
|
17
|
-
const
|
|
748
|
+
const diagnosticRedacted = redactSensitiveHttpHeaders(raw)
|
|
749
|
+
.replace(/^([ \t]*)(proxy[-_]?authorization|authorization|set[-_]?cookie|cookie)\b\s*:\s*\[redacted\]/gim, '$1$2=[redacted]')
|
|
18
750
|
.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi, '[redacted private key]')
|
|
19
751
|
.replace(/\b(https?:\/\/)[^/\s@]+@/gi, '$1')
|
|
20
752
|
.replace(/\b(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, '$1[redacted]')
|
|
21
|
-
.replace(/\b(
|
|
753
|
+
.replace(/\b(proxy[-_]?authorization|authorization)\b\s*=\s*(?!(?:\x22|')?\[redacted\](?:\x22|')?)(?:\x22[^\x22]*\x22|'[^']*'|[^\s,;\x22']+)/gi, '$1=[redacted]')
|
|
754
|
+
.replace(/\b(cookie|set[-_]?cookie)\b\s*=\s*(?!(?:\x22|')?\[redacted\](?:\x22|')?)(?:\x22[^\x22]*\x22|'[^']*'|[^\s,;\x22']+)/gi, '$1=[redacted]')
|
|
755
|
+
.replace(/\b(api[_-]?key|account[_-]?key|instrumentation[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|private[_-]?key|node[_-]?secret|token|password|passwd|secret)\b\s*[:=]\s*(?!(?:\x22|')?\[redacted\](?:\x22|')?)(?:\x22[^\x22]*\x22|'[^']*'|[^\s,;\x22']+)/gi, '$1=[redacted]')
|
|
22
756
|
.replace(/\b[A-Za-z]:\\[^\s"']+/g, '[path]')
|
|
23
757
|
.replace(/\\\\[^\\\s"']+\\[^\\\s"']+(?:\\[^\\\s"']+)*/g, '[path]')
|
|
24
758
|
.replace(/(?:\/(?:Users|home|var|tmp|private|opt)\/[^\s"']+)+/g, '[path]')
|
|
25
759
|
.trim();
|
|
760
|
+
const redacted = diagnosticRedacted
|
|
761
|
+
.split('[redacted]')
|
|
762
|
+
.map((segment) => hub.redactString(segment))
|
|
763
|
+
.join('[redacted]')
|
|
764
|
+
.trim();
|
|
26
765
|
return redacted.length > maxChars ? `${redacted.slice(0, maxChars)}…` : redacted;
|
|
27
766
|
}
|
|
28
767
|
export function sanitizeDiagnosticValue(value, depth = 0) {
|
package/dist/eventSnapshot.d.ts
CHANGED
package/dist/eventSnapshot.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { Worker } from 'node:worker_threads';
|
|
4
4
|
import { events as ev } from '@evomap/evolver-core';
|
|
5
5
|
const ARCHIVE_SEGMENT_PATTERN = /^root-events-\d{16}-\d{16}\.jsonl$/;
|
|
6
|
+
const MAX_VERSIONED_READ_ATTEMPTS = 2;
|
|
6
7
|
const EVENT_SNAPSHOT_WORKER_SOURCE = `
|
|
7
8
|
import { parentPort, workerData } from 'node:worker_threads';
|
|
8
9
|
|
|
@@ -132,6 +133,9 @@ export class EventSnapshotCache {
|
|
|
132
133
|
catch {
|
|
133
134
|
return await this.source.read();
|
|
134
135
|
}
|
|
136
|
+
return await this.readVersioned(before, MAX_VERSIONED_READ_ATTEMPTS);
|
|
137
|
+
}
|
|
138
|
+
async readVersioned(before, attemptsRemaining) {
|
|
135
139
|
if (this.cached?.version === before)
|
|
136
140
|
return this.cached.events;
|
|
137
141
|
const events = await this.source.read();
|
|
@@ -142,8 +146,12 @@ export class EventSnapshotCache {
|
|
|
142
146
|
catch {
|
|
143
147
|
return events;
|
|
144
148
|
}
|
|
145
|
-
if (before === after)
|
|
149
|
+
if (before === after) {
|
|
146
150
|
this.cached = { version: after, events };
|
|
147
|
-
|
|
151
|
+
return events;
|
|
152
|
+
}
|
|
153
|
+
if (attemptsRemaining === 1)
|
|
154
|
+
return events;
|
|
155
|
+
return await this.readVersioned(after, attemptsRemaining - 1);
|
|
148
156
|
}
|
|
149
157
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// jsonc-parser's main entry is a UMD wrapper whose shadowed `require` leaves
|
|
2
|
+
// './impl/format' unresolved inside bun standalone binaries, and its ESM entry
|
|
3
|
+
// uses extensionless imports that Node ESM rejects. Depend on the scanner impl
|
|
4
|
+
// directly: it has no sibling dependencies, so bundlers can inline it safely.
|
|
5
|
+
// Use a default import because the impl is CommonJS and named CJS exports are
|
|
6
|
+
// not reliably detected by Node's ESM loader. The explicit JsoncScanner type
|
|
7
|
+
// keeps the emitted .d.ts self-contained so consumers never resolve the deep
|
|
8
|
+
// specifier.
|
|
9
|
+
import scannerImpl from 'jsonc-parser/lib/umd/impl/scanner.js';
|
|
10
|
+
export const createScanner = scannerImpl.createScanner;
|
package/dist/logDiagnostics.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync } from 'node:fs';
|
|
2
|
+
import { hub } from '@evomap/evolver-core';
|
|
3
|
+
import { redactSensitiveHttpHeaders } from './diagnosticSanitize.js';
|
|
2
4
|
export const LOG_DIAGNOSTICS_MAX_BYTES = 128 * 1024;
|
|
3
5
|
export const LOG_DIAGNOSTICS_MAX_LINES = 200;
|
|
4
6
|
const LOG_DIAGNOSTICS_HARD_MAX_BYTES = 1024 * 1024;
|
|
@@ -10,13 +12,19 @@ function boundedInteger(value, fallback, maximum) {
|
|
|
10
12
|
return Math.max(1, Math.min(maximum, Math.floor(value)));
|
|
11
13
|
}
|
|
12
14
|
function redactSecrets(input) {
|
|
13
|
-
let text = input;
|
|
15
|
+
let text = replaceUnsafeControls(input);
|
|
14
16
|
text = text.replace(/-----BEGIN(?: [A-Z0-9]+)* PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z0-9]+)* PRIVATE KEY-----/gi, '[redacted private key]');
|
|
15
17
|
text = text.replace(/-----BEGIN(?: [A-Z0-9]+)* PRIVATE KEY-----[\s\S]*$/gi, '[redacted private key]');
|
|
16
18
|
text = text.replace(/\bBearer\s+[^\s,;"']+/gi, 'Bearer [redacted]');
|
|
17
|
-
text = text.replace(/\b(
|
|
19
|
+
text = text.replace(/\b(proxy[-_]?authorization|authorization)\b(\s*=\s*)(?!(?:"|')?\[redacted\](?:"|')?)(?:"[^"]*"|'[^']*'|[^\s,;"']+)/gi, '$1$2[redacted]');
|
|
20
|
+
text = text.replace(/\b(cookie|set[-_]?cookie)\b(\s*=\s*)(?!(?:"|')?\[redacted\](?:"|')?)(?:"[^"]*"|'[^']*'|[^\s,;"']+)/gi, '$1$2[redacted]');
|
|
21
|
+
text = text.replace(/\b(api[_ -]?key|token|access[_ -]?token|refresh[_ -]?token|password|passwd)\b(\s*[=:]\s*)(?!(?:"|')?\[redacted\](?:"|')?)(?:"[^"]*"|'[^']*'|[^\s,;"']+)/gi, '$1$2[redacted]');
|
|
18
22
|
text = text.replace(/([?&](?:api[_-]?key|token|access_token|password)=)[^&\s]+/gi, '$1[redacted]');
|
|
19
|
-
|
|
23
|
+
text = text.replace(/\b(id[_ -]?token|private[_ -]?key|node[_ -]?secret|account[_ -]?key|instrumentation[_ -]?key)\b(\s*[=:]\s*)[^\s,;]+/gi, '$1$2[redacted]');
|
|
24
|
+
return text
|
|
25
|
+
.split('[redacted]')
|
|
26
|
+
.map((segment) => hub.redactString(segment))
|
|
27
|
+
.join('[redacted]');
|
|
20
28
|
}
|
|
21
29
|
function replaceUnsafeControls(value) {
|
|
22
30
|
let out = '';
|
|
@@ -32,6 +40,9 @@ function safeLine(line) {
|
|
|
32
40
|
return '[redacted private key material]';
|
|
33
41
|
return normalized.slice(0, MAX_LINE_CHARS);
|
|
34
42
|
}
|
|
43
|
+
function redactTruncatedLeadingContinuations(value) {
|
|
44
|
+
return value.replace(/^(?:[ \t]+[^\r\n]*(?:\r?\n|$))+/, '[redacted truncated continuation]\n');
|
|
45
|
+
}
|
|
35
46
|
export function readLogDiagnostics(logFile, options = {}) {
|
|
36
47
|
const maxBytes = boundedInteger(options.maxBytes, LOG_DIAGNOSTICS_MAX_BYTES, LOG_DIAGNOSTICS_HARD_MAX_BYTES);
|
|
37
48
|
const maxLines = boundedInteger(options.maxLines, LOG_DIAGNOSTICS_MAX_LINES, LOG_DIAGNOSTICS_HARD_MAX_LINES);
|
|
@@ -54,10 +65,21 @@ export function readLogDiagnostics(logFile, options = {}) {
|
|
|
54
65
|
const buffer = Buffer.alloc(bytes);
|
|
55
66
|
if (bytes > 0)
|
|
56
67
|
readSync(fd, buffer, 0, bytes, start);
|
|
68
|
+
let startsAtLineBoundary = start === 0;
|
|
69
|
+
if (start > 0) {
|
|
70
|
+
const previous = Buffer.allocUnsafe(1);
|
|
71
|
+
startsAtLineBoundary = readSync(fd, previous, 0, 1, start - 1) === 1 && previous[0] === 0x0a;
|
|
72
|
+
}
|
|
57
73
|
let text = buffer.toString('utf8');
|
|
58
74
|
if (start > 0) {
|
|
59
|
-
|
|
60
|
-
|
|
75
|
+
if (!startsAtLineBoundary) {
|
|
76
|
+
const firstNewline = text.indexOf('\n');
|
|
77
|
+
text = firstNewline >= 0 ? text.slice(firstNewline + 1) : '';
|
|
78
|
+
}
|
|
79
|
+
text = redactTruncatedLeadingContinuations(replaceUnsafeControls(redactSensitiveHttpHeaders(text)));
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
text = replaceUnsafeControls(redactSensitiveHttpHeaders(text));
|
|
61
83
|
}
|
|
62
84
|
const redacted = redactSecrets(text);
|
|
63
85
|
const allLines = redacted.split(/\r?\n/).filter((line) => line.length > 0);
|
package/dist/server.js
CHANGED
|
@@ -256,6 +256,39 @@ export class WebUIServer {
|
|
|
256
256
|
}
|
|
257
257
|
if (p === '/api/triggers')
|
|
258
258
|
return this.json(res, ev.listTriggers(await this.eventSnapshots.read()));
|
|
259
|
+
if (p === '/api/evolution-graph') {
|
|
260
|
+
// Read-only projection of the SAME event snapshot the other cards read. The WebUI never re-derives lineage:
|
|
261
|
+
// core owns the projector, the server only bounds the window and serializes the summary + edge list.
|
|
262
|
+
if (!requireGet(req, res))
|
|
263
|
+
return;
|
|
264
|
+
try {
|
|
265
|
+
const graph = ops.projectEvolutionGraph(await this.eventSnapshots.read(), {
|
|
266
|
+
maxEvents: positiveIntParam(url.searchParams.get('maxEvents')),
|
|
267
|
+
});
|
|
268
|
+
return this.json(res, {
|
|
269
|
+
available: true,
|
|
270
|
+
graphId: graph.graphId,
|
|
271
|
+
generatedAt: graph.generatedAt,
|
|
272
|
+
dashboard: graph.dashboard,
|
|
273
|
+
nodes: graph.nodes.map((node) => ({ id: node.id, kind: node.kind, label: redactDiagnosticText(node.label) })),
|
|
274
|
+
edges: graph.edges.map((edge) => ({
|
|
275
|
+
id: edge.id,
|
|
276
|
+
kind: edge.kind,
|
|
277
|
+
from: edge.from,
|
|
278
|
+
to: edge.to,
|
|
279
|
+
...(edge.reason ? { reason: redactDiagnosticText(edge.reason) } : {}),
|
|
280
|
+
...(edge.metricDelta ? { metricDelta: edge.metricDelta } : {}),
|
|
281
|
+
provenance: edge.provenance.map((entry) => ({ kind: entry.kind, ref: entry.ref })),
|
|
282
|
+
})),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return this.send(res, 503, 'application/json', JSON.stringify({
|
|
287
|
+
available: false,
|
|
288
|
+
error: 'evolution_graph_unavailable',
|
|
289
|
+
}));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
259
292
|
if (p === '/api/daily-summary') {
|
|
260
293
|
const day = url.searchParams.get('day') ?? new Date(this.now()).toISOString().slice(0, 10);
|
|
261
294
|
return this.json(res, ev.dailySummary(await this.eventSnapshots.read(), day));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evomap/evolver-webui",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.22",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@evomap/evolver-core": "2.0.0-beta.
|
|
19
|
+
"@evomap/evolver-core": "2.0.0-beta.22",
|
|
20
|
+
"jsonc-parser": "3.3.1"
|
|
20
21
|
},
|
|
21
22
|
"repository": {
|
|
22
23
|
"type": "git",
|