gitnexus 1.6.4-rc.55 → 1.6.4-rc.57

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.
@@ -235,12 +235,22 @@ export const createWorkerPool = (workerUrl, poolSize, options) => {
235
235
  if (!settled) {
236
236
  settled = true;
237
237
  cleanup();
238
- activeWorkers--;
239
238
  inFlightProgress[workerIndex] = 0;
240
239
  const shouldContinue = requeueAfterTimeout(workerIndex, job, lastProgress);
241
- if (!shouldContinue)
240
+ if (!shouldContinue) {
241
+ activeWorkers--;
242
242
  return;
243
- await replaceWorker(workerIndex);
243
+ }
244
+ try {
245
+ await replaceWorker(workerIndex);
246
+ }
247
+ catch (err) {
248
+ void fail(err instanceof Error ? err : new Error(`Worker replacement failed: ${err}`));
249
+ return;
250
+ }
251
+ finally {
252
+ activeWorkers--;
253
+ }
244
254
  reportProgress();
245
255
  runWorker(workerIndex);
246
256
  maybeDone();
@@ -48,5 +48,25 @@ export declare const staticCacheControlSetHeaders: (res: express.Response, fileP
48
48
  export declare const registerWebUI: (app: express.Express, staticDir: string | null) => void;
49
49
  export declare const writeNdjsonRecord: (res: express.Response, record: GraphStreamRecord, signal?: AbortSignal) => Promise<void>;
50
50
  export declare const streamGraphNdjson: (res: express.Response, includeContent?: boolean, signal?: AbortSignal) => Promise<void>;
51
+ /**
52
+ * Handle a GET /api/file request body. Extracted from createServer's route
53
+ * registration so it can be unit-tested without spinning up an HTTP server
54
+ * — calling app.get(...) inside a test triggers CodeQL's
55
+ * js/missing-rate-limiting query, which is appropriate for production
56
+ * route handlers but a false positive for tests of the handler logic.
57
+ *
58
+ * The function takes the express req and res (typed loosely so test code
59
+ * can pass minimal mocks) plus the resolved repo path. All path-traversal
60
+ * containment is done inline at the readFile sink with the canonical
61
+ * path.relative idiom for CodeQL js/path-injection recognition.
62
+ */
63
+ export declare const handleFileRequest: (req: {
64
+ query: any;
65
+ }, res: {
66
+ status: (code: number) => {
67
+ json: (body: any) => void;
68
+ };
69
+ json: (body: any) => void;
70
+ }, repoPath: string) => Promise<void>;
51
71
  export declare const createServer: (port: number, host?: string) => Promise<void>;
52
72
  export {};
@@ -450,6 +450,77 @@ const requestedRepo = (req) => {
450
450
  }
451
451
  return undefined;
452
452
  };
453
+ /**
454
+ * Handle a GET /api/file request body. Extracted from createServer's route
455
+ * registration so it can be unit-tested without spinning up an HTTP server
456
+ * — calling app.get(...) inside a test triggers CodeQL's
457
+ * js/missing-rate-limiting query, which is appropriate for production
458
+ * route handlers but a false positive for tests of the handler logic.
459
+ *
460
+ * The function takes the express req and res (typed loosely so test code
461
+ * can pass minimal mocks) plus the resolved repo path. All path-traversal
462
+ * containment is done inline at the readFile sink with the canonical
463
+ * path.relative idiom for CodeQL js/path-injection recognition.
464
+ */
465
+ export const handleFileRequest = async (req, res, repoPath) => {
466
+ try {
467
+ // Type-confusion guard — req.query.path is `string | string[] | ParsedQs`.
468
+ // Without this, an attacker could pass `?path=a&path=b` to bypass the
469
+ // length-bound traversal check below (CodeQL js/type-confusion-through-
470
+ // parameter-tampering, same class as the /api/grep critical fix).
471
+ const rawFilePath = req.query.path;
472
+ if (rawFilePath === undefined || rawFilePath === '') {
473
+ res.status(400).json({ error: 'Missing path' });
474
+ return;
475
+ }
476
+ const filePath = assertString(rawFilePath, 'path');
477
+ // Path-injection containment — inline at the sink with the canonical
478
+ // path.relative idiom that CodeQL's js/path-injection sanitizer
479
+ // recognizes. assertSafePath in validation.ts performs the equivalent
480
+ // check, but cross-module helpers are not followed by CodeQL's
481
+ // interprocedural analysis for path-traversal sanitization in JS, so
482
+ // the barrier must be visible inline at the readFile sink.
483
+ const repoRoot = path.resolve(repoPath);
484
+ const fullPath = path.resolve(repoRoot, filePath);
485
+ const fullRel = path.relative(repoRoot, fullPath);
486
+ if (fullRel.startsWith('..') || path.isAbsolute(fullRel)) {
487
+ res.status(403).json({ error: 'Path traversal denied' });
488
+ return;
489
+ }
490
+ const raw = await fs.readFile(fullPath, 'utf-8');
491
+ // Optional line-range support: ?startLine=10&endLine=50
492
+ // Returns only the requested slice (0-indexed), plus metadata.
493
+ const startLine = req.query.startLine !== undefined ? Number(req.query.startLine) : undefined;
494
+ const endLine = req.query.endLine !== undefined ? Number(req.query.endLine) : undefined;
495
+ if (startLine !== undefined && Number.isFinite(startLine)) {
496
+ const lines = raw.split('\n');
497
+ const start = Math.max(0, startLine);
498
+ const end = endLine !== undefined && Number.isFinite(endLine)
499
+ ? Math.min(lines.length, endLine + 1)
500
+ : lines.length;
501
+ res.json({
502
+ content: lines.slice(start, end).join('\n'),
503
+ startLine: start,
504
+ endLine: end - 1,
505
+ totalLines: lines.length,
506
+ });
507
+ }
508
+ else {
509
+ res.json({ content: raw, totalLines: raw.split('\n').length });
510
+ }
511
+ }
512
+ catch (err) {
513
+ if (err.code === 'ENOENT') {
514
+ res.status(404).json({ error: 'File not found' });
515
+ }
516
+ else {
517
+ // statusFromError returns err.status for BadRequestError / ForbiddenError
518
+ // (assertString → 400 on array-form ?path=a&path=b; ForbiddenError → 403
519
+ // on traversal). Falls back to 500 for unrecognized failures.
520
+ res.status(statusFromError(err)).json({ error: err.message || 'Failed to read file' });
521
+ }
522
+ }
523
+ };
453
524
  export const createServer = async (port, host = '127.0.0.1') => {
454
525
  const app = express();
455
526
  app.disable('x-powered-by');
@@ -915,54 +986,12 @@ export const createServer = async (port, host = '127.0.0.1') => {
915
986
  });
916
987
  // Read file — with path traversal guard
917
988
  app.get('/api/file', async (req, res) => {
918
- try {
919
- const entry = await resolveRepo(requestedRepo(req));
920
- if (!entry) {
921
- res.status(404).json({ error: 'Repository not found' });
922
- return;
923
- }
924
- const filePath = req.query.path;
925
- if (!filePath) {
926
- res.status(400).json({ error: 'Missing path' });
927
- return;
928
- }
929
- // Prevent path traversal — resolve and verify the path stays within the repo root
930
- const repoRoot = path.resolve(entry.path);
931
- const fullPath = path.resolve(repoRoot, filePath);
932
- if (!fullPath.startsWith(repoRoot + path.sep) && fullPath !== repoRoot) {
933
- res.status(403).json({ error: 'Path traversal denied' });
934
- return;
935
- }
936
- const raw = await fs.readFile(fullPath, 'utf-8');
937
- // Optional line-range support: ?startLine=10&endLine=50
938
- // Returns only the requested slice (0-indexed), plus metadata.
939
- const startLine = req.query.startLine !== undefined ? Number(req.query.startLine) : undefined;
940
- const endLine = req.query.endLine !== undefined ? Number(req.query.endLine) : undefined;
941
- if (startLine !== undefined && Number.isFinite(startLine)) {
942
- const lines = raw.split('\n');
943
- const start = Math.max(0, startLine);
944
- const end = endLine !== undefined && Number.isFinite(endLine)
945
- ? Math.min(lines.length, endLine + 1)
946
- : lines.length;
947
- res.json({
948
- content: lines.slice(start, end).join('\n'),
949
- startLine: start,
950
- endLine: end - 1,
951
- totalLines: lines.length,
952
- });
953
- }
954
- else {
955
- res.json({ content: raw, totalLines: raw.split('\n').length });
956
- }
957
- }
958
- catch (err) {
959
- if (err.code === 'ENOENT') {
960
- res.status(404).json({ error: 'File not found' });
961
- }
962
- else {
963
- res.status(500).json({ error: err.message || 'Failed to read file' });
964
- }
989
+ const entry = await resolveRepo(requestedRepo(req));
990
+ if (!entry) {
991
+ res.status(404).json({ error: 'Repository not found' });
992
+ return;
965
993
  }
994
+ await handleFileRequest(req, res, entry.path);
966
995
  });
967
996
  // Grep — regex search across file contents in the indexed repo
968
997
  // Uses filesystem-based search for memory efficiency (never loads all files into memory)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.4-rc.55",
3
+ "version": "1.6.4-rc.57",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",