@solongate/proxy 0.58.0 → 0.59.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.
- package/dist/audit/index.js +0 -0
- package/dist/create.js +0 -0
- package/dist/global-install.js +1 -1
- package/dist/index.js +341 -52
- package/dist/inject.js +0 -0
- package/dist/login.js +5 -39
- package/dist/logs-server.d.ts +1 -0
- package/dist/logs-server.js +174 -0
- package/dist/pull-push.js +0 -0
- package/dist/shield.js +106 -7
- package/hooks/.solongate/.last-deny +1 -0
- package/hooks/.solongate/.last-eval +1 -0
- package/hooks/.solongate/.last-tool-call +1 -0
- package/hooks/audit.mjs +565 -544
- package/hooks/guard.bundled.mjs +21 -2
- package/hooks/guard.mjs +1626 -1604
- package/hooks/shield.mjs +271 -271
- package/hooks/stop.mjs +75 -75
- package/package.json +74 -74
package/dist/login.js
CHANGED
|
@@ -141,19 +141,6 @@ var SHIM_END = "# <<< SolonGate shield <<<";
|
|
|
141
141
|
function escapeRe(s) {
|
|
142
142
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
143
143
|
}
|
|
144
|
-
function resolveRealClaude() {
|
|
145
|
-
try {
|
|
146
|
-
const finder = process.platform === "win32" ? "where" : "which";
|
|
147
|
-
const out = execFileSync(finder, ["claude"], { encoding: "utf-8" }).split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
148
|
-
if (process.platform === "win32") {
|
|
149
|
-
const low = (s) => s.toLowerCase();
|
|
150
|
-
return out.find((l) => low(l).endsWith(".cmd")) || out.find((l) => low(l).endsWith(".exe")) || out.find((l) => low(l).endsWith(".bat")) || out[0] || null;
|
|
151
|
-
}
|
|
152
|
-
return out[0] || null;
|
|
153
|
-
} catch {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
144
|
function shimTargets() {
|
|
158
145
|
if (process.platform === "win32") {
|
|
159
146
|
try {
|
|
@@ -176,34 +163,13 @@ function writeShimBlock(file, block) {
|
|
|
176
163
|
mkdirSync(dirname(file), { recursive: true });
|
|
177
164
|
writeFileSync(file, content);
|
|
178
165
|
}
|
|
179
|
-
function
|
|
180
|
-
const
|
|
181
|
-
if (!real) {
|
|
182
|
-
console.log(" (Claude Code not found on PATH \u2014 skipped auto-shield. Install it, then re-run `login`.)");
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
const node = process.execPath.replace(/\\/g, "/");
|
|
186
|
-
const shield = shieldPath.replace(/\\/g, "/");
|
|
187
|
-
const win = process.platform === "win32";
|
|
188
|
-
const block = win ? `${SHIM_BEGIN}
|
|
189
|
-
function claude { & "${node}" "${shield}" -- "${real}" @args }
|
|
190
|
-
${SHIM_END}` : `${SHIM_BEGIN}
|
|
191
|
-
claude() { "${node}" "${shield}" -- "${real}" "$@"; }
|
|
192
|
-
${SHIM_END}`;
|
|
193
|
-
const targets = shimTargets();
|
|
194
|
-
if (targets.length === 0) {
|
|
195
|
-
console.log(" (No shell profile found for auto-shield; run `claude` via `solongate shield -- claude` manually.)");
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
for (const file of targets) {
|
|
166
|
+
function removeClaudeShim() {
|
|
167
|
+
for (const file of shimTargets()) {
|
|
199
168
|
try {
|
|
200
|
-
writeShimBlock(file,
|
|
201
|
-
|
|
202
|
-
} catch (e) {
|
|
203
|
-
console.log(` (Could not enable auto-shield in ${file}: ${e.message})`);
|
|
169
|
+
writeShimBlock(file, null);
|
|
170
|
+
} catch {
|
|
204
171
|
}
|
|
205
172
|
}
|
|
206
|
-
console.log(" Open a NEW terminal for it to take effect.");
|
|
207
173
|
}
|
|
208
174
|
async function runGlobalInstall(opts = {}) {
|
|
209
175
|
const p = globalPaths();
|
|
@@ -231,7 +197,7 @@ async function runGlobalInstall(opts = {}) {
|
|
|
231
197
|
writeFileSync(join(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
|
|
232
198
|
writeFileSync(join(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
|
|
233
199
|
console.log(` Installed hooks \u2192 ${p.hooksDir}`);
|
|
234
|
-
|
|
200
|
+
removeClaudeShim();
|
|
235
201
|
writeFileSync(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
|
|
236
202
|
console.log(` Wrote ${p.configPath}`);
|
|
237
203
|
let existing = {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runLogsServer(): Promise<void>;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// src/logs-server.ts
|
|
2
|
+
import { createServer } from "http";
|
|
3
|
+
import { readFileSync, statSync } from "fs";
|
|
4
|
+
import { resolve, join, isAbsolute } from "path";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { readdirSync } from "fs";
|
|
7
|
+
var LOG_FILENAME = "solongate-audit.jsonl";
|
|
8
|
+
var DEFAULT_PORT = 8788;
|
|
9
|
+
function allowedOrigins() {
|
|
10
|
+
const base = [
|
|
11
|
+
"https://dashboard.solongate.com",
|
|
12
|
+
"http://localhost:3000",
|
|
13
|
+
"http://localhost:3005",
|
|
14
|
+
"http://127.0.0.1:3000",
|
|
15
|
+
"http://127.0.0.1:3005"
|
|
16
|
+
];
|
|
17
|
+
const extra = (process.env.SOLONGATE_DASHBOARD_ORIGIN || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
18
|
+
return /* @__PURE__ */ new Set([...base, ...extra]);
|
|
19
|
+
}
|
|
20
|
+
function resolveLocalLogDir(rawPath) {
|
|
21
|
+
const dir = String(rawPath || "").trim().replace(/[\\/]+$/, "");
|
|
22
|
+
if (!dir) return null;
|
|
23
|
+
if (isAbsolute(dir)) return dir;
|
|
24
|
+
return resolve(homedir(), ".solongate", "local-logs");
|
|
25
|
+
}
|
|
26
|
+
async function findLogDir() {
|
|
27
|
+
const base = resolve(homedir(), ".solongate");
|
|
28
|
+
try {
|
|
29
|
+
const files = readdirSync(base).filter((f) => f.startsWith(".policy-cache-") && f.endsWith(".json"));
|
|
30
|
+
for (const f of files) {
|
|
31
|
+
try {
|
|
32
|
+
const c = JSON.parse(readFileSync(join(base, f), "utf-8"));
|
|
33
|
+
const p = c?.security?.localLogs?.path;
|
|
34
|
+
if (typeof p === "string" && p.trim()) return { dir: resolveLocalLogDir(p), configured: p };
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const cfgRaw = readFileSync(join(base, "cloud-guard.json"), "utf-8");
|
|
42
|
+
const { apiKey, apiUrl } = JSON.parse(cfgRaw);
|
|
43
|
+
if (apiKey) {
|
|
44
|
+
const url = `${apiUrl || "https://api.solongate.com"}/api/v1/policies/active`;
|
|
45
|
+
const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
46
|
+
if (res.ok) {
|
|
47
|
+
const body = await res.json();
|
|
48
|
+
const p = body?.security?.localLogs?.path;
|
|
49
|
+
if (typeof p === "string" && p.trim()) return { dir: resolveLocalLogDir(p), configured: p };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
return { dir: null, configured: null };
|
|
55
|
+
}
|
|
56
|
+
function setCors(req, res) {
|
|
57
|
+
const origin = req.headers.origin;
|
|
58
|
+
if (origin && allowedOrigins().has(origin)) {
|
|
59
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
60
|
+
res.setHeader("Vary", "Origin");
|
|
61
|
+
}
|
|
62
|
+
if (req.headers["access-control-request-private-network"] === "true") {
|
|
63
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
64
|
+
}
|
|
65
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
66
|
+
res.setHeader("Access-Control-Allow-Headers", "If-Modified-Since, Content-Type");
|
|
67
|
+
}
|
|
68
|
+
function fileInfo(dir) {
|
|
69
|
+
if (!dir) return { file: null, exists: false, size: 0, mtimeMs: 0 };
|
|
70
|
+
const file = join(dir, LOG_FILENAME);
|
|
71
|
+
try {
|
|
72
|
+
const st = statSync(file);
|
|
73
|
+
return { file, exists: true, size: st.size, mtimeMs: st.mtimeMs };
|
|
74
|
+
} catch {
|
|
75
|
+
return { file, exists: false, size: 0, mtimeMs: 0 };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function runLogsServer() {
|
|
79
|
+
const argv = process.argv.slice(3);
|
|
80
|
+
const portArg = argv[argv.indexOf("--port") + 1];
|
|
81
|
+
const port = Number(process.env.SOLONGATE_LOGS_PORT || (argv.includes("--port") ? portArg : "") || DEFAULT_PORT) || DEFAULT_PORT;
|
|
82
|
+
const server = createServer(async (req, res) => {
|
|
83
|
+
setCors(req, res);
|
|
84
|
+
if (req.method === "OPTIONS") {
|
|
85
|
+
res.writeHead(204);
|
|
86
|
+
res.end();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (req.method !== "GET") {
|
|
90
|
+
res.writeHead(405);
|
|
91
|
+
res.end();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const url = (req.url || "/").split("?")[0];
|
|
95
|
+
const { dir, configured } = await findLogDir();
|
|
96
|
+
if (url === "/health") {
|
|
97
|
+
const info = fileInfo(dir);
|
|
98
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
99
|
+
res.end(JSON.stringify({
|
|
100
|
+
ok: true,
|
|
101
|
+
agent: "solongate-logs-server",
|
|
102
|
+
configuredPath: configured,
|
|
103
|
+
resolvedDir: dir,
|
|
104
|
+
file: info.file,
|
|
105
|
+
exists: info.exists,
|
|
106
|
+
size: info.size,
|
|
107
|
+
mtime: info.mtimeMs ? new Date(info.mtimeMs).toISOString() : null
|
|
108
|
+
}));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (url === "/local-logs") {
|
|
112
|
+
const info = fileInfo(dir);
|
|
113
|
+
if (!dir) {
|
|
114
|
+
res.writeHead(200, { "Content-Type": "text/plain", "X-Solongate-Configured": "0" });
|
|
115
|
+
res.end("");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (!info.exists) {
|
|
119
|
+
res.writeHead(200, { "Content-Type": "text/plain", "X-Solongate-Exists": "0" });
|
|
120
|
+
res.end("");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const lastMod = new Date(info.mtimeMs).toUTCString();
|
|
124
|
+
const since = req.headers["if-modified-since"];
|
|
125
|
+
if (since && new Date(since).getTime() >= Math.floor(info.mtimeMs / 1e3) * 1e3) {
|
|
126
|
+
res.writeHead(304, { "Last-Modified": lastMod });
|
|
127
|
+
res.end();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const text = readFileSync(info.file, "utf-8");
|
|
132
|
+
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8", "Last-Modified": lastMod, "X-Solongate-Exists": "1" });
|
|
133
|
+
res.end(text);
|
|
134
|
+
} catch {
|
|
135
|
+
res.writeHead(500);
|
|
136
|
+
res.end("read error");
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
res.writeHead(404);
|
|
141
|
+
res.end("not found");
|
|
142
|
+
});
|
|
143
|
+
server.listen(port, "127.0.0.1", async () => {
|
|
144
|
+
const { dir, configured } = await findLogDir();
|
|
145
|
+
process.stdout.write(`[SolonGate] Local logs agent listening on http://127.0.0.1:${port}
|
|
146
|
+
`);
|
|
147
|
+
if (configured) {
|
|
148
|
+
process.stdout.write(`[SolonGate] Configured path: ${configured}
|
|
149
|
+
`);
|
|
150
|
+
if (dir && dir !== configured.trim().replace(/[\\/]+$/, "")) {
|
|
151
|
+
process.stdout.write(`[SolonGate] That path isn't absolute on this machine \u2014 reading from: ${dir}
|
|
152
|
+
`);
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
process.stdout.write(`[SolonGate] Local log storage not configured yet (set it in dashboard Settings).
|
|
156
|
+
`);
|
|
157
|
+
}
|
|
158
|
+
process.stdout.write(`[SolonGate] Keep this running; the dashboard reads your logs live from here. Ctrl+C to stop.
|
|
159
|
+
`);
|
|
160
|
+
});
|
|
161
|
+
server.on("error", (err) => {
|
|
162
|
+
if (err.code === "EADDRINUSE") {
|
|
163
|
+
process.stderr.write(`[SolonGate] Port ${port} is already in use. Pass --port <n> or set SOLONGATE_LOGS_PORT.
|
|
164
|
+
`);
|
|
165
|
+
} else {
|
|
166
|
+
process.stderr.write(`[SolonGate] Local logs agent error: ${err.message}
|
|
167
|
+
`);
|
|
168
|
+
}
|
|
169
|
+
process.exit(1);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
export {
|
|
173
|
+
runLogsServer
|
|
174
|
+
};
|
package/dist/pull-push.js
CHANGED
|
File without changes
|
package/dist/shield.js
CHANGED
|
@@ -17,14 +17,12 @@ var DLP_PATTERNS = [
|
|
|
17
17
|
{ name: "GitHub fine-grained PAT", re: /github_pat_[A-Za-z0-9_]{20,}/g },
|
|
18
18
|
{ name: "GitLab token", re: /glpat-[A-Za-z0-9_-]{20,}/g },
|
|
19
19
|
{ name: "Slack token", re: /xox[baprs]-[A-Za-z0-9-]{10,}/g },
|
|
20
|
-
{ name: "Google API key", re: /AIza[0-9A-Za-z_-]{35}/g },
|
|
21
20
|
{ name: "Stripe key", re: /[sr]k_(live|test)_[A-Za-z0-9]{20,}/g },
|
|
22
21
|
{ name: "SendGrid key", re: /SG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}/g },
|
|
23
22
|
{ name: "Twilio key", re: /SK[0-9a-fA-F]{32}/g },
|
|
24
23
|
{ name: "npm token", re: /npm_[A-Za-z0-9]{36}/g },
|
|
25
24
|
{ name: "JWT", re: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g },
|
|
26
|
-
{ name: "Bearer token", re: /bearer\s+[A-Za-z0-9._-]{20,}/gi }
|
|
27
|
-
{ name: "secret assignment", re: /(api[_-]?key|secret|token|password|passwd|access[_-]?key)["']?\s*[:=]\s*["']?[A-Za-z0-9/+_.-]{12,}/gi }
|
|
25
|
+
{ name: "Bearer token", re: /bearer\s+[A-Za-z0-9._-]{20,}/gi }
|
|
28
26
|
];
|
|
29
27
|
function findCacheFile() {
|
|
30
28
|
const dir = resolve(homedir(), ".solongate");
|
|
@@ -55,11 +53,104 @@ function loadCfg() {
|
|
|
55
53
|
if (f && existsSync(f)) {
|
|
56
54
|
const c = JSON.parse(readFileSync(f, "utf-8"));
|
|
57
55
|
const d = c?.security?.dlpRedact;
|
|
58
|
-
|
|
56
|
+
const g = c?.security?.ghost;
|
|
57
|
+
const ghost = g && Array.isArray(g.patterns) ? g.patterns : [];
|
|
58
|
+
if (d && Array.isArray(d.patterns)) return { patterns: d.patterns, custom: Array.isArray(d.custom) ? d.custom : [], ghost };
|
|
59
|
+
return { patterns: DLP_PATTERNS.map((p) => p.name), custom: [], ghost };
|
|
59
60
|
}
|
|
60
61
|
} catch {
|
|
61
62
|
}
|
|
62
|
-
return { patterns: DLP_PATTERNS.map((p) => p.name), custom: [] };
|
|
63
|
+
return { patterns: DLP_PATTERNS.map((p) => p.name), custom: [], ghost: [] };
|
|
64
|
+
}
|
|
65
|
+
function ghostGlobToRegExp(glob) {
|
|
66
|
+
let re = "";
|
|
67
|
+
for (let i = 0; i < glob.length; i++) {
|
|
68
|
+
const c = glob[i];
|
|
69
|
+
if (c === "*") {
|
|
70
|
+
if (glob[i + 1] === "*") {
|
|
71
|
+
re += ".*";
|
|
72
|
+
i++;
|
|
73
|
+
} else re += "[^/]*";
|
|
74
|
+
} else if (c === "?") re += "[^/]";
|
|
75
|
+
else if ("\\^$.|+()[]{}".indexOf(c) !== -1) re += "\\" + c;
|
|
76
|
+
else re += c;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
return new RegExp("^" + re + "$");
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function ghostMatch(targetPath, patterns) {
|
|
85
|
+
if (!targetPath || !Array.isArray(patterns) || patterns.length === 0) return false;
|
|
86
|
+
const norm = String(targetPath).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
87
|
+
if (!norm) return false;
|
|
88
|
+
const segments = norm.split("/").filter(Boolean);
|
|
89
|
+
const base = segments.length ? segments[segments.length - 1] : norm;
|
|
90
|
+
for (let pat of patterns) {
|
|
91
|
+
pat = String(pat || "").trim();
|
|
92
|
+
if (!pat) continue;
|
|
93
|
+
let dirOnly = false;
|
|
94
|
+
if (pat.endsWith("/")) {
|
|
95
|
+
dirOnly = true;
|
|
96
|
+
pat = pat.slice(0, -1);
|
|
97
|
+
}
|
|
98
|
+
if (!pat) continue;
|
|
99
|
+
const hasSlash = pat.indexOf("/") !== -1;
|
|
100
|
+
const hasWild = /[*?]/.test(pat);
|
|
101
|
+
const re = ghostGlobToRegExp(pat);
|
|
102
|
+
if (!re) continue;
|
|
103
|
+
if (dirOnly) {
|
|
104
|
+
if (!hasSlash && !hasWild) {
|
|
105
|
+
if (segments.indexOf(pat) !== -1) return true;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
let acc = "";
|
|
109
|
+
for (const s of segments) {
|
|
110
|
+
acc = acc ? acc + "/" + s : s;
|
|
111
|
+
if (re.test(acc) || re.test(s)) return true;
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (!hasSlash) {
|
|
116
|
+
if (re.test(base)) return true;
|
|
117
|
+
if (segments.some((s) => re.test(s))) return true;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (re.test(norm)) return true;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
function ghostCleanToken(tok) {
|
|
125
|
+
let t = String(tok || "").trim();
|
|
126
|
+
t = t.replace(/^[<>|;&(]+/, "").replace(/[);&|]+$/, "");
|
|
127
|
+
t = t.replace(/^['"]+/, "").replace(/['"]+$/, "");
|
|
128
|
+
t = t.replace(/^\d*>>?/, "");
|
|
129
|
+
return t.trim();
|
|
130
|
+
}
|
|
131
|
+
function ghostStripLines(text, pats) {
|
|
132
|
+
if (!Array.isArray(pats) || pats.length === 0) return text;
|
|
133
|
+
const lines = String(text).split("\n");
|
|
134
|
+
const kept = [];
|
|
135
|
+
for (const line of lines) {
|
|
136
|
+
const trimmed = line.trim();
|
|
137
|
+
if (!trimmed) {
|
|
138
|
+
kept.push(line);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (ghostMatch(trimmed, pats)) continue;
|
|
142
|
+
const toks = trimmed.split(/\s+/);
|
|
143
|
+
const anyHit = toks.some((t) => ghostMatch(ghostCleanToken(t), pats));
|
|
144
|
+
if (!anyHit) {
|
|
145
|
+
kept.push(line);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (toks.length > 3) continue;
|
|
149
|
+
const remaining = toks.filter((t) => !ghostMatch(ghostCleanToken(t), pats));
|
|
150
|
+
if (remaining.length === 0) continue;
|
|
151
|
+
kept.push(remaining.join(" "));
|
|
152
|
+
}
|
|
153
|
+
return kept.join("\n");
|
|
63
154
|
}
|
|
64
155
|
function dlpGlobToRe(glob, flags) {
|
|
65
156
|
let re = "";
|
|
@@ -84,8 +175,16 @@ function redactString(s, cfg) {
|
|
|
84
175
|
return out;
|
|
85
176
|
}
|
|
86
177
|
function redactDeep(value, cfg) {
|
|
87
|
-
|
|
88
|
-
if (
|
|
178
|
+
const ghost = cfg && Array.isArray(cfg.ghost) ? cfg.ghost : null;
|
|
179
|
+
if (typeof value === "string") {
|
|
180
|
+
let out = redactString(value, cfg);
|
|
181
|
+
if (ghost && ghost.length) out = ghostStripLines(out, ghost);
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
if (Array.isArray(value)) {
|
|
185
|
+
const arr = ghost && ghost.length ? value.filter((v) => !(typeof v === "string" && ghostMatch(v.trim(), ghost))) : value;
|
|
186
|
+
return arr.map((v) => redactDeep(v, cfg));
|
|
187
|
+
}
|
|
89
188
|
if (value && typeof value === "object") {
|
|
90
189
|
const out = {};
|
|
91
190
|
for (const [k, v] of Object.entries(value)) out[k] = redactDeep(v, cfg);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"tool":"Bash","ts":1783610904106}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"ms":2391,"ts":1783610920233,"tool":"Bash","session":"fc94b672-fd99-4d52-9105-c77c7906d9f0"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1783610904106
|