codebot-ai 1.4.3 → 1.6.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/history.js CHANGED
@@ -54,31 +54,57 @@ class SessionManager {
54
54
  }
55
55
  /** Append a message to the session file */
56
56
  save(message) {
57
- const line = JSON.stringify({
58
- ...message,
59
- _ts: new Date().toISOString(),
60
- _model: this.model,
61
- });
62
- fs.appendFileSync(this.filePath, line + '\n');
57
+ try {
58
+ const line = JSON.stringify({
59
+ ...message,
60
+ _ts: new Date().toISOString(),
61
+ _model: this.model,
62
+ });
63
+ fs.appendFileSync(this.filePath, line + '\n');
64
+ }
65
+ catch {
66
+ // Don't crash on write failure — session persistence is best-effort
67
+ }
63
68
  }
64
- /** Save all messages (overwrite) */
69
+ /** Save all messages (atomic overwrite via temp file + rename) */
65
70
  saveAll(messages) {
66
- const lines = messages.map(m => JSON.stringify({ ...m, _ts: new Date().toISOString(), _model: this.model }));
67
- fs.writeFileSync(this.filePath, lines.join('\n') + '\n');
71
+ try {
72
+ const lines = messages.map(m => JSON.stringify({ ...m, _ts: new Date().toISOString(), _model: this.model }));
73
+ const tmpPath = this.filePath + '.tmp';
74
+ fs.writeFileSync(tmpPath, lines.join('\n') + '\n');
75
+ fs.renameSync(tmpPath, this.filePath);
76
+ }
77
+ catch {
78
+ // Don't crash — session persistence is best-effort
79
+ }
68
80
  }
69
- /** Load messages from a session file */
81
+ /** Load messages from a session file (skips malformed lines) */
70
82
  load() {
71
83
  if (!fs.existsSync(this.filePath))
72
84
  return [];
73
- const content = fs.readFileSync(this.filePath, 'utf-8').trim();
85
+ let content;
86
+ try {
87
+ content = fs.readFileSync(this.filePath, 'utf-8').trim();
88
+ }
89
+ catch {
90
+ return [];
91
+ }
74
92
  if (!content)
75
93
  return [];
76
- return content.split('\n').map(line => {
77
- const obj = JSON.parse(line);
78
- delete obj._ts;
79
- delete obj._model;
80
- return obj;
81
- });
94
+ const messages = [];
95
+ for (const line of content.split('\n')) {
96
+ try {
97
+ const obj = JSON.parse(line);
98
+ delete obj._ts;
99
+ delete obj._model;
100
+ messages.push(obj);
101
+ }
102
+ catch {
103
+ // Skip malformed line — don't crash the whole load
104
+ continue;
105
+ }
106
+ }
107
+ return messages;
82
108
  }
83
109
  /** List recent sessions */
