shiva-code 0.5.0 → 0.5.2

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.
@@ -5,11 +5,86 @@ import {
5
5
  // src/services/session/manager.ts
6
6
  import * as fs from "fs";
7
7
  import * as fsPromises from "fs/promises";
8
+ import * as path2 from "path";
9
+ import * as os2 from "os";
10
+ import * as readline from "readline";
11
+
12
+ // src/utils/sanitize.ts
8
13
  import * as path from "path";
9
14
  import * as os from "os";
10
- import * as readline from "readline";
15
+ function isPathSafe(inputPath, allowedBase) {
16
+ try {
17
+ const resolved = path.resolve(inputPath);
18
+ if (inputPath.includes("\0")) {
19
+ return false;
20
+ }
21
+ if (allowedBase) {
22
+ const resolvedBase = path.resolve(allowedBase);
23
+ const relative2 = path.relative(resolvedBase, resolved);
24
+ if (relative2.startsWith("..") || path.isAbsolute(relative2)) {
25
+ return false;
26
+ }
27
+ }
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+ function isPathWithinAllowedBoundaries(inputPath) {
34
+ const resolved = path.resolve(inputPath);
35
+ const homeDir = os.homedir();
36
+ if (resolved.startsWith(homeDir)) {
37
+ return true;
38
+ }
39
+ const allowedPrefixes = [
40
+ "/tmp",
41
+ "/var/tmp",
42
+ homeDir
43
+ ];
44
+ return allowedPrefixes.some((prefix) => resolved.startsWith(prefix));
45
+ }
46
+ function isValidSessionId(sessionId) {
47
+ if (!sessionId || typeof sessionId !== "string") {
48
+ return false;
49
+ }
50
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
51
+ return uuidRegex.test(sessionId);
52
+ }
53
+ function isValidProjectPath(projectPath) {
54
+ if (!projectPath || typeof projectPath !== "string") {
55
+ return false;
56
+ }
57
+ if (!path.isAbsolute(projectPath)) {
58
+ return false;
59
+ }
60
+ if (projectPath.includes("\0")) {
61
+ return false;
62
+ }
63
+ const dangerousForPath = /[;&|`$(){}[\]<>\\!#*?"'\n\r\t]/;
64
+ if (dangerousForPath.test(projectPath)) {
65
+ return false;
66
+ }
67
+ return true;
68
+ }
69
+ function maskSecret(secret) {
70
+ if (!secret || secret.length < 8) {
71
+ return "********";
72
+ }
73
+ const visibleStart = secret.substring(0, 2);
74
+ const visibleEnd = secret.substring(secret.length - 2);
75
+ const maskedLength = Math.min(secret.length - 4, 20);
76
+ return `${visibleStart}${"*".repeat(maskedLength)}${visibleEnd}`;
77
+ }
78
+ function sanitizeForLog(input) {
79
+ if (!input || typeof input !== "string") {
80
+ return "[invalid]";
81
+ }
82
+ return input.replace(/[\x00-\x1F\x7F]/g, "").substring(0, 500);
83
+ }
84
+
85
+ // src/services/session/manager.ts
11
86
  function getClaudeProjectsPath() {
12
- return path.join(os.homedir(), ".claude", "projects");
87
+ return path2.join(os2.homedir(), ".claude", "projects");
13
88
  }
14
89
  function encodeProjectPath(projectPath) {
15
90
  return projectPath.replace(/\//g, "-").replace(/^-/, "");
@@ -19,7 +94,7 @@ function decodeProjectPath(encoded) {
19
94
  return "/" + parts.join("/");
20
95
  }
21
96
  function getProjectName(projectPath) {
22
- return path.basename(projectPath);
97
+ return path2.basename(projectPath);
23
98
  }
24
99
  async function getAllClaudeProjects(skipCache = false) {
25
100
  if (!skipCache) {
@@ -63,11 +138,12 @@ function invalidateSessionsCache() {
63
138
  cache.invalidateSessions();
64
139
  }
65
140
  function getSessionIndex(encodedPath) {
66
- const indexPath = path.join(
67
- getClaudeProjectsPath(),
68
- encodedPath,
69
- "sessions-index.json"
70
- );
141
+ const projectsPath = getClaudeProjectsPath();
142
+ const indexPath = path2.join(projectsPath, encodedPath, "sessions-index.json");
143
+ if (!isPathSafe(indexPath, projectsPath)) {
144
+ console.error(`[Security] Invalid path detected: ${sanitizeForLog(encodedPath)}`);
145
+ return null;
146
+ }
71
147
  if (!fs.existsSync(indexPath)) {
72
148
  return null;
73
149
  }
@@ -79,11 +155,12 @@ function getSessionIndex(encodedPath) {
79
155
  }
80
156
  }
81
157
  async function getSessionIndexAsync(encodedPath) {
82
- const indexPath = path.join(
83
- getClaudeProjectsPath(),
84
- encodedPath,
85
- "sessions-index.json"
86
- );
158
+ const projectsPath = getClaudeProjectsPath();
159
+ const indexPath = path2.join(projectsPath, encodedPath, "sessions-index.json");
160
+ if (!isPathSafe(indexPath, projectsPath)) {
161
+ console.error(`[Security] Invalid path detected: ${sanitizeForLog(encodedPath)}`);
162
+ return null;
163
+ }
87
164
  try {
88
165
  const content = await fsPromises.readFile(indexPath, "utf-8");
89
166
  return JSON.parse(content);
@@ -93,7 +170,7 @@ async function getSessionIndexAsync(encodedPath) {
93
170
  }
94
171
  async function findProject(nameOrPath, useCache = true) {
95
172
  const projects = await getAllClaudeProjects(!useCache);
96
- const absolutePath = path.resolve(nameOrPath);
173
+ const absolutePath = path2.resolve(nameOrPath);
97
174
  const byPath = projects.find((p) => p.absolutePath === absolutePath);
98
175
  if (byPath) return byPath;
99
176
  const lowerName = nameOrPath.toLowerCase();
@@ -103,7 +180,7 @@ async function findProject(nameOrPath, useCache = true) {
103
180
  return byName || null;
104
181
  }
105
182
  function findProjectFromArray(projects, nameOrPath) {
106
- const absolutePath = path.resolve(nameOrPath);
183
+ const absolutePath = path2.resolve(nameOrPath);
107
184
  const byPath = projects.find((p) => p.absolutePath === absolutePath);
108
185
  if (byPath) return byPath;
109
186
  const lowerName = nameOrPath.toLowerCase();
@@ -173,10 +250,15 @@ function isSessionActive(session) {
173
250
  }
174
251
  async function parseSessionFile(jsonlPath, level = "standard") {
175
252
  const summaries = [];
253
+ const resolvedPath = path2.resolve(jsonlPath);
254
+ if (!isPathWithinAllowedBoundaries(resolvedPath)) {
255
+ console.error(`[Security] Path outside allowed boundaries: ${sanitizeForLog(jsonlPath)}`);
256
+ return summaries;
257
+ }
176
258
  if (!fs.existsSync(jsonlPath)) {
177
259
  return summaries;
178
260
  }
179
- return new Promise((resolve2) => {
261
+ return new Promise((resolve3) => {
180
262
  const fileStream = fs.createReadStream(jsonlPath, { encoding: "utf-8" });
181
263
  const rl = readline.createInterface({
182
264
  input: fileStream,
@@ -213,10 +295,10 @@ async function parseSessionFile(jsonlPath, level = "standard") {
213
295
  }
214
296
  });
215
297
  rl.on("close", () => {
216
- resolve2(summaries);
298
+ resolve3(summaries);
217
299
  });
218
300
  rl.on("error", () => {
219
- resolve2(summaries);
301
+ resolve3(summaries);
220
302
  });
221
303
  });
222
304
  }
@@ -261,14 +343,21 @@ function formatRecoveredContextAsMarkdown(context) {
261
343
  return lines.join("\n");
262
344
  }
263
345
  function saveRecoveredContext(context, outputDir) {
264
- const dir = outputDir || path.join(os.homedir(), ".shiva", "recovery");
265
- if (!fs.existsSync(dir)) {
266
- fs.mkdirSync(dir, { recursive: true });
346
+ const dir = outputDir || path2.join(os2.homedir(), ".shiva", "recovery");
347
+ const resolvedDir = path2.resolve(dir);
348
+ if (!isPathWithinAllowedBoundaries(resolvedDir)) {
349
+ throw new Error(`Output directory outside allowed boundaries: ${sanitizeForLog(dir)}`);
350
+ }
351
+ if (!fs.existsSync(resolvedDir)) {
352
+ fs.mkdirSync(resolvedDir, { recursive: true });
267
353
  }
268
354
  const projectSlug = getProjectName(context.projectPath).toLowerCase().replace(/[^a-z0-9]+/g, "-");
269
355
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
270
356
  const filename = `${projectSlug}-${date}.md`;
271
- const filepath = path.join(dir, filename);
357
+ const filepath = path2.join(resolvedDir, filename);
358
+ if (!isPathWithinAllowedBoundaries(filepath)) {
359
+ throw new Error(`Output path outside allowed boundaries: ${sanitizeForLog(filepath)}`);
360
+ }
272
361
  const content = formatRecoveredContextAsMarkdown(context);
273
362
  fs.writeFileSync(filepath, content, "utf-8");
274
363
  return filepath;
@@ -313,6 +402,10 @@ function formatDate(date) {
313
402
  }
314
403
 
315
404
  export {
405
+ isValidSessionId,
406
+ isValidProjectPath,
407
+ maskSecret,
408
+ sanitizeForLog,
316
409
  getClaudeProjectsPath,
317
410
  encodeProjectPath,
318
411
  decodeProjectPath,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  findProject,
3
3
  getProjectName
4
- } from "./chunk-IDM6KY2R.js";
4
+ } from "./chunk-H5OFO4VS.js";
5
5
 
6
6
  // src/services/data/package-manager.ts
7
7
  import Conf from "conf";