gitnexus 1.6.4-rc.55 → 1.6.4-rc.56
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/server/api.d.ts +20 -0
- package/dist/server/api.js +76 -47
- package/package.json +1 -1
package/dist/server/api.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/server/api.js
CHANGED
|
@@ -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
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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