@launchsecure/launch-kit 0.0.25 → 0.0.26
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/README.md +50 -0
- package/dist/beacon/beacon.mjs +1016 -0
- package/dist/beacon/beacon.mjs.map +1 -0
- package/dist/beacon/beacon.umd.js +87 -0
- package/dist/beacon/beacon.umd.js.map +1 -0
- package/dist/beacon/index-DAIDnjfR.mjs +513 -0
- package/dist/beacon/index-DAIDnjfR.mjs.map +1 -0
- package/dist/beacon/types/capture/element.d.ts +3 -0
- package/dist/beacon/types/capture/element.d.ts.map +1 -0
- package/dist/beacon/types/capture/framework.d.ts +3 -0
- package/dist/beacon/types/capture/framework.d.ts.map +1 -0
- package/dist/beacon/types/capture/metadata.d.ts +3 -0
- package/dist/beacon/types/capture/metadata.d.ts.map +1 -0
- package/dist/beacon/types/capture/overlay.d.ts +7 -0
- package/dist/beacon/types/capture/overlay.d.ts.map +1 -0
- package/dist/beacon/types/capture/picker.d.ts +12 -0
- package/dist/beacon/types/capture/picker.d.ts.map +1 -0
- package/dist/beacon/types/capture/screenshot.d.ts +7 -0
- package/dist/beacon/types/capture/screenshot.d.ts.map +1 -0
- package/dist/beacon/types/capture/selector.d.ts +2 -0
- package/dist/beacon/types/capture/selector.d.ts.map +1 -0
- package/dist/beacon/types/element.d.ts +50 -0
- package/dist/beacon/types/element.d.ts.map +1 -0
- package/dist/beacon/types/index.d.ts +4 -0
- package/dist/beacon/types/index.d.ts.map +1 -0
- package/dist/beacon/types/transport/submit.d.ts +3 -0
- package/dist/beacon/types/transport/submit.d.ts.map +1 -0
- package/dist/beacon/types/types.d.ts +88 -0
- package/dist/beacon/types/types.d.ts.map +1 -0
- package/dist/beacon/types/ui/button.d.ts +2 -0
- package/dist/beacon/types/ui/button.d.ts.map +1 -0
- package/dist/beacon/types/ui/drawer.d.ts +31 -0
- package/dist/beacon/types/ui/drawer.d.ts.map +1 -0
- package/dist/beacon/types/ui/icons.d.ts +9 -0
- package/dist/beacon/types/ui/icons.d.ts.map +1 -0
- package/dist/beacon/types/ui/pick-mode-overlay.d.ts +25 -0
- package/dist/beacon/types/ui/pick-mode-overlay.d.ts.map +1 -0
- package/dist/beacon/types/ui/pin-popover.d.ts +14 -0
- package/dist/beacon/types/ui/pin-popover.d.ts.map +1 -0
- package/dist/chart-client/assets/{index-C8ANseEa.js → index-Bk1hawjD.js} +63 -58
- package/dist/chart-client/assets/index-DpaGa3bY.css +1 -0
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/index-Bfel4OQ5.css +32 -0
- package/dist/client/assets/{index-Ds9UP_cj.js → index-eC-WuUWB.js} +58 -58
- package/dist/client/index.html +2 -2
- package/dist/council-client/assets/{index-Dc41S-R2.js → index-Cs_MVXHf.js} +14 -14
- package/dist/council-client/assets/index-P5kMsT5a.css +1 -0
- package/dist/council-client/index.html +2 -2
- package/dist/deck-client/assets/{_baseUniq-2gclQXo7.js → _baseUniq-C2xT_eYu.js} +1 -1
- package/dist/deck-client/assets/{arc-DcMY5Wm0.js → arc-CmVL9pGd.js} +1 -1
- package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-B8iirmmJ.js → architectureDiagram-Q4EWVU46-BSFgdjve.js} +1 -1
- package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-B4JBLjmJ.js → blockDiagram-DXYQGD6D-DuLzscvP.js} +1 -1
- package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-CojrJAk8.js → c4Diagram-AHTNJAMY-CfCJB8eY.js} +1 -1
- package/dist/deck-client/assets/channel-B4aNO8ZB.js +1 -0
- package/dist/deck-client/assets/{chunk-4BX2VUAB-Bmb_BMDo.js → chunk-4BX2VUAB-DxmLYTWZ.js} +1 -1
- package/dist/deck-client/assets/{chunk-4TB4RGXK-CumBy8qe.js → chunk-4TB4RGXK-CCnf7GFE.js} +1 -1
- package/dist/deck-client/assets/{chunk-55IACEB6-Ka8Hb1wD.js → chunk-55IACEB6-Db9DApcj.js} +1 -1
- package/dist/deck-client/assets/{chunk-EDXVE4YY-B3sIPiQo.js → chunk-EDXVE4YY-DmYDq8ZI.js} +1 -1
- package/dist/deck-client/assets/{chunk-FMBD7UC4-C1tYkaqu.js → chunk-FMBD7UC4-BGhUlF20.js} +1 -1
- package/dist/deck-client/assets/{chunk-OYMX7WX6-D7Wacbky.js → chunk-OYMX7WX6-CpEnicQZ.js} +1 -1
- package/dist/deck-client/assets/{chunk-QZHKN3VN-ChXI0vO3.js → chunk-QZHKN3VN-Doa7LKwf.js} +1 -1
- package/dist/deck-client/assets/{chunk-YZCP3GAM-BXhiqf8u.js → chunk-YZCP3GAM-CpkIlH6V.js} +1 -1
- package/dist/deck-client/assets/classDiagram-6PBFFD2Q-BHTI0yWz.js +1 -0
- package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-BHTI0yWz.js +1 -0
- package/dist/deck-client/assets/clone-HduFm7qU.js +1 -0
- package/dist/deck-client/assets/{cose-bilkent-S5V4N54A-Bqp3p68D.js → cose-bilkent-S5V4N54A-Bkh8Bfcb.js} +1 -1
- package/dist/deck-client/assets/{dagre-KV5264BT-BS-rtyhZ.js → dagre-KV5264BT-Bp0XpTgH.js} +1 -1
- package/dist/deck-client/assets/{diagram-5BDNPKRD-BIrj9YGI.js → diagram-5BDNPKRD-ZHiyGYPQ.js} +1 -1
- package/dist/deck-client/assets/{diagram-G4DWMVQ6-noHWPIg4.js → diagram-G4DWMVQ6-BW-Q8_H5.js} +1 -1
- package/dist/deck-client/assets/{diagram-MMDJMWI5-C2qHxvqV.js → diagram-MMDJMWI5-6I3LTafu.js} +1 -1
- package/dist/deck-client/assets/{diagram-TYMM5635-BytnGQr-.js → diagram-TYMM5635-CyM5YK28.js} +1 -1
- package/dist/deck-client/assets/{erDiagram-SMLLAGMA-BfK5m2YQ.js → erDiagram-SMLLAGMA-CjNxVJHk.js} +1 -1
- package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-Cq925G1Z.js → flowDiagram-DWJPFMVM-BDQHuAJR.js} +1 -1
- package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-DhhHPAmj.js → ganttDiagram-T4ZO3ILL-B7MnkpbP.js} +1 -1
- package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-B3Lc0h9q.js → gitGraphDiagram-UUTBAWPF-C9dZAcYD.js} +1 -1
- package/dist/deck-client/assets/{graph-RTawgVWm.js → graph-CjdBnzUy.js} +1 -1
- package/dist/deck-client/assets/{index-BfIfJXmS.js → index-DeIVPW63.js} +68 -68
- package/dist/deck-client/assets/index-LKZDAS9S.css +1 -0
- package/dist/deck-client/assets/{infoDiagram-42DDH7IO-BlR584kX.js → infoDiagram-42DDH7IO-C7d3iRC3.js} +1 -1
- package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-DygKoNGY.js → ishikawaDiagram-UXIWVN3A-BcYGKj09.js} +1 -1
- package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-BnaiYp9N.js → journeyDiagram-VCZTEJTY-DqFlRrOL.js} +1 -1
- package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-BQBUBzJC.js → kanban-definition-6JOO6SKY-BJhPp1NR.js} +1 -1
- package/dist/deck-client/assets/{layout-DeZ8HI1T.js → layout-DIeS6GvK.js} +1 -1
- package/dist/deck-client/assets/{linear-C6roLi_9.js → linear-He_yJy5H.js} +1 -1
- package/dist/deck-client/assets/{min-CbUksbuI.js → min-DQ6Kx06t.js} +1 -1
- package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-iNxV62yN.js → mindmap-definition-QFDTVHPH-sQ62L8T2.js} +1 -1
- package/dist/deck-client/assets/{pieDiagram-DEJITSTG-DHVA0jaG.js → pieDiagram-DEJITSTG-BqCWmU2K.js} +1 -1
- package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-DBeKKLUQ.js → quadrantDiagram-34T5L4WZ-rQ1TJOoe.js} +1 -1
- package/dist/deck-client/assets/{requirementDiagram-MS252O5E-CBwITx7p.js → requirementDiagram-MS252O5E-BO2MPBOM.js} +1 -1
- package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-BtE-1YTU.js → sankeyDiagram-XADWPNL6-BgsHEVex.js} +1 -1
- package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-DN96yPP2.js → sequenceDiagram-FGHM5R23-B3j1yMLU.js} +1 -1
- package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-VUkKC2uJ.js → stateDiagram-FHFEXIEX-C8jFlZou.js} +1 -1
- package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-BoqepHW0.js +1 -0
- package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-oUeZhRns.js → timeline-definition-GMOUNBTQ-tM-qo4Zk.js} +1 -1
- package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-D87fK90n.js → vennDiagram-DHZGUBPP-B0-6kOEu.js} +1 -1
- package/dist/deck-client/assets/wardley-RL74JXVD-HpBk07P-.js +162 -0
- package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-Ca_i0QRA.js → wardleyDiagram-NUSXRM2D-BkA1NLDE.js} +1 -1
- package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-CUOJVIvq.js → xychartDiagram-5P7HB3ND-CEKGSuI-.js} +1 -1
- package/dist/deck-client/index.html +2 -2
- package/dist/server/chart-serve.js +990 -116
- package/dist/server/cli.js +28413 -6982
- package/dist/server/council-entry.js +0 -0
- package/dist/server/deck-mcp-entry.js +332 -3
- package/dist/server/deck-serve.js +288 -0
- package/dist/server/fb-wizard.js +0 -0
- package/dist/server/graph/queries/classify.scm +8 -0
- package/dist/server/graph/queries/exports.scm +7 -0
- package/dist/server/graph-mcp-entry.js +1639 -197
- package/dist/server/recall-entry.js +1112 -0
- package/package.json +47 -21
- package/dist/chart-client/assets/index--120d9P9.css +0 -1
- package/dist/client/assets/index-Bf8zdL3x.css +0 -32
- package/dist/council-client/assets/index-CofZh7pS.css +0 -1
- package/dist/deck-client/assets/channel-ERh5jKXV.js +0 -1
- package/dist/deck-client/assets/classDiagram-6PBFFD2Q-CMi1Gaev.js +0 -1
- package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-CMi1Gaev.js +0 -1
- package/dist/deck-client/assets/clone-DfWhlD4X.js +0 -1
- package/dist/deck-client/assets/index-765AIQ9z.css +0 -1
- package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-CA0IjulK.js +0 -1
- package/dist/deck-client/assets/wardley-RL74JXVD-DYbYcpDp.js +0 -162
- package/dist/server/deck-server/deck-mcp-entry.js +0 -1789
- package/dist/server/deck-server/deck-serve.js +0 -1275
- package/dist/server/server/chart-serve.js +0 -4643
- package/dist/server/server/cli.js +0 -13360
- package/dist/server/server/fb-wizard.js +0 -136
- package/dist/server/server/graph-mcp-entry.js +0 -6776
|
@@ -398,17 +398,20 @@ var init_resolve_paths = __esm({
|
|
|
398
398
|
// src/server/graph/core/ts-extractor.ts
|
|
399
399
|
var ts_extractor_exports = {};
|
|
400
400
|
__export(ts_extractor_exports, {
|
|
401
|
+
ParseCascadeError: () => ParseCascadeError,
|
|
401
402
|
classifyFile: () => classifyFile,
|
|
402
403
|
classifyRouteAgainstMiddleware: () => classifyRouteAgainstMiddleware,
|
|
403
404
|
createQuery: () => createQuery,
|
|
404
405
|
extractAuthWrappersTS: () => extractAuthWrappersTS,
|
|
405
406
|
extractDbCallsTS: () => extractDbCallsTS,
|
|
406
407
|
extractDeep: () => extractDeep,
|
|
408
|
+
extractEffects: () => extractEffects,
|
|
407
409
|
extractMiddlewareAuthTS: () => extractMiddlewareAuthTS,
|
|
408
410
|
initTreeSitter: () => initTreeSitter,
|
|
409
411
|
middlewarePatternToRegex: () => middlewarePatternToRegex,
|
|
410
412
|
parseCodeTS: () => parseCodeTS,
|
|
411
413
|
parseFileTS: () => parseFileTS,
|
|
414
|
+
parseSource: () => parseSource,
|
|
412
415
|
setExtractorConfig: () => setExtractorConfig
|
|
413
416
|
});
|
|
414
417
|
async function initTreeSitter() {
|
|
@@ -442,8 +445,38 @@ function getQuery(name) {
|
|
|
442
445
|
}
|
|
443
446
|
function parseSource(absPath) {
|
|
444
447
|
ensureInit();
|
|
445
|
-
|
|
446
|
-
|
|
448
|
+
let content;
|
|
449
|
+
try {
|
|
450
|
+
const stat = (0, import_node_fs5.statSync)(absPath);
|
|
451
|
+
if (stat.size > MAX_PARSEABLE_BYTES) {
|
|
452
|
+
process.stderr.write(`[lc-extractor] skipping ${absPath}: ${stat.size} bytes exceeds max ${MAX_PARSEABLE_BYTES}
|
|
453
|
+
`);
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
content = (0, import_node_fs5.readFileSync)(absPath, "utf-8");
|
|
457
|
+
} catch (e) {
|
|
458
|
+
process.stderr.write(`[lc-extractor] read failed for ${absPath}: ${e instanceof Error ? e.message : String(e)}
|
|
459
|
+
`);
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
const tree = parserInstance.parse(content);
|
|
464
|
+
consecutiveParseFailures = 0;
|
|
465
|
+
return tree;
|
|
466
|
+
} catch (e) {
|
|
467
|
+
consecutiveParseFailures++;
|
|
468
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
469
|
+
process.stderr.write(
|
|
470
|
+
`[lc-extractor] parse failed for ${absPath}: ${msg} (consecutive failures: ${consecutiveParseFailures}/${MAX_CONSECUTIVE_PARSE_FAILURES})
|
|
471
|
+
`
|
|
472
|
+
);
|
|
473
|
+
if (consecutiveParseFailures >= MAX_CONSECUTIVE_PARSE_FAILURES) {
|
|
474
|
+
const tripCount = consecutiveParseFailures;
|
|
475
|
+
consecutiveParseFailures = 0;
|
|
476
|
+
throw new ParseCascadeError(absPath, tripCount);
|
|
477
|
+
}
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
447
480
|
}
|
|
448
481
|
function parseCodeTS(code) {
|
|
449
482
|
ensureInit();
|
|
@@ -483,8 +516,20 @@ function childrenOfType(node, type) {
|
|
|
483
516
|
function childOfType(node, type) {
|
|
484
517
|
return node.children.find((n) => n.type === type);
|
|
485
518
|
}
|
|
519
|
+
function emptyParsedFile(absPath) {
|
|
520
|
+
return {
|
|
521
|
+
name: absPath.split("/").pop() ?? absPath,
|
|
522
|
+
exports: [],
|
|
523
|
+
imports: [],
|
|
524
|
+
reExports: [],
|
|
525
|
+
jsxElements: /* @__PURE__ */ new Set(),
|
|
526
|
+
navigations: [],
|
|
527
|
+
fetchCalls: []
|
|
528
|
+
};
|
|
529
|
+
}
|
|
486
530
|
function parseFileTS(absPath) {
|
|
487
531
|
const tree = parseSource(absPath);
|
|
532
|
+
if (!tree) return emptyParsedFile(absPath);
|
|
488
533
|
const root = tree.rootNode;
|
|
489
534
|
const imports = [];
|
|
490
535
|
const importStatements = childrenOfType(root, "import_statement");
|
|
@@ -679,6 +724,7 @@ function parseFileTS(absPath) {
|
|
|
679
724
|
}
|
|
680
725
|
function extractDbCallsTS(absPath) {
|
|
681
726
|
const tree = parseSource(absPath);
|
|
727
|
+
if (!tree) return [];
|
|
682
728
|
const root = tree.rootNode;
|
|
683
729
|
const dbQuery = getQuery("db-calls");
|
|
684
730
|
const matches = dbQuery.matches(root);
|
|
@@ -739,7 +785,9 @@ function classifyFile(absPath) {
|
|
|
739
785
|
const fileName = require("path").basename(absPath);
|
|
740
786
|
if (fileName.includes(".test.") || fileName.includes(".spec.") || fileName.includes("__test")) return "test";
|
|
741
787
|
if (fileName.includes(".stories.")) return "story";
|
|
788
|
+
if (fileName === "middleware.ts" || fileName === "middleware.tsx") return "middleware";
|
|
742
789
|
const tree = parseSource(absPath);
|
|
790
|
+
if (!tree) return "util";
|
|
743
791
|
const root = tree.rootNode;
|
|
744
792
|
const classifyQuery = getQuery("classify");
|
|
745
793
|
const captures = classifyQuery.captures(root);
|
|
@@ -758,6 +806,7 @@ function classifyFile(absPath) {
|
|
|
758
806
|
}
|
|
759
807
|
function extractAuthWrappersTS(absPath) {
|
|
760
808
|
const tree = parseSource(absPath);
|
|
809
|
+
if (!tree) return /* @__PURE__ */ new Set();
|
|
761
810
|
const root = tree.rootNode;
|
|
762
811
|
const wrapperQuery = getQuery("wrappers");
|
|
763
812
|
const matches = wrapperQuery.matches(root);
|
|
@@ -801,6 +850,9 @@ function extractAuthWrappersTS(absPath) {
|
|
|
801
850
|
return wrappers;
|
|
802
851
|
}
|
|
803
852
|
function inferIntentFromName(name) {
|
|
853
|
+
if (TRUST_AS_PROTECT_KEYS.has(name)) {
|
|
854
|
+
return { intent: "protect", hint: `Next.js config.${name} \u2014 paths matched run through middleware` };
|
|
855
|
+
}
|
|
804
856
|
for (const re of EXEMPT_NAME_PATTERNS) {
|
|
805
857
|
if (re.test(name)) return { intent: "exempt", hint: `name "${name}" matches /${re.source}/` };
|
|
806
858
|
}
|
|
@@ -877,6 +929,7 @@ function detectFallthroughProtect(root) {
|
|
|
877
929
|
function extractMiddlewareAuthTS(absPath) {
|
|
878
930
|
if (!require("node:fs").existsSync(absPath)) return null;
|
|
879
931
|
const tree = parseSource(absPath);
|
|
932
|
+
if (!tree) return null;
|
|
880
933
|
const root = tree.rootNode;
|
|
881
934
|
const matchers = [];
|
|
882
935
|
for (const stmt of root.children) {
|
|
@@ -920,6 +973,16 @@ function extractMiddlewareAuthTS(absPath) {
|
|
|
920
973
|
};
|
|
921
974
|
}
|
|
922
975
|
function middlewarePatternToRegex(pattern) {
|
|
976
|
+
const negLookaheadMatch = /^(\/?)\(\(\?\!([^)]+)\)(\.\*|\.\+)\)$/.exec(pattern);
|
|
977
|
+
if (negLookaheadMatch) {
|
|
978
|
+
const [, lead, altsRaw, body] = negLookaheadMatch;
|
|
979
|
+
const escapedAlts = altsRaw.split("|").map((alt) => alt.trim().replace(/[.+*?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
980
|
+
try {
|
|
981
|
+
return new RegExp(`^${lead}(?!${escapedAlts})${body}$`);
|
|
982
|
+
} catch {
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
923
986
|
if (/\(\?\!/.test(pattern)) return null;
|
|
924
987
|
if (pattern.startsWith("(")) return null;
|
|
925
988
|
let src = "^";
|
|
@@ -977,8 +1040,163 @@ function classifyRouteAgainstMiddleware(routePath, info) {
|
|
|
977
1040
|
function trunc(s, max = 120) {
|
|
978
1041
|
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
979
1042
|
}
|
|
1043
|
+
function dedup(arr) {
|
|
1044
|
+
return Array.from(new Set(arr));
|
|
1045
|
+
}
|
|
1046
|
+
function firstStringArg(callExpr) {
|
|
1047
|
+
const args = callExpr.childForFieldName("arguments");
|
|
1048
|
+
if (!args) return null;
|
|
1049
|
+
for (const c of args.namedChildren) {
|
|
1050
|
+
if (c.type === "string") {
|
|
1051
|
+
const frag = c.namedChildren.find((cc) => cc.type === "string_fragment");
|
|
1052
|
+
return frag ? frag.text : c.text.replace(/^['"`]|['"`]$/g, "");
|
|
1053
|
+
}
|
|
1054
|
+
if (c.type === "template_string") {
|
|
1055
|
+
return c.text.replace(/^`|`$/g, "");
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
function isFunctionLike(node) {
|
|
1061
|
+
return node.type === "arrow_function" || node.type === "function_expression" || node.type === "function_declaration" || node.type === "method_definition" || node.type === "generator_function" || node.type === "generator_function_declaration";
|
|
1062
|
+
}
|
|
1063
|
+
function stringLiteralValue(n) {
|
|
1064
|
+
if (n.type !== "string") return null;
|
|
1065
|
+
const frag = n.namedChildren.find((c) => c.type === "string_fragment");
|
|
1066
|
+
if (frag) return frag.text;
|
|
1067
|
+
return n.text.replace(/^['"`]|['"`]$/g, "");
|
|
1068
|
+
}
|
|
1069
|
+
function nthStringArg(callExpr, idx) {
|
|
1070
|
+
const args = callExpr.childForFieldName("arguments");
|
|
1071
|
+
if (!args) return null;
|
|
1072
|
+
const child = args.namedChildren[idx];
|
|
1073
|
+
return child ? stringLiteralValue(child) : null;
|
|
1074
|
+
}
|
|
1075
|
+
function objectArgIdValues(callExpr) {
|
|
1076
|
+
const args = callExpr.childForFieldName("arguments");
|
|
1077
|
+
if (!args) return [];
|
|
1078
|
+
const out = [];
|
|
1079
|
+
for (const arg of args.namedChildren) {
|
|
1080
|
+
if (arg.type !== "object") continue;
|
|
1081
|
+
for (const pair of arg.namedChildren) {
|
|
1082
|
+
if (pair.type !== "pair") continue;
|
|
1083
|
+
const key = pair.childForFieldName("key");
|
|
1084
|
+
const val = pair.childForFieldName("value");
|
|
1085
|
+
if (!key || !val) continue;
|
|
1086
|
+
const keyText = key.type === "property_identifier" ? key.text : stringLiteralValue(key) ?? key.text;
|
|
1087
|
+
if (keyText !== "id") continue;
|
|
1088
|
+
const strVal = stringLiteralValue(val);
|
|
1089
|
+
if (strVal) out.push(strVal);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return out;
|
|
1093
|
+
}
|
|
1094
|
+
function extractEffects(node, moduleOnly = false) {
|
|
1095
|
+
const calls = [];
|
|
1096
|
+
const dom_writes = [];
|
|
1097
|
+
const subscribes = [];
|
|
1098
|
+
const timers = [];
|
|
1099
|
+
const persists = [];
|
|
1100
|
+
const fetches = [];
|
|
1101
|
+
const globals = [];
|
|
1102
|
+
const dom_ids = [];
|
|
1103
|
+
const window_events = [];
|
|
1104
|
+
const storage_keys = [];
|
|
1105
|
+
const fetch_urls = [];
|
|
1106
|
+
function visit(n, depth) {
|
|
1107
|
+
if (moduleOnly && depth > 0 && isFunctionLike(n)) return;
|
|
1108
|
+
if (n.type === "call_expression") {
|
|
1109
|
+
const fn = n.childForFieldName("function");
|
|
1110
|
+
if (fn) {
|
|
1111
|
+
const fnText = fn.text;
|
|
1112
|
+
calls.push(fnText);
|
|
1113
|
+
for (const id of objectArgIdValues(n)) dom_ids.push(id);
|
|
1114
|
+
if (fn.type === "identifier") {
|
|
1115
|
+
if (TIMER_FNS.has(fnText)) timers.push(fnText);
|
|
1116
|
+
if (fnText === "fetch") {
|
|
1117
|
+
const url = firstStringArg(n);
|
|
1118
|
+
fetches.push(url ? `fetch("${url}")` : "fetch(...)");
|
|
1119
|
+
if (url) fetch_urls.push(url);
|
|
1120
|
+
}
|
|
1121
|
+
} else if (fn.type === "member_expression") {
|
|
1122
|
+
const obj = fn.childForFieldName("object");
|
|
1123
|
+
const prop = fn.childForFieldName("property");
|
|
1124
|
+
const objText = obj?.text ?? "";
|
|
1125
|
+
const propText = prop?.text ?? "";
|
|
1126
|
+
if (STORAGE_OBJECTS.has(objText)) {
|
|
1127
|
+
const key = firstStringArg(n);
|
|
1128
|
+
persists.push(key ? `${objText}.${propText}("${key}")` : `${objText}.${propText}(\u2026)`);
|
|
1129
|
+
if (key) storage_keys.push(key);
|
|
1130
|
+
} else if (propText === "addEventListener") {
|
|
1131
|
+
const event = firstStringArg(n);
|
|
1132
|
+
subscribes.push(event ? `${objText}.addEventListener("${event}")` : `${objText}.addEventListener(\u2026)`);
|
|
1133
|
+
if (event && (objText === "window" || objText === "document")) {
|
|
1134
|
+
window_events.push(`${objText}:${event}`);
|
|
1135
|
+
}
|
|
1136
|
+
} else if (propText === "removeEventListener") {
|
|
1137
|
+
const event = firstStringArg(n);
|
|
1138
|
+
subscribes.push(event ? `${objText}.removeEventListener("${event}")` : `${objText}.removeEventListener(\u2026)`);
|
|
1139
|
+
} else if (propText === "subscribe" || propText === "on") {
|
|
1140
|
+
subscribes.push(`${objText}.${propText}(\u2026)`);
|
|
1141
|
+
} else if (DOM_METHOD_NAMES.has(propText)) {
|
|
1142
|
+
dom_writes.push(`${objText}.${propText}(\u2026)`);
|
|
1143
|
+
if (propText === "setAttribute" && nthStringArg(n, 0) === "id") {
|
|
1144
|
+
const idVal = nthStringArg(n, 1);
|
|
1145
|
+
if (idVal) dom_ids.push(idVal);
|
|
1146
|
+
}
|
|
1147
|
+
} else if (objText.endsWith(".classList") && CLASSLIST_METHODS.has(propText)) {
|
|
1148
|
+
dom_writes.push(`${objText}.${propText}(\u2026)`);
|
|
1149
|
+
} else if (objText === "history" && HISTORY_METHODS.has(propText)) {
|
|
1150
|
+
globals.push(`history.${propText}(\u2026)`);
|
|
1151
|
+
} else if (objText === "location" && LOCATION_METHODS.has(propText)) {
|
|
1152
|
+
globals.push(`location.${propText}(\u2026)`);
|
|
1153
|
+
} else if (objText === "window" && propText === "matchMedia") {
|
|
1154
|
+
globals.push("window.matchMedia(\u2026)");
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
} else if (n.type === "assignment_expression") {
|
|
1159
|
+
const lhs = n.children.find((c) => c.type !== "=");
|
|
1160
|
+
const rhs = n.childForFieldName("right");
|
|
1161
|
+
if (lhs?.type === "member_expression") {
|
|
1162
|
+
const prop = lhs.childForFieldName("property");
|
|
1163
|
+
const propText = prop?.text ?? "";
|
|
1164
|
+
const lhsText = lhs.text;
|
|
1165
|
+
if (lhsText.startsWith("document.cookie")) {
|
|
1166
|
+
persists.push("document.cookie =");
|
|
1167
|
+
} else if (lhsText.startsWith("document.title") || lhsText.startsWith("location.")) {
|
|
1168
|
+
globals.push(`${lhsText} =`);
|
|
1169
|
+
} else if (ASSIGN_DOM_PROPS.has(propText) || lhsText.includes(".style.") || lhsText.includes(".dataset.")) {
|
|
1170
|
+
dom_writes.push(`${lhsText} =`);
|
|
1171
|
+
if (propText === "id" && rhs) {
|
|
1172
|
+
const idVal = stringLiteralValue(rhs);
|
|
1173
|
+
if (idVal) dom_ids.push(idVal);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
for (const child of n.children) visit(child, depth + 1);
|
|
1179
|
+
}
|
|
1180
|
+
visit(node, 0);
|
|
1181
|
+
const out = {};
|
|
1182
|
+
if (calls.length) out.calls = dedup(calls);
|
|
1183
|
+
if (dom_writes.length) out.dom_writes = dedup(dom_writes);
|
|
1184
|
+
if (subscribes.length) out.subscribes = dedup(subscribes);
|
|
1185
|
+
if (timers.length) out.timers = dedup(timers);
|
|
1186
|
+
if (persists.length) out.persists = dedup(persists);
|
|
1187
|
+
if (fetches.length) out.fetches = dedup(fetches);
|
|
1188
|
+
if (globals.length) out.globals = dedup(globals);
|
|
1189
|
+
if (dom_ids.length) out.dom_ids = dedup(dom_ids);
|
|
1190
|
+
if (window_events.length) out.window_events = dedup(window_events);
|
|
1191
|
+
if (storage_keys.length) out.storage_keys = dedup(storage_keys);
|
|
1192
|
+
if (fetch_urls.length) out.fetch_urls = dedup(fetch_urls);
|
|
1193
|
+
return out;
|
|
1194
|
+
}
|
|
980
1195
|
function extractDeep(absPath) {
|
|
981
1196
|
const tree = parseSource(absPath);
|
|
1197
|
+
if (!tree) {
|
|
1198
|
+
return { elements: [], stateVars: [], conditions: [], variables: [], responses: [], params: [] };
|
|
1199
|
+
}
|
|
982
1200
|
const root = tree.rootNode;
|
|
983
1201
|
const elements = [];
|
|
984
1202
|
const elQuery = getQuery("deep/jsx-semantic");
|
|
@@ -1071,12 +1289,18 @@ function extractDeep(absPath) {
|
|
|
1071
1289
|
const caps = captureMap(m);
|
|
1072
1290
|
const declNode = m.captures.find((c) => c.name === "var.decl" || c.name === "var.destructured" || c.name === "var.array")?.node;
|
|
1073
1291
|
const kind = declNode?.children.find((n) => n.type === "const" || n.type === "let" || n.type === "var")?.type ?? "const";
|
|
1292
|
+
const initNode = m.captures.find((c) => c.name === "var.init")?.node;
|
|
1074
1293
|
if (caps["var.name"] && caps["var.init"]) {
|
|
1075
|
-
|
|
1294
|
+
const variable = {
|
|
1076
1295
|
name: caps["var.name"],
|
|
1077
1296
|
kind,
|
|
1078
1297
|
init: trunc(caps["var.init"], 100)
|
|
1079
|
-
}
|
|
1298
|
+
};
|
|
1299
|
+
if (initNode && isFunctionLike(initNode)) {
|
|
1300
|
+
const eff = extractEffects(initNode);
|
|
1301
|
+
if (Object.keys(eff).length > 0) variable.effects = eff;
|
|
1302
|
+
}
|
|
1303
|
+
variables.push(variable);
|
|
1080
1304
|
}
|
|
1081
1305
|
if (caps["var.destructured.obj"]) {
|
|
1082
1306
|
variables.push({
|
|
@@ -1128,9 +1352,23 @@ function extractDeep(absPath) {
|
|
|
1128
1352
|
params.push({ name: caps["param.body"], source: "body" });
|
|
1129
1353
|
}
|
|
1130
1354
|
}
|
|
1131
|
-
|
|
1355
|
+
const fileEffects = extractEffects(
|
|
1356
|
+
root,
|
|
1357
|
+
/* moduleOnly */
|
|
1358
|
+
false
|
|
1359
|
+
);
|
|
1360
|
+
const hasEffects = Object.keys(fileEffects).length > 0;
|
|
1361
|
+
return {
|
|
1362
|
+
elements,
|
|
1363
|
+
stateVars,
|
|
1364
|
+
conditions,
|
|
1365
|
+
variables,
|
|
1366
|
+
responses,
|
|
1367
|
+
params,
|
|
1368
|
+
...hasEffects ? { effects: fileEffects } : {}
|
|
1369
|
+
};
|
|
1132
1370
|
}
|
|
1133
|
-
var import_node_fs5, import_node_path5, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, SUPABASE_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods, INLINE_AUTH_IMPORTS, EXEMPT_NAME_PATTERNS, PROTECT_NAME_PATTERNS;
|
|
1371
|
+
var import_node_fs5, import_node_path5, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, MAX_PARSEABLE_BYTES, MAX_CONSECUTIVE_PARSE_FAILURES, consecutiveParseFailures, ParseCascadeError, PRISMA_MUTATION_METHODS_BUILTIN, SUPABASE_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods, INLINE_AUTH_IMPORTS, EXEMPT_NAME_PATTERNS, PROTECT_NAME_PATTERNS, TRUST_AS_PROTECT_KEYS, TIMER_FNS, DOM_METHOD_NAMES, CLASSLIST_METHODS, STORAGE_OBJECTS, HISTORY_METHODS, LOCATION_METHODS, ASSIGN_DOM_PROPS;
|
|
1134
1372
|
var init_ts_extractor = __esm({
|
|
1135
1373
|
"src/server/graph/core/ts-extractor.ts"() {
|
|
1136
1374
|
"use strict";
|
|
@@ -1143,6 +1381,19 @@ var init_ts_extractor = __esm({
|
|
|
1143
1381
|
return (0, import_node_path5.join)((0, import_node_path5.dirname)(__filename), "graph", "queries");
|
|
1144
1382
|
})();
|
|
1145
1383
|
queryCache = /* @__PURE__ */ new Map();
|
|
1384
|
+
MAX_PARSEABLE_BYTES = 2 * 1024 * 1024;
|
|
1385
|
+
MAX_CONSECUTIVE_PARSE_FAILURES = 10;
|
|
1386
|
+
consecutiveParseFailures = 0;
|
|
1387
|
+
ParseCascadeError = class extends Error {
|
|
1388
|
+
constructor(lastPath, failureCount) {
|
|
1389
|
+
super(
|
|
1390
|
+
`tree-sitter parse cascade: ${failureCount} consecutive WASM failures (last file: ${lastPath}). The shared Parser's WASM heap is likely corrupted; aborting regen so the graph isn't silently degraded. Restart the chart server to reinitialize, then re-run generate_graph.`
|
|
1391
|
+
);
|
|
1392
|
+
this.lastPath = lastPath;
|
|
1393
|
+
this.failureCount = failureCount;
|
|
1394
|
+
this.name = "ParseCascadeError";
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1146
1397
|
PRISMA_MUTATION_METHODS_BUILTIN = [
|
|
1147
1398
|
"create",
|
|
1148
1399
|
"createMany",
|
|
@@ -1192,6 +1443,49 @@ var init_ts_extractor = __esm({
|
|
|
1192
1443
|
/^admin_?routes?/i,
|
|
1193
1444
|
/^secured/i
|
|
1194
1445
|
];
|
|
1446
|
+
TRUST_AS_PROTECT_KEYS = /* @__PURE__ */ new Set(["matcher", "matchers"]);
|
|
1447
|
+
TIMER_FNS = /* @__PURE__ */ new Set([
|
|
1448
|
+
"setInterval",
|
|
1449
|
+
"setTimeout",
|
|
1450
|
+
"clearInterval",
|
|
1451
|
+
"clearTimeout",
|
|
1452
|
+
"requestAnimationFrame",
|
|
1453
|
+
"cancelAnimationFrame",
|
|
1454
|
+
"queueMicrotask"
|
|
1455
|
+
]);
|
|
1456
|
+
DOM_METHOD_NAMES = /* @__PURE__ */ new Set([
|
|
1457
|
+
"setAttribute",
|
|
1458
|
+
"removeAttribute",
|
|
1459
|
+
"appendChild",
|
|
1460
|
+
"removeChild",
|
|
1461
|
+
"replaceChildren",
|
|
1462
|
+
"replaceWith",
|
|
1463
|
+
"insertBefore",
|
|
1464
|
+
"append",
|
|
1465
|
+
"prepend",
|
|
1466
|
+
"remove",
|
|
1467
|
+
"before",
|
|
1468
|
+
"after",
|
|
1469
|
+
"insertAdjacentHTML",
|
|
1470
|
+
"insertAdjacentElement"
|
|
1471
|
+
]);
|
|
1472
|
+
CLASSLIST_METHODS = /* @__PURE__ */ new Set(["add", "remove", "toggle", "replace"]);
|
|
1473
|
+
STORAGE_OBJECTS = /* @__PURE__ */ new Set(["localStorage", "sessionStorage"]);
|
|
1474
|
+
HISTORY_METHODS = /* @__PURE__ */ new Set(["pushState", "replaceState", "back", "forward", "go"]);
|
|
1475
|
+
LOCATION_METHODS = /* @__PURE__ */ new Set(["assign", "replace", "reload"]);
|
|
1476
|
+
ASSIGN_DOM_PROPS = /* @__PURE__ */ new Set([
|
|
1477
|
+
"textContent",
|
|
1478
|
+
"innerHTML",
|
|
1479
|
+
"innerText",
|
|
1480
|
+
"value",
|
|
1481
|
+
"src",
|
|
1482
|
+
"href",
|
|
1483
|
+
"className",
|
|
1484
|
+
"id",
|
|
1485
|
+
"checked",
|
|
1486
|
+
"selected",
|
|
1487
|
+
"disabled"
|
|
1488
|
+
]);
|
|
1195
1489
|
}
|
|
1196
1490
|
});
|
|
1197
1491
|
|
|
@@ -1277,6 +1571,8 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
|
|
|
1277
1571
|
function classifyType(absPath, id) {
|
|
1278
1572
|
const contentType = classifyFile(absPath);
|
|
1279
1573
|
if (contentType === "lib" && id.startsWith("server/mcp/")) return "mcp-tool";
|
|
1574
|
+
if (/^app\/(.+\/)?page\.tsx$/.test(id)) return "page";
|
|
1575
|
+
if (/^app\/(.+\/)?layout\.tsx$/.test(id)) return "layout";
|
|
1280
1576
|
return contentType;
|
|
1281
1577
|
}
|
|
1282
1578
|
function extractRoute(id) {
|
|
@@ -1587,6 +1883,7 @@ function generate(rootDir) {
|
|
|
1587
1883
|
variables: deep.variables,
|
|
1588
1884
|
responses: deep.responses,
|
|
1589
1885
|
params: deep.params,
|
|
1886
|
+
...deep.effects ? { effects: deep.effects } : {},
|
|
1590
1887
|
_dbCalls: dbCalls
|
|
1591
1888
|
// temp: used for cross-ref building below
|
|
1592
1889
|
});
|
|
@@ -1606,6 +1903,7 @@ function generate(rootDir) {
|
|
|
1606
1903
|
stateVars: deep.stateVars,
|
|
1607
1904
|
conditions: deep.conditions,
|
|
1608
1905
|
variables: deep.variables,
|
|
1906
|
+
...deep.effects ? { effects: deep.effects } : {},
|
|
1609
1907
|
...authWrappers.length > 0 ? { auth: authWrappers } : {},
|
|
1610
1908
|
...dbCalls.length > 0 ? { _dbCalls: dbCalls } : {}
|
|
1611
1909
|
});
|
|
@@ -2053,6 +2351,7 @@ var init_typescript_project = __esm({
|
|
|
2053
2351
|
config: "ui",
|
|
2054
2352
|
lib: "ui",
|
|
2055
2353
|
"mcp-tool": "ui",
|
|
2354
|
+
middleware: "ui",
|
|
2056
2355
|
external: "ui"
|
|
2057
2356
|
};
|
|
2058
2357
|
typescriptProjectParser = {
|
|
@@ -3962,6 +4261,119 @@ var init_static_ref_scanner = __esm({
|
|
|
3962
4261
|
}
|
|
3963
4262
|
});
|
|
3964
4263
|
|
|
4264
|
+
// src/server/graph/parsers/crosslayer/middleware-gates.ts
|
|
4265
|
+
function toNodeId4(srcDir, rootDir, absPath) {
|
|
4266
|
+
const relFromSrc = (0, import_node_path12.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
4267
|
+
if (relFromSrc.startsWith("..")) {
|
|
4268
|
+
return (0, import_node_path12.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
4269
|
+
}
|
|
4270
|
+
return relFromSrc;
|
|
4271
|
+
}
|
|
4272
|
+
function collectTargets(apiOutput, uiOutput) {
|
|
4273
|
+
const out = [];
|
|
4274
|
+
for (const n of apiOutput?.nodes ?? []) {
|
|
4275
|
+
if (n.type !== "endpoint") continue;
|
|
4276
|
+
const path3 = n.path;
|
|
4277
|
+
if (typeof path3 !== "string" || !path3) continue;
|
|
4278
|
+
out.push({ id: n.id, route: path3, layer: "api" });
|
|
4279
|
+
}
|
|
4280
|
+
for (const n of uiOutput?.nodes ?? []) {
|
|
4281
|
+
if (n.type !== "page") continue;
|
|
4282
|
+
const route = n.route;
|
|
4283
|
+
if (typeof route !== "string" || !route) continue;
|
|
4284
|
+
out.push({ id: n.id, route, layer: "ui" });
|
|
4285
|
+
}
|
|
4286
|
+
return out;
|
|
4287
|
+
}
|
|
4288
|
+
var import_node_path12, middlewareGatesParser;
|
|
4289
|
+
var init_middleware_gates = __esm({
|
|
4290
|
+
"src/server/graph/parsers/crosslayer/middleware-gates.ts"() {
|
|
4291
|
+
"use strict";
|
|
4292
|
+
import_node_path12 = require("node:path");
|
|
4293
|
+
init_ts_extractor();
|
|
4294
|
+
init_config();
|
|
4295
|
+
init_resolve_paths();
|
|
4296
|
+
middlewareGatesParser = {
|
|
4297
|
+
id: "middleware-gates",
|
|
4298
|
+
layer: "crosslayer",
|
|
4299
|
+
concern: "auth-gate",
|
|
4300
|
+
detect(rootDir) {
|
|
4301
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
4302
|
+
if (!paths) return false;
|
|
4303
|
+
return paths.conventionFiles.some((f) => /middleware\.tsx?$/.test(f));
|
|
4304
|
+
},
|
|
4305
|
+
generate(rootDir, layerOutputs) {
|
|
4306
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
4307
|
+
if (!paths) return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
4308
|
+
const middlewareFiles = paths.conventionFiles.filter((f) => /middleware\.tsx?$/.test(f));
|
|
4309
|
+
if (middlewareFiles.length === 0) {
|
|
4310
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
4311
|
+
}
|
|
4312
|
+
const apiOutput = layerOutputs.get("api");
|
|
4313
|
+
const uiOutput = layerOutputs.get("ui");
|
|
4314
|
+
const targets = collectTargets(apiOutput, uiOutput);
|
|
4315
|
+
const crossRefs = [];
|
|
4316
|
+
const flagged = [];
|
|
4317
|
+
const seenEdge = /* @__PURE__ */ new Set();
|
|
4318
|
+
const seenUnparseable = /* @__PURE__ */ new Set();
|
|
4319
|
+
let protectMatcherCount = 0;
|
|
4320
|
+
for (const file of middlewareFiles) {
|
|
4321
|
+
let info = null;
|
|
4322
|
+
try {
|
|
4323
|
+
info = extractMiddlewareAuthTS(file);
|
|
4324
|
+
} catch {
|
|
4325
|
+
}
|
|
4326
|
+
if (!info || info.matchers.length === 0) continue;
|
|
4327
|
+
const middlewareId = toNodeId4(paths.srcDir, rootDir, file);
|
|
4328
|
+
for (const matcher of info.matchers) {
|
|
4329
|
+
if (matcher.intent !== "protect") continue;
|
|
4330
|
+
protectMatcherCount += 1;
|
|
4331
|
+
for (const pattern of matcher.patterns) {
|
|
4332
|
+
const re = middlewarePatternToRegex(pattern);
|
|
4333
|
+
if (!re) {
|
|
4334
|
+
const key = `${middlewareId}|${pattern}`;
|
|
4335
|
+
if (seenUnparseable.has(key)) continue;
|
|
4336
|
+
seenUnparseable.add(key);
|
|
4337
|
+
flagged.push({
|
|
4338
|
+
source: middlewareId,
|
|
4339
|
+
target: "UNRESOLVED",
|
|
4340
|
+
type: "middleware_pattern_unparseable",
|
|
4341
|
+
label: `pattern "${pattern}" in matcher "${matcher.name}" \u2014 coverage unknown`,
|
|
4342
|
+
confidence: "high"
|
|
4343
|
+
});
|
|
4344
|
+
continue;
|
|
4345
|
+
}
|
|
4346
|
+
for (const target of targets) {
|
|
4347
|
+
if (!re.test(target.route)) continue;
|
|
4348
|
+
const key = `${middlewareId}|${target.id}|protects`;
|
|
4349
|
+
if (seenEdge.has(key)) continue;
|
|
4350
|
+
seenEdge.add(key);
|
|
4351
|
+
crossRefs.push({
|
|
4352
|
+
source: middlewareId,
|
|
4353
|
+
target: target.id,
|
|
4354
|
+
type: "protects",
|
|
4355
|
+
layer: target.layer
|
|
4356
|
+
});
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
return {
|
|
4362
|
+
cross_refs: crossRefs,
|
|
4363
|
+
flagged_edges: flagged,
|
|
4364
|
+
warnings: [],
|
|
4365
|
+
patterns: {
|
|
4366
|
+
middleware_files: middlewareFiles.length,
|
|
4367
|
+
protect_matchers: protectMatcherCount,
|
|
4368
|
+
protects_edges: crossRefs.length,
|
|
4369
|
+
unparseable_patterns: flagged.length
|
|
4370
|
+
}
|
|
4371
|
+
};
|
|
4372
|
+
}
|
|
4373
|
+
};
|
|
4374
|
+
}
|
|
4375
|
+
});
|
|
4376
|
+
|
|
3965
4377
|
// src/server/graph/core/parser-registry.ts
|
|
3966
4378
|
function isMultiLayerParser(p) {
|
|
3967
4379
|
return "layers" in p && Array.isArray(p.layers);
|
|
@@ -3975,7 +4387,8 @@ function registerBuiltins(registry, disabled) {
|
|
|
3975
4387
|
fetchResolverParser,
|
|
3976
4388
|
apiAnnotationsParser,
|
|
3977
4389
|
urlLiteralScannerParser,
|
|
3978
|
-
staticRefScannerParser
|
|
4390
|
+
staticRefScannerParser,
|
|
4391
|
+
middlewareGatesParser
|
|
3979
4392
|
];
|
|
3980
4393
|
for (const parser of builtins) {
|
|
3981
4394
|
if (disabled.has(parser.id)) continue;
|
|
@@ -3985,7 +4398,7 @@ function registerBuiltins(registry, disabled) {
|
|
|
3985
4398
|
function loadCustomParsers(registry, config, rootDir, disabled) {
|
|
3986
4399
|
for (const entry of config.parsers?.custom ?? []) {
|
|
3987
4400
|
try {
|
|
3988
|
-
const absPath = (0,
|
|
4401
|
+
const absPath = (0, import_node_path13.resolve)(rootDir, entry.path);
|
|
3989
4402
|
const mod = require(absPath);
|
|
3990
4403
|
const parser = "default" in mod ? mod.default : mod;
|
|
3991
4404
|
if (disabled.has(parser.id)) continue;
|
|
@@ -4012,11 +4425,11 @@ function createRegistry(config, rootDir) {
|
|
|
4012
4425
|
loadCustomParsers(registry, config, rootDir, disabled);
|
|
4013
4426
|
return registry;
|
|
4014
4427
|
}
|
|
4015
|
-
var
|
|
4428
|
+
var import_node_path13, ParserRegistry;
|
|
4016
4429
|
var init_parser_registry = __esm({
|
|
4017
4430
|
"src/server/graph/core/parser-registry.ts"() {
|
|
4018
4431
|
"use strict";
|
|
4019
|
-
|
|
4432
|
+
import_node_path13 = require("node:path");
|
|
4020
4433
|
init_typescript_project();
|
|
4021
4434
|
init_prisma_schema();
|
|
4022
4435
|
init_sql_migrations();
|
|
@@ -4025,6 +4438,7 @@ var init_parser_registry = __esm({
|
|
|
4025
4438
|
init_url_literal_scanner();
|
|
4026
4439
|
init_static_values();
|
|
4027
4440
|
init_static_ref_scanner();
|
|
4441
|
+
init_middleware_gates();
|
|
4028
4442
|
ParserRegistry = class {
|
|
4029
4443
|
constructor() {
|
|
4030
4444
|
this.singleLayerParsers = /* @__PURE__ */ new Map();
|
|
@@ -4187,7 +4601,7 @@ var init_merge = __esm({
|
|
|
4187
4601
|
|
|
4188
4602
|
// src/server/graph/core/graph-builder.ts
|
|
4189
4603
|
function readGraphFromDisk(rootDir, layer) {
|
|
4190
|
-
const filePath = (0,
|
|
4604
|
+
const filePath = (0, import_node_path14.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
4191
4605
|
if (!(0, import_node_fs13.existsSync)(filePath)) return null;
|
|
4192
4606
|
try {
|
|
4193
4607
|
return JSON.parse((0, import_node_fs13.readFileSync)(filePath, "utf-8"));
|
|
@@ -4286,12 +4700,12 @@ function generateAll(rootDir) {
|
|
|
4286
4700
|
const extras = [...byLayer.keys()].filter((l) => !wellKnownOrder.includes(l)).sort();
|
|
4287
4701
|
return [...wellKnownOrder, ...extras].map((l) => byLayer.get(l)).filter((r) => !!r);
|
|
4288
4702
|
}
|
|
4289
|
-
var import_node_fs13,
|
|
4703
|
+
var import_node_fs13, import_node_path14;
|
|
4290
4704
|
var init_graph_builder = __esm({
|
|
4291
4705
|
"src/server/graph/core/graph-builder.ts"() {
|
|
4292
4706
|
"use strict";
|
|
4293
4707
|
import_node_fs13 = require("node:fs");
|
|
4294
|
-
|
|
4708
|
+
import_node_path14 = require("node:path");
|
|
4295
4709
|
init_config();
|
|
4296
4710
|
init_parser_registry();
|
|
4297
4711
|
init_merge();
|
|
@@ -4330,13 +4744,13 @@ function detectConventionDirs(rootDir, extraConventionDirs = []) {
|
|
|
4330
4744
|
const conventionDirs = [...CONVENTION_DIRS_BUILTIN, ...extraConventionDirs];
|
|
4331
4745
|
const searchDirs = [
|
|
4332
4746
|
rootDir,
|
|
4333
|
-
(0,
|
|
4334
|
-
(0,
|
|
4335
|
-
(0,
|
|
4747
|
+
(0, import_node_path15.join)(rootDir, "src"),
|
|
4748
|
+
(0, import_node_path15.join)(rootDir, "app"),
|
|
4749
|
+
(0, import_node_path15.join)(rootDir, "lib")
|
|
4336
4750
|
];
|
|
4337
4751
|
for (const base of searchDirs) {
|
|
4338
4752
|
for (const convention of conventionDirs) {
|
|
4339
|
-
const dir = (0,
|
|
4753
|
+
const dir = (0, import_node_path15.join)(base, convention);
|
|
4340
4754
|
if (!(0, import_node_fs14.existsSync)(dir)) continue;
|
|
4341
4755
|
try {
|
|
4342
4756
|
const stat = (0, import_node_fs14.statSync)(dir);
|
|
@@ -4417,12 +4831,12 @@ function extractModuleFromPath(id, extraTrivial, extraSkipSegments, extraGeneric
|
|
|
4417
4831
|
}
|
|
4418
4832
|
return "root";
|
|
4419
4833
|
}
|
|
4420
|
-
var import_node_fs14,
|
|
4834
|
+
var import_node_fs14, import_node_path15, CONVENTION_DIRS_BUILTIN, GENERIC_ROLE_NAMES_BUILTIN, SKIP_SEGMENTS_BUILTIN, TRIVIAL_GROUPS, cachedRootDir, cachedConventionDirs, moduleTagger;
|
|
4421
4835
|
var init_module_tagger = __esm({
|
|
4422
4836
|
"src/server/graph/taggers/module-tagger.ts"() {
|
|
4423
4837
|
"use strict";
|
|
4424
4838
|
import_node_fs14 = require("node:fs");
|
|
4425
|
-
|
|
4839
|
+
import_node_path15 = require("node:path");
|
|
4426
4840
|
CONVENTION_DIRS_BUILTIN = ["features", "modules", "domains", "areas"];
|
|
4427
4841
|
GENERIC_ROLE_NAMES_BUILTIN = /* @__PURE__ */ new Set([
|
|
4428
4842
|
// JS/TS
|
|
@@ -4637,7 +5051,7 @@ function loadCustomTaggers(registry, config, rootDir, disabled) {
|
|
|
4637
5051
|
for (const entry of config.taggers?.custom ?? []) {
|
|
4638
5052
|
if (disabled.has(entry.id)) continue;
|
|
4639
5053
|
try {
|
|
4640
|
-
const absPath = (0,
|
|
5054
|
+
const absPath = (0, import_node_path16.resolve)(rootDir, entry.path);
|
|
4641
5055
|
const mod = require(absPath);
|
|
4642
5056
|
const tagger = "default" in mod ? mod.default : mod;
|
|
4643
5057
|
const override = config.taggers?.trackUntagged?.[tagger.id];
|
|
@@ -4658,11 +5072,11 @@ function createTaggerRegistry(config, rootDir) {
|
|
|
4658
5072
|
loadCustomTaggers(registry, config, rootDir, disabled);
|
|
4659
5073
|
return registry;
|
|
4660
5074
|
}
|
|
4661
|
-
var
|
|
5075
|
+
var import_node_path16, TaggerRegistry, BUILTIN_TAGGERS;
|
|
4662
5076
|
var init_tagger_registry = __esm({
|
|
4663
5077
|
"src/server/graph/core/tagger-registry.ts"() {
|
|
4664
5078
|
"use strict";
|
|
4665
|
-
|
|
5079
|
+
import_node_path16 = require("node:path");
|
|
4666
5080
|
init_module_tagger();
|
|
4667
5081
|
init_screen_tagger();
|
|
4668
5082
|
TaggerRegistry = class {
|
|
@@ -4690,7 +5104,7 @@ var init_tagger_registry = __esm({
|
|
|
4690
5104
|
|
|
4691
5105
|
// src/server/graph/core/tag-store.ts
|
|
4692
5106
|
function tagsFilePath(rootDir) {
|
|
4693
|
-
return (0,
|
|
5107
|
+
return (0, import_node_path17.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
|
|
4694
5108
|
}
|
|
4695
5109
|
function readTagStore(rootDir) {
|
|
4696
5110
|
const filePath = tagsFilePath(rootDir);
|
|
@@ -4711,7 +5125,7 @@ function readTagStore(rootDir) {
|
|
|
4711
5125
|
}
|
|
4712
5126
|
function writeTagStore(rootDir, store) {
|
|
4713
5127
|
const filePath = tagsFilePath(rootDir);
|
|
4714
|
-
const dir = (0,
|
|
5128
|
+
const dir = (0, import_node_path17.dirname)(filePath);
|
|
4715
5129
|
(0, import_node_fs15.mkdirSync)(dir, { recursive: true });
|
|
4716
5130
|
const cleaned = {};
|
|
4717
5131
|
for (const [nodeId, tags] of Object.entries(store)) {
|
|
@@ -4737,36 +5151,102 @@ function removeTag(rootDir, nodeId, key) {
|
|
|
4737
5151
|
}
|
|
4738
5152
|
writeTagStore(rootDir, store);
|
|
4739
5153
|
}
|
|
4740
|
-
var import_node_fs15,
|
|
5154
|
+
var import_node_fs15, import_node_path17, TAGS_FILENAME, GRAPHS_DIR, tagCache;
|
|
4741
5155
|
var init_tag_store = __esm({
|
|
4742
5156
|
"src/server/graph/core/tag-store.ts"() {
|
|
4743
5157
|
"use strict";
|
|
4744
5158
|
import_node_fs15 = require("node:fs");
|
|
4745
|
-
|
|
5159
|
+
import_node_path17 = require("node:path");
|
|
4746
5160
|
TAGS_FILENAME = "tags.json";
|
|
4747
5161
|
GRAPHS_DIR = ".launchsecure/graphs";
|
|
4748
5162
|
tagCache = /* @__PURE__ */ new Map();
|
|
4749
5163
|
}
|
|
4750
5164
|
});
|
|
4751
5165
|
|
|
5166
|
+
// src/server/graph/core/effects-index.ts
|
|
5167
|
+
function addTo(map, key, nodeId) {
|
|
5168
|
+
const list = map[key];
|
|
5169
|
+
if (!list) {
|
|
5170
|
+
map[key] = [nodeId];
|
|
5171
|
+
} else if (!list.includes(nodeId)) {
|
|
5172
|
+
list.push(nodeId);
|
|
5173
|
+
}
|
|
5174
|
+
}
|
|
5175
|
+
function collisionList(map) {
|
|
5176
|
+
const out = [];
|
|
5177
|
+
for (const [key, nodes] of Object.entries(map)) {
|
|
5178
|
+
if (nodes.length > 1) out.push({ key, nodes: nodes.slice().sort() });
|
|
5179
|
+
}
|
|
5180
|
+
out.sort((a, b) => b.nodes.length - a.nodes.length || a.key.localeCompare(b.key));
|
|
5181
|
+
return out;
|
|
5182
|
+
}
|
|
5183
|
+
function buildEffectsIndex(layerOutputs) {
|
|
5184
|
+
const idx = {
|
|
5185
|
+
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
5186
|
+
dom_ids: {},
|
|
5187
|
+
window_events: {},
|
|
5188
|
+
storage_keys: {},
|
|
5189
|
+
fetch_urls: {},
|
|
5190
|
+
timers: {},
|
|
5191
|
+
singleton_risks: [],
|
|
5192
|
+
collisions: { dom_ids: [], storage_keys: [], window_events: [] }
|
|
5193
|
+
};
|
|
5194
|
+
const singletonSet = /* @__PURE__ */ new Set();
|
|
5195
|
+
for (const output of Object.values(layerOutputs)) {
|
|
5196
|
+
for (const node of output.nodes) {
|
|
5197
|
+
const eff = node.effects;
|
|
5198
|
+
if (!eff) continue;
|
|
5199
|
+
for (const id of eff.dom_ids ?? []) addTo(idx.dom_ids, id, node.id);
|
|
5200
|
+
for (const ev of eff.window_events ?? []) addTo(idx.window_events, ev, node.id);
|
|
5201
|
+
for (const k of eff.storage_keys ?? []) addTo(idx.storage_keys, k, node.id);
|
|
5202
|
+
for (const u of eff.fetch_urls ?? []) addTo(idx.fetch_urls, u, node.id);
|
|
5203
|
+
for (const t of eff.timers ?? []) addTo(idx.timers, t, node.id);
|
|
5204
|
+
const hasTimer = (eff.timers?.length ?? 0) > 0;
|
|
5205
|
+
const hasWindowListener = (eff.window_events?.length ?? 0) > 0;
|
|
5206
|
+
const hasGlobalMutation = (eff.globals?.length ?? 0) > 0;
|
|
5207
|
+
if (hasTimer || hasWindowListener || hasGlobalMutation) {
|
|
5208
|
+
singletonSet.add(node.id);
|
|
5209
|
+
}
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
idx.singleton_risks = Array.from(singletonSet).sort();
|
|
5213
|
+
idx.collisions.dom_ids = collisionList(idx.dom_ids);
|
|
5214
|
+
idx.collisions.storage_keys = collisionList(idx.storage_keys);
|
|
5215
|
+
idx.collisions.window_events = collisionList(idx.window_events);
|
|
5216
|
+
return idx;
|
|
5217
|
+
}
|
|
5218
|
+
function writeEffectsIndex(rootDir, idx) {
|
|
5219
|
+
const path3 = (0, import_node_path18.join)(rootDir, ".launchsecure", "graphs", "effects-index.json");
|
|
5220
|
+
(0, import_node_fs16.writeFileSync)(path3, JSON.stringify(idx, null, 2) + "\n", "utf-8");
|
|
5221
|
+
return path3;
|
|
5222
|
+
}
|
|
5223
|
+
var import_node_fs16, import_node_path18;
|
|
5224
|
+
var init_effects_index = __esm({
|
|
5225
|
+
"src/server/graph/core/effects-index.ts"() {
|
|
5226
|
+
"use strict";
|
|
5227
|
+
import_node_fs16 = require("node:fs");
|
|
5228
|
+
import_node_path18 = require("node:path");
|
|
5229
|
+
}
|
|
5230
|
+
});
|
|
5231
|
+
|
|
4752
5232
|
// src/server/graph/index.ts
|
|
4753
5233
|
function getAvailableLayers(rootDir) {
|
|
4754
|
-
const dir = (0,
|
|
4755
|
-
if (!(0,
|
|
4756
|
-
return (0,
|
|
5234
|
+
const dir = (0, import_node_path19.join)(rootDir, GRAPHS_DIR2);
|
|
5235
|
+
if (!(0, import_node_fs17.existsSync)(dir)) return [];
|
|
5236
|
+
return (0, import_node_fs17.readdirSync)(dir).filter((f) => f.endsWith(".json") && !NON_LAYER_GRAPH_FILES.has(f)).map((f) => f.replace(".json", ""));
|
|
4757
5237
|
}
|
|
4758
5238
|
function graphsDir(rootDir) {
|
|
4759
|
-
return (0,
|
|
5239
|
+
return (0, import_node_path19.join)(rootDir, GRAPHS_DIR2);
|
|
4760
5240
|
}
|
|
4761
5241
|
function graphFilePath(rootDir, layer) {
|
|
4762
|
-
return (0,
|
|
5242
|
+
return (0, import_node_path19.join)(graphsDir(rootDir), `${layer}.json`);
|
|
4763
5243
|
}
|
|
4764
5244
|
function tagsFilePath2(rootDir) {
|
|
4765
|
-
return (0,
|
|
5245
|
+
return (0, import_node_path19.join)(graphsDir(rootDir), "tags.json");
|
|
4766
5246
|
}
|
|
4767
5247
|
function getMtimeMs(filePath) {
|
|
4768
|
-
if (!(0,
|
|
4769
|
-
return (0,
|
|
5248
|
+
if (!(0, import_node_fs17.existsSync)(filePath)) return 0;
|
|
5249
|
+
return (0, import_node_fs17.statSync)(filePath).mtimeMs;
|
|
4770
5250
|
}
|
|
4771
5251
|
function invalidateCache(filePath) {
|
|
4772
5252
|
graphCache.delete(filePath);
|
|
@@ -4805,20 +5285,20 @@ function applyTags(graph, layer, rootDir) {
|
|
|
4805
5285
|
}
|
|
4806
5286
|
function readGraphRaw(rootDir, layer) {
|
|
4807
5287
|
const filePath = graphFilePath(rootDir, layer);
|
|
4808
|
-
if (!(0,
|
|
4809
|
-
const stat = (0,
|
|
5288
|
+
if (!(0, import_node_fs17.existsSync)(filePath)) return null;
|
|
5289
|
+
const stat = (0, import_node_fs17.statSync)(filePath);
|
|
4810
5290
|
const cached = graphCache.get(filePath);
|
|
4811
5291
|
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
4812
5292
|
return cached.graph;
|
|
4813
5293
|
}
|
|
4814
|
-
const content = (0,
|
|
5294
|
+
const content = (0, import_node_fs17.readFileSync)(filePath, "utf-8");
|
|
4815
5295
|
const graph = JSON.parse(content);
|
|
4816
5296
|
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
4817
5297
|
return graph;
|
|
4818
5298
|
}
|
|
4819
5299
|
function readGraph(rootDir, layer) {
|
|
4820
5300
|
const rawFilePath = graphFilePath(rootDir, layer);
|
|
4821
|
-
if (!(0,
|
|
5301
|
+
if (!(0, import_node_fs17.existsSync)(rawFilePath)) return null;
|
|
4822
5302
|
const rawMtime = getMtimeMs(rawFilePath);
|
|
4823
5303
|
const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
|
|
4824
5304
|
const cacheKey = `${rootDir}:${layer}`;
|
|
@@ -4848,24 +5328,24 @@ async function generateGraph(rootDir, layer) {
|
|
|
4848
5328
|
mutationMethods: config.parsers?.patterns?.mutationMethods
|
|
4849
5329
|
});
|
|
4850
5330
|
const dir = graphsDir(rootDir);
|
|
4851
|
-
(0,
|
|
5331
|
+
(0, import_node_fs17.mkdirSync)(dir, { recursive: true });
|
|
4852
5332
|
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
4853
5333
|
for (const result of results) {
|
|
4854
5334
|
const filePath = graphFilePath(rootDir, result.layer);
|
|
4855
|
-
(0,
|
|
5335
|
+
(0, import_node_fs17.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
|
|
4856
5336
|
invalidateCache(filePath);
|
|
4857
5337
|
invalidateTaggedCache(rootDir, result.layer);
|
|
4858
5338
|
}
|
|
4859
5339
|
if (!layer) {
|
|
4860
5340
|
const producedLayers = new Set(results.map((r) => r.layer));
|
|
4861
5341
|
try {
|
|
4862
|
-
for (const f of (0,
|
|
4863
|
-
if (!f.endsWith(".json") || f === "tags.json") continue;
|
|
5342
|
+
for (const f of (0, import_node_fs17.readdirSync)(dir)) {
|
|
5343
|
+
if (!f.endsWith(".json") || f === "tags.json" || f === "effects-index.json") continue;
|
|
4864
5344
|
const layerName = f.replace(/\.json$/, "");
|
|
4865
5345
|
if (producedLayers.has(layerName)) continue;
|
|
4866
|
-
const orphan = (0,
|
|
5346
|
+
const orphan = (0, import_node_path19.join)(dir, f);
|
|
4867
5347
|
try {
|
|
4868
|
-
(0,
|
|
5348
|
+
(0, import_node_fs17.unlinkSync)(orphan);
|
|
4869
5349
|
invalidateCache(orphan);
|
|
4870
5350
|
invalidateTaggedCache(rootDir, layerName);
|
|
4871
5351
|
process.stderr.write(`[launch-chart] removed orphan layer file: ${f} (no parser produced ${layerName} this run)
|
|
@@ -4876,32 +5356,404 @@ async function generateGraph(rootDir, layer) {
|
|
|
4876
5356
|
} catch {
|
|
4877
5357
|
}
|
|
4878
5358
|
}
|
|
5359
|
+
try {
|
|
5360
|
+
const allLayers = {};
|
|
5361
|
+
for (const r of results) allLayers[r.layer] = r.output;
|
|
5362
|
+
if (layer) {
|
|
5363
|
+
for (const f of (0, import_node_fs17.readdirSync)(dir)) {
|
|
5364
|
+
if (!f.endsWith(".json") || f === "tags.json" || f === "effects-index.json") continue;
|
|
5365
|
+
const layerName = f.replace(/\.json$/, "");
|
|
5366
|
+
if (allLayers[layerName]) continue;
|
|
5367
|
+
const existing = readGraphRaw(rootDir, layerName);
|
|
5368
|
+
if (existing) allLayers[layerName] = existing;
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
const idx = buildEffectsIndex(allLayers);
|
|
5372
|
+
writeEffectsIndex(rootDir, idx);
|
|
5373
|
+
} catch (e) {
|
|
5374
|
+
process.stderr.write(`[launch-chart] effects-index build failed: ${e.message}
|
|
5375
|
+
`);
|
|
5376
|
+
}
|
|
4879
5377
|
return results;
|
|
4880
5378
|
}
|
|
4881
|
-
|
|
5379
|
+
function readEffectsIndex(rootDir) {
|
|
5380
|
+
const path3 = (0, import_node_path19.join)(rootDir, GRAPHS_DIR2, "effects-index.json");
|
|
5381
|
+
if (!(0, import_node_fs17.existsSync)(path3)) return null;
|
|
5382
|
+
try {
|
|
5383
|
+
return JSON.parse((0, import_node_fs17.readFileSync)(path3, "utf-8"));
|
|
5384
|
+
} catch {
|
|
5385
|
+
return null;
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
var import_node_fs17, import_node_path19, GRAPHS_DIR2, NON_LAYER_GRAPH_FILES, graphCache, taggedCache;
|
|
4882
5389
|
var init_graph = __esm({
|
|
4883
5390
|
"src/server/graph/index.ts"() {
|
|
4884
5391
|
"use strict";
|
|
4885
|
-
|
|
4886
|
-
|
|
5392
|
+
import_node_fs17 = require("node:fs");
|
|
5393
|
+
import_node_path19 = require("node:path");
|
|
4887
5394
|
init_graph_builder();
|
|
4888
5395
|
init_config();
|
|
4889
5396
|
init_tagger_registry();
|
|
4890
5397
|
init_tag_store();
|
|
4891
5398
|
init_ts_extractor();
|
|
5399
|
+
init_effects_index();
|
|
4892
5400
|
init_tag_store();
|
|
4893
5401
|
GRAPHS_DIR2 = ".launchsecure/graphs";
|
|
5402
|
+
NON_LAYER_GRAPH_FILES = /* @__PURE__ */ new Set(["tags.json", "effects-index.json"]);
|
|
4894
5403
|
graphCache = /* @__PURE__ */ new Map();
|
|
4895
5404
|
taggedCache = /* @__PURE__ */ new Map();
|
|
4896
5405
|
}
|
|
4897
5406
|
});
|
|
4898
5407
|
|
|
5408
|
+
// src/server/graph/core/audit-security.ts
|
|
5409
|
+
function collectSourceFiles(rootDir) {
|
|
5410
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
5411
|
+
if (!paths) return null;
|
|
5412
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5413
|
+
const out = [];
|
|
5414
|
+
for (const root of paths.srcRoots) {
|
|
5415
|
+
for (const f of walk(root, [".ts", ".tsx"])) {
|
|
5416
|
+
if (!seen.has(f)) {
|
|
5417
|
+
seen.add(f);
|
|
5418
|
+
out.push(f);
|
|
5419
|
+
}
|
|
5420
|
+
}
|
|
5421
|
+
}
|
|
5422
|
+
for (const conv of paths.conventionFiles) {
|
|
5423
|
+
if (!seen.has(conv)) {
|
|
5424
|
+
seen.add(conv);
|
|
5425
|
+
out.push(conv);
|
|
5426
|
+
}
|
|
5427
|
+
}
|
|
5428
|
+
return out;
|
|
5429
|
+
}
|
|
5430
|
+
function toNodeId5(rootDir, srcDir, absPath) {
|
|
5431
|
+
const relFromSrc = (0, import_node_path20.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
5432
|
+
if (relFromSrc.startsWith("..")) {
|
|
5433
|
+
return (0, import_node_path20.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
5434
|
+
}
|
|
5435
|
+
return relFromSrc;
|
|
5436
|
+
}
|
|
5437
|
+
function scanResponseSecretsInFile(absPath) {
|
|
5438
|
+
const tree = parseSource(absPath);
|
|
5439
|
+
if (!tree) return [];
|
|
5440
|
+
const query = createQuery(`
|
|
5441
|
+
(call_expression
|
|
5442
|
+
function: (member_expression
|
|
5443
|
+
property: (property_identifier) @method
|
|
5444
|
+
(#eq? @method "json"))
|
|
5445
|
+
arguments: (arguments
|
|
5446
|
+
(object) @payload))
|
|
5447
|
+
`);
|
|
5448
|
+
const matches = query.matches(tree.rootNode);
|
|
5449
|
+
const hits = [];
|
|
5450
|
+
for (const m of matches) {
|
|
5451
|
+
const caps = m.captures;
|
|
5452
|
+
const payload = caps.find((c) => c.name === "payload")?.node;
|
|
5453
|
+
if (!payload) continue;
|
|
5454
|
+
walkPairKeys(payload, (keyName, node) => {
|
|
5455
|
+
const trimmed = keyName.replace(/^["']|["']$/g, "");
|
|
5456
|
+
if (SECRET_KEY_ALLOWLIST.has(trimmed)) return;
|
|
5457
|
+
if (!SECRET_KEY_RE.test(trimmed)) return;
|
|
5458
|
+
hits.push({
|
|
5459
|
+
file: absPath,
|
|
5460
|
+
line: node.startPosition.row + 1,
|
|
5461
|
+
keyName: trimmed,
|
|
5462
|
+
callShape: payload.text.slice(0, 40).replace(/\s+/g, " ")
|
|
5463
|
+
});
|
|
5464
|
+
});
|
|
5465
|
+
}
|
|
5466
|
+
return hits;
|
|
5467
|
+
}
|
|
5468
|
+
function walkPairKeys(node, visit) {
|
|
5469
|
+
for (const child of node.children) {
|
|
5470
|
+
if (child.type === "pair") {
|
|
5471
|
+
const keyChild = child.children[0];
|
|
5472
|
+
if (keyChild && (keyChild.type === "property_identifier" || keyChild.type === "string")) {
|
|
5473
|
+
visit(keyChild.text, keyChild);
|
|
5474
|
+
}
|
|
5475
|
+
const valueChild = child.children[child.children.length - 1];
|
|
5476
|
+
if (valueChild && valueChild.type === "object") walkPairKeys(valueChild, visit);
|
|
5477
|
+
} else if (child.type === "shorthand_property_identifier") {
|
|
5478
|
+
visit(child.text, child);
|
|
5479
|
+
} else if (child.type === "object") {
|
|
5480
|
+
walkPairKeys(child, visit);
|
|
5481
|
+
}
|
|
5482
|
+
}
|
|
5483
|
+
}
|
|
5484
|
+
function checkResponseSecretLeak(rootDir, core) {
|
|
5485
|
+
const files = collectSourceFiles(rootDir);
|
|
5486
|
+
if (files === null || files.length === 0) {
|
|
5487
|
+
return core.buildSkipped(
|
|
5488
|
+
"security",
|
|
5489
|
+
"response_secret_leak",
|
|
5490
|
+
`no source files detected \u2014 not a TypeScript project, or src roots aren't configured`
|
|
5491
|
+
);
|
|
5492
|
+
}
|
|
5493
|
+
const findings = [];
|
|
5494
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
5495
|
+
for (const abs of files) {
|
|
5496
|
+
let hits = [];
|
|
5497
|
+
try {
|
|
5498
|
+
hits = scanResponseSecretsInFile(abs);
|
|
5499
|
+
} catch {
|
|
5500
|
+
}
|
|
5501
|
+
for (const h of hits) {
|
|
5502
|
+
const fileId = toNodeId5(rootDir, paths.srcDir, abs);
|
|
5503
|
+
findings.push({
|
|
5504
|
+
id: `secret-leak:${fileId}:${h.line}:${h.keyName}`,
|
|
5505
|
+
severity: "error",
|
|
5506
|
+
category: "response_secret_leak",
|
|
5507
|
+
title: `${h.keyName} in response payload`,
|
|
5508
|
+
detail: `Response body includes a "${h.keyName}" key \u2014 likely leaking a secret to the client. If this is a public identifier or anti-CSRF token, add it to the allow-list in audit-security.ts.`,
|
|
5509
|
+
file: fileId,
|
|
5510
|
+
line: h.line
|
|
5511
|
+
});
|
|
5512
|
+
}
|
|
5513
|
+
}
|
|
5514
|
+
return core.buildReport("security", "response_secret_leak", findings);
|
|
5515
|
+
}
|
|
5516
|
+
function collectDeclaredEnvKeys(rootDir) {
|
|
5517
|
+
const keys = /* @__PURE__ */ new Set();
|
|
5518
|
+
const files = [];
|
|
5519
|
+
let entries = [];
|
|
5520
|
+
try {
|
|
5521
|
+
entries = (0, import_node_fs18.readdirSync)(rootDir);
|
|
5522
|
+
} catch {
|
|
5523
|
+
return { keys, files };
|
|
5524
|
+
}
|
|
5525
|
+
for (const name of entries) {
|
|
5526
|
+
if (!name.startsWith(".env")) continue;
|
|
5527
|
+
const abs = (0, import_node_path20.join)(rootDir, name);
|
|
5528
|
+
if (!(0, import_node_fs18.existsSync)(abs)) continue;
|
|
5529
|
+
files.push(name);
|
|
5530
|
+
let content = "";
|
|
5531
|
+
try {
|
|
5532
|
+
content = (0, import_node_fs18.readFileSync)(abs, "utf-8");
|
|
5533
|
+
} catch {
|
|
5534
|
+
continue;
|
|
5535
|
+
}
|
|
5536
|
+
for (const line of content.split("\n")) {
|
|
5537
|
+
const trimmed = line.trim();
|
|
5538
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
5539
|
+
const eq = trimmed.indexOf("=");
|
|
5540
|
+
if (eq < 0) continue;
|
|
5541
|
+
const key = trimmed.slice(0, eq).trim();
|
|
5542
|
+
if (/^[A-Z_][A-Z0-9_]*$/.test(key)) keys.add(key);
|
|
5543
|
+
}
|
|
5544
|
+
}
|
|
5545
|
+
return { keys, files };
|
|
5546
|
+
}
|
|
5547
|
+
function scanEnvRefsInFile(absPath) {
|
|
5548
|
+
const tree = parseSource(absPath);
|
|
5549
|
+
if (!tree) return [];
|
|
5550
|
+
const query = createQuery(`
|
|
5551
|
+
; process.env.X
|
|
5552
|
+
(member_expression
|
|
5553
|
+
object: (member_expression
|
|
5554
|
+
object: (identifier) @_proc
|
|
5555
|
+
property: (property_identifier) @_env)
|
|
5556
|
+
property: (property_identifier) @env_name
|
|
5557
|
+
(#eq? @_proc "process")
|
|
5558
|
+
(#eq? @_env "env"))
|
|
5559
|
+
|
|
5560
|
+
; process.env["X"]
|
|
5561
|
+
(subscript_expression
|
|
5562
|
+
object: (member_expression
|
|
5563
|
+
object: (identifier) @_proc2
|
|
5564
|
+
property: (property_identifier) @_env2)
|
|
5565
|
+
index: (string (string_fragment) @env_name_str)
|
|
5566
|
+
(#eq? @_proc2 "process")
|
|
5567
|
+
(#eq? @_env2 "env"))
|
|
5568
|
+
`);
|
|
5569
|
+
const matches = query.matches(tree.rootNode);
|
|
5570
|
+
const refs = [];
|
|
5571
|
+
for (const m of matches) {
|
|
5572
|
+
const caps = m.captures;
|
|
5573
|
+
const key = caps.find((c) => c.name === "env_name" || c.name === "env_name_str");
|
|
5574
|
+
if (!key) continue;
|
|
5575
|
+
refs.push({ file: absPath, line: key.node.startPosition.row + 1, envKey: key.node.text });
|
|
5576
|
+
}
|
|
5577
|
+
return refs;
|
|
5578
|
+
}
|
|
5579
|
+
function checkEnvDeadAlias(rootDir, core) {
|
|
5580
|
+
const { keys: declared, files: envFiles } = collectDeclaredEnvKeys(rootDir);
|
|
5581
|
+
if (envFiles.length === 0) {
|
|
5582
|
+
return core.buildSkipped(
|
|
5583
|
+
"security",
|
|
5584
|
+
"env_dead_alias",
|
|
5585
|
+
`no .env* files in project root \u2014 this check needs a declared-env inventory to compare against`
|
|
5586
|
+
);
|
|
5587
|
+
}
|
|
5588
|
+
const files = collectSourceFiles(rootDir);
|
|
5589
|
+
if (files === null || files.length === 0) {
|
|
5590
|
+
return core.buildSkipped("security", "env_dead_alias", `no source files detected`);
|
|
5591
|
+
}
|
|
5592
|
+
const findings = [];
|
|
5593
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
5594
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5595
|
+
for (const abs of files) {
|
|
5596
|
+
let refs = [];
|
|
5597
|
+
try {
|
|
5598
|
+
refs = scanEnvRefsInFile(abs);
|
|
5599
|
+
} catch {
|
|
5600
|
+
}
|
|
5601
|
+
for (const r of refs) {
|
|
5602
|
+
if (declared.has(r.envKey)) continue;
|
|
5603
|
+
if (FRAMEWORK_ENV_KEYS.has(r.envKey)) continue;
|
|
5604
|
+
if (r.envKey.startsWith("npm_")) continue;
|
|
5605
|
+
const fileId = toNodeId5(rootDir, paths.srcDir, abs);
|
|
5606
|
+
const dedupeKey = `${fileId}:${r.envKey}`;
|
|
5607
|
+
if (seen.has(dedupeKey)) continue;
|
|
5608
|
+
seen.add(dedupeKey);
|
|
5609
|
+
findings.push({
|
|
5610
|
+
id: `dead-env:${fileId}:${r.envKey}`,
|
|
5611
|
+
severity: "warning",
|
|
5612
|
+
category: "env_dead_alias",
|
|
5613
|
+
title: `process.env.${r.envKey} not declared in any .env file`,
|
|
5614
|
+
detail: `Reference to env var "${r.envKey}" but no .env* file declares it (${envFiles.join(", ")}). Likely a rename left behind, a typo, or an env var that's only set in deployment but should be documented in .env.example.`,
|
|
5615
|
+
file: fileId,
|
|
5616
|
+
line: r.line
|
|
5617
|
+
});
|
|
5618
|
+
}
|
|
5619
|
+
}
|
|
5620
|
+
return core.buildReport("security", "env_dead_alias", findings);
|
|
5621
|
+
}
|
|
5622
|
+
function scanUrlFallbacksInFile(absPath) {
|
|
5623
|
+
const tree = parseSource(absPath);
|
|
5624
|
+
if (!tree) return [];
|
|
5625
|
+
const query = createQuery(`
|
|
5626
|
+
; process.env.X || "https://..." or process.env.X ?? "https://..."
|
|
5627
|
+
(binary_expression
|
|
5628
|
+
left: (member_expression
|
|
5629
|
+
object: (member_expression
|
|
5630
|
+
object: (identifier) @_proc
|
|
5631
|
+
property: (property_identifier) @_env)
|
|
5632
|
+
property: (property_identifier) @env_var)
|
|
5633
|
+
right: (string (string_fragment) @url)
|
|
5634
|
+
(#eq? @_proc "process")
|
|
5635
|
+
(#eq? @_env "env")
|
|
5636
|
+
(#match? @url "^https?://")) @expr
|
|
5637
|
+
`);
|
|
5638
|
+
const matches = query.matches(tree.rootNode);
|
|
5639
|
+
const hits = [];
|
|
5640
|
+
for (const m of matches) {
|
|
5641
|
+
const caps = m.captures;
|
|
5642
|
+
const envVar = caps.find((c) => c.name === "env_var")?.node;
|
|
5643
|
+
const url = caps.find((c) => c.name === "url")?.node;
|
|
5644
|
+
const expr = caps.find((c) => c.name === "expr")?.node;
|
|
5645
|
+
if (!envVar || !url || !expr) continue;
|
|
5646
|
+
const opMatch = / (\|\||\?\?) /.exec(expr.text);
|
|
5647
|
+
const operator = opMatch ? opMatch[1] : "||";
|
|
5648
|
+
hits.push({
|
|
5649
|
+
file: absPath,
|
|
5650
|
+
line: envVar.startPosition.row + 1,
|
|
5651
|
+
envKey: envVar.text,
|
|
5652
|
+
url: url.text,
|
|
5653
|
+
operator
|
|
5654
|
+
});
|
|
5655
|
+
}
|
|
5656
|
+
return hits;
|
|
5657
|
+
}
|
|
5658
|
+
function checkHardcodedUrlFallback(rootDir, core) {
|
|
5659
|
+
const files = collectSourceFiles(rootDir);
|
|
5660
|
+
if (files === null || files.length === 0) {
|
|
5661
|
+
return core.buildSkipped("security", "hardcoded_url_fallback", `no source files detected`);
|
|
5662
|
+
}
|
|
5663
|
+
const findings = [];
|
|
5664
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
5665
|
+
for (const abs of files) {
|
|
5666
|
+
let hits = [];
|
|
5667
|
+
try {
|
|
5668
|
+
hits = scanUrlFallbacksInFile(abs);
|
|
5669
|
+
} catch {
|
|
5670
|
+
}
|
|
5671
|
+
for (const h of hits) {
|
|
5672
|
+
const fileId = toNodeId5(rootDir, paths.srcDir, abs);
|
|
5673
|
+
findings.push({
|
|
5674
|
+
id: `url-fallback:${fileId}:${h.line}:${h.envKey}`,
|
|
5675
|
+
severity: "warning",
|
|
5676
|
+
category: "hardcoded_url_fallback",
|
|
5677
|
+
title: `${h.envKey} ${h.operator} "${h.url}"`,
|
|
5678
|
+
detail: `Hardcoded URL fallback for env var "${h.envKey}". If the env var is unset in prod, requests will silently route to "${h.url}" \u2014 common cause of dev/prod URL leaks and stale localhost ports. Make the env var required, or move the default to a deployment-aware config.`,
|
|
5679
|
+
file: fileId,
|
|
5680
|
+
line: h.line
|
|
5681
|
+
});
|
|
5682
|
+
}
|
|
5683
|
+
}
|
|
5684
|
+
return core.buildReport("security", "hardcoded_url_fallback", findings);
|
|
5685
|
+
}
|
|
5686
|
+
var import_node_fs18, import_node_path20, SECRET_KEY_RE, SECRET_KEY_ALLOWLIST, FRAMEWORK_ENV_KEYS;
|
|
5687
|
+
var init_audit_security = __esm({
|
|
5688
|
+
"src/server/graph/core/audit-security.ts"() {
|
|
5689
|
+
"use strict";
|
|
5690
|
+
import_node_fs18 = require("node:fs");
|
|
5691
|
+
import_node_path20 = require("node:path");
|
|
5692
|
+
init_ts_extractor();
|
|
5693
|
+
init_config();
|
|
5694
|
+
init_resolve_paths();
|
|
5695
|
+
init_walk();
|
|
5696
|
+
SECRET_KEY_RE = /^(.*_)?(token|secret|password|passwd|credential|credentials|apikey|api_key|access_?token|refresh_?token|client_?secret|private_?key|signing_?key|webhook_?secret|auth_?token|bearer)$/i;
|
|
5697
|
+
SECRET_KEY_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
5698
|
+
"csrfToken",
|
|
5699
|
+
"csrf_token",
|
|
5700
|
+
"publicKey",
|
|
5701
|
+
"public_key",
|
|
5702
|
+
"clientId",
|
|
5703
|
+
"idempotencyKey",
|
|
5704
|
+
"idempotency_key",
|
|
5705
|
+
"requestKey",
|
|
5706
|
+
"request_key",
|
|
5707
|
+
"sessionId",
|
|
5708
|
+
"session_id",
|
|
5709
|
+
"apiKeyId",
|
|
5710
|
+
"api_key_id",
|
|
5711
|
+
"tokenType",
|
|
5712
|
+
"token_type",
|
|
5713
|
+
"tokenExpiry",
|
|
5714
|
+
"token_expiry",
|
|
5715
|
+
"isAccessToken",
|
|
5716
|
+
"hasToken"
|
|
5717
|
+
]);
|
|
5718
|
+
FRAMEWORK_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
5719
|
+
"NODE_ENV",
|
|
5720
|
+
"NODE_OPTIONS",
|
|
5721
|
+
"NODE_PATH",
|
|
5722
|
+
"PATH",
|
|
5723
|
+
"HOME",
|
|
5724
|
+
"USER",
|
|
5725
|
+
"PWD",
|
|
5726
|
+
"CI",
|
|
5727
|
+
"GITHUB_ACTIONS",
|
|
5728
|
+
"GITHUB_WORKFLOW",
|
|
5729
|
+
"GITHUB_RUN_ID",
|
|
5730
|
+
"GITHUB_SHA",
|
|
5731
|
+
"VERCEL",
|
|
5732
|
+
"VERCEL_ENV",
|
|
5733
|
+
"VERCEL_URL",
|
|
5734
|
+
"VERCEL_REGION",
|
|
5735
|
+
"VERCEL_GIT_COMMIT_SHA",
|
|
5736
|
+
"VERCEL_GIT_COMMIT_REF",
|
|
5737
|
+
"VERCEL_GIT_REPO_SLUG",
|
|
5738
|
+
"VERCEL_GIT_REPO_OWNER",
|
|
5739
|
+
"PORT",
|
|
5740
|
+
"TZ",
|
|
5741
|
+
"LANG",
|
|
5742
|
+
"LC_ALL",
|
|
5743
|
+
"TERM",
|
|
5744
|
+
"DEBUG",
|
|
5745
|
+
"NEXT_RUNTIME",
|
|
5746
|
+
"NEXT_PUBLIC_VERCEL_URL"
|
|
5747
|
+
]);
|
|
5748
|
+
}
|
|
5749
|
+
});
|
|
5750
|
+
|
|
4899
5751
|
// src/server/graph/core/audit-core.ts
|
|
4900
5752
|
function readGraphFile(rootDir, layer) {
|
|
4901
|
-
const filePath = (0,
|
|
4902
|
-
if (!(0,
|
|
5753
|
+
const filePath = (0, import_node_path21.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
5754
|
+
if (!(0, import_node_fs19.existsSync)(filePath)) return null;
|
|
4903
5755
|
try {
|
|
4904
|
-
return JSON.parse((0,
|
|
5756
|
+
return JSON.parse((0, import_node_fs19.readFileSync)(filePath, "utf-8"));
|
|
4905
5757
|
} catch {
|
|
4906
5758
|
return null;
|
|
4907
5759
|
}
|
|
@@ -4910,8 +5762,7 @@ function checkSchemaDrift(rootDir) {
|
|
|
4910
5762
|
const findings = [];
|
|
4911
5763
|
const db = readGraphFile(rootDir, "db");
|
|
4912
5764
|
if (!db) {
|
|
4913
|
-
|
|
4914
|
-
return buildReport("db", "schema_drift", findings);
|
|
5765
|
+
return buildSkipped("db", "schema_drift", "no db graph \u2014 generate_graph first, or this project has no Prisma schema");
|
|
4915
5766
|
}
|
|
4916
5767
|
for (const c of db.contradictions ?? []) {
|
|
4917
5768
|
const isTableLevel = c.detail.includes("Table ") && (c.detail.includes("has no CREATE TABLE") || c.detail.includes("not in schema.prisma"));
|
|
@@ -4928,7 +5779,7 @@ function checkSchemaDrift(rootDir) {
|
|
|
4928
5779
|
function checkOrphanFks(rootDir) {
|
|
4929
5780
|
const findings = [];
|
|
4930
5781
|
const db = readGraphFile(rootDir, "db");
|
|
4931
|
-
if (!db) return
|
|
5782
|
+
if (!db) return buildSkipped("db", "orphan_fks", "no db graph");
|
|
4932
5783
|
for (const f of db.flagged_edges ?? []) {
|
|
4933
5784
|
findings.push({
|
|
4934
5785
|
id: `fk:${f.source}->${f.target}`,
|
|
@@ -4943,13 +5794,16 @@ function checkOrphanFks(rootDir) {
|
|
|
4943
5794
|
function checkUnprotectedRoutes(rootDir) {
|
|
4944
5795
|
const findings = [];
|
|
4945
5796
|
const api = readGraphFile(rootDir, "api");
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
5797
|
+
if (!api) return buildSkipped("api", "unprotected_routes", "no api graph");
|
|
5798
|
+
const routePermsPath = (0, import_node_path21.join)(rootDir, "src", "config", "route-permissions.ts");
|
|
5799
|
+
if (!(0, import_node_fs19.existsSync)(routePermsPath)) {
|
|
5800
|
+
return buildSkipped(
|
|
5801
|
+
"api",
|
|
5802
|
+
"unprotected_routes",
|
|
5803
|
+
`no src/config/route-permissions.ts \u2014 this check needs a centralized ROUTE_PERMISSIONS inventory to compare endpoints against`
|
|
5804
|
+
);
|
|
4952
5805
|
}
|
|
5806
|
+
const routePermsContent = (0, import_node_fs19.readFileSync)(routePermsPath, "utf-8");
|
|
4953
5807
|
const registeredRoutes = /* @__PURE__ */ new Set();
|
|
4954
5808
|
const routeEntryRe = /path:\s*'([^']+)'/g;
|
|
4955
5809
|
let rm;
|
|
@@ -4993,7 +5847,7 @@ function routeMatchesPattern(route, pattern) {
|
|
|
4993
5847
|
function checkDeadScreens(rootDir) {
|
|
4994
5848
|
const findings = [];
|
|
4995
5849
|
const ui = readGraphFile(rootDir, "ui");
|
|
4996
|
-
if (!ui) return
|
|
5850
|
+
if (!ui) return buildSkipped("ui", "dead_screens", "no ui graph");
|
|
4997
5851
|
const pages = ui.nodes.filter((n) => n.type === "page" || n.type === "layout");
|
|
4998
5852
|
const navTargets = /* @__PURE__ */ new Set();
|
|
4999
5853
|
for (const e of ui.edges) {
|
|
@@ -5025,13 +5879,24 @@ function checkDeadScreens(rootDir) {
|
|
|
5025
5879
|
function checkUnenforcedPermissions(rootDir) {
|
|
5026
5880
|
const findings = [];
|
|
5027
5881
|
const staticGraph = readGraphFile(rootDir, "static");
|
|
5028
|
-
if (!staticGraph) return
|
|
5882
|
+
if (!staticGraph) return buildSkipped("static", "unenforced_permissions", "no static graph");
|
|
5029
5883
|
const permissions = staticGraph.nodes.filter((n) => n.type === "seed_permission").map((n) => ({ id: n.id, key: n.value, name: n.name }));
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5884
|
+
if (permissions.length === 0) {
|
|
5885
|
+
return buildSkipped(
|
|
5886
|
+
"static",
|
|
5887
|
+
"unenforced_permissions",
|
|
5888
|
+
`no seed_permission nodes \u2014 this project either has no seed permissions or hasn't tagged them in seed.ts`
|
|
5889
|
+
);
|
|
5890
|
+
}
|
|
5891
|
+
const routePermsPath = (0, import_node_path21.join)(rootDir, "src", "config", "route-permissions.ts");
|
|
5892
|
+
if (!(0, import_node_fs19.existsSync)(routePermsPath)) {
|
|
5893
|
+
return buildSkipped(
|
|
5894
|
+
"static",
|
|
5895
|
+
"unenforced_permissions",
|
|
5896
|
+
`no src/config/route-permissions.ts to compare seed permissions against`
|
|
5897
|
+
);
|
|
5034
5898
|
}
|
|
5899
|
+
const routePermsContent = (0, import_node_fs19.readFileSync)(routePermsPath, "utf-8");
|
|
5035
5900
|
for (const perm of permissions) {
|
|
5036
5901
|
const regex = new RegExp(`permission:\\s*['"]${perm.key}['"]`);
|
|
5037
5902
|
if (!regex.test(routePermsContent)) {
|
|
@@ -5049,20 +5914,27 @@ function checkUnenforcedPermissions(rootDir) {
|
|
|
5049
5914
|
function checkHardcodedValues(rootDir) {
|
|
5050
5915
|
const findings = [];
|
|
5051
5916
|
const staticGraph = readGraphFile(rootDir, "static");
|
|
5052
|
-
if (!staticGraph) return
|
|
5917
|
+
if (!staticGraph) return buildSkipped("static", "hardcoded_values", "no static graph");
|
|
5053
5918
|
const knownValues = /* @__PURE__ */ new Set();
|
|
5054
5919
|
for (const n of staticGraph.nodes) {
|
|
5055
5920
|
if (n.type === "enum_value") knownValues.add(n.value);
|
|
5056
5921
|
}
|
|
5922
|
+
if (knownValues.size === 0) {
|
|
5923
|
+
return buildSkipped(
|
|
5924
|
+
"static",
|
|
5925
|
+
"hardcoded_values",
|
|
5926
|
+
`no enum_value nodes in static graph \u2014 without an inventory of known ALL_CAPS constants, this scan would flag every legitimate constant in your codebase`
|
|
5927
|
+
);
|
|
5928
|
+
}
|
|
5057
5929
|
const api = readGraphFile(rootDir, "api");
|
|
5058
|
-
if (!api) return
|
|
5930
|
+
if (!api) return buildSkipped("static", "hardcoded_values", "no api graph");
|
|
5059
5931
|
const allCapsRe = /['"]([A-Z][A-Z_]{2,})['"]/g;
|
|
5060
5932
|
const seen = /* @__PURE__ */ new Set();
|
|
5061
5933
|
for (const node of api.nodes) {
|
|
5062
5934
|
if (node.type !== "endpoint") continue;
|
|
5063
|
-
const filePath = (0,
|
|
5064
|
-
if (!(0,
|
|
5065
|
-
const content = (0,
|
|
5935
|
+
const filePath = (0, import_node_path21.join)(rootDir, "src", node.id);
|
|
5936
|
+
if (!(0, import_node_fs19.existsSync)(filePath)) continue;
|
|
5937
|
+
const content = (0, import_node_fs19.readFileSync)(filePath, "utf-8");
|
|
5066
5938
|
let m;
|
|
5067
5939
|
allCapsRe.lastIndex = 0;
|
|
5068
5940
|
while ((m = allCapsRe.exec(content)) !== null) {
|
|
@@ -5094,7 +5966,19 @@ function buildReport(layer, check, findings) {
|
|
|
5094
5966
|
warnings: findings.filter((f) => f.severity === "warning").length,
|
|
5095
5967
|
info: findings.filter((f) => f.severity === "info").length
|
|
5096
5968
|
},
|
|
5097
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5969
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5970
|
+
status: "ok"
|
|
5971
|
+
};
|
|
5972
|
+
}
|
|
5973
|
+
function buildSkipped(layer, check, reason) {
|
|
5974
|
+
return {
|
|
5975
|
+
layer,
|
|
5976
|
+
check,
|
|
5977
|
+
findings: [],
|
|
5978
|
+
summary: { errors: 0, warnings: 0, info: 0 },
|
|
5979
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5980
|
+
status: "skipped",
|
|
5981
|
+
skipReason: reason
|
|
5098
5982
|
};
|
|
5099
5983
|
}
|
|
5100
5984
|
function getAvailableChecks() {
|
|
@@ -5141,7 +6025,12 @@ function runAudit(rootDir, layer, check) {
|
|
|
5141
6025
|
}
|
|
5142
6026
|
function formatAsPrompt(reports) {
|
|
5143
6027
|
const lines = [];
|
|
6028
|
+
const skipped = [];
|
|
5144
6029
|
for (const report of reports) {
|
|
6030
|
+
if (report.status === "skipped") {
|
|
6031
|
+
skipped.push(report);
|
|
6032
|
+
continue;
|
|
6033
|
+
}
|
|
5145
6034
|
if (report.findings.length === 0) continue;
|
|
5146
6035
|
lines.push(`## ${report.layer.toUpperCase()} \u2014 ${report.check} (${report.findings.length} findings)`);
|
|
5147
6036
|
lines.push("");
|
|
@@ -5153,15 +6042,24 @@ function formatAsPrompt(reports) {
|
|
|
5153
6042
|
}
|
|
5154
6043
|
lines.push("");
|
|
5155
6044
|
}
|
|
6045
|
+
if (skipped.length > 0) {
|
|
6046
|
+
lines.push("## Skipped checks (no comparison target \u2014 NOT passes)");
|
|
6047
|
+
lines.push("");
|
|
6048
|
+
for (const r of skipped) {
|
|
6049
|
+
lines.push(`- ${r.layer}/${r.check}: ${r.skipReason ?? "comparison target missing"}`);
|
|
6050
|
+
}
|
|
6051
|
+
lines.push("");
|
|
6052
|
+
}
|
|
5156
6053
|
if (lines.length === 0) return "No audit findings.";
|
|
5157
6054
|
return lines.join("\n");
|
|
5158
6055
|
}
|
|
5159
|
-
var
|
|
6056
|
+
var import_node_fs19, import_node_path21, CHECKS;
|
|
5160
6057
|
var init_audit_core = __esm({
|
|
5161
6058
|
"src/server/graph/core/audit-core.ts"() {
|
|
5162
6059
|
"use strict";
|
|
5163
|
-
|
|
5164
|
-
|
|
6060
|
+
import_node_fs19 = require("node:fs");
|
|
6061
|
+
import_node_path21 = require("node:path");
|
|
6062
|
+
init_audit_security();
|
|
5165
6063
|
CHECKS = {
|
|
5166
6064
|
db: {
|
|
5167
6065
|
schema_drift: checkSchemaDrift,
|
|
@@ -5176,6 +6074,11 @@ var init_audit_core = __esm({
|
|
|
5176
6074
|
static: {
|
|
5177
6075
|
unenforced_permissions: checkUnenforcedPermissions,
|
|
5178
6076
|
hardcoded_values: checkHardcodedValues
|
|
6077
|
+
},
|
|
6078
|
+
security: {
|
|
6079
|
+
response_secret_leak: (rootDir) => checkResponseSecretLeak(rootDir, { buildReport, buildSkipped }),
|
|
6080
|
+
env_dead_alias: (rootDir) => checkEnvDeadAlias(rootDir, { buildReport, buildSkipped }),
|
|
6081
|
+
hardcoded_url_fallback: (rootDir) => checkHardcodedUrlFallback(rootDir, { buildReport, buildSkipped })
|
|
5179
6082
|
}
|
|
5180
6083
|
};
|
|
5181
6084
|
}
|
|
@@ -5193,16 +6096,16 @@ function randomPort() {
|
|
|
5193
6096
|
function findProjectRoot(startDir) {
|
|
5194
6097
|
let dir = startDir;
|
|
5195
6098
|
for (let i = 0; i < 8; i++) {
|
|
5196
|
-
const graphsDir2 =
|
|
5197
|
-
if (
|
|
5198
|
-
const parent =
|
|
6099
|
+
const graphsDir2 = import_node_path22.default.join(dir, ".launchsecure", "graphs");
|
|
6100
|
+
if (import_node_fs20.default.existsSync(import_node_path22.default.join(graphsDir2, "ui.json")) || import_node_fs20.default.existsSync(import_node_path22.default.join(graphsDir2, "api.json")) || import_node_fs20.default.existsSync(import_node_path22.default.join(graphsDir2, "db.json"))) return dir;
|
|
6101
|
+
const parent = import_node_path22.default.dirname(dir);
|
|
5199
6102
|
if (parent === dir) break;
|
|
5200
6103
|
dir = parent;
|
|
5201
6104
|
}
|
|
5202
6105
|
dir = startDir;
|
|
5203
6106
|
for (let i = 0; i < 8; i++) {
|
|
5204
|
-
if (
|
|
5205
|
-
const parent =
|
|
6107
|
+
if (import_node_fs20.default.existsSync(import_node_path22.default.join(dir, ".git"))) return dir;
|
|
6108
|
+
const parent = import_node_path22.default.dirname(dir);
|
|
5206
6109
|
if (parent === dir) break;
|
|
5207
6110
|
dir = parent;
|
|
5208
6111
|
}
|
|
@@ -5211,7 +6114,7 @@ function findProjectRoot(startDir) {
|
|
|
5211
6114
|
function resolveRequestRoot(url, monorepoRoot, projects) {
|
|
5212
6115
|
const projectParam = url.searchParams.get("project");
|
|
5213
6116
|
if (!projectParam || projects.length === 0) return monorepoRoot;
|
|
5214
|
-
const resolved =
|
|
6117
|
+
const resolved = import_node_path22.default.resolve(monorepoRoot, projectParam);
|
|
5215
6118
|
if (!resolved.startsWith(monorepoRoot)) {
|
|
5216
6119
|
throw new Error("Project path outside monorepo root");
|
|
5217
6120
|
}
|
|
@@ -5262,16 +6165,16 @@ async function buildMergedGraph(root) {
|
|
|
5262
6165
|
};
|
|
5263
6166
|
}
|
|
5264
6167
|
function serveStatic(res, filePath) {
|
|
5265
|
-
if (!
|
|
5266
|
-
const ext =
|
|
6168
|
+
if (!import_node_fs20.default.existsSync(filePath) || !import_node_fs20.default.statSync(filePath).isFile()) return false;
|
|
6169
|
+
const ext = import_node_path22.default.extname(filePath).toLowerCase();
|
|
5267
6170
|
const mime = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
5268
6171
|
res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
|
|
5269
|
-
|
|
6172
|
+
import_node_fs20.default.createReadStream(filePath).pipe(res);
|
|
5270
6173
|
return true;
|
|
5271
6174
|
}
|
|
5272
6175
|
function serveIndex(res, clientDir) {
|
|
5273
|
-
const indexPath =
|
|
5274
|
-
if (!
|
|
6176
|
+
const indexPath = import_node_path22.default.join(clientDir, "index.html");
|
|
6177
|
+
if (!import_node_fs20.default.existsSync(indexPath)) {
|
|
5275
6178
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
5276
6179
|
res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
|
|
5277
6180
|
return;
|
|
@@ -5279,14 +6182,14 @@ function serveIndex(res, clientDir) {
|
|
|
5279
6182
|
serveStatic(res, indexPath);
|
|
5280
6183
|
}
|
|
5281
6184
|
function tryListen(server, port) {
|
|
5282
|
-
return new Promise((
|
|
6185
|
+
return new Promise((resolve5, reject) => {
|
|
5283
6186
|
const onError = (err2) => {
|
|
5284
6187
|
server.off("listening", onListening);
|
|
5285
6188
|
reject(err2);
|
|
5286
6189
|
};
|
|
5287
6190
|
const onListening = () => {
|
|
5288
6191
|
server.off("error", onError);
|
|
5289
|
-
|
|
6192
|
+
resolve5(port);
|
|
5290
6193
|
};
|
|
5291
6194
|
server.once("error", onError);
|
|
5292
6195
|
server.once("listening", onListening);
|
|
@@ -5323,7 +6226,7 @@ async function startChartServer(opts = {}) {
|
|
|
5323
6226
|
}
|
|
5324
6227
|
return { port: existing.port, url: existing.url };
|
|
5325
6228
|
}
|
|
5326
|
-
const clientDir = opts.clientDir ??
|
|
6229
|
+
const clientDir = opts.clientDir ?? import_node_path22.default.join(__dirname, "..", "chart-client");
|
|
5327
6230
|
const rootConfig = loadConfig(projectRoot);
|
|
5328
6231
|
const projects = rootConfig.projects ?? [];
|
|
5329
6232
|
const server = import_node_http.default.createServer((req, res) => {
|
|
@@ -5339,11 +6242,11 @@ async function startChartServer(opts = {}) {
|
|
|
5339
6242
|
}
|
|
5340
6243
|
if (req.method === "GET" && url2.pathname === "/api/projects") {
|
|
5341
6244
|
const projectList = projects.length > 0 ? projects.map((p) => {
|
|
5342
|
-
const absRoot =
|
|
5343
|
-
const hasGraphs =
|
|
5344
|
-
const hasNextConfig =
|
|
6245
|
+
const absRoot = import_node_path22.default.resolve(projectRoot, p.root);
|
|
6246
|
+
const hasGraphs = import_node_fs20.default.existsSync(import_node_path22.default.join(absRoot, ".launchsecure", "graphs"));
|
|
6247
|
+
const hasNextConfig = import_node_fs20.default.existsSync(import_node_path22.default.join(absRoot, "next.config.ts")) || import_node_fs20.default.existsSync(import_node_path22.default.join(absRoot, "next.config.js")) || import_node_fs20.default.existsSync(import_node_path22.default.join(absRoot, "next.config.mjs"));
|
|
5345
6248
|
return { name: p.name, root: p.root, hasGraphs, hasNextConfig };
|
|
5346
|
-
}) : [{ name:
|
|
6249
|
+
}) : [{ name: import_node_path22.default.basename(projectRoot), root: ".", hasGraphs: import_node_fs20.default.existsSync(import_node_path22.default.join(projectRoot, ".launchsecure", "graphs")), hasNextConfig: true }];
|
|
5347
6250
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5348
6251
|
res.end(JSON.stringify({ projects: projectList, monorepoRoot: projectRoot }));
|
|
5349
6252
|
return;
|
|
@@ -5389,20 +6292,20 @@ async function startChartServer(opts = {}) {
|
|
|
5389
6292
|
}
|
|
5390
6293
|
if (req.method === "GET" && url2.pathname === "/api/file-content") {
|
|
5391
6294
|
const relPath = url2.searchParams.get("path");
|
|
5392
|
-
if (!relPath || relPath.includes("..") ||
|
|
6295
|
+
if (!relPath || relPath.includes("..") || import_node_path22.default.isAbsolute(relPath)) {
|
|
5393
6296
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
5394
6297
|
res.end(JSON.stringify({ error: "Invalid path" }));
|
|
5395
6298
|
return;
|
|
5396
6299
|
}
|
|
5397
|
-
const filePath =
|
|
5398
|
-
if (!filePath.startsWith(reqRoot) || !
|
|
6300
|
+
const filePath = import_node_path22.default.join(reqRoot, relPath);
|
|
6301
|
+
if (!filePath.startsWith(reqRoot) || !import_node_fs20.default.existsSync(filePath) || !import_node_fs20.default.statSync(filePath).isFile()) {
|
|
5399
6302
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
5400
6303
|
res.end(JSON.stringify({ error: "File not found" }));
|
|
5401
6304
|
return;
|
|
5402
6305
|
}
|
|
5403
|
-
const ext =
|
|
6306
|
+
const ext = import_node_path22.default.extname(filePath).toLowerCase();
|
|
5404
6307
|
const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
|
|
5405
|
-
const content =
|
|
6308
|
+
const content = import_node_fs20.default.readFileSync(filePath, "utf-8");
|
|
5406
6309
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5407
6310
|
res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
|
|
5408
6311
|
return;
|
|
@@ -5444,8 +6347,8 @@ async function startChartServer(opts = {}) {
|
|
|
5444
6347
|
req.on("end", () => {
|
|
5445
6348
|
try {
|
|
5446
6349
|
const newConfig = JSON.parse(body);
|
|
5447
|
-
const configPath =
|
|
5448
|
-
|
|
6350
|
+
const configPath = import_node_path22.default.join(reqRoot, ".launchchart.json");
|
|
6351
|
+
import_node_fs20.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
|
|
5449
6352
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5450
6353
|
res.end(JSON.stringify({ ok: true }));
|
|
5451
6354
|
} catch (err2) {
|
|
@@ -5478,8 +6381,8 @@ async function startChartServer(opts = {}) {
|
|
|
5478
6381
|
const taggerConfig = JSON.parse(body);
|
|
5479
6382
|
const config2 = loadConfig(reqRoot);
|
|
5480
6383
|
config2.taggers = taggerConfig;
|
|
5481
|
-
const configPath =
|
|
5482
|
-
|
|
6384
|
+
const configPath = import_node_path22.default.join(reqRoot, ".launchchart.json");
|
|
6385
|
+
import_node_fs20.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
|
|
5483
6386
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5484
6387
|
res.end(JSON.stringify({ ok: true }));
|
|
5485
6388
|
} catch (err2) {
|
|
@@ -5549,7 +6452,7 @@ async function startChartServer(opts = {}) {
|
|
|
5549
6452
|
dbDir: !!config2.paths?.dbDir,
|
|
5550
6453
|
srcRoots: !!(config2.paths?.srcRoots && config2.paths.srcRoots.length > 0)
|
|
5551
6454
|
};
|
|
5552
|
-
const relFromRoot = (abs) =>
|
|
6455
|
+
const relFromRoot = (abs) => import_node_path22.default.relative(reqRoot, abs) || ".";
|
|
5553
6456
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5554
6457
|
res.end(JSON.stringify({
|
|
5555
6458
|
projectRoot: reqRoot,
|
|
@@ -5571,19 +6474,19 @@ async function startChartServer(opts = {}) {
|
|
|
5571
6474
|
}
|
|
5572
6475
|
if (req.method === "GET" && url2.pathname === "/api/browse-dir") {
|
|
5573
6476
|
const browsePath = url2.searchParams.get("path") || projectRoot;
|
|
5574
|
-
const abs =
|
|
5575
|
-
const twoUp =
|
|
6477
|
+
const abs = import_node_path22.default.resolve(browsePath);
|
|
6478
|
+
const twoUp = import_node_path22.default.resolve(projectRoot, "..", "..");
|
|
5576
6479
|
if (!abs.startsWith(twoUp)) {
|
|
5577
6480
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
5578
6481
|
res.end(JSON.stringify({ ok: false, error: "Path outside allowed range" }));
|
|
5579
6482
|
return;
|
|
5580
6483
|
}
|
|
5581
6484
|
try {
|
|
5582
|
-
const entries =
|
|
6485
|
+
const entries = import_node_fs20.default.readdirSync(abs, { withFileTypes: true });
|
|
5583
6486
|
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules" && e.name !== "dist" && e.name !== ".next").map((e) => e.name).sort();
|
|
5584
|
-
const parent = abs !== twoUp ?
|
|
6487
|
+
const parent = abs !== twoUp ? import_node_path22.default.dirname(abs) : null;
|
|
5585
6488
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5586
|
-
res.end(JSON.stringify({ current: abs, parent, dirs, relative:
|
|
6489
|
+
res.end(JSON.stringify({ current: abs, parent, dirs, relative: import_node_path22.default.relative(projectRoot, abs) || "." }));
|
|
5587
6490
|
} catch (err2) {
|
|
5588
6491
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
5589
6492
|
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
@@ -5609,8 +6512,8 @@ async function startChartServer(opts = {}) {
|
|
|
5609
6512
|
const { projects: newProjects } = JSON.parse(body);
|
|
5610
6513
|
const config2 = loadConfig(projectRoot);
|
|
5611
6514
|
config2.projects = newProjects.length > 0 ? newProjects : void 0;
|
|
5612
|
-
const configPath =
|
|
5613
|
-
|
|
6515
|
+
const configPath = import_node_path22.default.join(projectRoot, ".launchchart.json");
|
|
6516
|
+
import_node_fs20.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
|
|
5614
6517
|
projects.length = 0;
|
|
5615
6518
|
if (config2.projects) projects.push(...config2.projects);
|
|
5616
6519
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -5623,7 +6526,7 @@ async function startChartServer(opts = {}) {
|
|
|
5623
6526
|
return;
|
|
5624
6527
|
}
|
|
5625
6528
|
if (url2.pathname !== "/") {
|
|
5626
|
-
const staticPath =
|
|
6529
|
+
const staticPath = import_node_path22.default.join(clientDir, url2.pathname);
|
|
5627
6530
|
if (serveStatic(res, staticPath)) return;
|
|
5628
6531
|
}
|
|
5629
6532
|
serveIndex(res, clientDir);
|
|
@@ -5683,13 +6586,13 @@ function runServeCli(argv) {
|
|
|
5683
6586
|
process.exit(1);
|
|
5684
6587
|
});
|
|
5685
6588
|
}
|
|
5686
|
-
var import_node_http,
|
|
6589
|
+
var import_node_http, import_node_fs20, import_node_path22, MAX_PORT_SCAN, MIME_TYPES;
|
|
5687
6590
|
var init_chart_serve = __esm({
|
|
5688
6591
|
"src/server/chart-serve.ts"() {
|
|
5689
6592
|
"use strict";
|
|
5690
6593
|
import_node_http = __toESM(require("node:http"));
|
|
5691
|
-
|
|
5692
|
-
|
|
6594
|
+
import_node_fs20 = __toESM(require("node:fs"));
|
|
6595
|
+
import_node_path22 = __toESM(require("node:path"));
|
|
5693
6596
|
init_graph();
|
|
5694
6597
|
init_lockfile();
|
|
5695
6598
|
init_config();
|
|
@@ -5711,13 +6614,183 @@ var init_chart_serve = __esm({
|
|
|
5711
6614
|
}
|
|
5712
6615
|
});
|
|
5713
6616
|
|
|
6617
|
+
// src/server/graph/core/projects.ts
|
|
6618
|
+
function listProjects(monorepoRoot) {
|
|
6619
|
+
const cfg = loadConfig(monorepoRoot);
|
|
6620
|
+
const entries = cfg.projects ?? [];
|
|
6621
|
+
return entries.map((p) => ({
|
|
6622
|
+
name: p.name,
|
|
6623
|
+
root: p.root,
|
|
6624
|
+
absoluteRoot: (0, import_node_path23.resolve)(monorepoRoot, p.root)
|
|
6625
|
+
}));
|
|
6626
|
+
}
|
|
6627
|
+
function resolveProject(name, projects) {
|
|
6628
|
+
const exact = projects.find((p) => p.name === name || p.root === name);
|
|
6629
|
+
if (exact) return exact;
|
|
6630
|
+
const ci = projects.find((p) => p.name.toLowerCase() === name.toLowerCase());
|
|
6631
|
+
if (ci) return ci;
|
|
6632
|
+
const available = projects.map((p) => `"${p.name}" (${p.root})`).join(", ");
|
|
6633
|
+
throw new Error(`Unknown project "${name}". Available: ${available}.`);
|
|
6634
|
+
}
|
|
6635
|
+
function resolveProjectRoot(project, monorepoRoot) {
|
|
6636
|
+
const raw = typeof project === "string" ? project.trim() : "";
|
|
6637
|
+
if (!raw) return monorepoRoot;
|
|
6638
|
+
const projects = listProjects(monorepoRoot);
|
|
6639
|
+
if (projects.length === 0) {
|
|
6640
|
+
throw new Error(
|
|
6641
|
+
`project="${raw}" requested but .launchchart.json has no projects[] configured. Remove the project arg, or add the project to .launchchart.json.`
|
|
6642
|
+
);
|
|
6643
|
+
}
|
|
6644
|
+
return resolveProject(raw, projects).absoluteRoot;
|
|
6645
|
+
}
|
|
6646
|
+
var import_node_path23, PROJECT_PARAM_DESCRIPTION;
|
|
6647
|
+
var init_projects = __esm({
|
|
6648
|
+
"src/server/graph/core/projects.ts"() {
|
|
6649
|
+
"use strict";
|
|
6650
|
+
import_node_path23 = require("node:path");
|
|
6651
|
+
init_config();
|
|
6652
|
+
PROJECT_PARAM_DESCRIPTION = "Optional sub-project name (or root path) from .launchchart.json projects[]. Defaults to the monorepo root. Run detect_project_stack to list configured projects.";
|
|
6653
|
+
}
|
|
6654
|
+
});
|
|
6655
|
+
|
|
6656
|
+
// src/server/graph-cli.ts
|
|
6657
|
+
var graph_cli_exports = {};
|
|
6658
|
+
__export(graph_cli_exports, {
|
|
6659
|
+
handleGraphCommand: () => handleGraphCommand
|
|
6660
|
+
});
|
|
6661
|
+
function parseLayerFlag(args) {
|
|
6662
|
+
const idx = args.indexOf("--layer");
|
|
6663
|
+
if (idx < 0 || idx + 1 >= args.length) return void 0;
|
|
6664
|
+
return args[idx + 1];
|
|
6665
|
+
}
|
|
6666
|
+
function parseProjectFlag(args) {
|
|
6667
|
+
const idx = args.indexOf("--project");
|
|
6668
|
+
if (idx < 0 || idx + 1 >= args.length) return void 0;
|
|
6669
|
+
return args[idx + 1];
|
|
6670
|
+
}
|
|
6671
|
+
async function handleGraphCommand(subcommand, args) {
|
|
6672
|
+
const monorepoRoot = process.cwd();
|
|
6673
|
+
if (subcommand === "graph:generate") {
|
|
6674
|
+
const layer = parseLayerFlag(args);
|
|
6675
|
+
const projectArg = parseProjectFlag(args);
|
|
6676
|
+
const projects = listProjects(monorepoRoot);
|
|
6677
|
+
if (projectArg) {
|
|
6678
|
+
let target;
|
|
6679
|
+
try {
|
|
6680
|
+
target = resolveProject(projectArg, projects);
|
|
6681
|
+
} catch (e) {
|
|
6682
|
+
console.error(e.message);
|
|
6683
|
+
process.exit(1);
|
|
6684
|
+
}
|
|
6685
|
+
const results2 = await generateGraph(target.absoluteRoot, layer);
|
|
6686
|
+
if (results2.length === 0) {
|
|
6687
|
+
console.error(
|
|
6688
|
+
layer ? `No parser detected for the "${layer}" layer in project "${projectArg}".` : `No parsers detected for project "${projectArg}".`
|
|
6689
|
+
);
|
|
6690
|
+
process.exit(1);
|
|
6691
|
+
}
|
|
6692
|
+
console.log(`\u2713 "${target.name}" (${target.root}):`);
|
|
6693
|
+
for (const r of results2) {
|
|
6694
|
+
const warnings = r.output.warnings.length;
|
|
6695
|
+
console.log(` ${r.layer}: ${r.nodeCount} nodes, ${r.edgeCount} edges${warnings ? ` (${warnings} warnings)` : ""}`);
|
|
6696
|
+
}
|
|
6697
|
+
console.log(`Output: ${target.root}/.launchsecure/graphs/`);
|
|
6698
|
+
return;
|
|
6699
|
+
}
|
|
6700
|
+
if (projects.length > 0) {
|
|
6701
|
+
let okCount = 0;
|
|
6702
|
+
let failCount = 0;
|
|
6703
|
+
for (const proj of projects) {
|
|
6704
|
+
try {
|
|
6705
|
+
const results2 = await generateGraph(proj.absoluteRoot, layer);
|
|
6706
|
+
if (results2.length === 0) {
|
|
6707
|
+
console.error(` \u2717 "${proj.name}" (${proj.root}): no parsers detected`);
|
|
6708
|
+
failCount++;
|
|
6709
|
+
continue;
|
|
6710
|
+
}
|
|
6711
|
+
console.log(` \u2713 "${proj.name}" (${proj.root}):`);
|
|
6712
|
+
for (const r of results2) {
|
|
6713
|
+
const warnings = r.output.warnings.length;
|
|
6714
|
+
console.log(` ${r.layer}: ${r.nodeCount} nodes, ${r.edgeCount} edges${warnings ? ` (${warnings} warnings)` : ""}`);
|
|
6715
|
+
}
|
|
6716
|
+
okCount++;
|
|
6717
|
+
} catch (e) {
|
|
6718
|
+
console.error(` \u2717 "${proj.name}" (${proj.root}): ${e.message}`);
|
|
6719
|
+
failCount++;
|
|
6720
|
+
}
|
|
6721
|
+
}
|
|
6722
|
+
console.log(`
|
|
6723
|
+
Regenerated ${okCount}/${projects.length} project graph(s)${failCount ? ` (${failCount} failed)` : ""}.`);
|
|
6724
|
+
if (failCount > 0 && okCount === 0) process.exit(1);
|
|
6725
|
+
return;
|
|
6726
|
+
}
|
|
6727
|
+
const results = await generateGraph(monorepoRoot, layer);
|
|
6728
|
+
if (results.length === 0) {
|
|
6729
|
+
console.error(
|
|
6730
|
+
layer ? `No parser detected for the "${layer}" layer in this project.` : "No parsers detected for this project. Check that the project has the expected structure."
|
|
6731
|
+
);
|
|
6732
|
+
process.exit(1);
|
|
6733
|
+
}
|
|
6734
|
+
for (const r of results) {
|
|
6735
|
+
const warnings = r.output.warnings.length;
|
|
6736
|
+
console.log(` ${r.layer}: ${r.nodeCount} nodes, ${r.edgeCount} edges${warnings ? ` (${warnings} warnings)` : ""}`);
|
|
6737
|
+
}
|
|
6738
|
+
console.log(`Output: .launchsecure/graphs/`);
|
|
6739
|
+
return;
|
|
6740
|
+
}
|
|
6741
|
+
if (subcommand === "graph:read") {
|
|
6742
|
+
const layer = parseLayerFlag(args);
|
|
6743
|
+
const projectArg = parseProjectFlag(args);
|
|
6744
|
+
const projects = listProjects(monorepoRoot);
|
|
6745
|
+
let rootDir = monorepoRoot;
|
|
6746
|
+
if (projectArg) {
|
|
6747
|
+
try {
|
|
6748
|
+
rootDir = resolveProject(projectArg, projects).absoluteRoot;
|
|
6749
|
+
} catch (e) {
|
|
6750
|
+
console.error(e.message);
|
|
6751
|
+
process.exit(1);
|
|
6752
|
+
}
|
|
6753
|
+
}
|
|
6754
|
+
if (layer) {
|
|
6755
|
+
const available = getAvailableLayers(rootDir);
|
|
6756
|
+
if (available.length > 0 && !available.includes(layer)) {
|
|
6757
|
+
console.error(`No graph found for layer "${layer}". Available: ${available.join(", ")}`);
|
|
6758
|
+
process.exit(1);
|
|
6759
|
+
}
|
|
6760
|
+
const graph = readGraph(rootDir, layer);
|
|
6761
|
+
if (!graph) {
|
|
6762
|
+
console.error(`No ${layer} graph found. Run: launchpod graph:generate${projectArg ? ` --project ${projectArg}` : ""}`);
|
|
6763
|
+
process.exit(1);
|
|
6764
|
+
}
|
|
6765
|
+
console.log(JSON.stringify(graph, null, 2));
|
|
6766
|
+
} else {
|
|
6767
|
+
const graphs = readAllGraphs(rootDir);
|
|
6768
|
+
if (Object.keys(graphs).length === 0) {
|
|
6769
|
+
console.error(`No graphs found. Run: launchpod graph:generate${projectArg ? ` --project ${projectArg}` : ""}`);
|
|
6770
|
+
process.exit(1);
|
|
6771
|
+
}
|
|
6772
|
+
console.log(JSON.stringify(graphs, null, 2));
|
|
6773
|
+
}
|
|
6774
|
+
return;
|
|
6775
|
+
}
|
|
6776
|
+
console.error(`Unknown graph subcommand: ${subcommand}`);
|
|
6777
|
+
process.exit(1);
|
|
6778
|
+
}
|
|
6779
|
+
var init_graph_cli = __esm({
|
|
6780
|
+
"src/server/graph-cli.ts"() {
|
|
6781
|
+
"use strict";
|
|
6782
|
+
init_graph();
|
|
6783
|
+
init_projects();
|
|
6784
|
+
}
|
|
6785
|
+
});
|
|
6786
|
+
|
|
5714
6787
|
// src/server/graph/core/language-detection.ts
|
|
5715
6788
|
function walkForExtensions(dir, extCounts, depth = 0) {
|
|
5716
6789
|
if (depth > 10) return;
|
|
5717
|
-
if (!(0,
|
|
6790
|
+
if (!(0, import_node_fs21.existsSync)(dir)) return;
|
|
5718
6791
|
let entries;
|
|
5719
6792
|
try {
|
|
5720
|
-
entries = (0,
|
|
6793
|
+
entries = (0, import_node_fs21.readdirSync)(dir, { withFileTypes: true });
|
|
5721
6794
|
} catch {
|
|
5722
6795
|
return;
|
|
5723
6796
|
}
|
|
@@ -5725,9 +6798,9 @@ function walkForExtensions(dir, extCounts, depth = 0) {
|
|
|
5725
6798
|
if (entry.name.startsWith(".") && entry.isDirectory()) continue;
|
|
5726
6799
|
if (entry.isDirectory()) {
|
|
5727
6800
|
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
5728
|
-
walkForExtensions((0,
|
|
6801
|
+
walkForExtensions((0, import_node_path24.join)(dir, entry.name), extCounts, depth + 1);
|
|
5729
6802
|
} else {
|
|
5730
|
-
const ext = (0,
|
|
6803
|
+
const ext = (0, import_node_path24.extname)(entry.name).toLowerCase();
|
|
5731
6804
|
if (ext && EXTENSION_TO_LANGUAGE[ext]) {
|
|
5732
6805
|
extCounts.set(ext, (extCounts.get(ext) ?? 0) + 1);
|
|
5733
6806
|
}
|
|
@@ -5766,12 +6839,12 @@ function detectLanguages(rootDir, supportedLanguages) {
|
|
|
5766
6839
|
});
|
|
5767
6840
|
return results;
|
|
5768
6841
|
}
|
|
5769
|
-
var
|
|
6842
|
+
var import_node_fs21, import_node_path24, EXTENSION_TO_LANGUAGE, IGNORE_DIRS, AUXILIARY_LANGUAGES;
|
|
5770
6843
|
var init_language_detection = __esm({
|
|
5771
6844
|
"src/server/graph/core/language-detection.ts"() {
|
|
5772
6845
|
"use strict";
|
|
5773
|
-
|
|
5774
|
-
|
|
6846
|
+
import_node_fs21 = require("node:fs");
|
|
6847
|
+
import_node_path24 = require("node:path");
|
|
5775
6848
|
EXTENSION_TO_LANGUAGE = {
|
|
5776
6849
|
// Web / Frontend
|
|
5777
6850
|
".ts": "typescript",
|
|
@@ -5884,9 +6957,128 @@ var init_language_detection = __esm({
|
|
|
5884
6957
|
}
|
|
5885
6958
|
});
|
|
5886
6959
|
|
|
6960
|
+
// src/server/graph/core/watcher.ts
|
|
6961
|
+
var watcher_exports = {};
|
|
6962
|
+
__export(watcher_exports, {
|
|
6963
|
+
startGraphWatcher: () => startGraphWatcher
|
|
6964
|
+
});
|
|
6965
|
+
function isIgnoredPath(rel) {
|
|
6966
|
+
if (rel.startsWith(GRAPHS_RELATIVE)) return true;
|
|
6967
|
+
if (rel.endsWith(".lock") || rel.endsWith(".log")) return true;
|
|
6968
|
+
for (const part of rel.split(import_node_path25.sep)) {
|
|
6969
|
+
if (IGNORE_SEGMENTS.has(part)) return true;
|
|
6970
|
+
}
|
|
6971
|
+
return false;
|
|
6972
|
+
}
|
|
6973
|
+
function isTriggerFile(rel) {
|
|
6974
|
+
if (isIgnoredPath(rel)) return false;
|
|
6975
|
+
const dot = rel.lastIndexOf(".");
|
|
6976
|
+
if (dot < 0) return false;
|
|
6977
|
+
return TRIGGER_EXTENSIONS.has(rel.slice(dot));
|
|
6978
|
+
}
|
|
6979
|
+
function startGraphWatcher(rootDir, opts = {}) {
|
|
6980
|
+
const debounceMs = opts.debounceMs ?? 500;
|
|
6981
|
+
const pending = /* @__PURE__ */ new Set();
|
|
6982
|
+
let timer = null;
|
|
6983
|
+
let regenerating = false;
|
|
6984
|
+
let lastAt = null;
|
|
6985
|
+
let lastErr = null;
|
|
6986
|
+
async function flush() {
|
|
6987
|
+
if (regenerating) {
|
|
6988
|
+
timer = setTimeout(flush, debounceMs);
|
|
6989
|
+
return;
|
|
6990
|
+
}
|
|
6991
|
+
const changedFiles = Array.from(pending);
|
|
6992
|
+
pending.clear();
|
|
6993
|
+
timer = null;
|
|
6994
|
+
if (changedFiles.length === 0) return;
|
|
6995
|
+
regenerating = true;
|
|
6996
|
+
const start = Date.now();
|
|
6997
|
+
try {
|
|
6998
|
+
await generateGraph(rootDir);
|
|
6999
|
+
lastAt = Date.now();
|
|
7000
|
+
lastErr = null;
|
|
7001
|
+
opts.onRegen?.({ changedFiles, durationMs: lastAt - start });
|
|
7002
|
+
} catch (e) {
|
|
7003
|
+
const err2 = e instanceof Error ? e : new Error(String(e));
|
|
7004
|
+
lastErr = err2.message;
|
|
7005
|
+
opts.onError?.(err2);
|
|
7006
|
+
} finally {
|
|
7007
|
+
regenerating = false;
|
|
7008
|
+
}
|
|
7009
|
+
}
|
|
7010
|
+
const watcher = (0, import_node_fs22.watch)(rootDir, { recursive: true }, (event, filename) => {
|
|
7011
|
+
if (!filename) return;
|
|
7012
|
+
const rel = filename.toString();
|
|
7013
|
+
if (process.env.LAUNCH_CHART_WATCH_TRACE === "1") {
|
|
7014
|
+
process.stderr.write(`[lc-watcher trace] event=${event} file=${rel} trigger=${isTriggerFile(rel)}
|
|
7015
|
+
`);
|
|
7016
|
+
}
|
|
7017
|
+
if (!isTriggerFile(rel)) return;
|
|
7018
|
+
pending.add(rel);
|
|
7019
|
+
if (timer) clearTimeout(timer);
|
|
7020
|
+
timer = setTimeout(flush, debounceMs);
|
|
7021
|
+
});
|
|
7022
|
+
watcher.on("error", (e) => {
|
|
7023
|
+
const err2 = e instanceof Error ? e : new Error(String(e));
|
|
7024
|
+
lastErr = `watcher: ${err2.message}`;
|
|
7025
|
+
opts.onError?.(err2);
|
|
7026
|
+
});
|
|
7027
|
+
return {
|
|
7028
|
+
rootDir,
|
|
7029
|
+
stop() {
|
|
7030
|
+
if (timer) {
|
|
7031
|
+
clearTimeout(timer);
|
|
7032
|
+
timer = null;
|
|
7033
|
+
}
|
|
7034
|
+
watcher.close();
|
|
7035
|
+
},
|
|
7036
|
+
isRegenerating: () => regenerating,
|
|
7037
|
+
lastRegenAt: () => lastAt,
|
|
7038
|
+
lastError: () => lastErr,
|
|
7039
|
+
pendingCount: () => pending.size
|
|
7040
|
+
};
|
|
7041
|
+
}
|
|
7042
|
+
var import_node_fs22, import_node_path25, IGNORE_SEGMENTS, TRIGGER_EXTENSIONS, GRAPHS_RELATIVE;
|
|
7043
|
+
var init_watcher = __esm({
|
|
7044
|
+
"src/server/graph/core/watcher.ts"() {
|
|
7045
|
+
"use strict";
|
|
7046
|
+
import_node_fs22 = require("node:fs");
|
|
7047
|
+
import_node_path25 = require("node:path");
|
|
7048
|
+
init_graph();
|
|
7049
|
+
IGNORE_SEGMENTS = /* @__PURE__ */ new Set([
|
|
7050
|
+
"node_modules",
|
|
7051
|
+
".git",
|
|
7052
|
+
".claude",
|
|
7053
|
+
".next",
|
|
7054
|
+
".turbo",
|
|
7055
|
+
"dist",
|
|
7056
|
+
"build",
|
|
7057
|
+
"coverage",
|
|
7058
|
+
".cache",
|
|
7059
|
+
".vite",
|
|
7060
|
+
".parcel-cache"
|
|
7061
|
+
]);
|
|
7062
|
+
TRIGGER_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
7063
|
+
".ts",
|
|
7064
|
+
".tsx",
|
|
7065
|
+
".js",
|
|
7066
|
+
".jsx",
|
|
7067
|
+
".mts",
|
|
7068
|
+
".cts",
|
|
7069
|
+
".mjs",
|
|
7070
|
+
".cjs",
|
|
7071
|
+
".prisma",
|
|
7072
|
+
".sql"
|
|
7073
|
+
]);
|
|
7074
|
+
GRAPHS_RELATIVE = (0, import_node_path25.join)(".launchsecure", "graphs");
|
|
7075
|
+
}
|
|
7076
|
+
});
|
|
7077
|
+
|
|
5887
7078
|
// src/server/graph-mcp.ts
|
|
5888
7079
|
var graph_mcp_exports = {};
|
|
5889
7080
|
__export(graph_mcp_exports, {
|
|
7081
|
+
getWatcherHandle: () => getWatcherHandle,
|
|
5890
7082
|
startGraphMcpServer: () => startGraphMcpServer
|
|
5891
7083
|
});
|
|
5892
7084
|
function matchesSearch(node, query) {
|
|
@@ -5906,6 +7098,14 @@ function toMinimal(nodes) {
|
|
|
5906
7098
|
return out;
|
|
5907
7099
|
});
|
|
5908
7100
|
}
|
|
7101
|
+
function crossRefsAsEdges(graph) {
|
|
7102
|
+
return (graph.cross_refs ?? []).map((c) => ({
|
|
7103
|
+
source: c.source,
|
|
7104
|
+
target: c.target,
|
|
7105
|
+
type: c.type,
|
|
7106
|
+
target_layer: c.layer
|
|
7107
|
+
}));
|
|
7108
|
+
}
|
|
5909
7109
|
function toCompactNode(n) {
|
|
5910
7110
|
const out = { i: n.id, t: n.type, n: n.name };
|
|
5911
7111
|
const tags = n.tags;
|
|
@@ -5930,6 +7130,8 @@ function toCompactEdges(edges, idx) {
|
|
|
5930
7130
|
t: e.type
|
|
5931
7131
|
};
|
|
5932
7132
|
if (e.label != null) o.l = e.label;
|
|
7133
|
+
const targetLayer = e.target_layer;
|
|
7134
|
+
if (targetLayer != null) o.tl = targetLayer;
|
|
5933
7135
|
return o;
|
|
5934
7136
|
});
|
|
5935
7137
|
}
|
|
@@ -5970,6 +7172,9 @@ function neighborhood(graph, centerId, hops, layer, minimal) {
|
|
|
5970
7172
|
const dstIn = visited.has(e.target) || next.has(e.target);
|
|
5971
7173
|
if (srcIn && dstIn) projectedEdges++;
|
|
5972
7174
|
}
|
|
7175
|
+
for (const c of graph.cross_refs ?? []) {
|
|
7176
|
+
if (visited.has(c.source) || next.has(c.source)) projectedEdges++;
|
|
7177
|
+
}
|
|
5973
7178
|
const perNode = minimal ? EST_CHARS_PER_NODE_MIN[layer] ?? DEFAULT_EST_NODE_MIN : EST_CHARS_PER_NODE_FULL[layer] ?? DEFAULT_EST_NODE_FULL;
|
|
5974
7179
|
const projectedChars = projectedVisited * perNode + projectedEdges * (EST_CHARS_PER_EDGE[layer] ?? DEFAULT_EST_EDGE);
|
|
5975
7180
|
if (projectedChars > NEIGHBORHOOD_BUDGET_CHARS) {
|
|
@@ -5982,8 +7187,9 @@ function neighborhood(graph, centerId, hops, layer, minimal) {
|
|
|
5982
7187
|
if (frontier.size === 0) break;
|
|
5983
7188
|
}
|
|
5984
7189
|
const nodes = graph.nodes.filter((n) => visited.has(n.id));
|
|
5985
|
-
const
|
|
5986
|
-
|
|
7190
|
+
const sameLayerEdges = graph.edges.filter((e) => visited.has(e.source) && visited.has(e.target));
|
|
7191
|
+
const crossLayerEdges = crossRefsAsEdges(graph).filter((e) => visited.has(e.source));
|
|
7192
|
+
return { nodes, edges: [...sameLayerEdges, ...crossLayerEdges], budgetExceeded, stoppedAtHop };
|
|
5987
7193
|
}
|
|
5988
7194
|
function reverseNeighborhood(graph, centerId, hops, direction) {
|
|
5989
7195
|
const center = graph.nodes.find((n) => n.id === centerId);
|
|
@@ -6013,7 +7219,9 @@ function reverseNeighborhood(graph, centerId, hops, direction) {
|
|
|
6013
7219
|
return { nodes: visited, edges };
|
|
6014
7220
|
}
|
|
6015
7221
|
function handleBlastPoints(args) {
|
|
6016
|
-
const
|
|
7222
|
+
const __resolved = resolveOrErr(args);
|
|
7223
|
+
if ("content" in __resolved) return __resolved;
|
|
7224
|
+
const { rootDir } = __resolved;
|
|
6017
7225
|
const nodeId = args.node_id;
|
|
6018
7226
|
const requestedLayer = args.layer;
|
|
6019
7227
|
const hops = args.hops ?? 2;
|
|
@@ -6057,7 +7265,11 @@ function handleBlastPoints(args) {
|
|
|
6057
7265
|
for (const otherLayer of otherLayers) {
|
|
6058
7266
|
const otherGraph = readGraph(rootDir, otherLayer);
|
|
6059
7267
|
if (!otherGraph) continue;
|
|
6060
|
-
|
|
7268
|
+
const candidates = [
|
|
7269
|
+
...otherGraph.edges,
|
|
7270
|
+
...otherGraph.cross_refs ?? []
|
|
7271
|
+
];
|
|
7272
|
+
for (const edge of candidates) {
|
|
6061
7273
|
if (edge.target === nodeId || edge.source === nodeId) {
|
|
6062
7274
|
const dependentId = edge.target === nodeId ? edge.source : edge.target;
|
|
6063
7275
|
if (affected.some((a) => a.id === dependentId)) continue;
|
|
@@ -6084,7 +7296,7 @@ function handleBlastPoints(args) {
|
|
|
6084
7296
|
byHop[String(a.hop)] = (byHop[String(a.hop)] ?? 0) + 1;
|
|
6085
7297
|
if (a.module) modulesSet.add(a.module);
|
|
6086
7298
|
}
|
|
6087
|
-
const crossesLayers = Object.keys(byLayer).
|
|
7299
|
+
const crossesLayers = Object.keys(byLayer).some((l) => l !== targetLayer);
|
|
6088
7300
|
const centerTags = center.tags;
|
|
6089
7301
|
return okJson({
|
|
6090
7302
|
center: {
|
|
@@ -6135,26 +7347,89 @@ function okJson(data) {
|
|
|
6135
7347
|
function err(text) {
|
|
6136
7348
|
return { content: [{ type: "text", text }], isError: true };
|
|
6137
7349
|
}
|
|
7350
|
+
function resolveOrErr(args) {
|
|
7351
|
+
try {
|
|
7352
|
+
return { rootDir: resolveProjectRoot(args.project, process.cwd()) };
|
|
7353
|
+
} catch (e) {
|
|
7354
|
+
return err(e.message);
|
|
7355
|
+
}
|
|
7356
|
+
}
|
|
6138
7357
|
async function handleGenerateGraph(args) {
|
|
6139
|
-
const
|
|
7358
|
+
const monorepoRoot = process.cwd();
|
|
6140
7359
|
const layer = args.layer;
|
|
6141
|
-
const
|
|
7360
|
+
const projectArg = typeof args.project === "string" ? args.project.trim() : "";
|
|
7361
|
+
function formatProjectResult(results2, relativeRoot) {
|
|
7362
|
+
return results2.map((r) => {
|
|
7363
|
+
const warnings = r.output.warnings.length;
|
|
7364
|
+
return ` ${r.layer}: ${r.nodeCount} nodes, ${r.edgeCount} edges${warnings ? ` (${warnings} warnings)` : ""}`;
|
|
7365
|
+
}).join("\n") + `
|
|
7366
|
+
\u2192 ${relativeRoot}/.launchsecure/graphs/`;
|
|
7367
|
+
}
|
|
7368
|
+
if (projectArg) {
|
|
7369
|
+
let rootDir;
|
|
7370
|
+
try {
|
|
7371
|
+
rootDir = resolveProjectRoot(projectArg, monorepoRoot);
|
|
7372
|
+
} catch (e) {
|
|
7373
|
+
return err(e.message);
|
|
7374
|
+
}
|
|
7375
|
+
const results2 = await generateGraph(rootDir, layer);
|
|
7376
|
+
if (results2.length === 0) {
|
|
7377
|
+
return err(
|
|
7378
|
+
layer ? `No parser detected for the "${layer}" layer in project "${projectArg}".` : `No parsers detected for project "${projectArg}". Check that the project root has the expected structure.`
|
|
7379
|
+
);
|
|
7380
|
+
}
|
|
7381
|
+
return ok(
|
|
7382
|
+
`Graph generated successfully for project "${projectArg}".
|
|
7383
|
+
|
|
7384
|
+
${formatProjectResult(results2, projectArg)}
|
|
7385
|
+
|
|
7386
|
+
Use read_graph (with project="${projectArg}") to query.`
|
|
7387
|
+
);
|
|
7388
|
+
}
|
|
7389
|
+
const projects = listProjects(monorepoRoot);
|
|
7390
|
+
if (projects.length > 0) {
|
|
7391
|
+
const lines = [];
|
|
7392
|
+
let okCount = 0;
|
|
7393
|
+
let failCount = 0;
|
|
7394
|
+
for (const proj of projects) {
|
|
7395
|
+
try {
|
|
7396
|
+
const results2 = await generateGraph(proj.absoluteRoot, layer);
|
|
7397
|
+
if (results2.length === 0) {
|
|
7398
|
+
lines.push(` \u2717 "${proj.name}" (${proj.root}): no parsers detected`);
|
|
7399
|
+
failCount++;
|
|
7400
|
+
continue;
|
|
7401
|
+
}
|
|
7402
|
+
lines.push(` \u2713 "${proj.name}" (${proj.root}):`);
|
|
7403
|
+
for (const r of results2) {
|
|
7404
|
+
const warnings = r.output.warnings.length;
|
|
7405
|
+
lines.push(` ${r.layer}: ${r.nodeCount} nodes, ${r.edgeCount} edges${warnings ? ` (${warnings} warnings)` : ""}`);
|
|
7406
|
+
}
|
|
7407
|
+
okCount++;
|
|
7408
|
+
} catch (e) {
|
|
7409
|
+
lines.push(` \u2717 "${proj.name}" (${proj.root}): ${e.message}`);
|
|
7410
|
+
failCount++;
|
|
7411
|
+
}
|
|
7412
|
+
}
|
|
7413
|
+
const header = `Regenerated ${okCount}/${projects.length} project graph(s)${failCount ? ` (${failCount} failed)` : ""}.`;
|
|
7414
|
+
return failCount > 0 && okCount === 0 ? err(`${header}
|
|
7415
|
+
|
|
7416
|
+
${lines.join("\n")}`) : ok(`${header}
|
|
7417
|
+
|
|
7418
|
+
${lines.join("\n")}
|
|
7419
|
+
|
|
7420
|
+
Use read_graph (with optional project="<name>") to query.`);
|
|
7421
|
+
}
|
|
7422
|
+
const results = await generateGraph(monorepoRoot, layer);
|
|
6142
7423
|
if (results.length === 0) {
|
|
6143
7424
|
return err(
|
|
6144
7425
|
layer ? `No parser detected for the "${layer}" layer in this project.` : "No parsers detected for this project. Check that the project has the expected structure."
|
|
6145
7426
|
);
|
|
6146
7427
|
}
|
|
6147
|
-
const summary = results.map((r) => {
|
|
6148
|
-
const warnings = r.output.warnings.length;
|
|
6149
|
-
return ` ${r.layer}: ${r.nodeCount} nodes, ${r.edgeCount} edges${warnings ? ` (${warnings} warnings)` : ""}`;
|
|
6150
|
-
}).join("\n");
|
|
6151
7428
|
return ok(
|
|
6152
7429
|
`Graph generated successfully.
|
|
6153
7430
|
|
|
6154
7431
|
Layers:
|
|
6155
|
-
${
|
|
6156
|
-
|
|
6157
|
-
Output: .launchsecure/graphs/
|
|
7432
|
+
${formatProjectResult(results, ".")}
|
|
6158
7433
|
|
|
6159
7434
|
Use read_graph with filters (search/type/module/node_id) to query.`
|
|
6160
7435
|
);
|
|
@@ -6237,8 +7512,6 @@ function runReadGraphQueryRaw(rootDir, args) {
|
|
|
6237
7512
|
if (tagKey && tagValue && nodeTags?.[tagKey] !== tagValue) return false;
|
|
6238
7513
|
return true;
|
|
6239
7514
|
});
|
|
6240
|
-
const matchedIds = new Set(matched.map((n) => n.id));
|
|
6241
|
-
const matchedEdges = graph.edges.filter((e) => matchedIds.has(e.source) && matchedIds.has(e.target));
|
|
6242
7515
|
if (matched.length === 0) {
|
|
6243
7516
|
return {
|
|
6244
7517
|
layer,
|
|
@@ -6252,7 +7525,9 @@ function runReadGraphQueryRaw(rootDir, args) {
|
|
|
6252
7525
|
const hasMore = offset + paginatedNodes.length < totalMatched;
|
|
6253
7526
|
const wantEdges = includeEdges ?? false;
|
|
6254
7527
|
const returnedIds = new Set(paginatedNodes.map((n) => n.id));
|
|
6255
|
-
const
|
|
7528
|
+
const sameLayerReturnedEdges = graph.edges.filter((e) => returnedIds.has(e.source) && returnedIds.has(e.target));
|
|
7529
|
+
const crossLayerReturnedEdges = crossRefsAsEdges(graph).filter((e) => returnedIds.has(e.source));
|
|
7530
|
+
const returnedEdges = [...sameLayerReturnedEdges, ...crossLayerReturnedEdges];
|
|
6256
7531
|
const result = {
|
|
6257
7532
|
layer,
|
|
6258
7533
|
filter: { search, type, module: module_ },
|
|
@@ -6267,7 +7542,14 @@ function runReadGraphQueryRaw(rootDir, args) {
|
|
|
6267
7542
|
result.next_offset = offset + paginatedNodes.length;
|
|
6268
7543
|
}
|
|
6269
7544
|
if (wantEdges) {
|
|
6270
|
-
|
|
7545
|
+
if (returnedEdges.length > MAX_FILTER_EDGES) {
|
|
7546
|
+
result.edges = returnedEdges.slice(0, MAX_FILTER_EDGES);
|
|
7547
|
+
result.edges_truncated = true;
|
|
7548
|
+
result.total_edges = returnedEdges.length;
|
|
7549
|
+
result.edges_hint = `Showing first ${MAX_FILTER_EDGES} of ${returnedEdges.length} edges. Narrow with type/module/node_id, or call read_graph on a single node_id to walk its neighborhood with full edges.`;
|
|
7550
|
+
} else {
|
|
7551
|
+
result.edges = returnedEdges;
|
|
7552
|
+
}
|
|
6271
7553
|
} else if (returnedEdges.length > 0) {
|
|
6272
7554
|
result.edges_hint = `${returnedEdges.length} edges between matched nodes omitted. Pass include_edges:true to retrieve them (only do this when you actually need edge data).`;
|
|
6273
7555
|
}
|
|
@@ -6278,12 +7560,13 @@ function runReadGraphQuery(rootDir, args) {
|
|
|
6278
7560
|
return compactResult(raw);
|
|
6279
7561
|
}
|
|
6280
7562
|
function handleReadGraph(args) {
|
|
6281
|
-
const
|
|
7563
|
+
const monorepoRoot = process.cwd();
|
|
6282
7564
|
if (Array.isArray(args.queries)) {
|
|
6283
7565
|
const queries = args.queries;
|
|
6284
7566
|
if (queries.length === 0) {
|
|
6285
7567
|
return err("queries array is empty. Provide at least one query object.");
|
|
6286
7568
|
}
|
|
7569
|
+
const inheritedProject = typeof args.project === "string" ? args.project : void 0;
|
|
6287
7570
|
const results = [];
|
|
6288
7571
|
let cumulativeChars = 0;
|
|
6289
7572
|
let budgetHit = false;
|
|
@@ -6301,7 +7584,15 @@ function handleReadGraph(args) {
|
|
|
6301
7584
|
});
|
|
6302
7585
|
continue;
|
|
6303
7586
|
}
|
|
6304
|
-
const
|
|
7587
|
+
const qWithProject = inheritedProject && !q.project ? { ...q, project: inheritedProject } : q;
|
|
7588
|
+
let perQueryRoot;
|
|
7589
|
+
try {
|
|
7590
|
+
perQueryRoot = resolveProjectRoot(qWithProject.project, monorepoRoot);
|
|
7591
|
+
} catch (e) {
|
|
7592
|
+
results.push({ index: i, query: q, result: { error: e.message } });
|
|
7593
|
+
continue;
|
|
7594
|
+
}
|
|
7595
|
+
const r = runReadGraphQuery(perQueryRoot, qWithProject);
|
|
6305
7596
|
const entry = { index: i, query: q, result: r };
|
|
6306
7597
|
const entrySize = JSON.stringify(entry, null, 2).length;
|
|
6307
7598
|
if (cumulativeChars + entrySize > BATCH_BUDGET_CHARS && results.length > 0) {
|
|
@@ -6328,20 +7619,24 @@ function handleReadGraph(args) {
|
|
|
6328
7619
|
results
|
|
6329
7620
|
});
|
|
6330
7621
|
}
|
|
6331
|
-
const
|
|
7622
|
+
const __resolved = resolveOrErr(args);
|
|
7623
|
+
if ("content" in __resolved) return __resolved;
|
|
7624
|
+
const result = runReadGraphQuery(__resolved.rootDir, args);
|
|
6332
7625
|
return okJson(result);
|
|
6333
7626
|
}
|
|
6334
7627
|
function nodeToFilePath(rootDir, layer, nodeId) {
|
|
6335
|
-
if (layer === "ui" || layer === "api") return (0,
|
|
6336
|
-
if (layer === "db") return (0,
|
|
6337
|
-
const withSrc = (0,
|
|
6338
|
-
if ((0,
|
|
6339
|
-
const direct = (0,
|
|
6340
|
-
if ((0,
|
|
7628
|
+
if (layer === "ui" || layer === "api") return (0, import_node_path26.join)(rootDir, "src", nodeId);
|
|
7629
|
+
if (layer === "db") return (0, import_node_path26.join)(rootDir, "prisma", "schema.prisma");
|
|
7630
|
+
const withSrc = (0, import_node_path26.join)(rootDir, "src", nodeId);
|
|
7631
|
+
if ((0, import_node_fs23.existsSync)(withSrc)) return withSrc;
|
|
7632
|
+
const direct = (0, import_node_path26.join)(rootDir, nodeId);
|
|
7633
|
+
if ((0, import_node_fs23.existsSync)(direct)) return direct;
|
|
6341
7634
|
return null;
|
|
6342
7635
|
}
|
|
6343
7636
|
function handleInspectNode(args) {
|
|
6344
|
-
const
|
|
7637
|
+
const __resolved = resolveOrErr(args);
|
|
7638
|
+
if ("content" in __resolved) return __resolved;
|
|
7639
|
+
const { rootDir } = __resolved;
|
|
6345
7640
|
const layer = args.layer;
|
|
6346
7641
|
const nodeId = args.node_id;
|
|
6347
7642
|
const search = args.search;
|
|
@@ -6365,7 +7660,7 @@ function handleInspectNode(args) {
|
|
|
6365
7660
|
} else {
|
|
6366
7661
|
matched = graph.nodes;
|
|
6367
7662
|
}
|
|
6368
|
-
const allDeepFields = ["elements", "stateVars", "conditions", "variables", "responses", "params"];
|
|
7663
|
+
const allDeepFields = ["elements", "stateVars", "conditions", "variables", "responses", "params", "effects"];
|
|
6369
7664
|
const requestedFields = fields ?? allDeepFields;
|
|
6370
7665
|
let filterRegex = null;
|
|
6371
7666
|
if (filter) {
|
|
@@ -6421,7 +7716,9 @@ function handleInspectNode(args) {
|
|
|
6421
7716
|
});
|
|
6422
7717
|
}
|
|
6423
7718
|
function handleGrepNodes(args) {
|
|
6424
|
-
const
|
|
7719
|
+
const __resolved = resolveOrErr(args);
|
|
7720
|
+
if ("content" in __resolved) return __resolved;
|
|
7721
|
+
const { rootDir } = __resolved;
|
|
6425
7722
|
const pattern = args.pattern;
|
|
6426
7723
|
const layer = args.layer;
|
|
6427
7724
|
if (!pattern) return err("pattern is required");
|
|
@@ -6480,11 +7777,11 @@ function handleGrepNodes(args) {
|
|
|
6480
7777
|
let filesSearched = 0;
|
|
6481
7778
|
let truncated = false;
|
|
6482
7779
|
for (const [filePath, nodeId] of filePaths) {
|
|
6483
|
-
if (!(0,
|
|
7780
|
+
if (!(0, import_node_fs23.existsSync)(filePath)) continue;
|
|
6484
7781
|
filesSearched++;
|
|
6485
7782
|
let content;
|
|
6486
7783
|
try {
|
|
6487
|
-
content = (0,
|
|
7784
|
+
content = (0, import_node_fs23.readFileSync)(filePath, "utf-8");
|
|
6488
7785
|
} catch {
|
|
6489
7786
|
continue;
|
|
6490
7787
|
}
|
|
@@ -6521,11 +7818,61 @@ function handleGrepNodes(args) {
|
|
|
6521
7818
|
truncated
|
|
6522
7819
|
});
|
|
6523
7820
|
}
|
|
7821
|
+
function handleEffectsIndex(args) {
|
|
7822
|
+
const __resolved = resolveOrErr(args);
|
|
7823
|
+
if ("content" in __resolved) return __resolved;
|
|
7824
|
+
const { rootDir } = __resolved;
|
|
7825
|
+
const idx = readEffectsIndex(rootDir);
|
|
7826
|
+
if (!idx) {
|
|
7827
|
+
return err("No effects-index.json found. Run generate_graph first (or wait for the file-watcher to fire).");
|
|
7828
|
+
}
|
|
7829
|
+
const kind = args.kind ?? "collisions";
|
|
7830
|
+
const key = args.key;
|
|
7831
|
+
const MAX_KEYS = 200;
|
|
7832
|
+
if (kind === "singleton_risks") {
|
|
7833
|
+
return okJson({ kind, count: idx.singleton_risks.length, nodes: idx.singleton_risks });
|
|
7834
|
+
}
|
|
7835
|
+
if (kind === "collisions") {
|
|
7836
|
+
return okJson({
|
|
7837
|
+
kind,
|
|
7838
|
+
dom_ids: idx.collisions.dom_ids,
|
|
7839
|
+
storage_keys: idx.collisions.storage_keys,
|
|
7840
|
+
window_events: idx.collisions.window_events
|
|
7841
|
+
});
|
|
7842
|
+
}
|
|
7843
|
+
const map = idx[kind];
|
|
7844
|
+
if (!map || typeof map !== "object") {
|
|
7845
|
+
return err(`Unknown kind "${kind}". Valid: dom_ids, window_events, storage_keys, fetch_urls, timers, singleton_risks, collisions.`);
|
|
7846
|
+
}
|
|
7847
|
+
if (key) {
|
|
7848
|
+
const nodes = map[key] ?? [];
|
|
7849
|
+
return okJson({ kind, key, nodes });
|
|
7850
|
+
}
|
|
7851
|
+
const entries = Object.entries(map);
|
|
7852
|
+
const truncated = entries.length > MAX_KEYS;
|
|
7853
|
+
const slice = entries.slice(0, MAX_KEYS);
|
|
7854
|
+
const results = {};
|
|
7855
|
+
for (const [k, v] of slice) results[k] = v;
|
|
7856
|
+
return okJson({
|
|
7857
|
+
kind,
|
|
7858
|
+
total_keys: entries.length,
|
|
7859
|
+
...truncated ? { truncated: true, showing: MAX_KEYS, hint: "Pass `key` to look up a specific value." } : {},
|
|
7860
|
+
results
|
|
7861
|
+
});
|
|
7862
|
+
}
|
|
6524
7863
|
function handleChartServerStatus() {
|
|
6525
7864
|
const rootDir = process.cwd();
|
|
6526
7865
|
const lock = getLiveLock(rootDir);
|
|
7866
|
+
const watcher = getWatcherHandle();
|
|
7867
|
+
const watcherInfo = watcher ? {
|
|
7868
|
+
active: true,
|
|
7869
|
+
regenerating: watcher.isRegenerating(),
|
|
7870
|
+
last_regen_at: watcher.lastRegenAt(),
|
|
7871
|
+
pending_events: watcher.pendingCount(),
|
|
7872
|
+
...watcher.lastError() ? { last_error: watcher.lastError() } : {}
|
|
7873
|
+
} : { active: false };
|
|
6527
7874
|
if (!lock) {
|
|
6528
|
-
return okJson({ running: false });
|
|
7875
|
+
return okJson({ running: false, watcher: watcherInfo });
|
|
6529
7876
|
}
|
|
6530
7877
|
return okJson({
|
|
6531
7878
|
running: true,
|
|
@@ -6533,7 +7880,8 @@ function handleChartServerStatus() {
|
|
|
6533
7880
|
port: lock.port,
|
|
6534
7881
|
pid: lock.pid,
|
|
6535
7882
|
cwd: lock.cwd,
|
|
6536
|
-
startedAt: lock.startedAt
|
|
7883
|
+
startedAt: lock.startedAt,
|
|
7884
|
+
watcher: watcherInfo
|
|
6537
7885
|
});
|
|
6538
7886
|
}
|
|
6539
7887
|
function handleStartChartServer(args) {
|
|
@@ -6549,11 +7897,11 @@ function handleStartChartServer(args) {
|
|
|
6549
7897
|
});
|
|
6550
7898
|
}
|
|
6551
7899
|
const entryPath = process.argv[1];
|
|
6552
|
-
const logDir = (0,
|
|
6553
|
-
(0,
|
|
6554
|
-
const logPath = (0,
|
|
6555
|
-
const out = (0,
|
|
6556
|
-
const err2 = (0,
|
|
7900
|
+
const logDir = (0, import_node_path26.join)((0, import_node_os2.homedir)(), ".launchsecure");
|
|
7901
|
+
(0, import_node_fs23.mkdirSync)(logDir, { recursive: true });
|
|
7902
|
+
const logPath = (0, import_node_path26.join)(logDir, "launch-chart.log");
|
|
7903
|
+
const out = (0, import_node_fs23.openSync)(logPath, "a");
|
|
7904
|
+
const err2 = (0, import_node_fs23.openSync)(logPath, "a");
|
|
6557
7905
|
const portArgs = args.port ? ["--port", String(args.port)] : [];
|
|
6558
7906
|
const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
|
|
6559
7907
|
detached: true,
|
|
@@ -6586,7 +7934,9 @@ function handleStopChartServer() {
|
|
|
6586
7934
|
}
|
|
6587
7935
|
}
|
|
6588
7936
|
function handleAddTag(args) {
|
|
6589
|
-
const
|
|
7937
|
+
const __resolved = resolveOrErr(args);
|
|
7938
|
+
if ("content" in __resolved) return __resolved;
|
|
7939
|
+
const { rootDir } = __resolved;
|
|
6590
7940
|
const nodeId = args.node_id;
|
|
6591
7941
|
const key = args.key;
|
|
6592
7942
|
const value = args.value;
|
|
@@ -6608,7 +7958,9 @@ function handleAddTag(args) {
|
|
|
6608
7958
|
return okJson({ ok: true, node_id: nodeId, tag: { [key]: value } });
|
|
6609
7959
|
}
|
|
6610
7960
|
function handleRemoveTag(args) {
|
|
6611
|
-
const
|
|
7961
|
+
const __resolved = resolveOrErr(args);
|
|
7962
|
+
if ("content" in __resolved) return __resolved;
|
|
7963
|
+
const { rootDir } = __resolved;
|
|
6612
7964
|
const nodeId = args.node_id;
|
|
6613
7965
|
const key = args.key;
|
|
6614
7966
|
if (!nodeId) return err("node_id is required");
|
|
@@ -6617,7 +7969,9 @@ function handleRemoveTag(args) {
|
|
|
6617
7969
|
return okJson({ ok: true, node_id: nodeId, removed_key: key });
|
|
6618
7970
|
}
|
|
6619
7971
|
function handleAuditLayer(args) {
|
|
6620
|
-
const
|
|
7972
|
+
const __resolved = resolveOrErr(args);
|
|
7973
|
+
if ("content" in __resolved) return __resolved;
|
|
7974
|
+
const { rootDir } = __resolved;
|
|
6621
7975
|
const layer = args.layer;
|
|
6622
7976
|
const check = args.check;
|
|
6623
7977
|
if (!layer) return err("layer is required");
|
|
@@ -6630,6 +7984,10 @@ function handleAuditLayer(args) {
|
|
|
6630
7984
|
lines.push(`Audit: ${layer}${check ? ` / ${check}` : ""} \u2014 ${totalFindings} findings (${totalErrors} errors, ${totalWarnings} warnings, ${totalInfo} info)`);
|
|
6631
7985
|
lines.push("");
|
|
6632
7986
|
for (const report of reports) {
|
|
7987
|
+
if (report.status === "skipped") {
|
|
7988
|
+
lines.push(`\u2298 ${report.check}: skipped \u2014 ${report.skipReason ?? "comparison target missing"} (NOT a pass)`);
|
|
7989
|
+
continue;
|
|
7990
|
+
}
|
|
6633
7991
|
if (report.findings.length === 0) {
|
|
6634
7992
|
lines.push(`\u2713 ${report.check}: no issues found`);
|
|
6635
7993
|
continue;
|
|
@@ -6673,20 +8031,20 @@ function handleDetectProjectStack() {
|
|
|
6673
8031
|
if (ref.type === "references_api") stats.references_api++;
|
|
6674
8032
|
}
|
|
6675
8033
|
}
|
|
6676
|
-
const srcDir = (0,
|
|
6677
|
-
if ((0,
|
|
8034
|
+
const srcDir = (0, import_node_path26.join)(rootDir, "src");
|
|
8035
|
+
if ((0, import_node_fs23.existsSync)(srcDir)) {
|
|
6678
8036
|
const scanDir = (dir) => {
|
|
6679
|
-
if (!(0,
|
|
6680
|
-
for (const entry of (0,
|
|
8037
|
+
if (!(0, import_node_fs23.existsSync)(dir)) return;
|
|
8038
|
+
for (const entry of (0, import_node_fs23.readdirSync)(dir, { withFileTypes: true })) {
|
|
6681
8039
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
6682
|
-
const full = (0,
|
|
8040
|
+
const full = (0, import_node_path26.join)(dir, entry.name);
|
|
6683
8041
|
if (entry.isDirectory()) {
|
|
6684
8042
|
scanDir(full);
|
|
6685
8043
|
continue;
|
|
6686
8044
|
}
|
|
6687
|
-
if (![".ts", ".tsx"].includes((0,
|
|
8045
|
+
if (![".ts", ".tsx"].includes((0, import_node_path26.extname)(entry.name))) continue;
|
|
6688
8046
|
try {
|
|
6689
|
-
const content = (0,
|
|
8047
|
+
const content = (0, import_node_fs23.readFileSync)(full, "utf-8");
|
|
6690
8048
|
const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
|
|
6691
8049
|
if (matches) stats.annotations += matches.length;
|
|
6692
8050
|
} catch {
|
|
@@ -6701,6 +8059,12 @@ function handleDetectProjectStack() {
|
|
|
6701
8059
|
const languages = detectLanguages(rootDir, supportedLanguages);
|
|
6702
8060
|
const unsupported = languages.filter((l) => !l.supported);
|
|
6703
8061
|
const unsupportedHint = unsupported.length > 0 ? unsupported.map((l) => `${l.id} (${l.fileCount} files)`).join(", ") + " \u2014 detected but not yet supported" : null;
|
|
8062
|
+
const projects = listProjects(rootDir).map((p) => ({
|
|
8063
|
+
name: p.name,
|
|
8064
|
+
root: p.root,
|
|
8065
|
+
absolute_root: p.absoluteRoot,
|
|
8066
|
+
has_graph: (0, import_node_fs23.existsSync)((0, import_node_path26.join)(p.absoluteRoot, ".launchsecure", "graphs"))
|
|
8067
|
+
}));
|
|
6704
8068
|
return okJson({
|
|
6705
8069
|
languages,
|
|
6706
8070
|
parsers: parserResults,
|
|
@@ -6723,7 +8087,11 @@ function handleDetectProjectStack() {
|
|
|
6723
8087
|
stats,
|
|
6724
8088
|
...unsupportedHint ? { unsupported_hint: unsupportedHint } : {},
|
|
6725
8089
|
current_config: Object.keys(config).length > 0 ? config : null,
|
|
6726
|
-
config_path: ".launchchart.json"
|
|
8090
|
+
config_path: ".launchchart.json",
|
|
8091
|
+
...projects.length > 0 && {
|
|
8092
|
+
projects,
|
|
8093
|
+
projects_hint: 'Pass `project: "<name>"` (or its root path) to other tools to target a sub-project. Omitting `project` targets the monorepo root. `generate_graph` with no `project` regenerates ALL configured projects.'
|
|
8094
|
+
}
|
|
6727
8095
|
});
|
|
6728
8096
|
}
|
|
6729
8097
|
function send(msg) {
|
|
@@ -6773,6 +8141,10 @@ async function handleMessage(msg) {
|
|
|
6773
8141
|
respond(id ?? null, handleInspectNode(args));
|
|
6774
8142
|
return;
|
|
6775
8143
|
}
|
|
8144
|
+
if (toolName === "effects_index") {
|
|
8145
|
+
respond(id ?? null, handleEffectsIndex(args));
|
|
8146
|
+
return;
|
|
8147
|
+
}
|
|
6776
8148
|
if (toolName === "chart_server_status") {
|
|
6777
8149
|
respond(id ?? null, handleChartServerStatus());
|
|
6778
8150
|
return;
|
|
@@ -6816,6 +8188,9 @@ async function handleMessage(msg) {
|
|
|
6816
8188
|
respondError(id, -32601, `Method not found: ${method}`);
|
|
6817
8189
|
}
|
|
6818
8190
|
}
|
|
8191
|
+
function getWatcherHandle() {
|
|
8192
|
+
return watcherHandle;
|
|
8193
|
+
}
|
|
6819
8194
|
function startGraphMcpServer() {
|
|
6820
8195
|
process.stdin.setEncoding("utf-8");
|
|
6821
8196
|
let buffer = "";
|
|
@@ -6835,17 +8210,39 @@ function startGraphMcpServer() {
|
|
|
6835
8210
|
}
|
|
6836
8211
|
});
|
|
6837
8212
|
process.stdin.on("end", () => {
|
|
8213
|
+
watcherHandle?.stop();
|
|
6838
8214
|
process.exit(0);
|
|
6839
8215
|
});
|
|
8216
|
+
if (process.env.LAUNCH_CHART_WATCH !== "0") {
|
|
8217
|
+
try {
|
|
8218
|
+
const { startGraphWatcher: startGraphWatcher2 } = (init_watcher(), __toCommonJS(watcher_exports));
|
|
8219
|
+
watcherHandle = startGraphWatcher2(process.cwd(), {
|
|
8220
|
+
onRegen: ({ changedFiles, durationMs }) => {
|
|
8221
|
+
const sample = changedFiles.slice(0, 3).join(", ") + (changedFiles.length > 3 ? `, +${changedFiles.length - 3} more` : "");
|
|
8222
|
+
process.stderr.write(`[launchsecure-graph] regen ${durationMs}ms (${sample})
|
|
8223
|
+
`);
|
|
8224
|
+
},
|
|
8225
|
+
onError: (err2) => {
|
|
8226
|
+
process.stderr.write(`[launchsecure-graph] watcher error: ${err2.message}
|
|
8227
|
+
`);
|
|
8228
|
+
}
|
|
8229
|
+
});
|
|
8230
|
+
process.stderr.write(`[launchsecure-graph] watcher started (LAUNCH_CHART_WATCH=0 to disable)
|
|
8231
|
+
`);
|
|
8232
|
+
} catch (err2) {
|
|
8233
|
+
process.stderr.write(`[launchsecure-graph] watcher failed to start: ${err2}
|
|
8234
|
+
`);
|
|
8235
|
+
}
|
|
8236
|
+
}
|
|
6840
8237
|
process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
|
|
6841
8238
|
`);
|
|
6842
8239
|
}
|
|
6843
|
-
var
|
|
8240
|
+
var import_node_fs23, import_node_path26, import_node_child_process2, import_node_os2, SERVER_INFO, TOOLS, COMPACT_SCHEMA, COMPACT_NODE_KNOWN_KEYS, DEEP_FIELDS, EST_CHARS_PER_NODE_FULL, EST_CHARS_PER_NODE_MIN, EST_CHARS_PER_EDGE, DEFAULT_EST_NODE_FULL, DEFAULT_EST_NODE_MIN, DEFAULT_EST_EDGE, NEIGHBORHOOD_BUDGET_CHARS, MAX_FILTER_EDGES, BATCH_BUDGET_CHARS, watcherHandle;
|
|
6844
8241
|
var init_graph_mcp = __esm({
|
|
6845
8242
|
"src/server/graph-mcp.ts"() {
|
|
6846
8243
|
"use strict";
|
|
6847
|
-
|
|
6848
|
-
|
|
8244
|
+
import_node_fs23 = require("node:fs");
|
|
8245
|
+
import_node_path26 = require("node:path");
|
|
6849
8246
|
import_node_child_process2 = require("node:child_process");
|
|
6850
8247
|
import_node_os2 = require("node:os");
|
|
6851
8248
|
init_graph();
|
|
@@ -6854,6 +8251,7 @@ var init_graph_mcp = __esm({
|
|
|
6854
8251
|
init_parser_registry();
|
|
6855
8252
|
init_language_detection();
|
|
6856
8253
|
init_audit_core();
|
|
8254
|
+
init_projects();
|
|
6857
8255
|
SERVER_INFO = {
|
|
6858
8256
|
name: "launchsecure-graph",
|
|
6859
8257
|
version: "0.0.1"
|
|
@@ -6861,20 +8259,24 @@ var init_graph_mcp = __esm({
|
|
|
6861
8259
|
TOOLS = [
|
|
6862
8260
|
{
|
|
6863
8261
|
name: "generate_graph",
|
|
6864
|
-
description: "Regenerate the structural project graph by scanning source code in the current working directory. Auto-detects project languages and frameworks, then parses into layers (e.g. ui, api, db) based on content classification. Writes JSON to .launchsecure/graphs/{layer}.json and returns node/edge counts. Run this when project structure has changed (new files, moved components, schema updates). Fast: typically <1s. After generation, use read_graph with filters to query.",
|
|
8262
|
+
description: "Regenerate the structural project graph by scanning source code in the current working directory. Auto-detects project languages and frameworks, then parses into layers (e.g. ui, api, db) based on content classification. Writes JSON to .launchsecure/graphs/{layer}.json and returns node/edge counts. Run this when project structure has changed (new files, moved components, schema updates). Fast: typically <1s. After generation, use read_graph with filters to query.\n\nMONOREPO BEHAVIOR: when .launchchart.json declares projects[] and no `project` arg is given, regenerates EVERY configured project (each writes to its own <root>/.launchsecure/graphs/). Pass `project` to limit regeneration to a single sub-project.",
|
|
6865
8263
|
inputSchema: {
|
|
6866
8264
|
type: "object",
|
|
6867
8265
|
properties: {
|
|
6868
8266
|
layer: {
|
|
6869
8267
|
type: "string",
|
|
6870
8268
|
description: "Specific layer to regenerate (e.g. 'ui', 'api', 'db'). Omit to regenerate all detectable layers. Run detect_project_stack to see available layers."
|
|
8269
|
+
},
|
|
8270
|
+
project: {
|
|
8271
|
+
type: "string",
|
|
8272
|
+
description: PROJECT_PARAM_DESCRIPTION + " Special: omit to regenerate ALL configured projects."
|
|
6871
8273
|
}
|
|
6872
8274
|
}
|
|
6873
8275
|
}
|
|
6874
8276
|
},
|
|
6875
8277
|
{
|
|
6876
8278
|
name: "read_graph",
|
|
6877
|
-
description: 'Query the structural project graph \u2014 use INSTEAD of Glob and Grep for locating files, understanding structure, and navigating the codebase. Faster and more accurate than file-system search because it returns typed nodes with metadata and relationships. \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module", "which endpoints touch the User table", "what auth strategy does this endpoint use". \n\nDO NOT USE FOR: understanding what\'s INSIDE a component (use inspect_node for elements, conditions, state, variables, responses), reading actual source code (use Read). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module tag (computed from directory structure, e.g. "auth", "admin", "settings")\n- node_id: return this node + its neighborhood (incoming+outgoing edges within `hops`)\n- hops: neighborhood radius when node_id is set (default 1)\n- minimal: return only id/type/name/module/route per node (skip heavy fields like columns, exports)\n- include_edges: return the actual edge list. Default: TRUE for neighborhood queries (node_id), FALSE for filter queries (search/type/module). Filter responses always include `edge_count`; only pass include_edges:true when you actually need to inspect individual edges (e.g. "which components render X"). This default cuts typical filter responses in half.\n\nBATCH MODE: pass `queries` (array of query objects) to run multiple independent queries in a single call. Each query object uses the same params (layer/search/type/module/node_id/hops/minimal). Returns { batch: true, count, results: [{index, query, result}, ...] }. Use this when you need multiple graph views up-front (e.g. scoping a feature across ui+api+db layers) to save round-trips. When batch mode is used, top-level params are ignored.\n\nReturns: filtered nodes + edges between them. If no filter given, returns per-layer counts and type breakdown only.\n\nWIRE FORMAT (compact): responses that include nodes/edges use short keys and edge-by-index refs to cut payload ~40-60%. Every such response carries a `_schema` legend. Quick reference:\n nodes[]: { i: id, t: type, n: name, m: module, r: route, mt: methods, x: exports, c: columns }\n edges[]: { s: source_node_index, d: target_node_index, t: type, l: label }\nedges.s / edges.d are 0-based indices into THIS response\'s nodes array. If a referenced node is not in the response (boundary case), s/d may instead contain the full node id string \u2014 always check the type.\n\nPAGINATION (filter queries):\n- Use `offset` and `limit` to paginate through large result sets.\n- Response includes: `total` (matched), `returned` (in this page), `has_more`, `next_offset`.\n- If `has_more: true`, call again with `offset: next_offset` to get the next page.\n\nBUDGET GUARDS:\n- Neighborhood queries stop expanding when the projected response exceeds budget. The response then contains `budget_exceeded: true` plus `hops_traversed < hops_requested`. When this happens, drill into a specific neighbor with another node_id call rather than retrying with larger hops \u2014 it will just truncate again.\n- Batch mode caps total response size. Once the budget is hit, later queries return `{skipped: true, reason: "batch_budget_exhausted"}` and you must re-run them individually.',
|
|
8279
|
+
description: 'Query the structural project graph \u2014 use INSTEAD of Glob and Grep for locating files, understanding structure, and navigating the codebase. Faster and more accurate than file-system search because it returns typed nodes with metadata and relationships. \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module", "which endpoints touch the User table", "what auth strategy does this endpoint use". \n\nDO NOT USE FOR: understanding what\'s INSIDE a component (use inspect_node for elements, conditions, state, variables, responses), reading actual source code (use Read). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module tag (computed from directory structure, e.g. "auth", "admin", "settings")\n- node_id: return this node + its neighborhood (incoming+outgoing edges within `hops`)\n- hops: neighborhood radius when node_id is set (default 1)\n- minimal: return only id/type/name/module/route per node (skip heavy fields like columns, exports)\n- include_edges: return the actual edge list. Default: TRUE for neighborhood queries (node_id), FALSE for filter queries (search/type/module). Filter responses always include `edge_count`; only pass include_edges:true when you actually need to inspect individual edges (e.g. "which components render X"). This default cuts typical filter responses in half.\n\nBATCH MODE: pass `queries` (array of query objects) to run multiple independent queries in a single call. Each query object uses the same params (layer/search/type/module/node_id/hops/minimal). Returns { batch: true, count, results: [{index, query, result}, ...] }. Use this when you need multiple graph views up-front (e.g. scoping a feature across ui+api+db layers) to save round-trips. When batch mode is used, top-level params are ignored.\n\nReturns: filtered nodes + edges between them. If no filter given, returns per-layer counts and type breakdown only.\n\nWIRE FORMAT (compact): responses that include nodes/edges use short keys and edge-by-index refs to cut payload ~40-60%. Every such response carries a `_schema` legend. Quick reference:\n nodes[]: { i: id, t: type, n: name, m: module, r: route, mt: methods, x: exports, c: columns }\n edges[]: { s: source_node_index, d: target_node_index, t: type, l: label }\nedges.s / edges.d are 0-based indices into THIS response\'s nodes array. If a referenced node is not in the response (boundary case), s/d may instead contain the full node id string \u2014 always check the type.\n\nPAGINATION (filter queries):\n- Use `offset` and `limit` to paginate through large result sets.\n- Response includes: `total` (matched), `returned` (in this page), `has_more`, `next_offset`.\n- If `has_more: true`, call again with `offset: next_offset` to get the next page.\n\nBUDGET GUARDS:\n- Neighborhood queries stop expanding when the projected response exceeds budget. The response then contains `budget_exceeded: true` plus `hops_traversed < hops_requested`. When this happens, drill into a specific neighbor with another node_id call rather than retrying with larger hops \u2014 it will just truncate again.\n- Batch mode caps total response size. Once the budget is hit, later queries return `{skipped: true, reason: "batch_budget_exhausted"}` and you must re-run them individually.\n\nMONOREPOS: pass `project: "<name>"` to query a sub-project graph (defined in .launchchart.json projects[]). Omitting `project` targets the monorepo root. In batch mode the top-level `project` is inherited by sub-queries that do not set their own. Run detect_project_stack to list configured projects.',
|
|
6878
8280
|
inputSchema: {
|
|
6879
8281
|
type: "object",
|
|
6880
8282
|
properties: {
|
|
@@ -6928,7 +8330,7 @@ var init_graph_mcp = __esm({
|
|
|
6928
8330
|
},
|
|
6929
8331
|
queries: {
|
|
6930
8332
|
type: "array",
|
|
6931
|
-
description: "Batch mode \u2014 array of query objects to run in a single call. Each uses the same param schema. When set, top-level params are ignored. Subject to an aggregate size budget \u2014 later queries may return a skipped stub.",
|
|
8333
|
+
description: "Batch mode \u2014 array of query objects to run in a single call. Each uses the same param schema (including `project`). When set, top-level params are ignored. Subject to an aggregate size budget \u2014 later queries may return a skipped stub.",
|
|
6932
8334
|
items: {
|
|
6933
8335
|
type: "object",
|
|
6934
8336
|
properties: {
|
|
@@ -6939,9 +8341,14 @@ var init_graph_mcp = __esm({
|
|
|
6939
8341
|
node_id: { type: "string" },
|
|
6940
8342
|
hops: { type: "number" },
|
|
6941
8343
|
minimal: { type: "boolean" },
|
|
6942
|
-
include_edges: { type: "boolean" }
|
|
8344
|
+
include_edges: { type: "boolean" },
|
|
8345
|
+
project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
|
|
6943
8346
|
}
|
|
6944
8347
|
}
|
|
8348
|
+
},
|
|
8349
|
+
project: {
|
|
8350
|
+
type: "string",
|
|
8351
|
+
description: PROJECT_PARAM_DESCRIPTION
|
|
6945
8352
|
}
|
|
6946
8353
|
}
|
|
6947
8354
|
}
|
|
@@ -6988,16 +8395,17 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
|
|
|
6988
8395
|
case_insensitive: { type: "boolean", description: "Case-insensitive regex. Default false." },
|
|
6989
8396
|
context: { type: "number", description: "Context lines around each match. Default 2." },
|
|
6990
8397
|
max_matches: { type: "number", description: "Max matches to return total. Default 50." },
|
|
6991
|
-
max_files: { type: "number", description: "Max files to search. Default 50." }
|
|
8398
|
+
max_files: { type: "number", description: "Max files to search. Default 50." },
|
|
8399
|
+
project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
|
|
6992
8400
|
},
|
|
6993
8401
|
required: ["layer", "pattern"]
|
|
6994
8402
|
}
|
|
6995
8403
|
},
|
|
6996
8404
|
{
|
|
6997
8405
|
name: "inspect_node",
|
|
6998
|
-
description: `Get deep AST data for specific graph nodes \u2014 what's INSIDE a component or endpoint. Returns elements (JSX), state hooks, conditions, variables, responses,
|
|
8406
|
+
description: `Get deep AST data for specific graph nodes \u2014 what's INSIDE a component or endpoint. Returns elements (JSX), state hooks, conditions, variables, responses, request params, and effects. Use INSTEAD of Grep/Read when you need to understand component internals without reading source.
|
|
6999
8407
|
|
|
7000
|
-
USE THIS FOR: "what elements does LoginPage have?", "what conditions does the login endpoint check?", "what state does SettingsPage manage?", "what responses can this endpoint return?", "what validation does this API do?", "what props does this component accept?", "which endpoints check for isAdmin?", "find all conditions mentioning rateLimit"
|
|
8408
|
+
USE THIS FOR: "what elements does LoginPage have?", "what conditions does the login endpoint check?", "what state does SettingsPage manage?", "what responses can this endpoint return?", "what validation does this API do?", "what props does this component accept?", "which endpoints check for isAdmin?", "find all conditions mentioning rateLimit", "is this function safe to instantiate twice?" (check effects.subscribes / .timers / .dom_writes / .persists / .globals)
|
|
7001
8409
|
|
|
7002
8410
|
DO NOT USE FOR: structural queries (use read_graph), content search (use grep_nodes).
|
|
7003
8411
|
|
|
@@ -7020,7 +8428,7 @@ Returns deep fields only \u2014 not structural metadata (use read_graph for that
|
|
|
7020
8428
|
fields: {
|
|
7021
8429
|
type: "array",
|
|
7022
8430
|
items: { type: "string" },
|
|
7023
|
-
description:
|
|
8431
|
+
description: 'Specific deep fields to return. Options: elements, stateVars, conditions, variables, responses, params, effects. Omit for all. "effects" returns the file-level side-effect summary (calls, dom_writes, subscribes, timers, persists, fetches, globals); per-arrow-function effects are also attached to each entry in "variables".'
|
|
7024
8432
|
},
|
|
7025
8433
|
filter: {
|
|
7026
8434
|
type: "string",
|
|
@@ -7029,7 +8437,8 @@ Returns deep fields only \u2014 not structural metadata (use read_graph for that
|
|
|
7029
8437
|
case_insensitive: {
|
|
7030
8438
|
type: "boolean",
|
|
7031
8439
|
description: "Case-insensitive filter matching. Default true."
|
|
7032
|
-
}
|
|
8440
|
+
},
|
|
8441
|
+
project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
|
|
7033
8442
|
},
|
|
7034
8443
|
required: ["layer"]
|
|
7035
8444
|
}
|
|
@@ -7065,6 +8474,25 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
7065
8474
|
properties: {}
|
|
7066
8475
|
}
|
|
7067
8476
|
},
|
|
8477
|
+
{
|
|
8478
|
+
name: "effects_index",
|
|
8479
|
+
description: 'Cross-layer inverted index over per-node side effects. Answers "who else touches X?" without re-walking every node. Built automatically when generate_graph runs (or whenever the watcher regenerates). \n\nUSE THIS FOR: "is mountFoo safe to instantiate twice?" (kind="singleton_risks"), "who writes DOM id moon-shadow-blur?" (kind="dom_ids", key="moon-shadow-blur"), "which files attach a window keydown listener?" (kind="window_events", key="window:keydown"), "who else writes localStorage key panchang.settings.v1?" (kind="storage_keys", key="..."), "any DOM-id collisions?" (kind="collisions"). \n\nReturns: { kind, results } where results is a {key: [nodeIds]} map for the chosen kind, or a list of multi-writer collisions when kind="collisions", or a flat node-id list when kind="singleton_risks".',
|
|
8480
|
+
inputSchema: {
|
|
8481
|
+
type: "object",
|
|
8482
|
+
properties: {
|
|
8483
|
+
kind: {
|
|
8484
|
+
type: "string",
|
|
8485
|
+
enum: ["dom_ids", "window_events", "storage_keys", "fetch_urls", "timers", "singleton_risks", "collisions"],
|
|
8486
|
+
description: "Which inverted index to query. Default: collisions (most actionable signal)."
|
|
8487
|
+
},
|
|
8488
|
+
key: {
|
|
8489
|
+
type: "string",
|
|
8490
|
+
description: 'Optional specific key to look up within the chosen kind (e.g. "moon-shadow-blur"). When omitted, returns the full {key:nodes} map for the kind.'
|
|
8491
|
+
},
|
|
8492
|
+
project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
|
|
8493
|
+
}
|
|
8494
|
+
}
|
|
8495
|
+
},
|
|
7068
8496
|
{
|
|
7069
8497
|
name: "detect_project_stack",
|
|
7070
8498
|
description: "Detect project languages, frameworks, available parsers, and recommend parser configuration. Scans the project to identify all languages present (TypeScript, Python, Go, etc.) and reports which are supported by registered parsers vs detected-but-unsupported. Also detects frameworks (Next.js, Prisma, React, etc.), provides cross-layer detection stats (fetch calls, @api annotations, URL literals), and returns available graph layers. \n\nUse this when setting up launch-chart for a new project, reviewing parser configuration, or checking what languages are in the project.",
|
|
@@ -7090,7 +8518,8 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
7090
8518
|
value: {
|
|
7091
8519
|
type: "string",
|
|
7092
8520
|
description: 'Tag value (e.g. "auth", "alice", "true").'
|
|
7093
|
-
}
|
|
8521
|
+
},
|
|
8522
|
+
project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
|
|
7094
8523
|
},
|
|
7095
8524
|
required: ["node_id", "key", "value"]
|
|
7096
8525
|
}
|
|
@@ -7108,7 +8537,8 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
7108
8537
|
key: {
|
|
7109
8538
|
type: "string",
|
|
7110
8539
|
description: "Tag key to remove."
|
|
7111
|
-
}
|
|
8540
|
+
},
|
|
8541
|
+
project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
|
|
7112
8542
|
},
|
|
7113
8543
|
required: ["node_id", "key"]
|
|
7114
8544
|
}
|
|
@@ -7126,7 +8556,8 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
7126
8556
|
check: {
|
|
7127
8557
|
type: "string",
|
|
7128
8558
|
description: "Specific check to run (e.g. 'schema_drift', 'unprotected_routes'). Omit to run all checks for the layer."
|
|
7129
|
-
}
|
|
8559
|
+
},
|
|
8560
|
+
project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
|
|
7130
8561
|
},
|
|
7131
8562
|
required: ["layer"]
|
|
7132
8563
|
}
|
|
@@ -7161,7 +8592,8 @@ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 retu
|
|
|
7161
8592
|
type: "string",
|
|
7162
8593
|
enum: ["reverse", "both"],
|
|
7163
8594
|
description: "'reverse' (default) = only what depends on this node. 'both' = full neighborhood."
|
|
7164
|
-
}
|
|
8595
|
+
},
|
|
8596
|
+
project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
|
|
7165
8597
|
},
|
|
7166
8598
|
required: ["node_id"]
|
|
7167
8599
|
}
|
|
@@ -7183,9 +8615,10 @@ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 retu
|
|
|
7183
8615
|
s: "source_node_index",
|
|
7184
8616
|
d: "target_node_index",
|
|
7185
8617
|
t: "type",
|
|
7186
|
-
l: "label"
|
|
8618
|
+
l: "label",
|
|
8619
|
+
tl: "target_layer (only set on cross-layer edges, e.g. ui\u2192api calls_api)"
|
|
7187
8620
|
},
|
|
7188
|
-
note: "edges.s/d are 0-based indices into this response's nodes array. If a referenced node is outside the response (boundary case), s/d may contain the full node id string instead of an index."
|
|
8621
|
+
note: "edges.s/d are 0-based indices into this response's nodes array. If a referenced node is outside the response (boundary case \u2014 common for cross-layer edges where the target lives in a different layer's graph), s/d may contain the full node id string instead of an index. Use tl to identify which layer the target belongs to."
|
|
7189
8622
|
};
|
|
7190
8623
|
COMPACT_NODE_KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
7191
8624
|
"id",
|
|
@@ -7204,7 +8637,8 @@ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 retu
|
|
|
7204
8637
|
"conditions",
|
|
7205
8638
|
"variables",
|
|
7206
8639
|
"responses",
|
|
7207
|
-
"params"
|
|
8640
|
+
"params",
|
|
8641
|
+
"effects"
|
|
7208
8642
|
]);
|
|
7209
8643
|
EST_CHARS_PER_NODE_FULL = {
|
|
7210
8644
|
ui: 300,
|
|
@@ -7225,16 +8659,18 @@ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 retu
|
|
|
7225
8659
|
DEFAULT_EST_NODE_MIN = 150;
|
|
7226
8660
|
DEFAULT_EST_EDGE = 65;
|
|
7227
8661
|
NEIGHBORHOOD_BUDGET_CHARS = 55e3;
|
|
8662
|
+
MAX_FILTER_EDGES = 200;
|
|
7228
8663
|
BATCH_BUDGET_CHARS = 6e4;
|
|
8664
|
+
watcherHandle = null;
|
|
7229
8665
|
}
|
|
7230
8666
|
});
|
|
7231
8667
|
|
|
7232
8668
|
// src/server/graph-mcp-entry.ts
|
|
7233
8669
|
var import_node_child_process3 = require("node:child_process");
|
|
7234
|
-
var
|
|
7235
|
-
var
|
|
8670
|
+
var import_node_fs24 = require("node:fs");
|
|
8671
|
+
var import_node_path27 = __toESM(require("node:path"));
|
|
7236
8672
|
var import_node_os3 = require("node:os");
|
|
7237
|
-
var
|
|
8673
|
+
var import_node_fs25 = require("node:fs");
|
|
7238
8674
|
init_lockfile();
|
|
7239
8675
|
function logStderr(msg) {
|
|
7240
8676
|
process.stderr.write(`[launch-chart] ${msg}
|
|
@@ -7250,11 +8686,11 @@ function maybeAutoServe() {
|
|
|
7250
8686
|
return;
|
|
7251
8687
|
}
|
|
7252
8688
|
try {
|
|
7253
|
-
const logDir =
|
|
7254
|
-
(0,
|
|
7255
|
-
const logPath =
|
|
7256
|
-
const out = (0,
|
|
7257
|
-
const err2 = (0,
|
|
8689
|
+
const logDir = import_node_path27.default.join((0, import_node_os3.homedir)(), ".launchsecure");
|
|
8690
|
+
(0, import_node_fs25.mkdirSync)(logDir, { recursive: true });
|
|
8691
|
+
const logPath = import_node_path27.default.join(logDir, "launch-chart.log");
|
|
8692
|
+
const out = (0, import_node_fs24.openSync)(logPath, "a");
|
|
8693
|
+
const err2 = (0, import_node_fs24.openSync)(logPath, "a");
|
|
7258
8694
|
const entryPath = process.argv[1];
|
|
7259
8695
|
const child = (0, import_node_child_process3.spawn)(process.execPath, [entryPath, "serve"], {
|
|
7260
8696
|
detached: true,
|
|
@@ -7276,6 +8712,12 @@ async function main() {
|
|
|
7276
8712
|
runServeCli2(argv.slice(1));
|
|
7277
8713
|
return;
|
|
7278
8714
|
}
|
|
8715
|
+
if (subcommand === "generate" || subcommand === "read") {
|
|
8716
|
+
const { handleGraphCommand: handleGraphCommand2 } = await Promise.resolve().then(() => (init_graph_cli(), graph_cli_exports));
|
|
8717
|
+
const mapped = subcommand === "generate" ? "graph:generate" : "graph:read";
|
|
8718
|
+
await handleGraphCommand2(mapped, argv.slice(1));
|
|
8719
|
+
process.exit(0);
|
|
8720
|
+
}
|
|
7279
8721
|
maybeAutoServe();
|
|
7280
8722
|
const { startGraphMcpServer: startGraphMcpServer2 } = await Promise.resolve().then(() => (init_graph_mcp(), graph_mcp_exports));
|
|
7281
8723
|
startGraphMcpServer2();
|