@icyouo/evt-cli 0.1.0
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 +214 -0
- package/README.zh-CN.md +214 -0
- package/bin/evt.js +8 -0
- package/data/apis/auth.example.yaml +44 -0
- package/data/apis/todo.example.yaml +52 -0
- package/data/flows/login.example.yaml +26 -0
- package/data/flows/smoke.example.yaml +9 -0
- package/data/profiles/local.example.json +29 -0
- package/data/scanner.example.json +40 -0
- package/package.json +35 -0
- package/src/api/liveTester.js +320 -0
- package/src/cache/sessionCache.js +37 -0
- package/src/config/loaders.js +108 -0
- package/src/config/schema.js +107 -0
- package/src/config/serviceScanner.js +426 -0
- package/src/config/validate.js +189 -0
- package/src/config/yaml.js +161 -0
- package/src/flow/inputResolver.js +72 -0
- package/src/flow/runner.js +156 -0
- package/src/http/client.js +46 -0
- package/src/http/requestBuilder.js +218 -0
- package/src/index.js +238 -0
- package/src/template/interpolate.js +65 -0
- package/src/util/args.js +74 -0
- package/src/util/duration.js +25 -0
- package/src/util/json.js +24 -0
- package/src/util/paths.js +38 -0
- package/src/util/redact.js +49 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
const { parseArgs } = require("./util/args");
|
|
2
|
+
const { parseJsonObject, stableJson } = require("./util/json");
|
|
3
|
+
const { redact } = require("./util/redact");
|
|
4
|
+
const { setConfigRoot } = require("./util/paths");
|
|
5
|
+
const {
|
|
6
|
+
listApiFiles,
|
|
7
|
+
listProfiles,
|
|
8
|
+
loadProfile,
|
|
9
|
+
loadApiRegistry,
|
|
10
|
+
listFlows,
|
|
11
|
+
loadFlow
|
|
12
|
+
} = require("./config/loaders");
|
|
13
|
+
const { readCache, writeCache, clearCache, resolveCachePath } = require("./cache/sessionCache");
|
|
14
|
+
const { executeEndpoint } = require("./http/client");
|
|
15
|
+
const { runFlow } = require("./flow/runner");
|
|
16
|
+
const { toCurl } = require("./http/requestBuilder");
|
|
17
|
+
const { compareScanToRegistry, scanServices } = require("./config/serviceScanner");
|
|
18
|
+
const { runLiveApiTest } = require("./api/liveTester");
|
|
19
|
+
|
|
20
|
+
function print(value, json = false) {
|
|
21
|
+
if (json || typeof value !== "string") {
|
|
22
|
+
console.log(stableJson(value));
|
|
23
|
+
} else {
|
|
24
|
+
console.log(value);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function usage() {
|
|
29
|
+
return [
|
|
30
|
+
"Usage:",
|
|
31
|
+
" evt profile list [--config-root ./cli]",
|
|
32
|
+
" evt profile show <name>",
|
|
33
|
+
" evt api list",
|
|
34
|
+
" evt api show <id>",
|
|
35
|
+
" evt api scan [--missing] [--scan-config path/to/scanner.json]",
|
|
36
|
+
" evt api call <id> [--profile local] [--set k=v] [--body '{...}'] [--dry-run]",
|
|
37
|
+
" evt api test-all [--profile local] [--include-dangerous] [--only namespace]",
|
|
38
|
+
" evt validate",
|
|
39
|
+
" evt flow list",
|
|
40
|
+
" evt flow run <name> [--profile local] [--set k=v] [--dry-run]",
|
|
41
|
+
" evt cache show|clear|path"
|
|
42
|
+
].join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function commonRuntime(options) {
|
|
46
|
+
const profile = loadProfile(options.profile || "local");
|
|
47
|
+
const cachePath = resolveCachePath(options.cache);
|
|
48
|
+
const cache = readCache(cachePath);
|
|
49
|
+
return {
|
|
50
|
+
profile,
|
|
51
|
+
cachePath,
|
|
52
|
+
cache,
|
|
53
|
+
registry: loadApiRegistry()
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function handleProfile(tokens) {
|
|
58
|
+
const sub = tokens[0];
|
|
59
|
+
const options = parseArgs(tokens.slice(1));
|
|
60
|
+
setConfigRoot(options.configRoot);
|
|
61
|
+
if (sub === "list") {
|
|
62
|
+
print(listProfiles(), options.json);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (sub === "show") {
|
|
66
|
+
print(loadProfile(options._[0]), true);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
throw new Error(usage());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function handleApi(tokens) {
|
|
73
|
+
const sub = tokens[0];
|
|
74
|
+
const options = parseArgs(tokens.slice(1));
|
|
75
|
+
setConfigRoot(options.configRoot);
|
|
76
|
+
const registry = loadApiRegistry();
|
|
77
|
+
|
|
78
|
+
if (sub === "list") {
|
|
79
|
+
print(Object.keys(registry).sort(), options.json);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (sub === "show") {
|
|
83
|
+
const endpoint = registry[options._[0]];
|
|
84
|
+
if (!endpoint) throw new Error(`Endpoint not found: ${options._[0]}`);
|
|
85
|
+
print(endpoint, true);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (sub === "scan") {
|
|
89
|
+
const scanned = scanServices({ scanConfig: options.scanConfig });
|
|
90
|
+
const comparison = compareScanToRegistry(scanned, registry);
|
|
91
|
+
const payload = options.missing ? comparison.missing : {
|
|
92
|
+
scanned,
|
|
93
|
+
covered: comparison.covered.length,
|
|
94
|
+
missing: comparison.missing
|
|
95
|
+
};
|
|
96
|
+
print(payload, true);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (sub === "call") {
|
|
100
|
+
const id = options._[0];
|
|
101
|
+
const endpoint = registry[id];
|
|
102
|
+
if (!endpoint) throw new Error(`Endpoint not found: ${id}`);
|
|
103
|
+
const runtime = commonRuntime(options);
|
|
104
|
+
const body = parseJsonObject(options.body, "--body");
|
|
105
|
+
const query = parseJsonObject(options.query, "--query");
|
|
106
|
+
const context = {
|
|
107
|
+
profile: runtime.profile,
|
|
108
|
+
cache: runtime.cache,
|
|
109
|
+
args: { ...(options.set || {}), ...body, ...query },
|
|
110
|
+
inputs: {},
|
|
111
|
+
env: process.env,
|
|
112
|
+
steps: {}
|
|
113
|
+
};
|
|
114
|
+
const result = await executeEndpoint(endpoint, {
|
|
115
|
+
...runtime,
|
|
116
|
+
context,
|
|
117
|
+
body,
|
|
118
|
+
query,
|
|
119
|
+
cliHeaders: options.headers,
|
|
120
|
+
baseUrl: options.baseUrl,
|
|
121
|
+
dryRun: options.dryRun,
|
|
122
|
+
unsafe: options.unsafe
|
|
123
|
+
});
|
|
124
|
+
if (options.curl) {
|
|
125
|
+
print(result.curl || toCurl(result.request), false);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
print(options.dryRun ? { ...result, request: redact(result.request) } : result, options.json || options.dryRun);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (sub === "test-all") {
|
|
132
|
+
const runtime = commonRuntime(options);
|
|
133
|
+
const report = await runLiveApiTest(runtime, {
|
|
134
|
+
...options,
|
|
135
|
+
includeDangerous: options.includeDangerous,
|
|
136
|
+
noLogin: options.noLogin
|
|
137
|
+
});
|
|
138
|
+
print({
|
|
139
|
+
ok: report.ok,
|
|
140
|
+
profile: report.profile,
|
|
141
|
+
total: report.total,
|
|
142
|
+
summary: report.summary,
|
|
143
|
+
login: report.login,
|
|
144
|
+
reportPath: report.reportPath
|
|
145
|
+
}, true);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
throw new Error(usage());
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function handleFlow(tokens) {
|
|
152
|
+
const sub = tokens[0];
|
|
153
|
+
const options = parseArgs(tokens.slice(1));
|
|
154
|
+
setConfigRoot(options.configRoot);
|
|
155
|
+
if (sub === "list") {
|
|
156
|
+
print(listFlows(), options.json);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (sub === "run") {
|
|
160
|
+
const name = options._[0];
|
|
161
|
+
const runtime = commonRuntime(options);
|
|
162
|
+
const flow = loadFlow(name);
|
|
163
|
+
const result = await runFlow(flow, {
|
|
164
|
+
...runtime,
|
|
165
|
+
set: options.set || {},
|
|
166
|
+
cliHeaders: options.headers,
|
|
167
|
+
baseUrl: options.baseUrl,
|
|
168
|
+
dryRun: options.dryRun,
|
|
169
|
+
unsafe: options.unsafe,
|
|
170
|
+
noInteractive: Boolean(options.noInteractive) || !process.stdin.isTTY
|
|
171
|
+
});
|
|
172
|
+
print(redact(result), options.json || options.dryRun);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
throw new Error(usage());
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function handleCache(tokens) {
|
|
179
|
+
const sub = tokens[0];
|
|
180
|
+
const options = parseArgs(tokens.slice(1));
|
|
181
|
+
setConfigRoot(options.configRoot);
|
|
182
|
+
const cachePath = resolveCachePath(options.cache);
|
|
183
|
+
if (sub === "show") {
|
|
184
|
+
print(redact(readCache(cachePath)), true);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (sub === "clear") {
|
|
188
|
+
clearCache(cachePath);
|
|
189
|
+
print(`Cleared ${cachePath}`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (sub === "path") {
|
|
193
|
+
print(cachePath);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (sub === "write") {
|
|
197
|
+
const value = parseJsonObject(options.body || options._[0], "cache write");
|
|
198
|
+
writeCache(cachePath, value);
|
|
199
|
+
print(`Wrote ${cachePath}`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
throw new Error(usage());
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function handleValidate(tokens) {
|
|
206
|
+
const options = parseArgs(tokens);
|
|
207
|
+
setConfigRoot(options.configRoot);
|
|
208
|
+
const profiles = listProfiles();
|
|
209
|
+
for (const profile of profiles) loadProfile(profile);
|
|
210
|
+
const registry = loadApiRegistry();
|
|
211
|
+
for (const flow of listFlows()) loadFlow(flow);
|
|
212
|
+
print({
|
|
213
|
+
ok: true,
|
|
214
|
+
profiles: profiles.length,
|
|
215
|
+
apiFiles: listApiFiles().length,
|
|
216
|
+
endpoints: Object.keys(registry).length,
|
|
217
|
+
flows: listFlows().length
|
|
218
|
+
}, options.json);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function run(argv) {
|
|
222
|
+
const [command, ...tokens] = argv;
|
|
223
|
+
if (!command || command === "help" || command === "--help") {
|
|
224
|
+
print(usage());
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (command === "profile") return handleProfile(tokens);
|
|
228
|
+
if (command === "api") return handleApi(tokens);
|
|
229
|
+
if (command === "flow") return handleFlow(tokens);
|
|
230
|
+
if (command === "cache") return handleCache(tokens);
|
|
231
|
+
if (command === "validate") return handleValidate(tokens);
|
|
232
|
+
throw new Error(usage());
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
run,
|
|
237
|
+
usage
|
|
238
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const crypto = require("node:crypto");
|
|
2
|
+
|
|
3
|
+
function getPath(source, dottedPath) {
|
|
4
|
+
if (dottedPath === "now.iso") return new Date().toISOString();
|
|
5
|
+
if (dottedPath === "uuid") return crypto.randomUUID();
|
|
6
|
+
|
|
7
|
+
const parts = dottedPath.split(".");
|
|
8
|
+
let current = source;
|
|
9
|
+
for (const part of parts) {
|
|
10
|
+
if (current === undefined || current === null) return undefined;
|
|
11
|
+
current = current[part];
|
|
12
|
+
}
|
|
13
|
+
return current;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function interpolateString(value, context) {
|
|
17
|
+
const exact = value.match(/^\s*\{\{\s*([^}]+?)\s*\}\}\s*$/);
|
|
18
|
+
if (exact) {
|
|
19
|
+
return getPath(context, exact[1].trim());
|
|
20
|
+
}
|
|
21
|
+
return value.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_, expression) => {
|
|
22
|
+
const resolved = getPath(context, expression.trim());
|
|
23
|
+
return resolved === undefined || resolved === null ? "" : String(resolved);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function interpolate(value, context) {
|
|
28
|
+
if (typeof value === "string") {
|
|
29
|
+
return interpolateString(value, context);
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(value)) {
|
|
32
|
+
return value.map((item) => interpolate(item, context));
|
|
33
|
+
}
|
|
34
|
+
if (value && typeof value === "object") {
|
|
35
|
+
return Object.fromEntries(
|
|
36
|
+
Object.entries(value).map(([key, item]) => [key, interpolate(item, context)])
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function pruneUndefined(value, options = {}) {
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
return value
|
|
45
|
+
.map((item) => pruneUndefined(item, options))
|
|
46
|
+
.filter((item) => item !== undefined && item !== null);
|
|
47
|
+
}
|
|
48
|
+
if (value && typeof value === "object") {
|
|
49
|
+
const result = {};
|
|
50
|
+
for (const [key, item] of Object.entries(value)) {
|
|
51
|
+
const pruned = pruneUndefined(item, options);
|
|
52
|
+
if (pruned === undefined || pruned === null) continue;
|
|
53
|
+
if (options.dropEmptyStrings && pruned === "") continue;
|
|
54
|
+
result[key] = pruned;
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
getPath,
|
|
63
|
+
interpolate,
|
|
64
|
+
pruneUndefined
|
|
65
|
+
};
|
package/src/util/args.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
function parseKeyValue(value, optionName) {
|
|
2
|
+
const index = value.indexOf("=");
|
|
3
|
+
if (index <= 0) {
|
|
4
|
+
throw new Error(`${optionName} expects key=value`);
|
|
5
|
+
}
|
|
6
|
+
return [value.slice(0, index), value.slice(index + 1)];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function camelOption(name) {
|
|
10
|
+
return name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseArgs(tokens) {
|
|
14
|
+
const options = {
|
|
15
|
+
_: [],
|
|
16
|
+
set: {},
|
|
17
|
+
headers: {}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
21
|
+
const token = tokens[index];
|
|
22
|
+
if (!token.startsWith("--")) {
|
|
23
|
+
options._.push(token);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const raw = token.slice(2);
|
|
28
|
+
const equals = raw.indexOf("=");
|
|
29
|
+
const name = camelOption(equals >= 0 ? raw.slice(0, equals) : raw);
|
|
30
|
+
const inlineValue = equals >= 0 ? raw.slice(equals + 1) : undefined;
|
|
31
|
+
const booleanOption = new Set([
|
|
32
|
+
"dryRun",
|
|
33
|
+
"json",
|
|
34
|
+
"verbose",
|
|
35
|
+
"curl",
|
|
36
|
+
"noInteractive",
|
|
37
|
+
"unsafe",
|
|
38
|
+
"includeDangerous",
|
|
39
|
+
"noLogin",
|
|
40
|
+
"missing",
|
|
41
|
+
"help"
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
let value;
|
|
45
|
+
if (booleanOption.has(name)) {
|
|
46
|
+
value = inlineValue === undefined ? true : inlineValue !== "false";
|
|
47
|
+
} else if (inlineValue !== undefined) {
|
|
48
|
+
value = inlineValue;
|
|
49
|
+
} else {
|
|
50
|
+
index += 1;
|
|
51
|
+
if (index >= tokens.length) {
|
|
52
|
+
throw new Error(`--${raw} expects a value`);
|
|
53
|
+
}
|
|
54
|
+
value = tokens[index];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (name === "set") {
|
|
58
|
+
const [key, parsedValue] = parseKeyValue(value, "--set");
|
|
59
|
+
options.set[key] = parsedValue;
|
|
60
|
+
} else if (name === "header") {
|
|
61
|
+
const [key, parsedValue] = parseKeyValue(value, "--header");
|
|
62
|
+
options.headers[key] = parsedValue;
|
|
63
|
+
} else {
|
|
64
|
+
options[name] = value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return options;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
parseArgs,
|
|
73
|
+
parseKeyValue
|
|
74
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function parseDuration(value) {
|
|
2
|
+
if (typeof value === "number") {
|
|
3
|
+
return value;
|
|
4
|
+
}
|
|
5
|
+
const raw = String(value).trim();
|
|
6
|
+
const match = raw.match(/^(\d+(?:\.\d+)?)(ms|s|m)?$/);
|
|
7
|
+
if (!match) {
|
|
8
|
+
throw new Error(`Invalid duration: ${value}`);
|
|
9
|
+
}
|
|
10
|
+
const amount = Number(match[1]);
|
|
11
|
+
const unit = match[2] || "ms";
|
|
12
|
+
if (unit === "ms") return amount;
|
|
13
|
+
if (unit === "s") return amount * 1000;
|
|
14
|
+
if (unit === "m") return amount * 60 * 1000;
|
|
15
|
+
throw new Error(`Invalid duration unit: ${unit}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sleep(ms) {
|
|
19
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
parseDuration,
|
|
24
|
+
sleep
|
|
25
|
+
};
|
package/src/util/json.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
function parseJsonObject(value, optionName) {
|
|
2
|
+
if (value === undefined || value === null || value === "") {
|
|
3
|
+
return {};
|
|
4
|
+
}
|
|
5
|
+
let parsed;
|
|
6
|
+
try {
|
|
7
|
+
parsed = JSON.parse(value);
|
|
8
|
+
} catch (error) {
|
|
9
|
+
throw new Error(`${optionName} must be valid JSON: ${error.message}`);
|
|
10
|
+
}
|
|
11
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
12
|
+
throw new Error(`${optionName} must be a JSON object`);
|
|
13
|
+
}
|
|
14
|
+
return parsed;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function stableJson(value) {
|
|
18
|
+
return JSON.stringify(value, null, 2);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
parseJsonObject,
|
|
23
|
+
stableJson
|
|
24
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const path = require("node:path");
|
|
2
|
+
|
|
3
|
+
const cliRoot = path.resolve(__dirname, "../..");
|
|
4
|
+
let configuredRoot;
|
|
5
|
+
|
|
6
|
+
function setConfigRoot(root) {
|
|
7
|
+
configuredRoot = root ? path.resolve(root) : undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function configRoot() {
|
|
11
|
+
return path.resolve(process.env.EVT_CLI_ROOT || process.env.EVERYTHING_CLI_ROOT || configuredRoot || cliRoot);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveCliPath(...parts) {
|
|
15
|
+
return path.resolve(configRoot(), ...parts);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveConfigDir(name) {
|
|
19
|
+
const fs = require("node:fs");
|
|
20
|
+
const candidates = [
|
|
21
|
+
resolveCliPath(name),
|
|
22
|
+
resolveCliPath("data", name)
|
|
23
|
+
];
|
|
24
|
+
return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function defaultCachePath() {
|
|
28
|
+
return resolveCliPath(".cache", "session.local.json");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
cliRoot,
|
|
33
|
+
configRoot,
|
|
34
|
+
defaultCachePath,
|
|
35
|
+
resolveCliPath,
|
|
36
|
+
resolveConfigDir,
|
|
37
|
+
setConfigRoot
|
|
38
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const SENSITIVE_KEYS = new Set([
|
|
2
|
+
"authorization",
|
|
3
|
+
"password",
|
|
4
|
+
"token",
|
|
5
|
+
"googlecode",
|
|
6
|
+
"idtoken",
|
|
7
|
+
"temptoken",
|
|
8
|
+
"encodedbody"
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const REQUEST_CODE_PARENT_KEYS = new Set([
|
|
12
|
+
"body",
|
|
13
|
+
"inputs",
|
|
14
|
+
"args",
|
|
15
|
+
"set"
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function isSensitiveKey(key) {
|
|
19
|
+
return SENSITIVE_KEYS.has(String(key).toLowerCase());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function redact(value, parentKey = "") {
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
return value.map((item) => redact(item, parentKey));
|
|
25
|
+
}
|
|
26
|
+
if (!value || typeof value !== "object") {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
return Object.fromEntries(
|
|
30
|
+
Object.entries(value).map(([key, item]) => [
|
|
31
|
+
key,
|
|
32
|
+
shouldRedactKey(key, parentKey) && item !== undefined && item !== null ? "***" : redact(item, key)
|
|
33
|
+
])
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function shouldRedactKey(key, parentKey) {
|
|
38
|
+
const normalized = String(key).toLowerCase();
|
|
39
|
+
if (isSensitiveKey(normalized)) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return normalized === "code" && REQUEST_CODE_PARENT_KEYS.has(String(parentKey).toLowerCase());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = {
|
|
46
|
+
redact,
|
|
47
|
+
isSensitiveKey,
|
|
48
|
+
shouldRedactKey
|
|
49
|
+
};
|