mcp-for-i 0.1.13 → 0.2.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 +17 -0
- package/dist/config/store.js +229 -109
- package/dist/controlplane/auth.js +128 -0
- package/dist/controlplane/clientIntegrationService.js +83 -4
- package/dist/controlplane/commands.js +14 -9
- package/dist/controlplane/connectionService.js +110 -5
- package/dist/controlplane/connectionTest.js +329 -0
- package/dist/controlplane/errors.js +12 -0
- package/dist/controlplane/runtimeService.js +98 -26
- package/dist/controlplane/server.js +122 -11
- package/dist/controlplane/serverShared.js +25 -6
- package/dist/controlplane/serviceManager.js +113 -100
- package/dist/controlplane/ui.js +1742 -920
- package/dist/ibmi/CompileTools.js +29 -1
- package/dist/ibmi/IBMiContent.js +435 -63
- package/dist/ibmi/IbmDocs.js +685 -0
- package/dist/ibmi/LocalLanguageActions.js +129 -3
- package/dist/ibmi/Search.js +158 -52
- package/dist/ibmi/Tools.js +7 -10
- package/dist/ibmi/client.js +220 -31
- package/dist/ibmi/evfevent.js +256 -0
- package/dist/ibmi/knownHosts.js +190 -0
- package/dist/ibmi/libraryList.js +93 -0
- package/dist/ibmi/sourceInspect.js +815 -0
- package/dist/ibmi/sqlJob.js +888 -0
- package/dist/ibmi/sqlParse.js +317 -0
- package/dist/ibmi/tn5250/certTrust.js +59 -0
- package/dist/ibmi/tn5250/client.js +310 -0
- package/dist/ibmi/tn5250/datastream.js +109 -0
- package/dist/ibmi/tn5250/ebcdic.js +74 -0
- package/dist/ibmi/tn5250/index.js +6 -0
- package/dist/ibmi/tn5250/screen.js +827 -0
- package/dist/ibmi/tn5250/telnet.js +186 -0
- package/dist/ibmi/variables.js +18 -7
- package/dist/index.js +43 -22
- package/dist/mcp/annotations.js +155 -0
- package/dist/mcp/audit.js +490 -133
- package/dist/mcp/confirm.js +48 -0
- package/dist/mcp/context.js +43 -5
- package/dist/mcp/deployMethods.js +192 -0
- package/dist/mcp/logger.js +5 -10
- package/dist/mcp/modules/bulkMembers.js +476 -0
- package/dist/mcp/modules/changedLines.js +189 -0
- package/dist/mcp/modules/compare.js +198 -0
- package/dist/mcp/modules/compile.js +515 -0
- package/dist/mcp/modules/ifsOps.js +123 -0
- package/dist/mcp/modules/index.js +38 -0
- package/dist/mcp/modules/inspect.js +811 -0
- package/dist/mcp/modules/libl.js +246 -0
- package/dist/mcp/modules/members.js +171 -0
- package/dist/mcp/modules/objects.js +339 -0
- package/dist/mcp/modules/system.js +266 -0
- package/dist/mcp/modules/tn5250.js +515 -0
- package/dist/mcp/policy.js +412 -0
- package/dist/mcp/tools.js +1700 -698
- package/dist/security/credentialStore.js +62 -13
- package/dist/security/localPaths.js +138 -0
- package/dist/security/redact.js +79 -0
- package/dist/util/atomicWrite.js +101 -0
- package/dist/util/fileLock.js +206 -0
- package/docs/capabilities.md +50 -7
- package/docs/configuration.md +189 -60
- package/docs/examples.md +109 -73
- package/docs/overview.md +15 -15
- package/docs/security.md +101 -16
- package/docs/tools.md +363 -188
- package/package.json +56 -53
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ElicitResultSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
/** How long a confirmation prompt waits for the person before the operation is refused. */
|
|
3
|
+
export const CONFIRMATION_TIMEOUT_MS = 10 * 60 * 1000;
|
|
4
|
+
/**
|
|
5
|
+
* Ask the person through the MCP client (elicitation) before a guarded
|
|
6
|
+
* operation runs. Clients that don't support elicitation get "unsupported",
|
|
7
|
+
* which refuses the operation.
|
|
8
|
+
*/
|
|
9
|
+
export function createElicitationConfirm(server, timeoutMs = CONFIRMATION_TIMEOUT_MS) {
|
|
10
|
+
return async (request) => {
|
|
11
|
+
if (!server.getClientCapabilities()?.elicitation)
|
|
12
|
+
return "unsupported";
|
|
13
|
+
const result = await server.request({
|
|
14
|
+
method: "elicitation/create",
|
|
15
|
+
params: {
|
|
16
|
+
mode: "form",
|
|
17
|
+
message: confirmationMessage(request),
|
|
18
|
+
requestedSchema: request.allowSession
|
|
19
|
+
? {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
allowForSession: {
|
|
23
|
+
type: "boolean",
|
|
24
|
+
title: "Allow this kind of change on this connection until the server restarts",
|
|
25
|
+
default: false
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
: { type: "object", properties: {} }
|
|
30
|
+
}
|
|
31
|
+
}, ElicitResultSchema, { timeout: timeoutMs });
|
|
32
|
+
return {
|
|
33
|
+
allowed: result.action === "accept",
|
|
34
|
+
forSession: result.action === "accept" && result.content?.allowForSession === true
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function confirmationMessage(request) {
|
|
39
|
+
const what = request.operation === "shell.run"
|
|
40
|
+
? "run a shell command"
|
|
41
|
+
: `perform a guarded operation (${request.operation})`;
|
|
42
|
+
const lines = [`The agent wants to ${what} on IBM i connection "${request.connectionName || "(active)"}".`];
|
|
43
|
+
if (request.toolName)
|
|
44
|
+
lines.push(`Tool: ${request.toolName}`);
|
|
45
|
+
lines.push(...request.details);
|
|
46
|
+
lines.push("", "Accept to let it run. Decline to stop it.");
|
|
47
|
+
return lines.join("\n");
|
|
48
|
+
}
|
package/dist/mcp/context.js
CHANGED
|
@@ -11,6 +11,13 @@ const DEFAULT_RECONNECT_ATTEMPTS = 2;
|
|
|
11
11
|
export class McpContext {
|
|
12
12
|
store = new ConfigStore();
|
|
13
13
|
activeName;
|
|
14
|
+
/**
|
|
15
|
+
* Asks the person to confirm a guarded operation. Set by the server from
|
|
16
|
+
* the MCP client's elicitation support; unset means nobody can be asked.
|
|
17
|
+
*/
|
|
18
|
+
confirm;
|
|
19
|
+
/** Operations the person allowed for the rest of this session, keyed by connection and operation. */
|
|
20
|
+
sessionApprovals = new Set();
|
|
14
21
|
sessions = new Map();
|
|
15
22
|
cursors = new Map();
|
|
16
23
|
connectLocks = new Map();
|
|
@@ -61,8 +68,9 @@ export class McpContext {
|
|
|
61
68
|
}
|
|
62
69
|
async ensureActive(name) {
|
|
63
70
|
const sessionName = name || this.activeName;
|
|
64
|
-
if (!sessionName)
|
|
65
|
-
throw new Error("
|
|
71
|
+
if (!sessionName) {
|
|
72
|
+
throw new Error("No active connection. Call ibmi.connect or pass connectionName.");
|
|
73
|
+
}
|
|
66
74
|
if (!this.sessions.has(sessionName)) {
|
|
67
75
|
await this.connectByName(sessionName);
|
|
68
76
|
}
|
|
@@ -70,6 +78,25 @@ export class McpContext {
|
|
|
70
78
|
this.touchSession(sessionName);
|
|
71
79
|
return this.sessions.get(sessionName).client;
|
|
72
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Policy for a connection, read from the stored connection when no session
|
|
83
|
+
* is open yet, so checks that run before connecting still see it.
|
|
84
|
+
*/
|
|
85
|
+
async resolvePolicy(name) {
|
|
86
|
+
const sessionName = name || this.activeName;
|
|
87
|
+
if (!sessionName)
|
|
88
|
+
return defaultPolicy();
|
|
89
|
+
const session = this.sessions.get(sessionName);
|
|
90
|
+
if (session)
|
|
91
|
+
return session.policy;
|
|
92
|
+
try {
|
|
93
|
+
const stored = await this.store.getConnection(sessionName);
|
|
94
|
+
return normalizePolicy(stored?.policy);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return defaultPolicy();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
73
100
|
getPolicy(name) {
|
|
74
101
|
const sessionName = name || this.activeName;
|
|
75
102
|
if (!sessionName)
|
|
@@ -108,7 +135,9 @@ export class McpContext {
|
|
|
108
135
|
lastValidatedAt: session.lastValidatedAt ? new Date(session.lastValidatedAt).toISOString() : undefined,
|
|
109
136
|
expiresAt: new Date(session.expiresAt).toISOString(),
|
|
110
137
|
idleSecondsRemaining: Math.max(0, Math.floor((session.expiresAt - now) / 1000)),
|
|
111
|
-
lastError: session.lastError
|
|
138
|
+
lastError: session.lastError,
|
|
139
|
+
// Persistent SQL job (Mapepire): open, fallen back to QZDFMDB2, or not started yet.
|
|
140
|
+
sqlJob: session.client.sqlJob?.status?.()
|
|
112
141
|
};
|
|
113
142
|
}
|
|
114
143
|
async keepaliveSession(name) {
|
|
@@ -125,6 +154,10 @@ export class McpContext {
|
|
|
125
154
|
throw new Error("connectionName is required (no active session)");
|
|
126
155
|
await this.disconnect(target);
|
|
127
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Closes the session, including its persistent SQL job (IBMiClient.disconnect
|
|
159
|
+
* ends the Mapepire job before closing SSH).
|
|
160
|
+
*/
|
|
128
161
|
async disconnect(name) {
|
|
129
162
|
if (name) {
|
|
130
163
|
const session = this.sessions.get(name);
|
|
@@ -142,7 +175,9 @@ export class McpContext {
|
|
|
142
175
|
}
|
|
143
176
|
this.sessions.delete(name);
|
|
144
177
|
if (this.activeName === name) {
|
|
145
|
-
|
|
178
|
+
// Never fall back to another open session: later calls without
|
|
179
|
+
// connectionName must not silently target a different system.
|
|
180
|
+
this.activeName = undefined;
|
|
146
181
|
}
|
|
147
182
|
await this.persistSessionState(true);
|
|
148
183
|
return;
|
|
@@ -188,7 +223,10 @@ export class McpContext {
|
|
|
188
223
|
const stored = await this.store.getConnection(name);
|
|
189
224
|
if (!stored)
|
|
190
225
|
throw new Error(`Unknown connection: ${name}`);
|
|
191
|
-
|
|
226
|
+
// Key-based connections must still work when the keychain is unavailable.
|
|
227
|
+
const password = stored.privateKeyPath
|
|
228
|
+
? await getPassword(name).catch(() => undefined)
|
|
229
|
+
: await getPassword(name);
|
|
192
230
|
const connection = {
|
|
193
231
|
name: stored.name,
|
|
194
232
|
host: stored.host,
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import { createReadStream } from "fs";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { execFile } from "child_process";
|
|
6
|
+
import { Tools } from "../ibmi/Tools.js";
|
|
7
|
+
export const DEPLOY_METHODS = ["all", "changed", "gitChanged", "selection"];
|
|
8
|
+
export function normalizeDeployMethod(value) {
|
|
9
|
+
if (value === undefined || value === null || value === "")
|
|
10
|
+
return "all";
|
|
11
|
+
const method = String(value);
|
|
12
|
+
if (!DEPLOY_METHODS.includes(method)) {
|
|
13
|
+
throw new Error(`Invalid method: ${method}. Expected one of ${DEPLOY_METHODS.join(", ")}`);
|
|
14
|
+
}
|
|
15
|
+
return method;
|
|
16
|
+
}
|
|
17
|
+
export async function sha256LocalFile(filePath) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const hash = crypto.createHash("sha256");
|
|
20
|
+
const stream = createReadStream(filePath);
|
|
21
|
+
stream.on("error", reject);
|
|
22
|
+
stream.on("data", chunk => hash.update(chunk));
|
|
23
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
// Upper bound on the size of one remote hashing command (bytes of quoted
|
|
27
|
+
// paths) and on the number of files hashed per call.
|
|
28
|
+
const REMOTE_HASH_BATCH_BYTES = 24_000;
|
|
29
|
+
const REMOTE_HASH_BATCH_FILES = 200;
|
|
30
|
+
const NO_HASH_TOOL_MARKER = "MCPFORI_NO_SHA256_TOOL";
|
|
31
|
+
/**
|
|
32
|
+
* Picks sha256sum (PATH or /QOpenSys/pkgs/bin) or openssl, then hashes the
|
|
33
|
+
* given files. Kept as one shell snippet so detection costs no extra call.
|
|
34
|
+
*/
|
|
35
|
+
function remoteHashCommand(remoteRoot, relPaths) {
|
|
36
|
+
const quoted = relPaths.map(rel => Tools.shellQuote(`./${rel}`)).join(" ");
|
|
37
|
+
return [
|
|
38
|
+
`cd ${Tools.shellQuote(remoteRoot)} || exit 2`,
|
|
39
|
+
`if command -v sha256sum >/dev/null 2>&1; then H="sha256sum"`,
|
|
40
|
+
`elif [ -x /QOpenSys/pkgs/bin/sha256sum ]; then H="/QOpenSys/pkgs/bin/sha256sum"`,
|
|
41
|
+
`elif command -v openssl >/dev/null 2>&1; then H="openssl dgst -sha256 -r"`,
|
|
42
|
+
`elif [ -x /QOpenSys/usr/bin/openssl ]; then H="/QOpenSys/usr/bin/openssl dgst -sha256 -r"`,
|
|
43
|
+
`else echo ${NO_HASH_TOOL_MARKER} >&2; exit 127; fi`,
|
|
44
|
+
`$H ${quoted}`
|
|
45
|
+
].join("; ");
|
|
46
|
+
}
|
|
47
|
+
/** Parses sha256sum / `openssl dgst -r` / `openssl dgst` output into rel path -> hex. */
|
|
48
|
+
export function parseRemoteHashOutput(output) {
|
|
49
|
+
const hashes = new Map();
|
|
50
|
+
for (const rawLine of String(output || "").split(/\r?\n/)) {
|
|
51
|
+
const line = rawLine.trimEnd();
|
|
52
|
+
if (!line)
|
|
53
|
+
continue;
|
|
54
|
+
let hex;
|
|
55
|
+
let file;
|
|
56
|
+
const coreutils = line.match(/^\\?([0-9a-fA-F]{64}) [ *](.+)$/);
|
|
57
|
+
if (coreutils) {
|
|
58
|
+
hex = coreutils[1];
|
|
59
|
+
file = coreutils[2];
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const bsd = line.match(/^SHA2?-?256\((.+)\)\s*=\s*([0-9a-fA-F]{64})$/);
|
|
63
|
+
if (bsd) {
|
|
64
|
+
file = bsd[1];
|
|
65
|
+
hex = bsd[2];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!hex || !file)
|
|
69
|
+
continue;
|
|
70
|
+
hashes.set(file.replace(/^\.\//, ""), hex.toLowerCase());
|
|
71
|
+
}
|
|
72
|
+
return hashes;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Hashes remote files (paths relative to remoteRoot) in batches. Files that
|
|
76
|
+
* could not be hashed are simply absent from the result. Throws when the
|
|
77
|
+
* remote system has neither sha256sum nor openssl.
|
|
78
|
+
*/
|
|
79
|
+
export async function sha256RemoteFiles(conn, remoteRoot, relPaths) {
|
|
80
|
+
const result = new Map();
|
|
81
|
+
const root = remoteRoot.replace(/\/+$/, "") || "/";
|
|
82
|
+
const usable = relPaths.filter(rel => !/[\r\n]/.test(rel));
|
|
83
|
+
let batch = [];
|
|
84
|
+
let batchBytes = 0;
|
|
85
|
+
const flush = async () => {
|
|
86
|
+
if (batch.length === 0)
|
|
87
|
+
return;
|
|
88
|
+
const res = await conn.sendCommand({ command: remoteHashCommand(root, batch) });
|
|
89
|
+
if (String(res?.stderr || "").includes(NO_HASH_TOOL_MARKER)) {
|
|
90
|
+
throw new Error("No SHA-256 tool (sha256sum or openssl) found on the IBM i; use method \"all\" instead");
|
|
91
|
+
}
|
|
92
|
+
if (res?.code === 2 && !String(res?.stdout || "").trim()) {
|
|
93
|
+
throw new Error(`Remote directory ${root} is not accessible: ${String(res?.stderr || "").trim()}`);
|
|
94
|
+
}
|
|
95
|
+
// A non-zero exit with partial output means some files could not be
|
|
96
|
+
// read; those stay unhashed and are treated as changed by the caller.
|
|
97
|
+
for (const [file, hex] of parseRemoteHashOutput(res?.stdout || ""))
|
|
98
|
+
result.set(file, hex);
|
|
99
|
+
batch = [];
|
|
100
|
+
batchBytes = 0;
|
|
101
|
+
};
|
|
102
|
+
for (const rel of usable) {
|
|
103
|
+
const size = rel.length + 6;
|
|
104
|
+
if (batch.length > 0 && (batch.length >= REMOTE_HASH_BATCH_FILES || batchBytes + size > REMOTE_HASH_BATCH_BYTES)) {
|
|
105
|
+
await flush();
|
|
106
|
+
}
|
|
107
|
+
batch.push(rel);
|
|
108
|
+
batchBytes += size;
|
|
109
|
+
}
|
|
110
|
+
await flush();
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
function runGit(cwd, args) {
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
execFile("git", ["-C", cwd, ...args], { maxBuffer: 64 * 1024 * 1024, windowsHide: true, shell: false }, (error, stdout, stderr) => {
|
|
116
|
+
if (error) {
|
|
117
|
+
const detail = String(stderr || error.message || "").trim();
|
|
118
|
+
reject(new Error(`git ${args[0]} failed: ${detail}`));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
resolve(String(stdout));
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/** Parses `git status --porcelain -z` output (paths relative to the repo root). */
|
|
126
|
+
export function parseGitPorcelainZ(output) {
|
|
127
|
+
const entries = [];
|
|
128
|
+
const tokens = String(output || "").split("\0");
|
|
129
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
130
|
+
const token = tokens[i];
|
|
131
|
+
if (token.length < 4)
|
|
132
|
+
continue;
|
|
133
|
+
const status = token.substring(0, 2);
|
|
134
|
+
const file = token.substring(3);
|
|
135
|
+
entries.push({ status, path: file });
|
|
136
|
+
// Renames and copies are followed by the original path.
|
|
137
|
+
if (status[0] === "R" || status[0] === "C")
|
|
138
|
+
i++;
|
|
139
|
+
}
|
|
140
|
+
return entries;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Files changed in the git working tree under localRoot (staged, unstaged
|
|
144
|
+
* and untracked), as paths relative to localRoot with forward slashes.
|
|
145
|
+
*/
|
|
146
|
+
export async function gitChangedFiles(localRoot) {
|
|
147
|
+
const prefix = (await runGit(localRoot, ["rev-parse", "--show-prefix"])).trim().replace(/\\/g, "/");
|
|
148
|
+
const output = await runGit(localRoot, ["status", "--porcelain", "-z", "--untracked-files=all", "--", "."]);
|
|
149
|
+
const files = new Set();
|
|
150
|
+
const deleted = new Set();
|
|
151
|
+
for (const entry of parseGitPorcelainZ(output)) {
|
|
152
|
+
const repoPath = entry.path.replace(/\\/g, "/");
|
|
153
|
+
if (prefix && !repoPath.startsWith(prefix))
|
|
154
|
+
continue;
|
|
155
|
+
const rel = repoPath.substring(prefix.length);
|
|
156
|
+
if (!rel || rel.endsWith("/"))
|
|
157
|
+
continue;
|
|
158
|
+
const full = path.join(localRoot, rel);
|
|
159
|
+
const stat = await fs.stat(full).catch(() => undefined);
|
|
160
|
+
if (stat?.isFile())
|
|
161
|
+
files.add(rel);
|
|
162
|
+
else if (!stat)
|
|
163
|
+
deleted.add(rel);
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
files: Array.from(files).sort((a, b) => a.localeCompare(b)),
|
|
167
|
+
deleted: Array.from(deleted).sort((a, b) => a.localeCompare(b))
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Normalizes an explicit file selection: relative paths only, no `..`, with
|
|
172
|
+
* forward slashes and duplicates removed.
|
|
173
|
+
*/
|
|
174
|
+
export function normalizeDeploySelection(value) {
|
|
175
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
176
|
+
throw new Error("files must be a non-empty array of paths relative to localPath for method \"selection\"");
|
|
177
|
+
}
|
|
178
|
+
const out = new Set();
|
|
179
|
+
for (const item of value) {
|
|
180
|
+
const raw = String(item ?? "").trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
181
|
+
if (!raw)
|
|
182
|
+
throw new Error("files contains an empty path");
|
|
183
|
+
if (raw.includes("\0") || /[\r\n]/.test(raw))
|
|
184
|
+
throw new Error(`Invalid file path: ${item}`);
|
|
185
|
+
if (raw.startsWith("/") || /^[A-Za-z]:/.test(raw))
|
|
186
|
+
throw new Error(`files must be relative to localPath: ${item}`);
|
|
187
|
+
if (raw.split("/").some(segment => segment === ".."))
|
|
188
|
+
throw new Error(`files must not contain '..': ${item}`);
|
|
189
|
+
out.add(raw.replace(/\/+/g, "/"));
|
|
190
|
+
}
|
|
191
|
+
return Array.from(out).sort((a, b) => a.localeCompare(b));
|
|
192
|
+
}
|
package/dist/mcp/logger.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
|
+
import { REDACTED, isSecretKey, redactSecretsInText } from "../security/redact.js";
|
|
2
3
|
const levelOrder = {
|
|
3
4
|
error: 0,
|
|
4
5
|
warn: 1,
|
|
@@ -35,25 +36,19 @@ function safeWrite(line) {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
export function redact(value) {
|
|
38
|
-
const secretKeys = ["password", "pass", "token", "secret", "key", "privatekey"];
|
|
39
39
|
if (Array.isArray(value)) {
|
|
40
40
|
return value.map(redact);
|
|
41
41
|
}
|
|
42
42
|
if (value && typeof value === "object") {
|
|
43
43
|
const out = {};
|
|
44
44
|
for (const [k, v] of Object.entries(value)) {
|
|
45
|
-
|
|
46
|
-
if (secretKeys.some(s => lower.includes(s))) {
|
|
47
|
-
out[k] = "***";
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
out[k] = redact(v);
|
|
51
|
-
}
|
|
45
|
+
out[k] = isSecretKey(k) ? REDACTED : redact(v);
|
|
52
46
|
}
|
|
53
47
|
return out;
|
|
54
48
|
}
|
|
55
|
-
if (typeof value === "string"
|
|
56
|
-
|
|
49
|
+
if (typeof value === "string") {
|
|
50
|
+
const clean = redactSecretsInText(value);
|
|
51
|
+
return clean.length > 500 ? clean.substring(0, 500) + "...(truncated)" : clean;
|
|
57
52
|
}
|
|
58
53
|
return value;
|
|
59
54
|
}
|