@burdenoff/bigconsole-cli 2026.1029.1 → 2026.1110.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 +96 -212
- package/dist/auth/store.d.ts +0 -4
- package/dist/cli.js +2959 -103
- package/dist/commands/base.d.ts +0 -4
- package/dist/graphql/operations.d.ts +1 -1
- package/dist/index.js +2895 -6
- package/dist/utils/parse.d.ts +5 -0
- package/package.json +42 -41
- package/dist/auth/device-code.js +0 -244
- package/dist/auth/store.js +0 -90
- package/dist/commands/auth.js +0 -353
- package/dist/commands/base.js +0 -120
- package/dist/commands/cli-config.js +0 -79
- package/dist/commands/dashboard.js +0 -223
- package/dist/commands/datasink.js +0 -175
- package/dist/commands/drilldown.js +0 -55
- package/dist/commands/index.js +0 -13
- package/dist/commands/parser.js +0 -181
- package/dist/commands/pipeline.js +0 -142
- package/dist/commands/prompt.js +0 -84
- package/dist/commands/share.js +0 -50
- package/dist/commands/template.js +0 -62
- package/dist/commands/widget.js +0 -164
- package/dist/commands/workflow.js +0 -54
- package/dist/commands/workspace.js +0 -95
- package/dist/config/index.js +0 -151
- package/dist/graphql/client.js +0 -254
- package/dist/graphql/operations.js +0 -680
- package/dist/types/index.d.ts +0 -30
- package/dist/types/index.js +0 -7
- package/dist/utils/index.d.ts +0 -2
- package/dist/utils/index.js +0 -2
- package/dist/utils/output.js +0 -237
- package/dist/utils/sanitize.js +0 -347
package/dist/utils/output.js
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Output utilities for CLI display.
|
|
3
|
-
* Provides consistent formatting for both interactive and non-interactive modes.
|
|
4
|
-
*
|
|
5
|
-
* stdout carries only command data (JSON, YAML, tables); status lines
|
|
6
|
-
* (success, info, warning, error) go to stderr so piping stays clean. Remote
|
|
7
|
-
* strings are made inert before they reach the terminal: tables and status
|
|
8
|
-
* lines pass through `sanitizeForTerminal`, while JSON and YAML stay exact
|
|
9
|
-
* data and only use escape forms their own syntax defines.
|
|
10
|
-
*/
|
|
11
|
-
import { DEFAULT_SCALAR_STYLE_RULES, JSON_SCHEMA, SCALAR_STYLE, dump, strTag, } from 'js-yaml';
|
|
12
|
-
import { escapeBidiControls, redactSecrets, sanitizeForTerminal, toUnicodeEscape, } from './sanitize.js';
|
|
13
|
-
/** Widest a table column is padded to; longer cells extend past it. */
|
|
14
|
-
const MAX_COLUMN_WIDTH = 50;
|
|
15
|
-
/**
|
|
16
|
-
* Check if the current terminal session is interactive (TTY).
|
|
17
|
-
*/
|
|
18
|
-
export function isInteractive() {
|
|
19
|
-
return Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Determine the output format from command options, defaulting based on TTY.
|
|
23
|
-
*/
|
|
24
|
-
export function getFormat(options) {
|
|
25
|
-
if (options.format) {
|
|
26
|
-
const f = options.format.toLowerCase();
|
|
27
|
-
if (f === 'json' || f === 'table' || f === 'yaml')
|
|
28
|
-
return f;
|
|
29
|
-
if (f === 'yml')
|
|
30
|
-
return 'yaml';
|
|
31
|
-
}
|
|
32
|
-
return isInteractive() ? 'table' : 'json';
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Characters that are valid raw inside JSON strings but act on, or are
|
|
36
|
-
* invisible in, a terminal: DEL, C1 controls, line/paragraph separators and
|
|
37
|
-
* bidirectional formatting controls.
|
|
38
|
-
*/
|
|
39
|
-
const JSON_DISPLAY_ESCAPES = /[\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g;
|
|
40
|
-
/**
|
|
41
|
-
* Serialize data as pretty-printed JSON. Characters in
|
|
42
|
-
* {@link JSON_DISPLAY_ESCAPES} can only occur inside JSON strings, so writing
|
|
43
|
-
* them as `\uXXXX` escapes leaves the parsed value unchanged.
|
|
44
|
-
*/
|
|
45
|
-
export function toJson(data) {
|
|
46
|
-
const json = JSON.stringify(data, null, 2) ?? 'null';
|
|
47
|
-
return json.replace(JSON_DISPLAY_ESCAPES, toUnicodeEscape);
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Print data as formatted JSON.
|
|
51
|
-
*/
|
|
52
|
-
export function printJson(data) {
|
|
53
|
-
console.log(toJson(data));
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Print data as a simple ASCII table. Headers and cells are sanitized for the
|
|
57
|
-
* terminal (single line, no control sequences) before widths are measured;
|
|
58
|
-
* columns are padded to at most {@link MAX_COLUMN_WIDTH} characters.
|
|
59
|
-
*/
|
|
60
|
-
export function printTable(headers, rows) {
|
|
61
|
-
if (headers.length === 0)
|
|
62
|
-
return;
|
|
63
|
-
const header = headers.map((h) => sanitizeForTerminal(h));
|
|
64
|
-
const body = rows.map((row) => headers.map((_, i) => sanitizeForTerminal(row[i])));
|
|
65
|
-
const colWidths = header.map((h, i) => Math.min(body.reduce((max, row) => Math.max(max, (row[i] ?? '').length), h.length), MAX_COLUMN_WIDTH));
|
|
66
|
-
const separator = colWidths.map((w) => '-'.repeat(w + 2)).join('+');
|
|
67
|
-
const formatRow = (cells) => cells.map((cell, i) => ` ${cell.padEnd(colWidths[i] ?? 0)} `).join('|');
|
|
68
|
-
console.log(formatRow(header));
|
|
69
|
-
console.log(separator);
|
|
70
|
-
for (const row of body) {
|
|
71
|
-
console.log(formatRow(row));
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
function allowsStyle(mask, style) {
|
|
75
|
-
return (mask & (1 << style)) !== 0;
|
|
76
|
-
}
|
|
77
|
-
/** Bidirectional formatting controls (see `escapeBidiControls`). */
|
|
78
|
-
const BIDI_CONTROL = /[\u202a-\u202e\u2066-\u2069]/;
|
|
79
|
-
/**
|
|
80
|
-
* Strings that read back as the same string under the YAML 1.2 JSON and core
|
|
81
|
-
* schemas and under YAML 1.1: a letter first, then only letters, digits,
|
|
82
|
-
* spaces and `_ . / @ + -`, and not one of the words those schemas map to
|
|
83
|
-
* booleans or null.
|
|
84
|
-
*/
|
|
85
|
-
const UNAMBIGUOUS_PLAIN = /^\p{L}[\p{L}\p{N} _./@+-]*$/u;
|
|
86
|
-
const NON_STRING_WORDS = /^(?:y|n|yes|no|on|off|true|false|null)$/i;
|
|
87
|
-
/**
|
|
88
|
-
* Double-quote any scalar containing a bidirectional formatting control.
|
|
89
|
-
* Double-quoted YAML supports `\uXXXX` escapes, so `toYaml` can then write
|
|
90
|
-
* those characters visibly without changing the parsed value.
|
|
91
|
-
*/
|
|
92
|
-
const doubleQuoteBidiControls = (layout) => {
|
|
93
|
-
if (BIDI_CONTROL.test(layout.node.value))
|
|
94
|
-
layout.style = SCALAR_STYLE.DOUBLE_QUOTED;
|
|
95
|
-
};
|
|
96
|
-
/**
|
|
97
|
-
* Quote string scalars (keys and values) that another YAML parser could read
|
|
98
|
-
* as a number, boolean, null, date or merge key. The JSON schema alone leaves
|
|
99
|
-
* values such as `yes`, `~`, `0x1F`, `2026-01-01` or the empty string plain.
|
|
100
|
-
* Runs after the default rules, so strings with invisible characters are
|
|
101
|
-
* already double-quoted and multi-line strings already use block scalars.
|
|
102
|
-
*/
|
|
103
|
-
const quoteAmbiguousStrings = (layout) => {
|
|
104
|
-
if (layout.style !== SCALAR_STYLE.PLAIN || layout.node.tag !== strTag.tagName)
|
|
105
|
-
return;
|
|
106
|
-
const value = layout.node.value;
|
|
107
|
-
if (UNAMBIGUOUS_PLAIN.test(value) && !NON_STRING_WORDS.test(value))
|
|
108
|
-
return;
|
|
109
|
-
layout.style = allowsStyle(layout.allowedStylesMask, SCALAR_STYLE.SINGLE_QUOTED)
|
|
110
|
-
? SCALAR_STYLE.SINGLE_QUOTED
|
|
111
|
-
: SCALAR_STYLE.DOUBLE_QUOTED;
|
|
112
|
-
};
|
|
113
|
-
const YAML_STYLE_RULES = [
|
|
114
|
-
doubleQuoteBidiControls,
|
|
115
|
-
...Object.values(DEFAULT_SCALAR_STYLE_RULES),
|
|
116
|
-
quoteAmbiguousStrings,
|
|
117
|
-
];
|
|
118
|
-
/**
|
|
119
|
-
* Serialize data as YAML with js-yaml, restricted to the JSON schema (no
|
|
120
|
-
* custom tags) and without anchors/aliases. Control characters are escaped by
|
|
121
|
-
* the dumper; bidirectional controls, which YAML treats as printable, are
|
|
122
|
-
* written as `\uXXXX` inside double-quoted scalars.
|
|
123
|
-
*/
|
|
124
|
-
export function toYaml(data) {
|
|
125
|
-
const yaml = dump(data ?? null, {
|
|
126
|
-
schema: JSON_SCHEMA,
|
|
127
|
-
noRefs: true,
|
|
128
|
-
skipInvalid: true,
|
|
129
|
-
lineWidth: -1,
|
|
130
|
-
scalarStyleRules: YAML_STYLE_RULES,
|
|
131
|
-
});
|
|
132
|
-
return escapeBidiControls(yaml).trimEnd();
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Print data as YAML.
|
|
136
|
-
*/
|
|
137
|
-
export function printYaml(data) {
|
|
138
|
-
console.log(toYaml(data));
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Print a success message (stderr).
|
|
142
|
-
*/
|
|
143
|
-
export function printSuccess(message) {
|
|
144
|
-
console.error(`\u2713 ${sanitizeForTerminal(message)}`);
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Print an error message to stderr. Credential-shaped values are masked.
|
|
148
|
-
*/
|
|
149
|
-
export function printError(message) {
|
|
150
|
-
console.error(`\u2717 ${redactSecrets(sanitizeForTerminal(message))}`);
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Print a warning message to stderr. Credential-shaped values are masked.
|
|
154
|
-
*/
|
|
155
|
-
export function printWarning(message) {
|
|
156
|
-
console.warn(`! ${redactSecrets(sanitizeForTerminal(message))}`);
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Print an info message (stderr).
|
|
160
|
-
*/
|
|
161
|
-
export function printInfo(message) {
|
|
162
|
-
console.error(`\u2139 ${sanitizeForTerminal(message)}`);
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Format an ISO date string for display.
|
|
166
|
-
*/
|
|
167
|
-
export function formatDate(date) {
|
|
168
|
-
if (!date)
|
|
169
|
-
return '-';
|
|
170
|
-
try {
|
|
171
|
-
return new Date(date).toLocaleString();
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
return date;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Format a status string with emoji indicators.
|
|
179
|
-
*/
|
|
180
|
-
export function formatStatus(status) {
|
|
181
|
-
if (!status)
|
|
182
|
-
return '-';
|
|
183
|
-
const upper = status.toUpperCase();
|
|
184
|
-
switch (upper) {
|
|
185
|
-
case 'ACTIVE':
|
|
186
|
-
case 'RUNNING':
|
|
187
|
-
case 'HEALTHY':
|
|
188
|
-
return `\u2713 ${status}`;
|
|
189
|
-
case 'INACTIVE':
|
|
190
|
-
case 'STOPPED':
|
|
191
|
-
case 'PENDING':
|
|
192
|
-
return `\u25CB ${status}`;
|
|
193
|
-
case 'ERROR':
|
|
194
|
-
return `\u2717 ${status}`;
|
|
195
|
-
case 'ARCHIVED':
|
|
196
|
-
return `\u2192 ${status}`;
|
|
197
|
-
case 'STARTING':
|
|
198
|
-
return `\u25B6 ${status}`;
|
|
199
|
-
case 'TERMINATED':
|
|
200
|
-
return `\u25A0 ${status}`;
|
|
201
|
-
default:
|
|
202
|
-
return status;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Output data in the specified format.
|
|
207
|
-
*/
|
|
208
|
-
export function outputData(format, data, tableConfig) {
|
|
209
|
-
switch (format) {
|
|
210
|
-
case 'json':
|
|
211
|
-
printJson(data);
|
|
212
|
-
break;
|
|
213
|
-
case 'yaml':
|
|
214
|
-
printYaml(data);
|
|
215
|
-
break;
|
|
216
|
-
case 'table':
|
|
217
|
-
if (tableConfig) {
|
|
218
|
-
printTable(tableConfig.headers, tableConfig.rows);
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
printJson(data);
|
|
222
|
-
}
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Sanitize a string for the terminal, then truncate it to a maximum length,
|
|
228
|
-
* appending an ellipsis if needed.
|
|
229
|
-
*/
|
|
230
|
-
export function truncate(str, maxLen) {
|
|
231
|
-
if (!str)
|
|
232
|
-
return '-';
|
|
233
|
-
const clean = sanitizeForTerminal(str);
|
|
234
|
-
if (clean.length <= maxLen)
|
|
235
|
-
return clean;
|
|
236
|
-
return clean.substring(0, maxLen - 3) + '...';
|
|
237
|
-
}
|
package/dist/utils/sanitize.js
DELETED
|
@@ -1,347 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Text hygiene for everything the CLI prints.
|
|
3
|
-
*
|
|
4
|
-
* Remote data (resource names, server error messages, HTTP response bodies)
|
|
5
|
-
* ends up in terminals, CI logs and support captures. The helpers here:
|
|
6
|
-
* - keep terminal output inert: escape sequences and control characters are
|
|
7
|
-
* removed, and bidirectional formatting characters are made visible;
|
|
8
|
-
* - mask credential-shaped values (bearer tokens, JWTs, URL credentials and
|
|
9
|
-
* values stored under secret-like keys);
|
|
10
|
-
* - turn any thrown value into a short, single-line message that is built
|
|
11
|
-
* from an allowlist of fields and never includes request variables,
|
|
12
|
-
* request headers or raw response bodies.
|
|
13
|
-
*
|
|
14
|
-
* The module is side-effect free and imports nothing from the rest of the CLI,
|
|
15
|
-
* so every layer can use it (and unit-test module mocks never replace it).
|
|
16
|
-
*/
|
|
17
|
-
/** Longest error message shown to the user. */
|
|
18
|
-
export const MAX_ERROR_MESSAGE_LENGTH = 500;
|
|
19
|
-
/** Longest excerpt of an HTTP response body or server message kept in an error. */
|
|
20
|
-
export const MAX_HTTP_BODY_EXCERPT_LENGTH = 200;
|
|
21
|
-
/** Text is cut to this length before the redaction patterns scan it. */
|
|
22
|
-
const MAX_SCAN_LENGTH = 4000;
|
|
23
|
-
const REDACTED = '[REDACTED]';
|
|
24
|
-
const ELLIPSIS = '\u2026';
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
// Terminal output
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
const ESC = 0x1b;
|
|
29
|
-
const BEL = 0x07;
|
|
30
|
-
const TAB = 0x09;
|
|
31
|
-
const LF = 0x0a;
|
|
32
|
-
const CR = 0x0d;
|
|
33
|
-
const DEL = 0x7f;
|
|
34
|
-
const NEL = 0x85;
|
|
35
|
-
const C1_CSI = 0x9b;
|
|
36
|
-
const C1_ST = 0x9c;
|
|
37
|
-
const LINE_SEPARATOR = 0x2028;
|
|
38
|
-
const PARAGRAPH_SEPARATOR = 0x2029;
|
|
39
|
-
/** 8-bit introducers of DCS, SOS, OSC, PM and APC control strings. */
|
|
40
|
-
const C1_STRING_INTRODUCERS = new Set([0x90, 0x98, 0x9d, 0x9e, 0x9f]);
|
|
41
|
-
/** Final bytes after ESC that open DCS, SOS, OSC, PM and APC control strings. */
|
|
42
|
-
const ESC_STRING_INTRODUCERS = new Set(['P', 'X', ']', '^', '_'].map((c) => c.charCodeAt(0)));
|
|
43
|
-
/** Bidirectional embedding, override and isolate controls. */
|
|
44
|
-
const BIDI_CONTROLS = /[\u202a-\u202e\u2066-\u2069]/g;
|
|
45
|
-
/** `\uXXXX` spelling of a single UTF-16 code unit (valid in JSON and YAML). */
|
|
46
|
-
export function toUnicodeEscape(char) {
|
|
47
|
-
return `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`;
|
|
48
|
-
}
|
|
49
|
-
/** Make bidirectional formatting controls visible as `\uXXXX` text. */
|
|
50
|
-
export function escapeBidiControls(text) {
|
|
51
|
-
return text.replace(BIDI_CONTROLS, toUnicodeEscape);
|
|
52
|
-
}
|
|
53
|
-
function isInRange(code, low, high) {
|
|
54
|
-
return code >= low && code <= high;
|
|
55
|
-
}
|
|
56
|
-
/** Index just past a CSI sequence whose parameters start at `index`. */
|
|
57
|
-
function skipControlSequence(text, index) {
|
|
58
|
-
let i = index;
|
|
59
|
-
while (i < text.length && isInRange(text.charCodeAt(i), 0x30, 0x3f))
|
|
60
|
-
i++;
|
|
61
|
-
while (i < text.length && isInRange(text.charCodeAt(i), 0x20, 0x2f))
|
|
62
|
-
i++;
|
|
63
|
-
if (i < text.length && isInRange(text.charCodeAt(i), 0x40, 0x7e))
|
|
64
|
-
i++;
|
|
65
|
-
return i;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Index just past a control string (OSC, DCS, ...) whose payload starts at
|
|
69
|
-
* `index`. Terminated by BEL, 8-bit ST or ESC \; an unterminated string runs
|
|
70
|
-
* to the end of the text, exactly as a terminal would consume it.
|
|
71
|
-
*/
|
|
72
|
-
function skipControlString(text, index) {
|
|
73
|
-
let i = index;
|
|
74
|
-
while (i < text.length) {
|
|
75
|
-
const code = text.charCodeAt(i);
|
|
76
|
-
if (code === BEL || code === C1_ST)
|
|
77
|
-
return i + 1;
|
|
78
|
-
if (code === ESC)
|
|
79
|
-
return text.charCodeAt(i + 1) === 0x5c ? i + 2 : i;
|
|
80
|
-
i++;
|
|
81
|
-
}
|
|
82
|
-
return i;
|
|
83
|
-
}
|
|
84
|
-
/** Index just past an ESC-introduced sequence; `index` points at the ESC. */
|
|
85
|
-
function skipEscapeSequence(text, index) {
|
|
86
|
-
const next = text.charCodeAt(index + 1);
|
|
87
|
-
if (Number.isNaN(next))
|
|
88
|
-
return index + 1;
|
|
89
|
-
if (next === 0x5b)
|
|
90
|
-
return skipControlSequence(text, index + 2);
|
|
91
|
-
if (ESC_STRING_INTRODUCERS.has(next))
|
|
92
|
-
return skipControlString(text, index + 2);
|
|
93
|
-
let i = index + 1;
|
|
94
|
-
while (i < text.length && isInRange(text.charCodeAt(i), 0x20, 0x2f))
|
|
95
|
-
i++;
|
|
96
|
-
if (i < text.length && isInRange(text.charCodeAt(i), 0x30, 0x7e))
|
|
97
|
-
i++;
|
|
98
|
-
return i;
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Make a value safe to print on a terminal.
|
|
102
|
-
*
|
|
103
|
-
* - ANSI/VT escape sequences (CSI, OSC, DCS, SOS, PM, APC and two-character
|
|
104
|
-
* ESC sequences, in 7-bit and 8-bit form) are removed entirely.
|
|
105
|
-
* - Other C0 controls, DEL and C1 controls are removed.
|
|
106
|
-
* - Line breaks and TAB become a space (see {@link TerminalSanitizeOptions}).
|
|
107
|
-
* - Bidirectional formatting controls are shown as `\uXXXX` text so the
|
|
108
|
-
* printed text reads in its stored order.
|
|
109
|
-
*
|
|
110
|
-
* `null`/`undefined` become an empty string. The function is idempotent.
|
|
111
|
-
*/
|
|
112
|
-
export function sanitizeForTerminal(value, options = {}) {
|
|
113
|
-
const text = value === null || value === undefined ? '' : String(value);
|
|
114
|
-
const multiline = options.preserveNewlines === true;
|
|
115
|
-
let out = '';
|
|
116
|
-
let i = 0;
|
|
117
|
-
while (i < text.length) {
|
|
118
|
-
const code = text.charCodeAt(i);
|
|
119
|
-
if (code === ESC) {
|
|
120
|
-
i = skipEscapeSequence(text, i);
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if (code === C1_CSI) {
|
|
124
|
-
i = skipControlSequence(text, i + 1);
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
if (C1_STRING_INTRODUCERS.has(code)) {
|
|
128
|
-
i = skipControlString(text, i + 1);
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
if (code === CR || code === LF) {
|
|
132
|
-
const crlf = code === CR && text.charCodeAt(i + 1) === LF;
|
|
133
|
-
out += multiline ? '\n' : ' ';
|
|
134
|
-
i += crlf ? 2 : 1;
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (code === NEL || code === LINE_SEPARATOR || code === PARAGRAPH_SEPARATOR) {
|
|
138
|
-
out += multiline ? toUnicodeEscape(text.charAt(i)) : ' ';
|
|
139
|
-
i++;
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
if (code === TAB) {
|
|
143
|
-
out += multiline ? '\t' : ' ';
|
|
144
|
-
i++;
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
if (code < 0x20 || isInRange(code, DEL, 0x9f)) {
|
|
148
|
-
i++;
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
out += text.charAt(i);
|
|
152
|
-
i++;
|
|
153
|
-
}
|
|
154
|
-
return escapeBidiControls(out);
|
|
155
|
-
}
|
|
156
|
-
// ---------------------------------------------------------------------------
|
|
157
|
-
// Credential masking
|
|
158
|
-
// ---------------------------------------------------------------------------
|
|
159
|
-
/** A key whose value is treated as a credential when it contains one of these. */
|
|
160
|
-
const SECRET_KEY_HINT = /pass(?:word|wd|phrase)?|secret|token|api[-_]?key|private[-_]?key|access[-_]?key|credential|authori[sz]ation|cookie|session[-_]?id/i;
|
|
161
|
-
/**
|
|
162
|
-
* `key: value` / `key=value` / `"key": value` openings. Keys may be quoted,
|
|
163
|
-
* including with backslash-escaped quotes (JSON serialised inside a string).
|
|
164
|
-
*/
|
|
165
|
-
const KEY_VALUE_OPENING = /\b([A-Za-z_$][\w$-]{0,127})\\*["']?\s*[:=]\s*/g;
|
|
166
|
-
/** Opening quote of a value, optionally backslash-escaped. */
|
|
167
|
-
const QUOTE_OPENING = /^(\\*)(["'])/;
|
|
168
|
-
/** Unquoted value: runs to whitespace, a quote, a backslash or a delimiter. */
|
|
169
|
-
const UNQUOTED_VALUE = /^[^\s"'\\,;&)}\]]*/;
|
|
170
|
-
/** An auth scheme word that precedes the credential in header values. */
|
|
171
|
-
const AUTH_SCHEME = /^(?:Bearer|Basic|Digest|Token)\s+/i;
|
|
172
|
-
const BEARER_CREDENTIAL = /\bBearer\s+[A-Za-z0-9\-._~+/]+=*/g;
|
|
173
|
-
const JWT_LIKE = /\beyJ[\w-]{4,}\.[\w-]{4,}\.[\w-]*/g;
|
|
174
|
-
const URL_USERINFO = /\b([a-z][a-z0-9+.-]{1,31}:\/\/)[^\s/?#@"'\\]+@/gi;
|
|
175
|
-
/** Number of consecutive backslashes immediately before `index`, not before `floor`. */
|
|
176
|
-
function backslashRun(text, index, floor) {
|
|
177
|
-
let run = 0;
|
|
178
|
-
while (index - run - 1 >= floor && text.charAt(index - run - 1) === '\\')
|
|
179
|
-
run++;
|
|
180
|
-
return run;
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Masked replacement for the value starting at `start`, and the index just
|
|
184
|
-
* past that value. A quoted value ends at the matching quote (same escaping
|
|
185
|
-
* depth) or, when unterminated, at the end of the text.
|
|
186
|
-
*/
|
|
187
|
-
function maskValue(text, start) {
|
|
188
|
-
if (text.startsWith(REDACTED, start)) {
|
|
189
|
-
return { replacement: REDACTED, end: start + REDACTED.length };
|
|
190
|
-
}
|
|
191
|
-
const quote = QUOTE_OPENING.exec(text.slice(start, start + 64));
|
|
192
|
-
if (quote) {
|
|
193
|
-
const opener = quote[0];
|
|
194
|
-
const depth = (quote[1] ?? '').length;
|
|
195
|
-
const quoteChar = quote[2] ?? '"';
|
|
196
|
-
let search = start + opener.length;
|
|
197
|
-
while (search < text.length) {
|
|
198
|
-
const candidate = text.indexOf(quoteChar, search);
|
|
199
|
-
if (candidate === -1)
|
|
200
|
-
break;
|
|
201
|
-
if (backslashRun(text, candidate, start + opener.length) === depth) {
|
|
202
|
-
return { replacement: `${opener}${REDACTED}${opener}`, end: candidate + 1 };
|
|
203
|
-
}
|
|
204
|
-
search = candidate + 1;
|
|
205
|
-
}
|
|
206
|
-
return { replacement: `${opener}${REDACTED}`, end: text.length };
|
|
207
|
-
}
|
|
208
|
-
const rest = text.slice(start);
|
|
209
|
-
const scheme = AUTH_SCHEME.exec(rest);
|
|
210
|
-
const offset = scheme ? scheme[0].length : 0;
|
|
211
|
-
const run = UNQUOTED_VALUE.exec(rest.slice(offset));
|
|
212
|
-
const length = offset + (run ? run[0].length : 0);
|
|
213
|
-
return { replacement: length > 0 ? REDACTED : '', end: start + length };
|
|
214
|
-
}
|
|
215
|
-
function redactKeyValues(text) {
|
|
216
|
-
let out = '';
|
|
217
|
-
let copiedUpTo = 0;
|
|
218
|
-
KEY_VALUE_OPENING.lastIndex = 0;
|
|
219
|
-
let match;
|
|
220
|
-
while ((match = KEY_VALUE_OPENING.exec(text)) !== null) {
|
|
221
|
-
if (!SECRET_KEY_HINT.test(match[1] ?? ''))
|
|
222
|
-
continue;
|
|
223
|
-
const start = match.index + match[0].length;
|
|
224
|
-
const { replacement, end } = maskValue(text, start);
|
|
225
|
-
if (end === start)
|
|
226
|
-
continue;
|
|
227
|
-
out += text.slice(copiedUpTo, start) + replacement;
|
|
228
|
-
copiedUpTo = end;
|
|
229
|
-
KEY_VALUE_OPENING.lastIndex = end;
|
|
230
|
-
}
|
|
231
|
-
return out + text.slice(copiedUpTo);
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Mask credential-shaped values in free text: values under secret-like keys
|
|
235
|
-
* (`password`, `token`, `refreshToken`, `clientSecret`, `Authorization`, ...),
|
|
236
|
-
* `Bearer` credentials, JWT-shaped strings and credentials embedded in URLs.
|
|
237
|
-
*/
|
|
238
|
-
export function redactSecrets(text) {
|
|
239
|
-
return redactKeyValues(text)
|
|
240
|
-
.replace(BEARER_CREDENTIAL, `Bearer ${REDACTED}`)
|
|
241
|
-
.replace(JWT_LIKE, REDACTED)
|
|
242
|
-
.replace(URL_USERINFO, `$1${REDACTED}@`);
|
|
243
|
-
}
|
|
244
|
-
// ---------------------------------------------------------------------------
|
|
245
|
-
// Display lines and error messages
|
|
246
|
-
// ---------------------------------------------------------------------------
|
|
247
|
-
const LINE_BREAK = /\r\n|[\r\n\u0085\u2028\u2029]/;
|
|
248
|
-
/**
|
|
249
|
-
* Reduce untrusted text to one printable, credential-masked line of at most
|
|
250
|
-
* `maxLength` characters. Returns an empty string when nothing is left.
|
|
251
|
-
*/
|
|
252
|
-
export function toDisplayLine(value, maxLength = MAX_ERROR_MESSAGE_LENGTH) {
|
|
253
|
-
if (value === null || value === undefined)
|
|
254
|
-
return '';
|
|
255
|
-
const firstLine = String(value).split(LINE_BREAK, 1)[0] ?? '';
|
|
256
|
-
const scanned = firstLine.slice(0, MAX_SCAN_LENGTH);
|
|
257
|
-
const line = redactSecrets(sanitizeForTerminal(scanned)).trim();
|
|
258
|
-
return line.length > maxLength ? `${line.slice(0, maxLength)}${ELLIPSIS}` : line;
|
|
259
|
-
}
|
|
260
|
-
/** Short, printable excerpt of an HTTP response body for an error message. */
|
|
261
|
-
export function summarizeHttpBody(body) {
|
|
262
|
-
return toDisplayLine(body, MAX_HTTP_BODY_EXCERPT_LENGTH) || 'empty response body';
|
|
263
|
-
}
|
|
264
|
-
const ERROR_CODE = /^[A-Z][A-Z0-9_]{1,63}$/;
|
|
265
|
-
function asRecord(value) {
|
|
266
|
-
// Narrowing an arbitrary thrown value; every field read below is type-checked.
|
|
267
|
-
return typeof value === 'object' && value !== null
|
|
268
|
-
? value
|
|
269
|
-
: undefined;
|
|
270
|
-
}
|
|
271
|
-
function stringField(record, key) {
|
|
272
|
-
const value = record?.[key];
|
|
273
|
-
return typeof value === 'string' ? value : undefined;
|
|
274
|
-
}
|
|
275
|
-
function isAbortError(error) {
|
|
276
|
-
const name = stringField(asRecord(error), 'name');
|
|
277
|
-
return name === 'AbortError' || name === 'TimeoutError';
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* Describe an error that carries an HTTP/GraphQL `response` (graphql-request's
|
|
281
|
-
* ClientError). The library-composed `message` of such errors embeds the full
|
|
282
|
-
* request (query and variables) and the raw response body, so it is never
|
|
283
|
-
* read: the description is built only from the status, the first GraphQL
|
|
284
|
-
* error's message and its `extensions.code`.
|
|
285
|
-
*/
|
|
286
|
-
function describeResponseError(response) {
|
|
287
|
-
const status = typeof response.status === 'number' ? response.status : undefined;
|
|
288
|
-
const errors = Array.isArray(response.errors) ? response.errors : [];
|
|
289
|
-
const first = asRecord(errors[0]);
|
|
290
|
-
const extensions = asRecord(first?.extensions);
|
|
291
|
-
// Federated subgraph errors carry the useful text under
|
|
292
|
-
// extensions.originalError.message; the top-level message is often generic.
|
|
293
|
-
const original = toDisplayLine(stringField(asRecord(extensions?.originalError), 'message'));
|
|
294
|
-
const wrapper = toDisplayLine(stringField(first, 'message'));
|
|
295
|
-
const rawCode = stringField(extensions, 'code');
|
|
296
|
-
const code = rawCode !== undefined && ERROR_CODE.test(rawCode) ? rawCode : undefined;
|
|
297
|
-
const base = original ||
|
|
298
|
-
wrapper ||
|
|
299
|
-
(status !== undefined ? `Request failed (HTTP ${status})` : 'Request failed');
|
|
300
|
-
return {
|
|
301
|
-
message: code ? `${base} [${code}]` : base,
|
|
302
|
-
...(status !== undefined ? { status } : {}),
|
|
303
|
-
...(code ? { code } : {}),
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
function networkErrorCode(error) {
|
|
307
|
-
const code = stringField(asRecord(asRecord(error)?.cause), 'code');
|
|
308
|
-
return code !== undefined && ERROR_CODE.test(code) ? code : undefined;
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Describe a failure of a GraphQL request for display. Network-level
|
|
312
|
-
* `TypeError`s (fetch failures) are reported as network errors; aborted
|
|
313
|
-
* requests as timeouts.
|
|
314
|
-
*/
|
|
315
|
-
export function describeRequestError(error) {
|
|
316
|
-
const response = asRecord(asRecord(error)?.response);
|
|
317
|
-
if (response)
|
|
318
|
-
return describeResponseError(response);
|
|
319
|
-
if (isAbortError(error))
|
|
320
|
-
return { message: 'Request timed out' };
|
|
321
|
-
if (error instanceof TypeError) {
|
|
322
|
-
const detail = toDisplayLine(error.message, MAX_HTTP_BODY_EXCERPT_LENGTH);
|
|
323
|
-
const code = networkErrorCode(error);
|
|
324
|
-
return {
|
|
325
|
-
message: `Network error${detail ? `: ${detail}` : ''}${code ? ` (${code})` : ''}`,
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
return { message: toSafeErrorMessage(error) };
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Convert any thrown value into a message that is safe to print: a single
|
|
332
|
-
* line, at most {@link MAX_ERROR_MESSAGE_LENGTH} characters, free of terminal
|
|
333
|
-
* control sequences and with credential-shaped values masked. Response-bearing
|
|
334
|
-
* errors are described from an allowlist of fields only.
|
|
335
|
-
*/
|
|
336
|
-
export function toSafeErrorMessage(error) {
|
|
337
|
-
const response = asRecord(asRecord(error)?.response);
|
|
338
|
-
if (response)
|
|
339
|
-
return describeResponseError(response).message;
|
|
340
|
-
if (isAbortError(error))
|
|
341
|
-
return 'Request timed out';
|
|
342
|
-
if (error instanceof Error || typeof error === 'string') {
|
|
343
|
-
const text = typeof error === 'string' ? error : error.message;
|
|
344
|
-
return toDisplayLine(text) || 'Unexpected error';
|
|
345
|
-
}
|
|
346
|
-
return 'Unexpected error';
|
|
347
|
-
}
|