machine-bridge-mcp 0.6.2 → 0.7.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/CHANGELOG.md +42 -0
- package/CONTRIBUTING.md +23 -0
- package/README.md +16 -13
- package/SECURITY.md +11 -7
- package/docs/ARCHITECTURE.md +6 -6
- package/docs/CLIENTS.md +2 -2
- package/docs/LOGGING.md +4 -0
- package/docs/MANAGED_JOBS.md +14 -13
- package/docs/OPERATIONS.md +9 -6
- package/docs/PRIVACY.md +37 -0
- package/docs/RELEASING.md +9 -4
- package/docs/TESTING.md +17 -1
- package/package.json +31 -8
- package/scripts/network-retry.mjs +47 -0
- package/scripts/privacy-check.mjs +177 -0
- package/scripts/release-impact-check.mjs +78 -0
- package/src/local/cli.mjs +54 -16
- package/src/local/daemon.mjs +49 -12
- package/src/local/job-runner.mjs +70 -13
- package/src/local/log.mjs +26 -2
- package/src/local/managed-jobs.mjs +163 -74
- package/src/local/process-sessions.mjs +5 -5
- package/src/local/service.mjs +128 -30
- package/src/local/shell.mjs +1 -1
- package/src/local/ssh-key.mjs +4 -2
- package/src/local/state.mjs +7 -5
- package/src/local/stdio.mjs +76 -16
- package/src/shared/server-metadata.json +1 -0
- package/src/shared/tool-catalog.json +5 -1
- package/src/worker/index.ts +35 -18
- package/wrangler.jsonc +1 -1
package/src/local/daemon.mjs
CHANGED
|
@@ -86,7 +86,7 @@ export class LocalDaemon {
|
|
|
86
86
|
protocol_version: MCP_PROTOCOL_VERSION,
|
|
87
87
|
supported_protocol_versions: MCP_SUPPORTED_PROTOCOL_VERSIONS,
|
|
88
88
|
workspace: this.displayPath(this.workspace),
|
|
89
|
-
workspace_name: basename(this.workspace),
|
|
89
|
+
workspace_name: this.policy.exposeAbsolutePaths ? basename(this.workspace) : "workspace",
|
|
90
90
|
policy: this.policy,
|
|
91
91
|
policy_contract: {
|
|
92
92
|
named_profile_is_canonical: this.policy.profile === "custom" || policyMatchesNamedProfile(this.policy),
|
|
@@ -99,6 +99,12 @@ export class LocalDaemon {
|
|
|
99
99
|
operating_system_permissions_apply: true,
|
|
100
100
|
host_policy_is_independent: true,
|
|
101
101
|
},
|
|
102
|
+
tool_delivery: {
|
|
103
|
+
full_profile_scope: "local-daemon-and-relay-advertisement",
|
|
104
|
+
daemon_advertised_tool_count: this.tools().length + 1,
|
|
105
|
+
host_exposed_tools_known_to_server: false,
|
|
106
|
+
host_may_expose_subset: true,
|
|
107
|
+
},
|
|
102
108
|
tools: ["server_info", ...this.tools()],
|
|
103
109
|
observability: {
|
|
104
110
|
per_tool_events: "debug-only",
|
|
@@ -144,7 +150,7 @@ export class LocalDaemon {
|
|
|
144
150
|
if (this.closed) return;
|
|
145
151
|
const wsUrl = `${this.workerUrl.replace(/^http/i, "ws")}/daemon/ws`;
|
|
146
152
|
this.logger.debug?.("connecting to remote relay", { endpoint: redactUrl(wsUrl) });
|
|
147
|
-
const socket = new WebSocket(wsUrl, { headers: { "X-Bridge-Token": this.secret } });
|
|
153
|
+
const socket = new WebSocket(wsUrl, { headers: { "X-Bridge-Token": this.secret }, maxPayload: MAX_WS_MESSAGE_BYTES });
|
|
148
154
|
this.ws = socket;
|
|
149
155
|
|
|
150
156
|
socket.on("open", () => {
|
|
@@ -266,7 +272,7 @@ export class LocalDaemon {
|
|
|
266
272
|
const durationMs = Date.now() - started;
|
|
267
273
|
this.logger.debug?.(durationMs >= SLOW_TOOL_CALL_MS ? "slow tool call completed" : "tool call completed", { call_id: shortCallId(id), tool, duration_ms: durationMs });
|
|
268
274
|
} catch (error) {
|
|
269
|
-
const safeError = this.safeErrorMessage(error);
|
|
275
|
+
const safeError = this.safeErrorMessage(error, argumentsValue);
|
|
270
276
|
this.send({ type: "tool_result", id, ok: false, error: { message: safeError } });
|
|
271
277
|
const durationMs = Date.now() - started;
|
|
272
278
|
this.logger.debug?.("tool call failed", { call_id: shortCallId(id), tool, duration_ms: durationMs, error_class: classifyOperationalError(error) });
|
|
@@ -339,7 +345,7 @@ export class LocalDaemon {
|
|
|
339
345
|
const git = await this.runProcess("git", ["-c", "core.fsmonitor=false", "-C", this.workspace, "rev-parse", "--show-toplevel"], 10_000, true, 512 * 1024, context);
|
|
340
346
|
return {
|
|
341
347
|
workspace: this.displayPath(this.workspace),
|
|
342
|
-
workspaceName: basename(this.workspace),
|
|
348
|
+
workspaceName: this.policy.exposeAbsolutePaths ? basename(this.workspace) : "workspace",
|
|
343
349
|
gitRoot: git.code === 0 ? this.displayPath(git.stdout.trim()) : "",
|
|
344
350
|
policy: this.policy,
|
|
345
351
|
tools: ["server_info", ...this.tools()],
|
|
@@ -348,7 +354,7 @@ export class LocalDaemon {
|
|
|
348
354
|
}
|
|
349
355
|
|
|
350
356
|
listRoots() {
|
|
351
|
-
const roots = [{ name: basename(this.workspace), path: this.displayPath(this.workspace), default: true }];
|
|
357
|
+
const roots = [{ name: this.policy.exposeAbsolutePaths ? basename(this.workspace) : "workspace", path: this.displayPath(this.workspace), default: true }];
|
|
352
358
|
if (this.policy.unrestrictedPaths) {
|
|
353
359
|
const home = process.env.HOME || process.env.USERPROFILE;
|
|
354
360
|
if (home && home !== this.workspace) roots.push({ name: "home", path: this.displayPath(resolve(home)), default: false });
|
|
@@ -757,18 +763,22 @@ export class LocalDaemon {
|
|
|
757
763
|
targetPath: target,
|
|
758
764
|
comment: args.comment || `machine-mcp:${args.name}`,
|
|
759
765
|
});
|
|
766
|
+
const exposePaths = args.expose_paths === true;
|
|
760
767
|
return {
|
|
761
768
|
name: key.name,
|
|
762
769
|
created: key.created,
|
|
763
770
|
registered: key.registered,
|
|
764
|
-
private_key_path: this.displayPath(key.privateKeyPath),
|
|
765
|
-
public_key_path: this.displayPath(key.publicKeyPath),
|
|
766
771
|
fingerprint: key.fingerprint,
|
|
767
772
|
key_type: key.keyType,
|
|
768
773
|
private_mode: key.privateMode,
|
|
769
774
|
public_mode: key.publicMode,
|
|
770
775
|
private_key_content_exposed: key.privateKeyContentExposed,
|
|
771
776
|
available_to_new_jobs_immediately: key.availableToNewJobsImmediately,
|
|
777
|
+
paths_exposed: exposePaths,
|
|
778
|
+
...(exposePaths ? {
|
|
779
|
+
private_key_path: resolve(key.privateKeyPath),
|
|
780
|
+
public_key_path: resolve(key.publicKeyPath),
|
|
781
|
+
} : {}),
|
|
772
782
|
};
|
|
773
783
|
}
|
|
774
784
|
|
|
@@ -831,7 +841,7 @@ export class LocalDaemon {
|
|
|
831
841
|
timer.unref?.();
|
|
832
842
|
const cleanup = () => {
|
|
833
843
|
clearTimeout(timer);
|
|
834
|
-
if (killTimer
|
|
844
|
+
if (killTimer) clearTimeout(killTimer);
|
|
835
845
|
this.activeProcesses.delete(child);
|
|
836
846
|
if (context.callId) {
|
|
837
847
|
const set = this.callProcesses.get(context.callId);
|
|
@@ -944,19 +954,25 @@ export class LocalDaemon {
|
|
|
944
954
|
|
|
945
955
|
displayPath(fullPath) {
|
|
946
956
|
const absolute = resolve(fullPath);
|
|
947
|
-
if (this.policy.exposeAbsolutePaths
|
|
948
|
-
assertContainedPath(this.workspace, absolute);
|
|
957
|
+
if (this.policy.exposeAbsolutePaths) return absolute;
|
|
949
958
|
const shown = relative(this.workspace, absolute);
|
|
950
|
-
|
|
959
|
+
const insideWorkspace = shown === "" || (!shown.startsWith(`..${sep}`) && shown !== ".." && !isAbsolute(shown));
|
|
960
|
+
if (insideWorkspace) return shown ? shown.split(sep).join("/") : ".";
|
|
961
|
+
return `<external-path:${sha256(absolute).slice(0, 12)}>`;
|
|
951
962
|
}
|
|
952
963
|
|
|
953
|
-
safeErrorMessage(error) {
|
|
964
|
+
safeErrorMessage(error, toolArgs = {}) {
|
|
954
965
|
let message = boundedErrorMessage(error);
|
|
955
966
|
if (!this.policy.exposeAbsolutePaths) {
|
|
956
967
|
for (const prefix of equivalentPathPrefixes(this.workspace, this.workspaceInput)) message = replacePathPrefix(message, prefix, ".");
|
|
957
968
|
for (const prefix of equivalentPathPrefixes(this.runtimeDir)) message = replacePathPrefix(message, prefix, "<runtime>");
|
|
958
969
|
const home = process.env.HOME || process.env.USERPROFILE;
|
|
959
970
|
if (home) message = replacePathPrefix(message, resolve(home), "<home>");
|
|
971
|
+
for (const candidate of collectToolPathCandidates(error, toolArgs, this.workspaceInput)) {
|
|
972
|
+
const absolute = isAbsolute(candidate) ? resolve(candidate) : resolve(this.workspaceInput, candidate);
|
|
973
|
+
const replacement = this.displayPath(absolute);
|
|
974
|
+
for (const prefix of equivalentPathPrefixes(candidate, absolute)) message = replacePathPrefix(message, prefix, replacement);
|
|
975
|
+
}
|
|
960
976
|
}
|
|
961
977
|
return message;
|
|
962
978
|
}
|
|
@@ -1202,6 +1218,27 @@ function isPlainRecord(value) {
|
|
|
1202
1218
|
}
|
|
1203
1219
|
|
|
1204
1220
|
|
|
1221
|
+
function collectToolPathCandidates(error, toolArgs, workspace) {
|
|
1222
|
+
const candidates = new Set();
|
|
1223
|
+
for (const value of [error?.path, error?.dest]) if (typeof value === "string" && value) candidates.add(value);
|
|
1224
|
+
const visit = (value, key = "", depth = 0) => {
|
|
1225
|
+
if (depth > 5 || value === null || value === undefined) return;
|
|
1226
|
+
if (typeof value === "string") {
|
|
1227
|
+
if (/(?:^|[_-])(?:path|cwd|workspace|root|directory|dir)(?:$|[_-])/i.test(key) && value && !value.includes("\0")) candidates.add(value);
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
if (Array.isArray(value)) {
|
|
1231
|
+
for (const item of value.slice(0, 64)) visit(item, key, depth + 1);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
if (typeof value !== "object") return;
|
|
1235
|
+
for (const [childKey, child] of Object.entries(value).slice(0, 128)) visit(child, childKey, depth + 1);
|
|
1236
|
+
};
|
|
1237
|
+
visit(toolArgs);
|
|
1238
|
+
candidates.delete(workspace);
|
|
1239
|
+
return [...candidates].sort((left, right) => right.length - left.length);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1205
1242
|
function equivalentPathPrefixes(...values) {
|
|
1206
1243
|
const prefixes = new Set(values.filter(Boolean).map((value) => String(value)));
|
|
1207
1244
|
for (const value of [...prefixes]) {
|
package/src/local/job-runner.mjs
CHANGED
|
@@ -13,10 +13,13 @@ const MAX_OUTPUT_BYTES = 64 * 1024;
|
|
|
13
13
|
const MAX_JOB_CAPTURE_BYTES = 256 * 1024;
|
|
14
14
|
const MAX_RESULT_BYTES = 4 * 1024 * 1024;
|
|
15
15
|
const MAX_STATUS_BYTES = 256 * 1024;
|
|
16
|
+
const JOB_ID = /^job_[A-Za-z0-9_-]{24,}$/;
|
|
16
17
|
|
|
17
18
|
const options = parseArgs(process.argv.slice(2));
|
|
18
|
-
const
|
|
19
|
-
if (!
|
|
19
|
+
const jobDirInput = typeof options.jobDir === "string" ? options.jobDir.trim() : "";
|
|
20
|
+
if (!jobDirInput) throw new Error("--job-dir is required");
|
|
21
|
+
const jobDir = resolve(jobDirInput);
|
|
22
|
+
if (!JOB_ID.test(basename(jobDir))) throw new Error("--job-dir must name a managed job directory");
|
|
20
23
|
const recover = options.recover === true;
|
|
21
24
|
const planFile = join(jobDir, "plan.json");
|
|
22
25
|
const statusFile = join(jobDir, "status.json");
|
|
@@ -43,17 +46,19 @@ for (const signal of ["SIGTERM", "SIGINT"]) {
|
|
|
43
46
|
process.on(signal, () => requestCancellation());
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
const initial = readJson(statusFile, MAX_STATUS_BYTES);
|
|
50
|
+
assertLaunchState(initial);
|
|
51
|
+
writeFileSync(runnerPidFile, `${process.pid}\n`, { mode: 0o600, flag: "wx" });
|
|
47
52
|
if (recover) rmSync(join(jobDir, "recovery.lock"), { force: true });
|
|
48
53
|
try {
|
|
49
|
-
|
|
54
|
+
const plan = readJson(planFile, 1024 * 1024);
|
|
55
|
+
assertPlanIntegrity(plan, initial);
|
|
56
|
+
await main(plan, initial);
|
|
50
57
|
} catch (error) {
|
|
51
58
|
recordFatalRunnerError(error);
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
async function main() {
|
|
55
|
-
const plan = readJson(planFile, 1024 * 1024);
|
|
56
|
-
const initial = readJson(statusFile, MAX_STATUS_BYTES);
|
|
61
|
+
async function main(plan, initial) {
|
|
57
62
|
const status = {
|
|
58
63
|
...initial,
|
|
59
64
|
runner_pid: process.pid,
|
|
@@ -137,6 +142,20 @@ async function main() {
|
|
|
137
142
|
try { rmSync(cancelFile, { force: true }); } catch {}
|
|
138
143
|
}
|
|
139
144
|
|
|
145
|
+
function assertLaunchState(status) {
|
|
146
|
+
if (!status || typeof status !== "object" || Array.isArray(status)) throw new Error("job status is unavailable or invalid");
|
|
147
|
+
const expected = recover ? "interrupted" : "queued";
|
|
148
|
+
if (status.status !== expected) throw new Error(`runner cannot start job in status: ${status.status}`);
|
|
149
|
+
if (status.approval === "pending-local-operator" || !status.approval) throw new Error("runner cannot start an unapproved managed job");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function assertPlanIntegrity(plan, status) {
|
|
153
|
+
if (!plan || typeof plan !== "object" || Array.isArray(plan)) throw new Error("job plan is unavailable or invalid");
|
|
154
|
+
const expected = String(status?.plan_sha256 || "");
|
|
155
|
+
const actual = createHash("sha256").update(JSON.stringify(plan)).digest("hex");
|
|
156
|
+
if (!/^[a-f0-9]{64}$/.test(expected) || actual !== expected) throw new Error("managed job plan integrity check failed");
|
|
157
|
+
}
|
|
158
|
+
|
|
140
159
|
function recordFatalRunnerError(error) {
|
|
141
160
|
rmSync(runtimeDir, { recursive: true, force: true });
|
|
142
161
|
const now = new Date().toISOString();
|
|
@@ -234,11 +253,12 @@ function spawnStep(argv, { cwd, env, input, timeoutMs, cancellationAware, captur
|
|
|
234
253
|
let stderrTruncated = 0;
|
|
235
254
|
let timedOut = false;
|
|
236
255
|
let closed = false;
|
|
256
|
+
let killTimer = null;
|
|
237
257
|
const timer = setTimeout(() => {
|
|
238
258
|
timedOut = true;
|
|
239
259
|
terminateProcessTree(child, "SIGTERM");
|
|
240
|
-
|
|
241
|
-
|
|
260
|
+
killTimer = setTimeout(() => terminateProcessTree(child, "SIGKILL"), 2000);
|
|
261
|
+
killTimer.unref?.();
|
|
242
262
|
}, timeoutMs);
|
|
243
263
|
timer.unref?.();
|
|
244
264
|
const cancellationPoll = setInterval(() => {
|
|
@@ -279,6 +299,7 @@ function spawnStep(argv, { cwd, env, input, timeoutMs, cancellationAware, captur
|
|
|
279
299
|
if (closed) return;
|
|
280
300
|
closed = true;
|
|
281
301
|
clearTimeout(timer);
|
|
302
|
+
if (killTimer) clearTimeout(killTimer);
|
|
282
303
|
clearInterval(cancellationPoll);
|
|
283
304
|
activeChild = null;
|
|
284
305
|
activeChildCancellationAware = false;
|
|
@@ -293,6 +314,7 @@ function materializeResources(resources) {
|
|
|
293
314
|
mkdirSync(resourcesDir, { recursive: true, mode: 0o700 });
|
|
294
315
|
chmodSync(resourcesDir, 0o700);
|
|
295
316
|
const paths = {};
|
|
317
|
+
const sourcePaths = {};
|
|
296
318
|
const bytes = {};
|
|
297
319
|
const redactions = {};
|
|
298
320
|
for (const [name, resource] of Object.entries(resources)) {
|
|
@@ -303,6 +325,10 @@ function materializeResources(resources) {
|
|
|
303
325
|
const target = join(resourcesDir, name);
|
|
304
326
|
writeFileSync(target, data, { mode: 0o600, flag: "wx" });
|
|
305
327
|
paths[name] = target;
|
|
328
|
+
sourcePaths[name] = [...new Set([
|
|
329
|
+
...resourcePathVariants(resource.path),
|
|
330
|
+
...(Array.isArray(resource.pathAliases) ? resource.pathAliases.flatMap(resourcePathVariants) : []),
|
|
331
|
+
])].sort((left, right) => right.length - left.length);
|
|
306
332
|
bytes[name] = data;
|
|
307
333
|
const patterns = [];
|
|
308
334
|
try {
|
|
@@ -316,7 +342,7 @@ function materializeResources(resources) {
|
|
|
316
342
|
}
|
|
317
343
|
redactions[name] = [...new Set(patterns.filter((value) => value.length > 0))].sort((a, b) => b.length - a.length);
|
|
318
344
|
}
|
|
319
|
-
return { paths, bytes, redactions };
|
|
345
|
+
return { paths, sourcePaths, bytes, redactions };
|
|
320
346
|
}
|
|
321
347
|
|
|
322
348
|
function materializeTemporaryFiles(files) {
|
|
@@ -366,15 +392,46 @@ function substitute(value, plan, context) {
|
|
|
366
392
|
});
|
|
367
393
|
}
|
|
368
394
|
|
|
395
|
+
function resourcePathVariants(value) {
|
|
396
|
+
const canonical = resolve(value);
|
|
397
|
+
const variants = new Set(pathTextVariants(canonical));
|
|
398
|
+
if (process.platform === "darwin" && canonical.startsWith("/private/")) {
|
|
399
|
+
for (const variant of pathTextVariants(canonical.slice("/private".length))) variants.add(variant);
|
|
400
|
+
}
|
|
401
|
+
return [...variants].sort((left, right) => right.length - left.length);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function pathTextVariants(value) {
|
|
405
|
+
const path = String(value);
|
|
406
|
+
return [...new Set([path, path.replaceAll("\\", "/"), path.replaceAll("/", "\\")])];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function replacePathText(text, value, replacement) {
|
|
410
|
+
let output = text;
|
|
411
|
+
for (const variant of pathTextVariants(value)) {
|
|
412
|
+
if (!variant) continue;
|
|
413
|
+
if (process.platform === "win32") output = output.replace(new RegExp(escapeRegExp(variant), "gi"), replacement);
|
|
414
|
+
else output = output.split(variant).join(replacement);
|
|
415
|
+
}
|
|
416
|
+
return output;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function escapeRegExp(value) {
|
|
420
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
421
|
+
}
|
|
422
|
+
|
|
369
423
|
function redactOutput(buffer, context) {
|
|
370
424
|
let text = new TextDecoder("utf-8").decode(buffer);
|
|
371
425
|
for (const [name, path] of Object.entries(context.paths)) {
|
|
372
|
-
text = text
|
|
426
|
+
text = replacePathText(text, path, `<resource:${name}>`);
|
|
427
|
+
}
|
|
428
|
+
for (const [name, paths] of Object.entries(context.sourcePaths || {})) {
|
|
429
|
+
for (const path of paths) text = replacePathText(text, path, `<resource-source:${name}>`);
|
|
373
430
|
}
|
|
374
431
|
for (const [name, path] of Object.entries(context.temporaryPaths)) {
|
|
375
|
-
text = text
|
|
432
|
+
text = replacePathText(text, path, `<temp:${name}>`);
|
|
376
433
|
}
|
|
377
|
-
text = text
|
|
434
|
+
text = replacePathText(text, runtimeDir, "<job-runtime>");
|
|
378
435
|
for (const [name, patterns] of Object.entries(context.redactions)) {
|
|
379
436
|
for (const value of patterns) text = text.split(value).join(`<redacted-resource:${name}>`);
|
|
380
437
|
}
|
package/src/local/log.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
|
+
import os from "node:os";
|
|
2
3
|
|
|
3
4
|
const COLORS = {
|
|
4
5
|
reset: "\x1b[0m",
|
|
@@ -15,8 +16,16 @@ const MAX_LOG_ARRAY_ITEMS = 32;
|
|
|
15
16
|
const MAX_LOG_OBJECT_KEYS = 48;
|
|
16
17
|
const LEVEL_RANK = Object.freeze({ debug: 10, info: 20, success: 20, warn: 30, error: 40 });
|
|
17
18
|
const SENSITIVE_KEY = /(authorization|cookie|password|passwd|secret|token|api[_-]?key|private[_-]?key|credential)/i;
|
|
19
|
+
const LOCAL_PATH_KEY = /(path|paths|cwd|workspace|directory|(?:^|[_-])dir(?:$|[_-])|root|home)/i;
|
|
18
20
|
const SECRET_VALUE = /\b(?:mcp_password|daemon_secret|token_version|mcp_at|mcp_code)_[A-Za-z0-9_-]+\b/g;
|
|
19
21
|
const BEARER_VALUE = /\bBearer\s+[A-Za-z0-9._~+\/-]+=*\b/gi;
|
|
22
|
+
const EMAIL_VALUE = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
|
|
23
|
+
const AWS_ACCESS_KEY = /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g;
|
|
24
|
+
const GITHUB_TOKEN = /\bgh[pousr]_[A-Za-z0-9_]{30,}\b/g;
|
|
25
|
+
const API_SECRET = /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g;
|
|
26
|
+
const PRIVATE_KEY_HEADER = /-----BEGIN\s+(?:OPENSSH|RSA|EC|DSA)\s+PRIVATE\s+KEY-----/g;
|
|
27
|
+
const HOME_PATHS = [...new Set([process.env.HOME, process.env.USERPROFILE, safeHomeDirectory()].filter(value => typeof value === "string" && value.length > 1))]
|
|
28
|
+
.sort((left, right) => right.length - left.length);
|
|
20
29
|
|
|
21
30
|
export function createLogger(options = {}) {
|
|
22
31
|
const quiet = Boolean(options.quiet);
|
|
@@ -86,8 +95,9 @@ export function formatFields(fields) {
|
|
|
86
95
|
}
|
|
87
96
|
}
|
|
88
97
|
|
|
89
|
-
|
|
98
|
+
function sanitizeLogValue(value, key = "", seen = new WeakSet(), depth = 0) {
|
|
90
99
|
if (SENSITIVE_KEY.test(key)) return "<redacted>";
|
|
100
|
+
if (LOCAL_PATH_KEY.test(key)) return "<local-path>";
|
|
91
101
|
if (value instanceof Error) return sanitizeLogText(value.message || value.name);
|
|
92
102
|
if (typeof value === "string") return sanitizeLogText(value);
|
|
93
103
|
if (typeof value === "bigint") return value.toString();
|
|
@@ -106,9 +116,19 @@ export function sanitizeLogValue(value, key = "", seen = new WeakSet(), depth =
|
|
|
106
116
|
export function sanitizeLogText(value, maxChars = MAX_LOG_MESSAGE_CHARS) {
|
|
107
117
|
let raw;
|
|
108
118
|
try { raw = String(value ?? ""); } catch { raw = "<unprintable>"; }
|
|
109
|
-
|
|
119
|
+
let sanitized = raw
|
|
110
120
|
.replace(SECRET_VALUE, "<redacted-secret>")
|
|
111
121
|
.replace(BEARER_VALUE, "Bearer <redacted>")
|
|
122
|
+
.replace(AWS_ACCESS_KEY, "<redacted-cloud-key>")
|
|
123
|
+
.replace(GITHUB_TOKEN, "<redacted-access-token>")
|
|
124
|
+
.replace(API_SECRET, "<redacted-api-secret>")
|
|
125
|
+
.replace(PRIVATE_KEY_HEADER, "<redacted-private-key-header>")
|
|
126
|
+
.replace(EMAIL_VALUE, "<redacted-email>");
|
|
127
|
+
for (const home of HOME_PATHS) sanitized = sanitized.split(home).join("<home>");
|
|
128
|
+
sanitized = sanitized
|
|
129
|
+
.replace(/\/(?:Users|home)\/[^/\s"'<>]+(?=\/|$)/g, "<home>")
|
|
130
|
+
.replace(/\b[A-Za-z]:\\Users\\[^\\\s"'<>]+(?=\\|$)/g, "<home>")
|
|
131
|
+
.replace(/[\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]/g, "")
|
|
112
132
|
.replace(/[\r\n\t]/g, match => match === "\t" ? "\\t" : "\\n")
|
|
113
133
|
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "?");
|
|
114
134
|
if (!Number.isFinite(Number(maxChars)) || Number(maxChars) <= 0) return "";
|
|
@@ -116,6 +136,10 @@ export function sanitizeLogText(value, maxChars = MAX_LOG_MESSAGE_CHARS) {
|
|
|
116
136
|
return sanitized.length > limit ? `${sanitized.slice(0, limit - 1)}…` : sanitized;
|
|
117
137
|
}
|
|
118
138
|
|
|
139
|
+
function safeHomeDirectory() {
|
|
140
|
+
try { return os.homedir(); } catch { return ""; }
|
|
141
|
+
}
|
|
142
|
+
|
|
119
143
|
function shouldUseColor(options) {
|
|
120
144
|
if (options.color === false || process.env.NO_COLOR) return false;
|
|
121
145
|
if (options.color === true) return true;
|