@lifeaitools/clauth 0.4.1 → 0.5.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/.clauth-skill/SKILL.md +184 -184
- package/.clauth-skill/references/keys-guide.md +270 -270
- package/.clauth-skill/references/operator-guide.md +148 -148
- package/README.md +125 -125
- package/cli/api.js +113 -113
- package/cli/commands/install.js +291 -291
- package/cli/commands/scrub.js +231 -231
- package/cli/commands/serve.js +526 -1
- package/cli/commands/uninstall.js +164 -164
- package/cli/fingerprint.js +91 -91
- package/cli/index.js +5 -1
- package/install.ps1 +44 -44
- package/install.sh +38 -38
- package/package.json +54 -54
- package/scripts/bin/bootstrap-linux +0 -0
- package/scripts/bin/bootstrap-macos +0 -0
- package/scripts/bootstrap.cjs +43 -43
- package/scripts/build.sh +45 -45
- package/supabase/functions/auth-vault/index.ts +235 -235
- package/supabase/migrations/001_clauth_schema.sql +103 -103
- package/supabase/migrations/002_vault_helpers.sql +90 -90
- package/supabase/migrations/20260317_lockout.sql +26 -26
package/cli/commands/scrub.js
CHANGED
|
@@ -1,231 +1,231 @@
|
|
|
1
|
-
// cli/commands/scrub.js — Scrub credentials from Claude Code transcript logs
|
|
2
|
-
//
|
|
3
|
-
// clauth scrub → scrub active transcript (most recent .jsonl)
|
|
4
|
-
// clauth scrub <file> → scrub a specific file
|
|
5
|
-
// clauth scrub all → scrub every transcript .jsonl
|
|
6
|
-
// clauth scrub --force → rescrub even if already marked
|
|
7
|
-
|
|
8
|
-
import fs from "fs";
|
|
9
|
-
import path from "path";
|
|
10
|
-
import os from "os";
|
|
11
|
-
import chalk from "chalk";
|
|
12
|
-
import ora from "ora";
|
|
13
|
-
|
|
14
|
-
const SCRUB_MARKER = "[CLAUTH-SCRUBBED]";
|
|
15
|
-
const SCRUB_VERSION = "1.1";
|
|
16
|
-
|
|
17
|
-
// ──────────────────────────────────────────────
|
|
18
|
-
// Credential patterns — regex + replacement
|
|
19
|
-
// ──────────────────────────────────────────────
|
|
20
|
-
const PATTERNS = [
|
|
21
|
-
// Supabase JWTs (anon, service_role)
|
|
22
|
-
[/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
|
|
23
|
-
"[SUPABASE_JWT_REDACTED]"],
|
|
24
|
-
|
|
25
|
-
// Vercel tokens
|
|
26
|
-
[/vcp_[A-Za-z0-9]{20,80}/g,
|
|
27
|
-
"[VERCEL_TOKEN_REDACTED]"],
|
|
28
|
-
|
|
29
|
-
// R2 / S3 secret access keys (64-char hex)
|
|
30
|
-
[/"secret_access_key"\s*:\s*"[a-f0-9]{64}"/g,
|
|
31
|
-
'"secret_access_key": "[R2_SECRET_REDACTED]"'],
|
|
32
|
-
|
|
33
|
-
// R2 / S3 access key IDs (32-char hex)
|
|
34
|
-
[/"access_key_id"\s*:\s*"[a-f0-9]{32}"/g,
|
|
35
|
-
'"access_key_id": "[R2_KEY_REDACTED]"'],
|
|
36
|
-
|
|
37
|
-
// Cloudflare admin tokens
|
|
38
|
-
[/"admin_token"\s*:\s*"[A-Za-z0-9_-]{20,60}"/g,
|
|
39
|
-
'"admin_token": "[CF_TOKEN_REDACTED]"'],
|
|
40
|
-
|
|
41
|
-
// Cloudflare account IDs (32-char hex)
|
|
42
|
-
[/"account_id"\s*:\s*"[a-f0-9]{32}"/g,
|
|
43
|
-
'"account_id": "[CF_ACCOUNT_REDACTED]"'],
|
|
44
|
-
|
|
45
|
-
// GitHub tokens
|
|
46
|
-
[/ghp_[A-Za-z0-9]{36}/g,
|
|
47
|
-
"[GITHUB_TOKEN_REDACTED]"],
|
|
48
|
-
[/github_pat_[A-Za-z0-9_]{40,100}/g,
|
|
49
|
-
"[GITHUB_PAT_REDACTED]"],
|
|
50
|
-
|
|
51
|
-
// Neo4j connection strings with passwords
|
|
52
|
-
[/neo4j\+s?:\/\/[^"\\]+/g,
|
|
53
|
-
"[NEO4J_CONNSTRING_REDACTED]"],
|
|
54
|
-
|
|
55
|
-
// Generic Bearer tokens in Authorization headers
|
|
56
|
-
[/Bearer [A-Za-z0-9_-]{20,}/g,
|
|
57
|
-
"Bearer [TOKEN_REDACTED]"],
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
// ──────────────────────────────────────────────
|
|
61
|
-
// Marker check — read last 512 bytes
|
|
62
|
-
// ──────────────────────────────────────────────
|
|
63
|
-
function isAlreadyScrubbed(filePath) {
|
|
64
|
-
try {
|
|
65
|
-
const stat = fs.statSync(filePath);
|
|
66
|
-
const fd = fs.openSync(filePath, "r");
|
|
67
|
-
const bufSize = Math.min(512, stat.size);
|
|
68
|
-
const buf = Buffer.alloc(bufSize);
|
|
69
|
-
fs.readSync(fd, buf, 0, bufSize, Math.max(0, stat.size - bufSize));
|
|
70
|
-
fs.closeSync(fd);
|
|
71
|
-
|
|
72
|
-
const tail = buf.toString("utf-8");
|
|
73
|
-
const lastLine = tail.trim().split("\n").pop();
|
|
74
|
-
if (lastLine && lastLine.includes(SCRUB_MARKER)) {
|
|
75
|
-
try {
|
|
76
|
-
const marker = JSON.parse(lastLine);
|
|
77
|
-
return marker.version === SCRUB_VERSION;
|
|
78
|
-
} catch { return false; }
|
|
79
|
-
}
|
|
80
|
-
return false;
|
|
81
|
-
} catch { return false; }
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ──────────────────────────────────────────────
|
|
85
|
-
// Stamp file as scrubbed
|
|
86
|
-
// ──────────────────────────────────────────────
|
|
87
|
-
function stampScrubbed(filePath) {
|
|
88
|
-
const marker = JSON.stringify({
|
|
89
|
-
type: SCRUB_MARKER,
|
|
90
|
-
version: SCRUB_VERSION,
|
|
91
|
-
scrubbed_at: new Date().toISOString(),
|
|
92
|
-
patterns: PATTERNS.length,
|
|
93
|
-
});
|
|
94
|
-
fs.appendFileSync(filePath, "\n" + marker + "\n", "utf-8");
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// ──────────────────────────────────────────────
|
|
98
|
-
// Scrub a single file
|
|
99
|
-
// ──────────────────────────────────────────────
|
|
100
|
-
function scrubFile(filePath, force = false) {
|
|
101
|
-
if (!force && isAlreadyScrubbed(filePath)) {
|
|
102
|
-
return "skipped";
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
let content = fs.readFileSync(filePath, "utf-8");
|
|
106
|
-
let total = 0;
|
|
107
|
-
|
|
108
|
-
for (const [pattern, replacement] of PATTERNS) {
|
|
109
|
-
// Reset lastIndex for global regexes
|
|
110
|
-
pattern.lastIndex = 0;
|
|
111
|
-
const matches = content.match(pattern);
|
|
112
|
-
if (matches) {
|
|
113
|
-
total += matches.length;
|
|
114
|
-
content = content.replace(pattern, replacement);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (total > 0) {
|
|
119
|
-
fs.writeFileSync(filePath, content, "utf-8");
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Stamp as clean (whether creds found or not)
|
|
123
|
-
stampScrubbed(filePath);
|
|
124
|
-
return total;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ──────────────────────────────────────────────
|
|
128
|
-
// Find all transcript .jsonl files
|
|
129
|
-
// ──────────────────────────────────────────────
|
|
130
|
-
function findTranscripts() {
|
|
131
|
-
const claudeDir = path.join(os.homedir(), ".claude", "projects");
|
|
132
|
-
if (!fs.existsSync(claudeDir)) return [];
|
|
133
|
-
|
|
134
|
-
const results = [];
|
|
135
|
-
function walk(dir) {
|
|
136
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
137
|
-
const full = path.join(dir, entry.name);
|
|
138
|
-
if (entry.isDirectory()) walk(full);
|
|
139
|
-
else if (entry.name.endsWith(".jsonl")) results.push(full);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
walk(claudeDir);
|
|
143
|
-
return results;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ──────────────────────────────────────────────
|
|
147
|
-
// Find the most recent transcript (active session)
|
|
148
|
-
// ──────────────────────────────────────────────
|
|
149
|
-
function findMostRecent() {
|
|
150
|
-
const files = findTranscripts();
|
|
151
|
-
if (files.length === 0) return null;
|
|
152
|
-
|
|
153
|
-
let newest = files[0];
|
|
154
|
-
let newestMtime = fs.statSync(files[0]).mtimeMs;
|
|
155
|
-
for (const f of files.slice(1)) {
|
|
156
|
-
const mt = fs.statSync(f).mtimeMs;
|
|
157
|
-
if (mt > newestMtime) { newest = f; newestMtime = mt; }
|
|
158
|
-
}
|
|
159
|
-
return newest;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// ──────────────────────────────────────────────
|
|
163
|
-
// Exported runner
|
|
164
|
-
// ──────────────────────────────────────────────
|
|
165
|
-
export async function runScrub(target, opts = {}) {
|
|
166
|
-
const force = opts.force || false;
|
|
167
|
-
|
|
168
|
-
// Determine which files to scrub
|
|
169
|
-
let files;
|
|
170
|
-
if (target === "all") {
|
|
171
|
-
files = findTranscripts();
|
|
172
|
-
if (files.length === 0) {
|
|
173
|
-
console.log(chalk.yellow("\n No transcript files found.\n"));
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
console.log(chalk.cyan(`\n Scrubbing ${files.length} transcript file(s)...\n`));
|
|
177
|
-
} else if (target && target !== "all") {
|
|
178
|
-
// Specific file
|
|
179
|
-
const resolved = path.resolve(target);
|
|
180
|
-
if (!fs.existsSync(resolved)) {
|
|
181
|
-
console.log(chalk.red(`\n File not found: ${resolved}\n`));
|
|
182
|
-
process.exit(1);
|
|
183
|
-
}
|
|
184
|
-
files = [resolved];
|
|
185
|
-
} else {
|
|
186
|
-
// No target — find most recent
|
|
187
|
-
const recent = findMostRecent();
|
|
188
|
-
if (!recent) {
|
|
189
|
-
console.log(chalk.yellow("\n No transcript files found.\n"));
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
files = [recent];
|
|
193
|
-
console.log(chalk.gray(`\n Active transcript: ${path.basename(recent)}`));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Scrub
|
|
197
|
-
const spinner = ora("Scrubbing credentials...").start();
|
|
198
|
-
let grandTotal = 0;
|
|
199
|
-
let skipped = 0;
|
|
200
|
-
let scanned = 0;
|
|
201
|
-
|
|
202
|
-
for (const f of files) {
|
|
203
|
-
const result = scrubFile(f, force);
|
|
204
|
-
if (result === "skipped") {
|
|
205
|
-
skipped++;
|
|
206
|
-
} else {
|
|
207
|
-
scanned++;
|
|
208
|
-
if (result > 0) {
|
|
209
|
-
spinner.stop();
|
|
210
|
-
console.log(chalk.yellow(` ${result} redaction(s) in ${path.basename(f)}`));
|
|
211
|
-
spinner.start("Scrubbing credentials...");
|
|
212
|
-
grandTotal += result;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
spinner.stop();
|
|
218
|
-
|
|
219
|
-
// Summary
|
|
220
|
-
const parts = [];
|
|
221
|
-
if (scanned) parts.push(`scanned ${scanned}`);
|
|
222
|
-
if (skipped) parts.push(chalk.gray(`skipped ${skipped} (already clean)`));
|
|
223
|
-
if (grandTotal) parts.push(chalk.green(`${grandTotal} credential(s) scrubbed`));
|
|
224
|
-
else if (scanned) parts.push(chalk.green("no credentials found"));
|
|
225
|
-
|
|
226
|
-
console.log(`\n ${files.length} file(s): ${parts.join(", ")}.`);
|
|
227
|
-
if (skipped && !force) {
|
|
228
|
-
console.log(chalk.gray(" (use --force to rescan all files)"));
|
|
229
|
-
}
|
|
230
|
-
console.log();
|
|
231
|
-
}
|
|
1
|
+
// cli/commands/scrub.js — Scrub credentials from Claude Code transcript logs
|
|
2
|
+
//
|
|
3
|
+
// clauth scrub → scrub active transcript (most recent .jsonl)
|
|
4
|
+
// clauth scrub <file> → scrub a specific file
|
|
5
|
+
// clauth scrub all → scrub every transcript .jsonl
|
|
6
|
+
// clauth scrub --force → rescrub even if already marked
|
|
7
|
+
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import os from "os";
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
import ora from "ora";
|
|
13
|
+
|
|
14
|
+
const SCRUB_MARKER = "[CLAUTH-SCRUBBED]";
|
|
15
|
+
const SCRUB_VERSION = "1.1";
|
|
16
|
+
|
|
17
|
+
// ──────────────────────────────────────────────
|
|
18
|
+
// Credential patterns — regex + replacement
|
|
19
|
+
// ──────────────────────────────────────────────
|
|
20
|
+
const PATTERNS = [
|
|
21
|
+
// Supabase JWTs (anon, service_role)
|
|
22
|
+
[/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
|
|
23
|
+
"[SUPABASE_JWT_REDACTED]"],
|
|
24
|
+
|
|
25
|
+
// Vercel tokens
|
|
26
|
+
[/vcp_[A-Za-z0-9]{20,80}/g,
|
|
27
|
+
"[VERCEL_TOKEN_REDACTED]"],
|
|
28
|
+
|
|
29
|
+
// R2 / S3 secret access keys (64-char hex)
|
|
30
|
+
[/"secret_access_key"\s*:\s*"[a-f0-9]{64}"/g,
|
|
31
|
+
'"secret_access_key": "[R2_SECRET_REDACTED]"'],
|
|
32
|
+
|
|
33
|
+
// R2 / S3 access key IDs (32-char hex)
|
|
34
|
+
[/"access_key_id"\s*:\s*"[a-f0-9]{32}"/g,
|
|
35
|
+
'"access_key_id": "[R2_KEY_REDACTED]"'],
|
|
36
|
+
|
|
37
|
+
// Cloudflare admin tokens
|
|
38
|
+
[/"admin_token"\s*:\s*"[A-Za-z0-9_-]{20,60}"/g,
|
|
39
|
+
'"admin_token": "[CF_TOKEN_REDACTED]"'],
|
|
40
|
+
|
|
41
|
+
// Cloudflare account IDs (32-char hex)
|
|
42
|
+
[/"account_id"\s*:\s*"[a-f0-9]{32}"/g,
|
|
43
|
+
'"account_id": "[CF_ACCOUNT_REDACTED]"'],
|
|
44
|
+
|
|
45
|
+
// GitHub tokens
|
|
46
|
+
[/ghp_[A-Za-z0-9]{36}/g,
|
|
47
|
+
"[GITHUB_TOKEN_REDACTED]"],
|
|
48
|
+
[/github_pat_[A-Za-z0-9_]{40,100}/g,
|
|
49
|
+
"[GITHUB_PAT_REDACTED]"],
|
|
50
|
+
|
|
51
|
+
// Neo4j connection strings with passwords
|
|
52
|
+
[/neo4j\+s?:\/\/[^"\\]+/g,
|
|
53
|
+
"[NEO4J_CONNSTRING_REDACTED]"],
|
|
54
|
+
|
|
55
|
+
// Generic Bearer tokens in Authorization headers
|
|
56
|
+
[/Bearer [A-Za-z0-9_-]{20,}/g,
|
|
57
|
+
"Bearer [TOKEN_REDACTED]"],
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
// ──────────────────────────────────────────────
|
|
61
|
+
// Marker check — read last 512 bytes
|
|
62
|
+
// ──────────────────────────────────────────────
|
|
63
|
+
function isAlreadyScrubbed(filePath) {
|
|
64
|
+
try {
|
|
65
|
+
const stat = fs.statSync(filePath);
|
|
66
|
+
const fd = fs.openSync(filePath, "r");
|
|
67
|
+
const bufSize = Math.min(512, stat.size);
|
|
68
|
+
const buf = Buffer.alloc(bufSize);
|
|
69
|
+
fs.readSync(fd, buf, 0, bufSize, Math.max(0, stat.size - bufSize));
|
|
70
|
+
fs.closeSync(fd);
|
|
71
|
+
|
|
72
|
+
const tail = buf.toString("utf-8");
|
|
73
|
+
const lastLine = tail.trim().split("\n").pop();
|
|
74
|
+
if (lastLine && lastLine.includes(SCRUB_MARKER)) {
|
|
75
|
+
try {
|
|
76
|
+
const marker = JSON.parse(lastLine);
|
|
77
|
+
return marker.version === SCRUB_VERSION;
|
|
78
|
+
} catch { return false; }
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
} catch { return false; }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ──────────────────────────────────────────────
|
|
85
|
+
// Stamp file as scrubbed
|
|
86
|
+
// ──────────────────────────────────────────────
|
|
87
|
+
function stampScrubbed(filePath) {
|
|
88
|
+
const marker = JSON.stringify({
|
|
89
|
+
type: SCRUB_MARKER,
|
|
90
|
+
version: SCRUB_VERSION,
|
|
91
|
+
scrubbed_at: new Date().toISOString(),
|
|
92
|
+
patterns: PATTERNS.length,
|
|
93
|
+
});
|
|
94
|
+
fs.appendFileSync(filePath, "\n" + marker + "\n", "utf-8");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ──────────────────────────────────────────────
|
|
98
|
+
// Scrub a single file
|
|
99
|
+
// ──────────────────────────────────────────────
|
|
100
|
+
function scrubFile(filePath, force = false) {
|
|
101
|
+
if (!force && isAlreadyScrubbed(filePath)) {
|
|
102
|
+
return "skipped";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let content = fs.readFileSync(filePath, "utf-8");
|
|
106
|
+
let total = 0;
|
|
107
|
+
|
|
108
|
+
for (const [pattern, replacement] of PATTERNS) {
|
|
109
|
+
// Reset lastIndex for global regexes
|
|
110
|
+
pattern.lastIndex = 0;
|
|
111
|
+
const matches = content.match(pattern);
|
|
112
|
+
if (matches) {
|
|
113
|
+
total += matches.length;
|
|
114
|
+
content = content.replace(pattern, replacement);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (total > 0) {
|
|
119
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Stamp as clean (whether creds found or not)
|
|
123
|
+
stampScrubbed(filePath);
|
|
124
|
+
return total;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ──────────────────────────────────────────────
|
|
128
|
+
// Find all transcript .jsonl files
|
|
129
|
+
// ──────────────────────────────────────────────
|
|
130
|
+
function findTranscripts() {
|
|
131
|
+
const claudeDir = path.join(os.homedir(), ".claude", "projects");
|
|
132
|
+
if (!fs.existsSync(claudeDir)) return [];
|
|
133
|
+
|
|
134
|
+
const results = [];
|
|
135
|
+
function walk(dir) {
|
|
136
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
137
|
+
const full = path.join(dir, entry.name);
|
|
138
|
+
if (entry.isDirectory()) walk(full);
|
|
139
|
+
else if (entry.name.endsWith(".jsonl")) results.push(full);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
walk(claudeDir);
|
|
143
|
+
return results;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ──────────────────────────────────────────────
|
|
147
|
+
// Find the most recent transcript (active session)
|
|
148
|
+
// ──────────────────────────────────────────────
|
|
149
|
+
function findMostRecent() {
|
|
150
|
+
const files = findTranscripts();
|
|
151
|
+
if (files.length === 0) return null;
|
|
152
|
+
|
|
153
|
+
let newest = files[0];
|
|
154
|
+
let newestMtime = fs.statSync(files[0]).mtimeMs;
|
|
155
|
+
for (const f of files.slice(1)) {
|
|
156
|
+
const mt = fs.statSync(f).mtimeMs;
|
|
157
|
+
if (mt > newestMtime) { newest = f; newestMtime = mt; }
|
|
158
|
+
}
|
|
159
|
+
return newest;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ──────────────────────────────────────────────
|
|
163
|
+
// Exported runner
|
|
164
|
+
// ──────────────────────────────────────────────
|
|
165
|
+
export async function runScrub(target, opts = {}) {
|
|
166
|
+
const force = opts.force || false;
|
|
167
|
+
|
|
168
|
+
// Determine which files to scrub
|
|
169
|
+
let files;
|
|
170
|
+
if (target === "all") {
|
|
171
|
+
files = findTranscripts();
|
|
172
|
+
if (files.length === 0) {
|
|
173
|
+
console.log(chalk.yellow("\n No transcript files found.\n"));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
console.log(chalk.cyan(`\n Scrubbing ${files.length} transcript file(s)...\n`));
|
|
177
|
+
} else if (target && target !== "all") {
|
|
178
|
+
// Specific file
|
|
179
|
+
const resolved = path.resolve(target);
|
|
180
|
+
if (!fs.existsSync(resolved)) {
|
|
181
|
+
console.log(chalk.red(`\n File not found: ${resolved}\n`));
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
files = [resolved];
|
|
185
|
+
} else {
|
|
186
|
+
// No target — find most recent
|
|
187
|
+
const recent = findMostRecent();
|
|
188
|
+
if (!recent) {
|
|
189
|
+
console.log(chalk.yellow("\n No transcript files found.\n"));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
files = [recent];
|
|
193
|
+
console.log(chalk.gray(`\n Active transcript: ${path.basename(recent)}`));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Scrub
|
|
197
|
+
const spinner = ora("Scrubbing credentials...").start();
|
|
198
|
+
let grandTotal = 0;
|
|
199
|
+
let skipped = 0;
|
|
200
|
+
let scanned = 0;
|
|
201
|
+
|
|
202
|
+
for (const f of files) {
|
|
203
|
+
const result = scrubFile(f, force);
|
|
204
|
+
if (result === "skipped") {
|
|
205
|
+
skipped++;
|
|
206
|
+
} else {
|
|
207
|
+
scanned++;
|
|
208
|
+
if (result > 0) {
|
|
209
|
+
spinner.stop();
|
|
210
|
+
console.log(chalk.yellow(` ${result} redaction(s) in ${path.basename(f)}`));
|
|
211
|
+
spinner.start("Scrubbing credentials...");
|
|
212
|
+
grandTotal += result;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
spinner.stop();
|
|
218
|
+
|
|
219
|
+
// Summary
|
|
220
|
+
const parts = [];
|
|
221
|
+
if (scanned) parts.push(`scanned ${scanned}`);
|
|
222
|
+
if (skipped) parts.push(chalk.gray(`skipped ${skipped} (already clean)`));
|
|
223
|
+
if (grandTotal) parts.push(chalk.green(`${grandTotal} credential(s) scrubbed`));
|
|
224
|
+
else if (scanned) parts.push(chalk.green("no credentials found"));
|
|
225
|
+
|
|
226
|
+
console.log(`\n ${files.length} file(s): ${parts.join(", ")}.`);
|
|
227
|
+
if (skipped && !force) {
|
|
228
|
+
console.log(chalk.gray(" (use --force to rescan all files)"));
|
|
229
|
+
}
|
|
230
|
+
console.log();
|
|
231
|
+
}
|