|
1 | 1 | import assert from "node:assert/strict"; |
2 | 2 | import { spawn, spawnSync } from "node:child_process"; |
3 | | -import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; |
| 3 | +import { chmodSync, mkdirSync, mkdtempSync, readFileSync, statSync, writeFileSync } from "node:fs"; |
4 | 4 | import { tmpdir } from "node:os"; |
5 | 5 | import { join } from "node:path"; |
6 | 6 | import test from "node:test"; |
@@ -117,3 +117,48 @@ test("TUI /install rejects an Object.prototype name instead of crashing the pit" |
117 | 117 | assert.match(result.stdout, /unknown engine or tool "constructor"/); |
118 | 118 | assert.doesNotMatch(result.stderr, /TypeError/); |
119 | 119 | }); |
| 120 | + |
| 121 | +// The pit persists every line typed at the prompt to ~/.moshcode_history, and |
| 122 | +// the documented flows put secrets on those lines (`/mcp install <url> -H |
| 123 | +// "Authorization: Bearer …"`, `/secrets`, `/coinpay`, `!export TOKEN=…`). The |
| 124 | +// file must be owner-only, the way credentials.json already is. |
| 125 | +const posixMode = { skip: process.platform === "win32" ? "POSIX permission bits" : false }; |
| 126 | + |
| 127 | +function runTuiWithHome(home, input) { |
| 128 | + return new Promise((resolve, reject) => { |
| 129 | + const child = spawn(process.execPath, [BIN], { |
| 130 | + stdio: ["pipe", "pipe", "pipe"], |
| 131 | + env: { ...process.env, HOME: home, USERPROFILE: home }, |
| 132 | + }); |
| 133 | + let stdout = ""; |
| 134 | + let stderr = ""; |
| 135 | + child.stdout.on("data", (chunk) => { stdout += chunk; }); |
| 136 | + child.stderr.on("data", (chunk) => { stderr += chunk; }); |
| 137 | + child.on("error", reject); |
| 138 | + child.on("close", (status, signal) => resolve({ status, signal, stdout, stderr })); |
| 139 | + child.stdin.end(input); |
| 140 | + }); |
| 141 | +} |
| 142 | + |
| 143 | +test("TUI writes the command history owner-only", posixMode, async () => { |
| 144 | + const home = mkdtempSync(join(tmpdir(), "moshcode-history-")); |
| 145 | + |
| 146 | + const result = await runTuiWithHome(home, "/quit\n"); |
| 147 | + |
| 148 | + assert.equal(result.status, 0, result.stderr || result.stdout); |
| 149 | + const file = join(home, ".moshcode_history"); |
| 150 | + assert.equal(statSync(file).mode & 0o777, 0o600); |
| 151 | +}); |
| 152 | + |
| 153 | +test("TUI tightens a history file that was already world-readable", posixMode, async () => { |
| 154 | + const home = mkdtempSync(join(tmpdir(), "moshcode-history-")); |
| 155 | + const file = join(home, ".moshcode_history"); |
| 156 | + writeFileSync(file, "/mcp install https://mcp.example.com/sse -H \"Authorization: Bearer sk-live\"\n"); |
| 157 | + chmodSync(file, 0o644); |
| 158 | + |
| 159 | + const result = await runTuiWithHome(home, "/quit\n"); |
| 160 | + |
| 161 | + assert.equal(result.status, 0, result.stderr || result.stdout); |
| 162 | + assert.equal(statSync(file).mode & 0o777, 0o600); |
| 163 | + assert.match(readFileSync(file, "utf8"), /Bearer sk-live/); // history itself survives |
| 164 | +}); |
0 commit comments