@backtrack-js/browser 0.4.2 → 0.5.1
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/README.md +507 -364
- package/bin/symbolize.cjs +205 -62
- package/dev-server/package.json +6 -0
- package/dist/dev-server/index.d.ts +2 -0
- package/dist/dev-server/index.d.ts.map +1 -0
- package/dist/dev-server/middleware.d.ts +14 -0
- package/dist/dev-server/middleware.d.ts.map +1 -0
- package/dist/dev-server.cjs +7 -0
- package/dist/dev-server.cjs.map +1 -0
- package/dist/dev-server.js +173 -0
- package/dist/dev-server.js.map +1 -0
- package/dist/index.cjs +41 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1724 -2043
- package/dist/index.js.map +1 -1
- package/dist/markdown-Ao5Ah9MF.cjs +9 -0
- package/dist/markdown-Ao5Ah9MF.cjs.map +1 -0
- package/dist/markdown-D3C94OlY.js +494 -0
- package/dist/markdown-D3C94OlY.js.map +1 -0
- package/dist/types/timeline.d.ts +3 -2
- package/dist/types/timeline.d.ts.map +1 -1
- package/dist/utils/markdown.d.ts +11 -3
- package/dist/utils/markdown.d.ts.map +1 -1
- package/dist/validation/validate.d.ts.map +1 -1
- package/dist/viewer/assets/index-C3SZvpLL.js +197 -0
- package/dist/viewer/assets/{index-cVrM07fr.css → index-CKVr0pQ5.css} +1 -1
- package/dist/viewer/index.html +2 -2
- package/package.json +7 -1
- package/dist/viewer/assets/index-BXa4ZYfb.js +0 -191
package/bin/symbolize.cjs
CHANGED
|
@@ -7,9 +7,77 @@ const {
|
|
|
7
7
|
sourceContentFor
|
|
8
8
|
} = require('@jridgewell/trace-mapping');
|
|
9
9
|
|
|
10
|
-
// ponytail: parser inicial cobre stacks V8; adicionar Firefox/Safari quando fixtures reais exigirem.
|
|
11
10
|
const FRAME_WITH_FUNCTION = /^\s*at\s+(.+?)\s+\((.+):(\d+):(\d+)\)\s*$/;
|
|
11
|
+
const FRAME_WITHOUT_FUNCTION_PARENS = /^\s*at\s+\((.+):(\d+):(\d+)\)\s*$/;
|
|
12
12
|
const FRAME_WITHOUT_FUNCTION = /^\s*at\s+(.+):(\d+):(\d+)\s*$/;
|
|
13
|
+
const FRAME_FIREFOX_SAFARI = /^\s*(.*?)\s*@\s*(.+):(\d+):(\d+)\s*$/;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Analisa uma linha de stack trace (V8, Firefox ou Safari) e extrai
|
|
17
|
+
* nome da função, arquivo gerado, linha e coluna.
|
|
18
|
+
* @param {string} line
|
|
19
|
+
* @returns {{ functionName?: string, generatedFile: string, generatedLine: number, generatedColumn: number } | undefined}
|
|
20
|
+
*/
|
|
21
|
+
function parseStackFrame(line) {
|
|
22
|
+
if (!line || typeof line !== 'string') {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let functionName = undefined;
|
|
27
|
+
let generatedFile = undefined;
|
|
28
|
+
let lineStr = undefined;
|
|
29
|
+
let colStr = undefined;
|
|
30
|
+
|
|
31
|
+
const withFnMatch = line.match(FRAME_WITH_FUNCTION);
|
|
32
|
+
if (withFnMatch) {
|
|
33
|
+
functionName = withFnMatch[1].trim();
|
|
34
|
+
generatedFile = withFnMatch[2].trim();
|
|
35
|
+
lineStr = withFnMatch[3];
|
|
36
|
+
colStr = withFnMatch[4];
|
|
37
|
+
} else {
|
|
38
|
+
const withoutFnParensMatch = line.match(FRAME_WITHOUT_FUNCTION_PARENS);
|
|
39
|
+
if (withoutFnParensMatch) {
|
|
40
|
+
generatedFile = withoutFnParensMatch[1].trim();
|
|
41
|
+
lineStr = withoutFnParensMatch[2];
|
|
42
|
+
colStr = withoutFnParensMatch[3];
|
|
43
|
+
} else {
|
|
44
|
+
const withoutFnMatch = line.match(FRAME_WITHOUT_FUNCTION);
|
|
45
|
+
if (withoutFnMatch) {
|
|
46
|
+
generatedFile = withoutFnMatch[1].trim();
|
|
47
|
+
lineStr = withoutFnMatch[2];
|
|
48
|
+
colStr = withoutFnMatch[3];
|
|
49
|
+
} else {
|
|
50
|
+
const ffSafariMatch = line.match(FRAME_FIREFOX_SAFARI);
|
|
51
|
+
if (ffSafariMatch) {
|
|
52
|
+
const fn = ffSafariMatch[1].trim();
|
|
53
|
+
functionName = fn.length > 0 ? fn : undefined;
|
|
54
|
+
generatedFile = ffSafariMatch[2].trim();
|
|
55
|
+
lineStr = ffSafariMatch[3];
|
|
56
|
+
colStr = ffSafariMatch[4];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!generatedFile || !lineStr || !colStr) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const generatedLine = parseInt(lineStr, 10);
|
|
67
|
+
const generatedColumn = parseInt(colStr, 10);
|
|
68
|
+
|
|
69
|
+
if (!Number.isInteger(generatedLine) || generatedLine <= 0 ||
|
|
70
|
+
!Number.isInteger(generatedColumn) || generatedColumn <= 0) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
functionName,
|
|
76
|
+
generatedFile,
|
|
77
|
+
generatedLine,
|
|
78
|
+
generatedColumn
|
|
79
|
+
};
|
|
80
|
+
}
|
|
13
81
|
|
|
14
82
|
/**
|
|
15
83
|
* Remove query string, hash, diretórios e decodifica URL para obter o nome do bundle.
|
|
@@ -220,45 +288,14 @@ function symbolicateStack(stack, options) {
|
|
|
220
288
|
const traceMapCache = new Map();
|
|
221
289
|
|
|
222
290
|
const lines = stack.split(/\r?\n/);
|
|
223
|
-
// Ignora a primeira linha contendo a mensagem do erro (se não for um frame)
|
|
224
|
-
const candidateLines = lines.length > 1 && !lines[0].trim().startsWith('at ')
|
|
225
|
-
? lines.slice(1)
|
|
226
|
-
: lines;
|
|
227
|
-
|
|
228
|
-
for (const line of candidateLines) {
|
|
229
|
-
let parsedFunctionName = undefined;
|
|
230
|
-
let generatedFile = undefined;
|
|
231
|
-
let lineStr = undefined;
|
|
232
|
-
let colStr = undefined;
|
|
233
|
-
|
|
234
|
-
const withFnMatch = line.match(FRAME_WITH_FUNCTION);
|
|
235
|
-
if (withFnMatch) {
|
|
236
|
-
parsedFunctionName = withFnMatch[1];
|
|
237
|
-
generatedFile = withFnMatch[2];
|
|
238
|
-
lineStr = withFnMatch[3];
|
|
239
|
-
colStr = withFnMatch[4];
|
|
240
|
-
} else {
|
|
241
|
-
const withoutFnMatch = line.match(FRAME_WITHOUT_FUNCTION);
|
|
242
|
-
if (withoutFnMatch) {
|
|
243
|
-
generatedFile = withoutFnMatch[1];
|
|
244
|
-
lineStr = withoutFnMatch[2];
|
|
245
|
-
colStr = withoutFnMatch[3];
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
291
|
|
|
249
|
-
|
|
250
|
-
|
|
292
|
+
for (const line of lines) {
|
|
293
|
+
const parsed = parseStackFrame(line);
|
|
294
|
+
if (!parsed) {
|
|
251
295
|
continue;
|
|
252
296
|
}
|
|
253
297
|
|
|
254
|
-
const generatedLine =
|
|
255
|
-
const generatedColumn = parseInt(colStr, 10);
|
|
256
|
-
|
|
257
|
-
// Valida que linha e coluna são inteiros positivos
|
|
258
|
-
if (!Number.isInteger(generatedLine) || generatedLine <= 0 ||
|
|
259
|
-
!Number.isInteger(generatedColumn) || generatedColumn <= 0) {
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
298
|
+
const { functionName, generatedFile, generatedLine, generatedColumn } = parsed;
|
|
262
299
|
|
|
263
300
|
const bundleName = extractBundleName(generatedFile);
|
|
264
301
|
const matchingMaps = bundleMapFiles.get(bundleName);
|
|
@@ -267,7 +304,7 @@ function symbolicateStack(stack, options) {
|
|
|
267
304
|
generatedFile,
|
|
268
305
|
generatedLine,
|
|
269
306
|
generatedColumn,
|
|
270
|
-
...(
|
|
307
|
+
...(functionName ? { functionName } : {})
|
|
271
308
|
};
|
|
272
309
|
|
|
273
310
|
if (!matchingMaps || matchingMaps.length === 0) {
|
|
@@ -284,13 +321,17 @@ function symbolicateStack(stack, options) {
|
|
|
284
321
|
}
|
|
285
322
|
|
|
286
323
|
const mapPath = matchingMaps[0];
|
|
287
|
-
let
|
|
288
|
-
if (!
|
|
324
|
+
let cached = traceMapCache.get(mapPath);
|
|
325
|
+
if (!cached) {
|
|
289
326
|
try {
|
|
290
327
|
const rawContent = fs.readFileSync(mapPath, 'utf-8');
|
|
291
328
|
const parsedJson = JSON.parse(rawContent);
|
|
292
|
-
|
|
293
|
-
|
|
329
|
+
const declaredBundle = parsedJson.file && typeof parsedJson.file === 'string'
|
|
330
|
+
? extractBundleName(parsedJson.file)
|
|
331
|
+
: null;
|
|
332
|
+
const traceMap = new TraceMap(parsedJson);
|
|
333
|
+
cached = { traceMap, declaredBundle };
|
|
334
|
+
traceMapCache.set(mapPath, cached);
|
|
294
335
|
} catch (err) {
|
|
295
336
|
result.frames.push(unresolvedFrame);
|
|
296
337
|
result.unresolvedCount++;
|
|
@@ -299,7 +340,19 @@ function symbolicateStack(stack, options) {
|
|
|
299
340
|
}
|
|
300
341
|
}
|
|
301
342
|
|
|
302
|
-
|
|
343
|
+
if (cached.declaredBundle && cached.declaredBundle !== bundleName) {
|
|
344
|
+
result.frames.push(unresolvedFrame);
|
|
345
|
+
result.unresolvedCount++;
|
|
346
|
+
const mapName = path.basename(mapPath);
|
|
347
|
+
result.warnings.push(
|
|
348
|
+
`Source map ${mapName} declara o bundle ${cached.declaredBundle}, mas o frame pertence a ${bundleName}.`
|
|
349
|
+
);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const traceMap = cached.traceMap;
|
|
354
|
+
|
|
355
|
+
// A coluna da stack do navegador (V8/Firefox/Safari) é 1-based, enquanto o TraceMap espera coluna 0-based
|
|
303
356
|
const lookupColumn = generatedColumn > 0 ? generatedColumn - 1 : 0;
|
|
304
357
|
let original = null;
|
|
305
358
|
try {
|
|
@@ -326,7 +379,7 @@ function symbolicateStack(stack, options) {
|
|
|
326
379
|
sourceFile: normalizeSourceFile(original.source),
|
|
327
380
|
sourceLine: original.line,
|
|
328
381
|
sourceColumn: original.column,
|
|
329
|
-
functionName: original.name ??
|
|
382
|
+
...((original.name ?? functionName) ? { functionName: original.name ?? functionName } : {})
|
|
330
383
|
};
|
|
331
384
|
|
|
332
385
|
const { context, warning } = extractContext(traceMap, original.source, original.line, contextLines);
|
|
@@ -388,6 +441,7 @@ Argumentos obrigatórios:
|
|
|
388
441
|
--sourcemaps <diretório> Diretório contendo os arquivos .map
|
|
389
442
|
|
|
390
443
|
Opções adicionais:
|
|
444
|
+
--release <versão> Versão da release para validação de compatibilidade
|
|
391
445
|
--output <arquivo> Caminho do arquivo de saída (padrão: <incidente>.symbolicated.ffr.json)
|
|
392
446
|
--context-lines <número> Quantidade de linhas de contexto antes e depois do erro (0-10, padrão: 3)
|
|
393
447
|
-h, --help Mostra esta mensagem de ajuda
|
|
@@ -422,6 +476,7 @@ async function run(args = []) {
|
|
|
422
476
|
let incidentPath = null;
|
|
423
477
|
let sourcemapsDir = null;
|
|
424
478
|
let outputPath = null;
|
|
479
|
+
let releaseArg = null;
|
|
425
480
|
let contextLines = 3;
|
|
426
481
|
|
|
427
482
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -434,6 +489,13 @@ async function run(args = []) {
|
|
|
434
489
|
sourcemapsDir = args[++i];
|
|
435
490
|
} else if (arg === '--output') {
|
|
436
491
|
outputPath = args[++i];
|
|
492
|
+
} else if (arg === '--release') {
|
|
493
|
+
releaseArg = args[++i];
|
|
494
|
+
if (!releaseArg || releaseArg.startsWith('-')) {
|
|
495
|
+
console.error('[Backtrack] Erro: Opção --release exige uma versão.');
|
|
496
|
+
process.exitCode = 1;
|
|
497
|
+
return 1;
|
|
498
|
+
}
|
|
437
499
|
} else if (arg === '--context-lines') {
|
|
438
500
|
const val = parseInt(args[++i], 10);
|
|
439
501
|
if (!Number.isNaN(val)) {
|
|
@@ -546,37 +608,112 @@ async function run(args = []) {
|
|
|
546
608
|
}
|
|
547
609
|
}
|
|
548
610
|
|
|
549
|
-
let errorEventsCount = 0;
|
|
550
|
-
let totalResolvedFrames = 0;
|
|
551
|
-
let totalUnresolvedFrames = 0;
|
|
552
611
|
const allWarnings = [];
|
|
553
612
|
|
|
613
|
+
const artifactRelease = artifact.context && typeof artifact.context.release === 'string'
|
|
614
|
+
? artifact.context.release
|
|
615
|
+
: undefined;
|
|
616
|
+
|
|
617
|
+
let releaseStatusMsg = null;
|
|
618
|
+
|
|
619
|
+
if (releaseArg) {
|
|
620
|
+
if (artifactRelease) {
|
|
621
|
+
if (artifactRelease !== releaseArg) {
|
|
622
|
+
console.error(
|
|
623
|
+
`[Backtrack] Erro: Release informada (${releaseArg}) é incompatível com a release do incidente (${artifactRelease}).`
|
|
624
|
+
);
|
|
625
|
+
process.exitCode = 1;
|
|
626
|
+
return 1;
|
|
627
|
+
} else {
|
|
628
|
+
releaseStatusMsg = `Release: ${releaseArg} (release informada compatível)`;
|
|
629
|
+
}
|
|
630
|
+
} else {
|
|
631
|
+
allWarnings.push('A versão da release não pôde ser confirmada.');
|
|
632
|
+
}
|
|
633
|
+
} else if (artifactRelease) {
|
|
634
|
+
releaseStatusMsg = `Release do incidente: ${artifactRelease}`;
|
|
635
|
+
allWarnings.push(`Os source maps devem pertencer exatamente à release ${artifactRelease}.`);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
let errorEventsCount = 0;
|
|
639
|
+
let exceptionResolvedFrames = 0;
|
|
640
|
+
let exceptionUnresolvedFrames = 0;
|
|
641
|
+
let reactResolvedFrames = 0;
|
|
642
|
+
let reactUnresolvedFrames = 0;
|
|
643
|
+
|
|
554
644
|
const newTimeline = artifact.timeline.map((event) => {
|
|
555
|
-
if (event.type !== 'error'
|
|
645
|
+
if (event.type !== 'error') {
|
|
646
|
+
return event;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const hasStack = typeof event.stack === 'string' && event.stack.trim().length > 0;
|
|
650
|
+
const hasComponentStack = typeof event.componentStack === 'string' && event.componentStack.trim().length > 0;
|
|
651
|
+
|
|
652
|
+
if (!hasStack && !hasComponentStack) {
|
|
556
653
|
return event;
|
|
557
654
|
}
|
|
558
655
|
|
|
559
656
|
errorEventsCount++;
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
657
|
+
|
|
658
|
+
let stackFrames = [];
|
|
659
|
+
let stackResolvedCount = 0;
|
|
660
|
+
if (hasStack) {
|
|
661
|
+
const stackResult = symbolicateStack(event.stack, {
|
|
662
|
+
sourceMapsDirectory: sourcemapsDir,
|
|
663
|
+
contextLines
|
|
664
|
+
});
|
|
665
|
+
stackResolvedCount = stackResult.resolvedCount;
|
|
666
|
+
exceptionResolvedFrames += stackResult.resolvedCount;
|
|
667
|
+
exceptionUnresolvedFrames += stackResult.unresolvedCount;
|
|
668
|
+
if (stackResult.warnings && stackResult.warnings.length > 0) {
|
|
669
|
+
allWarnings.push(...stackResult.warnings);
|
|
670
|
+
}
|
|
671
|
+
stackFrames = stackResult.frames;
|
|
569
672
|
}
|
|
570
673
|
|
|
674
|
+
let componentFrames = [];
|
|
675
|
+
let componentResolvedCount = 0;
|
|
676
|
+
if (hasComponentStack) {
|
|
677
|
+
const componentResult = symbolicateStack(event.componentStack, {
|
|
678
|
+
sourceMapsDirectory: sourcemapsDir,
|
|
679
|
+
contextLines
|
|
680
|
+
});
|
|
681
|
+
componentResolvedCount = componentResult.resolvedCount;
|
|
682
|
+
reactResolvedFrames += componentResult.resolvedCount;
|
|
683
|
+
reactUnresolvedFrames += componentResult.unresolvedCount;
|
|
684
|
+
if (componentResult.warnings && componentResult.warnings.length > 0) {
|
|
685
|
+
allWarnings.push(...componentResult.warnings);
|
|
686
|
+
}
|
|
687
|
+
componentFrames = componentResult.frames;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Preserva stack e componentStack exatamente como recebidas.
|
|
691
|
+
// Não remove resolvedStack ou resolvedComponentStack existente se a nova execução não resolver nada.
|
|
692
|
+
const resolvedStack = stackFrames.length > 0 &&
|
|
693
|
+
(stackResolvedCount > 0 || !event.resolvedStack?.length)
|
|
694
|
+
? stackFrames
|
|
695
|
+
: event.resolvedStack;
|
|
696
|
+
|
|
697
|
+
const resolvedComponentStack = componentFrames.length > 0 &&
|
|
698
|
+
(componentResolvedCount > 0 || !event.resolvedComponentStack?.length)
|
|
699
|
+
? componentFrames
|
|
700
|
+
: event.resolvedComponentStack;
|
|
701
|
+
|
|
571
702
|
return {
|
|
572
703
|
...event,
|
|
573
|
-
...(
|
|
574
|
-
? { resolvedStack
|
|
704
|
+
...(resolvedStack && resolvedStack.length > 0
|
|
705
|
+
? { resolvedStack }
|
|
706
|
+
: {}),
|
|
707
|
+
...(resolvedComponentStack && resolvedComponentStack.length > 0
|
|
708
|
+
? { resolvedComponentStack }
|
|
575
709
|
: {})
|
|
576
710
|
};
|
|
577
711
|
});
|
|
578
712
|
|
|
579
|
-
|
|
713
|
+
const totalResolved = exceptionResolvedFrames + reactResolvedFrames;
|
|
714
|
+
const totalUnresolved = exceptionUnresolvedFrames + reactUnresolvedFrames;
|
|
715
|
+
|
|
716
|
+
if (totalResolved === 0 && totalUnresolved === 0) {
|
|
580
717
|
console.log('[Backtrack] Nenhuma stack compatível foi encontrada.');
|
|
581
718
|
process.exitCode = 0;
|
|
582
719
|
return 0;
|
|
@@ -584,16 +721,21 @@ async function run(args = []) {
|
|
|
584
721
|
|
|
585
722
|
const uniqueWarnings = Array.from(new Set(allWarnings));
|
|
586
723
|
|
|
587
|
-
if (
|
|
724
|
+
if (totalUnresolved > 0) {
|
|
588
725
|
console.log('[Backtrack] Symbolication parcial.\n');
|
|
589
726
|
} else {
|
|
590
727
|
console.log('[Backtrack] Symbolication concluída.\n');
|
|
591
728
|
}
|
|
592
729
|
|
|
593
730
|
console.log(`Incidente: ${artifact.incident.id}`);
|
|
731
|
+
if (releaseStatusMsg) {
|
|
732
|
+
console.log(releaseStatusMsg);
|
|
733
|
+
}
|
|
594
734
|
console.log(`Erros encontrados: ${errorEventsCount}`);
|
|
595
|
-
console.log(`Frames resolvidos: ${
|
|
596
|
-
console.log(`Frames não resolvidos: ${
|
|
735
|
+
console.log(`Frames da exceção resolvidos: ${exceptionResolvedFrames}`);
|
|
736
|
+
console.log(`Frames da exceção não resolvidos: ${exceptionUnresolvedFrames}`);
|
|
737
|
+
console.log(`Frames React resolvidos: ${reactResolvedFrames}`);
|
|
738
|
+
console.log(`Frames React não resolvidos: ${reactUnresolvedFrames}`);
|
|
597
739
|
console.log(`Saída: ${resolvedOutput}`);
|
|
598
740
|
|
|
599
741
|
if (uniqueWarnings.length > 0) {
|
|
@@ -642,6 +784,7 @@ if (require.main === module) {
|
|
|
642
784
|
}
|
|
643
785
|
|
|
644
786
|
module.exports = {
|
|
787
|
+
parseStackFrame,
|
|
645
788
|
symbolicateStack,
|
|
646
789
|
FRAME_WITH_FUNCTION,
|
|
647
790
|
FRAME_WITHOUT_FUNCTION,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/dev-server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,yBAAyB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,qBAAqB,EAC3B,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
2
|
+
export interface BacktrackMiddlewareOptions {
|
|
3
|
+
/** Absolute directory; use only one middleware instance per directory. */
|
|
4
|
+
directory?: string;
|
|
5
|
+
/** Alias for directory. */
|
|
6
|
+
outputDir?: string;
|
|
7
|
+
allowedOrigins?: string[];
|
|
8
|
+
maxBodyBytes?: number;
|
|
9
|
+
maxIncidents?: number;
|
|
10
|
+
maxStorageBytes?: number;
|
|
11
|
+
}
|
|
12
|
+
export type ConnectLikeMiddleware = (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => void;
|
|
13
|
+
export declare function createBacktrackMiddleware(options: BacktrackMiddlewareOptions): ConnectLikeMiddleware;
|
|
14
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/dev-server/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,MAAM,CAAC;AAM5D,MAAM,WAAW,0BAA0B;IACzC,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2BAA2B;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AACD,MAAM,MAAM,qBAAqB,GAAG,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,OAAO,KAAK,IAAI,KAAK,IAAI,CAAC;AAmCxH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,0BAA0B,GAAG,qBAAqB,CAqKpG"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("fs"),y=require("path"),I=require("./markdown-Ao5Ah9MF.cjs"),U=/^[a-zA-Z0-9_-]{1,128}$/,W=/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i,_=t=>U.test(t)&&!W.test(t),A=t=>["localhost","127.0.0.1","[::1]"].includes(t.toLowerCase()),Q=t=>t==="127.0.0.1"||t==="::1"||t==="::ffff:127.0.0.1";function V(t,o){if(!Q(t.socket.remoteAddress))return!1;try{const S=t.headers.host;if(!S)return!1;const d=new URL(`http://${S}`);if(d.host!==S.toLowerCase()||!A(d.hostname))return!1;const f=t.headers.origin;if(!f)return!(o!=null&&o.length);const h=new URL(f);return!["http:","https:"].includes(h.protocol)||!A(h.hostname)||h.origin!==f?!1:o!=null&&o.length?o.includes(f):h.host===d.host}catch{return!1}}const Z=(t,o)=>t.triggeredAt-o.triggeredAt||(t.id<o.id?-1:t.id>o.id?1:0);function m(t){return e.existsSync(t)&&e.lstatSync(t).isFile()}function G(t){var L;const o=(t==null?void 0:t.directory)??(t==null?void 0:t.outputDir);if(!t||typeof o!="string"||!y.isAbsolute(o))throw new Error('[Backtrack dev-server] A opção "directory" (ou "outputDir") absoluta é obrigatória.');const S=y.resolve(o),d=y.join(S,"incidents"),f=y.join(S,"latest.md"),h=t.maxBodyBytes??50*1024*1024,F=t.maxIncidents??20,v=t.maxStorageBytes??200*1024*1024;for(const i of[h,F,v])if(!Number.isSafeInteger(i)||i<=0)throw new Error("Backtrack limits must be positive safe integers.");if((L=t.allowedOrigins)!=null&&L.some(i=>{try{const r=new URL(i);return!A(r.hostname)||!["http:","https:"].includes(r.protocol)||r.origin!==i}catch{return!0}}))throw new Error("Backtrack origins must be local HTTP(S) origins.");let P=Promise.resolve();function x(){const i=[];for(const r of e.readdirSync(d)){if(!r.endsWith(".json"))continue;const s=r.slice(0,-5);if(!_(s))continue;const n=y.join(d,r),c=y.join(d,`${s}.md`);try{if(!m(n)||!m(c)||e.statSync(n).size>h)continue;const a=I.validateFlightRecorderArtifact(JSON.parse(e.readFileSync(n,"utf8")));if(!a.success||a.data.incident.id!==s||!e.readFileSync(c,"utf8").startsWith(`# Incidente Backtrack
|
|
2
|
+
`))continue;i.push({id:s,jsonPath:n,mdPath:c,triggeredAt:a.data.incident.triggeredAt,size:e.statSync(n).size+e.statSync(c).size})}catch{}}return i.sort(Z)}function D(){const i=x(),r=i[i.length-1];if(!r)throw new Error("No complete incident to publish");const s=`> **Artefato completo:** [\`${r.id}.json\`](incidents/${r.id}.json)
|
|
3
|
+
|
|
4
|
+
`+e.readFileSync(r.mdPath,"utf8"),n=`${f}.${Date.now()}.tmp`;try{e.writeFileSync(n,s,{encoding:"utf8",flag:"wx"}),e.renameSync(n,f)}finally{m(n)&&e.unlinkSync(n)}return r.id}function E(i){const r=x();let s=r.length,n=r.reduce((c,a)=>c+a.size,e.statSync(f).size);for(const c of r){if(s<=F&&n<=v)break;c.id!==i&&(e.unlinkSync(c.mdPath),e.unlinkSync(c.jsonPath),s--,n-=c.size)}}return(i,r,s)=>{const n=(l,$)=>{!r.destroyed&&!r.headersSent&&(r.writeHead(l,{"Content-Type":"application/json",...l===405?{Allow:"POST"}:{}}),r.end(JSON.stringify($)))};if((i.url||"").split("?")[0]!=="/__backtrack/incident"){s?s():n(404,{error:"Not found"});return}if(i.method!=="POST"){n(405,{error:"Method Not Allowed"});return}if((i.headers["content-type"]||"").split(";")[0].trim().toLowerCase()!=="application/json"){n(415,{error:"Unsupported Media Type: application/json required"});return}if(!V(i,t.allowedOrigins)){n(403,{error:"Forbidden: invalid origin or non-local access"});return}let c=0,a=!1;const p=[];i.on("error",()=>{a=!0,p.length=0}),i.on("aborted",()=>{a=!0,p.length=0}),i.on("data",l=>{a||(c+=l.length,c>h?(a=!0,p.length=0,r.setHeader("Connection","close"),n(413,{error:"Payload Too Large"})):p.push(l))}),i.on("end",()=>{if(a)return;const l=Buffer.concat(p).toString("utf8");p.length=0;let $;try{$=JSON.parse(l)}catch{n(400,{error:"Invalid JSON"});return}const z=I.validateFlightRecorderArtifact($);if(!z.success){n(400,{error:"Invalid artifact schema"});return}const M=z.data,g=M.incident.id;if(!_(g)){n(400,{error:"Invalid incident ID format"});return}P=P.then(()=>{try{for(const u of[S,d]){if(e.existsSync(u)&&!e.lstatSync(u).isDirectory())throw new Error("Invalid output directory");e.mkdirSync(u,{recursive:!0})}const w=y.join(d,`${g}.json`),k=y.join(d,`${g}.md`),j=e.existsSync(w),O=e.existsSync(k);if(j&&(!m(w)||e.readFileSync(w,"utf8")!==l)||O&&(!m(k)||!j)){n(409,{error:"Conflict: incident ID exists with different content"});return}if(j&&O){const u=D();E(u),n(200,{id:g,status:"already_persisted"});return}const N=I.formatIncidentMarkdown(M),H=`> **Artefato completo:** [\`${g}.json\`](incidents/${g}.json)
|
|
5
|
+
|
|
6
|
+
`;if(Buffer.byteLength(l)+2*Buffer.byteLength(N)+Buffer.byteLength(H)>v){n(413,{error:"Incident size exceeds maxStorageBytes limit"});return}const R=`${Date.now()}-${Math.random().toString(36).slice(2)}`,B=`${w}.${R}.tmp`,b=`${k}.${R}.tmp`;let T=!1,C=!1;try{j||e.writeFileSync(B,l,{encoding:"utf8",flag:"wx"}),e.writeFileSync(b,N,{encoding:"utf8",flag:"wx"}),j||(e.renameSync(B,w),T=!0),e.renameSync(b,k),C=!0,D()}catch(u){throw C&&m(k)&&e.unlinkSync(k),T&&m(w)&&e.unlinkSync(w),u}finally{for(const u of[B,b])m(u)&&e.unlinkSync(u)}const J=x();E(J[J.length-1].id),n(201,{id:g,status:"saved"})}catch{n(500,{error:"Failed to persist incident"})}})})}}exports.createBacktrackMiddleware=G;
|
|
7
|
+
//# sourceMappingURL=dev-server.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-server.cjs","sources":["../src/dev-server/middleware.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'http';\nimport fs from 'fs';\nimport path from 'path';\nimport { validateFlightRecorderArtifact } from '../validation/validate';\nimport { formatIncidentMarkdown } from '../utils/markdown';\n\nexport interface BacktrackMiddlewareOptions {\n /** Absolute directory; use only one middleware instance per directory. */\n directory?: string;\n /** Alias for directory. */\n outputDir?: string;\n allowedOrigins?: string[];\n maxBodyBytes?: number;\n maxIncidents?: number;\n maxStorageBytes?: number;\n}\nexport type ConnectLikeMiddleware = (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => void;\nconst SAFE_ID = /^[a-zA-Z0-9_-]{1,128}$/;\nconst RESERVED_ID = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;\nconst safeId = (id: string) => SAFE_ID.test(id) && !RESERVED_ID.test(id);\nconst localHostname = (name: string) => ['localhost', '127.0.0.1', '[::1]'].includes(name.toLowerCase());\nconst localAddress = (address?: string) => address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1';\nfunction allowedRequest(req: IncomingMessage, origins?: string[]): boolean {\n if (!localAddress(req.socket.remoteAddress)) return false;\n try {\n const host = req.headers.host;\n if (!host) return false;\n const target = new URL(`http://${host}`);\n if (target.host !== host.toLowerCase() || !localHostname(target.hostname)) return false;\n const origin = req.headers.origin;\n if (!origin) return !origins?.length;\n const source = new URL(origin);\n if (!['http:', 'https:'].includes(source.protocol) || !localHostname(source.hostname) || source.origin !== origin) return false;\n return origins?.length ? origins.includes(origin) : source.host === target.host;\n } catch {\n return false;\n }\n}\ninterface ManagedIncident {\n id: string;\n triggeredAt: number;\n jsonPath: string;\n mdPath: string;\n size: number;\n}\nconst compareIncidents = (a: ManagedIncident, b: ManagedIncident) =>\n a.triggeredAt - b.triggeredAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);\nfunction regularFile(file: string): boolean {\n return fs.existsSync(file) && fs.lstatSync(file).isFile();\n}\n\nexport function createBacktrackMiddleware(options: BacktrackMiddlewareOptions): ConnectLikeMiddleware {\n const dir = options?.directory ?? options?.outputDir;\n if (!options || typeof dir !== 'string' || !path.isAbsolute(dir)) {\n throw new Error('[Backtrack dev-server] A opção \"directory\" (ou \"outputDir\") absoluta é obrigatória.');\n }\n const baseDirectory = path.resolve(dir);\n const incidentsDir = path.join(baseDirectory, 'incidents');\n const latestPath = path.join(baseDirectory, 'latest.md');\n const maxBodyBytes = options.maxBodyBytes ?? 50 * 1024 * 1024;\n const maxIncidents = options.maxIncidents ?? 20;\n const maxStorageBytes = options.maxStorageBytes ?? 200 * 1024 * 1024;\n for (const value of [maxBodyBytes, maxIncidents, maxStorageBytes]) {\n if (!Number.isSafeInteger(value) || value <= 0) throw new Error('Backtrack limits must be positive safe integers.');\n }\n if (options.allowedOrigins?.some(origin => {\n try {\n const url = new URL(origin);\n return !localHostname(url.hostname) || !['http:', 'https:'].includes(url.protocol) || url.origin !== origin;\n } catch { return true; }\n })) throw new Error('Backtrack origins must be local HTTP(S) origins.');\n\n // ponytail: synchronous local writes serialized per instance; use async IO if measured dev-server latency warrants it.\n let persistenceQueue = Promise.resolve();\n function scan(): ManagedIncident[] {\n const list: ManagedIncident[] = [];\n for (const file of fs.readdirSync(incidentsDir)) {\n if (!file.endsWith('.json')) continue;\n const id = file.slice(0, -5);\n if (!safeId(id)) continue;\n const jsonPath = path.join(incidentsDir, file);\n const mdPath = path.join(incidentsDir, `${id}.md`);\n try {\n if (!regularFile(jsonPath) || !regularFile(mdPath) || fs.statSync(jsonPath).size > maxBodyBytes) continue;\n const validated = validateFlightRecorderArtifact(JSON.parse(fs.readFileSync(jsonPath, 'utf8')));\n if (!validated.success || validated.data.incident.id !== id) continue;\n if (!fs.readFileSync(mdPath, 'utf8').startsWith('# Incidente Backtrack\\n')) continue;\n list.push({ id, jsonPath, mdPath, triggeredAt: validated.data.incident.triggeredAt, size: fs.statSync(jsonPath).size + fs.statSync(mdPath).size });\n } catch {\n // Unknown, incomplete or invalid files are never retention candidates.\n }\n }\n return list.sort(compareIncidents);\n }\n function publishLatest(): string {\n const list = scan();\n const latest = list[list.length - 1];\n if (!latest) throw new Error('No complete incident to publish');\n const content = `> **Artefato completo:** [\\`${latest.id}.json\\`](incidents/${latest.id}.json)\\n\\n` + fs.readFileSync(latest.mdPath, 'utf8');\n const temporary = `${latestPath}.${Date.now()}.tmp`;\n try {\n fs.writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx' });\n fs.renameSync(temporary, latestPath);\n } finally {\n if (regularFile(temporary)) fs.unlinkSync(temporary);\n }\n return latest.id;\n }\n function retain(latestId: string): void {\n const list = scan();\n let count = list.length;\n let size = list.reduce((total, item) => total + item.size, fs.statSync(latestPath).size);\n for (const item of list) {\n if (count <= maxIncidents && size <= maxStorageBytes) break;\n if (item.id === latestId) continue;\n fs.unlinkSync(item.mdPath);\n fs.unlinkSync(item.jsonPath);\n count--;\n size -= item.size;\n }\n }\n return (req, res, next) => {\n const respond = (status: number, body: Record<string, unknown>) => {\n if (!res.destroyed && !res.headersSent) {\n res.writeHead(status, { 'Content-Type': 'application/json', ...(status === 405 ? { Allow: 'POST' } : {}) });\n res.end(JSON.stringify(body));\n }\n };\n if ((req.url || '').split('?')[0] !== '/__backtrack/incident') {\n if (next) next(); else respond(404, { error: 'Not found' });\n return;\n }\n if (req.method !== 'POST') { respond(405, { error: 'Method Not Allowed' }); return; }\n if ((req.headers['content-type'] || '').split(';')[0].trim().toLowerCase() !== 'application/json') {\n respond(415, { error: 'Unsupported Media Type: application/json required' }); return;\n }\n if (!allowedRequest(req, options.allowedOrigins)) { respond(403, { error: 'Forbidden: invalid origin or non-local access' }); return; }\n let bytes = 0;\n let rejected = false;\n const chunks: Buffer[] = [];\n req.on('error', () => { rejected = true; chunks.length = 0; });\n req.on('aborted', () => { rejected = true; chunks.length = 0; });\n req.on('data', (chunk: Buffer) => {\n if (rejected) return;\n bytes += chunk.length;\n if (bytes > maxBodyBytes) {\n rejected = true;\n chunks.length = 0;\n res.setHeader('Connection', 'close');\n respond(413, { error: 'Payload Too Large' });\n // Drain without retaining data so the HTTP response can reach the client.\n } else chunks.push(chunk);\n });\n req.on('end', () => {\n if (rejected) return;\n const rawBody = Buffer.concat(chunks).toString('utf8');\n chunks.length = 0;\n let parsed: unknown;\n try { parsed = JSON.parse(rawBody); }\n catch { respond(400, { error: 'Invalid JSON' }); return; }\n const validated = validateFlightRecorderArtifact(parsed);\n if (!validated.success) { respond(400, { error: 'Invalid artifact schema' }); return; }\n const artifact = validated.data;\n const id = artifact.incident.id;\n if (!safeId(id)) { respond(400, { error: 'Invalid incident ID format' }); return; }\n persistenceQueue = persistenceQueue.then(() => {\n try {\n for (const dir of [baseDirectory, incidentsDir]) {\n if (fs.existsSync(dir) && !fs.lstatSync(dir).isDirectory()) throw new Error('Invalid output directory');\n fs.mkdirSync(dir, { recursive: true });\n }\n const jsonPath = path.join(incidentsDir, `${id}.json`);\n const mdPath = path.join(incidentsDir, `${id}.md`);\n const jsonExists = fs.existsSync(jsonPath);\n const mdExists = fs.existsSync(mdPath);\n if ((jsonExists && (!regularFile(jsonPath) || fs.readFileSync(jsonPath, 'utf8') !== rawBody)) ||\n (mdExists && (!regularFile(mdPath) || !jsonExists))) {\n respond(409, { error: 'Conflict: incident ID exists with different content' }); return;\n }\n if (jsonExists && mdExists) {\n const latestId = publishLatest();\n retain(latestId);\n respond(200, { id, status: 'already_persisted' }); return;\n }\n const markdown = formatIncidentMarkdown(artifact);\n const header = `> **Artefato completo:** [\\`${id}.json\\`](incidents/${id}.json)\\n\\n`;\n if (Buffer.byteLength(rawBody) + 2 * Buffer.byteLength(markdown) + Buffer.byteLength(header) > maxStorageBytes) {\n respond(413, { error: 'Incident size exceeds maxStorageBytes limit' }); return;\n }\n const nonce = `${Date.now()}-${Math.random().toString(36).slice(2)}`;\n const tmpJson = `${jsonPath}.${nonce}.tmp`;\n const tmpMd = `${mdPath}.${nonce}.tmp`;\n let createdJson = false;\n let createdMd = false;\n try {\n if (!jsonExists) fs.writeFileSync(tmpJson, rawBody, { encoding: 'utf8', flag: 'wx' });\n fs.writeFileSync(tmpMd, markdown, { encoding: 'utf8', flag: 'wx' });\n if (!jsonExists) { fs.renameSync(tmpJson, jsonPath); createdJson = true; }\n fs.renameSync(tmpMd, mdPath); createdMd = true;\n publishLatest();\n } catch (error) {\n if (createdMd && regularFile(mdPath)) fs.unlinkSync(mdPath);\n if (createdJson && regularFile(jsonPath)) fs.unlinkSync(jsonPath);\n throw error;\n } finally {\n for (const file of [tmpJson, tmpMd]) if (regularFile(file)) fs.unlinkSync(file);\n }\n const list = scan();\n retain(list[list.length - 1].id);\n respond(201, { id, status: 'saved' });\n } catch {\n respond(500, { error: 'Failed to persist incident' });\n }\n });\n });\n };\n}\r\n"],"names":["SAFE_ID","RESERVED_ID","safeId","id","localHostname","name","localAddress","address","allowedRequest","req","origins","host","target","origin","source","compareIncidents","a","b","regularFile","file","fs","createBacktrackMiddleware","options","dir","path","baseDirectory","incidentsDir","latestPath","maxBodyBytes","maxIncidents","maxStorageBytes","value","_a","url","persistenceQueue","scan","list","jsonPath","mdPath","validated","validateFlightRecorderArtifact","publishLatest","latest","content","temporary","retain","latestId","count","size","total","item","res","next","respond","status","body","bytes","rejected","chunks","chunk","rawBody","parsed","artifact","jsonExists","mdExists","markdown","formatIncidentMarkdown","header","nonce","tmpJson","tmpMd","createdJson","createdMd","error"],"mappings":"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"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import e from "fs";
|
|
2
|
+
import y from "path";
|
|
3
|
+
import { v as J, f as U } from "./markdown-D3C94OlY.js";
|
|
4
|
+
const W = /^[a-zA-Z0-9_-]{1,128}$/, Q = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i, _ = (t) => W.test(t) && !Q.test(t), b = (t) => ["localhost", "127.0.0.1", "[::1]"].includes(t.toLowerCase()), V = (t) => t === "127.0.0.1" || t === "::1" || t === "::ffff:127.0.0.1";
|
|
5
|
+
function Z(t, i) {
|
|
6
|
+
if (!V(t.socket.remoteAddress)) return !1;
|
|
7
|
+
try {
|
|
8
|
+
const S = t.headers.host;
|
|
9
|
+
if (!S) return !1;
|
|
10
|
+
const d = new URL(`http://${S}`);
|
|
11
|
+
if (d.host !== S.toLowerCase() || !b(d.hostname)) return !1;
|
|
12
|
+
const u = t.headers.origin;
|
|
13
|
+
if (!u) return !(i != null && i.length);
|
|
14
|
+
const h = new URL(u);
|
|
15
|
+
return !["http:", "https:"].includes(h.protocol) || !b(h.hostname) || h.origin !== u ? !1 : i != null && i.length ? i.includes(u) : h.host === d.host;
|
|
16
|
+
} catch {
|
|
17
|
+
return !1;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const G = (t, i) => t.triggeredAt - i.triggeredAt || (t.id < i.id ? -1 : t.id > i.id ? 1 : 0);
|
|
21
|
+
function m(t) {
|
|
22
|
+
return e.existsSync(t) && e.lstatSync(t).isFile();
|
|
23
|
+
}
|
|
24
|
+
function q(t) {
|
|
25
|
+
var E;
|
|
26
|
+
const i = (t == null ? void 0 : t.directory) ?? (t == null ? void 0 : t.outputDir);
|
|
27
|
+
if (!t || typeof i != "string" || !y.isAbsolute(i))
|
|
28
|
+
throw new Error('[Backtrack dev-server] A opção "directory" (ou "outputDir") absoluta é obrigatória.');
|
|
29
|
+
const S = y.resolve(i), d = y.join(S, "incidents"), u = y.join(S, "latest.md"), h = t.maxBodyBytes ?? 50 * 1024 * 1024, A = t.maxIncidents ?? 20, $ = t.maxStorageBytes ?? 200 * 1024 * 1024;
|
|
30
|
+
for (const o of [h, A, $])
|
|
31
|
+
if (!Number.isSafeInteger(o) || o <= 0) throw new Error("Backtrack limits must be positive safe integers.");
|
|
32
|
+
if ((E = t.allowedOrigins) != null && E.some((o) => {
|
|
33
|
+
try {
|
|
34
|
+
const r = new URL(o);
|
|
35
|
+
return !b(r.hostname) || !["http:", "https:"].includes(r.protocol) || r.origin !== o;
|
|
36
|
+
} catch {
|
|
37
|
+
return !0;
|
|
38
|
+
}
|
|
39
|
+
})) throw new Error("Backtrack origins must be local HTTP(S) origins.");
|
|
40
|
+
let F = Promise.resolve();
|
|
41
|
+
function v() {
|
|
42
|
+
const o = [];
|
|
43
|
+
for (const r of e.readdirSync(d)) {
|
|
44
|
+
if (!r.endsWith(".json")) continue;
|
|
45
|
+
const c = r.slice(0, -5);
|
|
46
|
+
if (!_(c)) continue;
|
|
47
|
+
const n = y.join(d, r), s = y.join(d, `${c}.md`);
|
|
48
|
+
try {
|
|
49
|
+
if (!m(n) || !m(s) || e.statSync(n).size > h) continue;
|
|
50
|
+
const a = J(JSON.parse(e.readFileSync(n, "utf8")));
|
|
51
|
+
if (!a.success || a.data.incident.id !== c || !e.readFileSync(s, "utf8").startsWith(`# Incidente Backtrack
|
|
52
|
+
`)) continue;
|
|
53
|
+
o.push({ id: c, jsonPath: n, mdPath: s, triggeredAt: a.data.incident.triggeredAt, size: e.statSync(n).size + e.statSync(s).size });
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return o.sort(G);
|
|
58
|
+
}
|
|
59
|
+
function P() {
|
|
60
|
+
const o = v(), r = o[o.length - 1];
|
|
61
|
+
if (!r) throw new Error("No complete incident to publish");
|
|
62
|
+
const c = `> **Artefato completo:** [\`${r.id}.json\`](incidents/${r.id}.json)
|
|
63
|
+
|
|
64
|
+
` + e.readFileSync(r.mdPath, "utf8"), n = `${u}.${Date.now()}.tmp`;
|
|
65
|
+
try {
|
|
66
|
+
e.writeFileSync(n, c, { encoding: "utf8", flag: "wx" }), e.renameSync(n, u);
|
|
67
|
+
} finally {
|
|
68
|
+
m(n) && e.unlinkSync(n);
|
|
69
|
+
}
|
|
70
|
+
return r.id;
|
|
71
|
+
}
|
|
72
|
+
function D(o) {
|
|
73
|
+
const r = v();
|
|
74
|
+
let c = r.length, n = r.reduce((s, a) => s + a.size, e.statSync(u).size);
|
|
75
|
+
for (const s of r) {
|
|
76
|
+
if (c <= A && n <= $) break;
|
|
77
|
+
s.id !== o && (e.unlinkSync(s.mdPath), e.unlinkSync(s.jsonPath), c--, n -= s.size);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return (o, r, c) => {
|
|
81
|
+
const n = (l, x) => {
|
|
82
|
+
!r.destroyed && !r.headersSent && (r.writeHead(l, { "Content-Type": "application/json", ...l === 405 ? { Allow: "POST" } : {} }), r.end(JSON.stringify(x)));
|
|
83
|
+
};
|
|
84
|
+
if ((o.url || "").split("?")[0] !== "/__backtrack/incident") {
|
|
85
|
+
c ? c() : n(404, { error: "Not found" });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (o.method !== "POST") {
|
|
89
|
+
n(405, { error: "Method Not Allowed" });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if ((o.headers["content-type"] || "").split(";")[0].trim().toLowerCase() !== "application/json") {
|
|
93
|
+
n(415, { error: "Unsupported Media Type: application/json required" });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!Z(o, t.allowedOrigins)) {
|
|
97
|
+
n(403, { error: "Forbidden: invalid origin or non-local access" });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
let s = 0, a = !1;
|
|
101
|
+
const w = [];
|
|
102
|
+
o.on("error", () => {
|
|
103
|
+
a = !0, w.length = 0;
|
|
104
|
+
}), o.on("aborted", () => {
|
|
105
|
+
a = !0, w.length = 0;
|
|
106
|
+
}), o.on("data", (l) => {
|
|
107
|
+
a || (s += l.length, s > h ? (a = !0, w.length = 0, r.setHeader("Connection", "close"), n(413, { error: "Payload Too Large" })) : w.push(l));
|
|
108
|
+
}), o.on("end", () => {
|
|
109
|
+
if (a) return;
|
|
110
|
+
const l = Buffer.concat(w).toString("utf8");
|
|
111
|
+
w.length = 0;
|
|
112
|
+
let x;
|
|
113
|
+
try {
|
|
114
|
+
x = JSON.parse(l);
|
|
115
|
+
} catch {
|
|
116
|
+
n(400, { error: "Invalid JSON" });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const L = J(x);
|
|
120
|
+
if (!L.success) {
|
|
121
|
+
n(400, { error: "Invalid artifact schema" });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const z = L.data, g = z.incident.id;
|
|
125
|
+
if (!_(g)) {
|
|
126
|
+
n(400, { error: "Invalid incident ID format" });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
F = F.then(() => {
|
|
130
|
+
try {
|
|
131
|
+
for (const f of [S, d]) {
|
|
132
|
+
if (e.existsSync(f) && !e.lstatSync(f).isDirectory()) throw new Error("Invalid output directory");
|
|
133
|
+
e.mkdirSync(f, { recursive: !0 });
|
|
134
|
+
}
|
|
135
|
+
const p = y.join(d, `${g}.json`), k = y.join(d, `${g}.md`), j = e.existsSync(p), N = e.existsSync(k);
|
|
136
|
+
if (j && (!m(p) || e.readFileSync(p, "utf8") !== l) || N && (!m(k) || !j)) {
|
|
137
|
+
n(409, { error: "Conflict: incident ID exists with different content" });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (j && N) {
|
|
141
|
+
const f = P();
|
|
142
|
+
D(f), n(200, { id: g, status: "already_persisted" });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const O = U(z), H = `> **Artefato completo:** [\`${g}.json\`](incidents/${g}.json)
|
|
146
|
+
|
|
147
|
+
`;
|
|
148
|
+
if (Buffer.byteLength(l) + 2 * Buffer.byteLength(O) + Buffer.byteLength(H) > $) {
|
|
149
|
+
n(413, { error: "Incident size exceeds maxStorageBytes limit" });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const M = `${Date.now()}-${Math.random().toString(36).slice(2)}`, B = `${p}.${M}.tmp`, I = `${k}.${M}.tmp`;
|
|
153
|
+
let R = !1, T = !1;
|
|
154
|
+
try {
|
|
155
|
+
j || e.writeFileSync(B, l, { encoding: "utf8", flag: "wx" }), e.writeFileSync(I, O, { encoding: "utf8", flag: "wx" }), j || (e.renameSync(B, p), R = !0), e.renameSync(I, k), T = !0, P();
|
|
156
|
+
} catch (f) {
|
|
157
|
+
throw T && m(k) && e.unlinkSync(k), R && m(p) && e.unlinkSync(p), f;
|
|
158
|
+
} finally {
|
|
159
|
+
for (const f of [B, I]) m(f) && e.unlinkSync(f);
|
|
160
|
+
}
|
|
161
|
+
const C = v();
|
|
162
|
+
D(C[C.length - 1].id), n(201, { id: g, status: "saved" });
|
|
163
|
+
} catch {
|
|
164
|
+
n(500, { error: "Failed to persist incident" });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
export {
|
|
171
|
+
q as createBacktrackMiddleware
|
|
172
|
+
};
|
|
173
|
+
//# sourceMappingURL=dev-server.js.map
|