84
110
  static list(limit = 10) {
package/dist/mcp.js CHANGED
@@ -38,6 +38,37 @@ const child_process_1 = require("child_process");
38
38
  const fs = __importStar(require("fs"));
39
39
  const path = __importStar(require("path"));
40
40
  const os = __importStar(require("os"));
41
+ /**
42
+ * MCP (Model Context Protocol) client.
43
+ *
44
+ * Connects to MCP servers defined in `.codebot/mcp.json` or `~/.codebot/mcp.json`:
45
+ *
46
+ * {
47
+ * "servers": [
48
+ * {
49
+ * "name": "my-server",
50
+ * "command": "npx",
51
+ * "args": ["-y", "@my/mcp-server"],
52
+ * "env": {}
53
+ * }
54
+ * ]
55
+ * }
56
+ *
57
+ * Each server is launched as a subprocess communicating via JSON-RPC over stdio.
58
+ */
59
+ /** Allowlist of commands that MCP servers are permitted to run */
60
+ const ALLOWED_MCP_COMMANDS = new Set([
61
+ 'npx', 'node', 'python', 'python3', 'deno', 'bun', 'docker', 'uvx',
62
+ ]);
63
+ /** Safe environment variables to pass to MCP subprocesses */
64
+ const SAFE_ENV_VARS = new Set([
65
+ 'PATH', 'HOME', 'USER', 'SHELL', 'LANG', 'TERM', 'TMPDIR', 'TMP', 'TEMP',
66
+ 'LC_ALL', 'LC_CTYPE', 'DISPLAY', 'XDG_RUNTIME_DIR',
67
+ // Node.js
68
+ 'NODE_ENV', 'NODE_PATH',
69
+ // Python
70
+ 'PYTHONPATH', 'VIRTUAL_ENV',
71
+ ]);
41
72
  class MCPConnection {
42
73
  process;
43
74
  buffer = '';
@@ -46,9 +77,25 @@ class MCPConnection {
46
77
  name;
47
78
  constructor(config) {
48
79
  this.name = config.name;
80
+ // Security: validate command against allowlist
81
+ const command = path.basename(config.command);
82
+ if (!ALLOWED_MCP_COMMANDS.has(command)) {
83
+ throw new Error(`Blocked MCP command: "${config.command}". Allowed: ${[...ALLOWED_MCP_COMMANDS].join(', ')}`);
84
+ }
85
+ // Security: build safe environment — only pass safe vars + config-defined vars
86
+ const safeEnv = {};
87
+ for (const key of SAFE_ENV_VARS) {
88
+ if (process.env[key]) {
89
+ safeEnv[key] = process.env[key];
90
+ }
91
+ }
92
+ // Config-defined env vars override safe defaults
93
+ if (config.env) {
94
+ Object.assign(safeEnv, config.env);
95
+ }
49
96
  this.process = (0, child_process_1.spawn)(config.command, config.args || [], {
50
97
  stdio: ['pipe', 'pipe', 'pipe'],
51
- env: { ...process.env, ...config.env },
98
+ env: safeEnv,
52
99
  });
53
100
  this.process.stdout?.on('data', (chunk) => {
54
101
  this.buffer += chunk.toString();
@@ -97,7 +144,7 @@ class MCPConnection {
97
144
  await this.request('initialize', {
98
145
  protocolVersion: '2024-11-05',
99
146
  capabilities: {},
100
- clientInfo: { name: 'codebot-ai', version: '1.1.0' },
147
+ clientInfo: { name: 'codebot-ai', version: '1.6.0' },
101
148
  });
102
149
  await this.request('notifications/initialized');
103
150
  }
@@ -121,9 +168,13 @@ class MCPConnection {
121
168
  }
122
169
  /** Create Tool wrappers from an MCP server's tools */
123
170
  function mcpToolToTool(connection, def) {
171
+ // Security: sanitize tool description (limit length, strip control chars)
172
+ const safeDescription = (def.description || '')
173
+ .substring(0, 500)
174
+ .replace(/[\x00-\x1F\x7F]/g, '');
124
175
  return {
125
176
  name: `mcp_${connection.name}_${def.name}`,
126
- description: `[MCP:${connection.name}] ${def.description}`,
177
+ description: `[MCP:${connection.name}] ${safeDescription}`,
127
178
  permission: 'prompt',
128
179
  parameters: def.inputSchema || { type: 'object', properties: {} },
129
180
  execute: async (args) => {
package/dist/memory.d.ts CHANGED
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Sanitize memory content by stripping lines that look like prompt injection.
3
+ */
4
+ export declare function sanitizeMemory(content: string): string;
1
5
  export interface MemoryEntry {
2
6
  key: string;
3
7
  value: string;
@@ -8,6 +12,9 @@ export interface MemoryEntry {
8
12
  * Persistent memory system for CodeBot.
9
13
  * Stores project-level and global notes that survive across sessions.
10
14
  * Memory is injected into the system prompt so the model always has context.
15
+ *
16
+ * Security: content is sanitized before injection to prevent prompt injection.
17
+ * Size limits: 2KB per file, 10KB total.
11
18
  */
12
19
  export declare class MemoryManager {
13
20
  private projectDir;
package/dist/memory.js CHANGED
@@ -34,15 +34,57 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.MemoryManager = void 0;
37
+ exports.sanitizeMemory = sanitizeMemory;
37
38
  const fs = __importStar(require("fs"));
38
39
  const path = __importStar(require("path"));
39
40
  const os = __importStar(require("os"));
40
41
  const MEMORY_DIR = path.join(os.homedir(), '.codebot', 'memory');
41
42
  const GLOBAL_MEMORY = path.join(MEMORY_DIR, 'MEMORY.md');
43
+ /** Maximum size per memory file (2KB) */
44
+ const MAX_FILE_SIZE = 2048;
45
+ /** Maximum total memory size across all files (10KB) */
46
+ const MAX_TOTAL_SIZE = 10240;
47
+ /** Patterns that indicate potential prompt injection in memory content */
48
+ const INJECTION_PATTERNS = [
49
+ /^(system|assistant|user):\s/i,
50
+ /ignore (previous|all|above) instructions/i,
51
+ /you are now/i,
52
+ /new instructions:/i,
53
+ /override:/i,
54
+ /<\/?system>/i,
55
+ /\bforget (all|everything|your)\b/i,
56
+ /\bact as\b/i,
57
+ /\brole:\s*(system|admin)/i,
58
+ /\bpretend (you are|to be)\b/i,
59
+ ];
60
+ /**
61
+ * Sanitize memory content by stripping lines that look like prompt injection.
62
+ */
63
+ function sanitizeMemory(content) {
64
+ return content.split('\n')
65
+ .filter(line => !INJECTION_PATTERNS.some(p => p.test(line)))
66
+ .join('\n');
67
+ }
68
+ /**
69
+ * Truncate content to a maximum byte size.
70
+ */
71
+ function truncateToSize(content, maxSize) {
72
+ if (Buffer.byteLength(content, 'utf-8') <= maxSize)
73
+ return content;
74
+ // Truncate by chars (approximation — will be close to byte limit)
75
+ let truncated = content;
76
+ while (Buffer.byteLength(truncated, 'utf-8') > maxSize - 50) { // leave room for marker
77
+ truncated = truncated.substring(0, Math.floor(truncated.length * 0.9));
78
+ }
79
+ return truncated.trimEnd() + '\n[truncated — exceeded size limit]';
80
+ }
42
81
  /**
43
82
  * Persistent memory system for CodeBot.
44
83
  * Stores project-level and global notes that survive across sessions.
45
84
  * Memory is injected into the system prompt so the model always has context.
85
+ *
86
+ * Security: content is sanitized before injection to prevent prompt injection.
87
+ * Size limits: 2KB per file, 10KB total.
46
88
  */
47
89
  class MemoryManager {
48
90
  projectDir;
@@ -76,14 +118,16 @@ class MemoryManager {
76
118
  }
77
119
  /** Write to global memory */
78
120
  writeGlobal(content) {
79
- fs.writeFileSync(GLOBAL_MEMORY, content);
121
+ const safe = truncateToSize(content, MAX_FILE_SIZE);
122
+ fs.writeFileSync(GLOBAL_MEMORY, safe);
80
123
  }
81
124
  /** Write to project memory */
82
125
  writeProject(content) {
83
126
  if (!this.projectDir)
84
127
  return;
85
128
  const memFile = path.join(this.projectDir, 'MEMORY.md');
86
- fs.writeFileSync(memFile, content);
129
+ const safe = truncateToSize(content, MAX_FILE_SIZE);
130
+ fs.writeFileSync(memFile, safe);
87
131
  }
88
132
  /** Append an entry to global memory */
89
133
  appendGlobal(entry) {
@@ -114,20 +158,34 @@ class MemoryManager {
114
158
  /** Get all memory content formatted for system prompt injection */
115
159
  getContextBlock() {
116
160
  const parts = [];
161
+ let totalSize = 0;
117
162
  const global = this.readGlobal();
118
163
  if (global.trim()) {
119
- parts.push(`## Global Memory\n${global.trim()}`);
164
+ const sanitized = sanitizeMemory(global.trim());
165
+ const truncated = truncateToSize(sanitized, MAX_FILE_SIZE);
166
+ totalSize += Buffer.byteLength(truncated, 'utf-8');
167
+ parts.push(`## Global Memory\n${truncated}`);
120
168
  }
121
169
  // Read additional global topic files
122
170
  const globalFiles = this.readDir(this.globalDir);
123
171
  for (const [name, content] of Object.entries(globalFiles)) {
124
172
  if (name === 'MEMORY.md' || !content.trim())
125
173
  continue;
126
- parts.push(`## ${name.replace('.md', '')}\n${content.trim()}`);
174
+ if (totalSize >= MAX_TOTAL_SIZE)
175
+ break;
176
+ const sanitized = sanitizeMemory(content.trim());
177
+ const remaining = MAX_TOTAL_SIZE - totalSize;
178
+ const truncated = truncateToSize(sanitized, Math.min(MAX_FILE_SIZE, remaining));
179
+ totalSize += Buffer.byteLength(truncated, 'utf-8');
180
+ parts.push(`## ${name.replace('.md', '')}\n${truncated}`);
127
181
  }
128
182
  const project = this.readProject();
129
- if (project.trim()) {
130
- parts.push(`## Project Memory\n${project.trim()}`);
183
+ if (project.trim() && totalSize < MAX_TOTAL_SIZE) {
184
+ const sanitized = sanitizeMemory(project.trim());
185
+ const remaining = MAX_TOTAL_SIZE - totalSize;
186
+ const truncated = truncateToSize(sanitized, Math.min(MAX_FILE_SIZE, remaining));
187
+ totalSize += Buffer.byteLength(truncated, 'utf-8');
188
+ parts.push(`## Project Memory\n${truncated}`);
131
189
  }
132
190
  // Read additional project topic files
133
191
  if (this.projectDir) {
@@ -135,7 +193,13 @@ class MemoryManager {
135
193
  for (const [name, content] of Object.entries(projFiles)) {
136
194
  if (name === 'MEMORY.md' || !content.trim())
137
195
  continue;
138
- parts.push(`## Project: ${name.replace('.md', '')}\n${content.trim()}`);
196
+ if (totalSize >= MAX_TOTAL_SIZE)
197
+ break;
198
+ const sanitized = sanitizeMemory(content.trim());
199
+ const remaining = MAX_TOTAL_SIZE - totalSize;
200
+ const truncated = truncateToSize(sanitized, Math.min(MAX_FILE_SIZE, remaining));
201
+ totalSize += Buffer.byteLength(truncated, 'utf-8');
202
+ parts.push(`## Project: ${name.replace('.md', '')}\n${truncated}`);
139
203
  }
140
204
  }
141
205
  if (parts.length === 0)
package/dist/plugins.d.ts CHANGED
@@ -1,17 +1,3 @@
1
1
  import { Tool } from './types';
2
- /**
3
- * Plugin system for CodeBot.
4
- *
5
- * Plugins are .js files in `.codebot/plugins/` (project) or `~/.codebot/plugins/` (global).
6
- * Each plugin exports a default function or object that implements the Tool interface:
7
- *
8
- * module.exports = {
9
- * name: 'my_tool',
10
- * description: 'Does something useful',
11
- * permission: 'prompt',
12
- * parameters: { type: 'object', properties: { ... }, required: [...] },
13
- * execute: async (args) => { return 'result'; }
14
- * };
15
- */
16
2
  export declare function loadPlugins(projectRoot?: string): Tool[];
17
3
  //# sourceMappingURL=plugins.d.ts.map
package/dist/plugins.js CHANGED
@@ -36,20 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.loadPlugins = loadPlugins;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
- /**
40
- * Plugin system for CodeBot.
41
- *
42
- * Plugins are .js files in `.codebot/plugins/` (project) or `~/.codebot/plugins/` (global).
43
- * Each plugin exports a default function or object that implements the Tool interface:
44
- *
45
- * module.exports = {
46
- * name: 'my_tool',
47
- * description: 'Does something useful',
48
- * permission: 'prompt',
49
- * parameters: { type: 'object', properties: { ... }, required: [...] },
50
- * execute: async (args) => { return 'result'; }
51
- * };
52
- */
39
+ const crypto = __importStar(require("crypto"));
53
40
  function loadPlugins(projectRoot) {
54
41
  const plugins = [];
55
42
  const os = require('os');
@@ -74,6 +61,32 @@ function loadPlugins(projectRoot) {
74
61
  continue;
75
62
  try {
76
63
  const pluginPath = path.join(dir, entry.name);
64
+ // Security: verify plugin against manifest hash
65
+ const manifestPath = path.join(dir, 'plugin.json');
66
+ if (!fs.existsSync(manifestPath)) {
67
+ console.error(`Plugin skipped (${entry.name}): no plugin.json manifest found. Create one with: { "name": "...", "version": "...", "hash": "sha256:..." }`);
68
+ continue;
69
+ }
70
+ let manifest;
71
+ try {
72
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
73
+ }
74
+ catch {
75
+ console.error(`Plugin skipped (${entry.name}): invalid plugin.json manifest`);
76
+ continue;
77
+ }
78
+ if (!manifest.hash || !manifest.hash.startsWith('sha256:')) {
79
+ console.error(`Plugin skipped (${entry.name}): manifest missing valid sha256 hash`);
80
+ continue;
81
+ }
82
+ // Compute SHA-256 of the plugin file
83
+ const pluginContent = fs.readFileSync(pluginPath);
84
+ const computedHash = 'sha256:' + crypto.createHash('sha256').update(pluginContent).digest('hex');
85
+ if (computedHash !== manifest.hash) {
86
+ console.error(`Plugin skipped (${entry.name}): hash mismatch. Expected ${manifest.hash}, got ${computedHash}. Plugin may have been tampered with.`);
87
+ continue;
88
+ }
89
+ // Hash verified — safe to load
77
90
  // eslint-disable-next-line @typescript-eslint/no-require-imports
78
91
  const mod = require(pluginPath);
79
92
  const plugin = mod.default || mod;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Per-tool Rate Limiter
3
+ *
4
+ * Simple sliding-window throttle that enforces minimum intervals between
5
+ * calls to the same tool. Prevents hammering web services and self-DOS.
6
+ *
7
+ * @since v1.5.0
8
+ */
9
+ export declare class RateLimiter {
10
+ private lastCall;
11
+ /** Minimum interval (ms) between calls per tool */
12
+ private limits;
13
+ constructor(overrides?: Record<string, number>);
14
+ /** Wait if needed to respect the tool's rate limit */
15
+ throttle(toolName: string): Promise<void>;
16
+ /** Get the configured limit for a tool (0 = no limit) */
17
+ getLimit(toolName: string): number;
18
+ /** Update a tool's rate limit at runtime */
19
+ setLimit(toolName: string, intervalMs: number): void;
20
+ /** Reset all tracking (useful for tests) */
21
+ reset(): void;
22
+ }
23
+ //# sourceMappingURL=rate-limiter.d.ts.map
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ /**
3
+ * Per-tool Rate Limiter
4
+ *
5
+ * Simple sliding-window throttle that enforces minimum intervals between
6
+ * calls to the same tool. Prevents hammering web services and self-DOS.
7
+ *
8
+ * @since v1.5.0
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.RateLimiter = void 0;
12
+ class RateLimiter {
13
+ lastCall = new Map();
14
+ /** Minimum interval (ms) between calls per tool */
15
+ limits = {
16
+ browser: 200,
17
+ web_fetch: 500,
18
+ web_search: 1000,
19
+ execute: 100,
20
+ };
21
+ constructor(overrides) {
22
+ if (overrides) {
23
+ Object.assign(this.limits, overrides);
24
+ }
25
+ }
26
+ /** Wait if needed to respect the tool's rate limit */
27
+ async throttle(toolName) {
28
+ const limit = this.limits[toolName];
29
+ if (!limit)
30
+ return; // no limit for this tool
31
+ const last = this.lastCall.get(toolName) || 0;
32
+ const elapsed = Date.now() - last;
33
+ if (elapsed < limit) {
34
+ await new Promise(resolve => setTimeout(resolve, limit - elapsed));
35
+ }
36
+ this.lastCall.set(toolName, Date.now());
37
+ }
38
+ /** Get the configured limit for a tool (0 = no limit) */
39
+ getLimit(toolName) {
40
+ return this.limits[toolName] || 0;
41
+ }
42
+ /** Update a tool's rate limit at runtime */
43
+ setLimit(toolName, intervalMs) {
44
+ this.limits[toolName] = intervalMs;
45
+ }
46
+ /** Reset all tracking (useful for tests) */
47
+ reset() {
48
+ this.lastCall.clear();
49
+ }
50
+ }
51
+ exports.RateLimiter = RateLimiter;
52
+ //# sourceMappingURL=rate-limiter.js.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Secret detection module for CodeBot.
3
+ *
4
+ * Scans content for common credential patterns (API keys, tokens, passwords).
5
+ * Returns matches with line numbers and masked excerpts.
6
+ * Used to warn before writing secrets to files — does NOT block writes.
7
+ */
8
+ export interface SecretMatch {
9
+ type: string;
10
+ line: number;
11
+ snippet: string;
12
+ }
13
+ /**
14
+ * Scan content for secrets. Returns array of matches with line numbers and masked snippets.
15
+ */
16
+ export declare function scanForSecrets(content: string): SecretMatch[];
17
+ /**
18
+ * Quick check: does the content contain any secrets?
19
+ */
20
+ export declare function hasSecrets(content: string): boolean;
21
+ /**
22
+ * Mask secrets in an arbitrary string (e.g., for audit logging).
23
+ * Replaces all detected secret matches with masked versions.
24
+ */
25
+ export declare function maskSecretsInString(text: string): string;
26
+ //# sourceMappingURL=secrets.d.ts.map
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ /**
3
+ * Secret detection module for CodeBot.
4
+ *
5
+ * Scans content for common credential patterns (API keys, tokens, passwords).
6
+ * Returns matches with line numbers and masked excerpts.
7
+ * Used to warn before writing secrets to files — does NOT block writes.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.scanForSecrets = scanForSecrets;
11
+ exports.hasSecrets = hasSecrets;
12
+ exports.maskSecretsInString = maskSecretsInString;
13
+ const SECRET_PATTERNS = [
14
+ { name: 'aws_access_key', pattern: /AKIA[0-9A-Z]{16}/ },
15
+ { name: 'aws_secret_key', pattern: /(?:aws_secret_access_key|aws_secret)\s*[:=]\s*['"]?[A-Za-z0-9/+=]{40}/ },
16
+ { name: 'private_key', pattern: /-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/ },
17
+ { name: 'github_token', pattern: /gh[ps]_[A-Za-z0-9_]{36,}/ },
18
+ { name: 'github_oauth', pattern: /gho_[A-Za-z0-9_]{36,}/ },
19
+ { name: 'generic_api_key', pattern: /(?:api[_-]?key|apikey|secret[_-]?key)\s*[:=]\s*['"]?[A-Za-z0-9_\-]{20,}/i },
20
+ { name: 'jwt', pattern: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_\-]+/ },
21
+ { name: 'password_assign', pattern: /(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]/i },
22
+ { name: 'connection_string', pattern: /(?:mongodb|postgres|postgresql|mysql|redis|amqp):\/\/[^\s]{10,}/ },
23
+ { name: 'slack_token', pattern: /xox[bprs]-[0-9]{10,}-[A-Za-z0-9\-]+/ },
24
+ { name: 'slack_webhook', pattern: /hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+/ },
25
+ { name: 'generic_secret', pattern: /(?:secret|token|credential|auth_token)\s*[:=]\s*['"][^'"]{16,}['"]/i },
26
+ { name: 'npm_token', pattern: /\/\/registry\.npmjs\.org\/:_authToken=[^\s]+/ },
27
+ { name: 'sendgrid_key', pattern: /SG\.[A-Za-z0-9_\-]{22}\.[A-Za-z0-9_\-]{43}/ },
28
+ { name: 'stripe_key', pattern: /sk_(live|test)_[A-Za-z0-9]{24,}/ },
29
+ ];
30
+ /**
31
+ * Mask a matched secret for safe display.
32
+ * Shows first 4 chars + **** + last 4 chars for strings >= 12 chars.
33
+ * For shorter strings, shows first 2 + **** + last 2.
34
+ */
35
+ function maskSecret(match) {
36
+ if (match.length >= 12) {
37
+ return match.substring(0, 4) + '****' + match.substring(match.length - 4);
38
+ }
39
+ if (match.length >= 6) {
40
+ return match.substring(0, 2) + '****' + match.substring(match.length - 2);
41
+ }
42
+ return '****';
43
+ }
44
+ /**
45
+ * Scan content for secrets. Returns array of matches with line numbers and masked snippets.
46
+ */
47
+ function scanForSecrets(content) {
48
+ const matches = [];
49
+ const lines = content.split('\n');
50
+ for (let i = 0; i < lines.length; i++) {
51
+ const line = lines[i];
52
+ for (const { name, pattern } of SECRET_PATTERNS) {
53
+ const match = line.match(pattern);
54
+ if (match) {
55
+ matches.push({
56
+ type: name,
57
+ line: i + 1,
58
+ snippet: maskSecret(match[0]),
59
+ });
60
+ }
61
+ }
62
+ }
63
+ return matches;
64
+ }
65
+ /**
66
+ * Quick check: does the content contain any secrets?
67
+ */
68
+ function hasSecrets(content) {
69
+ for (const { pattern } of SECRET_PATTERNS) {
70
+ if (pattern.test(content))
71
+ return true;
72
+ }
73
+ return false;
74
+ }
75
+ /**
76
+ * Mask secrets in an arbitrary string (e.g., for audit logging).
77
+ * Replaces all detected secret matches with masked versions.
78
+ */
79
+ function maskSecretsInString(text) {
80
+ let masked = text;
81
+ for (const { pattern } of SECRET_PATTERNS) {
82
+ masked = masked.replace(new RegExp(pattern.source, pattern.flags + (pattern.flags.includes('g') ? '' : 'g')), match => maskSecret(match));
83
+ }
84
+ return masked;
85
+ }
86
+ //# sourceMappingURL=secrets.js.map
@@ -0,0 +1,18 @@
1
+ export interface PathSafetyResult {
2
+ safe: boolean;
3
+ reason?: string;
4
+ }
5
+ /**
6
+ * Check if a file path is safe for write/edit operations.
7
+ *
8
+ * Resolves symlinks, checks against blocked system paths,
9
+ * and verifies the path is within the project or user home.
10
+ */
11
+ export declare function isPathSafe(targetPath: string, projectRoot: string): PathSafetyResult;
12
+ /**
13
+ * Check if a working directory is safe for command execution.
14
+ *
15
+ * Ensures the CWD exists, is a directory, and is under the project root.
16
+ */
17
+ export declare function isCwdSafe(cwd: string, projectRoot: string): PathSafetyResult;
18
+ //# sourceMappingURL=security.d.ts.map