@wonderwhy-er/desktop-commander
Version:
MCP server for terminal operations and file editing
77 lines (76 loc) • 3.23 kB
JavaScript
import fs from 'fs';
import os from 'os';
/**
* The one masker for the remote diagnostics: every line the device log keeps
* and everything `remote --report` writes from free text passes through here.
*
* Masks JWTs, `access_token` / `refresh_token` / `apikey` / `password` values
* after `=` or `:`, `Bearer …` values, emails, UUIDs (device and user ids),
* the home folder (→ `~`), the user name and the host name.
*/
const SECRET_KEYS = 'access_token|refresh_token|provider_token|provider_refresh_token|apikey|api_key|password|passwd|secret|client_secret';
/** `key=value`, `key: value`, `"key":"value"` — the key stays, the value goes. */
const KEY_VALUE = new RegExp(`(["']?)\\b(${SECRET_KEYS})\\1(\\s*[:=]\\s*)(["']?)[^"'\\s&,;}]+`, 'gi');
const BEARER = /\b(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi;
const JWT = /\beyJ[A-Za-z0-9_-]*(?:\.[A-Za-z0-9_-]*){0,2}/g;
const EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
const UUID = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
function escapeRegExp(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* The home folder as it may appear: as is and as its real path (on macOS a
* temp folder sits under the /var -> /private/var symlink, and execPath is the
* real one), each with forward slashes and JSON-escaped. Only a whole path
* matches: not "/backup/Users/x" nor "/Users/xy" for "/Users/x".
*/
function homePattern(home) {
if (home.length < 2)
return null;
const roots = [home];
try {
roots.push(fs.realpathSync.native(home));
}
catch { /* no such folder: the plain form only */ }
const forms = new Set(roots.flatMap((root) => [root, root.replace(/\\/g, '/'), root.replace(/\\/g, '\\\\')]));
// Longest first, so the JSON-escaped and real forms are not half-matched by a shorter one
const alternatives = [...forms].sort((a, b) => b.length - a.length).map(escapeRegExp);
const pathChar = '[A-Za-z0-9_.-]';
return new RegExp(`(?<!${pathChar})(?:${alternatives.join('|')})(?!${pathChar})`, process.platform === 'win32' ? 'gi' : 'g');
}
/** A name as a whole word: "alice" in "C:/x/alice/y", not in "aliceb". */
function wordPattern(word) {
if (word.length < 3)
return null;
return new RegExp(`(?<![A-Za-z0-9])${escapeRegExp(word)}(?![A-Za-z0-9])`, 'gi');
}
function safeUserName() {
try {
return os.userInfo().username;
}
catch {
return '';
}
}
export function redact(text) {
let out = text;
const home = homePattern(os.homedir());
if (home)
out = out.replace(home, '~');
out = out.replace(KEY_VALUE, '$1$2$1$3$4<redacted>');
out = out.replace(BEARER, '$1<redacted>');
out = out.replace(JWT, '<jwt>');
out = out.replace(EMAIL, '<email>');
out = out.replace(UUID, '<id>');
// "name.local" and the bare "name" a Mac also goes by
const hostname = os.hostname();
for (const name of new Set([hostname, hostname.split('.')[0]])) {
const host = wordPattern(name);
if (host)
out = out.replace(host, '<host>');
}
const user = wordPattern(safeUserName());
if (user)
out = out.replace(user, '<user>');
return out;
}