machine-bridge-mcp 0.11.0 → 0.12.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.
@@ -0,0 +1,199 @@
1
+ import path from "node:path";
2
+ import process from "node:process";
3
+ import {
4
+ acquireDaemonLock,
5
+ daemonLockPathForState,
6
+ readDaemonLockOwner,
7
+ resolveWorkspace,
8
+ } from "./state.mjs";
9
+ import { inspectProcessInstance, isPidAlive, processCommandLine, splitProcessCommandLine } from "./process-identity.mjs";
10
+
11
+ const DEFAULT_TAKEOVER_TIMEOUT_MS = 15_000;
12
+ const DEFAULT_TAKEOVER_POLL_MS = 100;
13
+
14
+ export async function acquireDaemonLockWithTakeover(state, options = {}) {
15
+ const ownerMetadata = options.ownerMetadata || {};
16
+ let lock = acquireDaemonLock(state, ownerMetadata);
17
+ if (lock.acquired || !options.takeOverServiceOwner) return lock;
18
+
19
+ const stopped = await stopWorkspaceServiceDaemon(state, {
20
+ owner: lock.owner,
21
+ timeoutMs: options.timeoutMs,
22
+ pollMs: options.pollMs,
23
+ logger: options.logger,
24
+ reason: "foreground startup",
25
+ });
26
+ if (!stopped.verified_service_daemon) return lock;
27
+ if (!stopped.ok) {
28
+ const pid = stopped.pid ? `pid ${stopped.pid}` : "unknown pid";
29
+ throw new Error(`background daemon did not release the workspace within ${Math.ceil(stopped.timeout_ms / 1000)} seconds (${pid}); run \`machine-mcp service stop\`, verify \`machine-mcp service status\`, and retry`);
30
+ }
31
+
32
+ lock = acquireDaemonLock(state, ownerMetadata);
33
+ if (lock.acquired) options.logger?.info?.("background daemon stopped; foreground startup is taking over the workspace");
34
+ return lock;
35
+ }
36
+
37
+ export async function stopWorkspaceServiceDaemon(state, options = {}) {
38
+ const timeoutMs = boundedPositiveInt(options.timeoutMs, DEFAULT_TAKEOVER_TIMEOUT_MS);
39
+ const pollMs = boundedPositiveInt(options.pollMs, DEFAULT_TAKEOVER_POLL_MS);
40
+ const logger = options.logger || { info() {}, warn() {} };
41
+ const deadline = Date.now() + timeoutMs;
42
+ const signalled = new Set();
43
+ let owner = options.owner || readDaemonLockOwner(daemonLockPathForState(state));
44
+ let verified = false;
45
+ let lastOwner = owner;
46
+
47
+ while (true) {
48
+ if (owner?.pid && isPidAlive(owner.pid)) {
49
+ lastOwner = owner;
50
+ const identity = inspectWorkspaceDaemonOwner(state, owner);
51
+ if (!identity.verified_service_daemon) {
52
+ return {
53
+ ok: false,
54
+ found: true,
55
+ verified_service_daemon: false,
56
+ reason: identity.reason,
57
+ timeout_ms: timeoutMs,
58
+ ...publicDaemonOwner(owner),
59
+ };
60
+ }
61
+ verified = true;
62
+ if (!signalled.has(Number(owner.pid))) {
63
+ const purpose = options.reason || "service stop";
64
+ logger.info?.(`stopping detached background daemon (pid ${owner.pid}) for ${purpose}`);
65
+ try {
66
+ process.kill(Number(owner.pid), "SIGTERM");
67
+ } catch (error) {
68
+ if (error?.code !== "ESRCH") {
69
+ return {
70
+ ok: false,
71
+ found: true,
72
+ verified_service_daemon: true,
73
+ reason: "signal_failed",
74
+ timeout_ms: timeoutMs,
75
+ ...publicDaemonOwner(owner),
76
+ };
77
+ }
78
+ }
79
+ signalled.add(Number(owner.pid));
80
+ }
81
+ }
82
+
83
+ const liveSignalled = [...signalled].filter((pid) => isPidAlive(pid));
84
+ const currentOwnerAlive = Boolean(owner?.pid && isPidAlive(owner.pid));
85
+ if (!currentOwnerAlive && liveSignalled.length === 0) break;
86
+
87
+ if (Date.now() >= deadline) {
88
+ logger.warn?.(`detached background daemon did not stop within ${Math.ceil(timeoutMs / 1000)} seconds`);
89
+ const remainingPid = Number(owner?.pid) || liveSignalled[0] || null;
90
+ return {
91
+ ok: false,
92
+ found: true,
93
+ stopped: false,
94
+ verified_service_daemon: verified,
95
+ reason: "timeout",
96
+ timeout_ms: timeoutMs,
97
+ pid: remainingPid,
98
+ mode: publicDaemonMode(lastOwner),
99
+ version: typeof lastOwner?.version === "string" ? lastOwner.version : "unknown",
100
+ };
101
+ }
102
+ await sleep(Math.min(pollMs, Math.max(1, deadline - Date.now())));
103
+ owner = readDaemonLockOwner(daemonLockPathForState(state));
104
+ }
105
+
106
+ // Reclaim a stale lock only through the normal token-aware primitive.
107
+ const staleCleanup = acquireDaemonLock(state, { mode: "service" });
108
+ if (staleCleanup.acquired) staleCleanup.release();
109
+ return {
110
+ ok: true,
111
+ found: verified,
112
+ stopped: verified,
113
+ verified_service_daemon: verified,
114
+ reason: verified ? "stopped" : "not_running",
115
+ timeout_ms: timeoutMs,
116
+ ...(lastOwner ? publicDaemonOwner(lastOwner) : {}),
117
+ };
118
+ }
119
+
120
+ export function inspectWorkspaceDaemon(state) {
121
+ const owner = readDaemonLockOwner(daemonLockPathForState(state));
122
+ if (!owner) return { present: false, alive: false, verified_service_daemon: false };
123
+ const alive = Boolean(owner.pid && isPidAlive(owner.pid));
124
+ const identity = alive
125
+ ? inspectWorkspaceDaemonOwner(state, owner)
126
+ : { verified_service_daemon: false, reason: "stale_lock" };
127
+ return {
128
+ present: true,
129
+ alive,
130
+ verified_service_daemon: identity.verified_service_daemon,
131
+ identity_reason: identity.reason,
132
+ ...publicDaemonOwner(owner),
133
+ };
134
+ }
135
+
136
+ function inspectWorkspaceDaemonOwner(state, owner) {
137
+ if (!owner || owner.purpose !== "daemon") return { verified_service_daemon: false, reason: "invalid_lock_owner" };
138
+ const pid = Number(owner.pid);
139
+ if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid) return { verified_service_daemon: false, reason: "invalid_pid" };
140
+ const processIdentity = inspectProcessInstance(owner);
141
+ if (!processIdentity.current) return { verified_service_daemon: false, reason: processIdentity.reason };
142
+ if (!sameCanonicalPath(owner.workspace, state.workspace.path)) return { verified_service_daemon: false, reason: "workspace_mismatch" };
143
+ if (owner.mode === "foreground") return { verified_service_daemon: false, reason: "foreground_daemon" };
144
+
145
+ const command = processCommandLine(pid);
146
+ if (!command) {
147
+ return { verified_service_daemon: false, reason: owner.mode === "service" ? "service_identity_unavailable" : "legacy_identity_unavailable" };
148
+ }
149
+ const argv = splitProcessCommandLine(command);
150
+ const entryName = path.basename(String(owner.entryScript || "machine-mcp"));
151
+ const workspaceArg = commandFlagValue(argv, "--workspace");
152
+ const stateRootArg = commandFlagValue(argv, "--state-dir");
153
+ const entryMatches = argv.some((value) => {
154
+ const name = path.basename(value);
155
+ return name === entryName || name === "machine-mcp" || name === "machine-mcp.mjs";
156
+ });
157
+ const matches = argv.includes("--daemon-only")
158
+ && sameCanonicalPath(workspaceArg, state.workspace.path)
159
+ && sameCanonicalPath(stateRootArg, state.paths.stateRoot)
160
+ && entryMatches;
161
+ return { verified_service_daemon: matches, reason: matches ? "service_command" : "command_mismatch" };
162
+ }
163
+
164
+ function commandFlagValue(argv, name) {
165
+ const index = argv.indexOf(name);
166
+ return index >= 0 && index + 1 < argv.length ? argv[index + 1] : "";
167
+ }
168
+
169
+ function sameCanonicalPath(left, right) {
170
+ if (typeof left !== "string" || typeof right !== "string") return false;
171
+ try {
172
+ const a = resolveWorkspace(left);
173
+ const b = resolveWorkspace(right);
174
+ return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
175
+ } catch {
176
+ return false;
177
+ }
178
+ }
179
+
180
+ function publicDaemonOwner(owner) {
181
+ return {
182
+ pid: Number(owner?.pid) || null,
183
+ mode: publicDaemonMode(owner),
184
+ version: typeof owner?.version === "string" ? owner.version : "unknown",
185
+ };
186
+ }
187
+
188
+ function publicDaemonMode(owner) {
189
+ return owner?.mode === "service" || owner?.mode === "foreground" ? owner.mode : "legacy";
190
+ }
191
+
192
+ function boundedPositiveInt(value, fallback) {
193
+ const parsed = Number(value);
194
+ return Number.isFinite(parsed) ? Math.max(1, parsed) : fallback;
195
+ }
196
+
197
+ function sleep(ms) {
198
+ return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
199
+ }
@@ -0,0 +1,94 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import {
3
+ chmodSync,
4
+ closeSync,
5
+ fsyncSync,
6
+ linkSync,
7
+ lstatSync,
8
+ openSync,
9
+ unlinkSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import { basename, dirname, join } from "node:path";
13
+ import { replaceFileSync } from "./atomic-fs.mjs";
14
+ import { readBoundedRegularFileSync } from "./secure-file.mjs";
15
+
16
+ export function createExclusiveFileSync(target, content, options = {}) {
17
+ const mode = Number.isInteger(options.mode) ? options.mode : 0o600;
18
+ const temporary = join(dirname(target), `.${basename(target)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
19
+ let fd;
20
+ let linked = false;
21
+ try {
22
+ fd = openSync(temporary, "wx", mode);
23
+ writeFileSync(fd, content);
24
+ fsyncSync(fd);
25
+ closeSync(fd);
26
+ fd = undefined;
27
+ linkSync(temporary, target);
28
+ linked = true;
29
+ try { chmodSync(target, mode); } catch {}
30
+ return { created: true, path: target };
31
+ } finally {
32
+ if (fd !== undefined) try { closeSync(fd); } catch {}
33
+ try { unlinkSync(temporary); } catch {}
34
+ if (!linked && options.cleanupTargetOnFailure === true) {
35
+ try { unlinkSync(target); } catch {}
36
+ }
37
+ }
38
+ }
39
+
40
+ export function replaceFileAtomicallySync(target, content, options = {}) {
41
+ const mode = Number.isInteger(options.mode) ? options.mode : 0o600;
42
+ const temporary = join(dirname(target), `.${basename(target)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
43
+ let fd;
44
+ try {
45
+ fd = openSync(temporary, "wx", mode);
46
+ writeFileSync(fd, content);
47
+ fsyncSync(fd);
48
+ closeSync(fd);
49
+ fd = undefined;
50
+ replaceFileSync(temporary, target);
51
+ try { chmodSync(target, mode); } catch {}
52
+ return { replaced: true, path: target };
53
+ } catch (error) {
54
+ if (fd !== undefined) try { closeSync(fd); } catch {}
55
+ try { unlinkSync(temporary); } catch {}
56
+ throw error;
57
+ }
58
+ }
59
+
60
+ export function removeOwnedJsonFileSync(target, expected = {}, options = {}) {
61
+ const maxBytes = Number.isInteger(options.maxBytes) ? options.maxBytes : 4096;
62
+ const snapshot = ownedJsonSnapshot(target, maxBytes);
63
+ if (!snapshot || !matchesExpected(snapshot.value, expected)) return false;
64
+ let current;
65
+ try { current = lstatSync(target); } catch (error) { return error?.code === "ENOENT"; }
66
+ if (current.isSymbolicLink() || !current.isFile() || !sameIdentity(snapshot.info, current)) return false;
67
+ const currentSnapshot = ownedJsonSnapshot(target, maxBytes);
68
+ if (!currentSnapshot || !sameIdentity(snapshot.info, currentSnapshot.info) || !matchesExpected(currentSnapshot.value, expected)) return false;
69
+ try { unlinkSync(target); return true; } catch (error) { return error?.code === "ENOENT"; }
70
+ }
71
+
72
+ function ownedJsonSnapshot(target, maxBytes) {
73
+ let info;
74
+ try { info = lstatSync(target); } catch (error) { if (error?.code === "ENOENT") return null; throw error; }
75
+ if (info.isSymbolicLink() || !info.isFile()) return null;
76
+ try {
77
+ const value = JSON.parse(readBoundedRegularFileSync(target, maxBytes).toString("utf8"));
78
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
79
+ return { value, info };
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ function matchesExpected(value, expected) {
86
+ return Object.entries(expected).every(([key, expectedValue]) => value?.[key] === expectedValue);
87
+ }
88
+
89
+ function sameIdentity(left, right) {
90
+ return Number(left.dev) === Number(right.dev)
91
+ && Number(left.ino) === Number(right.ino)
92
+ && Number(left.size) === Number(right.size)
93
+ && Number(left.mtimeMs) === Number(right.mtimeMs);
94
+ }
@@ -1,10 +1,11 @@
1
1
  import { spawn } from "node:child_process";
2
- import { createHash, randomBytes } from "node:crypto";
3
- import { chmodSync, closeSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
4
- import { basename, dirname, join, resolve } from "node:path";
2
+ import { createHash } from "node:crypto";
3
+ import { chmodSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { basename, join, resolve } from "node:path";
5
5
  import { executionEnv } from "./shell.mjs";
6
6
  import { terminateProcessTree } from "./process-sessions.mjs";
7
- import { replaceFileSync } from "./atomic-fs.mjs";
7
+ import { createExclusiveFileSync, removeOwnedJsonFileSync, replaceFileAtomicallySync } from "./exclusive-file.mjs";
8
+ import { currentProcessStartTimeMs } from "./process-identity.mjs";
8
9
  import { readBoundedRegularFileSync } from "./secure-file.mjs";
9
10
 
10
11
  const RESOURCE_TOKEN = /\{\{resource:([a-z][a-z0-9._-]{0,63})\}\}/g;
@@ -22,6 +23,8 @@ if (!jobDirInput) throw new Error("--job-dir is required");
22
23
  const jobDir = resolve(jobDirInput);
23
24
  if (!JOB_ID.test(basename(jobDir))) throw new Error("--job-dir must name a managed job directory");
24
25
  const recover = options.recover === true;
26
+ const recoveryLockToken = typeof process.env.MBM_RECOVERY_LOCK_TOKEN === "string" ? process.env.MBM_RECOVERY_LOCK_TOKEN : "";
27
+ delete process.env.MBM_RECOVERY_LOCK_TOKEN;
25
28
  const planFile = join(jobDir, "plan.json");
26
29
  const statusFile = join(jobDir, "status.json");
27
30
  const resultFile = join(jobDir, "result.json");
@@ -30,6 +33,7 @@ const runtimeDir = join(jobDir, "runtime");
30
33
  const resourcesDir = join(runtimeDir, "resources");
31
34
  const temporaryFilesDir = join(runtimeDir, "files");
32
35
  const runnerPidFile = join(jobDir, "runner.pid");
36
+ const RUNNER_PROCESS_STARTED_AT = new Date(currentProcessStartTimeMs()).toISOString();
33
37
 
34
38
  class JobCancelledError extends Error {
35
39
  constructor() {
@@ -49,7 +53,7 @@ for (const signal of ["SIGTERM", "SIGINT"]) {
49
53
 
50
54
  const initial = readJson(statusFile, MAX_STATUS_BYTES);
51
55
  assertLaunchState(initial);
52
- writeFileSync(runnerPidFile, `${process.pid}\n`, { mode: 0o600, flag: "wx" });
56
+ createExclusiveFileSync(runnerPidFile, `${JSON.stringify({ pid: process.pid, processStartedAt: RUNNER_PROCESS_STARTED_AT })}\n`, { mode: 0o600 });
53
57
  if (recover) rmSync(join(jobDir, "recovery.lock"), { force: true });
54
58
  try {
55
59
  const plan = readJson(planFile, 1024 * 1024);
@@ -59,10 +63,22 @@ try {
59
63
  recordFatalRunnerError(error);
60
64
  }
61
65
 
66
+ async function releaseRecoveryClaim() {
67
+ if (!/^[a-f0-9]{32}$/.test(recoveryLockToken)) throw new Error("recovery runner is missing its ownership token");
68
+ const file = join(jobDir, "recovery.lock");
69
+ const deadline = Date.now() + 5000;
70
+ while (Date.now() < deadline) {
71
+ if (removeOwnedJsonFileSync(file, { pid: process.pid, token: recoveryLockToken })) return;
72
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 10));
73
+ }
74
+ throw new Error("recovery runner could not verify ownership of the recovery lock");
75
+ }
76
+
62
77
  async function main(plan, initial) {
63
78
  const status = {
64
79
  ...initial,
65
80
  runner_pid: process.pid,
81
+ runner_process_started_at: RUNNER_PROCESS_STARTED_AT,
66
82
  started_at: initial.started_at || new Date().toISOString(),
67
83
  updated_at: new Date().toISOString(),
68
84
  status: recover ? "cleaning" : "running",
@@ -182,6 +198,7 @@ function recordFatalRunnerError(error) {
182
198
  current_phase: null,
183
199
  current_step: null,
184
200
  runner_pid: process.pid,
201
+ runner_process_started_at: RUNNER_PROCESS_STARTED_AT,
185
202
  updated_at: now,
186
203
  finished_at: now,
187
204
  error_class: result.error_class,
@@ -259,7 +276,6 @@ function spawnStep(argv, { cwd, env, input, timeoutMs, cancellationAware, captur
259
276
  timedOut = true;
260
277
  terminateProcessTree(child, "SIGTERM");
261
278
  killTimer = setTimeout(() => terminateProcessTree(child, "SIGKILL"), 2000);
262
- killTimer.unref?.();
263
279
  }, timeoutMs);
264
280
  timer.unref?.();
265
281
  const cancellationPoll = setInterval(() => {
@@ -300,12 +316,14 @@ function spawnStep(argv, { cwd, env, input, timeoutMs, cancellationAware, captur
300
316
  if (closed) return;
301
317
  closed = true;
302
318
  clearTimeout(timer);
303
- if (killTimer) clearTimeout(killTimer);
319
+ if (killTimer && !timedOut) clearTimeout(killTimer);
304
320
  clearInterval(cancellationPoll);
305
321
  activeChild = null;
306
322
  activeChildCancellationAware = false;
307
- if (cancellationEscalation) clearTimeout(cancellationEscalation);
308
- cancellationEscalation = null;
323
+ if (cancellationEscalation && !isCancellationRequested()) {
324
+ clearTimeout(cancellationEscalation);
325
+ cancellationEscalation = null;
326
+ }
309
327
  callback();
310
328
  }
311
329
  });
@@ -440,7 +458,7 @@ function redactOutput(buffer, context) {
440
458
  }
441
459
 
442
460
  function updateStatus(status, changes) {
443
- Object.assign(status, changes, { runner_pid: process.pid, updated_at: new Date().toISOString() });
461
+ Object.assign(status, changes, { runner_pid: process.pid, runner_process_started_at: RUNNER_PROCESS_STARTED_AT, updated_at: new Date().toISOString() });
444
462
  writeJson(statusFile, status, MAX_STATUS_BYTES);
445
463
  }
446
464
 
@@ -451,9 +469,9 @@ function requestCancellation() {
451
469
  terminateProcessTree(child, "SIGTERM");
452
470
  if (cancellationEscalation) return;
453
471
  cancellationEscalation = setTimeout(() => {
454
- if (activeChild === child && activeChildCancellationAware) terminateProcessTree(child, "SIGKILL");
472
+ terminateProcessTree(child, "SIGKILL");
473
+ cancellationEscalation = null;
455
474
  }, 2000);
456
- cancellationEscalation.unref?.();
457
475
  }
458
476
 
459
477
  function isCancellationRequested() {
@@ -474,12 +492,10 @@ function appendLimited(current, chunk, limit, budget) {
474
492
  }
475
493
 
476
494
  function writeJson(file, value, maxBytes) {
477
- const text = `${JSON.stringify(value, null, 2)}\n`;
495
+ const text = `${JSON.stringify(value, null, 2)}
496
+ `;
478
497
  if (Buffer.byteLength(text) > maxBytes) throw new Error(`job JSON exceeds ${maxBytes} bytes`);
479
- const temp = `${file}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
480
- writeFileSync(temp, text, { mode: 0o600, flag: "wx" });
481
- replaceFileSync(temp, file);
482
- chmodSync(file, 0o600);
498
+ replaceFileAtomicallySync(file, text, { mode: 0o600 });
483
499
  }
484
500
 
485
501
  function readJson(file, maxBytes) {
package/src/local/log.mjs CHANGED
@@ -22,8 +22,15 @@ const BEARER_VALUE = /\bBearer\s+[A-Za-z0-9._~+\/-]+=*\b/gi;
22
22
  const EMAIL_VALUE = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
23
23
  const AWS_ACCESS_KEY = /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g;
24
24
  const GITHUB_TOKEN = /\bgh[pousr]_[A-Za-z0-9_]{30,}\b/g;
25
+ const GITLAB_TOKEN = /\bglpat-[A-Za-z0-9_-]{20,}\b/g;
26
+ const NPM_TOKEN = /\bnpm_[A-Za-z0-9]{30,}\b/g;
27
+ const SLACK_TOKEN = /\bxox[aboprs]-[A-Za-z0-9-]{10,}\b/g;
28
+ const GOOGLE_API_KEY = /\bAIza[A-Za-z0-9_-]{30,}\b/g;
29
+ const PAYMENT_API_KEY = /\b(?:sk|rk|pk)_live_[A-Za-z0-9]{16,}\b/g;
30
+ const JWT_VALUE = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g;
31
+ const URL_CREDENTIALS = /https?:\/\/[^\s/@:"'<>]+:[^\s/@"'<>]+@[^\s/"'<>]+/gi;
25
32
  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;
33
+ const PRIVATE_KEY_HEADER = /-----BEGIN\s+(?:(?:OPENSSH|RSA|EC|DSA)\s+|ENCRYPTED\s+)?PRIVATE\s+KEY-----/g;
27
34
  const HOME_PATHS = [...new Set([process.env.HOME, process.env.USERPROFILE, safeHomeDirectory()].filter(value => typeof value === "string" && value.length > 1))]
28
35
  .sort((left, right) => right.length - left.length);
29
36
 
@@ -54,6 +61,7 @@ export function createLogger(options = {}) {
54
61
  error(message, fields) { write(process.stderr, "error", "[error]", COLORS.red, message, fields); },
55
62
  debug(message, fields) { write(process.stderr, "debug", "[debug]", COLORS.gray, message, fields); },
56
63
  plain(message = "") { if (!quiet) process.stdout.write(`${String(message)}\n`); },
64
+ safePlain(message = "") { if (!quiet) process.stdout.write(`${sanitizeLogText(message, MAX_LOG_MESSAGE_CHARS)}\n`); },
57
65
  json(value) { process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); },
58
66
  };
59
67
  }
@@ -123,6 +131,13 @@ export function sanitizeLogText(value, maxChars = MAX_LOG_MESSAGE_CHARS) {
123
131
  .replace(BEARER_VALUE, "Bearer <redacted>")
124
132
  .replace(AWS_ACCESS_KEY, "<redacted-cloud-key>")
125
133
  .replace(GITHUB_TOKEN, "<redacted-access-token>")
134
+ .replace(GITLAB_TOKEN, "<redacted-access-token>")
135
+ .replace(NPM_TOKEN, "<redacted-access-token>")
136
+ .replace(SLACK_TOKEN, "<redacted-access-token>")
137
+ .replace(GOOGLE_API_KEY, "<redacted-cloud-key>")
138
+ .replace(PAYMENT_API_KEY, "<redacted-api-secret>")
139
+ .replace(JWT_VALUE, "<redacted-bearer-token>")
140
+ .replace(URL_CREDENTIALS, "<redacted-credential-url>")
126
141
  .replace(API_SECRET, "<redacted-api-secret>")
127
142
  .replace(PRIVATE_KEY_HEADER, "<redacted-private-key-header>")
128
143
  .replace(EMAIL_VALUE, "<redacted-email>");