norn-cli 1.3.16 → 1.3.18
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/AGENTS.md +72 -0
- package/CHANGELOG.md +34 -1
- package/README.md +4 -2
- package/dist/cli.js +135 -63
- package/out/assertionRunner.js +537 -0
- package/out/cli/colors.js +129 -0
- package/out/cli/formatters/assertion.js +75 -0
- package/out/cli/formatters/index.js +23 -0
- package/out/cli/formatters/response.js +106 -0
- package/out/cli/formatters/summary.js +187 -0
- package/out/cli/redaction.js +237 -0
- package/out/cli/reporters/html.js +634 -0
- package/out/cli/reporters/index.js +22 -0
- package/out/cli/reporters/junit.js +211 -0
- package/out/cli.js +926 -0
- package/out/codeLensProvider.js +254 -0
- package/out/compareContentProvider.js +85 -0
- package/out/completionProvider.js +1886 -0
- package/out/contractDecorationProvider.js +243 -0
- package/out/coverageCalculator.js +756 -0
- package/out/coveragePanel.js +542 -0
- package/out/diagnosticProvider.js +980 -0
- package/out/environmentProvider.js +373 -0
- package/out/extension.js +1025 -0
- package/out/httpClient.js +269 -0
- package/out/jsonFileReader.js +320 -0
- package/out/nornapiParser.js +326 -0
- package/out/parser.js +725 -0
- package/out/responsePanel.js +4674 -0
- package/out/schemaGenerator.js +393 -0
- package/out/scriptRunner.js +419 -0
- package/out/sequenceRunner.js +3046 -0
- package/out/swaggerParser.js +339 -0
- package/out/test/extension.test.js +48 -0
- package/out/testProvider.js +658 -0
- package/out/validationCache.js +245 -0
- package/package.json +1 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Assertion formatter for CLI output
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.formatAssertion = formatAssertion;
|
|
7
|
+
exports.formatAssertionSummary = formatAssertionSummary;
|
|
8
|
+
/**
|
|
9
|
+
* Format a single assertion result
|
|
10
|
+
*/
|
|
11
|
+
function formatAssertion(assertion, options) {
|
|
12
|
+
const { colors, verbose } = options;
|
|
13
|
+
const lines = [];
|
|
14
|
+
const icon = assertion.passed ? colors.checkmark : colors.cross;
|
|
15
|
+
// Build display expression with friendly name when available
|
|
16
|
+
let displayExpr = assertion.expression;
|
|
17
|
+
if (assertion.friendlyName && assertion.responseIndex) {
|
|
18
|
+
// Replace $N with the friendly variable name
|
|
19
|
+
displayExpr = displayExpr.replace('$' + assertion.responseIndex, assertion.friendlyName);
|
|
20
|
+
}
|
|
21
|
+
const displayMessage = assertion.message || displayExpr;
|
|
22
|
+
lines.push(`${icon} assert ${displayMessage}`);
|
|
23
|
+
// Show details for failures, or for all assertions in verbose mode
|
|
24
|
+
if (!assertion.passed || verbose) {
|
|
25
|
+
if (assertion.error) {
|
|
26
|
+
lines.push(` ${colors.error(`Error: ${assertion.error}`)}`);
|
|
27
|
+
}
|
|
28
|
+
else if (!assertion.passed) {
|
|
29
|
+
// Show expected vs actual for failures
|
|
30
|
+
lines.push(` ${colors.dim('Expected:')} ${formatValue(assertion.rightExpression ?? assertion.operator)}`);
|
|
31
|
+
lines.push(` ${colors.dim('Actual:')} ${formatValue(assertion.leftValue)}`);
|
|
32
|
+
// If expressions differ from values, show original expressions
|
|
33
|
+
if (verbose && assertion.leftExpression !== String(assertion.leftValue)) {
|
|
34
|
+
lines.push(` ${colors.dim('Expression:')} ${assertion.leftExpression}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (verbose) {
|
|
38
|
+
// In verbose mode, show values for passing assertions too
|
|
39
|
+
lines.push(` ${colors.dim('Value:')} ${formatValue(assertion.leftValue)}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return lines;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Format a value for display (handles objects, arrays, primitives)
|
|
46
|
+
*/
|
|
47
|
+
function formatValue(value) {
|
|
48
|
+
if (value === undefined) {
|
|
49
|
+
return 'undefined';
|
|
50
|
+
}
|
|
51
|
+
if (value === null) {
|
|
52
|
+
return 'null';
|
|
53
|
+
}
|
|
54
|
+
if (typeof value === 'object') {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.stringify(value);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return String(value);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return String(value);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Format assertion summary (passed/failed counts)
|
|
66
|
+
*/
|
|
67
|
+
function formatAssertionSummary(results, colors) {
|
|
68
|
+
const passed = results.filter(a => a.passed).length;
|
|
69
|
+
const failed = results.length - passed;
|
|
70
|
+
if (failed > 0) {
|
|
71
|
+
return `${colors.error(`${passed}/${results.length} assertions passed`)} (${failed} failed)`;
|
|
72
|
+
}
|
|
73
|
+
return `${colors.success(`${passed}/${results.length} assertions passed`)}`;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=assertion.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CLI formatters index - re-exports all formatter functions
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
17
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
__exportStar(require("./response"), exports);
|
|
21
|
+
__exportStar(require("./assertion"), exports);
|
|
22
|
+
__exportStar(require("./summary"), exports);
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* HTTP Response formatter for CLI output
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.formatResponse = formatResponse;
|
|
7
|
+
exports.formatResponseCompact = formatResponseCompact;
|
|
8
|
+
const colors_1 = require("../colors");
|
|
9
|
+
const redaction_1 = require("../redaction");
|
|
10
|
+
/**
|
|
11
|
+
* Format a single HTTP response for CLI output
|
|
12
|
+
*/
|
|
13
|
+
function formatResponse(response, options) {
|
|
14
|
+
const { colors, redaction, verbose, showDetails, index, method, url } = options;
|
|
15
|
+
const lines = [];
|
|
16
|
+
const prefix = index !== undefined ? `[${index}] ` : '';
|
|
17
|
+
const statusColor = (0, colors_1.getStatusColor)(colors, response.status);
|
|
18
|
+
const isSuccess = response.status >= 200 && response.status < 300;
|
|
19
|
+
const icon = isSuccess ? colors.checkmark : colors.cross;
|
|
20
|
+
// Status line with optional method/url
|
|
21
|
+
if (method && url) {
|
|
22
|
+
const displayUrl = (0, redaction_1.redactUrl)(url, redaction);
|
|
23
|
+
const retryInfo = response.retryInfo && response.retryInfo.attemptsMade > 1
|
|
24
|
+
? ` ${colors.warning(`[retried ${response.retryInfo.attemptsMade - 1}x]`)}`
|
|
25
|
+
: '';
|
|
26
|
+
lines.push(`${prefix}${icon} ${colors.method(method)} ${displayUrl} ` +
|
|
27
|
+
`${statusColor(`(${response.status} ${response.statusText})`)} ` +
|
|
28
|
+
`${colors.duration((0, colors_1.formatDuration)(response.duration))}${retryInfo}`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const retryInfo = response.retryInfo && response.retryInfo.attemptsMade > 1
|
|
32
|
+
? ` ${colors.warning(`[retried ${response.retryInfo.attemptsMade - 1}x]`)}`
|
|
33
|
+
: '';
|
|
34
|
+
lines.push(`${prefix}${statusColor(`${response.status} ${response.statusText}`)} ` +
|
|
35
|
+
`${colors.duration(`(${(0, colors_1.formatDuration)(response.duration)})`)}${retryInfo}`);
|
|
36
|
+
}
|
|
37
|
+
// Only show details in verbose mode OR when there's an error/showDetails is true
|
|
38
|
+
if (verbose || showDetails) {
|
|
39
|
+
lines.push((0, colors_1.line)());
|
|
40
|
+
// Request details (if available and showing details)
|
|
41
|
+
if (showDetails && (options.requestHeaders || options.requestBody)) {
|
|
42
|
+
lines.push(colors.bold('Request:'));
|
|
43
|
+
if (options.requestHeaders) {
|
|
44
|
+
const redactedHeaders = (0, redaction_1.redactHeaders)(options.requestHeaders, redaction);
|
|
45
|
+
for (const [key, value] of Object.entries(redactedHeaders)) {
|
|
46
|
+
lines.push(` ${colors.header(key)}: ${colors.headerValue(value)}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (options.requestBody) {
|
|
50
|
+
lines.push('');
|
|
51
|
+
lines.push(colors.dim(' Body:'));
|
|
52
|
+
const redactedBody = (0, redaction_1.redactBody)(options.requestBody, redaction);
|
|
53
|
+
const bodyStr = typeof redactedBody === 'object'
|
|
54
|
+
? JSON.stringify(redactedBody, null, 2)
|
|
55
|
+
: String(redactedBody);
|
|
56
|
+
for (const bodyLine of bodyStr.split('\n')) {
|
|
57
|
+
lines.push(` ${colors.dim(bodyLine)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
lines.push('');
|
|
61
|
+
lines.push(colors.bold('Response:'));
|
|
62
|
+
}
|
|
63
|
+
// Response headers (verbose or on error)
|
|
64
|
+
if (verbose && response.headers) {
|
|
65
|
+
lines.push(colors.dim('Headers:'));
|
|
66
|
+
const redactedHeaders = (0, redaction_1.redactHeaders)(response.headers, redaction);
|
|
67
|
+
for (const [key, value] of Object.entries(redactedHeaders)) {
|
|
68
|
+
lines.push(` ${colors.header(key)}: ${colors.headerValue(value)}`);
|
|
69
|
+
}
|
|
70
|
+
lines.push((0, colors_1.line)());
|
|
71
|
+
}
|
|
72
|
+
// Response body
|
|
73
|
+
if (response.body !== undefined && response.body !== null) {
|
|
74
|
+
const redactedBody = (0, redaction_1.redactBody)(response.body, redaction);
|
|
75
|
+
if (typeof redactedBody === 'object') {
|
|
76
|
+
lines.push(JSON.stringify(redactedBody, null, 2));
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
lines.push(String(redactedBody));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return lines;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Format a compact one-line response (for quiet mode on success)
|
|
87
|
+
*/
|
|
88
|
+
function formatResponseCompact(response, options) {
|
|
89
|
+
const { colors, redaction, method, url, index } = options;
|
|
90
|
+
const prefix = index !== undefined ? `[${index}] ` : '';
|
|
91
|
+
const statusColor = (0, colors_1.getStatusColor)(colors, response.status);
|
|
92
|
+
const isSuccess = response.status >= 200 && response.status < 300;
|
|
93
|
+
const icon = isSuccess ? colors.checkmark : colors.cross;
|
|
94
|
+
const retryInfo = response.retryInfo && response.retryInfo.attemptsMade > 1
|
|
95
|
+
? ` ${colors.warning(`[retried ${response.retryInfo.attemptsMade - 1}x]`)}`
|
|
96
|
+
: '';
|
|
97
|
+
if (method && url) {
|
|
98
|
+
const displayUrl = (0, redaction_1.redactUrl)(url, redaction);
|
|
99
|
+
return (`${prefix}${icon} ${colors.method(method)} ${displayUrl} ` +
|
|
100
|
+
`${statusColor(`${response.status}`)} ` +
|
|
101
|
+
`${colors.duration((0, colors_1.formatDuration)(response.duration))}${retryInfo}`);
|
|
102
|
+
}
|
|
103
|
+
return (`${prefix}${icon} ${statusColor(`${response.status} ${response.statusText}`)} ` +
|
|
104
|
+
`${colors.duration((0, colors_1.formatDuration)(response.duration))}${retryInfo}`);
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=response.js.map
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Summary formatter for CLI output - formats sequence results and overall run summaries
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.formatSequenceResult = formatSequenceResult;
|
|
7
|
+
exports.formatRunSummary = formatRunSummary;
|
|
8
|
+
exports.formatProgressLine = formatProgressLine;
|
|
9
|
+
const colors_1 = require("../colors");
|
|
10
|
+
const response_1 = require("./response");
|
|
11
|
+
const assertion_1 = require("./assertion");
|
|
12
|
+
/**
|
|
13
|
+
* Format a single sequence result
|
|
14
|
+
*/
|
|
15
|
+
function formatSequenceResult(result, options) {
|
|
16
|
+
const { colors, redaction, verbose } = options;
|
|
17
|
+
const lines = [];
|
|
18
|
+
// Sequence header
|
|
19
|
+
const statusIcon = result.success ? colors.checkmark : colors.cross;
|
|
20
|
+
const statusColor = result.success ? colors.success : colors.error;
|
|
21
|
+
lines.push('');
|
|
22
|
+
lines.push(`${statusIcon} ${statusColor(`Sequence: ${result.name}`)}`);
|
|
23
|
+
lines.push(`${colors.dim('Duration:')} ${(0, colors_1.formatDuration)(result.duration)}`);
|
|
24
|
+
// Assertion summary if any
|
|
25
|
+
if (result.assertionResults && result.assertionResults.length > 0) {
|
|
26
|
+
lines.push(`${colors.dim('Assertions:')} ${(0, assertion_1.formatAssertionSummary)(result.assertionResults, colors)}`);
|
|
27
|
+
}
|
|
28
|
+
// Request count
|
|
29
|
+
const requestCount = result.steps.filter(s => s.type === 'request').length;
|
|
30
|
+
lines.push(`${colors.dim('Requests:')} ${requestCount}`);
|
|
31
|
+
lines.push((0, colors_1.doubleLine)());
|
|
32
|
+
// Track if we've shown any expanded details
|
|
33
|
+
let requestIdx = 0;
|
|
34
|
+
// Print steps in order
|
|
35
|
+
for (const step of result.steps) {
|
|
36
|
+
const stepLines = formatStep(step, {
|
|
37
|
+
colors,
|
|
38
|
+
redaction,
|
|
39
|
+
verbose,
|
|
40
|
+
requestIndex: step.type === 'request' ? ++requestIdx : undefined,
|
|
41
|
+
});
|
|
42
|
+
lines.push(...stepLines);
|
|
43
|
+
}
|
|
44
|
+
// Show accumulated errors/warnings at the end
|
|
45
|
+
if (result.errors.length > 0) {
|
|
46
|
+
lines.push('');
|
|
47
|
+
lines.push(colors.warning('Warnings/Errors:'));
|
|
48
|
+
for (const err of result.errors) {
|
|
49
|
+
lines.push(` ${colors.bullet} ${err}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return lines;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Format a single step result
|
|
56
|
+
*/
|
|
57
|
+
function formatStep(step, options) {
|
|
58
|
+
const { colors, redaction, verbose, requestIndex } = options;
|
|
59
|
+
switch (step.type) {
|
|
60
|
+
case 'print':
|
|
61
|
+
return formatPrintStep(step, colors);
|
|
62
|
+
case 'request':
|
|
63
|
+
return formatRequestStep(step, { colors, redaction, verbose, requestIndex });
|
|
64
|
+
case 'assertion':
|
|
65
|
+
return formatAssertionStep(step, { colors, verbose });
|
|
66
|
+
case 'script':
|
|
67
|
+
return formatScriptStep(step, colors, verbose);
|
|
68
|
+
case 'json':
|
|
69
|
+
return formatJsonStep(step, colors, verbose);
|
|
70
|
+
default:
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function formatPrintStep(step, colors) {
|
|
75
|
+
const lines = [];
|
|
76
|
+
if (step.print) {
|
|
77
|
+
lines.push(`${colors.warning('[print]')} ${step.print.title}`);
|
|
78
|
+
if (step.print.body) {
|
|
79
|
+
lines.push(` ${step.print.body}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return lines;
|
|
83
|
+
}
|
|
84
|
+
function formatRequestStep(step, options) {
|
|
85
|
+
const { colors, redaction, verbose, requestIndex } = options;
|
|
86
|
+
if (!step.response) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
const isSuccess = step.response.status >= 200 && step.response.status < 300;
|
|
90
|
+
// In quiet mode (not verbose), only show compact line for successful requests
|
|
91
|
+
// Show full details for failures
|
|
92
|
+
if (!verbose && isSuccess) {
|
|
93
|
+
return [(0, response_1.formatResponseCompact)(step.response, {
|
|
94
|
+
colors,
|
|
95
|
+
redaction,
|
|
96
|
+
method: step.requestMethod,
|
|
97
|
+
url: step.requestUrl,
|
|
98
|
+
index: requestIndex,
|
|
99
|
+
})];
|
|
100
|
+
}
|
|
101
|
+
// Show full details for failures or in verbose mode
|
|
102
|
+
return (0, response_1.formatResponse)(step.response, {
|
|
103
|
+
colors,
|
|
104
|
+
redaction,
|
|
105
|
+
verbose,
|
|
106
|
+
showDetails: !isSuccess || verbose, // Show request details on failure or verbose
|
|
107
|
+
index: requestIndex,
|
|
108
|
+
method: step.requestMethod,
|
|
109
|
+
url: step.requestUrl,
|
|
110
|
+
requestBody: step.response.requestBody,
|
|
111
|
+
requestHeaders: step.response.requestHeaders,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function formatAssertionStep(step, options) {
|
|
115
|
+
if (!step.assertion) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
return (0, assertion_1.formatAssertion)(step.assertion, {
|
|
119
|
+
colors: options.colors,
|
|
120
|
+
verbose: options.verbose,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function formatScriptStep(step, colors, verbose) {
|
|
124
|
+
const lines = [];
|
|
125
|
+
if (step.script) {
|
|
126
|
+
const icon = step.script.success ? colors.checkmark : colors.cross;
|
|
127
|
+
lines.push(`${icon} ${colors.dim('[script]')} ${step.script.type}`);
|
|
128
|
+
if (!step.script.success || verbose) {
|
|
129
|
+
if (step.script.error) {
|
|
130
|
+
lines.push(` ${colors.error(`Error: ${step.script.error}`)}`);
|
|
131
|
+
}
|
|
132
|
+
if (verbose && step.script.output) {
|
|
133
|
+
lines.push(` ${colors.dim('output:')} ${step.script.output}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return lines;
|
|
138
|
+
}
|
|
139
|
+
function formatJsonStep(step, colors, verbose) {
|
|
140
|
+
const lines = [];
|
|
141
|
+
if (step.json) {
|
|
142
|
+
const icon = step.json.success ? colors.checkmark : colors.cross;
|
|
143
|
+
lines.push(`${icon} ${colors.dim('[json]')} Loaded: ${step.json.varName}`);
|
|
144
|
+
if (!step.json.success) {
|
|
145
|
+
lines.push(` ${colors.error(`Error: ${step.json.error}`)}`);
|
|
146
|
+
}
|
|
147
|
+
else if (verbose) {
|
|
148
|
+
lines.push(` ${colors.dim('File:')} ${step.json.filePath}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return lines;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Format the overall run summary (for multiple sequences)
|
|
155
|
+
*/
|
|
156
|
+
function formatRunSummary(results, totalDuration, colors) {
|
|
157
|
+
const lines = [];
|
|
158
|
+
const passed = results.filter(r => r.success).length;
|
|
159
|
+
const failed = results.length - passed;
|
|
160
|
+
lines.push('');
|
|
161
|
+
lines.push((0, colors_1.doubleLine)());
|
|
162
|
+
lines.push(colors.bold('Summary'));
|
|
163
|
+
lines.push((0, colors_1.line)());
|
|
164
|
+
lines.push(`${colors.dim('Total sequences:')} ${results.length}`);
|
|
165
|
+
lines.push(`${colors.success(`Passed: ${passed}`)}`);
|
|
166
|
+
if (failed > 0) {
|
|
167
|
+
lines.push(`${colors.error(`Failed: ${failed}`)}`);
|
|
168
|
+
// List failed sequences
|
|
169
|
+
lines.push('');
|
|
170
|
+
lines.push(colors.error('Failed sequences:'));
|
|
171
|
+
for (const result of results.filter(r => !r.success)) {
|
|
172
|
+
lines.push(` ${colors.cross} ${result.name}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
lines.push('');
|
|
176
|
+
lines.push(`${colors.dim('Total duration:')} ${(0, colors_1.formatDuration)(totalDuration)}`);
|
|
177
|
+
return lines;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Format a single-line progress indicator (for streaming output)
|
|
181
|
+
*/
|
|
182
|
+
function formatProgressLine(sequenceName, currentStep, totalSteps, stepDescription, colors) {
|
|
183
|
+
return (`${colors.info(`[${sequenceName}]`)} ` +
|
|
184
|
+
`${colors.dim(`Step ${currentStep}/${totalSteps}:`)} ` +
|
|
185
|
+
`${stepDescription}`);
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=summary.js.map
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Redaction module for sensitive data in CLI output
|
|
4
|
+
*
|
|
5
|
+
* Automatically redacts:
|
|
6
|
+
* - Authorization headers (Bearer tokens, Basic auth, API keys)
|
|
7
|
+
* - Common sensitive field names (password, secret, token, api_key, etc.)
|
|
8
|
+
* - User-defined secrets from .nornenv `secret` declarations
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.createRedactionOptions = createRedactionOptions;
|
|
12
|
+
exports.redactHeaders = redactHeaders;
|
|
13
|
+
exports.redactString = redactString;
|
|
14
|
+
exports.redactBody = redactBody;
|
|
15
|
+
exports.redactUrl = redactUrl;
|
|
16
|
+
exports.getRedactedMarker = getRedactedMarker;
|
|
17
|
+
const REDACTED = '***REDACTED***';
|
|
18
|
+
/**
|
|
19
|
+
* Common header names that should always be redacted
|
|
20
|
+
*/
|
|
21
|
+
const SENSITIVE_HEADERS = new Set([
|
|
22
|
+
'authorization',
|
|
23
|
+
'x-api-key',
|
|
24
|
+
'x-auth-token',
|
|
25
|
+
'x-access-token',
|
|
26
|
+
'api-key',
|
|
27
|
+
'apikey',
|
|
28
|
+
'cookie',
|
|
29
|
+
'set-cookie',
|
|
30
|
+
'x-csrf-token',
|
|
31
|
+
'x-xsrf-token',
|
|
32
|
+
]);
|
|
33
|
+
/**
|
|
34
|
+
* Patterns for sensitive values in headers/bodies
|
|
35
|
+
* These patterns match common token/key formats
|
|
36
|
+
*/
|
|
37
|
+
const SENSITIVE_PATTERNS = [
|
|
38
|
+
// Bearer tokens
|
|
39
|
+
/Bearer\s+[A-Za-z0-9\-_=]+\.?[A-Za-z0-9\-_=]*\.?[A-Za-z0-9\-_=]*/gi,
|
|
40
|
+
// Basic auth
|
|
41
|
+
/Basic\s+[A-Za-z0-9+/=]+/gi,
|
|
42
|
+
// API keys (common formats)
|
|
43
|
+
/api[_-]?key[=:]\s*["']?[A-Za-z0-9\-_]+["']?/gi,
|
|
44
|
+
];
|
|
45
|
+
/**
|
|
46
|
+
* Field names in JSON that should have their values redacted
|
|
47
|
+
*/
|
|
48
|
+
const SENSITIVE_FIELD_NAMES = new Set([
|
|
49
|
+
'password',
|
|
50
|
+
'passwd',
|
|
51
|
+
'secret',
|
|
52
|
+
'token',
|
|
53
|
+
'access_token',
|
|
54
|
+
'accesstoken',
|
|
55
|
+
'refresh_token',
|
|
56
|
+
'refreshtoken',
|
|
57
|
+
'api_key',
|
|
58
|
+
'apikey',
|
|
59
|
+
'api-key',
|
|
60
|
+
'private_key',
|
|
61
|
+
'privatekey',
|
|
62
|
+
'client_secret',
|
|
63
|
+
'clientsecret',
|
|
64
|
+
'auth',
|
|
65
|
+
'authorization',
|
|
66
|
+
'credential',
|
|
67
|
+
'credentials',
|
|
68
|
+
]);
|
|
69
|
+
/**
|
|
70
|
+
* Create default redaction options
|
|
71
|
+
*/
|
|
72
|
+
function createRedactionOptions(secretNames = new Set(), secretValues = new Map(), enabled = true) {
|
|
73
|
+
return { secretNames, secretValues, enabled };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Redact sensitive headers
|
|
77
|
+
*/
|
|
78
|
+
function redactHeaders(headers, options) {
|
|
79
|
+
if (!options.enabled) {
|
|
80
|
+
return headers;
|
|
81
|
+
}
|
|
82
|
+
const redacted = {};
|
|
83
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
84
|
+
const lowerKey = key.toLowerCase();
|
|
85
|
+
// Check if header name is sensitive
|
|
86
|
+
if (SENSITIVE_HEADERS.has(lowerKey)) {
|
|
87
|
+
redacted[key] = REDACTED;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
// Check for Bearer/Basic patterns in value
|
|
91
|
+
let redactedValue = value;
|
|
92
|
+
for (const pattern of SENSITIVE_PATTERNS) {
|
|
93
|
+
redactedValue = redactedValue.replace(pattern, REDACTED);
|
|
94
|
+
}
|
|
95
|
+
// Check if value matches any secret values
|
|
96
|
+
redactedValue = redactSecretValues(redactedValue, options);
|
|
97
|
+
redacted[key] = redactedValue;
|
|
98
|
+
}
|
|
99
|
+
return redacted;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Redact sensitive values in a string
|
|
103
|
+
*/
|
|
104
|
+
function redactString(text, options) {
|
|
105
|
+
if (!options.enabled || !text) {
|
|
106
|
+
return text;
|
|
107
|
+
}
|
|
108
|
+
let result = text;
|
|
109
|
+
// Apply pattern-based redaction
|
|
110
|
+
for (const pattern of SENSITIVE_PATTERNS) {
|
|
111
|
+
result = result.replace(pattern, REDACTED);
|
|
112
|
+
}
|
|
113
|
+
// Redact user-defined secret values
|
|
114
|
+
result = redactSecretValues(result, options);
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Redact secret values from .nornenv
|
|
119
|
+
*/
|
|
120
|
+
function redactSecretValues(text, options) {
|
|
121
|
+
if (!text || options.secretValues.size === 0) {
|
|
122
|
+
return text;
|
|
123
|
+
}
|
|
124
|
+
let result = text;
|
|
125
|
+
// Replace each secret value with redacted marker
|
|
126
|
+
for (const [, value] of options.secretValues) {
|
|
127
|
+
if (value && value.length > 0) {
|
|
128
|
+
// Escape special regex characters in the value
|
|
129
|
+
const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
130
|
+
result = result.replace(new RegExp(escaped, 'g'), REDACTED);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Redact sensitive fields in JSON body
|
|
137
|
+
*/
|
|
138
|
+
function redactBody(body, options) {
|
|
139
|
+
if (!options.enabled) {
|
|
140
|
+
return body;
|
|
141
|
+
}
|
|
142
|
+
if (typeof body === 'string') {
|
|
143
|
+
// Try to parse as JSON first
|
|
144
|
+
try {
|
|
145
|
+
const parsed = JSON.parse(body);
|
|
146
|
+
return JSON.stringify(redactObject(parsed, options), null, 2);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// Not JSON, apply string redaction
|
|
150
|
+
return redactString(body, options);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (typeof body === 'object' && body !== null) {
|
|
154
|
+
return redactObject(body, options);
|
|
155
|
+
}
|
|
156
|
+
return body;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Recursively redact sensitive fields in an object
|
|
160
|
+
*/
|
|
161
|
+
function redactObject(obj, options) {
|
|
162
|
+
if (Array.isArray(obj)) {
|
|
163
|
+
return obj.map(item => redactObject(item, options));
|
|
164
|
+
}
|
|
165
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
166
|
+
if (typeof obj === 'string') {
|
|
167
|
+
return redactSecretValues(obj, options);
|
|
168
|
+
}
|
|
169
|
+
return obj;
|
|
170
|
+
}
|
|
171
|
+
const result = {};
|
|
172
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
173
|
+
const lowerKey = key.toLowerCase();
|
|
174
|
+
// Check if field name indicates sensitive data
|
|
175
|
+
if (SENSITIVE_FIELD_NAMES.has(lowerKey)) {
|
|
176
|
+
result[key] = REDACTED;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
// Check if this is a user-defined secret variable name
|
|
180
|
+
if (options.secretNames.has(key)) {
|
|
181
|
+
result[key] = REDACTED;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
// Recursively process nested objects
|
|
185
|
+
if (typeof value === 'object' && value !== null) {
|
|
186
|
+
result[key] = redactObject(value, options);
|
|
187
|
+
}
|
|
188
|
+
else if (typeof value === 'string') {
|
|
189
|
+
result[key] = redactSecretValues(value, options);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
result[key] = value;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Redact a URL (query parameters with sensitive names)
|
|
199
|
+
*/
|
|
200
|
+
function redactUrl(url, options) {
|
|
201
|
+
if (!options.enabled) {
|
|
202
|
+
return url;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
const urlObj = new URL(url);
|
|
206
|
+
const params = new URLSearchParams(urlObj.search);
|
|
207
|
+
let modified = false;
|
|
208
|
+
for (const [key] of params.entries()) {
|
|
209
|
+
const lowerKey = key.toLowerCase();
|
|
210
|
+
if (SENSITIVE_FIELD_NAMES.has(lowerKey) ||
|
|
211
|
+
lowerKey.includes('token') ||
|
|
212
|
+
lowerKey.includes('key') ||
|
|
213
|
+
lowerKey.includes('secret') ||
|
|
214
|
+
lowerKey.includes('password')) {
|
|
215
|
+
params.set(key, REDACTED);
|
|
216
|
+
modified = true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (modified) {
|
|
220
|
+
urlObj.search = params.toString();
|
|
221
|
+
return urlObj.toString();
|
|
222
|
+
}
|
|
223
|
+
// Also redact secret values that appear in the URL
|
|
224
|
+
return redactSecretValues(url, options);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// If URL parsing fails, just do string-based redaction
|
|
228
|
+
return redactSecretValues(url, options);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get the redaction marker (for display purposes)
|
|
233
|
+
*/
|
|
234
|
+
function getRedactedMarker() {
|
|
235
|
+
return REDACTED;
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=redaction.js.map
|