@launchsecure/launch-kit 0.0.24 → 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 +1336 -141
- package/dist/server/cli.js +28423 -6671
- 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 +1987 -224
- 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
|
@@ -175,6 +175,7 @@ function walkWithIgnore(dir, exts, opts = {}) {
|
|
|
175
175
|
const skip = opts.extraIgnore ? /* @__PURE__ */ new Set([...DEFAULT_IGNORE_DIRS, ...opts.extraIgnore]) : DEFAULT_IGNORE_DIRS;
|
|
176
176
|
for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
|
|
177
177
|
if (entry.isDirectory()) {
|
|
178
|
+
if (entry.name.startsWith(".")) continue;
|
|
178
179
|
if (skip.has(entry.name)) continue;
|
|
179
180
|
results.push(...walkWithIgnore((0, import_node_path3.join)(dir, entry.name), exts, opts));
|
|
180
181
|
} else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
|
|
@@ -191,15 +192,9 @@ var init_walk = __esm({
|
|
|
191
192
|
import_node_path3 = require("node:path");
|
|
192
193
|
DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
193
194
|
"node_modules",
|
|
194
|
-
".git",
|
|
195
|
-
".next",
|
|
196
|
-
".launchsecure",
|
|
197
|
-
".claude",
|
|
198
195
|
"dist",
|
|
199
196
|
"build",
|
|
200
197
|
"out",
|
|
201
|
-
".turbo",
|
|
202
|
-
".vercel",
|
|
203
198
|
"coverage"
|
|
204
199
|
]);
|
|
205
200
|
}
|
|
@@ -403,14 +398,20 @@ var init_resolve_paths = __esm({
|
|
|
403
398
|
// src/server/graph/core/ts-extractor.ts
|
|
404
399
|
var ts_extractor_exports = {};
|
|
405
400
|
__export(ts_extractor_exports, {
|
|
401
|
+
ParseCascadeError: () => ParseCascadeError,
|
|
406
402
|
classifyFile: () => classifyFile,
|
|
403
|
+
classifyRouteAgainstMiddleware: () => classifyRouteAgainstMiddleware,
|
|
407
404
|
createQuery: () => createQuery,
|
|
408
405
|
extractAuthWrappersTS: () => extractAuthWrappersTS,
|
|
409
406
|
extractDbCallsTS: () => extractDbCallsTS,
|
|
410
407
|
extractDeep: () => extractDeep,
|
|
408
|
+
extractEffects: () => extractEffects,
|
|
409
|
+
extractMiddlewareAuthTS: () => extractMiddlewareAuthTS,
|
|
411
410
|
initTreeSitter: () => initTreeSitter,
|
|
411
|
+
middlewarePatternToRegex: () => middlewarePatternToRegex,
|
|
412
412
|
parseCodeTS: () => parseCodeTS,
|
|
413
413
|
parseFileTS: () => parseFileTS,
|
|
414
|
+
parseSource: () => parseSource,
|
|
414
415
|
setExtractorConfig: () => setExtractorConfig
|
|
415
416
|
});
|
|
416
417
|
async function initTreeSitter() {
|
|
@@ -444,8 +445,38 @@ function getQuery(name) {
|
|
|
444
445
|
}
|
|
445
446
|
function parseSource(absPath) {
|
|
446
447
|
ensureInit();
|
|
447
|
-
|
|
448
|
-
|
|
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
|
+
}
|
|
449
480
|
}
|
|
450
481
|
function parseCodeTS(code) {
|
|
451
482
|
ensureInit();
|
|
@@ -485,8 +516,20 @@ function childrenOfType(node, type) {
|
|
|
485
516
|
function childOfType(node, type) {
|
|
486
517
|
return node.children.find((n) => n.type === type);
|
|
487
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
|
+
}
|
|
488
530
|
function parseFileTS(absPath) {
|
|
489
531
|
const tree = parseSource(absPath);
|
|
532
|
+
if (!tree) return emptyParsedFile(absPath);
|
|
490
533
|
const root = tree.rootNode;
|
|
491
534
|
const imports = [];
|
|
492
535
|
const importStatements = childrenOfType(root, "import_statement");
|
|
@@ -681,6 +724,7 @@ function parseFileTS(absPath) {
|
|
|
681
724
|
}
|
|
682
725
|
function extractDbCallsTS(absPath) {
|
|
683
726
|
const tree = parseSource(absPath);
|
|
727
|
+
if (!tree) return [];
|
|
684
728
|
const root = tree.rootNode;
|
|
685
729
|
const dbQuery = getQuery("db-calls");
|
|
686
730
|
const matches = dbQuery.matches(root);
|
|
@@ -741,7 +785,9 @@ function classifyFile(absPath) {
|
|
|
741
785
|
const fileName = require("path").basename(absPath);
|
|
742
786
|
if (fileName.includes(".test.") || fileName.includes(".spec.") || fileName.includes("__test")) return "test";
|
|
743
787
|
if (fileName.includes(".stories.")) return "story";
|
|
788
|
+
if (fileName === "middleware.ts" || fileName === "middleware.tsx") return "middleware";
|
|
744
789
|
const tree = parseSource(absPath);
|
|
790
|
+
if (!tree) return "util";
|
|
745
791
|
const root = tree.rootNode;
|
|
746
792
|
const classifyQuery = getQuery("classify");
|
|
747
793
|
const captures = classifyQuery.captures(root);
|
|
@@ -760,6 +806,7 @@ function classifyFile(absPath) {
|
|
|
760
806
|
}
|
|
761
807
|
function extractAuthWrappersTS(absPath) {
|
|
762
808
|
const tree = parseSource(absPath);
|
|
809
|
+
if (!tree) return /* @__PURE__ */ new Set();
|
|
763
810
|
const root = tree.rootNode;
|
|
764
811
|
const wrapperQuery = getQuery("wrappers");
|
|
765
812
|
const matches = wrapperQuery.matches(root);
|
|
@@ -802,11 +849,354 @@ function extractAuthWrappersTS(absPath) {
|
|
|
802
849
|
}
|
|
803
850
|
return wrappers;
|
|
804
851
|
}
|
|
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
|
+
}
|
|
856
|
+
for (const re of EXEMPT_NAME_PATTERNS) {
|
|
857
|
+
if (re.test(name)) return { intent: "exempt", hint: `name "${name}" matches /${re.source}/` };
|
|
858
|
+
}
|
|
859
|
+
for (const re of PROTECT_NAME_PATTERNS) {
|
|
860
|
+
if (re.test(name)) return { intent: "protect", hint: `name "${name}" matches /${re.source}/` };
|
|
861
|
+
}
|
|
862
|
+
return { intent: "ambiguous", hint: `name "${name}" has no exempt/protect signal` };
|
|
863
|
+
}
|
|
864
|
+
function looksLikeRoutePattern(s) {
|
|
865
|
+
return s.startsWith("/") && !s.startsWith("//");
|
|
866
|
+
}
|
|
867
|
+
function collectStringsFromArray(arrNode) {
|
|
868
|
+
const out = [];
|
|
869
|
+
for (const child of arrNode.children) {
|
|
870
|
+
if (child.type !== "string") continue;
|
|
871
|
+
const frag = childOfType(child, "string_fragment");
|
|
872
|
+
if (frag) out.push(frag.text);
|
|
873
|
+
}
|
|
874
|
+
return out;
|
|
875
|
+
}
|
|
876
|
+
function findArrayInValue(valueNode) {
|
|
877
|
+
if (!valueNode) return null;
|
|
878
|
+
if (valueNode.type === "array") return valueNode;
|
|
879
|
+
if (valueNode.type === "call_expression") {
|
|
880
|
+
const args = childOfType(valueNode, "arguments");
|
|
881
|
+
if (!args) return null;
|
|
882
|
+
for (const c of args.children) {
|
|
883
|
+
if (c.type === "array") return c;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
function extractMatcherFromDeclarator(decl) {
|
|
889
|
+
const nameNode = childOfType(decl, "identifier");
|
|
890
|
+
if (!nameNode) return null;
|
|
891
|
+
let valueNode;
|
|
892
|
+
for (const c of decl.children) {
|
|
893
|
+
if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
|
|
894
|
+
valueNode = c;
|
|
895
|
+
}
|
|
896
|
+
const arr = findArrayInValue(valueNode);
|
|
897
|
+
if (!arr) return null;
|
|
898
|
+
const strings = collectStringsFromArray(arr);
|
|
899
|
+
const routes = strings.filter(looksLikeRoutePattern);
|
|
900
|
+
if (routes.length === 0) return null;
|
|
901
|
+
const { intent, hint } = inferIntentFromName(nameNode.text);
|
|
902
|
+
return { name: nameNode.text, patterns: routes, intent, hint };
|
|
903
|
+
}
|
|
904
|
+
function extractMatchersFromObject(objNode) {
|
|
905
|
+
const out = [];
|
|
906
|
+
for (const pair of childrenOfType(objNode, "pair")) {
|
|
907
|
+
const key = childOfType(pair, "property_identifier");
|
|
908
|
+
if (!key) continue;
|
|
909
|
+
const arr = pair.children.find((c) => c.type === "array");
|
|
910
|
+
if (!arr) continue;
|
|
911
|
+
const routes = collectStringsFromArray(arr).filter(looksLikeRoutePattern);
|
|
912
|
+
if (routes.length === 0) continue;
|
|
913
|
+
const { intent, hint } = inferIntentFromName(key.text);
|
|
914
|
+
out.push({ name: key.text, patterns: routes, intent, hint });
|
|
915
|
+
}
|
|
916
|
+
return out;
|
|
917
|
+
}
|
|
918
|
+
function detectFallthroughProtect(root) {
|
|
919
|
+
const text = root.text;
|
|
920
|
+
const signals = [
|
|
921
|
+
/\bauth\.protect\s*\(/,
|
|
922
|
+
/\bauth\(\)\.protect\s*\(/,
|
|
923
|
+
/\bredirect\s*\(\s*['"`]\/(sign-?in|log-?in|auth)/i,
|
|
924
|
+
/\bNextResponse\.redirect\s*\(/,
|
|
925
|
+
/\bthrow\s+new\s+\w*Unauthorized/i
|
|
926
|
+
];
|
|
927
|
+
return signals.some((re) => re.test(text));
|
|
928
|
+
}
|
|
929
|
+
function extractMiddlewareAuthTS(absPath) {
|
|
930
|
+
if (!require("node:fs").existsSync(absPath)) return null;
|
|
931
|
+
const tree = parseSource(absPath);
|
|
932
|
+
if (!tree) return null;
|
|
933
|
+
const root = tree.rootNode;
|
|
934
|
+
const matchers = [];
|
|
935
|
+
for (const stmt of root.children) {
|
|
936
|
+
if (stmt.type !== "lexical_declaration" && stmt.type !== "variable_declaration") continue;
|
|
937
|
+
for (const decl of childrenOfType(stmt, "variable_declarator")) {
|
|
938
|
+
const m = extractMatcherFromDeclarator(decl);
|
|
939
|
+
if (m) matchers.push(m);
|
|
940
|
+
let valueNode;
|
|
941
|
+
for (const c of decl.children) {
|
|
942
|
+
if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
|
|
943
|
+
valueNode = c;
|
|
944
|
+
}
|
|
945
|
+
if (valueNode?.type === "object") {
|
|
946
|
+
for (const inner of extractMatchersFromObject(valueNode)) matchers.push(inner);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
for (const stmt of root.children) {
|
|
951
|
+
if (stmt.type !== "export_statement") continue;
|
|
952
|
+
const decl = childOfType(stmt, "lexical_declaration") ?? childOfType(stmt, "variable_declaration");
|
|
953
|
+
if (!decl) continue;
|
|
954
|
+
for (const d of childrenOfType(decl, "variable_declarator")) {
|
|
955
|
+
let valueNode;
|
|
956
|
+
for (const c of d.children) {
|
|
957
|
+
if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
|
|
958
|
+
valueNode = c;
|
|
959
|
+
}
|
|
960
|
+
if (valueNode?.type === "object") {
|
|
961
|
+
for (const inner of extractMatchersFromObject(valueNode)) matchers.push(inner);
|
|
962
|
+
}
|
|
963
|
+
const m = extractMatcherFromDeclarator(d);
|
|
964
|
+
if (m && !matchers.some((x) => x.name === m.name && x.patterns.join() === m.patterns.join())) {
|
|
965
|
+
matchers.push(m);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
file: absPath,
|
|
971
|
+
matchers,
|
|
972
|
+
hasFallthroughProtect: detectFallthroughProtect(root)
|
|
973
|
+
};
|
|
974
|
+
}
|
|
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
|
+
}
|
|
986
|
+
if (/\(\?\!/.test(pattern)) return null;
|
|
987
|
+
if (pattern.startsWith("(")) return null;
|
|
988
|
+
let src = "^";
|
|
989
|
+
let i = 0;
|
|
990
|
+
while (i < pattern.length) {
|
|
991
|
+
const ch = pattern[i];
|
|
992
|
+
if (ch === "(" && pattern.slice(i, i + 4) === "(.*)") {
|
|
993
|
+
src += ".*";
|
|
994
|
+
i += 4;
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
if (ch === ":") {
|
|
998
|
+
i++;
|
|
999
|
+
while (i < pattern.length && /[a-zA-Z0-9_]/.test(pattern[i])) i++;
|
|
1000
|
+
src += "[^/]+";
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
if (ch === "*") {
|
|
1004
|
+
i++;
|
|
1005
|
+
while (i < pattern.length && /[a-zA-Z0-9_]/.test(pattern[i])) i++;
|
|
1006
|
+
if (pattern[i] === "?") {
|
|
1007
|
+
i++;
|
|
1008
|
+
src += ".*";
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
src += ".+";
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
if (/[.\\+?^${}()|[\]]/.test(ch)) {
|
|
1015
|
+
src += "\\" + ch;
|
|
1016
|
+
i++;
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
src += ch;
|
|
1020
|
+
i++;
|
|
1021
|
+
}
|
|
1022
|
+
src += "$";
|
|
1023
|
+
try {
|
|
1024
|
+
return new RegExp(src);
|
|
1025
|
+
} catch {
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
function classifyRouteAgainstMiddleware(routePath, info) {
|
|
1030
|
+
for (const m of info.matchers) {
|
|
1031
|
+
if (m.intent === "ambiguous") continue;
|
|
1032
|
+
for (const pat of m.patterns) {
|
|
1033
|
+
const re = middlewarePatternToRegex(pat);
|
|
1034
|
+
if (!re) continue;
|
|
1035
|
+
if (re.test(routePath)) return { intent: m.intent, matcher: m.name, pattern: pat };
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
return null;
|
|
1039
|
+
}
|
|
805
1040
|
function trunc(s, max = 120) {
|
|
806
1041
|
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
807
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
|
+
}
|
|
808
1195
|
function extractDeep(absPath) {
|
|
809
1196
|
const tree = parseSource(absPath);
|
|
1197
|
+
if (!tree) {
|
|
1198
|
+
return { elements: [], stateVars: [], conditions: [], variables: [], responses: [], params: [] };
|
|
1199
|
+
}
|
|
810
1200
|
const root = tree.rootNode;
|
|
811
1201
|
const elements = [];
|
|
812
1202
|
const elQuery = getQuery("deep/jsx-semantic");
|
|
@@ -899,12 +1289,18 @@ function extractDeep(absPath) {
|
|
|
899
1289
|
const caps = captureMap(m);
|
|
900
1290
|
const declNode = m.captures.find((c) => c.name === "var.decl" || c.name === "var.destructured" || c.name === "var.array")?.node;
|
|
901
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;
|
|
902
1293
|
if (caps["var.name"] && caps["var.init"]) {
|
|
903
|
-
|
|
1294
|
+
const variable = {
|
|
904
1295
|
name: caps["var.name"],
|
|
905
1296
|
kind,
|
|
906
1297
|
init: trunc(caps["var.init"], 100)
|
|
907
|
-
}
|
|
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);
|
|
908
1304
|
}
|
|
909
1305
|
if (caps["var.destructured.obj"]) {
|
|
910
1306
|
variables.push({
|
|
@@ -956,9 +1352,23 @@ function extractDeep(absPath) {
|
|
|
956
1352
|
params.push({ name: caps["param.body"], source: "body" });
|
|
957
1353
|
}
|
|
958
1354
|
}
|
|
959
|
-
|
|
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
|
+
};
|
|
960
1370
|
}
|
|
961
|
-
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;
|
|
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;
|
|
962
1372
|
var init_ts_extractor = __esm({
|
|
963
1373
|
"src/server/graph/core/ts-extractor.ts"() {
|
|
964
1374
|
"use strict";
|
|
@@ -971,6 +1381,19 @@ var init_ts_extractor = __esm({
|
|
|
971
1381
|
return (0, import_node_path5.join)((0, import_node_path5.dirname)(__filename), "graph", "queries");
|
|
972
1382
|
})();
|
|
973
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
|
+
};
|
|
974
1397
|
PRISMA_MUTATION_METHODS_BUILTIN = [
|
|
975
1398
|
"create",
|
|
976
1399
|
"createMany",
|
|
@@ -997,6 +1420,72 @@ var init_ts_extractor = __esm({
|
|
|
997
1420
|
{ module: /^@auth\//, helpers: ["auth"] },
|
|
998
1421
|
{ module: /^@supabase\/auth-helpers/, helpers: ["createServerClient"] }
|
|
999
1422
|
];
|
|
1423
|
+
EXEMPT_NAME_PATTERNS = [
|
|
1424
|
+
/^is_?public/i,
|
|
1425
|
+
/^public_?routes?/i,
|
|
1426
|
+
/^public_?paths?/i,
|
|
1427
|
+
/^whitelist/i,
|
|
1428
|
+
/^allowlist/i,
|
|
1429
|
+
/^unauthenticated/i,
|
|
1430
|
+
/^anonymous/i,
|
|
1431
|
+
/^guest/i,
|
|
1432
|
+
/^skip_?auth/i,
|
|
1433
|
+
/^bypass/i
|
|
1434
|
+
];
|
|
1435
|
+
PROTECT_NAME_PATTERNS = [
|
|
1436
|
+
/^is_?protected/i,
|
|
1437
|
+
/^protected_?routes?/i,
|
|
1438
|
+
/^protected_?paths?/i,
|
|
1439
|
+
/^require_?auth/i,
|
|
1440
|
+
/^auth_?required/i,
|
|
1441
|
+
/^private_?routes?/i,
|
|
1442
|
+
/^is_?admin/i,
|
|
1443
|
+
/^admin_?routes?/i,
|
|
1444
|
+
/^secured/i
|
|
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
|
+
]);
|
|
1000
1489
|
}
|
|
1001
1490
|
});
|
|
1002
1491
|
|
|
@@ -1082,6 +1571,8 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
|
|
|
1082
1571
|
function classifyType(absPath, id) {
|
|
1083
1572
|
const contentType = classifyFile(absPath);
|
|
1084
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";
|
|
1085
1576
|
return contentType;
|
|
1086
1577
|
}
|
|
1087
1578
|
function extractRoute(id) {
|
|
@@ -1392,6 +1883,7 @@ function generate(rootDir) {
|
|
|
1392
1883
|
variables: deep.variables,
|
|
1393
1884
|
responses: deep.responses,
|
|
1394
1885
|
params: deep.params,
|
|
1886
|
+
...deep.effects ? { effects: deep.effects } : {},
|
|
1395
1887
|
_dbCalls: dbCalls
|
|
1396
1888
|
// temp: used for cross-ref building below
|
|
1397
1889
|
});
|
|
@@ -1411,6 +1903,7 @@ function generate(rootDir) {
|
|
|
1411
1903
|
stateVars: deep.stateVars,
|
|
1412
1904
|
conditions: deep.conditions,
|
|
1413
1905
|
variables: deep.variables,
|
|
1906
|
+
...deep.effects ? { effects: deep.effects } : {},
|
|
1414
1907
|
...authWrappers.length > 0 ? { auth: authWrappers } : {},
|
|
1415
1908
|
...dbCalls.length > 0 ? { _dbCalls: dbCalls } : {}
|
|
1416
1909
|
});
|
|
@@ -1540,43 +2033,169 @@ function generate(rootDir) {
|
|
|
1540
2033
|
nodeIdSet.add(externalId);
|
|
1541
2034
|
uiEdges.push(...edgesFromThis);
|
|
1542
2035
|
}
|
|
2036
|
+
const tablesByFile = /* @__PURE__ */ new Map();
|
|
2037
|
+
const allDbNodes = [...apiNodes, ...uiNodes];
|
|
2038
|
+
for (const node of allDbNodes) {
|
|
2039
|
+
const calls = node._dbCalls;
|
|
2040
|
+
if (!calls || calls.length === 0) continue;
|
|
2041
|
+
const map = /* @__PURE__ */ new Map();
|
|
2042
|
+
for (const c of calls) {
|
|
2043
|
+
const key = `${c.kind}:${c.model}:${c.isMutation ? "m" : "r"}`;
|
|
2044
|
+
if (!map.has(key)) {
|
|
2045
|
+
map.set(key, { model: c.model, method: c.method, isMutation: c.isMutation, kind: c.kind, via: [] });
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
tablesByFile.set(node.id, map);
|
|
2049
|
+
}
|
|
2050
|
+
const reverseRuntimeImports = /* @__PURE__ */ new Map();
|
|
2051
|
+
for (const edge of uiEdges) {
|
|
2052
|
+
if (edge.type !== "imports" && edge.type !== "renders") continue;
|
|
2053
|
+
if (!reverseRuntimeImports.has(edge.target)) {
|
|
2054
|
+
reverseRuntimeImports.set(edge.target, /* @__PURE__ */ new Set());
|
|
2055
|
+
}
|
|
2056
|
+
reverseRuntimeImports.get(edge.target).add(edge.source);
|
|
2057
|
+
}
|
|
2058
|
+
let changed = true;
|
|
2059
|
+
let iterations = 0;
|
|
2060
|
+
while (changed && iterations < 50) {
|
|
2061
|
+
changed = false;
|
|
2062
|
+
iterations++;
|
|
2063
|
+
for (const [target, tableMap] of [...tablesByFile]) {
|
|
2064
|
+
const importers = reverseRuntimeImports.get(target);
|
|
2065
|
+
if (!importers) continue;
|
|
2066
|
+
for (const importer of importers) {
|
|
2067
|
+
if (importer === target) continue;
|
|
2068
|
+
let importerMap = tablesByFile.get(importer);
|
|
2069
|
+
if (!importerMap) {
|
|
2070
|
+
importerMap = /* @__PURE__ */ new Map();
|
|
2071
|
+
tablesByFile.set(importer, importerMap);
|
|
2072
|
+
}
|
|
2073
|
+
for (const [key, call] of tableMap) {
|
|
2074
|
+
if (importerMap.has(key)) continue;
|
|
2075
|
+
importerMap.set(key, {
|
|
2076
|
+
model: call.model,
|
|
2077
|
+
method: null,
|
|
2078
|
+
isMutation: call.isMutation,
|
|
2079
|
+
kind: call.kind,
|
|
2080
|
+
via: [...call.via, target]
|
|
2081
|
+
});
|
|
2082
|
+
changed = true;
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
for (const node of apiNodes) {
|
|
2088
|
+
const map = tablesByFile.get(node.id);
|
|
2089
|
+
if (!map) continue;
|
|
2090
|
+
node.db_models = [...new Set([...map.values()].map((c) => c.model))];
|
|
2091
|
+
node.db_operations = [...new Set(
|
|
2092
|
+
[...map.values()].filter((c) => c.via.length === 0 && c.method).map((c) => `${c.model}.${c.method}`)
|
|
2093
|
+
)];
|
|
2094
|
+
node.mutates = [...map.values()].some((c) => c.isMutation);
|
|
2095
|
+
}
|
|
1543
2096
|
const apiCrossRefs = [];
|
|
1544
2097
|
for (const node of apiNodes) {
|
|
1545
|
-
const
|
|
1546
|
-
if (!
|
|
1547
|
-
|
|
1548
|
-
|
|
2098
|
+
const map = tablesByFile.get(node.id);
|
|
2099
|
+
if (!map) {
|
|
2100
|
+
delete node._dbCalls;
|
|
2101
|
+
continue;
|
|
2102
|
+
}
|
|
2103
|
+
const seenTargets = /* @__PURE__ */ new Set();
|
|
2104
|
+
for (const call of map.values()) {
|
|
1549
2105
|
const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
|
|
1550
|
-
if (
|
|
1551
|
-
|
|
1552
|
-
|
|
2106
|
+
if (seenTargets.has(target)) continue;
|
|
2107
|
+
seenTargets.add(target);
|
|
2108
|
+
const isTransitive = call.via.length > 0;
|
|
2109
|
+
const ref = {
|
|
1553
2110
|
source: node.id,
|
|
1554
2111
|
target,
|
|
1555
|
-
type: call.isMutation ? "mutates" : "reads",
|
|
2112
|
+
type: call.isMutation ? isTransitive ? "mutates_via" : "mutates" : isTransitive ? "reads_via" : "reads",
|
|
1556
2113
|
layer: "db"
|
|
1557
|
-
}
|
|
2114
|
+
};
|
|
2115
|
+
if (isTransitive) ref.via = call.via;
|
|
2116
|
+
apiCrossRefs.push(ref);
|
|
1558
2117
|
}
|
|
1559
2118
|
delete node._dbCalls;
|
|
1560
2119
|
}
|
|
1561
2120
|
const uiCrossRefs = [];
|
|
1562
2121
|
for (const node of uiNodes) {
|
|
1563
|
-
const
|
|
1564
|
-
if (!
|
|
1565
|
-
|
|
1566
|
-
|
|
2122
|
+
const map = tablesByFile.get(node.id);
|
|
2123
|
+
if (!map) {
|
|
2124
|
+
if (node._dbCalls) delete node._dbCalls;
|
|
2125
|
+
continue;
|
|
2126
|
+
}
|
|
2127
|
+
const seenTargets = /* @__PURE__ */ new Set();
|
|
2128
|
+
for (const call of map.values()) {
|
|
1567
2129
|
const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
|
|
1568
|
-
if (
|
|
1569
|
-
|
|
1570
|
-
|
|
2130
|
+
if (seenTargets.has(target)) continue;
|
|
2131
|
+
seenTargets.add(target);
|
|
2132
|
+
const isTransitive = call.via.length > 0;
|
|
2133
|
+
const ref = {
|
|
1571
2134
|
source: node.id,
|
|
1572
2135
|
target,
|
|
1573
|
-
type: call.isMutation ? "mutates" : "reads",
|
|
2136
|
+
type: call.isMutation ? isTransitive ? "mutates_via" : "mutates" : isTransitive ? "reads_via" : "reads",
|
|
1574
2137
|
layer: "db"
|
|
1575
|
-
}
|
|
2138
|
+
};
|
|
2139
|
+
if (isTransitive) ref.via = call.via;
|
|
2140
|
+
uiCrossRefs.push(ref);
|
|
1576
2141
|
}
|
|
1577
|
-
delete node._dbCalls;
|
|
2142
|
+
if (node._dbCalls) delete node._dbCalls;
|
|
1578
2143
|
}
|
|
1579
2144
|
uiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
2145
|
+
const middlewareInfos = [];
|
|
2146
|
+
for (const conv of paths.conventionFiles) {
|
|
2147
|
+
if (!/middleware\.tsx?$/.test(conv)) continue;
|
|
2148
|
+
try {
|
|
2149
|
+
const info = extractMiddlewareAuthTS(conv);
|
|
2150
|
+
if (info && info.matchers.length > 0) middlewareInfos.push(info);
|
|
2151
|
+
} catch {
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
if (middlewareInfos.length > 0) {
|
|
2155
|
+
let setAuth2 = function(node, newTags, replaceAll) {
|
|
2156
|
+
const existing = node.auth ?? [];
|
|
2157
|
+
const meaningful = existing.filter((a) => a !== "public");
|
|
2158
|
+
const merged = replaceAll ? newTags : [.../* @__PURE__ */ new Set([...newTags, ...meaningful])];
|
|
2159
|
+
node.auth = merged.length > 0 ? merged : ["public"];
|
|
2160
|
+
}, applyMiddleware2 = function(node, routePath) {
|
|
2161
|
+
let resolved = null;
|
|
2162
|
+
let label = "";
|
|
2163
|
+
let hasAnyExemptMatcher = false;
|
|
2164
|
+
let hasAnyFallthrough = false;
|
|
2165
|
+
for (const info of middlewareInfos) {
|
|
2166
|
+
if (info.hasFallthroughProtect) hasAnyFallthrough = true;
|
|
2167
|
+
if (info.matchers.some((m) => m.intent === "exempt")) hasAnyExemptMatcher = true;
|
|
2168
|
+
const c = classifyRouteAgainstMiddleware(routePath, info);
|
|
2169
|
+
if (!c) continue;
|
|
2170
|
+
if (!resolved) {
|
|
2171
|
+
resolved = c.intent;
|
|
2172
|
+
label = c.matcher;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
if (resolved === "exempt") {
|
|
2176
|
+
setAuth2(node, ["public"], true);
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
if (resolved === "protect") {
|
|
2180
|
+
setAuth2(node, [`middleware:${label}`], false);
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
if (hasAnyExemptMatcher && hasAnyFallthrough) {
|
|
2184
|
+
setAuth2(node, ["middleware-protected"], false);
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
var setAuth = setAuth2, applyMiddleware = applyMiddleware2;
|
|
2188
|
+
for (const node of apiNodes) {
|
|
2189
|
+
const routePath = node.path;
|
|
2190
|
+
if (!routePath) continue;
|
|
2191
|
+
applyMiddleware2(node, routePath);
|
|
2192
|
+
}
|
|
2193
|
+
for (const node of uiNodes) {
|
|
2194
|
+
const route = node.route;
|
|
2195
|
+
if (!route) continue;
|
|
2196
|
+
applyMiddleware2(node, route);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
1580
2199
|
const apiNodeIds = new Set(apiNodes.map((n) => n.id));
|
|
1581
2200
|
const apiEdges = [];
|
|
1582
2201
|
const uiOnlyEdges = [];
|
|
@@ -1732,6 +2351,7 @@ var init_typescript_project = __esm({
|
|
|
1732
2351
|
config: "ui",
|
|
1733
2352
|
lib: "ui",
|
|
1734
2353
|
"mcp-tool": "ui",
|
|
2354
|
+
middleware: "ui",
|
|
1735
2355
|
external: "ui"
|
|
1736
2356
|
};
|
|
1737
2357
|
typescriptProjectParser = {
|
|
@@ -3641,6 +4261,119 @@ var init_static_ref_scanner = __esm({
|
|
|
3641
4261
|
}
|
|
3642
4262
|
});
|
|
3643
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
|
+
|
|
3644
4377
|
// src/server/graph/core/parser-registry.ts
|
|
3645
4378
|
function isMultiLayerParser(p) {
|
|
3646
4379
|
return "layers" in p && Array.isArray(p.layers);
|
|
@@ -3654,7 +4387,8 @@ function registerBuiltins(registry, disabled) {
|
|
|
3654
4387
|
fetchResolverParser,
|
|
3655
4388
|
apiAnnotationsParser,
|
|
3656
4389
|
urlLiteralScannerParser,
|
|
3657
|
-
staticRefScannerParser
|
|
4390
|
+
staticRefScannerParser,
|
|
4391
|
+
middlewareGatesParser
|
|
3658
4392
|
];
|
|
3659
4393
|
for (const parser of builtins) {
|
|
3660
4394
|
if (disabled.has(parser.id)) continue;
|
|
@@ -3664,7 +4398,7 @@ function registerBuiltins(registry, disabled) {
|
|
|
3664
4398
|
function loadCustomParsers(registry, config, rootDir, disabled) {
|
|
3665
4399
|
for (const entry of config.parsers?.custom ?? []) {
|
|
3666
4400
|
try {
|
|
3667
|
-
const absPath = (0,
|
|
4401
|
+
const absPath = (0, import_node_path13.resolve)(rootDir, entry.path);
|
|
3668
4402
|
const mod = require(absPath);
|
|
3669
4403
|
const parser = "default" in mod ? mod.default : mod;
|
|
3670
4404
|
if (disabled.has(parser.id)) continue;
|
|
@@ -3691,11 +4425,11 @@ function createRegistry(config, rootDir) {
|
|
|
3691
4425
|
loadCustomParsers(registry, config, rootDir, disabled);
|
|
3692
4426
|
return registry;
|
|
3693
4427
|
}
|
|
3694
|
-
var
|
|
4428
|
+
var import_node_path13, ParserRegistry;
|
|
3695
4429
|
var init_parser_registry = __esm({
|
|
3696
4430
|
"src/server/graph/core/parser-registry.ts"() {
|
|
3697
4431
|
"use strict";
|
|
3698
|
-
|
|
4432
|
+
import_node_path13 = require("node:path");
|
|
3699
4433
|
init_typescript_project();
|
|
3700
4434
|
init_prisma_schema();
|
|
3701
4435
|
init_sql_migrations();
|
|
@@ -3704,6 +4438,7 @@ var init_parser_registry = __esm({
|
|
|
3704
4438
|
init_url_literal_scanner();
|
|
3705
4439
|
init_static_values();
|
|
3706
4440
|
init_static_ref_scanner();
|
|
4441
|
+
init_middleware_gates();
|
|
3707
4442
|
ParserRegistry = class {
|
|
3708
4443
|
constructor() {
|
|
3709
4444
|
this.singleLayerParsers = /* @__PURE__ */ new Map();
|
|
@@ -3866,7 +4601,7 @@ var init_merge = __esm({
|
|
|
3866
4601
|
|
|
3867
4602
|
// src/server/graph/core/graph-builder.ts
|
|
3868
4603
|
function readGraphFromDisk(rootDir, layer) {
|
|
3869
|
-
const filePath = (0,
|
|
4604
|
+
const filePath = (0, import_node_path14.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
3870
4605
|
if (!(0, import_node_fs13.existsSync)(filePath)) return null;
|
|
3871
4606
|
try {
|
|
3872
4607
|
return JSON.parse((0, import_node_fs13.readFileSync)(filePath, "utf-8"));
|
|
@@ -3965,12 +4700,12 @@ function generateAll(rootDir) {
|
|
|
3965
4700
|
const extras = [...byLayer.keys()].filter((l) => !wellKnownOrder.includes(l)).sort();
|
|
3966
4701
|
return [...wellKnownOrder, ...extras].map((l) => byLayer.get(l)).filter((r) => !!r);
|
|
3967
4702
|
}
|
|
3968
|
-
var import_node_fs13,
|
|
4703
|
+
var import_node_fs13, import_node_path14;
|
|
3969
4704
|
var init_graph_builder = __esm({
|
|
3970
4705
|
"src/server/graph/core/graph-builder.ts"() {
|
|
3971
4706
|
"use strict";
|
|
3972
4707
|
import_node_fs13 = require("node:fs");
|
|
3973
|
-
|
|
4708
|
+
import_node_path14 = require("node:path");
|
|
3974
4709
|
init_config();
|
|
3975
4710
|
init_parser_registry();
|
|
3976
4711
|
init_merge();
|
|
@@ -4009,13 +4744,13 @@ function detectConventionDirs(rootDir, extraConventionDirs = []) {
|
|
|
4009
4744
|
const conventionDirs = [...CONVENTION_DIRS_BUILTIN, ...extraConventionDirs];
|
|
4010
4745
|
const searchDirs = [
|
|
4011
4746
|
rootDir,
|
|
4012
|
-
(0,
|
|
4013
|
-
(0,
|
|
4014
|
-
(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")
|
|
4015
4750
|
];
|
|
4016
4751
|
for (const base of searchDirs) {
|
|
4017
4752
|
for (const convention of conventionDirs) {
|
|
4018
|
-
const dir = (0,
|
|
4753
|
+
const dir = (0, import_node_path15.join)(base, convention);
|
|
4019
4754
|
if (!(0, import_node_fs14.existsSync)(dir)) continue;
|
|
4020
4755
|
try {
|
|
4021
4756
|
const stat = (0, import_node_fs14.statSync)(dir);
|
|
@@ -4096,12 +4831,12 @@ function extractModuleFromPath(id, extraTrivial, extraSkipSegments, extraGeneric
|
|
|
4096
4831
|
}
|
|
4097
4832
|
return "root";
|
|
4098
4833
|
}
|
|
4099
|
-
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;
|
|
4100
4835
|
var init_module_tagger = __esm({
|
|
4101
4836
|
"src/server/graph/taggers/module-tagger.ts"() {
|
|
4102
4837
|
"use strict";
|
|
4103
4838
|
import_node_fs14 = require("node:fs");
|
|
4104
|
-
|
|
4839
|
+
import_node_path15 = require("node:path");
|
|
4105
4840
|
CONVENTION_DIRS_BUILTIN = ["features", "modules", "domains", "areas"];
|
|
4106
4841
|
GENERIC_ROLE_NAMES_BUILTIN = /* @__PURE__ */ new Set([
|
|
4107
4842
|
// JS/TS
|
|
@@ -4316,7 +5051,7 @@ function loadCustomTaggers(registry, config, rootDir, disabled) {
|
|
|
4316
5051
|
for (const entry of config.taggers?.custom ?? []) {
|
|
4317
5052
|
if (disabled.has(entry.id)) continue;
|
|
4318
5053
|
try {
|
|
4319
|
-
const absPath = (0,
|
|
5054
|
+
const absPath = (0, import_node_path16.resolve)(rootDir, entry.path);
|
|
4320
5055
|
const mod = require(absPath);
|
|
4321
5056
|
const tagger = "default" in mod ? mod.default : mod;
|
|
4322
5057
|
const override = config.taggers?.trackUntagged?.[tagger.id];
|
|
@@ -4337,11 +5072,11 @@ function createTaggerRegistry(config, rootDir) {
|
|
|
4337
5072
|
loadCustomTaggers(registry, config, rootDir, disabled);
|
|
4338
5073
|
return registry;
|
|
4339
5074
|
}
|
|
4340
|
-
var
|
|
5075
|
+
var import_node_path16, TaggerRegistry, BUILTIN_TAGGERS;
|
|
4341
5076
|
var init_tagger_registry = __esm({
|
|
4342
5077
|
"src/server/graph/core/tagger-registry.ts"() {
|
|
4343
5078
|
"use strict";
|
|
4344
|
-
|
|
5079
|
+
import_node_path16 = require("node:path");
|
|
4345
5080
|
init_module_tagger();
|
|
4346
5081
|
init_screen_tagger();
|
|
4347
5082
|
TaggerRegistry = class {
|
|
@@ -4369,7 +5104,7 @@ var init_tagger_registry = __esm({
|
|
|
4369
5104
|
|
|
4370
5105
|
// src/server/graph/core/tag-store.ts
|
|
4371
5106
|
function tagsFilePath(rootDir) {
|
|
4372
|
-
return (0,
|
|
5107
|
+
return (0, import_node_path17.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
|
|
4373
5108
|
}
|
|
4374
5109
|
function readTagStore(rootDir) {
|
|
4375
5110
|
const filePath = tagsFilePath(rootDir);
|
|
@@ -4390,7 +5125,7 @@ function readTagStore(rootDir) {
|
|
|
4390
5125
|
}
|
|
4391
5126
|
function writeTagStore(rootDir, store) {
|
|
4392
5127
|
const filePath = tagsFilePath(rootDir);
|
|
4393
|
-
const dir = (0,
|
|
5128
|
+
const dir = (0, import_node_path17.dirname)(filePath);
|
|
4394
5129
|
(0, import_node_fs15.mkdirSync)(dir, { recursive: true });
|
|
4395
5130
|
const cleaned = {};
|
|
4396
5131
|
for (const [nodeId, tags] of Object.entries(store)) {
|
|
@@ -4416,36 +5151,102 @@ function removeTag(rootDir, nodeId, key) {
|
|
|
4416
5151
|
}
|
|
4417
5152
|
writeTagStore(rootDir, store);
|
|
4418
5153
|
}
|
|
4419
|
-
var import_node_fs15,
|
|
5154
|
+
var import_node_fs15, import_node_path17, TAGS_FILENAME, GRAPHS_DIR, tagCache;
|
|
4420
5155
|
var init_tag_store = __esm({
|
|
4421
5156
|
"src/server/graph/core/tag-store.ts"() {
|
|
4422
5157
|
"use strict";
|
|
4423
5158
|
import_node_fs15 = require("node:fs");
|
|
4424
|
-
|
|
5159
|
+
import_node_path17 = require("node:path");
|
|
4425
5160
|
TAGS_FILENAME = "tags.json";
|
|
4426
5161
|
GRAPHS_DIR = ".launchsecure/graphs";
|
|
4427
5162
|
tagCache = /* @__PURE__ */ new Map();
|
|
4428
5163
|
}
|
|
4429
5164
|
});
|
|
4430
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
|
+
|
|
4431
5232
|
// src/server/graph/index.ts
|
|
4432
5233
|
function getAvailableLayers(rootDir) {
|
|
4433
|
-
const dir = (0,
|
|
4434
|
-
if (!(0,
|
|
4435
|
-
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", ""));
|
|
4436
5237
|
}
|
|
4437
5238
|
function graphsDir(rootDir) {
|
|
4438
|
-
return (0,
|
|
5239
|
+
return (0, import_node_path19.join)(rootDir, GRAPHS_DIR2);
|
|
4439
5240
|
}
|
|
4440
5241
|
function graphFilePath(rootDir, layer) {
|
|
4441
|
-
return (0,
|
|
5242
|
+
return (0, import_node_path19.join)(graphsDir(rootDir), `${layer}.json`);
|
|
4442
5243
|
}
|
|
4443
5244
|
function tagsFilePath2(rootDir) {
|
|
4444
|
-
return (0,
|
|
5245
|
+
return (0, import_node_path19.join)(graphsDir(rootDir), "tags.json");
|
|
4445
5246
|
}
|
|
4446
5247
|
function getMtimeMs(filePath) {
|
|
4447
|
-
if (!(0,
|
|
4448
|
-
return (0,
|
|
5248
|
+
if (!(0, import_node_fs17.existsSync)(filePath)) return 0;
|
|
5249
|
+
return (0, import_node_fs17.statSync)(filePath).mtimeMs;
|
|
4449
5250
|
}
|
|
4450
5251
|
function invalidateCache(filePath) {
|
|
4451
5252
|
graphCache.delete(filePath);
|
|
@@ -4484,20 +5285,20 @@ function applyTags(graph, layer, rootDir) {
|
|
|
4484
5285
|
}
|
|
4485
5286
|
function readGraphRaw(rootDir, layer) {
|
|
4486
5287
|
const filePath = graphFilePath(rootDir, layer);
|
|
4487
|
-
if (!(0,
|
|
4488
|
-
const stat = (0,
|
|
5288
|
+
if (!(0, import_node_fs17.existsSync)(filePath)) return null;
|
|
5289
|
+
const stat = (0, import_node_fs17.statSync)(filePath);
|
|
4489
5290
|
const cached = graphCache.get(filePath);
|
|
4490
5291
|
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
4491
5292
|
return cached.graph;
|
|
4492
5293
|
}
|
|
4493
|
-
const content = (0,
|
|
5294
|
+
const content = (0, import_node_fs17.readFileSync)(filePath, "utf-8");
|
|
4494
5295
|
const graph = JSON.parse(content);
|
|
4495
5296
|
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
4496
5297
|
return graph;
|
|
4497
5298
|
}
|
|
4498
5299
|
function readGraph(rootDir, layer) {
|
|
4499
5300
|
const rawFilePath = graphFilePath(rootDir, layer);
|
|
4500
|
-
if (!(0,
|
|
5301
|
+
if (!(0, import_node_fs17.existsSync)(rawFilePath)) return null;
|
|
4501
5302
|
const rawMtime = getMtimeMs(rawFilePath);
|
|
4502
5303
|
const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
|
|
4503
5304
|
const cacheKey = `${rootDir}:${layer}`;
|
|
@@ -4527,24 +5328,24 @@ async function generateGraph(rootDir, layer) {
|
|
|
4527
5328
|
mutationMethods: config.parsers?.patterns?.mutationMethods
|
|
4528
5329
|
});
|
|
4529
5330
|
const dir = graphsDir(rootDir);
|
|
4530
|
-
(0,
|
|
5331
|
+
(0, import_node_fs17.mkdirSync)(dir, { recursive: true });
|
|
4531
5332
|
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
4532
5333
|
for (const result of results) {
|
|
4533
5334
|
const filePath = graphFilePath(rootDir, result.layer);
|
|
4534
|
-
(0,
|
|
5335
|
+
(0, import_node_fs17.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
|
|
4535
5336
|
invalidateCache(filePath);
|
|
4536
5337
|
invalidateTaggedCache(rootDir, result.layer);
|
|
4537
5338
|
}
|
|
4538
5339
|
if (!layer) {
|
|
4539
5340
|
const producedLayers = new Set(results.map((r) => r.layer));
|
|
4540
5341
|
try {
|
|
4541
|
-
for (const f of (0,
|
|
4542
|
-
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;
|
|
4543
5344
|
const layerName = f.replace(/\.json$/, "");
|
|
4544
5345
|
if (producedLayers.has(layerName)) continue;
|
|
4545
|
-
const orphan = (0,
|
|
5346
|
+
const orphan = (0, import_node_path19.join)(dir, f);
|
|
4546
5347
|
try {
|
|
4547
|
-
(0,
|
|
5348
|
+
(0, import_node_fs17.unlinkSync)(orphan);
|
|
4548
5349
|
invalidateCache(orphan);
|
|
4549
5350
|
invalidateTaggedCache(rootDir, layerName);
|
|
4550
5351
|
process.stderr.write(`[launch-chart] removed orphan layer file: ${f} (no parser produced ${layerName} this run)
|
|
@@ -4555,32 +5356,404 @@ async function generateGraph(rootDir, layer) {
|
|
|
4555
5356
|
} catch {
|
|
4556
5357
|
}
|
|
4557
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
|
+
}
|
|
4558
5377
|
return results;
|
|
4559
5378
|
}
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
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;
|
|
5389
|
+
var init_graph = __esm({
|
|
5390
|
+
"src/server/graph/index.ts"() {
|
|
4563
5391
|
"use strict";
|
|
4564
|
-
|
|
4565
|
-
|
|
5392
|
+
import_node_fs17 = require("node:fs");
|
|
5393
|
+
import_node_path19 = require("node:path");
|
|
4566
5394
|
init_graph_builder();
|
|
4567
5395
|
init_config();
|
|
4568
5396
|
init_tagger_registry();
|
|
4569
5397
|
init_tag_store();
|
|
4570
5398
|
init_ts_extractor();
|
|
5399
|
+
init_effects_index();
|
|
4571
5400
|
init_tag_store();
|
|
4572
5401
|
GRAPHS_DIR2 = ".launchsecure/graphs";
|
|
5402
|
+
NON_LAYER_GRAPH_FILES = /* @__PURE__ */ new Set(["tags.json", "effects-index.json"]);
|
|
4573
5403
|
graphCache = /* @__PURE__ */ new Map();
|
|
4574
5404
|
taggedCache = /* @__PURE__ */ new Map();
|
|
4575
5405
|
}
|
|
4576
5406
|
});
|
|
4577
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
|
+
|
|
4578
5751
|
// src/server/graph/core/audit-core.ts
|
|
4579
5752
|
function readGraphFile(rootDir, layer) {
|
|
4580
|
-
const filePath = (0,
|
|
4581
|
-
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;
|
|
4582
5755
|
try {
|
|
4583
|
-
return JSON.parse((0,
|
|
5756
|
+
return JSON.parse((0, import_node_fs19.readFileSync)(filePath, "utf-8"));
|
|
4584
5757
|
} catch {
|
|
4585
5758
|
return null;
|
|
4586
5759
|
}
|
|
@@ -4589,8 +5762,7 @@ function checkSchemaDrift(rootDir) {
|
|
|
4589
5762
|
const findings = [];
|
|
4590
5763
|
const db = readGraphFile(rootDir, "db");
|
|
4591
5764
|
if (!db) {
|
|
4592
|
-
|
|
4593
|
-
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");
|
|
4594
5766
|
}
|
|
4595
5767
|
for (const c of db.contradictions ?? []) {
|
|
4596
5768
|
const isTableLevel = c.detail.includes("Table ") && (c.detail.includes("has no CREATE TABLE") || c.detail.includes("not in schema.prisma"));
|
|
@@ -4607,7 +5779,7 @@ function checkSchemaDrift(rootDir) {
|
|
|
4607
5779
|
function checkOrphanFks(rootDir) {
|
|
4608
5780
|
const findings = [];
|
|
4609
5781
|
const db = readGraphFile(rootDir, "db");
|
|
4610
|
-
if (!db) return
|
|
5782
|
+
if (!db) return buildSkipped("db", "orphan_fks", "no db graph");
|
|
4611
5783
|
for (const f of db.flagged_edges ?? []) {
|
|
4612
5784
|
findings.push({
|
|
4613
5785
|
id: `fk:${f.source}->${f.target}`,
|
|
@@ -4622,13 +5794,16 @@ function checkOrphanFks(rootDir) {
|
|
|
4622
5794
|
function checkUnprotectedRoutes(rootDir) {
|
|
4623
5795
|
const findings = [];
|
|
4624
5796
|
const api = readGraphFile(rootDir, "api");
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
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
|
+
);
|
|
4631
5805
|
}
|
|
5806
|
+
const routePermsContent = (0, import_node_fs19.readFileSync)(routePermsPath, "utf-8");
|
|
4632
5807
|
const registeredRoutes = /* @__PURE__ */ new Set();
|
|
4633
5808
|
const routeEntryRe = /path:\s*'([^']+)'/g;
|
|
4634
5809
|
let rm;
|
|
@@ -4672,7 +5847,7 @@ function routeMatchesPattern(route, pattern) {
|
|
|
4672
5847
|
function checkDeadScreens(rootDir) {
|
|
4673
5848
|
const findings = [];
|
|
4674
5849
|
const ui = readGraphFile(rootDir, "ui");
|
|
4675
|
-
if (!ui) return
|
|
5850
|
+
if (!ui) return buildSkipped("ui", "dead_screens", "no ui graph");
|
|
4676
5851
|
const pages = ui.nodes.filter((n) => n.type === "page" || n.type === "layout");
|
|
4677
5852
|
const navTargets = /* @__PURE__ */ new Set();
|
|
4678
5853
|
for (const e of ui.edges) {
|
|
@@ -4704,13 +5879,24 @@ function checkDeadScreens(rootDir) {
|
|
|
4704
5879
|
function checkUnenforcedPermissions(rootDir) {
|
|
4705
5880
|
const findings = [];
|
|
4706
5881
|
const staticGraph = readGraphFile(rootDir, "static");
|
|
4707
|
-
if (!staticGraph) return
|
|
5882
|
+
if (!staticGraph) return buildSkipped("static", "unenforced_permissions", "no static graph");
|
|
4708
5883
|
const permissions = staticGraph.nodes.filter((n) => n.type === "seed_permission").map((n) => ({ id: n.id, key: n.value, name: n.name }));
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
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
|
+
);
|
|
4713
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
|
+
);
|
|
5898
|
+
}
|
|
5899
|
+
const routePermsContent = (0, import_node_fs19.readFileSync)(routePermsPath, "utf-8");
|
|
4714
5900
|
for (const perm of permissions) {
|
|
4715
5901
|
const regex = new RegExp(`permission:\\s*['"]${perm.key}['"]`);
|
|
4716
5902
|
if (!regex.test(routePermsContent)) {
|
|
@@ -4728,20 +5914,27 @@ function checkUnenforcedPermissions(rootDir) {
|
|
|
4728
5914
|
function checkHardcodedValues(rootDir) {
|
|
4729
5915
|
const findings = [];
|
|
4730
5916
|
const staticGraph = readGraphFile(rootDir, "static");
|
|
4731
|
-
if (!staticGraph) return
|
|
5917
|
+
if (!staticGraph) return buildSkipped("static", "hardcoded_values", "no static graph");
|
|
4732
5918
|
const knownValues = /* @__PURE__ */ new Set();
|
|
4733
5919
|
for (const n of staticGraph.nodes) {
|
|
4734
5920
|
if (n.type === "enum_value") knownValues.add(n.value);
|
|
4735
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
|
+
}
|
|
4736
5929
|
const api = readGraphFile(rootDir, "api");
|
|
4737
|
-
if (!api) return
|
|
5930
|
+
if (!api) return buildSkipped("static", "hardcoded_values", "no api graph");
|
|
4738
5931
|
const allCapsRe = /['"]([A-Z][A-Z_]{2,})['"]/g;
|
|
4739
5932
|
const seen = /* @__PURE__ */ new Set();
|
|
4740
5933
|
for (const node of api.nodes) {
|
|
4741
5934
|
if (node.type !== "endpoint") continue;
|
|
4742
|
-
const filePath = (0,
|
|
4743
|
-
if (!(0,
|
|
4744
|
-
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");
|
|
4745
5938
|
let m;
|
|
4746
5939
|
allCapsRe.lastIndex = 0;
|
|
4747
5940
|
while ((m = allCapsRe.exec(content)) !== null) {
|
|
@@ -4773,7 +5966,19 @@ function buildReport(layer, check, findings) {
|
|
|
4773
5966
|
warnings: findings.filter((f) => f.severity === "warning").length,
|
|
4774
5967
|
info: findings.filter((f) => f.severity === "info").length
|
|
4775
5968
|
},
|
|
4776
|
-
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
|
|
4777
5982
|
};
|
|
4778
5983
|
}
|
|
4779
5984
|
function getAvailableChecks() {
|
|
@@ -4820,7 +6025,12 @@ function runAudit(rootDir, layer, check) {
|
|
|
4820
6025
|
}
|
|
4821
6026
|
function formatAsPrompt(reports) {
|
|
4822
6027
|
const lines = [];
|
|
6028
|
+
const skipped = [];
|
|
4823
6029
|
for (const report of reports) {
|
|
6030
|
+
if (report.status === "skipped") {
|
|
6031
|
+
skipped.push(report);
|
|
6032
|
+
continue;
|
|
6033
|
+
}
|
|
4824
6034
|
if (report.findings.length === 0) continue;
|
|
4825
6035
|
lines.push(`## ${report.layer.toUpperCase()} \u2014 ${report.check} (${report.findings.length} findings)`);
|
|
4826
6036
|
lines.push("");
|
|
@@ -4832,15 +6042,24 @@ function formatAsPrompt(reports) {
|
|
|
4832
6042
|
}
|
|
4833
6043
|
lines.push("");
|
|
4834
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
|
+
}
|
|
4835
6053
|
if (lines.length === 0) return "No audit findings.";
|
|
4836
6054
|
return lines.join("\n");
|
|
4837
6055
|
}
|
|
4838
|
-
var
|
|
6056
|
+
var import_node_fs19, import_node_path21, CHECKS;
|
|
4839
6057
|
var init_audit_core = __esm({
|
|
4840
6058
|
"src/server/graph/core/audit-core.ts"() {
|
|
4841
6059
|
"use strict";
|
|
4842
|
-
|
|
4843
|
-
|
|
6060
|
+
import_node_fs19 = require("node:fs");
|
|
6061
|
+
import_node_path21 = require("node:path");
|
|
6062
|
+
init_audit_security();
|
|
4844
6063
|
CHECKS = {
|
|
4845
6064
|
db: {
|
|
4846
6065
|
schema_drift: checkSchemaDrift,
|
|
@@ -4855,6 +6074,11 @@ var init_audit_core = __esm({
|
|
|
4855
6074
|
static: {
|
|
4856
6075
|
unenforced_permissions: checkUnenforcedPermissions,
|
|
4857
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 })
|
|
4858
6082
|
}
|
|
4859
6083
|
};
|
|
4860
6084
|
}
|
|
@@ -4872,16 +6096,16 @@ function randomPort() {
|
|
|
4872
6096
|
function findProjectRoot(startDir) {
|
|
4873
6097
|
let dir = startDir;
|
|
4874
6098
|
for (let i = 0; i < 8; i++) {
|
|
4875
|
-
const graphsDir2 =
|
|
4876
|
-
if (
|
|
4877
|
-
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);
|
|
4878
6102
|
if (parent === dir) break;
|
|
4879
6103
|
dir = parent;
|
|
4880
6104
|
}
|
|
4881
6105
|
dir = startDir;
|
|
4882
6106
|
for (let i = 0; i < 8; i++) {
|
|
4883
|
-
if (
|
|
4884
|
-
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);
|
|
4885
6109
|
if (parent === dir) break;
|
|
4886
6110
|
dir = parent;
|
|
4887
6111
|
}
|
|
@@ -4890,7 +6114,7 @@ function findProjectRoot(startDir) {
|
|
|
4890
6114
|
function resolveRequestRoot(url, monorepoRoot, projects) {
|
|
4891
6115
|
const projectParam = url.searchParams.get("project");
|
|
4892
6116
|
if (!projectParam || projects.length === 0) return monorepoRoot;
|
|
4893
|
-
const resolved =
|
|
6117
|
+
const resolved = import_node_path22.default.resolve(monorepoRoot, projectParam);
|
|
4894
6118
|
if (!resolved.startsWith(monorepoRoot)) {
|
|
4895
6119
|
throw new Error("Project path outside monorepo root");
|
|
4896
6120
|
}
|
|
@@ -4941,16 +6165,16 @@ async function buildMergedGraph(root) {
|
|
|
4941
6165
|
};
|
|
4942
6166
|
}
|
|
4943
6167
|
function serveStatic(res, filePath) {
|
|
4944
|
-
if (!
|
|
4945
|
-
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();
|
|
4946
6170
|
const mime = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
4947
6171
|
res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
|
|
4948
|
-
|
|
6172
|
+
import_node_fs20.default.createReadStream(filePath).pipe(res);
|
|
4949
6173
|
return true;
|
|
4950
6174
|
}
|
|
4951
6175
|
function serveIndex(res, clientDir) {
|
|
4952
|
-
const indexPath =
|
|
4953
|
-
if (!
|
|
6176
|
+
const indexPath = import_node_path22.default.join(clientDir, "index.html");
|
|
6177
|
+
if (!import_node_fs20.default.existsSync(indexPath)) {
|
|
4954
6178
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
4955
6179
|
res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
|
|
4956
6180
|
return;
|
|
@@ -4958,14 +6182,14 @@ function serveIndex(res, clientDir) {
|
|
|
4958
6182
|
serveStatic(res, indexPath);
|
|
4959
6183
|
}
|
|
4960
6184
|
function tryListen(server, port) {
|
|
4961
|
-
return new Promise((
|
|
6185
|
+
return new Promise((resolve5, reject) => {
|
|
4962
6186
|
const onError = (err2) => {
|
|
4963
6187
|
server.off("listening", onListening);
|
|
4964
6188
|
reject(err2);
|
|
4965
6189
|
};
|
|
4966
6190
|
const onListening = () => {
|
|
4967
6191
|
server.off("error", onError);
|
|
4968
|
-
|
|
6192
|
+
resolve5(port);
|
|
4969
6193
|
};
|
|
4970
6194
|
server.once("error", onError);
|
|
4971
6195
|
server.once("listening", onListening);
|
|
@@ -5002,7 +6226,7 @@ async function startChartServer(opts = {}) {
|
|
|
5002
6226
|
}
|
|
5003
6227
|
return { port: existing.port, url: existing.url };
|
|
5004
6228
|
}
|
|
5005
|
-
const clientDir = opts.clientDir ??
|
|
6229
|
+
const clientDir = opts.clientDir ?? import_node_path22.default.join(__dirname, "..", "chart-client");
|
|
5006
6230
|
const rootConfig = loadConfig(projectRoot);
|
|
5007
6231
|
const projects = rootConfig.projects ?? [];
|
|
5008
6232
|
const server = import_node_http.default.createServer((req, res) => {
|
|
@@ -5018,11 +6242,11 @@ async function startChartServer(opts = {}) {
|
|
|
5018
6242
|
}
|
|
5019
6243
|
if (req.method === "GET" && url2.pathname === "/api/projects") {
|
|
5020
6244
|
const projectList = projects.length > 0 ? projects.map((p) => {
|
|
5021
|
-
const absRoot =
|
|
5022
|
-
const hasGraphs =
|
|
5023
|
-
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"));
|
|
5024
6248
|
return { name: p.name, root: p.root, hasGraphs, hasNextConfig };
|
|
5025
|
-
}) : [{ 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 }];
|
|
5026
6250
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5027
6251
|
res.end(JSON.stringify({ projects: projectList, monorepoRoot: projectRoot }));
|
|
5028
6252
|
return;
|
|
@@ -5068,20 +6292,20 @@ async function startChartServer(opts = {}) {
|
|
|
5068
6292
|
}
|
|
5069
6293
|
if (req.method === "GET" && url2.pathname === "/api/file-content") {
|
|
5070
6294
|
const relPath = url2.searchParams.get("path");
|
|
5071
|
-
if (!relPath || relPath.includes("..") ||
|
|
6295
|
+
if (!relPath || relPath.includes("..") || import_node_path22.default.isAbsolute(relPath)) {
|
|
5072
6296
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
5073
6297
|
res.end(JSON.stringify({ error: "Invalid path" }));
|
|
5074
6298
|
return;
|
|
5075
6299
|
}
|
|
5076
|
-
const filePath =
|
|
5077
|
-
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()) {
|
|
5078
6302
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
5079
6303
|
res.end(JSON.stringify({ error: "File not found" }));
|
|
5080
6304
|
return;
|
|
5081
6305
|
}
|
|
5082
|
-
const ext =
|
|
6306
|
+
const ext = import_node_path22.default.extname(filePath).toLowerCase();
|
|
5083
6307
|
const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
|
|
5084
|
-
const content =
|
|
6308
|
+
const content = import_node_fs20.default.readFileSync(filePath, "utf-8");
|
|
5085
6309
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5086
6310
|
res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
|
|
5087
6311
|
return;
|
|
@@ -5123,8 +6347,8 @@ async function startChartServer(opts = {}) {
|
|
|
5123
6347
|
req.on("end", () => {
|
|
5124
6348
|
try {
|
|
5125
6349
|
const newConfig = JSON.parse(body);
|
|
5126
|
-
const configPath =
|
|
5127
|
-
|
|
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");
|
|
5128
6352
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5129
6353
|
res.end(JSON.stringify({ ok: true }));
|
|
5130
6354
|
} catch (err2) {
|
|
@@ -5157,8 +6381,8 @@ async function startChartServer(opts = {}) {
|
|
|
5157
6381
|
const taggerConfig = JSON.parse(body);
|
|
5158
6382
|
const config2 = loadConfig(reqRoot);
|
|
5159
6383
|
config2.taggers = taggerConfig;
|
|
5160
|
-
const configPath =
|
|
5161
|
-
|
|
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");
|
|
5162
6386
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5163
6387
|
res.end(JSON.stringify({ ok: true }));
|
|
5164
6388
|
} catch (err2) {
|
|
@@ -5228,7 +6452,7 @@ async function startChartServer(opts = {}) {
|
|
|
5228
6452
|
dbDir: !!config2.paths?.dbDir,
|
|
5229
6453
|
srcRoots: !!(config2.paths?.srcRoots && config2.paths.srcRoots.length > 0)
|
|
5230
6454
|
};
|
|
5231
|
-
const relFromRoot = (abs) =>
|
|
6455
|
+
const relFromRoot = (abs) => import_node_path22.default.relative(reqRoot, abs) || ".";
|
|
5232
6456
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5233
6457
|
res.end(JSON.stringify({
|
|
5234
6458
|
projectRoot: reqRoot,
|
|
@@ -5250,19 +6474,19 @@ async function startChartServer(opts = {}) {
|
|
|
5250
6474
|
}
|
|
5251
6475
|
if (req.method === "GET" && url2.pathname === "/api/browse-dir") {
|
|
5252
6476
|
const browsePath = url2.searchParams.get("path") || projectRoot;
|
|
5253
|
-
const abs =
|
|
5254
|
-
const twoUp =
|
|
6477
|
+
const abs = import_node_path22.default.resolve(browsePath);
|
|
6478
|
+
const twoUp = import_node_path22.default.resolve(projectRoot, "..", "..");
|
|
5255
6479
|
if (!abs.startsWith(twoUp)) {
|
|
5256
6480
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
5257
6481
|
res.end(JSON.stringify({ ok: false, error: "Path outside allowed range" }));
|
|
5258
6482
|
return;
|
|
5259
6483
|
}
|
|
5260
6484
|
try {
|
|
5261
|
-
const entries =
|
|
6485
|
+
const entries = import_node_fs20.default.readdirSync(abs, { withFileTypes: true });
|
|
5262
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();
|
|
5263
|
-
const parent = abs !== twoUp ?
|
|
6487
|
+
const parent = abs !== twoUp ? import_node_path22.default.dirname(abs) : null;
|
|
5264
6488
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5265
|
-
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) || "." }));
|
|
5266
6490
|
} catch (err2) {
|
|
5267
6491
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
5268
6492
|
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
@@ -5288,8 +6512,8 @@ async function startChartServer(opts = {}) {
|
|
|
5288
6512
|
const { projects: newProjects } = JSON.parse(body);
|
|
5289
6513
|
const config2 = loadConfig(projectRoot);
|
|
5290
6514
|
config2.projects = newProjects.length > 0 ? newProjects : void 0;
|
|
5291
|
-
const configPath =
|
|
5292
|
-
|
|
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");
|
|
5293
6517
|
projects.length = 0;
|
|
5294
6518
|
if (config2.projects) projects.push(...config2.projects);
|
|
5295
6519
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -5302,7 +6526,7 @@ async function startChartServer(opts = {}) {
|
|
|
5302
6526
|
return;
|
|
5303
6527
|
}
|
|
5304
6528
|
if (url2.pathname !== "/") {
|
|
5305
|
-
const staticPath =
|
|
6529
|
+
const staticPath = import_node_path22.default.join(clientDir, url2.pathname);
|
|
5306
6530
|
if (serveStatic(res, staticPath)) return;
|
|
5307
6531
|
}
|
|
5308
6532
|
serveIndex(res, clientDir);
|
|
@@ -5362,13 +6586,13 @@ function runServeCli(argv) {
|
|
|
5362
6586
|
process.exit(1);
|
|
5363
6587
|
});
|
|
5364
6588
|
}
|
|
5365
|
-
var import_node_http,
|
|
6589
|
+
var import_node_http, import_node_fs20, import_node_path22, MAX_PORT_SCAN, MIME_TYPES;
|
|
5366
6590
|
var init_chart_serve = __esm({
|
|
5367
6591
|
"src/server/chart-serve.ts"() {
|
|
5368
6592
|
"use strict";
|
|
5369
6593
|
import_node_http = __toESM(require("node:http"));
|
|
5370
|
-
|
|
5371
|
-
|
|
6594
|
+
import_node_fs20 = __toESM(require("node:fs"));
|
|
6595
|
+
import_node_path22 = __toESM(require("node:path"));
|
|
5372
6596
|
init_graph();
|
|
5373
6597
|
init_lockfile();
|
|
5374
6598
|
init_config();
|
|
@@ -5390,13 +6614,183 @@ var init_chart_serve = __esm({
|
|
|
5390
6614
|
}
|
|
5391
6615
|
});
|
|
5392
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
|
+
|
|
5393
6787
|
// src/server/graph/core/language-detection.ts
|
|
5394
6788
|
function walkForExtensions(dir, extCounts, depth = 0) {
|
|
5395
6789
|
if (depth > 10) return;
|
|
5396
|
-
if (!(0,
|
|
6790
|
+
if (!(0, import_node_fs21.existsSync)(dir)) return;
|
|
5397
6791
|
let entries;
|
|
5398
6792
|
try {
|
|
5399
|
-
entries = (0,
|
|
6793
|
+
entries = (0, import_node_fs21.readdirSync)(dir, { withFileTypes: true });
|
|
5400
6794
|
} catch {
|
|
5401
6795
|
return;
|
|
5402
6796
|
}
|
|
@@ -5404,9 +6798,9 @@ function walkForExtensions(dir, extCounts, depth = 0) {
|
|
|
5404
6798
|
if (entry.name.startsWith(".") && entry.isDirectory()) continue;
|
|
5405
6799
|
if (entry.isDirectory()) {
|
|
5406
6800
|
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
5407
|
-
walkForExtensions((0,
|
|
6801
|
+
walkForExtensions((0, import_node_path24.join)(dir, entry.name), extCounts, depth + 1);
|
|
5408
6802
|
} else {
|
|
5409
|
-
const ext = (0,
|
|
6803
|
+
const ext = (0, import_node_path24.extname)(entry.name).toLowerCase();
|
|
5410
6804
|
if (ext && EXTENSION_TO_LANGUAGE[ext]) {
|
|
5411
6805
|
extCounts.set(ext, (extCounts.get(ext) ?? 0) + 1);
|
|
5412
6806
|
}
|
|
@@ -5445,12 +6839,12 @@ function detectLanguages(rootDir, supportedLanguages) {
|
|
|
5445
6839
|
});
|
|
5446
6840
|
return results;
|
|
5447
6841
|
}
|
|
5448
|
-
var
|
|
6842
|
+
var import_node_fs21, import_node_path24, EXTENSION_TO_LANGUAGE, IGNORE_DIRS, AUXILIARY_LANGUAGES;
|
|
5449
6843
|
var init_language_detection = __esm({
|
|
5450
6844
|
"src/server/graph/core/language-detection.ts"() {
|
|
5451
6845
|
"use strict";
|
|
5452
|
-
|
|
5453
|
-
|
|
6846
|
+
import_node_fs21 = require("node:fs");
|
|
6847
|
+
import_node_path24 = require("node:path");
|
|
5454
6848
|
EXTENSION_TO_LANGUAGE = {
|
|
5455
6849
|
// Web / Frontend
|
|
5456
6850
|
".ts": "typescript",
|
|
@@ -5563,9 +6957,128 @@ var init_language_detection = __esm({
|
|
|
5563
6957
|
}
|
|
5564
6958
|
});
|
|
5565
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
|
+
|
|
5566
7078
|
// src/server/graph-mcp.ts
|
|
5567
7079
|
var graph_mcp_exports = {};
|
|
5568
7080
|
__export(graph_mcp_exports, {
|
|
7081
|
+
getWatcherHandle: () => getWatcherHandle,
|
|
5569
7082
|
startGraphMcpServer: () => startGraphMcpServer
|
|
5570
7083
|
});
|
|
5571
7084
|
function matchesSearch(node, query) {
|
|
@@ -5585,6 +7098,14 @@ function toMinimal(nodes) {
|
|
|
5585
7098
|
return out;
|
|
5586
7099
|
});
|
|
5587
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
|
+
}
|
|
5588
7109
|
function toCompactNode(n) {
|
|
5589
7110
|
const out = { i: n.id, t: n.type, n: n.name };
|
|
5590
7111
|
const tags = n.tags;
|
|
@@ -5609,6 +7130,8 @@ function toCompactEdges(edges, idx) {
|
|
|
5609
7130
|
t: e.type
|
|
5610
7131
|
};
|
|
5611
7132
|
if (e.label != null) o.l = e.label;
|
|
7133
|
+
const targetLayer = e.target_layer;
|
|
7134
|
+
if (targetLayer != null) o.tl = targetLayer;
|
|
5612
7135
|
return o;
|
|
5613
7136
|
});
|
|
5614
7137
|
}
|
|
@@ -5649,6 +7172,9 @@ function neighborhood(graph, centerId, hops, layer, minimal) {
|
|
|
5649
7172
|
const dstIn = visited.has(e.target) || next.has(e.target);
|
|
5650
7173
|
if (srcIn && dstIn) projectedEdges++;
|
|
5651
7174
|
}
|
|
7175
|
+
for (const c of graph.cross_refs ?? []) {
|
|
7176
|
+
if (visited.has(c.source) || next.has(c.source)) projectedEdges++;
|
|
7177
|
+
}
|
|
5652
7178
|
const perNode = minimal ? EST_CHARS_PER_NODE_MIN[layer] ?? DEFAULT_EST_NODE_MIN : EST_CHARS_PER_NODE_FULL[layer] ?? DEFAULT_EST_NODE_FULL;
|
|
5653
7179
|
const projectedChars = projectedVisited * perNode + projectedEdges * (EST_CHARS_PER_EDGE[layer] ?? DEFAULT_EST_EDGE);
|
|
5654
7180
|
if (projectedChars > NEIGHBORHOOD_BUDGET_CHARS) {
|
|
@@ -5661,8 +7187,9 @@ function neighborhood(graph, centerId, hops, layer, minimal) {
|
|
|
5661
7187
|
if (frontier.size === 0) break;
|
|
5662
7188
|
}
|
|
5663
7189
|
const nodes = graph.nodes.filter((n) => visited.has(n.id));
|
|
5664
|
-
const
|
|
5665
|
-
|
|
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 };
|
|
5666
7193
|
}
|
|
5667
7194
|
function reverseNeighborhood(graph, centerId, hops, direction) {
|
|
5668
7195
|
const center = graph.nodes.find((n) => n.id === centerId);
|
|
@@ -5692,7 +7219,9 @@ function reverseNeighborhood(graph, centerId, hops, direction) {
|
|
|
5692
7219
|
return { nodes: visited, edges };
|
|
5693
7220
|
}
|
|
5694
7221
|
function handleBlastPoints(args) {
|
|
5695
|
-
const
|
|
7222
|
+
const __resolved = resolveOrErr(args);
|
|
7223
|
+
if ("content" in __resolved) return __resolved;
|
|
7224
|
+
const { rootDir } = __resolved;
|
|
5696
7225
|
const nodeId = args.node_id;
|
|
5697
7226
|
const requestedLayer = args.layer;
|
|
5698
7227
|
const hops = args.hops ?? 2;
|
|
@@ -5736,7 +7265,11 @@ function handleBlastPoints(args) {
|
|
|
5736
7265
|
for (const otherLayer of otherLayers) {
|
|
5737
7266
|
const otherGraph = readGraph(rootDir, otherLayer);
|
|
5738
7267
|
if (!otherGraph) continue;
|
|
5739
|
-
|
|
7268
|
+
const candidates = [
|
|
7269
|
+
...otherGraph.edges,
|
|
7270
|
+
...otherGraph.cross_refs ?? []
|
|
7271
|
+
];
|
|
7272
|
+
for (const edge of candidates) {
|
|
5740
7273
|
if (edge.target === nodeId || edge.source === nodeId) {
|
|
5741
7274
|
const dependentId = edge.target === nodeId ? edge.source : edge.target;
|
|
5742
7275
|
if (affected.some((a) => a.id === dependentId)) continue;
|
|
@@ -5763,7 +7296,7 @@ function handleBlastPoints(args) {
|
|
|
5763
7296
|
byHop[String(a.hop)] = (byHop[String(a.hop)] ?? 0) + 1;
|
|
5764
7297
|
if (a.module) modulesSet.add(a.module);
|
|
5765
7298
|
}
|
|
5766
|
-
const crossesLayers = Object.keys(byLayer).
|
|
7299
|
+
const crossesLayers = Object.keys(byLayer).some((l) => l !== targetLayer);
|
|
5767
7300
|
const centerTags = center.tags;
|
|
5768
7301
|
return okJson({
|
|
5769
7302
|
center: {
|
|
@@ -5814,26 +7347,89 @@ function okJson(data) {
|
|
|
5814
7347
|
function err(text) {
|
|
5815
7348
|
return { content: [{ type: "text", text }], isError: true };
|
|
5816
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
|
+
}
|
|
5817
7357
|
async function handleGenerateGraph(args) {
|
|
5818
|
-
const
|
|
7358
|
+
const monorepoRoot = process.cwd();
|
|
5819
7359
|
const layer = args.layer;
|
|
5820
|
-
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);
|
|
5821
7423
|
if (results.length === 0) {
|
|
5822
7424
|
return err(
|
|
5823
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."
|
|
5824
7426
|
);
|
|
5825
7427
|
}
|
|
5826
|
-
const summary = results.map((r) => {
|
|
5827
|
-
const warnings = r.output.warnings.length;
|
|
5828
|
-
return ` ${r.layer}: ${r.nodeCount} nodes, ${r.edgeCount} edges${warnings ? ` (${warnings} warnings)` : ""}`;
|
|
5829
|
-
}).join("\n");
|
|
5830
7428
|
return ok(
|
|
5831
7429
|
`Graph generated successfully.
|
|
5832
7430
|
|
|
5833
7431
|
Layers:
|
|
5834
|
-
${
|
|
5835
|
-
|
|
5836
|
-
Output: .launchsecure/graphs/
|
|
7432
|
+
${formatProjectResult(results, ".")}
|
|
5837
7433
|
|
|
5838
7434
|
Use read_graph with filters (search/type/module/node_id) to query.`
|
|
5839
7435
|
);
|
|
@@ -5916,8 +7512,6 @@ function runReadGraphQueryRaw(rootDir, args) {
|
|
|
5916
7512
|
if (tagKey && tagValue && nodeTags?.[tagKey] !== tagValue) return false;
|
|
5917
7513
|
return true;
|
|
5918
7514
|
});
|
|
5919
|
-
const matchedIds = new Set(matched.map((n) => n.id));
|
|
5920
|
-
const matchedEdges = graph.edges.filter((e) => matchedIds.has(e.source) && matchedIds.has(e.target));
|
|
5921
7515
|
if (matched.length === 0) {
|
|
5922
7516
|
return {
|
|
5923
7517
|
layer,
|
|
@@ -5931,7 +7525,9 @@ function runReadGraphQueryRaw(rootDir, args) {
|
|
|
5931
7525
|
const hasMore = offset + paginatedNodes.length < totalMatched;
|
|
5932
7526
|
const wantEdges = includeEdges ?? false;
|
|
5933
7527
|
const returnedIds = new Set(paginatedNodes.map((n) => n.id));
|
|
5934
|
-
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];
|
|
5935
7531
|
const result = {
|
|
5936
7532
|
layer,
|
|
5937
7533
|
filter: { search, type, module: module_ },
|
|
@@ -5946,7 +7542,14 @@ function runReadGraphQueryRaw(rootDir, args) {
|
|
|
5946
7542
|
result.next_offset = offset + paginatedNodes.length;
|
|
5947
7543
|
}
|
|
5948
7544
|
if (wantEdges) {
|
|
5949
|
-
|
|
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
|
+
}
|
|
5950
7553
|
} else if (returnedEdges.length > 0) {
|
|
5951
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).`;
|
|
5952
7555
|
}
|
|
@@ -5957,12 +7560,13 @@ function runReadGraphQuery(rootDir, args) {
|
|
|
5957
7560
|
return compactResult(raw);
|
|
5958
7561
|
}
|
|
5959
7562
|
function handleReadGraph(args) {
|
|
5960
|
-
const
|
|
7563
|
+
const monorepoRoot = process.cwd();
|
|
5961
7564
|
if (Array.isArray(args.queries)) {
|
|
5962
7565
|
const queries = args.queries;
|
|
5963
7566
|
if (queries.length === 0) {
|
|
5964
7567
|
return err("queries array is empty. Provide at least one query object.");
|
|
5965
7568
|
}
|
|
7569
|
+
const inheritedProject = typeof args.project === "string" ? args.project : void 0;
|
|
5966
7570
|
const results = [];
|
|
5967
7571
|
let cumulativeChars = 0;
|
|
5968
7572
|
let budgetHit = false;
|
|
@@ -5980,7 +7584,15 @@ function handleReadGraph(args) {
|
|
|
5980
7584
|
});
|
|
5981
7585
|
continue;
|
|
5982
7586
|
}
|
|
5983
|
-
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);
|
|
5984
7596
|
const entry = { index: i, query: q, result: r };
|
|
5985
7597
|
const entrySize = JSON.stringify(entry, null, 2).length;
|
|
5986
7598
|
if (cumulativeChars + entrySize > BATCH_BUDGET_CHARS && results.length > 0) {
|
|
@@ -6007,20 +7619,24 @@ function handleReadGraph(args) {
|
|
|
6007
7619
|
results
|
|
6008
7620
|
});
|
|
6009
7621
|
}
|
|
6010
|
-
const
|
|
7622
|
+
const __resolved = resolveOrErr(args);
|
|
7623
|
+
if ("content" in __resolved) return __resolved;
|
|
7624
|
+
const result = runReadGraphQuery(__resolved.rootDir, args);
|
|
6011
7625
|
return okJson(result);
|
|
6012
7626
|
}
|
|
6013
7627
|
function nodeToFilePath(rootDir, layer, nodeId) {
|
|
6014
|
-
if (layer === "ui" || layer === "api") return (0,
|
|
6015
|
-
if (layer === "db") return (0,
|
|
6016
|
-
const withSrc = (0,
|
|
6017
|
-
if ((0,
|
|
6018
|
-
const direct = (0,
|
|
6019
|
-
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;
|
|
6020
7634
|
return null;
|
|
6021
7635
|
}
|
|
6022
7636
|
function handleInspectNode(args) {
|
|
6023
|
-
const
|
|
7637
|
+
const __resolved = resolveOrErr(args);
|
|
7638
|
+
if ("content" in __resolved) return __resolved;
|
|
7639
|
+
const { rootDir } = __resolved;
|
|
6024
7640
|
const layer = args.layer;
|
|
6025
7641
|
const nodeId = args.node_id;
|
|
6026
7642
|
const search = args.search;
|
|
@@ -6044,7 +7660,7 @@ function handleInspectNode(args) {
|
|
|
6044
7660
|
} else {
|
|
6045
7661
|
matched = graph.nodes;
|
|
6046
7662
|
}
|
|
6047
|
-
const allDeepFields = ["elements", "stateVars", "conditions", "variables", "responses", "params"];
|
|
7663
|
+
const allDeepFields = ["elements", "stateVars", "conditions", "variables", "responses", "params", "effects"];
|
|
6048
7664
|
const requestedFields = fields ?? allDeepFields;
|
|
6049
7665
|
let filterRegex = null;
|
|
6050
7666
|
if (filter) {
|
|
@@ -6100,7 +7716,9 @@ function handleInspectNode(args) {
|
|
|
6100
7716
|
});
|
|
6101
7717
|
}
|
|
6102
7718
|
function handleGrepNodes(args) {
|
|
6103
|
-
const
|
|
7719
|
+
const __resolved = resolveOrErr(args);
|
|
7720
|
+
if ("content" in __resolved) return __resolved;
|
|
7721
|
+
const { rootDir } = __resolved;
|
|
6104
7722
|
const pattern = args.pattern;
|
|
6105
7723
|
const layer = args.layer;
|
|
6106
7724
|
if (!pattern) return err("pattern is required");
|
|
@@ -6159,11 +7777,11 @@ function handleGrepNodes(args) {
|
|
|
6159
7777
|
let filesSearched = 0;
|
|
6160
7778
|
let truncated = false;
|
|
6161
7779
|
for (const [filePath, nodeId] of filePaths) {
|
|
6162
|
-
if (!(0,
|
|
7780
|
+
if (!(0, import_node_fs23.existsSync)(filePath)) continue;
|
|
6163
7781
|
filesSearched++;
|
|
6164
7782
|
let content;
|
|
6165
7783
|
try {
|
|
6166
|
-
content = (0,
|
|
7784
|
+
content = (0, import_node_fs23.readFileSync)(filePath, "utf-8");
|
|
6167
7785
|
} catch {
|
|
6168
7786
|
continue;
|
|
6169
7787
|
}
|
|
@@ -6200,11 +7818,61 @@ function handleGrepNodes(args) {
|
|
|
6200
7818
|
truncated
|
|
6201
7819
|
});
|
|
6202
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
|
+
}
|
|
6203
7863
|
function handleChartServerStatus() {
|
|
6204
7864
|
const rootDir = process.cwd();
|
|
6205
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 };
|
|
6206
7874
|
if (!lock) {
|
|
6207
|
-
return okJson({ running: false });
|
|
7875
|
+
return okJson({ running: false, watcher: watcherInfo });
|
|
6208
7876
|
}
|
|
6209
7877
|
return okJson({
|
|
6210
7878
|
running: true,
|
|
@@ -6212,7 +7880,8 @@ function handleChartServerStatus() {
|
|
|
6212
7880
|
port: lock.port,
|
|
6213
7881
|
pid: lock.pid,
|
|
6214
7882
|
cwd: lock.cwd,
|
|
6215
|
-
startedAt: lock.startedAt
|
|
7883
|
+
startedAt: lock.startedAt,
|
|
7884
|
+
watcher: watcherInfo
|
|
6216
7885
|
});
|
|
6217
7886
|
}
|
|
6218
7887
|
function handleStartChartServer(args) {
|
|
@@ -6228,11 +7897,11 @@ function handleStartChartServer(args) {
|
|
|
6228
7897
|
});
|
|
6229
7898
|
}
|
|
6230
7899
|
const entryPath = process.argv[1];
|
|
6231
|
-
const logDir = (0,
|
|
6232
|
-
(0,
|
|
6233
|
-
const logPath = (0,
|
|
6234
|
-
const out = (0,
|
|
6235
|
-
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");
|
|
6236
7905
|
const portArgs = args.port ? ["--port", String(args.port)] : [];
|
|
6237
7906
|
const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
|
|
6238
7907
|
detached: true,
|
|
@@ -6265,7 +7934,9 @@ function handleStopChartServer() {
|
|
|
6265
7934
|
}
|
|
6266
7935
|
}
|
|
6267
7936
|
function handleAddTag(args) {
|
|
6268
|
-
const
|
|
7937
|
+
const __resolved = resolveOrErr(args);
|
|
7938
|
+
if ("content" in __resolved) return __resolved;
|
|
7939
|
+
const { rootDir } = __resolved;
|
|
6269
7940
|
const nodeId = args.node_id;
|
|
6270
7941
|
const key = args.key;
|
|
6271
7942
|
const value = args.value;
|
|
@@ -6287,7 +7958,9 @@ function handleAddTag(args) {
|
|
|
6287
7958
|
return okJson({ ok: true, node_id: nodeId, tag: { [key]: value } });
|
|
6288
7959
|
}
|
|
6289
7960
|
function handleRemoveTag(args) {
|
|
6290
|
-
const
|
|
7961
|
+
const __resolved = resolveOrErr(args);
|
|
7962
|
+
if ("content" in __resolved) return __resolved;
|
|
7963
|
+
const { rootDir } = __resolved;
|
|
6291
7964
|
const nodeId = args.node_id;
|
|
6292
7965
|
const key = args.key;
|
|
6293
7966
|
if (!nodeId) return err("node_id is required");
|
|
@@ -6296,7 +7969,9 @@ function handleRemoveTag(args) {
|
|
|
6296
7969
|
return okJson({ ok: true, node_id: nodeId, removed_key: key });
|
|
6297
7970
|
}
|
|
6298
7971
|
function handleAuditLayer(args) {
|
|
6299
|
-
const
|
|
7972
|
+
const __resolved = resolveOrErr(args);
|
|
7973
|
+
if ("content" in __resolved) return __resolved;
|
|
7974
|
+
const { rootDir } = __resolved;
|
|
6300
7975
|
const layer = args.layer;
|
|
6301
7976
|
const check = args.check;
|
|
6302
7977
|
if (!layer) return err("layer is required");
|
|
@@ -6309,6 +7984,10 @@ function handleAuditLayer(args) {
|
|
|
6309
7984
|
lines.push(`Audit: ${layer}${check ? ` / ${check}` : ""} \u2014 ${totalFindings} findings (${totalErrors} errors, ${totalWarnings} warnings, ${totalInfo} info)`);
|
|
6310
7985
|
lines.push("");
|
|
6311
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
|
+
}
|
|
6312
7991
|
if (report.findings.length === 0) {
|
|
6313
7992
|
lines.push(`\u2713 ${report.check}: no issues found`);
|
|
6314
7993
|
continue;
|
|
@@ -6352,20 +8031,20 @@ function handleDetectProjectStack() {
|
|
|
6352
8031
|
if (ref.type === "references_api") stats.references_api++;
|
|
6353
8032
|
}
|
|
6354
8033
|
}
|
|
6355
|
-
const srcDir = (0,
|
|
6356
|
-
if ((0,
|
|
8034
|
+
const srcDir = (0, import_node_path26.join)(rootDir, "src");
|
|
8035
|
+
if ((0, import_node_fs23.existsSync)(srcDir)) {
|
|
6357
8036
|
const scanDir = (dir) => {
|
|
6358
|
-
if (!(0,
|
|
6359
|
-
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 })) {
|
|
6360
8039
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
6361
|
-
const full = (0,
|
|
8040
|
+
const full = (0, import_node_path26.join)(dir, entry.name);
|
|
6362
8041
|
if (entry.isDirectory()) {
|
|
6363
8042
|
scanDir(full);
|
|
6364
8043
|
continue;
|
|
6365
8044
|
}
|
|
6366
|
-
if (![".ts", ".tsx"].includes((0,
|
|
8045
|
+
if (![".ts", ".tsx"].includes((0, import_node_path26.extname)(entry.name))) continue;
|
|
6367
8046
|
try {
|
|
6368
|
-
const content = (0,
|
|
8047
|
+
const content = (0, import_node_fs23.readFileSync)(full, "utf-8");
|
|
6369
8048
|
const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
|
|
6370
8049
|
if (matches) stats.annotations += matches.length;
|
|
6371
8050
|
} catch {
|
|
@@ -6380,6 +8059,12 @@ function handleDetectProjectStack() {
|
|
|
6380
8059
|
const languages = detectLanguages(rootDir, supportedLanguages);
|
|
6381
8060
|
const unsupported = languages.filter((l) => !l.supported);
|
|
6382
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
|
+
}));
|
|
6383
8068
|
return okJson({
|
|
6384
8069
|
languages,
|
|
6385
8070
|
parsers: parserResults,
|
|
@@ -6402,7 +8087,11 @@ function handleDetectProjectStack() {
|
|
|
6402
8087
|
stats,
|
|
6403
8088
|
...unsupportedHint ? { unsupported_hint: unsupportedHint } : {},
|
|
6404
8089
|
current_config: Object.keys(config).length > 0 ? config : null,
|
|
6405
|
-
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
|
+
}
|
|
6406
8095
|
});
|
|
6407
8096
|
}
|
|
6408
8097
|
function send(msg) {
|
|
@@ -6452,6 +8141,10 @@ async function handleMessage(msg) {
|
|
|
6452
8141
|
respond(id ?? null, handleInspectNode(args));
|
|
6453
8142
|
return;
|
|
6454
8143
|
}
|
|
8144
|
+
if (toolName === "effects_index") {
|
|
8145
|
+
respond(id ?? null, handleEffectsIndex(args));
|
|
8146
|
+
return;
|
|
8147
|
+
}
|
|
6455
8148
|
if (toolName === "chart_server_status") {
|
|
6456
8149
|
respond(id ?? null, handleChartServerStatus());
|
|
6457
8150
|
return;
|
|
@@ -6495,6 +8188,9 @@ async function handleMessage(msg) {
|
|
|
6495
8188
|
respondError(id, -32601, `Method not found: ${method}`);
|
|
6496
8189
|
}
|
|
6497
8190
|
}
|
|
8191
|
+
function getWatcherHandle() {
|
|
8192
|
+
return watcherHandle;
|
|
8193
|
+
}
|
|
6498
8194
|
function startGraphMcpServer() {
|
|
6499
8195
|
process.stdin.setEncoding("utf-8");
|
|
6500
8196
|
let buffer = "";
|
|
@@ -6514,17 +8210,39 @@ function startGraphMcpServer() {
|
|
|
6514
8210
|
}
|
|
6515
8211
|
});
|
|
6516
8212
|
process.stdin.on("end", () => {
|
|
8213
|
+
watcherHandle?.stop();
|
|
6517
8214
|
process.exit(0);
|
|
6518
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
|
+
}
|
|
6519
8237
|
process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
|
|
6520
8238
|
`);
|
|
6521
8239
|
}
|
|
6522
|
-
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;
|
|
6523
8241
|
var init_graph_mcp = __esm({
|
|
6524
8242
|
"src/server/graph-mcp.ts"() {
|
|
6525
8243
|
"use strict";
|
|
6526
|
-
|
|
6527
|
-
|
|
8244
|
+
import_node_fs23 = require("node:fs");
|
|
8245
|
+
import_node_path26 = require("node:path");
|
|
6528
8246
|
import_node_child_process2 = require("node:child_process");
|
|
6529
8247
|
import_node_os2 = require("node:os");
|
|
6530
8248
|
init_graph();
|
|
@@ -6533,6 +8251,7 @@ var init_graph_mcp = __esm({
|
|
|
6533
8251
|
init_parser_registry();
|
|
6534
8252
|
init_language_detection();
|
|
6535
8253
|
init_audit_core();
|
|
8254
|
+
init_projects();
|
|
6536
8255
|
SERVER_INFO = {
|
|
6537
8256
|
name: "launchsecure-graph",
|
|
6538
8257
|
version: "0.0.1"
|
|
@@ -6540,20 +8259,24 @@ var init_graph_mcp = __esm({
|
|
|
6540
8259
|
TOOLS = [
|
|
6541
8260
|
{
|
|
6542
8261
|
name: "generate_graph",
|
|
6543
|
-
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.",
|
|
6544
8263
|
inputSchema: {
|
|
6545
8264
|
type: "object",
|
|
6546
8265
|
properties: {
|
|
6547
8266
|
layer: {
|
|
6548
8267
|
type: "string",
|
|
6549
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."
|
|
6550
8273
|
}
|
|
6551
8274
|
}
|
|
6552
8275
|
}
|
|
6553
8276
|
},
|
|
6554
8277
|
{
|
|
6555
8278
|
name: "read_graph",
|
|
6556
|
-
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.',
|
|
6557
8280
|
inputSchema: {
|
|
6558
8281
|
type: "object",
|
|
6559
8282
|
properties: {
|
|
@@ -6607,7 +8330,7 @@ var init_graph_mcp = __esm({
|
|
|
6607
8330
|
},
|
|
6608
8331
|
queries: {
|
|
6609
8332
|
type: "array",
|
|
6610
|
-
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.",
|
|
6611
8334
|
items: {
|
|
6612
8335
|
type: "object",
|
|
6613
8336
|
properties: {
|
|
@@ -6618,9 +8341,14 @@ var init_graph_mcp = __esm({
|
|
|
6618
8341
|
node_id: { type: "string" },
|
|
6619
8342
|
hops: { type: "number" },
|
|
6620
8343
|
minimal: { type: "boolean" },
|
|
6621
|
-
include_edges: { type: "boolean" }
|
|
8344
|
+
include_edges: { type: "boolean" },
|
|
8345
|
+
project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
|
|
6622
8346
|
}
|
|
6623
8347
|
}
|
|
8348
|
+
},
|
|
8349
|
+
project: {
|
|
8350
|
+
type: "string",
|
|
8351
|
+
description: PROJECT_PARAM_DESCRIPTION
|
|
6624
8352
|
}
|
|
6625
8353
|
}
|
|
6626
8354
|
}
|
|
@@ -6667,16 +8395,17 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
|
|
|
6667
8395
|
case_insensitive: { type: "boolean", description: "Case-insensitive regex. Default false." },
|
|
6668
8396
|
context: { type: "number", description: "Context lines around each match. Default 2." },
|
|
6669
8397
|
max_matches: { type: "number", description: "Max matches to return total. Default 50." },
|
|
6670
|
-
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 }
|
|
6671
8400
|
},
|
|
6672
8401
|
required: ["layer", "pattern"]
|
|
6673
8402
|
}
|
|
6674
8403
|
},
|
|
6675
8404
|
{
|
|
6676
8405
|
name: "inspect_node",
|
|
6677
|
-
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.
|
|
6678
8407
|
|
|
6679
|
-
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)
|
|
6680
8409
|
|
|
6681
8410
|
DO NOT USE FOR: structural queries (use read_graph), content search (use grep_nodes).
|
|
6682
8411
|
|
|
@@ -6699,7 +8428,7 @@ Returns deep fields only \u2014 not structural metadata (use read_graph for that
|
|
|
6699
8428
|
fields: {
|
|
6700
8429
|
type: "array",
|
|
6701
8430
|
items: { type: "string" },
|
|
6702
|
-
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".'
|
|
6703
8432
|
},
|
|
6704
8433
|
filter: {
|
|
6705
8434
|
type: "string",
|
|
@@ -6708,7 +8437,8 @@ Returns deep fields only \u2014 not structural metadata (use read_graph for that
|
|
|
6708
8437
|
case_insensitive: {
|
|
6709
8438
|
type: "boolean",
|
|
6710
8439
|
description: "Case-insensitive filter matching. Default true."
|
|
6711
|
-
}
|
|
8440
|
+
},
|
|
8441
|
+
project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
|
|
6712
8442
|
},
|
|
6713
8443
|
required: ["layer"]
|
|
6714
8444
|
}
|
|
@@ -6744,6 +8474,25 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
6744
8474
|
properties: {}
|
|
6745
8475
|
}
|
|
6746
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
|
+
},
|
|
6747
8496
|
{
|
|
6748
8497
|
name: "detect_project_stack",
|
|
6749
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.",
|
|
@@ -6769,7 +8518,8 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
6769
8518
|
value: {
|
|
6770
8519
|
type: "string",
|
|
6771
8520
|
description: 'Tag value (e.g. "auth", "alice", "true").'
|
|
6772
|
-
}
|
|
8521
|
+
},
|
|
8522
|
+
project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
|
|
6773
8523
|
},
|
|
6774
8524
|
required: ["node_id", "key", "value"]
|
|
6775
8525
|
}
|
|
@@ -6787,7 +8537,8 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
6787
8537
|
key: {
|
|
6788
8538
|
type: "string",
|
|
6789
8539
|
description: "Tag key to remove."
|
|
6790
|
-
}
|
|
8540
|
+
},
|
|
8541
|
+
project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
|
|
6791
8542
|
},
|
|
6792
8543
|
required: ["node_id", "key"]
|
|
6793
8544
|
}
|
|
@@ -6805,7 +8556,8 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
6805
8556
|
check: {
|
|
6806
8557
|
type: "string",
|
|
6807
8558
|
description: "Specific check to run (e.g. 'schema_drift', 'unprotected_routes'). Omit to run all checks for the layer."
|
|
6808
|
-
}
|
|
8559
|
+
},
|
|
8560
|
+
project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
|
|
6809
8561
|
},
|
|
6810
8562
|
required: ["layer"]
|
|
6811
8563
|
}
|
|
@@ -6840,7 +8592,8 @@ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 retu
|
|
|
6840
8592
|
type: "string",
|
|
6841
8593
|
enum: ["reverse", "both"],
|
|
6842
8594
|
description: "'reverse' (default) = only what depends on this node. 'both' = full neighborhood."
|
|
6843
|
-
}
|
|
8595
|
+
},
|
|
8596
|
+
project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
|
|
6844
8597
|
},
|
|
6845
8598
|
required: ["node_id"]
|
|
6846
8599
|
}
|
|
@@ -6862,9 +8615,10 @@ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 retu
|
|
|
6862
8615
|
s: "source_node_index",
|
|
6863
8616
|
d: "target_node_index",
|
|
6864
8617
|
t: "type",
|
|
6865
|
-
l: "label"
|
|
8618
|
+
l: "label",
|
|
8619
|
+
tl: "target_layer (only set on cross-layer edges, e.g. ui\u2192api calls_api)"
|
|
6866
8620
|
},
|
|
6867
|
-
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."
|
|
6868
8622
|
};
|
|
6869
8623
|
COMPACT_NODE_KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
6870
8624
|
"id",
|
|
@@ -6883,7 +8637,8 @@ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 retu
|
|
|
6883
8637
|
"conditions",
|
|
6884
8638
|
"variables",
|
|
6885
8639
|
"responses",
|
|
6886
|
-
"params"
|
|
8640
|
+
"params",
|
|
8641
|
+
"effects"
|
|
6887
8642
|
]);
|
|
6888
8643
|
EST_CHARS_PER_NODE_FULL = {
|
|
6889
8644
|
ui: 300,
|
|
@@ -6904,16 +8659,18 @@ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 retu
|
|
|
6904
8659
|
DEFAULT_EST_NODE_MIN = 150;
|
|
6905
8660
|
DEFAULT_EST_EDGE = 65;
|
|
6906
8661
|
NEIGHBORHOOD_BUDGET_CHARS = 55e3;
|
|
8662
|
+
MAX_FILTER_EDGES = 200;
|
|
6907
8663
|
BATCH_BUDGET_CHARS = 6e4;
|
|
8664
|
+
watcherHandle = null;
|
|
6908
8665
|
}
|
|
6909
8666
|
});
|
|
6910
8667
|
|
|
6911
8668
|
// src/server/graph-mcp-entry.ts
|
|
6912
8669
|
var import_node_child_process3 = require("node:child_process");
|
|
6913
|
-
var
|
|
6914
|
-
var
|
|
8670
|
+
var import_node_fs24 = require("node:fs");
|
|
8671
|
+
var import_node_path27 = __toESM(require("node:path"));
|
|
6915
8672
|
var import_node_os3 = require("node:os");
|
|
6916
|
-
var
|
|
8673
|
+
var import_node_fs25 = require("node:fs");
|
|
6917
8674
|
init_lockfile();
|
|
6918
8675
|
function logStderr(msg) {
|
|
6919
8676
|
process.stderr.write(`[launch-chart] ${msg}
|
|
@@ -6929,11 +8686,11 @@ function maybeAutoServe() {
|
|
|
6929
8686
|
return;
|
|
6930
8687
|
}
|
|
6931
8688
|
try {
|
|
6932
|
-
const logDir =
|
|
6933
|
-
(0,
|
|
6934
|
-
const logPath =
|
|
6935
|
-
const out = (0,
|
|
6936
|
-
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");
|
|
6937
8694
|
const entryPath = process.argv[1];
|
|
6938
8695
|
const child = (0, import_node_child_process3.spawn)(process.execPath, [entryPath, "serve"], {
|
|
6939
8696
|
detached: true,
|
|
@@ -6955,6 +8712,12 @@ async function main() {
|
|
|
6955
8712
|
runServeCli2(argv.slice(1));
|
|
6956
8713
|
return;
|
|
6957
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
|
+
}
|
|
6958
8721
|
maybeAutoServe();
|
|
6959
8722
|
const { startGraphMcpServer: startGraphMcpServer2 } = await Promise.resolve().then(() => (init_graph_mcp(), graph_mcp_exports));
|
|
6960
8723
|
startGraphMcpServer2();
|