@launchsecure/launch-kit 0.0.25 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/dist/beacon/beacon.mjs +1016 -0
- package/dist/beacon/beacon.mjs.map +1 -0
- package/dist/beacon/beacon.umd.js +87 -0
- package/dist/beacon/beacon.umd.js.map +1 -0
- package/dist/beacon/index-DAIDnjfR.mjs +513 -0
- package/dist/beacon/index-DAIDnjfR.mjs.map +1 -0
- package/dist/beacon/types/capture/element.d.ts +3 -0
- package/dist/beacon/types/capture/element.d.ts.map +1 -0
- package/dist/beacon/types/capture/framework.d.ts +3 -0
- package/dist/beacon/types/capture/framework.d.ts.map +1 -0
- package/dist/beacon/types/capture/metadata.d.ts +3 -0
- package/dist/beacon/types/capture/metadata.d.ts.map +1 -0
- package/dist/beacon/types/capture/overlay.d.ts +7 -0
- package/dist/beacon/types/capture/overlay.d.ts.map +1 -0
- package/dist/beacon/types/capture/picker.d.ts +12 -0
- package/dist/beacon/types/capture/picker.d.ts.map +1 -0
- package/dist/beacon/types/capture/screenshot.d.ts +7 -0
- package/dist/beacon/types/capture/screenshot.d.ts.map +1 -0
- package/dist/beacon/types/capture/selector.d.ts +2 -0
- package/dist/beacon/types/capture/selector.d.ts.map +1 -0
- package/dist/beacon/types/element.d.ts +50 -0
- package/dist/beacon/types/element.d.ts.map +1 -0
- package/dist/beacon/types/index.d.ts +4 -0
- package/dist/beacon/types/index.d.ts.map +1 -0
- package/dist/beacon/types/transport/submit.d.ts +3 -0
- package/dist/beacon/types/transport/submit.d.ts.map +1 -0
- package/dist/beacon/types/types.d.ts +88 -0
- package/dist/beacon/types/types.d.ts.map +1 -0
- package/dist/beacon/types/ui/button.d.ts +2 -0
- package/dist/beacon/types/ui/button.d.ts.map +1 -0
- package/dist/beacon/types/ui/drawer.d.ts +31 -0
- package/dist/beacon/types/ui/drawer.d.ts.map +1 -0
- package/dist/beacon/types/ui/icons.d.ts +9 -0
- package/dist/beacon/types/ui/icons.d.ts.map +1 -0
- package/dist/beacon/types/ui/pick-mode-overlay.d.ts +25 -0
- package/dist/beacon/types/ui/pick-mode-overlay.d.ts.map +1 -0
- package/dist/beacon/types/ui/pin-popover.d.ts +14 -0
- package/dist/beacon/types/ui/pin-popover.d.ts.map +1 -0
- package/dist/chart-client/assets/{index-C8ANseEa.js → index-Bk1hawjD.js} +63 -58
- package/dist/chart-client/assets/index-DpaGa3bY.css +1 -0
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/index-Bfel4OQ5.css +32 -0
- package/dist/client/assets/{index-Ds9UP_cj.js → index-eC-WuUWB.js} +58 -58
- package/dist/client/index.html +2 -2
- package/dist/council-client/assets/{index-Dc41S-R2.js → index-Cs_MVXHf.js} +14 -14
- package/dist/council-client/assets/index-P5kMsT5a.css +1 -0
- package/dist/council-client/index.html +2 -2
- package/dist/deck-client/assets/{_baseUniq-2gclQXo7.js → _baseUniq-C2xT_eYu.js} +1 -1
- package/dist/deck-client/assets/{arc-DcMY5Wm0.js → arc-CmVL9pGd.js} +1 -1
- package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-B8iirmmJ.js → architectureDiagram-Q4EWVU46-BSFgdjve.js} +1 -1
- package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-B4JBLjmJ.js → blockDiagram-DXYQGD6D-DuLzscvP.js} +1 -1
- package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-CojrJAk8.js → c4Diagram-AHTNJAMY-CfCJB8eY.js} +1 -1
- package/dist/deck-client/assets/channel-B4aNO8ZB.js +1 -0
- package/dist/deck-client/assets/{chunk-4BX2VUAB-Bmb_BMDo.js → chunk-4BX2VUAB-DxmLYTWZ.js} +1 -1
- package/dist/deck-client/assets/{chunk-4TB4RGXK-CumBy8qe.js → chunk-4TB4RGXK-CCnf7GFE.js} +1 -1
- package/dist/deck-client/assets/{chunk-55IACEB6-Ka8Hb1wD.js → chunk-55IACEB6-Db9DApcj.js} +1 -1
- package/dist/deck-client/assets/{chunk-EDXVE4YY-B3sIPiQo.js → chunk-EDXVE4YY-DmYDq8ZI.js} +1 -1
- package/dist/deck-client/assets/{chunk-FMBD7UC4-C1tYkaqu.js → chunk-FMBD7UC4-BGhUlF20.js} +1 -1
- package/dist/deck-client/assets/{chunk-OYMX7WX6-D7Wacbky.js → chunk-OYMX7WX6-CpEnicQZ.js} +1 -1
- package/dist/deck-client/assets/{chunk-QZHKN3VN-ChXI0vO3.js → chunk-QZHKN3VN-Doa7LKwf.js} +1 -1
- package/dist/deck-client/assets/{chunk-YZCP3GAM-BXhiqf8u.js → chunk-YZCP3GAM-CpkIlH6V.js} +1 -1
- package/dist/deck-client/assets/classDiagram-6PBFFD2Q-BHTI0yWz.js +1 -0
- package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-BHTI0yWz.js +1 -0
- package/dist/deck-client/assets/clone-HduFm7qU.js +1 -0
- package/dist/deck-client/assets/{cose-bilkent-S5V4N54A-Bqp3p68D.js → cose-bilkent-S5V4N54A-Bkh8Bfcb.js} +1 -1
- package/dist/deck-client/assets/{dagre-KV5264BT-BS-rtyhZ.js → dagre-KV5264BT-Bp0XpTgH.js} +1 -1
- package/dist/deck-client/assets/{diagram-5BDNPKRD-BIrj9YGI.js → diagram-5BDNPKRD-ZHiyGYPQ.js} +1 -1
- package/dist/deck-client/assets/{diagram-G4DWMVQ6-noHWPIg4.js → diagram-G4DWMVQ6-BW-Q8_H5.js} +1 -1
- package/dist/deck-client/assets/{diagram-MMDJMWI5-C2qHxvqV.js → diagram-MMDJMWI5-6I3LTafu.js} +1 -1
- package/dist/deck-client/assets/{diagram-TYMM5635-BytnGQr-.js → diagram-TYMM5635-CyM5YK28.js} +1 -1
- package/dist/deck-client/assets/{erDiagram-SMLLAGMA-BfK5m2YQ.js → erDiagram-SMLLAGMA-CjNxVJHk.js} +1 -1
- package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-Cq925G1Z.js → flowDiagram-DWJPFMVM-BDQHuAJR.js} +1 -1
- package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-DhhHPAmj.js → ganttDiagram-T4ZO3ILL-B7MnkpbP.js} +1 -1
- package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-B3Lc0h9q.js → gitGraphDiagram-UUTBAWPF-C9dZAcYD.js} +1 -1
- package/dist/deck-client/assets/{graph-RTawgVWm.js → graph-CjdBnzUy.js} +1 -1
- package/dist/deck-client/assets/{index-BfIfJXmS.js → index-DeIVPW63.js} +68 -68
- package/dist/deck-client/assets/index-LKZDAS9S.css +1 -0
- package/dist/deck-client/assets/{infoDiagram-42DDH7IO-BlR584kX.js → infoDiagram-42DDH7IO-C7d3iRC3.js} +1 -1
- package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-DygKoNGY.js → ishikawaDiagram-UXIWVN3A-BcYGKj09.js} +1 -1
- package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-BnaiYp9N.js → journeyDiagram-VCZTEJTY-DqFlRrOL.js} +1 -1
- package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-BQBUBzJC.js → kanban-definition-6JOO6SKY-BJhPp1NR.js} +1 -1
- package/dist/deck-client/assets/{layout-DeZ8HI1T.js → layout-DIeS6GvK.js} +1 -1
- package/dist/deck-client/assets/{linear-C6roLi_9.js → linear-He_yJy5H.js} +1 -1
- package/dist/deck-client/assets/{min-CbUksbuI.js → min-DQ6Kx06t.js} +1 -1
- package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-iNxV62yN.js → mindmap-definition-QFDTVHPH-sQ62L8T2.js} +1 -1
- package/dist/deck-client/assets/{pieDiagram-DEJITSTG-DHVA0jaG.js → pieDiagram-DEJITSTG-BqCWmU2K.js} +1 -1
- package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-DBeKKLUQ.js → quadrantDiagram-34T5L4WZ-rQ1TJOoe.js} +1 -1
- package/dist/deck-client/assets/{requirementDiagram-MS252O5E-CBwITx7p.js → requirementDiagram-MS252O5E-BO2MPBOM.js} +1 -1
- package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-BtE-1YTU.js → sankeyDiagram-XADWPNL6-BgsHEVex.js} +1 -1
- package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-DN96yPP2.js → sequenceDiagram-FGHM5R23-B3j1yMLU.js} +1 -1
- package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-VUkKC2uJ.js → stateDiagram-FHFEXIEX-C8jFlZou.js} +1 -1
- package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-BoqepHW0.js +1 -0
- package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-oUeZhRns.js → timeline-definition-GMOUNBTQ-tM-qo4Zk.js} +1 -1
- package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-D87fK90n.js → vennDiagram-DHZGUBPP-B0-6kOEu.js} +1 -1
- package/dist/deck-client/assets/wardley-RL74JXVD-HpBk07P-.js +162 -0
- package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-Ca_i0QRA.js → wardleyDiagram-NUSXRM2D-BkA1NLDE.js} +1 -1
- package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-CUOJVIvq.js → xychartDiagram-5P7HB3ND-CEKGSuI-.js} +1 -1
- package/dist/deck-client/index.html +2 -2
- package/dist/server/chart-serve.js +990 -116
- package/dist/server/cli.js +28413 -6982
- package/dist/server/council-entry.js +0 -0
- package/dist/server/deck-mcp-entry.js +332 -3
- package/dist/server/deck-serve.js +288 -0
- package/dist/server/fb-wizard.js +0 -0
- package/dist/server/graph/queries/classify.scm +8 -0
- package/dist/server/graph/queries/exports.scm +7 -0
- package/dist/server/graph-mcp-entry.js +1639 -197
- package/dist/server/recall-entry.js +1112 -0
- package/package.json +47 -21
- package/dist/chart-client/assets/index--120d9P9.css +0 -1
- package/dist/client/assets/index-Bf8zdL3x.css +0 -32
- package/dist/council-client/assets/index-CofZh7pS.css +0 -1
- package/dist/deck-client/assets/channel-ERh5jKXV.js +0 -1
- package/dist/deck-client/assets/classDiagram-6PBFFD2Q-CMi1Gaev.js +0 -1
- package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-CMi1Gaev.js +0 -1
- package/dist/deck-client/assets/clone-DfWhlD4X.js +0 -1
- package/dist/deck-client/assets/index-765AIQ9z.css +0 -1
- package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-CA0IjulK.js +0 -1
- package/dist/deck-client/assets/wardley-RL74JXVD-DYbYcpDp.js +0 -162
- package/dist/server/deck-server/deck-mcp-entry.js +0 -1789
- package/dist/server/deck-server/deck-serve.js +0 -1275
- package/dist/server/server/chart-serve.js +0 -4643
- package/dist/server/server/cli.js +0 -13360
- package/dist/server/server/fb-wizard.js +0 -136
- package/dist/server/server/graph-mcp-entry.js +0 -6776
|
@@ -57,17 +57,20 @@ var init_config = __esm({
|
|
|
57
57
|
// src/server/graph/core/ts-extractor.ts
|
|
58
58
|
var ts_extractor_exports = {};
|
|
59
59
|
__export(ts_extractor_exports, {
|
|
60
|
+
ParseCascadeError: () => ParseCascadeError,
|
|
60
61
|
classifyFile: () => classifyFile,
|
|
61
62
|
classifyRouteAgainstMiddleware: () => classifyRouteAgainstMiddleware,
|
|
62
63
|
createQuery: () => createQuery,
|
|
63
64
|
extractAuthWrappersTS: () => extractAuthWrappersTS,
|
|
64
65
|
extractDbCallsTS: () => extractDbCallsTS,
|
|
65
66
|
extractDeep: () => extractDeep,
|
|
67
|
+
extractEffects: () => extractEffects,
|
|
66
68
|
extractMiddlewareAuthTS: () => extractMiddlewareAuthTS,
|
|
67
69
|
initTreeSitter: () => initTreeSitter,
|
|
68
70
|
middlewarePatternToRegex: () => middlewarePatternToRegex,
|
|
69
71
|
parseCodeTS: () => parseCodeTS,
|
|
70
72
|
parseFileTS: () => parseFileTS,
|
|
73
|
+
parseSource: () => parseSource,
|
|
71
74
|
setExtractorConfig: () => setExtractorConfig
|
|
72
75
|
});
|
|
73
76
|
async function initTreeSitter() {
|
|
@@ -101,8 +104,38 @@ function getQuery(name) {
|
|
|
101
104
|
}
|
|
102
105
|
function parseSource(absPath) {
|
|
103
106
|
ensureInit();
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
let content;
|
|
108
|
+
try {
|
|
109
|
+
const stat = (0, import_node_fs4.statSync)(absPath);
|
|
110
|
+
if (stat.size > MAX_PARSEABLE_BYTES) {
|
|
111
|
+
process.stderr.write(`[lc-extractor] skipping ${absPath}: ${stat.size} bytes exceeds max ${MAX_PARSEABLE_BYTES}
|
|
112
|
+
`);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
content = (0, import_node_fs4.readFileSync)(absPath, "utf-8");
|
|
116
|
+
} catch (e) {
|
|
117
|
+
process.stderr.write(`[lc-extractor] read failed for ${absPath}: ${e instanceof Error ? e.message : String(e)}
|
|
118
|
+
`);
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const tree = parserInstance.parse(content);
|
|
123
|
+
consecutiveParseFailures = 0;
|
|
124
|
+
return tree;
|
|
125
|
+
} catch (e) {
|
|
126
|
+
consecutiveParseFailures++;
|
|
127
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
128
|
+
process.stderr.write(
|
|
129
|
+
`[lc-extractor] parse failed for ${absPath}: ${msg} (consecutive failures: ${consecutiveParseFailures}/${MAX_CONSECUTIVE_PARSE_FAILURES})
|
|
130
|
+
`
|
|
131
|
+
);
|
|
132
|
+
if (consecutiveParseFailures >= MAX_CONSECUTIVE_PARSE_FAILURES) {
|
|
133
|
+
const tripCount = consecutiveParseFailures;
|
|
134
|
+
consecutiveParseFailures = 0;
|
|
135
|
+
throw new ParseCascadeError(absPath, tripCount);
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
106
139
|
}
|
|
107
140
|
function parseCodeTS(code) {
|
|
108
141
|
ensureInit();
|
|
@@ -142,8 +175,20 @@ function childrenOfType(node, type) {
|
|
|
142
175
|
function childOfType(node, type) {
|
|
143
176
|
return node.children.find((n) => n.type === type);
|
|
144
177
|
}
|
|
178
|
+
function emptyParsedFile(absPath) {
|
|
179
|
+
return {
|
|
180
|
+
name: absPath.split("/").pop() ?? absPath,
|
|
181
|
+
exports: [],
|
|
182
|
+
imports: [],
|
|
183
|
+
reExports: [],
|
|
184
|
+
jsxElements: /* @__PURE__ */ new Set(),
|
|
185
|
+
navigations: [],
|
|
186
|
+
fetchCalls: []
|
|
187
|
+
};
|
|
188
|
+
}
|
|
145
189
|
function parseFileTS(absPath) {
|
|
146
190
|
const tree = parseSource(absPath);
|
|
191
|
+
if (!tree) return emptyParsedFile(absPath);
|
|
147
192
|
const root = tree.rootNode;
|
|
148
193
|
const imports = [];
|
|
149
194
|
const importStatements = childrenOfType(root, "import_statement");
|
|
@@ -338,6 +383,7 @@ function parseFileTS(absPath) {
|
|
|
338
383
|
}
|
|
339
384
|
function extractDbCallsTS(absPath) {
|
|
340
385
|
const tree = parseSource(absPath);
|
|
386
|
+
if (!tree) return [];
|
|
341
387
|
const root = tree.rootNode;
|
|
342
388
|
const dbQuery = getQuery("db-calls");
|
|
343
389
|
const matches = dbQuery.matches(root);
|
|
@@ -398,7 +444,9 @@ function classifyFile(absPath) {
|
|
|
398
444
|
const fileName = require("path").basename(absPath);
|
|
399
445
|
if (fileName.includes(".test.") || fileName.includes(".spec.") || fileName.includes("__test")) return "test";
|
|
400
446
|
if (fileName.includes(".stories.")) return "story";
|
|
447
|
+
if (fileName === "middleware.ts" || fileName === "middleware.tsx") return "middleware";
|
|
401
448
|
const tree = parseSource(absPath);
|
|
449
|
+
if (!tree) return "util";
|
|
402
450
|
const root = tree.rootNode;
|
|
403
451
|
const classifyQuery = getQuery("classify");
|
|
404
452
|
const captures = classifyQuery.captures(root);
|
|
@@ -417,6 +465,7 @@ function classifyFile(absPath) {
|
|
|
417
465
|
}
|
|
418
466
|
function extractAuthWrappersTS(absPath) {
|
|
419
467
|
const tree = parseSource(absPath);
|
|
468
|
+
if (!tree) return /* @__PURE__ */ new Set();
|
|
420
469
|
const root = tree.rootNode;
|
|
421
470
|
const wrapperQuery = getQuery("wrappers");
|
|
422
471
|
const matches = wrapperQuery.matches(root);
|
|
@@ -460,6 +509,9 @@ function extractAuthWrappersTS(absPath) {
|
|
|
460
509
|
return wrappers;
|
|
461
510
|
}
|
|
462
511
|
function inferIntentFromName(name) {
|
|
512
|
+
if (TRUST_AS_PROTECT_KEYS.has(name)) {
|
|
513
|
+
return { intent: "protect", hint: `Next.js config.${name} \u2014 paths matched run through middleware` };
|
|
514
|
+
}
|
|
463
515
|
for (const re of EXEMPT_NAME_PATTERNS) {
|
|
464
516
|
if (re.test(name)) return { intent: "exempt", hint: `name "${name}" matches /${re.source}/` };
|
|
465
517
|
}
|
|
@@ -536,6 +588,7 @@ function detectFallthroughProtect(root) {
|
|
|
536
588
|
function extractMiddlewareAuthTS(absPath) {
|
|
537
589
|
if (!require("node:fs").existsSync(absPath)) return null;
|
|
538
590
|
const tree = parseSource(absPath);
|
|
591
|
+
if (!tree) return null;
|
|
539
592
|
const root = tree.rootNode;
|
|
540
593
|
const matchers = [];
|
|
541
594
|
for (const stmt of root.children) {
|
|
@@ -579,6 +632,16 @@ function extractMiddlewareAuthTS(absPath) {
|
|
|
579
632
|
};
|
|
580
633
|
}
|
|
581
634
|
function middlewarePatternToRegex(pattern) {
|
|
635
|
+
const negLookaheadMatch = /^(\/?)\(\(\?\!([^)]+)\)(\.\*|\.\+)\)$/.exec(pattern);
|
|
636
|
+
if (negLookaheadMatch) {
|
|
637
|
+
const [, lead, altsRaw, body] = negLookaheadMatch;
|
|
638
|
+
const escapedAlts = altsRaw.split("|").map((alt) => alt.trim().replace(/[.+*?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
639
|
+
try {
|
|
640
|
+
return new RegExp(`^${lead}(?!${escapedAlts})${body}$`);
|
|
641
|
+
} catch {
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
582
645
|
if (/\(\?\!/.test(pattern)) return null;
|
|
583
646
|
if (pattern.startsWith("(")) return null;
|
|
584
647
|
let src = "^";
|
|
@@ -636,8 +699,163 @@ function classifyRouteAgainstMiddleware(routePath, info) {
|
|
|
636
699
|
function trunc(s, max = 120) {
|
|
637
700
|
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
638
701
|
}
|
|
702
|
+
function dedup(arr) {
|
|
703
|
+
return Array.from(new Set(arr));
|
|
704
|
+
}
|
|
705
|
+
function firstStringArg(callExpr) {
|
|
706
|
+
const args = callExpr.childForFieldName("arguments");
|
|
707
|
+
if (!args) return null;
|
|
708
|
+
for (const c of args.namedChildren) {
|
|
709
|
+
if (c.type === "string") {
|
|
710
|
+
const frag = c.namedChildren.find((cc) => cc.type === "string_fragment");
|
|
711
|
+
return frag ? frag.text : c.text.replace(/^['"`]|['"`]$/g, "");
|
|
712
|
+
}
|
|
713
|
+
if (c.type === "template_string") {
|
|
714
|
+
return c.text.replace(/^`|`$/g, "");
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
function isFunctionLike(node) {
|
|
720
|
+
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";
|
|
721
|
+
}
|
|
722
|
+
function stringLiteralValue(n) {
|
|
723
|
+
if (n.type !== "string") return null;
|
|
724
|
+
const frag = n.namedChildren.find((c) => c.type === "string_fragment");
|
|
725
|
+
if (frag) return frag.text;
|
|
726
|
+
return n.text.replace(/^['"`]|['"`]$/g, "");
|
|
727
|
+
}
|
|
728
|
+
function nthStringArg(callExpr, idx) {
|
|
729
|
+
const args = callExpr.childForFieldName("arguments");
|
|
730
|
+
if (!args) return null;
|
|
731
|
+
const child = args.namedChildren[idx];
|
|
732
|
+
return child ? stringLiteralValue(child) : null;
|
|
733
|
+
}
|
|
734
|
+
function objectArgIdValues(callExpr) {
|
|
735
|
+
const args = callExpr.childForFieldName("arguments");
|
|
736
|
+
if (!args) return [];
|
|
737
|
+
const out = [];
|
|
738
|
+
for (const arg of args.namedChildren) {
|
|
739
|
+
if (arg.type !== "object") continue;
|
|
740
|
+
for (const pair of arg.namedChildren) {
|
|
741
|
+
if (pair.type !== "pair") continue;
|
|
742
|
+
const key = pair.childForFieldName("key");
|
|
743
|
+
const val = pair.childForFieldName("value");
|
|
744
|
+
if (!key || !val) continue;
|
|
745
|
+
const keyText = key.type === "property_identifier" ? key.text : stringLiteralValue(key) ?? key.text;
|
|
746
|
+
if (keyText !== "id") continue;
|
|
747
|
+
const strVal = stringLiteralValue(val);
|
|
748
|
+
if (strVal) out.push(strVal);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return out;
|
|
752
|
+
}
|
|
753
|
+
function extractEffects(node, moduleOnly = false) {
|
|
754
|
+
const calls = [];
|
|
755
|
+
const dom_writes = [];
|
|
756
|
+
const subscribes = [];
|
|
757
|
+
const timers = [];
|
|
758
|
+
const persists = [];
|
|
759
|
+
const fetches = [];
|
|
760
|
+
const globals = [];
|
|
761
|
+
const dom_ids = [];
|
|
762
|
+
const window_events = [];
|
|
763
|
+
const storage_keys = [];
|
|
764
|
+
const fetch_urls = [];
|
|
765
|
+
function visit(n, depth) {
|
|
766
|
+
if (moduleOnly && depth > 0 && isFunctionLike(n)) return;
|
|
767
|
+
if (n.type === "call_expression") {
|
|
768
|
+
const fn = n.childForFieldName("function");
|
|
769
|
+
if (fn) {
|
|
770
|
+
const fnText = fn.text;
|
|
771
|
+
calls.push(fnText);
|
|
772
|
+
for (const id of objectArgIdValues(n)) dom_ids.push(id);
|
|
773
|
+
if (fn.type === "identifier") {
|
|
774
|
+
if (TIMER_FNS.has(fnText)) timers.push(fnText);
|
|
775
|
+
if (fnText === "fetch") {
|
|
776
|
+
const url = firstStringArg(n);
|
|
777
|
+
fetches.push(url ? `fetch("${url}")` : "fetch(...)");
|
|
778
|
+
if (url) fetch_urls.push(url);
|
|
779
|
+
}
|
|
780
|
+
} else if (fn.type === "member_expression") {
|
|
781
|
+
const obj = fn.childForFieldName("object");
|
|
782
|
+
const prop = fn.childForFieldName("property");
|
|
783
|
+
const objText = obj?.text ?? "";
|
|
784
|
+
const propText = prop?.text ?? "";
|
|
785
|
+
if (STORAGE_OBJECTS.has(objText)) {
|
|
786
|
+
const key = firstStringArg(n);
|
|
787
|
+
persists.push(key ? `${objText}.${propText}("${key}")` : `${objText}.${propText}(\u2026)`);
|
|
788
|
+
if (key) storage_keys.push(key);
|
|
789
|
+
} else if (propText === "addEventListener") {
|
|
790
|
+
const event = firstStringArg(n);
|
|
791
|
+
subscribes.push(event ? `${objText}.addEventListener("${event}")` : `${objText}.addEventListener(\u2026)`);
|
|
792
|
+
if (event && (objText === "window" || objText === "document")) {
|
|
793
|
+
window_events.push(`${objText}:${event}`);
|
|
794
|
+
}
|
|
795
|
+
} else if (propText === "removeEventListener") {
|
|
796
|
+
const event = firstStringArg(n);
|
|
797
|
+
subscribes.push(event ? `${objText}.removeEventListener("${event}")` : `${objText}.removeEventListener(\u2026)`);
|
|
798
|
+
} else if (propText === "subscribe" || propText === "on") {
|
|
799
|
+
subscribes.push(`${objText}.${propText}(\u2026)`);
|
|
800
|
+
} else if (DOM_METHOD_NAMES.has(propText)) {
|
|
801
|
+
dom_writes.push(`${objText}.${propText}(\u2026)`);
|
|
802
|
+
if (propText === "setAttribute" && nthStringArg(n, 0) === "id") {
|
|
803
|
+
const idVal = nthStringArg(n, 1);
|
|
804
|
+
if (idVal) dom_ids.push(idVal);
|
|
805
|
+
}
|
|
806
|
+
} else if (objText.endsWith(".classList") && CLASSLIST_METHODS.has(propText)) {
|
|
807
|
+
dom_writes.push(`${objText}.${propText}(\u2026)`);
|
|
808
|
+
} else if (objText === "history" && HISTORY_METHODS.has(propText)) {
|
|
809
|
+
globals.push(`history.${propText}(\u2026)`);
|
|
810
|
+
} else if (objText === "location" && LOCATION_METHODS.has(propText)) {
|
|
811
|
+
globals.push(`location.${propText}(\u2026)`);
|
|
812
|
+
} else if (objText === "window" && propText === "matchMedia") {
|
|
813
|
+
globals.push("window.matchMedia(\u2026)");
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
} else if (n.type === "assignment_expression") {
|
|
818
|
+
const lhs = n.children.find((c) => c.type !== "=");
|
|
819
|
+
const rhs = n.childForFieldName("right");
|
|
820
|
+
if (lhs?.type === "member_expression") {
|
|
821
|
+
const prop = lhs.childForFieldName("property");
|
|
822
|
+
const propText = prop?.text ?? "";
|
|
823
|
+
const lhsText = lhs.text;
|
|
824
|
+
if (lhsText.startsWith("document.cookie")) {
|
|
825
|
+
persists.push("document.cookie =");
|
|
826
|
+
} else if (lhsText.startsWith("document.title") || lhsText.startsWith("location.")) {
|
|
827
|
+
globals.push(`${lhsText} =`);
|
|
828
|
+
} else if (ASSIGN_DOM_PROPS.has(propText) || lhsText.includes(".style.") || lhsText.includes(".dataset.")) {
|
|
829
|
+
dom_writes.push(`${lhsText} =`);
|
|
830
|
+
if (propText === "id" && rhs) {
|
|
831
|
+
const idVal = stringLiteralValue(rhs);
|
|
832
|
+
if (idVal) dom_ids.push(idVal);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
for (const child of n.children) visit(child, depth + 1);
|
|
838
|
+
}
|
|
839
|
+
visit(node, 0);
|
|
840
|
+
const out = {};
|
|
841
|
+
if (calls.length) out.calls = dedup(calls);
|
|
842
|
+
if (dom_writes.length) out.dom_writes = dedup(dom_writes);
|
|
843
|
+
if (subscribes.length) out.subscribes = dedup(subscribes);
|
|
844
|
+
if (timers.length) out.timers = dedup(timers);
|
|
845
|
+
if (persists.length) out.persists = dedup(persists);
|
|
846
|
+
if (fetches.length) out.fetches = dedup(fetches);
|
|
847
|
+
if (globals.length) out.globals = dedup(globals);
|
|
848
|
+
if (dom_ids.length) out.dom_ids = dedup(dom_ids);
|
|
849
|
+
if (window_events.length) out.window_events = dedup(window_events);
|
|
850
|
+
if (storage_keys.length) out.storage_keys = dedup(storage_keys);
|
|
851
|
+
if (fetch_urls.length) out.fetch_urls = dedup(fetch_urls);
|
|
852
|
+
return out;
|
|
853
|
+
}
|
|
639
854
|
function extractDeep(absPath) {
|
|
640
855
|
const tree = parseSource(absPath);
|
|
856
|
+
if (!tree) {
|
|
857
|
+
return { elements: [], stateVars: [], conditions: [], variables: [], responses: [], params: [] };
|
|
858
|
+
}
|
|
641
859
|
const root = tree.rootNode;
|
|
642
860
|
const elements = [];
|
|
643
861
|
const elQuery = getQuery("deep/jsx-semantic");
|
|
@@ -730,12 +948,18 @@ function extractDeep(absPath) {
|
|
|
730
948
|
const caps = captureMap(m);
|
|
731
949
|
const declNode = m.captures.find((c) => c.name === "var.decl" || c.name === "var.destructured" || c.name === "var.array")?.node;
|
|
732
950
|
const kind = declNode?.children.find((n) => n.type === "const" || n.type === "let" || n.type === "var")?.type ?? "const";
|
|
951
|
+
const initNode = m.captures.find((c) => c.name === "var.init")?.node;
|
|
733
952
|
if (caps["var.name"] && caps["var.init"]) {
|
|
734
|
-
|
|
953
|
+
const variable = {
|
|
735
954
|
name: caps["var.name"],
|
|
736
955
|
kind,
|
|
737
956
|
init: trunc(caps["var.init"], 100)
|
|
738
|
-
}
|
|
957
|
+
};
|
|
958
|
+
if (initNode && isFunctionLike(initNode)) {
|
|
959
|
+
const eff = extractEffects(initNode);
|
|
960
|
+
if (Object.keys(eff).length > 0) variable.effects = eff;
|
|
961
|
+
}
|
|
962
|
+
variables.push(variable);
|
|
739
963
|
}
|
|
740
964
|
if (caps["var.destructured.obj"]) {
|
|
741
965
|
variables.push({
|
|
@@ -787,9 +1011,23 @@ function extractDeep(absPath) {
|
|
|
787
1011
|
params.push({ name: caps["param.body"], source: "body" });
|
|
788
1012
|
}
|
|
789
1013
|
}
|
|
790
|
-
|
|
1014
|
+
const fileEffects = extractEffects(
|
|
1015
|
+
root,
|
|
1016
|
+
/* moduleOnly */
|
|
1017
|
+
false
|
|
1018
|
+
);
|
|
1019
|
+
const hasEffects = Object.keys(fileEffects).length > 0;
|
|
1020
|
+
return {
|
|
1021
|
+
elements,
|
|
1022
|
+
stateVars,
|
|
1023
|
+
conditions,
|
|
1024
|
+
variables,
|
|
1025
|
+
responses,
|
|
1026
|
+
params,
|
|
1027
|
+
...hasEffects ? { effects: fileEffects } : {}
|
|
1028
|
+
};
|
|
791
1029
|
}
|
|
792
|
-
var import_node_fs4, import_node_path4, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, SUPABASE_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods, INLINE_AUTH_IMPORTS, EXEMPT_NAME_PATTERNS, PROTECT_NAME_PATTERNS;
|
|
1030
|
+
var import_node_fs4, import_node_path4, 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;
|
|
793
1031
|
var init_ts_extractor = __esm({
|
|
794
1032
|
"src/server/graph/core/ts-extractor.ts"() {
|
|
795
1033
|
"use strict";
|
|
@@ -802,6 +1040,19 @@ var init_ts_extractor = __esm({
|
|
|
802
1040
|
return (0, import_node_path4.join)((0, import_node_path4.dirname)(__filename), "graph", "queries");
|
|
803
1041
|
})();
|
|
804
1042
|
queryCache = /* @__PURE__ */ new Map();
|
|
1043
|
+
MAX_PARSEABLE_BYTES = 2 * 1024 * 1024;
|
|
1044
|
+
MAX_CONSECUTIVE_PARSE_FAILURES = 10;
|
|
1045
|
+
consecutiveParseFailures = 0;
|
|
1046
|
+
ParseCascadeError = class extends Error {
|
|
1047
|
+
constructor(lastPath, failureCount) {
|
|
1048
|
+
super(
|
|
1049
|
+
`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.`
|
|
1050
|
+
);
|
|
1051
|
+
this.lastPath = lastPath;
|
|
1052
|
+
this.failureCount = failureCount;
|
|
1053
|
+
this.name = "ParseCascadeError";
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
805
1056
|
PRISMA_MUTATION_METHODS_BUILTIN = [
|
|
806
1057
|
"create",
|
|
807
1058
|
"createMany",
|
|
@@ -851,6 +1102,49 @@ var init_ts_extractor = __esm({
|
|
|
851
1102
|
/^admin_?routes?/i,
|
|
852
1103
|
/^secured/i
|
|
853
1104
|
];
|
|
1105
|
+
TRUST_AS_PROTECT_KEYS = /* @__PURE__ */ new Set(["matcher", "matchers"]);
|
|
1106
|
+
TIMER_FNS = /* @__PURE__ */ new Set([
|
|
1107
|
+
"setInterval",
|
|
1108
|
+
"setTimeout",
|
|
1109
|
+
"clearInterval",
|
|
1110
|
+
"clearTimeout",
|
|
1111
|
+
"requestAnimationFrame",
|
|
1112
|
+
"cancelAnimationFrame",
|
|
1113
|
+
"queueMicrotask"
|
|
1114
|
+
]);
|
|
1115
|
+
DOM_METHOD_NAMES = /* @__PURE__ */ new Set([
|
|
1116
|
+
"setAttribute",
|
|
1117
|
+
"removeAttribute",
|
|
1118
|
+
"appendChild",
|
|
1119
|
+
"removeChild",
|
|
1120
|
+
"replaceChildren",
|
|
1121
|
+
"replaceWith",
|
|
1122
|
+
"insertBefore",
|
|
1123
|
+
"append",
|
|
1124
|
+
"prepend",
|
|
1125
|
+
"remove",
|
|
1126
|
+
"before",
|
|
1127
|
+
"after",
|
|
1128
|
+
"insertAdjacentHTML",
|
|
1129
|
+
"insertAdjacentElement"
|
|
1130
|
+
]);
|
|
1131
|
+
CLASSLIST_METHODS = /* @__PURE__ */ new Set(["add", "remove", "toggle", "replace"]);
|
|
1132
|
+
STORAGE_OBJECTS = /* @__PURE__ */ new Set(["localStorage", "sessionStorage"]);
|
|
1133
|
+
HISTORY_METHODS = /* @__PURE__ */ new Set(["pushState", "replaceState", "back", "forward", "go"]);
|
|
1134
|
+
LOCATION_METHODS = /* @__PURE__ */ new Set(["assign", "replace", "reload"]);
|
|
1135
|
+
ASSIGN_DOM_PROPS = /* @__PURE__ */ new Set([
|
|
1136
|
+
"textContent",
|
|
1137
|
+
"innerHTML",
|
|
1138
|
+
"innerText",
|
|
1139
|
+
"value",
|
|
1140
|
+
"src",
|
|
1141
|
+
"href",
|
|
1142
|
+
"className",
|
|
1143
|
+
"id",
|
|
1144
|
+
"checked",
|
|
1145
|
+
"selected",
|
|
1146
|
+
"disabled"
|
|
1147
|
+
]);
|
|
854
1148
|
}
|
|
855
1149
|
});
|
|
856
1150
|
|
|
@@ -862,20 +1156,20 @@ __export(chart_serve_exports, {
|
|
|
862
1156
|
});
|
|
863
1157
|
module.exports = __toCommonJS(chart_serve_exports);
|
|
864
1158
|
var import_node_http = __toESM(require("node:http"));
|
|
865
|
-
var
|
|
866
|
-
var
|
|
1159
|
+
var import_node_fs20 = __toESM(require("node:fs"));
|
|
1160
|
+
var import_node_path22 = __toESM(require("node:path"));
|
|
867
1161
|
|
|
868
1162
|
// src/server/graph/index.ts
|
|
869
|
-
var
|
|
870
|
-
var
|
|
1163
|
+
var import_node_fs16 = require("node:fs");
|
|
1164
|
+
var import_node_path18 = require("node:path");
|
|
871
1165
|
|
|
872
1166
|
// src/server/graph/core/graph-builder.ts
|
|
873
1167
|
var import_node_fs12 = require("node:fs");
|
|
874
|
-
var
|
|
1168
|
+
var import_node_path13 = require("node:path");
|
|
875
1169
|
init_config();
|
|
876
1170
|
|
|
877
1171
|
// src/server/graph/core/parser-registry.ts
|
|
878
|
-
var
|
|
1172
|
+
var import_node_path12 = require("node:path");
|
|
879
1173
|
|
|
880
1174
|
// src/server/graph/parsers/ts/typescript-project.ts
|
|
881
1175
|
var import_node_fs5 = require("node:fs");
|
|
@@ -1126,6 +1420,7 @@ var CLASSIFICATION_TO_LAYER = {
|
|
|
1126
1420
|
config: "ui",
|
|
1127
1421
|
lib: "ui",
|
|
1128
1422
|
"mcp-tool": "ui",
|
|
1423
|
+
middleware: "ui",
|
|
1129
1424
|
external: "ui"
|
|
1130
1425
|
};
|
|
1131
1426
|
function toNodeId(srcDir, rootDir, absPath) {
|
|
@@ -1209,6 +1504,8 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
|
|
|
1209
1504
|
function classifyType(absPath, id) {
|
|
1210
1505
|
const contentType = classifyFile(absPath);
|
|
1211
1506
|
if (contentType === "lib" && id.startsWith("server/mcp/")) return "mcp-tool";
|
|
1507
|
+
if (/^app\/(.+\/)?page\.tsx$/.test(id)) return "page";
|
|
1508
|
+
if (/^app\/(.+\/)?layout\.tsx$/.test(id)) return "layout";
|
|
1212
1509
|
return contentType;
|
|
1213
1510
|
}
|
|
1214
1511
|
function extractRoute(id) {
|
|
@@ -1519,6 +1816,7 @@ function generate(rootDir) {
|
|
|
1519
1816
|
variables: deep.variables,
|
|
1520
1817
|
responses: deep.responses,
|
|
1521
1818
|
params: deep.params,
|
|
1819
|
+
...deep.effects ? { effects: deep.effects } : {},
|
|
1522
1820
|
_dbCalls: dbCalls
|
|
1523
1821
|
// temp: used for cross-ref building below
|
|
1524
1822
|
});
|
|
@@ -1538,6 +1836,7 @@ function generate(rootDir) {
|
|
|
1538
1836
|
stateVars: deep.stateVars,
|
|
1539
1837
|
conditions: deep.conditions,
|
|
1540
1838
|
variables: deep.variables,
|
|
1839
|
+
...deep.effects ? { effects: deep.effects } : {},
|
|
1541
1840
|
...authWrappers.length > 0 ? { auth: authWrappers } : {},
|
|
1542
1841
|
...dbCalls.length > 0 ? { _dbCalls: dbCalls } : {}
|
|
1543
1842
|
});
|
|
@@ -3808,6 +4107,112 @@ var staticRefScannerParser = {
|
|
|
3808
4107
|
}
|
|
3809
4108
|
};
|
|
3810
4109
|
|
|
4110
|
+
// src/server/graph/parsers/crosslayer/middleware-gates.ts
|
|
4111
|
+
var import_node_path11 = require("node:path");
|
|
4112
|
+
init_ts_extractor();
|
|
4113
|
+
init_config();
|
|
4114
|
+
function toNodeId4(srcDir, rootDir, absPath) {
|
|
4115
|
+
const relFromSrc = (0, import_node_path11.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
4116
|
+
if (relFromSrc.startsWith("..")) {
|
|
4117
|
+
return (0, import_node_path11.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
4118
|
+
}
|
|
4119
|
+
return relFromSrc;
|
|
4120
|
+
}
|
|
4121
|
+
function collectTargets(apiOutput, uiOutput) {
|
|
4122
|
+
const out = [];
|
|
4123
|
+
for (const n of apiOutput?.nodes ?? []) {
|
|
4124
|
+
if (n.type !== "endpoint") continue;
|
|
4125
|
+
const path2 = n.path;
|
|
4126
|
+
if (typeof path2 !== "string" || !path2) continue;
|
|
4127
|
+
out.push({ id: n.id, route: path2, layer: "api" });
|
|
4128
|
+
}
|
|
4129
|
+
for (const n of uiOutput?.nodes ?? []) {
|
|
4130
|
+
if (n.type !== "page") continue;
|
|
4131
|
+
const route = n.route;
|
|
4132
|
+
if (typeof route !== "string" || !route) continue;
|
|
4133
|
+
out.push({ id: n.id, route, layer: "ui" });
|
|
4134
|
+
}
|
|
4135
|
+
return out;
|
|
4136
|
+
}
|
|
4137
|
+
var middlewareGatesParser = {
|
|
4138
|
+
id: "middleware-gates",
|
|
4139
|
+
layer: "crosslayer",
|
|
4140
|
+
concern: "auth-gate",
|
|
4141
|
+
detect(rootDir) {
|
|
4142
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
4143
|
+
if (!paths) return false;
|
|
4144
|
+
return paths.conventionFiles.some((f) => /middleware\.tsx?$/.test(f));
|
|
4145
|
+
},
|
|
4146
|
+
generate(rootDir, layerOutputs) {
|
|
4147
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
4148
|
+
if (!paths) return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
4149
|
+
const middlewareFiles = paths.conventionFiles.filter((f) => /middleware\.tsx?$/.test(f));
|
|
4150
|
+
if (middlewareFiles.length === 0) {
|
|
4151
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
4152
|
+
}
|
|
4153
|
+
const apiOutput = layerOutputs.get("api");
|
|
4154
|
+
const uiOutput = layerOutputs.get("ui");
|
|
4155
|
+
const targets = collectTargets(apiOutput, uiOutput);
|
|
4156
|
+
const crossRefs = [];
|
|
4157
|
+
const flagged = [];
|
|
4158
|
+
const seenEdge = /* @__PURE__ */ new Set();
|
|
4159
|
+
const seenUnparseable = /* @__PURE__ */ new Set();
|
|
4160
|
+
let protectMatcherCount = 0;
|
|
4161
|
+
for (const file of middlewareFiles) {
|
|
4162
|
+
let info = null;
|
|
4163
|
+
try {
|
|
4164
|
+
info = extractMiddlewareAuthTS(file);
|
|
4165
|
+
} catch {
|
|
4166
|
+
}
|
|
4167
|
+
if (!info || info.matchers.length === 0) continue;
|
|
4168
|
+
const middlewareId = toNodeId4(paths.srcDir, rootDir, file);
|
|
4169
|
+
for (const matcher of info.matchers) {
|
|
4170
|
+
if (matcher.intent !== "protect") continue;
|
|
4171
|
+
protectMatcherCount += 1;
|
|
4172
|
+
for (const pattern of matcher.patterns) {
|
|
4173
|
+
const re = middlewarePatternToRegex(pattern);
|
|
4174
|
+
if (!re) {
|
|
4175
|
+
const key = `${middlewareId}|${pattern}`;
|
|
4176
|
+
if (seenUnparseable.has(key)) continue;
|
|
4177
|
+
seenUnparseable.add(key);
|
|
4178
|
+
flagged.push({
|
|
4179
|
+
source: middlewareId,
|
|
4180
|
+
target: "UNRESOLVED",
|
|
4181
|
+
type: "middleware_pattern_unparseable",
|
|
4182
|
+
label: `pattern "${pattern}" in matcher "${matcher.name}" \u2014 coverage unknown`,
|
|
4183
|
+
confidence: "high"
|
|
4184
|
+
});
|
|
4185
|
+
continue;
|
|
4186
|
+
}
|
|
4187
|
+
for (const target of targets) {
|
|
4188
|
+
if (!re.test(target.route)) continue;
|
|
4189
|
+
const key = `${middlewareId}|${target.id}|protects`;
|
|
4190
|
+
if (seenEdge.has(key)) continue;
|
|
4191
|
+
seenEdge.add(key);
|
|
4192
|
+
crossRefs.push({
|
|
4193
|
+
source: middlewareId,
|
|
4194
|
+
target: target.id,
|
|
4195
|
+
type: "protects",
|
|
4196
|
+
layer: target.layer
|
|
4197
|
+
});
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
}
|
|
4202
|
+
return {
|
|
4203
|
+
cross_refs: crossRefs,
|
|
4204
|
+
flagged_edges: flagged,
|
|
4205
|
+
warnings: [],
|
|
4206
|
+
patterns: {
|
|
4207
|
+
middleware_files: middlewareFiles.length,
|
|
4208
|
+
protect_matchers: protectMatcherCount,
|
|
4209
|
+
protects_edges: crossRefs.length,
|
|
4210
|
+
unparseable_patterns: flagged.length
|
|
4211
|
+
}
|
|
4212
|
+
};
|
|
4213
|
+
}
|
|
4214
|
+
};
|
|
4215
|
+
|
|
3811
4216
|
// src/server/graph/core/parser-registry.ts
|
|
3812
4217
|
function isMultiLayerParser(p) {
|
|
3813
4218
|
return "layers" in p && Array.isArray(p.layers);
|
|
@@ -3873,7 +4278,8 @@ function registerBuiltins(registry, disabled) {
|
|
|
3873
4278
|
fetchResolverParser,
|
|
3874
4279
|
apiAnnotationsParser,
|
|
3875
4280
|
urlLiteralScannerParser,
|
|
3876
|
-
staticRefScannerParser
|
|
4281
|
+
staticRefScannerParser,
|
|
4282
|
+
middlewareGatesParser
|
|
3877
4283
|
];
|
|
3878
4284
|
for (const parser of builtins) {
|
|
3879
4285
|
if (disabled.has(parser.id)) continue;
|
|
@@ -3883,7 +4289,7 @@ function registerBuiltins(registry, disabled) {
|
|
|
3883
4289
|
function loadCustomParsers(registry, config, rootDir, disabled) {
|
|
3884
4290
|
for (const entry of config.parsers?.custom ?? []) {
|
|
3885
4291
|
try {
|
|
3886
|
-
const absPath = (0,
|
|
4292
|
+
const absPath = (0, import_node_path12.resolve)(rootDir, entry.path);
|
|
3887
4293
|
const mod = require(absPath);
|
|
3888
4294
|
const parser = "default" in mod ? mod.default : mod;
|
|
3889
4295
|
if (disabled.has(parser.id)) continue;
|
|
@@ -4013,7 +4419,7 @@ function applyCrossLayerResults(uiOutput, results) {
|
|
|
4013
4419
|
|
|
4014
4420
|
// src/server/graph/core/graph-builder.ts
|
|
4015
4421
|
function readGraphFromDisk(rootDir, layer) {
|
|
4016
|
-
const filePath = (0,
|
|
4422
|
+
const filePath = (0, import_node_path13.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
4017
4423
|
if (!(0, import_node_fs12.existsSync)(filePath)) return null;
|
|
4018
4424
|
try {
|
|
4019
4425
|
return JSON.parse((0, import_node_fs12.readFileSync)(filePath, "utf-8"));
|
|
@@ -4117,11 +4523,11 @@ function generateAll(rootDir) {
|
|
|
4117
4523
|
init_config();
|
|
4118
4524
|
|
|
4119
4525
|
// src/server/graph/core/tagger-registry.ts
|
|
4120
|
-
var
|
|
4526
|
+
var import_node_path15 = require("node:path");
|
|
4121
4527
|
|
|
4122
4528
|
// src/server/graph/taggers/module-tagger.ts
|
|
4123
4529
|
var import_node_fs13 = require("node:fs");
|
|
4124
|
-
var
|
|
4530
|
+
var import_node_path14 = require("node:path");
|
|
4125
4531
|
function matchGlob(pattern, id) {
|
|
4126
4532
|
const patParts = pattern.split("/");
|
|
4127
4533
|
const idParts = id.split("/");
|
|
@@ -4154,13 +4560,13 @@ function detectConventionDirs(rootDir, extraConventionDirs = []) {
|
|
|
4154
4560
|
const conventionDirs = [...CONVENTION_DIRS_BUILTIN, ...extraConventionDirs];
|
|
4155
4561
|
const searchDirs = [
|
|
4156
4562
|
rootDir,
|
|
4157
|
-
(0,
|
|
4158
|
-
(0,
|
|
4159
|
-
(0,
|
|
4563
|
+
(0, import_node_path14.join)(rootDir, "src"),
|
|
4564
|
+
(0, import_node_path14.join)(rootDir, "app"),
|
|
4565
|
+
(0, import_node_path14.join)(rootDir, "lib")
|
|
4160
4566
|
];
|
|
4161
4567
|
for (const base of searchDirs) {
|
|
4162
4568
|
for (const convention of conventionDirs) {
|
|
4163
|
-
const dir = (0,
|
|
4569
|
+
const dir = (0, import_node_path14.join)(base, convention);
|
|
4164
4570
|
if (!(0, import_node_fs13.existsSync)(dir)) continue;
|
|
4165
4571
|
try {
|
|
4166
4572
|
const stat = (0, import_node_fs13.statSync)(dir);
|
|
@@ -4466,7 +4872,7 @@ function loadCustomTaggers(registry, config, rootDir, disabled) {
|
|
|
4466
4872
|
for (const entry of config.taggers?.custom ?? []) {
|
|
4467
4873
|
if (disabled.has(entry.id)) continue;
|
|
4468
4874
|
try {
|
|
4469
|
-
const absPath = (0,
|
|
4875
|
+
const absPath = (0, import_node_path15.resolve)(rootDir, entry.path);
|
|
4470
4876
|
const mod = require(absPath);
|
|
4471
4877
|
const tagger = "default" in mod ? mod.default : mod;
|
|
4472
4878
|
const override = config.taggers?.trackUntagged?.[tagger.id];
|
|
@@ -4490,12 +4896,12 @@ function createTaggerRegistry(config, rootDir) {
|
|
|
4490
4896
|
|
|
4491
4897
|
// src/server/graph/core/tag-store.ts
|
|
4492
4898
|
var import_node_fs14 = require("node:fs");
|
|
4493
|
-
var
|
|
4899
|
+
var import_node_path16 = require("node:path");
|
|
4494
4900
|
var TAGS_FILENAME = "tags.json";
|
|
4495
4901
|
var GRAPHS_DIR = ".launchsecure/graphs";
|
|
4496
4902
|
var tagCache = /* @__PURE__ */ new Map();
|
|
4497
4903
|
function tagsFilePath(rootDir) {
|
|
4498
|
-
return (0,
|
|
4904
|
+
return (0, import_node_path16.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
|
|
4499
4905
|
}
|
|
4500
4906
|
function readTagStore(rootDir) {
|
|
4501
4907
|
const filePath = tagsFilePath(rootDir);
|
|
@@ -4516,7 +4922,7 @@ function readTagStore(rootDir) {
|
|
|
4516
4922
|
}
|
|
4517
4923
|
function writeTagStore(rootDir, store) {
|
|
4518
4924
|
const filePath = tagsFilePath(rootDir);
|
|
4519
|
-
const dir = (0,
|
|
4925
|
+
const dir = (0, import_node_path16.dirname)(filePath);
|
|
4520
4926
|
(0, import_node_fs14.mkdirSync)(dir, { recursive: true });
|
|
4521
4927
|
const cleaned = {};
|
|
4522
4928
|
for (const [nodeId, tags] of Object.entries(store)) {
|
|
@@ -4545,26 +4951,89 @@ function removeTag(rootDir, nodeId, key) {
|
|
|
4545
4951
|
|
|
4546
4952
|
// src/server/graph/index.ts
|
|
4547
4953
|
init_ts_extractor();
|
|
4954
|
+
|
|
4955
|
+
// src/server/graph/core/effects-index.ts
|
|
4956
|
+
var import_node_fs15 = require("node:fs");
|
|
4957
|
+
var import_node_path17 = require("node:path");
|
|
4958
|
+
function addTo(map, key, nodeId) {
|
|
4959
|
+
const list = map[key];
|
|
4960
|
+
if (!list) {
|
|
4961
|
+
map[key] = [nodeId];
|
|
4962
|
+
} else if (!list.includes(nodeId)) {
|
|
4963
|
+
list.push(nodeId);
|
|
4964
|
+
}
|
|
4965
|
+
}
|
|
4966
|
+
function collisionList(map) {
|
|
4967
|
+
const out = [];
|
|
4968
|
+
for (const [key, nodes] of Object.entries(map)) {
|
|
4969
|
+
if (nodes.length > 1) out.push({ key, nodes: nodes.slice().sort() });
|
|
4970
|
+
}
|
|
4971
|
+
out.sort((a, b) => b.nodes.length - a.nodes.length || a.key.localeCompare(b.key));
|
|
4972
|
+
return out;
|
|
4973
|
+
}
|
|
4974
|
+
function buildEffectsIndex(layerOutputs) {
|
|
4975
|
+
const idx = {
|
|
4976
|
+
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
4977
|
+
dom_ids: {},
|
|
4978
|
+
window_events: {},
|
|
4979
|
+
storage_keys: {},
|
|
4980
|
+
fetch_urls: {},
|
|
4981
|
+
timers: {},
|
|
4982
|
+
singleton_risks: [],
|
|
4983
|
+
collisions: { dom_ids: [], storage_keys: [], window_events: [] }
|
|
4984
|
+
};
|
|
4985
|
+
const singletonSet = /* @__PURE__ */ new Set();
|
|
4986
|
+
for (const output of Object.values(layerOutputs)) {
|
|
4987
|
+
for (const node of output.nodes) {
|
|
4988
|
+
const eff = node.effects;
|
|
4989
|
+
if (!eff) continue;
|
|
4990
|
+
for (const id of eff.dom_ids ?? []) addTo(idx.dom_ids, id, node.id);
|
|
4991
|
+
for (const ev of eff.window_events ?? []) addTo(idx.window_events, ev, node.id);
|
|
4992
|
+
for (const k of eff.storage_keys ?? []) addTo(idx.storage_keys, k, node.id);
|
|
4993
|
+
for (const u of eff.fetch_urls ?? []) addTo(idx.fetch_urls, u, node.id);
|
|
4994
|
+
for (const t of eff.timers ?? []) addTo(idx.timers, t, node.id);
|
|
4995
|
+
const hasTimer = (eff.timers?.length ?? 0) > 0;
|
|
4996
|
+
const hasWindowListener = (eff.window_events?.length ?? 0) > 0;
|
|
4997
|
+
const hasGlobalMutation = (eff.globals?.length ?? 0) > 0;
|
|
4998
|
+
if (hasTimer || hasWindowListener || hasGlobalMutation) {
|
|
4999
|
+
singletonSet.add(node.id);
|
|
5000
|
+
}
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
idx.singleton_risks = Array.from(singletonSet).sort();
|
|
5004
|
+
idx.collisions.dom_ids = collisionList(idx.dom_ids);
|
|
5005
|
+
idx.collisions.storage_keys = collisionList(idx.storage_keys);
|
|
5006
|
+
idx.collisions.window_events = collisionList(idx.window_events);
|
|
5007
|
+
return idx;
|
|
5008
|
+
}
|
|
5009
|
+
function writeEffectsIndex(rootDir, idx) {
|
|
5010
|
+
const path2 = (0, import_node_path17.join)(rootDir, ".launchsecure", "graphs", "effects-index.json");
|
|
5011
|
+
(0, import_node_fs15.writeFileSync)(path2, JSON.stringify(idx, null, 2) + "\n", "utf-8");
|
|
5012
|
+
return path2;
|
|
5013
|
+
}
|
|
5014
|
+
|
|
5015
|
+
// src/server/graph/index.ts
|
|
4548
5016
|
var GRAPHS_DIR2 = ".launchsecure/graphs";
|
|
5017
|
+
var NON_LAYER_GRAPH_FILES = /* @__PURE__ */ new Set(["tags.json", "effects-index.json"]);
|
|
4549
5018
|
function getAvailableLayers(rootDir) {
|
|
4550
|
-
const dir = (0,
|
|
4551
|
-
if (!(0,
|
|
4552
|
-
return (0,
|
|
5019
|
+
const dir = (0, import_node_path18.join)(rootDir, GRAPHS_DIR2);
|
|
5020
|
+
if (!(0, import_node_fs16.existsSync)(dir)) return [];
|
|
5021
|
+
return (0, import_node_fs16.readdirSync)(dir).filter((f) => f.endsWith(".json") && !NON_LAYER_GRAPH_FILES.has(f)).map((f) => f.replace(".json", ""));
|
|
4553
5022
|
}
|
|
4554
5023
|
var graphCache = /* @__PURE__ */ new Map();
|
|
4555
5024
|
var taggedCache = /* @__PURE__ */ new Map();
|
|
4556
5025
|
function graphsDir(rootDir) {
|
|
4557
|
-
return (0,
|
|
5026
|
+
return (0, import_node_path18.join)(rootDir, GRAPHS_DIR2);
|
|
4558
5027
|
}
|
|
4559
5028
|
function graphFilePath(rootDir, layer) {
|
|
4560
|
-
return (0,
|
|
5029
|
+
return (0, import_node_path18.join)(graphsDir(rootDir), `${layer}.json`);
|
|
4561
5030
|
}
|
|
4562
5031
|
function tagsFilePath2(rootDir) {
|
|
4563
|
-
return (0,
|
|
5032
|
+
return (0, import_node_path18.join)(graphsDir(rootDir), "tags.json");
|
|
4564
5033
|
}
|
|
4565
5034
|
function getMtimeMs(filePath) {
|
|
4566
|
-
if (!(0,
|
|
4567
|
-
return (0,
|
|
5035
|
+
if (!(0, import_node_fs16.existsSync)(filePath)) return 0;
|
|
5036
|
+
return (0, import_node_fs16.statSync)(filePath).mtimeMs;
|
|
4568
5037
|
}
|
|
4569
5038
|
function invalidateCache(filePath) {
|
|
4570
5039
|
graphCache.delete(filePath);
|
|
@@ -4603,20 +5072,20 @@ function applyTags(graph, layer, rootDir) {
|
|
|
4603
5072
|
}
|
|
4604
5073
|
function readGraphRaw(rootDir, layer) {
|
|
4605
5074
|
const filePath = graphFilePath(rootDir, layer);
|
|
4606
|
-
if (!(0,
|
|
4607
|
-
const stat = (0,
|
|
5075
|
+
if (!(0, import_node_fs16.existsSync)(filePath)) return null;
|
|
5076
|
+
const stat = (0, import_node_fs16.statSync)(filePath);
|
|
4608
5077
|
const cached = graphCache.get(filePath);
|
|
4609
5078
|
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
4610
5079
|
return cached.graph;
|
|
4611
5080
|
}
|
|
4612
|
-
const content = (0,
|
|
5081
|
+
const content = (0, import_node_fs16.readFileSync)(filePath, "utf-8");
|
|
4613
5082
|
const graph = JSON.parse(content);
|
|
4614
5083
|
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
4615
5084
|
return graph;
|
|
4616
5085
|
}
|
|
4617
5086
|
function readGraph(rootDir, layer) {
|
|
4618
5087
|
const rawFilePath = graphFilePath(rootDir, layer);
|
|
4619
|
-
if (!(0,
|
|
5088
|
+
if (!(0, import_node_fs16.existsSync)(rawFilePath)) return null;
|
|
4620
5089
|
const rawMtime = getMtimeMs(rawFilePath);
|
|
4621
5090
|
const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
|
|
4622
5091
|
const cacheKey = `${rootDir}:${layer}`;
|
|
@@ -4646,24 +5115,24 @@ async function generateGraph(rootDir, layer) {
|
|
|
4646
5115
|
mutationMethods: config.parsers?.patterns?.mutationMethods
|
|
4647
5116
|
});
|
|
4648
5117
|
const dir = graphsDir(rootDir);
|
|
4649
|
-
(0,
|
|
5118
|
+
(0, import_node_fs16.mkdirSync)(dir, { recursive: true });
|
|
4650
5119
|
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
4651
5120
|
for (const result of results) {
|
|
4652
5121
|
const filePath = graphFilePath(rootDir, result.layer);
|
|
4653
|
-
(0,
|
|
5122
|
+
(0, import_node_fs16.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
|
|
4654
5123
|
invalidateCache(filePath);
|
|
4655
5124
|
invalidateTaggedCache(rootDir, result.layer);
|
|
4656
5125
|
}
|
|
4657
5126
|
if (!layer) {
|
|
4658
5127
|
const producedLayers = new Set(results.map((r) => r.layer));
|
|
4659
5128
|
try {
|
|
4660
|
-
for (const f of (0,
|
|
4661
|
-
if (!f.endsWith(".json") || f === "tags.json") continue;
|
|
5129
|
+
for (const f of (0, import_node_fs16.readdirSync)(dir)) {
|
|
5130
|
+
if (!f.endsWith(".json") || f === "tags.json" || f === "effects-index.json") continue;
|
|
4662
5131
|
const layerName = f.replace(/\.json$/, "");
|
|
4663
5132
|
if (producedLayers.has(layerName)) continue;
|
|
4664
|
-
const orphan = (0,
|
|
5133
|
+
const orphan = (0, import_node_path18.join)(dir, f);
|
|
4665
5134
|
try {
|
|
4666
|
-
(0,
|
|
5135
|
+
(0, import_node_fs16.unlinkSync)(orphan);
|
|
4667
5136
|
invalidateCache(orphan);
|
|
4668
5137
|
invalidateTaggedCache(rootDir, layerName);
|
|
4669
5138
|
process.stderr.write(`[launch-chart] removed orphan layer file: ${f} (no parser produced ${layerName} this run)
|
|
@@ -4674,33 +5143,51 @@ async function generateGraph(rootDir, layer) {
|
|
|
4674
5143
|
} catch {
|
|
4675
5144
|
}
|
|
4676
5145
|
}
|
|
5146
|
+
try {
|
|
5147
|
+
const allLayers = {};
|
|
5148
|
+
for (const r of results) allLayers[r.layer] = r.output;
|
|
5149
|
+
if (layer) {
|
|
5150
|
+
for (const f of (0, import_node_fs16.readdirSync)(dir)) {
|
|
5151
|
+
if (!f.endsWith(".json") || f === "tags.json" || f === "effects-index.json") continue;
|
|
5152
|
+
const layerName = f.replace(/\.json$/, "");
|
|
5153
|
+
if (allLayers[layerName]) continue;
|
|
5154
|
+
const existing = readGraphRaw(rootDir, layerName);
|
|
5155
|
+
if (existing) allLayers[layerName] = existing;
|
|
5156
|
+
}
|
|
5157
|
+
}
|
|
5158
|
+
const idx = buildEffectsIndex(allLayers);
|
|
5159
|
+
writeEffectsIndex(rootDir, idx);
|
|
5160
|
+
} catch (e) {
|
|
5161
|
+
process.stderr.write(`[launch-chart] effects-index build failed: ${e.message}
|
|
5162
|
+
`);
|
|
5163
|
+
}
|
|
4677
5164
|
return results;
|
|
4678
5165
|
}
|
|
4679
5166
|
|
|
4680
5167
|
// src/server/lockfile.ts
|
|
4681
5168
|
var import_node_child_process = require("node:child_process");
|
|
4682
|
-
var
|
|
5169
|
+
var import_node_fs17 = require("node:fs");
|
|
4683
5170
|
var import_node_os = require("node:os");
|
|
4684
|
-
var
|
|
5171
|
+
var import_node_path19 = require("node:path");
|
|
4685
5172
|
function lockDir(projectRoot) {
|
|
4686
5173
|
if (projectRoot) {
|
|
4687
|
-
return (0,
|
|
5174
|
+
return (0, import_node_path19.join)(projectRoot, ".launchsecure");
|
|
4688
5175
|
}
|
|
4689
|
-
return (0,
|
|
5176
|
+
return (0, import_node_path19.join)((0, import_node_os.homedir)(), ".launchsecure");
|
|
4690
5177
|
}
|
|
4691
5178
|
function lockPath(projectRoot) {
|
|
4692
|
-
return (0,
|
|
5179
|
+
return (0, import_node_path19.join)(lockDir(projectRoot), "launch-chart.lock");
|
|
4693
5180
|
}
|
|
4694
5181
|
var _activeProjectRoot;
|
|
4695
5182
|
function readLock(projectRoot) {
|
|
4696
5183
|
const root = projectRoot ?? _activeProjectRoot;
|
|
4697
5184
|
const p = lockPath(root);
|
|
4698
|
-
if (!(0,
|
|
5185
|
+
if (!(0, import_node_fs17.existsSync)(p)) {
|
|
4699
5186
|
if (root) {
|
|
4700
5187
|
const globalP = lockPath();
|
|
4701
|
-
if ((0,
|
|
5188
|
+
if ((0, import_node_fs17.existsSync)(globalP)) {
|
|
4702
5189
|
try {
|
|
4703
|
-
const data = JSON.parse((0,
|
|
5190
|
+
const data = JSON.parse((0, import_node_fs17.readFileSync)(globalP, "utf-8"));
|
|
4704
5191
|
if (typeof data.pid === "number" && typeof data.port === "number" && data.cwd === root) {
|
|
4705
5192
|
return data;
|
|
4706
5193
|
}
|
|
@@ -4711,7 +5198,7 @@ function readLock(projectRoot) {
|
|
|
4711
5198
|
return null;
|
|
4712
5199
|
}
|
|
4713
5200
|
try {
|
|
4714
|
-
const data = JSON.parse((0,
|
|
5201
|
+
const data = JSON.parse((0, import_node_fs17.readFileSync)(p, "utf-8"));
|
|
4715
5202
|
if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
|
|
4716
5203
|
return data;
|
|
4717
5204
|
} catch {
|
|
@@ -4748,7 +5235,7 @@ function getLiveLock(projectRoot) {
|
|
|
4748
5235
|
const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
|
|
4749
5236
|
if (!live) {
|
|
4750
5237
|
try {
|
|
4751
|
-
(0,
|
|
5238
|
+
(0, import_node_fs17.unlinkSync)(lockPath(root));
|
|
4752
5239
|
} catch {
|
|
4753
5240
|
}
|
|
4754
5241
|
return null;
|
|
@@ -4757,14 +5244,14 @@ function getLiveLock(projectRoot) {
|
|
|
4757
5244
|
}
|
|
4758
5245
|
function writeLock(data, projectRoot) {
|
|
4759
5246
|
const root = projectRoot ?? _activeProjectRoot;
|
|
4760
|
-
(0,
|
|
4761
|
-
(0,
|
|
5247
|
+
(0, import_node_fs17.mkdirSync)(lockDir(root), { recursive: true });
|
|
5248
|
+
(0, import_node_fs17.writeFileSync)(lockPath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
4762
5249
|
if (root) _activeProjectRoot = root;
|
|
4763
5250
|
}
|
|
4764
5251
|
function clearLock(projectRoot) {
|
|
4765
5252
|
const root = projectRoot ?? _activeProjectRoot;
|
|
4766
5253
|
try {
|
|
4767
|
-
(0,
|
|
5254
|
+
(0, import_node_fs17.unlinkSync)(lockPath(root));
|
|
4768
5255
|
} catch {
|
|
4769
5256
|
}
|
|
4770
5257
|
}
|
|
@@ -4773,13 +5260,350 @@ function clearLock(projectRoot) {
|
|
|
4773
5260
|
init_config();
|
|
4774
5261
|
|
|
4775
5262
|
// src/server/graph/core/audit-core.ts
|
|
4776
|
-
var
|
|
4777
|
-
var
|
|
5263
|
+
var import_node_fs19 = require("node:fs");
|
|
5264
|
+
var import_node_path21 = require("node:path");
|
|
5265
|
+
|
|
5266
|
+
// src/server/graph/core/audit-security.ts
|
|
5267
|
+
var import_node_fs18 = require("node:fs");
|
|
5268
|
+
var import_node_path20 = require("node:path");
|
|
5269
|
+
init_ts_extractor();
|
|
5270
|
+
init_config();
|
|
5271
|
+
function collectSourceFiles(rootDir) {
|
|
5272
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
5273
|
+
if (!paths) return null;
|
|
5274
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5275
|
+
const out = [];
|
|
5276
|
+
for (const root of paths.srcRoots) {
|
|
5277
|
+
for (const f of walk(root, [".ts", ".tsx"])) {
|
|
5278
|
+
if (!seen.has(f)) {
|
|
5279
|
+
seen.add(f);
|
|
5280
|
+
out.push(f);
|
|
5281
|
+
}
|
|
5282
|
+
}
|
|
5283
|
+
}
|
|
5284
|
+
for (const conv of paths.conventionFiles) {
|
|
5285
|
+
if (!seen.has(conv)) {
|
|
5286
|
+
seen.add(conv);
|
|
5287
|
+
out.push(conv);
|
|
5288
|
+
}
|
|
5289
|
+
}
|
|
5290
|
+
return out;
|
|
5291
|
+
}
|
|
5292
|
+
function toNodeId5(rootDir, srcDir, absPath) {
|
|
5293
|
+
const relFromSrc = (0, import_node_path20.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
5294
|
+
if (relFromSrc.startsWith("..")) {
|
|
5295
|
+
return (0, import_node_path20.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
5296
|
+
}
|
|
5297
|
+
return relFromSrc;
|
|
5298
|
+
}
|
|
5299
|
+
var 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;
|
|
5300
|
+
var SECRET_KEY_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
5301
|
+
"csrfToken",
|
|
5302
|
+
"csrf_token",
|
|
5303
|
+
"publicKey",
|
|
5304
|
+
"public_key",
|
|
5305
|
+
"clientId",
|
|
5306
|
+
"idempotencyKey",
|
|
5307
|
+
"idempotency_key",
|
|
5308
|
+
"requestKey",
|
|
5309
|
+
"request_key",
|
|
5310
|
+
"sessionId",
|
|
5311
|
+
"session_id",
|
|
5312
|
+
"apiKeyId",
|
|
5313
|
+
"api_key_id",
|
|
5314
|
+
"tokenType",
|
|
5315
|
+
"token_type",
|
|
5316
|
+
"tokenExpiry",
|
|
5317
|
+
"token_expiry",
|
|
5318
|
+
"isAccessToken",
|
|
5319
|
+
"hasToken"
|
|
5320
|
+
]);
|
|
5321
|
+
function scanResponseSecretsInFile(absPath) {
|
|
5322
|
+
const tree = parseSource(absPath);
|
|
5323
|
+
if (!tree) return [];
|
|
5324
|
+
const query = createQuery(`
|
|
5325
|
+
(call_expression
|
|
5326
|
+
function: (member_expression
|
|
5327
|
+
property: (property_identifier) @method
|
|
5328
|
+
(#eq? @method "json"))
|
|
5329
|
+
arguments: (arguments
|
|
5330
|
+
(object) @payload))
|
|
5331
|
+
`);
|
|
5332
|
+
const matches = query.matches(tree.rootNode);
|
|
5333
|
+
const hits = [];
|
|
5334
|
+
for (const m of matches) {
|
|
5335
|
+
const caps = m.captures;
|
|
5336
|
+
const payload = caps.find((c) => c.name === "payload")?.node;
|
|
5337
|
+
if (!payload) continue;
|
|
5338
|
+
walkPairKeys(payload, (keyName, node) => {
|
|
5339
|
+
const trimmed = keyName.replace(/^["']|["']$/g, "");
|
|
5340
|
+
if (SECRET_KEY_ALLOWLIST.has(trimmed)) return;
|
|
5341
|
+
if (!SECRET_KEY_RE.test(trimmed)) return;
|
|
5342
|
+
hits.push({
|
|
5343
|
+
file: absPath,
|
|
5344
|
+
line: node.startPosition.row + 1,
|
|
5345
|
+
keyName: trimmed,
|
|
5346
|
+
callShape: payload.text.slice(0, 40).replace(/\s+/g, " ")
|
|
5347
|
+
});
|
|
5348
|
+
});
|
|
5349
|
+
}
|
|
5350
|
+
return hits;
|
|
5351
|
+
}
|
|
5352
|
+
function walkPairKeys(node, visit) {
|
|
5353
|
+
for (const child of node.children) {
|
|
5354
|
+
if (child.type === "pair") {
|
|
5355
|
+
const keyChild = child.children[0];
|
|
5356
|
+
if (keyChild && (keyChild.type === "property_identifier" || keyChild.type === "string")) {
|
|
5357
|
+
visit(keyChild.text, keyChild);
|
|
5358
|
+
}
|
|
5359
|
+
const valueChild = child.children[child.children.length - 1];
|
|
5360
|
+
if (valueChild && valueChild.type === "object") walkPairKeys(valueChild, visit);
|
|
5361
|
+
} else if (child.type === "shorthand_property_identifier") {
|
|
5362
|
+
visit(child.text, child);
|
|
5363
|
+
} else if (child.type === "object") {
|
|
5364
|
+
walkPairKeys(child, visit);
|
|
5365
|
+
}
|
|
5366
|
+
}
|
|
5367
|
+
}
|
|
5368
|
+
function checkResponseSecretLeak(rootDir, core) {
|
|
5369
|
+
const files = collectSourceFiles(rootDir);
|
|
5370
|
+
if (files === null || files.length === 0) {
|
|
5371
|
+
return core.buildSkipped(
|
|
5372
|
+
"security",
|
|
5373
|
+
"response_secret_leak",
|
|
5374
|
+
`no source files detected \u2014 not a TypeScript project, or src roots aren't configured`
|
|
5375
|
+
);
|
|
5376
|
+
}
|
|
5377
|
+
const findings = [];
|
|
5378
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
5379
|
+
for (const abs of files) {
|
|
5380
|
+
let hits = [];
|
|
5381
|
+
try {
|
|
5382
|
+
hits = scanResponseSecretsInFile(abs);
|
|
5383
|
+
} catch {
|
|
5384
|
+
}
|
|
5385
|
+
for (const h of hits) {
|
|
5386
|
+
const fileId = toNodeId5(rootDir, paths.srcDir, abs);
|
|
5387
|
+
findings.push({
|
|
5388
|
+
id: `secret-leak:${fileId}:${h.line}:${h.keyName}`,
|
|
5389
|
+
severity: "error",
|
|
5390
|
+
category: "response_secret_leak",
|
|
5391
|
+
title: `${h.keyName} in response payload`,
|
|
5392
|
+
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.`,
|
|
5393
|
+
file: fileId,
|
|
5394
|
+
line: h.line
|
|
5395
|
+
});
|
|
5396
|
+
}
|
|
5397
|
+
}
|
|
5398
|
+
return core.buildReport("security", "response_secret_leak", findings);
|
|
5399
|
+
}
|
|
5400
|
+
function collectDeclaredEnvKeys(rootDir) {
|
|
5401
|
+
const keys = /* @__PURE__ */ new Set();
|
|
5402
|
+
const files = [];
|
|
5403
|
+
let entries = [];
|
|
5404
|
+
try {
|
|
5405
|
+
entries = (0, import_node_fs18.readdirSync)(rootDir);
|
|
5406
|
+
} catch {
|
|
5407
|
+
return { keys, files };
|
|
5408
|
+
}
|
|
5409
|
+
for (const name of entries) {
|
|
5410
|
+
if (!name.startsWith(".env")) continue;
|
|
5411
|
+
const abs = (0, import_node_path20.join)(rootDir, name);
|
|
5412
|
+
if (!(0, import_node_fs18.existsSync)(abs)) continue;
|
|
5413
|
+
files.push(name);
|
|
5414
|
+
let content = "";
|
|
5415
|
+
try {
|
|
5416
|
+
content = (0, import_node_fs18.readFileSync)(abs, "utf-8");
|
|
5417
|
+
} catch {
|
|
5418
|
+
continue;
|
|
5419
|
+
}
|
|
5420
|
+
for (const line of content.split("\n")) {
|
|
5421
|
+
const trimmed = line.trim();
|
|
5422
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
5423
|
+
const eq = trimmed.indexOf("=");
|
|
5424
|
+
if (eq < 0) continue;
|
|
5425
|
+
const key = trimmed.slice(0, eq).trim();
|
|
5426
|
+
if (/^[A-Z_][A-Z0-9_]*$/.test(key)) keys.add(key);
|
|
5427
|
+
}
|
|
5428
|
+
}
|
|
5429
|
+
return { keys, files };
|
|
5430
|
+
}
|
|
5431
|
+
var FRAMEWORK_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
5432
|
+
"NODE_ENV",
|
|
5433
|
+
"NODE_OPTIONS",
|
|
5434
|
+
"NODE_PATH",
|
|
5435
|
+
"PATH",
|
|
5436
|
+
"HOME",
|
|
5437
|
+
"USER",
|
|
5438
|
+
"PWD",
|
|
5439
|
+
"CI",
|
|
5440
|
+
"GITHUB_ACTIONS",
|
|
5441
|
+
"GITHUB_WORKFLOW",
|
|
5442
|
+
"GITHUB_RUN_ID",
|
|
5443
|
+
"GITHUB_SHA",
|
|
5444
|
+
"VERCEL",
|
|
5445
|
+
"VERCEL_ENV",
|
|
5446
|
+
"VERCEL_URL",
|
|
5447
|
+
"VERCEL_REGION",
|
|
5448
|
+
"VERCEL_GIT_COMMIT_SHA",
|
|
5449
|
+
"VERCEL_GIT_COMMIT_REF",
|
|
5450
|
+
"VERCEL_GIT_REPO_SLUG",
|
|
5451
|
+
"VERCEL_GIT_REPO_OWNER",
|
|
5452
|
+
"PORT",
|
|
5453
|
+
"TZ",
|
|
5454
|
+
"LANG",
|
|
5455
|
+
"LC_ALL",
|
|
5456
|
+
"TERM",
|
|
5457
|
+
"DEBUG",
|
|
5458
|
+
"NEXT_RUNTIME",
|
|
5459
|
+
"NEXT_PUBLIC_VERCEL_URL"
|
|
5460
|
+
]);
|
|
5461
|
+
function scanEnvRefsInFile(absPath) {
|
|
5462
|
+
const tree = parseSource(absPath);
|
|
5463
|
+
if (!tree) return [];
|
|
5464
|
+
const query = createQuery(`
|
|
5465
|
+
; process.env.X
|
|
5466
|
+
(member_expression
|
|
5467
|
+
object: (member_expression
|
|
5468
|
+
object: (identifier) @_proc
|
|
5469
|
+
property: (property_identifier) @_env)
|
|
5470
|
+
property: (property_identifier) @env_name
|
|
5471
|
+
(#eq? @_proc "process")
|
|
5472
|
+
(#eq? @_env "env"))
|
|
5473
|
+
|
|
5474
|
+
; process.env["X"]
|
|
5475
|
+
(subscript_expression
|
|
5476
|
+
object: (member_expression
|
|
5477
|
+
object: (identifier) @_proc2
|
|
5478
|
+
property: (property_identifier) @_env2)
|
|
5479
|
+
index: (string (string_fragment) @env_name_str)
|
|
5480
|
+
(#eq? @_proc2 "process")
|
|
5481
|
+
(#eq? @_env2 "env"))
|
|
5482
|
+
`);
|
|
5483
|
+
const matches = query.matches(tree.rootNode);
|
|
5484
|
+
const refs = [];
|
|
5485
|
+
for (const m of matches) {
|
|
5486
|
+
const caps = m.captures;
|
|
5487
|
+
const key = caps.find((c) => c.name === "env_name" || c.name === "env_name_str");
|
|
5488
|
+
if (!key) continue;
|
|
5489
|
+
refs.push({ file: absPath, line: key.node.startPosition.row + 1, envKey: key.node.text });
|
|
5490
|
+
}
|
|
5491
|
+
return refs;
|
|
5492
|
+
}
|
|
5493
|
+
function checkEnvDeadAlias(rootDir, core) {
|
|
5494
|
+
const { keys: declared, files: envFiles } = collectDeclaredEnvKeys(rootDir);
|
|
5495
|
+
if (envFiles.length === 0) {
|
|
5496
|
+
return core.buildSkipped(
|
|
5497
|
+
"security",
|
|
5498
|
+
"env_dead_alias",
|
|
5499
|
+
`no .env* files in project root \u2014 this check needs a declared-env inventory to compare against`
|
|
5500
|
+
);
|
|
5501
|
+
}
|
|
5502
|
+
const files = collectSourceFiles(rootDir);
|
|
5503
|
+
if (files === null || files.length === 0) {
|
|
5504
|
+
return core.buildSkipped("security", "env_dead_alias", `no source files detected`);
|
|
5505
|
+
}
|
|
5506
|
+
const findings = [];
|
|
5507
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
5508
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5509
|
+
for (const abs of files) {
|
|
5510
|
+
let refs = [];
|
|
5511
|
+
try {
|
|
5512
|
+
refs = scanEnvRefsInFile(abs);
|
|
5513
|
+
} catch {
|
|
5514
|
+
}
|
|
5515
|
+
for (const r of refs) {
|
|
5516
|
+
if (declared.has(r.envKey)) continue;
|
|
5517
|
+
if (FRAMEWORK_ENV_KEYS.has(r.envKey)) continue;
|
|
5518
|
+
if (r.envKey.startsWith("npm_")) continue;
|
|
5519
|
+
const fileId = toNodeId5(rootDir, paths.srcDir, abs);
|
|
5520
|
+
const dedupeKey = `${fileId}:${r.envKey}`;
|
|
5521
|
+
if (seen.has(dedupeKey)) continue;
|
|
5522
|
+
seen.add(dedupeKey);
|
|
5523
|
+
findings.push({
|
|
5524
|
+
id: `dead-env:${fileId}:${r.envKey}`,
|
|
5525
|
+
severity: "warning",
|
|
5526
|
+
category: "env_dead_alias",
|
|
5527
|
+
title: `process.env.${r.envKey} not declared in any .env file`,
|
|
5528
|
+
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.`,
|
|
5529
|
+
file: fileId,
|
|
5530
|
+
line: r.line
|
|
5531
|
+
});
|
|
5532
|
+
}
|
|
5533
|
+
}
|
|
5534
|
+
return core.buildReport("security", "env_dead_alias", findings);
|
|
5535
|
+
}
|
|
5536
|
+
function scanUrlFallbacksInFile(absPath) {
|
|
5537
|
+
const tree = parseSource(absPath);
|
|
5538
|
+
if (!tree) return [];
|
|
5539
|
+
const query = createQuery(`
|
|
5540
|
+
; process.env.X || "https://..." or process.env.X ?? "https://..."
|
|
5541
|
+
(binary_expression
|
|
5542
|
+
left: (member_expression
|
|
5543
|
+
object: (member_expression
|
|
5544
|
+
object: (identifier) @_proc
|
|
5545
|
+
property: (property_identifier) @_env)
|
|
5546
|
+
property: (property_identifier) @env_var)
|
|
5547
|
+
right: (string (string_fragment) @url)
|
|
5548
|
+
(#eq? @_proc "process")
|
|
5549
|
+
(#eq? @_env "env")
|
|
5550
|
+
(#match? @url "^https?://")) @expr
|
|
5551
|
+
`);
|
|
5552
|
+
const matches = query.matches(tree.rootNode);
|
|
5553
|
+
const hits = [];
|
|
5554
|
+
for (const m of matches) {
|
|
5555
|
+
const caps = m.captures;
|
|
5556
|
+
const envVar = caps.find((c) => c.name === "env_var")?.node;
|
|
5557
|
+
const url = caps.find((c) => c.name === "url")?.node;
|
|
5558
|
+
const expr = caps.find((c) => c.name === "expr")?.node;
|
|
5559
|
+
if (!envVar || !url || !expr) continue;
|
|
5560
|
+
const opMatch = / (\|\||\?\?) /.exec(expr.text);
|
|
5561
|
+
const operator = opMatch ? opMatch[1] : "||";
|
|
5562
|
+
hits.push({
|
|
5563
|
+
file: absPath,
|
|
5564
|
+
line: envVar.startPosition.row + 1,
|
|
5565
|
+
envKey: envVar.text,
|
|
5566
|
+
url: url.text,
|
|
5567
|
+
operator
|
|
5568
|
+
});
|
|
5569
|
+
}
|
|
5570
|
+
return hits;
|
|
5571
|
+
}
|
|
5572
|
+
function checkHardcodedUrlFallback(rootDir, core) {
|
|
5573
|
+
const files = collectSourceFiles(rootDir);
|
|
5574
|
+
if (files === null || files.length === 0) {
|
|
5575
|
+
return core.buildSkipped("security", "hardcoded_url_fallback", `no source files detected`);
|
|
5576
|
+
}
|
|
5577
|
+
const findings = [];
|
|
5578
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
5579
|
+
for (const abs of files) {
|
|
5580
|
+
let hits = [];
|
|
5581
|
+
try {
|
|
5582
|
+
hits = scanUrlFallbacksInFile(abs);
|
|
5583
|
+
} catch {
|
|
5584
|
+
}
|
|
5585
|
+
for (const h of hits) {
|
|
5586
|
+
const fileId = toNodeId5(rootDir, paths.srcDir, abs);
|
|
5587
|
+
findings.push({
|
|
5588
|
+
id: `url-fallback:${fileId}:${h.line}:${h.envKey}`,
|
|
5589
|
+
severity: "warning",
|
|
5590
|
+
category: "hardcoded_url_fallback",
|
|
5591
|
+
title: `${h.envKey} ${h.operator} "${h.url}"`,
|
|
5592
|
+
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.`,
|
|
5593
|
+
file: fileId,
|
|
5594
|
+
line: h.line
|
|
5595
|
+
});
|
|
5596
|
+
}
|
|
5597
|
+
}
|
|
5598
|
+
return core.buildReport("security", "hardcoded_url_fallback", findings);
|
|
5599
|
+
}
|
|
5600
|
+
|
|
5601
|
+
// src/server/graph/core/audit-core.ts
|
|
4778
5602
|
function readGraphFile(rootDir, layer) {
|
|
4779
|
-
const filePath = (0,
|
|
4780
|
-
if (!(0,
|
|
5603
|
+
const filePath = (0, import_node_path21.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
5604
|
+
if (!(0, import_node_fs19.existsSync)(filePath)) return null;
|
|
4781
5605
|
try {
|
|
4782
|
-
return JSON.parse((0,
|
|
5606
|
+
return JSON.parse((0, import_node_fs19.readFileSync)(filePath, "utf-8"));
|
|
4783
5607
|
} catch {
|
|
4784
5608
|
return null;
|
|
4785
5609
|
}
|
|
@@ -4788,8 +5612,7 @@ function checkSchemaDrift(rootDir) {
|
|
|
4788
5612
|
const findings = [];
|
|
4789
5613
|
const db = readGraphFile(rootDir, "db");
|
|
4790
5614
|
if (!db) {
|
|
4791
|
-
|
|
4792
|
-
return buildReport("db", "schema_drift", findings);
|
|
5615
|
+
return buildSkipped("db", "schema_drift", "no db graph \u2014 generate_graph first, or this project has no Prisma schema");
|
|
4793
5616
|
}
|
|
4794
5617
|
for (const c of db.contradictions ?? []) {
|
|
4795
5618
|
const isTableLevel = c.detail.includes("Table ") && (c.detail.includes("has no CREATE TABLE") || c.detail.includes("not in schema.prisma"));
|
|
@@ -4806,7 +5629,7 @@ function checkSchemaDrift(rootDir) {
|
|
|
4806
5629
|
function checkOrphanFks(rootDir) {
|
|
4807
5630
|
const findings = [];
|
|
4808
5631
|
const db = readGraphFile(rootDir, "db");
|
|
4809
|
-
if (!db) return
|
|
5632
|
+
if (!db) return buildSkipped("db", "orphan_fks", "no db graph");
|
|
4810
5633
|
for (const f of db.flagged_edges ?? []) {
|
|
4811
5634
|
findings.push({
|
|
4812
5635
|
id: `fk:${f.source}->${f.target}`,
|
|
@@ -4821,13 +5644,16 @@ function checkOrphanFks(rootDir) {
|
|
|
4821
5644
|
function checkUnprotectedRoutes(rootDir) {
|
|
4822
5645
|
const findings = [];
|
|
4823
5646
|
const api = readGraphFile(rootDir, "api");
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
5647
|
+
if (!api) return buildSkipped("api", "unprotected_routes", "no api graph");
|
|
5648
|
+
const routePermsPath = (0, import_node_path21.join)(rootDir, "src", "config", "route-permissions.ts");
|
|
5649
|
+
if (!(0, import_node_fs19.existsSync)(routePermsPath)) {
|
|
5650
|
+
return buildSkipped(
|
|
5651
|
+
"api",
|
|
5652
|
+
"unprotected_routes",
|
|
5653
|
+
`no src/config/route-permissions.ts \u2014 this check needs a centralized ROUTE_PERMISSIONS inventory to compare endpoints against`
|
|
5654
|
+
);
|
|
4830
5655
|
}
|
|
5656
|
+
const routePermsContent = (0, import_node_fs19.readFileSync)(routePermsPath, "utf-8");
|
|
4831
5657
|
const registeredRoutes = /* @__PURE__ */ new Set();
|
|
4832
5658
|
const routeEntryRe = /path:\s*'([^']+)'/g;
|
|
4833
5659
|
let rm;
|
|
@@ -4871,7 +5697,7 @@ function routeMatchesPattern(route, pattern) {
|
|
|
4871
5697
|
function checkDeadScreens(rootDir) {
|
|
4872
5698
|
const findings = [];
|
|
4873
5699
|
const ui = readGraphFile(rootDir, "ui");
|
|
4874
|
-
if (!ui) return
|
|
5700
|
+
if (!ui) return buildSkipped("ui", "dead_screens", "no ui graph");
|
|
4875
5701
|
const pages = ui.nodes.filter((n) => n.type === "page" || n.type === "layout");
|
|
4876
5702
|
const navTargets = /* @__PURE__ */ new Set();
|
|
4877
5703
|
for (const e of ui.edges) {
|
|
@@ -4903,13 +5729,24 @@ function checkDeadScreens(rootDir) {
|
|
|
4903
5729
|
function checkUnenforcedPermissions(rootDir) {
|
|
4904
5730
|
const findings = [];
|
|
4905
5731
|
const staticGraph = readGraphFile(rootDir, "static");
|
|
4906
|
-
if (!staticGraph) return
|
|
5732
|
+
if (!staticGraph) return buildSkipped("static", "unenforced_permissions", "no static graph");
|
|
4907
5733
|
const permissions = staticGraph.nodes.filter((n) => n.type === "seed_permission").map((n) => ({ id: n.id, key: n.value, name: n.name }));
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
5734
|
+
if (permissions.length === 0) {
|
|
5735
|
+
return buildSkipped(
|
|
5736
|
+
"static",
|
|
5737
|
+
"unenforced_permissions",
|
|
5738
|
+
`no seed_permission nodes \u2014 this project either has no seed permissions or hasn't tagged them in seed.ts`
|
|
5739
|
+
);
|
|
4912
5740
|
}
|
|
5741
|
+
const routePermsPath = (0, import_node_path21.join)(rootDir, "src", "config", "route-permissions.ts");
|
|
5742
|
+
if (!(0, import_node_fs19.existsSync)(routePermsPath)) {
|
|
5743
|
+
return buildSkipped(
|
|
5744
|
+
"static",
|
|
5745
|
+
"unenforced_permissions",
|
|
5746
|
+
`no src/config/route-permissions.ts to compare seed permissions against`
|
|
5747
|
+
);
|
|
5748
|
+
}
|
|
5749
|
+
const routePermsContent = (0, import_node_fs19.readFileSync)(routePermsPath, "utf-8");
|
|
4913
5750
|
for (const perm of permissions) {
|
|
4914
5751
|
const regex = new RegExp(`permission:\\s*['"]${perm.key}['"]`);
|
|
4915
5752
|
if (!regex.test(routePermsContent)) {
|
|
@@ -4927,20 +5764,27 @@ function checkUnenforcedPermissions(rootDir) {
|
|
|
4927
5764
|
function checkHardcodedValues(rootDir) {
|
|
4928
5765
|
const findings = [];
|
|
4929
5766
|
const staticGraph = readGraphFile(rootDir, "static");
|
|
4930
|
-
if (!staticGraph) return
|
|
5767
|
+
if (!staticGraph) return buildSkipped("static", "hardcoded_values", "no static graph");
|
|
4931
5768
|
const knownValues = /* @__PURE__ */ new Set();
|
|
4932
5769
|
for (const n of staticGraph.nodes) {
|
|
4933
5770
|
if (n.type === "enum_value") knownValues.add(n.value);
|
|
4934
5771
|
}
|
|
5772
|
+
if (knownValues.size === 0) {
|
|
5773
|
+
return buildSkipped(
|
|
5774
|
+
"static",
|
|
5775
|
+
"hardcoded_values",
|
|
5776
|
+
`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`
|
|
5777
|
+
);
|
|
5778
|
+
}
|
|
4935
5779
|
const api = readGraphFile(rootDir, "api");
|
|
4936
|
-
if (!api) return
|
|
5780
|
+
if (!api) return buildSkipped("static", "hardcoded_values", "no api graph");
|
|
4937
5781
|
const allCapsRe = /['"]([A-Z][A-Z_]{2,})['"]/g;
|
|
4938
5782
|
const seen = /* @__PURE__ */ new Set();
|
|
4939
5783
|
for (const node of api.nodes) {
|
|
4940
5784
|
if (node.type !== "endpoint") continue;
|
|
4941
|
-
const filePath = (0,
|
|
4942
|
-
if (!(0,
|
|
4943
|
-
const content = (0,
|
|
5785
|
+
const filePath = (0, import_node_path21.join)(rootDir, "src", node.id);
|
|
5786
|
+
if (!(0, import_node_fs19.existsSync)(filePath)) continue;
|
|
5787
|
+
const content = (0, import_node_fs19.readFileSync)(filePath, "utf-8");
|
|
4944
5788
|
let m;
|
|
4945
5789
|
allCapsRe.lastIndex = 0;
|
|
4946
5790
|
while ((m = allCapsRe.exec(content)) !== null) {
|
|
@@ -4972,7 +5816,19 @@ function buildReport(layer, check, findings) {
|
|
|
4972
5816
|
warnings: findings.filter((f) => f.severity === "warning").length,
|
|
4973
5817
|
info: findings.filter((f) => f.severity === "info").length
|
|
4974
5818
|
},
|
|
4975
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5819
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5820
|
+
status: "ok"
|
|
5821
|
+
};
|
|
5822
|
+
}
|
|
5823
|
+
function buildSkipped(layer, check, reason) {
|
|
5824
|
+
return {
|
|
5825
|
+
layer,
|
|
5826
|
+
check,
|
|
5827
|
+
findings: [],
|
|
5828
|
+
summary: { errors: 0, warnings: 0, info: 0 },
|
|
5829
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5830
|
+
status: "skipped",
|
|
5831
|
+
skipReason: reason
|
|
4976
5832
|
};
|
|
4977
5833
|
}
|
|
4978
5834
|
var CHECKS = {
|
|
@@ -4989,6 +5845,11 @@ var CHECKS = {
|
|
|
4989
5845
|
static: {
|
|
4990
5846
|
unenforced_permissions: checkUnenforcedPermissions,
|
|
4991
5847
|
hardcoded_values: checkHardcodedValues
|
|
5848
|
+
},
|
|
5849
|
+
security: {
|
|
5850
|
+
response_secret_leak: (rootDir) => checkResponseSecretLeak(rootDir, { buildReport, buildSkipped }),
|
|
5851
|
+
env_dead_alias: (rootDir) => checkEnvDeadAlias(rootDir, { buildReport, buildSkipped }),
|
|
5852
|
+
hardcoded_url_fallback: (rootDir) => checkHardcodedUrlFallback(rootDir, { buildReport, buildSkipped })
|
|
4992
5853
|
}
|
|
4993
5854
|
};
|
|
4994
5855
|
function getAvailableChecks() {
|
|
@@ -5035,7 +5896,12 @@ function runAudit(rootDir, layer, check) {
|
|
|
5035
5896
|
}
|
|
5036
5897
|
function formatAsPrompt(reports) {
|
|
5037
5898
|
const lines = [];
|
|
5899
|
+
const skipped = [];
|
|
5038
5900
|
for (const report of reports) {
|
|
5901
|
+
if (report.status === "skipped") {
|
|
5902
|
+
skipped.push(report);
|
|
5903
|
+
continue;
|
|
5904
|
+
}
|
|
5039
5905
|
if (report.findings.length === 0) continue;
|
|
5040
5906
|
lines.push(`## ${report.layer.toUpperCase()} \u2014 ${report.check} (${report.findings.length} findings)`);
|
|
5041
5907
|
lines.push("");
|
|
@@ -5047,6 +5913,14 @@ function formatAsPrompt(reports) {
|
|
|
5047
5913
|
}
|
|
5048
5914
|
lines.push("");
|
|
5049
5915
|
}
|
|
5916
|
+
if (skipped.length > 0) {
|
|
5917
|
+
lines.push("## Skipped checks (no comparison target \u2014 NOT passes)");
|
|
5918
|
+
lines.push("");
|
|
5919
|
+
for (const r of skipped) {
|
|
5920
|
+
lines.push(`- ${r.layer}/${r.check}: ${r.skipReason ?? "comparison target missing"}`);
|
|
5921
|
+
}
|
|
5922
|
+
lines.push("");
|
|
5923
|
+
}
|
|
5050
5924
|
if (lines.length === 0) return "No audit findings.";
|
|
5051
5925
|
return lines.join("\n");
|
|
5052
5926
|
}
|
|
@@ -5070,16 +5944,16 @@ var MIME_TYPES = {
|
|
|
5070
5944
|
function findProjectRoot(startDir) {
|
|
5071
5945
|
let dir = startDir;
|
|
5072
5946
|
for (let i = 0; i < 8; i++) {
|
|
5073
|
-
const graphsDir2 =
|
|
5074
|
-
if (
|
|
5075
|
-
const parent =
|
|
5947
|
+
const graphsDir2 = import_node_path22.default.join(dir, ".launchsecure", "graphs");
|
|
5948
|
+
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;
|
|
5949
|
+
const parent = import_node_path22.default.dirname(dir);
|
|
5076
5950
|
if (parent === dir) break;
|
|
5077
5951
|
dir = parent;
|
|
5078
5952
|
}
|
|
5079
5953
|
dir = startDir;
|
|
5080
5954
|
for (let i = 0; i < 8; i++) {
|
|
5081
|
-
if (
|
|
5082
|
-
const parent =
|
|
5955
|
+
if (import_node_fs20.default.existsSync(import_node_path22.default.join(dir, ".git"))) return dir;
|
|
5956
|
+
const parent = import_node_path22.default.dirname(dir);
|
|
5083
5957
|
if (parent === dir) break;
|
|
5084
5958
|
dir = parent;
|
|
5085
5959
|
}
|
|
@@ -5088,7 +5962,7 @@ function findProjectRoot(startDir) {
|
|
|
5088
5962
|
function resolveRequestRoot(url, monorepoRoot, projects) {
|
|
5089
5963
|
const projectParam = url.searchParams.get("project");
|
|
5090
5964
|
if (!projectParam || projects.length === 0) return monorepoRoot;
|
|
5091
|
-
const resolved =
|
|
5965
|
+
const resolved = import_node_path22.default.resolve(monorepoRoot, projectParam);
|
|
5092
5966
|
if (!resolved.startsWith(monorepoRoot)) {
|
|
5093
5967
|
throw new Error("Project path outside monorepo root");
|
|
5094
5968
|
}
|
|
@@ -5139,16 +6013,16 @@ async function buildMergedGraph(root) {
|
|
|
5139
6013
|
};
|
|
5140
6014
|
}
|
|
5141
6015
|
function serveStatic(res, filePath) {
|
|
5142
|
-
if (!
|
|
5143
|
-
const ext =
|
|
6016
|
+
if (!import_node_fs20.default.existsSync(filePath) || !import_node_fs20.default.statSync(filePath).isFile()) return false;
|
|
6017
|
+
const ext = import_node_path22.default.extname(filePath).toLowerCase();
|
|
5144
6018
|
const mime = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
5145
6019
|
res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
|
|
5146
|
-
|
|
6020
|
+
import_node_fs20.default.createReadStream(filePath).pipe(res);
|
|
5147
6021
|
return true;
|
|
5148
6022
|
}
|
|
5149
6023
|
function serveIndex(res, clientDir) {
|
|
5150
|
-
const indexPath =
|
|
5151
|
-
if (!
|
|
6024
|
+
const indexPath = import_node_path22.default.join(clientDir, "index.html");
|
|
6025
|
+
if (!import_node_fs20.default.existsSync(indexPath)) {
|
|
5152
6026
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
5153
6027
|
res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
|
|
5154
6028
|
return;
|
|
@@ -5200,7 +6074,7 @@ async function startChartServer(opts = {}) {
|
|
|
5200
6074
|
}
|
|
5201
6075
|
return { port: existing.port, url: existing.url };
|
|
5202
6076
|
}
|
|
5203
|
-
const clientDir = opts.clientDir ??
|
|
6077
|
+
const clientDir = opts.clientDir ?? import_node_path22.default.join(__dirname, "..", "chart-client");
|
|
5204
6078
|
const rootConfig = loadConfig(projectRoot);
|
|
5205
6079
|
const projects = rootConfig.projects ?? [];
|
|
5206
6080
|
const server = import_node_http.default.createServer((req, res) => {
|
|
@@ -5216,11 +6090,11 @@ async function startChartServer(opts = {}) {
|
|
|
5216
6090
|
}
|
|
5217
6091
|
if (req.method === "GET" && url2.pathname === "/api/projects") {
|
|
5218
6092
|
const projectList = projects.length > 0 ? projects.map((p) => {
|
|
5219
|
-
const absRoot =
|
|
5220
|
-
const hasGraphs =
|
|
5221
|
-
const hasNextConfig =
|
|
6093
|
+
const absRoot = import_node_path22.default.resolve(projectRoot, p.root);
|
|
6094
|
+
const hasGraphs = import_node_fs20.default.existsSync(import_node_path22.default.join(absRoot, ".launchsecure", "graphs"));
|
|
6095
|
+
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"));
|
|
5222
6096
|
return { name: p.name, root: p.root, hasGraphs, hasNextConfig };
|
|
5223
|
-
}) : [{ name:
|
|
6097
|
+
}) : [{ name: import_node_path22.default.basename(projectRoot), root: ".", hasGraphs: import_node_fs20.default.existsSync(import_node_path22.default.join(projectRoot, ".launchsecure", "graphs")), hasNextConfig: true }];
|
|
5224
6098
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5225
6099
|
res.end(JSON.stringify({ projects: projectList, monorepoRoot: projectRoot }));
|
|
5226
6100
|
return;
|
|
@@ -5266,20 +6140,20 @@ async function startChartServer(opts = {}) {
|
|
|
5266
6140
|
}
|
|
5267
6141
|
if (req.method === "GET" && url2.pathname === "/api/file-content") {
|
|
5268
6142
|
const relPath = url2.searchParams.get("path");
|
|
5269
|
-
if (!relPath || relPath.includes("..") ||
|
|
6143
|
+
if (!relPath || relPath.includes("..") || import_node_path22.default.isAbsolute(relPath)) {
|
|
5270
6144
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
5271
6145
|
res.end(JSON.stringify({ error: "Invalid path" }));
|
|
5272
6146
|
return;
|
|
5273
6147
|
}
|
|
5274
|
-
const filePath =
|
|
5275
|
-
if (!filePath.startsWith(reqRoot) || !
|
|
6148
|
+
const filePath = import_node_path22.default.join(reqRoot, relPath);
|
|
6149
|
+
if (!filePath.startsWith(reqRoot) || !import_node_fs20.default.existsSync(filePath) || !import_node_fs20.default.statSync(filePath).isFile()) {
|
|
5276
6150
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
5277
6151
|
res.end(JSON.stringify({ error: "File not found" }));
|
|
5278
6152
|
return;
|
|
5279
6153
|
}
|
|
5280
|
-
const ext =
|
|
6154
|
+
const ext = import_node_path22.default.extname(filePath).toLowerCase();
|
|
5281
6155
|
const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
|
|
5282
|
-
const content =
|
|
6156
|
+
const content = import_node_fs20.default.readFileSync(filePath, "utf-8");
|
|
5283
6157
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5284
6158
|
res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
|
|
5285
6159
|
return;
|
|
@@ -5321,8 +6195,8 @@ async function startChartServer(opts = {}) {
|
|
|
5321
6195
|
req.on("end", () => {
|
|
5322
6196
|
try {
|
|
5323
6197
|
const newConfig = JSON.parse(body);
|
|
5324
|
-
const configPath =
|
|
5325
|
-
|
|
6198
|
+
const configPath = import_node_path22.default.join(reqRoot, ".launchchart.json");
|
|
6199
|
+
import_node_fs20.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
|
|
5326
6200
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5327
6201
|
res.end(JSON.stringify({ ok: true }));
|
|
5328
6202
|
} catch (err) {
|
|
@@ -5355,8 +6229,8 @@ async function startChartServer(opts = {}) {
|
|
|
5355
6229
|
const taggerConfig = JSON.parse(body);
|
|
5356
6230
|
const config2 = loadConfig(reqRoot);
|
|
5357
6231
|
config2.taggers = taggerConfig;
|
|
5358
|
-
const configPath =
|
|
5359
|
-
|
|
6232
|
+
const configPath = import_node_path22.default.join(reqRoot, ".launchchart.json");
|
|
6233
|
+
import_node_fs20.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
|
|
5360
6234
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5361
6235
|
res.end(JSON.stringify({ ok: true }));
|
|
5362
6236
|
} catch (err) {
|
|
@@ -5426,7 +6300,7 @@ async function startChartServer(opts = {}) {
|
|
|
5426
6300
|
dbDir: !!config2.paths?.dbDir,
|
|
5427
6301
|
srcRoots: !!(config2.paths?.srcRoots && config2.paths.srcRoots.length > 0)
|
|
5428
6302
|
};
|
|
5429
|
-
const relFromRoot = (abs) =>
|
|
6303
|
+
const relFromRoot = (abs) => import_node_path22.default.relative(reqRoot, abs) || ".";
|
|
5430
6304
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5431
6305
|
res.end(JSON.stringify({
|
|
5432
6306
|
projectRoot: reqRoot,
|
|
@@ -5448,19 +6322,19 @@ async function startChartServer(opts = {}) {
|
|
|
5448
6322
|
}
|
|
5449
6323
|
if (req.method === "GET" && url2.pathname === "/api/browse-dir") {
|
|
5450
6324
|
const browsePath = url2.searchParams.get("path") || projectRoot;
|
|
5451
|
-
const abs =
|
|
5452
|
-
const twoUp =
|
|
6325
|
+
const abs = import_node_path22.default.resolve(browsePath);
|
|
6326
|
+
const twoUp = import_node_path22.default.resolve(projectRoot, "..", "..");
|
|
5453
6327
|
if (!abs.startsWith(twoUp)) {
|
|
5454
6328
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
5455
6329
|
res.end(JSON.stringify({ ok: false, error: "Path outside allowed range" }));
|
|
5456
6330
|
return;
|
|
5457
6331
|
}
|
|
5458
6332
|
try {
|
|
5459
|
-
const entries =
|
|
6333
|
+
const entries = import_node_fs20.default.readdirSync(abs, { withFileTypes: true });
|
|
5460
6334
|
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();
|
|
5461
|
-
const parent = abs !== twoUp ?
|
|
6335
|
+
const parent = abs !== twoUp ? import_node_path22.default.dirname(abs) : null;
|
|
5462
6336
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5463
|
-
res.end(JSON.stringify({ current: abs, parent, dirs, relative:
|
|
6337
|
+
res.end(JSON.stringify({ current: abs, parent, dirs, relative: import_node_path22.default.relative(projectRoot, abs) || "." }));
|
|
5464
6338
|
} catch (err) {
|
|
5465
6339
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
5466
6340
|
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
|
@@ -5486,8 +6360,8 @@ async function startChartServer(opts = {}) {
|
|
|
5486
6360
|
const { projects: newProjects } = JSON.parse(body);
|
|
5487
6361
|
const config2 = loadConfig(projectRoot);
|
|
5488
6362
|
config2.projects = newProjects.length > 0 ? newProjects : void 0;
|
|
5489
|
-
const configPath =
|
|
5490
|
-
|
|
6363
|
+
const configPath = import_node_path22.default.join(projectRoot, ".launchchart.json");
|
|
6364
|
+
import_node_fs20.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
|
|
5491
6365
|
projects.length = 0;
|
|
5492
6366
|
if (config2.projects) projects.push(...config2.projects);
|
|
5493
6367
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -5500,7 +6374,7 @@ async function startChartServer(opts = {}) {
|
|
|
5500
6374
|
return;
|
|
5501
6375
|
}
|
|
5502
6376
|
if (url2.pathname !== "/") {
|
|
5503
|
-
const staticPath =
|
|
6377
|
+
const staticPath = import_node_path22.default.join(clientDir, url2.pathname);
|
|
5504
6378
|
if (serveStatic(res, staticPath)) return;
|
|
5505
6379
|
}
|
|
5506
6380
|
serveIndex(res, clientDir);
|