@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
|
@@ -57,14 +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,
|
|
62
|
+
classifyRouteAgainstMiddleware: () => classifyRouteAgainstMiddleware,
|
|
61
63
|
createQuery: () => createQuery,
|
|
62
64
|
extractAuthWrappersTS: () => extractAuthWrappersTS,
|
|
63
65
|
extractDbCallsTS: () => extractDbCallsTS,
|
|
64
66
|
extractDeep: () => extractDeep,
|
|
67
|
+
extractEffects: () => extractEffects,
|
|
68
|
+
extractMiddlewareAuthTS: () => extractMiddlewareAuthTS,
|
|
65
69
|
initTreeSitter: () => initTreeSitter,
|
|
70
|
+
middlewarePatternToRegex: () => middlewarePatternToRegex,
|
|
66
71
|
parseCodeTS: () => parseCodeTS,
|
|
67
72
|
parseFileTS: () => parseFileTS,
|
|
73
|
+
parseSource: () => parseSource,
|
|
68
74
|
setExtractorConfig: () => setExtractorConfig
|
|
69
75
|
});
|
|
70
76
|
async function initTreeSitter() {
|
|
@@ -98,8 +104,38 @@ function getQuery(name) {
|
|
|
98
104
|
}
|
|
99
105
|
function parseSource(absPath) {
|
|
100
106
|
ensureInit();
|
|
101
|
-
|
|
102
|
-
|
|
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
|
+
}
|
|
103
139
|
}
|
|
104
140
|
function parseCodeTS(code) {
|
|
105
141
|
ensureInit();
|
|
@@ -139,8 +175,20 @@ function childrenOfType(node, type) {
|
|
|
139
175
|
function childOfType(node, type) {
|
|
140
176
|
return node.children.find((n) => n.type === type);
|
|
141
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
|
+
}
|
|
142
189
|
function parseFileTS(absPath) {
|
|
143
190
|
const tree = parseSource(absPath);
|
|
191
|
+
if (!tree) return emptyParsedFile(absPath);
|
|
144
192
|
const root = tree.rootNode;
|
|
145
193
|
const imports = [];
|
|
146
194
|
const importStatements = childrenOfType(root, "import_statement");
|
|
@@ -335,6 +383,7 @@ function parseFileTS(absPath) {
|
|
|
335
383
|
}
|
|
336
384
|
function extractDbCallsTS(absPath) {
|
|
337
385
|
const tree = parseSource(absPath);
|
|
386
|
+
if (!tree) return [];
|
|
338
387
|
const root = tree.rootNode;
|
|
339
388
|
const dbQuery = getQuery("db-calls");
|
|
340
389
|
const matches = dbQuery.matches(root);
|
|
@@ -395,7 +444,9 @@ function classifyFile(absPath) {
|
|
|
395
444
|
const fileName = require("path").basename(absPath);
|
|
396
445
|
if (fileName.includes(".test.") || fileName.includes(".spec.") || fileName.includes("__test")) return "test";
|
|
397
446
|
if (fileName.includes(".stories.")) return "story";
|
|
447
|
+
if (fileName === "middleware.ts" || fileName === "middleware.tsx") return "middleware";
|
|
398
448
|
const tree = parseSource(absPath);
|
|
449
|
+
if (!tree) return "util";
|
|
399
450
|
const root = tree.rootNode;
|
|
400
451
|
const classifyQuery = getQuery("classify");
|
|
401
452
|
const captures = classifyQuery.captures(root);
|
|
@@ -414,6 +465,7 @@ function classifyFile(absPath) {
|
|
|
414
465
|
}
|
|
415
466
|
function extractAuthWrappersTS(absPath) {
|
|
416
467
|
const tree = parseSource(absPath);
|
|
468
|
+
if (!tree) return /* @__PURE__ */ new Set();
|
|
417
469
|
const root = tree.rootNode;
|
|
418
470
|
const wrapperQuery = getQuery("wrappers");
|
|
419
471
|
const matches = wrapperQuery.matches(root);
|
|
@@ -456,11 +508,354 @@ function extractAuthWrappersTS(absPath) {
|
|
|
456
508
|
}
|
|
457
509
|
return wrappers;
|
|
458
510
|
}
|
|
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
|
+
}
|
|
515
|
+
for (const re of EXEMPT_NAME_PATTERNS) {
|
|
516
|
+
if (re.test(name)) return { intent: "exempt", hint: `name "${name}" matches /${re.source}/` };
|
|
517
|
+
}
|
|
518
|
+
for (const re of PROTECT_NAME_PATTERNS) {
|
|
519
|
+
if (re.test(name)) return { intent: "protect", hint: `name "${name}" matches /${re.source}/` };
|
|
520
|
+
}
|
|
521
|
+
return { intent: "ambiguous", hint: `name "${name}" has no exempt/protect signal` };
|
|
522
|
+
}
|
|
523
|
+
function looksLikeRoutePattern(s) {
|
|
524
|
+
return s.startsWith("/") && !s.startsWith("//");
|
|
525
|
+
}
|
|
526
|
+
function collectStringsFromArray(arrNode) {
|
|
527
|
+
const out = [];
|
|
528
|
+
for (const child of arrNode.children) {
|
|
529
|
+
if (child.type !== "string") continue;
|
|
530
|
+
const frag = childOfType(child, "string_fragment");
|
|
531
|
+
if (frag) out.push(frag.text);
|
|
532
|
+
}
|
|
533
|
+
return out;
|
|
534
|
+
}
|
|
535
|
+
function findArrayInValue(valueNode) {
|
|
536
|
+
if (!valueNode) return null;
|
|
537
|
+
if (valueNode.type === "array") return valueNode;
|
|
538
|
+
if (valueNode.type === "call_expression") {
|
|
539
|
+
const args = childOfType(valueNode, "arguments");
|
|
540
|
+
if (!args) return null;
|
|
541
|
+
for (const c of args.children) {
|
|
542
|
+
if (c.type === "array") return c;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
function extractMatcherFromDeclarator(decl) {
|
|
548
|
+
const nameNode = childOfType(decl, "identifier");
|
|
549
|
+
if (!nameNode) return null;
|
|
550
|
+
let valueNode;
|
|
551
|
+
for (const c of decl.children) {
|
|
552
|
+
if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
|
|
553
|
+
valueNode = c;
|
|
554
|
+
}
|
|
555
|
+
const arr = findArrayInValue(valueNode);
|
|
556
|
+
if (!arr) return null;
|
|
557
|
+
const strings = collectStringsFromArray(arr);
|
|
558
|
+
const routes = strings.filter(looksLikeRoutePattern);
|
|
559
|
+
if (routes.length === 0) return null;
|
|
560
|
+
const { intent, hint } = inferIntentFromName(nameNode.text);
|
|
561
|
+
return { name: nameNode.text, patterns: routes, intent, hint };
|
|
562
|
+
}
|
|
563
|
+
function extractMatchersFromObject(objNode) {
|
|
564
|
+
const out = [];
|
|
565
|
+
for (const pair of childrenOfType(objNode, "pair")) {
|
|
566
|
+
const key = childOfType(pair, "property_identifier");
|
|
567
|
+
if (!key) continue;
|
|
568
|
+
const arr = pair.children.find((c) => c.type === "array");
|
|
569
|
+
if (!arr) continue;
|
|
570
|
+
const routes = collectStringsFromArray(arr).filter(looksLikeRoutePattern);
|
|
571
|
+
if (routes.length === 0) continue;
|
|
572
|
+
const { intent, hint } = inferIntentFromName(key.text);
|
|
573
|
+
out.push({ name: key.text, patterns: routes, intent, hint });
|
|
574
|
+
}
|
|
575
|
+
return out;
|
|
576
|
+
}
|
|
577
|
+
function detectFallthroughProtect(root) {
|
|
578
|
+
const text = root.text;
|
|
579
|
+
const signals = [
|
|
580
|
+
/\bauth\.protect\s*\(/,
|
|
581
|
+
/\bauth\(\)\.protect\s*\(/,
|
|
582
|
+
/\bredirect\s*\(\s*['"`]\/(sign-?in|log-?in|auth)/i,
|
|
583
|
+
/\bNextResponse\.redirect\s*\(/,
|
|
584
|
+
/\bthrow\s+new\s+\w*Unauthorized/i
|
|
585
|
+
];
|
|
586
|
+
return signals.some((re) => re.test(text));
|
|
587
|
+
}
|
|
588
|
+
function extractMiddlewareAuthTS(absPath) {
|
|
589
|
+
if (!require("node:fs").existsSync(absPath)) return null;
|
|
590
|
+
const tree = parseSource(absPath);
|
|
591
|
+
if (!tree) return null;
|
|
592
|
+
const root = tree.rootNode;
|
|
593
|
+
const matchers = [];
|
|
594
|
+
for (const stmt of root.children) {
|
|
595
|
+
if (stmt.type !== "lexical_declaration" && stmt.type !== "variable_declaration") continue;
|
|
596
|
+
for (const decl of childrenOfType(stmt, "variable_declarator")) {
|
|
597
|
+
const m = extractMatcherFromDeclarator(decl);
|
|
598
|
+
if (m) matchers.push(m);
|
|
599
|
+
let valueNode;
|
|
600
|
+
for (const c of decl.children) {
|
|
601
|
+
if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
|
|
602
|
+
valueNode = c;
|
|
603
|
+
}
|
|
604
|
+
if (valueNode?.type === "object") {
|
|
605
|
+
for (const inner of extractMatchersFromObject(valueNode)) matchers.push(inner);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
for (const stmt of root.children) {
|
|
610
|
+
if (stmt.type !== "export_statement") continue;
|
|
611
|
+
const decl = childOfType(stmt, "lexical_declaration") ?? childOfType(stmt, "variable_declaration");
|
|
612
|
+
if (!decl) continue;
|
|
613
|
+
for (const d of childrenOfType(decl, "variable_declarator")) {
|
|
614
|
+
let valueNode;
|
|
615
|
+
for (const c of d.children) {
|
|
616
|
+
if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
|
|
617
|
+
valueNode = c;
|
|
618
|
+
}
|
|
619
|
+
if (valueNode?.type === "object") {
|
|
620
|
+
for (const inner of extractMatchersFromObject(valueNode)) matchers.push(inner);
|
|
621
|
+
}
|
|
622
|
+
const m = extractMatcherFromDeclarator(d);
|
|
623
|
+
if (m && !matchers.some((x) => x.name === m.name && x.patterns.join() === m.patterns.join())) {
|
|
624
|
+
matchers.push(m);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return {
|
|
629
|
+
file: absPath,
|
|
630
|
+
matchers,
|
|
631
|
+
hasFallthroughProtect: detectFallthroughProtect(root)
|
|
632
|
+
};
|
|
633
|
+
}
|
|
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
|
+
}
|
|
645
|
+
if (/\(\?\!/.test(pattern)) return null;
|
|
646
|
+
if (pattern.startsWith("(")) return null;
|
|
647
|
+
let src = "^";
|
|
648
|
+
let i = 0;
|
|
649
|
+
while (i < pattern.length) {
|
|
650
|
+
const ch = pattern[i];
|
|
651
|
+
if (ch === "(" && pattern.slice(i, i + 4) === "(.*)") {
|
|
652
|
+
src += ".*";
|
|
653
|
+
i += 4;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (ch === ":") {
|
|
657
|
+
i++;
|
|
658
|
+
while (i < pattern.length && /[a-zA-Z0-9_]/.test(pattern[i])) i++;
|
|
659
|
+
src += "[^/]+";
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (ch === "*") {
|
|
663
|
+
i++;
|
|
664
|
+
while (i < pattern.length && /[a-zA-Z0-9_]/.test(pattern[i])) i++;
|
|
665
|
+
if (pattern[i] === "?") {
|
|
666
|
+
i++;
|
|
667
|
+
src += ".*";
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
src += ".+";
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
if (/[.\\+?^${}()|[\]]/.test(ch)) {
|
|
674
|
+
src += "\\" + ch;
|
|
675
|
+
i++;
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
src += ch;
|
|
679
|
+
i++;
|
|
680
|
+
}
|
|
681
|
+
src += "$";
|
|
682
|
+
try {
|
|
683
|
+
return new RegExp(src);
|
|
684
|
+
} catch {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
function classifyRouteAgainstMiddleware(routePath, info) {
|
|
689
|
+
for (const m of info.matchers) {
|
|
690
|
+
if (m.intent === "ambiguous") continue;
|
|
691
|
+
for (const pat of m.patterns) {
|
|
692
|
+
const re = middlewarePatternToRegex(pat);
|
|
693
|
+
if (!re) continue;
|
|
694
|
+
if (re.test(routePath)) return { intent: m.intent, matcher: m.name, pattern: pat };
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
459
699
|
function trunc(s, max = 120) {
|
|
460
700
|
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
461
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
|
+
}
|
|
462
854
|
function extractDeep(absPath) {
|
|
463
855
|
const tree = parseSource(absPath);
|
|
856
|
+
if (!tree) {
|
|
857
|
+
return { elements: [], stateVars: [], conditions: [], variables: [], responses: [], params: [] };
|
|
858
|
+
}
|
|
464
859
|
const root = tree.rootNode;
|
|
465
860
|
const elements = [];
|
|
466
861
|
const elQuery = getQuery("deep/jsx-semantic");
|
|
@@ -553,12 +948,18 @@ function extractDeep(absPath) {
|
|
|
553
948
|
const caps = captureMap(m);
|
|
554
949
|
const declNode = m.captures.find((c) => c.name === "var.decl" || c.name === "var.destructured" || c.name === "var.array")?.node;
|
|
555
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;
|
|
556
952
|
if (caps["var.name"] && caps["var.init"]) {
|
|
557
|
-
|
|
953
|
+
const variable = {
|
|
558
954
|
name: caps["var.name"],
|
|
559
955
|
kind,
|
|
560
956
|
init: trunc(caps["var.init"], 100)
|
|
561
|
-
}
|
|
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);
|
|
562
963
|
}
|
|
563
964
|
if (caps["var.destructured.obj"]) {
|
|
564
965
|
variables.push({
|
|
@@ -610,9 +1011,23 @@ function extractDeep(absPath) {
|
|
|
610
1011
|
params.push({ name: caps["param.body"], source: "body" });
|
|
611
1012
|
}
|
|
612
1013
|
}
|
|
613
|
-
|
|
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
|
+
};
|
|
614
1029
|
}
|
|
615
|
-
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;
|
|
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;
|
|
616
1031
|
var init_ts_extractor = __esm({
|
|
617
1032
|
"src/server/graph/core/ts-extractor.ts"() {
|
|
618
1033
|
"use strict";
|
|
@@ -625,6 +1040,19 @@ var init_ts_extractor = __esm({
|
|
|
625
1040
|
return (0, import_node_path4.join)((0, import_node_path4.dirname)(__filename), "graph", "queries");
|
|
626
1041
|
})();
|
|
627
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
|
+
};
|
|
628
1056
|
PRISMA_MUTATION_METHODS_BUILTIN = [
|
|
629
1057
|
"create",
|
|
630
1058
|
"createMany",
|
|
@@ -651,6 +1079,72 @@ var init_ts_extractor = __esm({
|
|
|
651
1079
|
{ module: /^@auth\//, helpers: ["auth"] },
|
|
652
1080
|
{ module: /^@supabase\/auth-helpers/, helpers: ["createServerClient"] }
|
|
653
1081
|
];
|
|
1082
|
+
EXEMPT_NAME_PATTERNS = [
|
|
1083
|
+
/^is_?public/i,
|
|
1084
|
+
/^public_?routes?/i,
|
|
1085
|
+
/^public_?paths?/i,
|
|
1086
|
+
/^whitelist/i,
|
|
1087
|
+
/^allowlist/i,
|
|
1088
|
+
/^unauthenticated/i,
|
|
1089
|
+
/^anonymous/i,
|
|
1090
|
+
/^guest/i,
|
|
1091
|
+
/^skip_?auth/i,
|
|
1092
|
+
/^bypass/i
|
|
1093
|
+
];
|
|
1094
|
+
PROTECT_NAME_PATTERNS = [
|
|
1095
|
+
/^is_?protected/i,
|
|
1096
|
+
/^protected_?routes?/i,
|
|
1097
|
+
/^protected_?paths?/i,
|
|
1098
|
+
/^require_?auth/i,
|
|
1099
|
+
/^auth_?required/i,
|
|
1100
|
+
/^private_?routes?/i,
|
|
1101
|
+
/^is_?admin/i,
|
|
1102
|
+
/^admin_?routes?/i,
|
|
1103
|
+
/^secured/i
|
|
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
|
+
]);
|
|
654
1148
|
}
|
|
655
1149
|
});
|
|
656
1150
|
|
|
@@ -662,20 +1156,20 @@ __export(chart_serve_exports, {
|
|
|
662
1156
|
});
|
|
663
1157
|
module.exports = __toCommonJS(chart_serve_exports);
|
|
664
1158
|
var import_node_http = __toESM(require("node:http"));
|
|
665
|
-
var
|
|
666
|
-
var
|
|
1159
|
+
var import_node_fs20 = __toESM(require("node:fs"));
|
|
1160
|
+
var import_node_path22 = __toESM(require("node:path"));
|
|
667
1161
|
|
|
668
1162
|
// src/server/graph/index.ts
|
|
669
|
-
var
|
|
670
|
-
var
|
|
1163
|
+
var import_node_fs16 = require("node:fs");
|
|
1164
|
+
var import_node_path18 = require("node:path");
|
|
671
1165
|
|
|
672
1166
|
// src/server/graph/core/graph-builder.ts
|
|
673
1167
|
var import_node_fs12 = require("node:fs");
|
|
674
|
-
var
|
|
1168
|
+
var import_node_path13 = require("node:path");
|
|
675
1169
|
init_config();
|
|
676
1170
|
|
|
677
1171
|
// src/server/graph/core/parser-registry.ts
|
|
678
|
-
var
|
|
1172
|
+
var import_node_path12 = require("node:path");
|
|
679
1173
|
|
|
680
1174
|
// src/server/graph/parsers/ts/typescript-project.ts
|
|
681
1175
|
var import_node_fs5 = require("node:fs");
|
|
@@ -691,15 +1185,9 @@ var import_node_fs2 = require("node:fs");
|
|
|
691
1185
|
var import_node_path2 = require("node:path");
|
|
692
1186
|
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
693
1187
|
"node_modules",
|
|
694
|
-
".git",
|
|
695
|
-
".next",
|
|
696
|
-
".launchsecure",
|
|
697
|
-
".claude",
|
|
698
1188
|
"dist",
|
|
699
1189
|
"build",
|
|
700
1190
|
"out",
|
|
701
|
-
".turbo",
|
|
702
|
-
".vercel",
|
|
703
1191
|
"coverage"
|
|
704
1192
|
]);
|
|
705
1193
|
function walk(dir, exts) {
|
|
@@ -721,6 +1209,7 @@ function walkWithIgnore(dir, exts, opts = {}) {
|
|
|
721
1209
|
const skip = opts.extraIgnore ? /* @__PURE__ */ new Set([...DEFAULT_IGNORE_DIRS, ...opts.extraIgnore]) : DEFAULT_IGNORE_DIRS;
|
|
722
1210
|
for (const entry of (0, import_node_fs2.readdirSync)(dir, { withFileTypes: true })) {
|
|
723
1211
|
if (entry.isDirectory()) {
|
|
1212
|
+
if (entry.name.startsWith(".")) continue;
|
|
724
1213
|
if (skip.has(entry.name)) continue;
|
|
725
1214
|
results.push(...walkWithIgnore((0, import_node_path2.join)(dir, entry.name), exts, opts));
|
|
726
1215
|
} else if (exts.includes((0, import_node_path2.extname)(entry.name))) {
|
|
@@ -931,6 +1420,7 @@ var CLASSIFICATION_TO_LAYER = {
|
|
|
931
1420
|
config: "ui",
|
|
932
1421
|
lib: "ui",
|
|
933
1422
|
"mcp-tool": "ui",
|
|
1423
|
+
middleware: "ui",
|
|
934
1424
|
external: "ui"
|
|
935
1425
|
};
|
|
936
1426
|
function toNodeId(srcDir, rootDir, absPath) {
|
|
@@ -1014,6 +1504,8 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
|
|
|
1014
1504
|
function classifyType(absPath, id) {
|
|
1015
1505
|
const contentType = classifyFile(absPath);
|
|
1016
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";
|
|
1017
1509
|
return contentType;
|
|
1018
1510
|
}
|
|
1019
1511
|
function extractRoute(id) {
|
|
@@ -1324,6 +1816,7 @@ function generate(rootDir) {
|
|
|
1324
1816
|
variables: deep.variables,
|
|
1325
1817
|
responses: deep.responses,
|
|
1326
1818
|
params: deep.params,
|
|
1819
|
+
...deep.effects ? { effects: deep.effects } : {},
|
|
1327
1820
|
_dbCalls: dbCalls
|
|
1328
1821
|
// temp: used for cross-ref building below
|
|
1329
1822
|
});
|
|
@@ -1343,6 +1836,7 @@ function generate(rootDir) {
|
|
|
1343
1836
|
stateVars: deep.stateVars,
|
|
1344
1837
|
conditions: deep.conditions,
|
|
1345
1838
|
variables: deep.variables,
|
|
1839
|
+
...deep.effects ? { effects: deep.effects } : {},
|
|
1346
1840
|
...authWrappers.length > 0 ? { auth: authWrappers } : {},
|
|
1347
1841
|
...dbCalls.length > 0 ? { _dbCalls: dbCalls } : {}
|
|
1348
1842
|
});
|
|
@@ -1472,43 +1966,169 @@ function generate(rootDir) {
|
|
|
1472
1966
|
nodeIdSet.add(externalId);
|
|
1473
1967
|
uiEdges.push(...edgesFromThis);
|
|
1474
1968
|
}
|
|
1969
|
+
const tablesByFile = /* @__PURE__ */ new Map();
|
|
1970
|
+
const allDbNodes = [...apiNodes, ...uiNodes];
|
|
1971
|
+
for (const node of allDbNodes) {
|
|
1972
|
+
const calls = node._dbCalls;
|
|
1973
|
+
if (!calls || calls.length === 0) continue;
|
|
1974
|
+
const map = /* @__PURE__ */ new Map();
|
|
1975
|
+
for (const c of calls) {
|
|
1976
|
+
const key = `${c.kind}:${c.model}:${c.isMutation ? "m" : "r"}`;
|
|
1977
|
+
if (!map.has(key)) {
|
|
1978
|
+
map.set(key, { model: c.model, method: c.method, isMutation: c.isMutation, kind: c.kind, via: [] });
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
tablesByFile.set(node.id, map);
|
|
1982
|
+
}
|
|
1983
|
+
const reverseRuntimeImports = /* @__PURE__ */ new Map();
|
|
1984
|
+
for (const edge of uiEdges) {
|
|
1985
|
+
if (edge.type !== "imports" && edge.type !== "renders") continue;
|
|
1986
|
+
if (!reverseRuntimeImports.has(edge.target)) {
|
|
1987
|
+
reverseRuntimeImports.set(edge.target, /* @__PURE__ */ new Set());
|
|
1988
|
+
}
|
|
1989
|
+
reverseRuntimeImports.get(edge.target).add(edge.source);
|
|
1990
|
+
}
|
|
1991
|
+
let changed = true;
|
|
1992
|
+
let iterations = 0;
|
|
1993
|
+
while (changed && iterations < 50) {
|
|
1994
|
+
changed = false;
|
|
1995
|
+
iterations++;
|
|
1996
|
+
for (const [target, tableMap] of [...tablesByFile]) {
|
|
1997
|
+
const importers = reverseRuntimeImports.get(target);
|
|
1998
|
+
if (!importers) continue;
|
|
1999
|
+
for (const importer of importers) {
|
|
2000
|
+
if (importer === target) continue;
|
|
2001
|
+
let importerMap = tablesByFile.get(importer);
|
|
2002
|
+
if (!importerMap) {
|
|
2003
|
+
importerMap = /* @__PURE__ */ new Map();
|
|
2004
|
+
tablesByFile.set(importer, importerMap);
|
|
2005
|
+
}
|
|
2006
|
+
for (const [key, call] of tableMap) {
|
|
2007
|
+
if (importerMap.has(key)) continue;
|
|
2008
|
+
importerMap.set(key, {
|
|
2009
|
+
model: call.model,
|
|
2010
|
+
method: null,
|
|
2011
|
+
isMutation: call.isMutation,
|
|
2012
|
+
kind: call.kind,
|
|
2013
|
+
via: [...call.via, target]
|
|
2014
|
+
});
|
|
2015
|
+
changed = true;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
for (const node of apiNodes) {
|
|
2021
|
+
const map = tablesByFile.get(node.id);
|
|
2022
|
+
if (!map) continue;
|
|
2023
|
+
node.db_models = [...new Set([...map.values()].map((c) => c.model))];
|
|
2024
|
+
node.db_operations = [...new Set(
|
|
2025
|
+
[...map.values()].filter((c) => c.via.length === 0 && c.method).map((c) => `${c.model}.${c.method}`)
|
|
2026
|
+
)];
|
|
2027
|
+
node.mutates = [...map.values()].some((c) => c.isMutation);
|
|
2028
|
+
}
|
|
1475
2029
|
const apiCrossRefs = [];
|
|
1476
2030
|
for (const node of apiNodes) {
|
|
1477
|
-
const
|
|
1478
|
-
if (!
|
|
1479
|
-
|
|
1480
|
-
|
|
2031
|
+
const map = tablesByFile.get(node.id);
|
|
2032
|
+
if (!map) {
|
|
2033
|
+
delete node._dbCalls;
|
|
2034
|
+
continue;
|
|
2035
|
+
}
|
|
2036
|
+
const seenTargets = /* @__PURE__ */ new Set();
|
|
2037
|
+
for (const call of map.values()) {
|
|
1481
2038
|
const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
|
|
1482
|
-
if (
|
|
1483
|
-
|
|
1484
|
-
|
|
2039
|
+
if (seenTargets.has(target)) continue;
|
|
2040
|
+
seenTargets.add(target);
|
|
2041
|
+
const isTransitive = call.via.length > 0;
|
|
2042
|
+
const ref = {
|
|
1485
2043
|
source: node.id,
|
|
1486
2044
|
target,
|
|
1487
|
-
type: call.isMutation ? "mutates" : "reads",
|
|
2045
|
+
type: call.isMutation ? isTransitive ? "mutates_via" : "mutates" : isTransitive ? "reads_via" : "reads",
|
|
1488
2046
|
layer: "db"
|
|
1489
|
-
}
|
|
2047
|
+
};
|
|
2048
|
+
if (isTransitive) ref.via = call.via;
|
|
2049
|
+
apiCrossRefs.push(ref);
|
|
1490
2050
|
}
|
|
1491
2051
|
delete node._dbCalls;
|
|
1492
2052
|
}
|
|
1493
2053
|
const uiCrossRefs = [];
|
|
1494
2054
|
for (const node of uiNodes) {
|
|
1495
|
-
const
|
|
1496
|
-
if (!
|
|
1497
|
-
|
|
1498
|
-
|
|
2055
|
+
const map = tablesByFile.get(node.id);
|
|
2056
|
+
if (!map) {
|
|
2057
|
+
if (node._dbCalls) delete node._dbCalls;
|
|
2058
|
+
continue;
|
|
2059
|
+
}
|
|
2060
|
+
const seenTargets = /* @__PURE__ */ new Set();
|
|
2061
|
+
for (const call of map.values()) {
|
|
1499
2062
|
const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
|
|
1500
|
-
if (
|
|
1501
|
-
|
|
1502
|
-
|
|
2063
|
+
if (seenTargets.has(target)) continue;
|
|
2064
|
+
seenTargets.add(target);
|
|
2065
|
+
const isTransitive = call.via.length > 0;
|
|
2066
|
+
const ref = {
|
|
1503
2067
|
source: node.id,
|
|
1504
2068
|
target,
|
|
1505
|
-
type: call.isMutation ? "mutates" : "reads",
|
|
2069
|
+
type: call.isMutation ? isTransitive ? "mutates_via" : "mutates" : isTransitive ? "reads_via" : "reads",
|
|
1506
2070
|
layer: "db"
|
|
1507
|
-
}
|
|
2071
|
+
};
|
|
2072
|
+
if (isTransitive) ref.via = call.via;
|
|
2073
|
+
uiCrossRefs.push(ref);
|
|
1508
2074
|
}
|
|
1509
|
-
delete node._dbCalls;
|
|
2075
|
+
if (node._dbCalls) delete node._dbCalls;
|
|
1510
2076
|
}
|
|
1511
2077
|
uiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
2078
|
+
const middlewareInfos = [];
|
|
2079
|
+
for (const conv of paths.conventionFiles) {
|
|
2080
|
+
if (!/middleware\.tsx?$/.test(conv)) continue;
|
|
2081
|
+
try {
|
|
2082
|
+
const info = extractMiddlewareAuthTS(conv);
|
|
2083
|
+
if (info && info.matchers.length > 0) middlewareInfos.push(info);
|
|
2084
|
+
} catch {
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
if (middlewareInfos.length > 0) {
|
|
2088
|
+
let setAuth2 = function(node, newTags, replaceAll) {
|
|
2089
|
+
const existing = node.auth ?? [];
|
|
2090
|
+
const meaningful = existing.filter((a) => a !== "public");
|
|
2091
|
+
const merged = replaceAll ? newTags : [.../* @__PURE__ */ new Set([...newTags, ...meaningful])];
|
|
2092
|
+
node.auth = merged.length > 0 ? merged : ["public"];
|
|
2093
|
+
}, applyMiddleware2 = function(node, routePath) {
|
|
2094
|
+
let resolved = null;
|
|
2095
|
+
let label = "";
|
|
2096
|
+
let hasAnyExemptMatcher = false;
|
|
2097
|
+
let hasAnyFallthrough = false;
|
|
2098
|
+
for (const info of middlewareInfos) {
|
|
2099
|
+
if (info.hasFallthroughProtect) hasAnyFallthrough = true;
|
|
2100
|
+
if (info.matchers.some((m) => m.intent === "exempt")) hasAnyExemptMatcher = true;
|
|
2101
|
+
const c = classifyRouteAgainstMiddleware(routePath, info);
|
|
2102
|
+
if (!c) continue;
|
|
2103
|
+
if (!resolved) {
|
|
2104
|
+
resolved = c.intent;
|
|
2105
|
+
label = c.matcher;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
if (resolved === "exempt") {
|
|
2109
|
+
setAuth2(node, ["public"], true);
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
if (resolved === "protect") {
|
|
2113
|
+
setAuth2(node, [`middleware:${label}`], false);
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
if (hasAnyExemptMatcher && hasAnyFallthrough) {
|
|
2117
|
+
setAuth2(node, ["middleware-protected"], false);
|
|
2118
|
+
}
|
|
2119
|
+
};
|
|
2120
|
+
var setAuth = setAuth2, applyMiddleware = applyMiddleware2;
|
|
2121
|
+
for (const node of apiNodes) {
|
|
2122
|
+
const routePath = node.path;
|
|
2123
|
+
if (!routePath) continue;
|
|
2124
|
+
applyMiddleware2(node, routePath);
|
|
2125
|
+
}
|
|
2126
|
+
for (const node of uiNodes) {
|
|
2127
|
+
const route = node.route;
|
|
2128
|
+
if (!route) continue;
|
|
2129
|
+
applyMiddleware2(node, route);
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
1512
2132
|
const apiNodeIds = new Set(apiNodes.map((n) => n.id));
|
|
1513
2133
|
const apiEdges = [];
|
|
1514
2134
|
const uiOnlyEdges = [];
|
|
@@ -3487,6 +4107,112 @@ var staticRefScannerParser = {
|
|
|
3487
4107
|
}
|
|
3488
4108
|
};
|
|
3489
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
|
+
|
|
3490
4216
|
// src/server/graph/core/parser-registry.ts
|
|
3491
4217
|
function isMultiLayerParser(p) {
|
|
3492
4218
|
return "layers" in p && Array.isArray(p.layers);
|
|
@@ -3552,7 +4278,8 @@ function registerBuiltins(registry, disabled) {
|
|
|
3552
4278
|
fetchResolverParser,
|
|
3553
4279
|
apiAnnotationsParser,
|
|
3554
4280
|
urlLiteralScannerParser,
|
|
3555
|
-
staticRefScannerParser
|
|
4281
|
+
staticRefScannerParser,
|
|
4282
|
+
middlewareGatesParser
|
|
3556
4283
|
];
|
|
3557
4284
|
for (const parser of builtins) {
|
|
3558
4285
|
if (disabled.has(parser.id)) continue;
|
|
@@ -3562,7 +4289,7 @@ function registerBuiltins(registry, disabled) {
|
|
|
3562
4289
|
function loadCustomParsers(registry, config, rootDir, disabled) {
|
|
3563
4290
|
for (const entry of config.parsers?.custom ?? []) {
|
|
3564
4291
|
try {
|
|
3565
|
-
const absPath = (0,
|
|
4292
|
+
const absPath = (0, import_node_path12.resolve)(rootDir, entry.path);
|
|
3566
4293
|
const mod = require(absPath);
|
|
3567
4294
|
const parser = "default" in mod ? mod.default : mod;
|
|
3568
4295
|
if (disabled.has(parser.id)) continue;
|
|
@@ -3692,7 +4419,7 @@ function applyCrossLayerResults(uiOutput, results) {
|
|
|
3692
4419
|
|
|
3693
4420
|
// src/server/graph/core/graph-builder.ts
|
|
3694
4421
|
function readGraphFromDisk(rootDir, layer) {
|
|
3695
|
-
const filePath = (0,
|
|
4422
|
+
const filePath = (0, import_node_path13.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
3696
4423
|
if (!(0, import_node_fs12.existsSync)(filePath)) return null;
|
|
3697
4424
|
try {
|
|
3698
4425
|
return JSON.parse((0, import_node_fs12.readFileSync)(filePath, "utf-8"));
|
|
@@ -3796,11 +4523,11 @@ function generateAll(rootDir) {
|
|
|
3796
4523
|
init_config();
|
|
3797
4524
|
|
|
3798
4525
|
// src/server/graph/core/tagger-registry.ts
|
|
3799
|
-
var
|
|
4526
|
+
var import_node_path15 = require("node:path");
|
|
3800
4527
|
|
|
3801
4528
|
// src/server/graph/taggers/module-tagger.ts
|
|
3802
4529
|
var import_node_fs13 = require("node:fs");
|
|
3803
|
-
var
|
|
4530
|
+
var import_node_path14 = require("node:path");
|
|
3804
4531
|
function matchGlob(pattern, id) {
|
|
3805
4532
|
const patParts = pattern.split("/");
|
|
3806
4533
|
const idParts = id.split("/");
|
|
@@ -3833,13 +4560,13 @@ function detectConventionDirs(rootDir, extraConventionDirs = []) {
|
|
|
3833
4560
|
const conventionDirs = [...CONVENTION_DIRS_BUILTIN, ...extraConventionDirs];
|
|
3834
4561
|
const searchDirs = [
|
|
3835
4562
|
rootDir,
|
|
3836
|
-
(0,
|
|
3837
|
-
(0,
|
|
3838
|
-
(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")
|
|
3839
4566
|
];
|
|
3840
4567
|
for (const base of searchDirs) {
|
|
3841
4568
|
for (const convention of conventionDirs) {
|
|
3842
|
-
const dir = (0,
|
|
4569
|
+
const dir = (0, import_node_path14.join)(base, convention);
|
|
3843
4570
|
if (!(0, import_node_fs13.existsSync)(dir)) continue;
|
|
3844
4571
|
try {
|
|
3845
4572
|
const stat = (0, import_node_fs13.statSync)(dir);
|
|
@@ -4145,7 +4872,7 @@ function loadCustomTaggers(registry, config, rootDir, disabled) {
|
|
|
4145
4872
|
for (const entry of config.taggers?.custom ?? []) {
|
|
4146
4873
|
if (disabled.has(entry.id)) continue;
|
|
4147
4874
|
try {
|
|
4148
|
-
const absPath = (0,
|
|
4875
|
+
const absPath = (0, import_node_path15.resolve)(rootDir, entry.path);
|
|
4149
4876
|
const mod = require(absPath);
|
|
4150
4877
|
const tagger = "default" in mod ? mod.default : mod;
|
|
4151
4878
|
const override = config.taggers?.trackUntagged?.[tagger.id];
|
|
@@ -4169,12 +4896,12 @@ function createTaggerRegistry(config, rootDir) {
|
|
|
4169
4896
|
|
|
4170
4897
|
// src/server/graph/core/tag-store.ts
|
|
4171
4898
|
var import_node_fs14 = require("node:fs");
|
|
4172
|
-
var
|
|
4899
|
+
var import_node_path16 = require("node:path");
|
|
4173
4900
|
var TAGS_FILENAME = "tags.json";
|
|
4174
4901
|
var GRAPHS_DIR = ".launchsecure/graphs";
|
|
4175
4902
|
var tagCache = /* @__PURE__ */ new Map();
|
|
4176
4903
|
function tagsFilePath(rootDir) {
|
|
4177
|
-
return (0,
|
|
4904
|
+
return (0, import_node_path16.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
|
|
4178
4905
|
}
|
|
4179
4906
|
function readTagStore(rootDir) {
|
|
4180
4907
|
const filePath = tagsFilePath(rootDir);
|
|
@@ -4195,7 +4922,7 @@ function readTagStore(rootDir) {
|
|
|
4195
4922
|
}
|
|
4196
4923
|
function writeTagStore(rootDir, store) {
|
|
4197
4924
|
const filePath = tagsFilePath(rootDir);
|
|
4198
|
-
const dir = (0,
|
|
4925
|
+
const dir = (0, import_node_path16.dirname)(filePath);
|
|
4199
4926
|
(0, import_node_fs14.mkdirSync)(dir, { recursive: true });
|
|
4200
4927
|
const cleaned = {};
|
|
4201
4928
|
for (const [nodeId, tags] of Object.entries(store)) {
|
|
@@ -4224,26 +4951,89 @@ function removeTag(rootDir, nodeId, key) {
|
|
|
4224
4951
|
|
|
4225
4952
|
// src/server/graph/index.ts
|
|
4226
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
|
|
4227
5016
|
var GRAPHS_DIR2 = ".launchsecure/graphs";
|
|
5017
|
+
var NON_LAYER_GRAPH_FILES = /* @__PURE__ */ new Set(["tags.json", "effects-index.json"]);
|
|
4228
5018
|
function getAvailableLayers(rootDir) {
|
|
4229
|
-
const dir = (0,
|
|
4230
|
-
if (!(0,
|
|
4231
|
-
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", ""));
|
|
4232
5022
|
}
|
|
4233
5023
|
var graphCache = /* @__PURE__ */ new Map();
|
|
4234
5024
|
var taggedCache = /* @__PURE__ */ new Map();
|
|
4235
5025
|
function graphsDir(rootDir) {
|
|
4236
|
-
return (0,
|
|
5026
|
+
return (0, import_node_path18.join)(rootDir, GRAPHS_DIR2);
|
|
4237
5027
|
}
|
|
4238
5028
|
function graphFilePath(rootDir, layer) {
|
|
4239
|
-
return (0,
|
|
5029
|
+
return (0, import_node_path18.join)(graphsDir(rootDir), `${layer}.json`);
|
|
4240
5030
|
}
|
|
4241
5031
|
function tagsFilePath2(rootDir) {
|
|
4242
|
-
return (0,
|
|
5032
|
+
return (0, import_node_path18.join)(graphsDir(rootDir), "tags.json");
|
|
4243
5033
|
}
|
|
4244
5034
|
function getMtimeMs(filePath) {
|
|
4245
|
-
if (!(0,
|
|
4246
|
-
return (0,
|
|
5035
|
+
if (!(0, import_node_fs16.existsSync)(filePath)) return 0;
|
|
5036
|
+
return (0, import_node_fs16.statSync)(filePath).mtimeMs;
|
|
4247
5037
|
}
|
|
4248
5038
|
function invalidateCache(filePath) {
|
|
4249
5039
|
graphCache.delete(filePath);
|
|
@@ -4282,20 +5072,20 @@ function applyTags(graph, layer, rootDir) {
|
|
|
4282
5072
|
}
|
|
4283
5073
|
function readGraphRaw(rootDir, layer) {
|
|
4284
5074
|
const filePath = graphFilePath(rootDir, layer);
|
|
4285
|
-
if (!(0,
|
|
4286
|
-
const stat = (0,
|
|
5075
|
+
if (!(0, import_node_fs16.existsSync)(filePath)) return null;
|
|
5076
|
+
const stat = (0, import_node_fs16.statSync)(filePath);
|
|
4287
5077
|
const cached = graphCache.get(filePath);
|
|
4288
5078
|
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
4289
5079
|
return cached.graph;
|
|
4290
5080
|
}
|
|
4291
|
-
const content = (0,
|
|
5081
|
+
const content = (0, import_node_fs16.readFileSync)(filePath, "utf-8");
|
|
4292
5082
|
const graph = JSON.parse(content);
|
|
4293
5083
|
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
4294
5084
|
return graph;
|
|
4295
5085
|
}
|
|
4296
5086
|
function readGraph(rootDir, layer) {
|
|
4297
5087
|
const rawFilePath = graphFilePath(rootDir, layer);
|
|
4298
|
-
if (!(0,
|
|
5088
|
+
if (!(0, import_node_fs16.existsSync)(rawFilePath)) return null;
|
|
4299
5089
|
const rawMtime = getMtimeMs(rawFilePath);
|
|
4300
5090
|
const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
|
|
4301
5091
|
const cacheKey = `${rootDir}:${layer}`;
|
|
@@ -4325,24 +5115,24 @@ async function generateGraph(rootDir, layer) {
|
|
|
4325
5115
|
mutationMethods: config.parsers?.patterns?.mutationMethods
|
|
4326
5116
|
});
|
|
4327
5117
|
const dir = graphsDir(rootDir);
|
|
4328
|
-
(0,
|
|
5118
|
+
(0, import_node_fs16.mkdirSync)(dir, { recursive: true });
|
|
4329
5119
|
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
4330
5120
|
for (const result of results) {
|
|
4331
5121
|
const filePath = graphFilePath(rootDir, result.layer);
|
|
4332
|
-
(0,
|
|
5122
|
+
(0, import_node_fs16.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
|
|
4333
5123
|
invalidateCache(filePath);
|
|
4334
5124
|
invalidateTaggedCache(rootDir, result.layer);
|
|
4335
5125
|
}
|
|
4336
5126
|
if (!layer) {
|
|
4337
5127
|
const producedLayers = new Set(results.map((r) => r.layer));
|
|
4338
5128
|
try {
|
|
4339
|
-
for (const f of (0,
|
|
4340
|
-
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;
|
|
4341
5131
|
const layerName = f.replace(/\.json$/, "");
|
|
4342
5132
|
if (producedLayers.has(layerName)) continue;
|
|
4343
|
-
const orphan = (0,
|
|
5133
|
+
const orphan = (0, import_node_path18.join)(dir, f);
|
|
4344
5134
|
try {
|
|
4345
|
-
(0,
|
|
5135
|
+
(0, import_node_fs16.unlinkSync)(orphan);
|
|
4346
5136
|
invalidateCache(orphan);
|
|
4347
5137
|
invalidateTaggedCache(rootDir, layerName);
|
|
4348
5138
|
process.stderr.write(`[launch-chart] removed orphan layer file: ${f} (no parser produced ${layerName} this run)
|
|
@@ -4353,33 +5143,51 @@ async function generateGraph(rootDir, layer) {
|
|
|
4353
5143
|
} catch {
|
|
4354
5144
|
}
|
|
4355
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
|
+
}
|
|
4356
5164
|
return results;
|
|
4357
5165
|
}
|
|
4358
5166
|
|
|
4359
5167
|
// src/server/lockfile.ts
|
|
4360
5168
|
var import_node_child_process = require("node:child_process");
|
|
4361
|
-
var
|
|
5169
|
+
var import_node_fs17 = require("node:fs");
|
|
4362
5170
|
var import_node_os = require("node:os");
|
|
4363
|
-
var
|
|
5171
|
+
var import_node_path19 = require("node:path");
|
|
4364
5172
|
function lockDir(projectRoot) {
|
|
4365
5173
|
if (projectRoot) {
|
|
4366
|
-
return (0,
|
|
5174
|
+
return (0, import_node_path19.join)(projectRoot, ".launchsecure");
|
|
4367
5175
|
}
|
|
4368
|
-
return (0,
|
|
5176
|
+
return (0, import_node_path19.join)((0, import_node_os.homedir)(), ".launchsecure");
|
|
4369
5177
|
}
|
|
4370
5178
|
function lockPath(projectRoot) {
|
|
4371
|
-
return (0,
|
|
5179
|
+
return (0, import_node_path19.join)(lockDir(projectRoot), "launch-chart.lock");
|
|
4372
5180
|
}
|
|
4373
5181
|
var _activeProjectRoot;
|
|
4374
5182
|
function readLock(projectRoot) {
|
|
4375
5183
|
const root = projectRoot ?? _activeProjectRoot;
|
|
4376
5184
|
const p = lockPath(root);
|
|
4377
|
-
if (!(0,
|
|
5185
|
+
if (!(0, import_node_fs17.existsSync)(p)) {
|
|
4378
5186
|
if (root) {
|
|
4379
5187
|
const globalP = lockPath();
|
|
4380
|
-
if ((0,
|
|
5188
|
+
if ((0, import_node_fs17.existsSync)(globalP)) {
|
|
4381
5189
|
try {
|
|
4382
|
-
const data = JSON.parse((0,
|
|
5190
|
+
const data = JSON.parse((0, import_node_fs17.readFileSync)(globalP, "utf-8"));
|
|
4383
5191
|
if (typeof data.pid === "number" && typeof data.port === "number" && data.cwd === root) {
|
|
4384
5192
|
return data;
|
|
4385
5193
|
}
|
|
@@ -4390,7 +5198,7 @@ function readLock(projectRoot) {
|
|
|
4390
5198
|
return null;
|
|
4391
5199
|
}
|
|
4392
5200
|
try {
|
|
4393
|
-
const data = JSON.parse((0,
|
|
5201
|
+
const data = JSON.parse((0, import_node_fs17.readFileSync)(p, "utf-8"));
|
|
4394
5202
|
if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
|
|
4395
5203
|
return data;
|
|
4396
5204
|
} catch {
|
|
@@ -4427,7 +5235,7 @@ function getLiveLock(projectRoot) {
|
|
|
4427
5235
|
const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
|
|
4428
5236
|
if (!live) {
|
|
4429
5237
|
try {
|
|
4430
|
-
(0,
|
|
5238
|
+
(0, import_node_fs17.unlinkSync)(lockPath(root));
|
|
4431
5239
|
} catch {
|
|
4432
5240
|
}
|
|
4433
5241
|
return null;
|
|
@@ -4436,14 +5244,14 @@ function getLiveLock(projectRoot) {
|
|
|
4436
5244
|
}
|
|
4437
5245
|
function writeLock(data, projectRoot) {
|
|
4438
5246
|
const root = projectRoot ?? _activeProjectRoot;
|
|
4439
|
-
(0,
|
|
4440
|
-
(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");
|
|
4441
5249
|
if (root) _activeProjectRoot = root;
|
|
4442
5250
|
}
|
|
4443
5251
|
function clearLock(projectRoot) {
|
|
4444
5252
|
const root = projectRoot ?? _activeProjectRoot;
|
|
4445
5253
|
try {
|
|
4446
|
-
(0,
|
|
5254
|
+
(0, import_node_fs17.unlinkSync)(lockPath(root));
|
|
4447
5255
|
} catch {
|
|
4448
5256
|
}
|
|
4449
5257
|
}
|
|
@@ -4452,13 +5260,350 @@ function clearLock(projectRoot) {
|
|
|
4452
5260
|
init_config();
|
|
4453
5261
|
|
|
4454
5262
|
// src/server/graph/core/audit-core.ts
|
|
4455
|
-
var
|
|
4456
|
-
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
|
|
4457
5602
|
function readGraphFile(rootDir, layer) {
|
|
4458
|
-
const filePath = (0,
|
|
4459
|
-
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;
|
|
4460
5605
|
try {
|
|
4461
|
-
return JSON.parse((0,
|
|
5606
|
+
return JSON.parse((0, import_node_fs19.readFileSync)(filePath, "utf-8"));
|
|
4462
5607
|
} catch {
|
|
4463
5608
|
return null;
|
|
4464
5609
|
}
|
|
@@ -4467,8 +5612,7 @@ function checkSchemaDrift(rootDir) {
|
|
|
4467
5612
|
const findings = [];
|
|
4468
5613
|
const db = readGraphFile(rootDir, "db");
|
|
4469
5614
|
if (!db) {
|
|
4470
|
-
|
|
4471
|
-
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");
|
|
4472
5616
|
}
|
|
4473
5617
|
for (const c of db.contradictions ?? []) {
|
|
4474
5618
|
const isTableLevel = c.detail.includes("Table ") && (c.detail.includes("has no CREATE TABLE") || c.detail.includes("not in schema.prisma"));
|
|
@@ -4485,7 +5629,7 @@ function checkSchemaDrift(rootDir) {
|
|
|
4485
5629
|
function checkOrphanFks(rootDir) {
|
|
4486
5630
|
const findings = [];
|
|
4487
5631
|
const db = readGraphFile(rootDir, "db");
|
|
4488
|
-
if (!db) return
|
|
5632
|
+
if (!db) return buildSkipped("db", "orphan_fks", "no db graph");
|
|
4489
5633
|
for (const f of db.flagged_edges ?? []) {
|
|
4490
5634
|
findings.push({
|
|
4491
5635
|
id: `fk:${f.source}->${f.target}`,
|
|
@@ -4500,13 +5644,16 @@ function checkOrphanFks(rootDir) {
|
|
|
4500
5644
|
function checkUnprotectedRoutes(rootDir) {
|
|
4501
5645
|
const findings = [];
|
|
4502
5646
|
const api = readGraphFile(rootDir, "api");
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
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
|
+
);
|
|
4509
5655
|
}
|
|
5656
|
+
const routePermsContent = (0, import_node_fs19.readFileSync)(routePermsPath, "utf-8");
|
|
4510
5657
|
const registeredRoutes = /* @__PURE__ */ new Set();
|
|
4511
5658
|
const routeEntryRe = /path:\s*'([^']+)'/g;
|
|
4512
5659
|
let rm;
|
|
@@ -4550,7 +5697,7 @@ function routeMatchesPattern(route, pattern) {
|
|
|
4550
5697
|
function checkDeadScreens(rootDir) {
|
|
4551
5698
|
const findings = [];
|
|
4552
5699
|
const ui = readGraphFile(rootDir, "ui");
|
|
4553
|
-
if (!ui) return
|
|
5700
|
+
if (!ui) return buildSkipped("ui", "dead_screens", "no ui graph");
|
|
4554
5701
|
const pages = ui.nodes.filter((n) => n.type === "page" || n.type === "layout");
|
|
4555
5702
|
const navTargets = /* @__PURE__ */ new Set();
|
|
4556
5703
|
for (const e of ui.edges) {
|
|
@@ -4582,13 +5729,24 @@ function checkDeadScreens(rootDir) {
|
|
|
4582
5729
|
function checkUnenforcedPermissions(rootDir) {
|
|
4583
5730
|
const findings = [];
|
|
4584
5731
|
const staticGraph = readGraphFile(rootDir, "static");
|
|
4585
|
-
if (!staticGraph) return
|
|
5732
|
+
if (!staticGraph) return buildSkipped("static", "unenforced_permissions", "no static graph");
|
|
4586
5733
|
const permissions = staticGraph.nodes.filter((n) => n.type === "seed_permission").map((n) => ({ id: n.id, key: n.value, name: n.name }));
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
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
|
+
);
|
|
4591
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");
|
|
4592
5750
|
for (const perm of permissions) {
|
|
4593
5751
|
const regex = new RegExp(`permission:\\s*['"]${perm.key}['"]`);
|
|
4594
5752
|
if (!regex.test(routePermsContent)) {
|
|
@@ -4606,20 +5764,27 @@ function checkUnenforcedPermissions(rootDir) {
|
|
|
4606
5764
|
function checkHardcodedValues(rootDir) {
|
|
4607
5765
|
const findings = [];
|
|
4608
5766
|
const staticGraph = readGraphFile(rootDir, "static");
|
|
4609
|
-
if (!staticGraph) return
|
|
5767
|
+
if (!staticGraph) return buildSkipped("static", "hardcoded_values", "no static graph");
|
|
4610
5768
|
const knownValues = /* @__PURE__ */ new Set();
|
|
4611
5769
|
for (const n of staticGraph.nodes) {
|
|
4612
5770
|
if (n.type === "enum_value") knownValues.add(n.value);
|
|
4613
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
|
+
}
|
|
4614
5779
|
const api = readGraphFile(rootDir, "api");
|
|
4615
|
-
if (!api) return
|
|
5780
|
+
if (!api) return buildSkipped("static", "hardcoded_values", "no api graph");
|
|
4616
5781
|
const allCapsRe = /['"]([A-Z][A-Z_]{2,})['"]/g;
|
|
4617
5782
|
const seen = /* @__PURE__ */ new Set();
|
|
4618
5783
|
for (const node of api.nodes) {
|
|
4619
5784
|
if (node.type !== "endpoint") continue;
|
|
4620
|
-
const filePath = (0,
|
|
4621
|
-
if (!(0,
|
|
4622
|
-
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");
|
|
4623
5788
|
let m;
|
|
4624
5789
|
allCapsRe.lastIndex = 0;
|
|
4625
5790
|
while ((m = allCapsRe.exec(content)) !== null) {
|
|
@@ -4651,7 +5816,19 @@ function buildReport(layer, check, findings) {
|
|
|
4651
5816
|
warnings: findings.filter((f) => f.severity === "warning").length,
|
|
4652
5817
|
info: findings.filter((f) => f.severity === "info").length
|
|
4653
5818
|
},
|
|
4654
|
-
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
|
|
4655
5832
|
};
|
|
4656
5833
|
}
|
|
4657
5834
|
var CHECKS = {
|
|
@@ -4668,6 +5845,11 @@ var CHECKS = {
|
|
|
4668
5845
|
static: {
|
|
4669
5846
|
unenforced_permissions: checkUnenforcedPermissions,
|
|
4670
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 })
|
|
4671
5853
|
}
|
|
4672
5854
|
};
|
|
4673
5855
|
function getAvailableChecks() {
|
|
@@ -4714,7 +5896,12 @@ function runAudit(rootDir, layer, check) {
|
|
|
4714
5896
|
}
|
|
4715
5897
|
function formatAsPrompt(reports) {
|
|
4716
5898
|
const lines = [];
|
|
5899
|
+
const skipped = [];
|
|
4717
5900
|
for (const report of reports) {
|
|
5901
|
+
if (report.status === "skipped") {
|
|
5902
|
+
skipped.push(report);
|
|
5903
|
+
continue;
|
|
5904
|
+
}
|
|
4718
5905
|
if (report.findings.length === 0) continue;
|
|
4719
5906
|
lines.push(`## ${report.layer.toUpperCase()} \u2014 ${report.check} (${report.findings.length} findings)`);
|
|
4720
5907
|
lines.push("");
|
|
@@ -4726,6 +5913,14 @@ function formatAsPrompt(reports) {
|
|
|
4726
5913
|
}
|
|
4727
5914
|
lines.push("");
|
|
4728
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
|
+
}
|
|
4729
5924
|
if (lines.length === 0) return "No audit findings.";
|
|
4730
5925
|
return lines.join("\n");
|
|
4731
5926
|
}
|
|
@@ -4749,16 +5944,16 @@ var MIME_TYPES = {
|
|
|
4749
5944
|
function findProjectRoot(startDir) {
|
|
4750
5945
|
let dir = startDir;
|
|
4751
5946
|
for (let i = 0; i < 8; i++) {
|
|
4752
|
-
const graphsDir2 =
|
|
4753
|
-
if (
|
|
4754
|
-
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);
|
|
4755
5950
|
if (parent === dir) break;
|
|
4756
5951
|
dir = parent;
|
|
4757
5952
|
}
|
|
4758
5953
|
dir = startDir;
|
|
4759
5954
|
for (let i = 0; i < 8; i++) {
|
|
4760
|
-
if (
|
|
4761
|
-
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);
|
|
4762
5957
|
if (parent === dir) break;
|
|
4763
5958
|
dir = parent;
|
|
4764
5959
|
}
|
|
@@ -4767,7 +5962,7 @@ function findProjectRoot(startDir) {
|
|
|
4767
5962
|
function resolveRequestRoot(url, monorepoRoot, projects) {
|
|
4768
5963
|
const projectParam = url.searchParams.get("project");
|
|
4769
5964
|
if (!projectParam || projects.length === 0) return monorepoRoot;
|
|
4770
|
-
const resolved =
|
|
5965
|
+
const resolved = import_node_path22.default.resolve(monorepoRoot, projectParam);
|
|
4771
5966
|
if (!resolved.startsWith(monorepoRoot)) {
|
|
4772
5967
|
throw new Error("Project path outside monorepo root");
|
|
4773
5968
|
}
|
|
@@ -4818,16 +6013,16 @@ async function buildMergedGraph(root) {
|
|
|
4818
6013
|
};
|
|
4819
6014
|
}
|
|
4820
6015
|
function serveStatic(res, filePath) {
|
|
4821
|
-
if (!
|
|
4822
|
-
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();
|
|
4823
6018
|
const mime = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
4824
6019
|
res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
|
|
4825
|
-
|
|
6020
|
+
import_node_fs20.default.createReadStream(filePath).pipe(res);
|
|
4826
6021
|
return true;
|
|
4827
6022
|
}
|
|
4828
6023
|
function serveIndex(res, clientDir) {
|
|
4829
|
-
const indexPath =
|
|
4830
|
-
if (!
|
|
6024
|
+
const indexPath = import_node_path22.default.join(clientDir, "index.html");
|
|
6025
|
+
if (!import_node_fs20.default.existsSync(indexPath)) {
|
|
4831
6026
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
4832
6027
|
res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
|
|
4833
6028
|
return;
|
|
@@ -4879,7 +6074,7 @@ async function startChartServer(opts = {}) {
|
|
|
4879
6074
|
}
|
|
4880
6075
|
return { port: existing.port, url: existing.url };
|
|
4881
6076
|
}
|
|
4882
|
-
const clientDir = opts.clientDir ??
|
|
6077
|
+
const clientDir = opts.clientDir ?? import_node_path22.default.join(__dirname, "..", "chart-client");
|
|
4883
6078
|
const rootConfig = loadConfig(projectRoot);
|
|
4884
6079
|
const projects = rootConfig.projects ?? [];
|
|
4885
6080
|
const server = import_node_http.default.createServer((req, res) => {
|
|
@@ -4895,11 +6090,11 @@ async function startChartServer(opts = {}) {
|
|
|
4895
6090
|
}
|
|
4896
6091
|
if (req.method === "GET" && url2.pathname === "/api/projects") {
|
|
4897
6092
|
const projectList = projects.length > 0 ? projects.map((p) => {
|
|
4898
|
-
const absRoot =
|
|
4899
|
-
const hasGraphs =
|
|
4900
|
-
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"));
|
|
4901
6096
|
return { name: p.name, root: p.root, hasGraphs, hasNextConfig };
|
|
4902
|
-
}) : [{ 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 }];
|
|
4903
6098
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4904
6099
|
res.end(JSON.stringify({ projects: projectList, monorepoRoot: projectRoot }));
|
|
4905
6100
|
return;
|
|
@@ -4945,20 +6140,20 @@ async function startChartServer(opts = {}) {
|
|
|
4945
6140
|
}
|
|
4946
6141
|
if (req.method === "GET" && url2.pathname === "/api/file-content") {
|
|
4947
6142
|
const relPath = url2.searchParams.get("path");
|
|
4948
|
-
if (!relPath || relPath.includes("..") ||
|
|
6143
|
+
if (!relPath || relPath.includes("..") || import_node_path22.default.isAbsolute(relPath)) {
|
|
4949
6144
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4950
6145
|
res.end(JSON.stringify({ error: "Invalid path" }));
|
|
4951
6146
|
return;
|
|
4952
6147
|
}
|
|
4953
|
-
const filePath =
|
|
4954
|
-
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()) {
|
|
4955
6150
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
4956
6151
|
res.end(JSON.stringify({ error: "File not found" }));
|
|
4957
6152
|
return;
|
|
4958
6153
|
}
|
|
4959
|
-
const ext =
|
|
6154
|
+
const ext = import_node_path22.default.extname(filePath).toLowerCase();
|
|
4960
6155
|
const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
|
|
4961
|
-
const content =
|
|
6156
|
+
const content = import_node_fs20.default.readFileSync(filePath, "utf-8");
|
|
4962
6157
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4963
6158
|
res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
|
|
4964
6159
|
return;
|
|
@@ -5000,8 +6195,8 @@ async function startChartServer(opts = {}) {
|
|
|
5000
6195
|
req.on("end", () => {
|
|
5001
6196
|
try {
|
|
5002
6197
|
const newConfig = JSON.parse(body);
|
|
5003
|
-
const configPath =
|
|
5004
|
-
|
|
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");
|
|
5005
6200
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5006
6201
|
res.end(JSON.stringify({ ok: true }));
|
|
5007
6202
|
} catch (err) {
|
|
@@ -5034,8 +6229,8 @@ async function startChartServer(opts = {}) {
|
|
|
5034
6229
|
const taggerConfig = JSON.parse(body);
|
|
5035
6230
|
const config2 = loadConfig(reqRoot);
|
|
5036
6231
|
config2.taggers = taggerConfig;
|
|
5037
|
-
const configPath =
|
|
5038
|
-
|
|
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");
|
|
5039
6234
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5040
6235
|
res.end(JSON.stringify({ ok: true }));
|
|
5041
6236
|
} catch (err) {
|
|
@@ -5105,7 +6300,7 @@ async function startChartServer(opts = {}) {
|
|
|
5105
6300
|
dbDir: !!config2.paths?.dbDir,
|
|
5106
6301
|
srcRoots: !!(config2.paths?.srcRoots && config2.paths.srcRoots.length > 0)
|
|
5107
6302
|
};
|
|
5108
|
-
const relFromRoot = (abs) =>
|
|
6303
|
+
const relFromRoot = (abs) => import_node_path22.default.relative(reqRoot, abs) || ".";
|
|
5109
6304
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5110
6305
|
res.end(JSON.stringify({
|
|
5111
6306
|
projectRoot: reqRoot,
|
|
@@ -5127,19 +6322,19 @@ async function startChartServer(opts = {}) {
|
|
|
5127
6322
|
}
|
|
5128
6323
|
if (req.method === "GET" && url2.pathname === "/api/browse-dir") {
|
|
5129
6324
|
const browsePath = url2.searchParams.get("path") || projectRoot;
|
|
5130
|
-
const abs =
|
|
5131
|
-
const twoUp =
|
|
6325
|
+
const abs = import_node_path22.default.resolve(browsePath);
|
|
6326
|
+
const twoUp = import_node_path22.default.resolve(projectRoot, "..", "..");
|
|
5132
6327
|
if (!abs.startsWith(twoUp)) {
|
|
5133
6328
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
5134
6329
|
res.end(JSON.stringify({ ok: false, error: "Path outside allowed range" }));
|
|
5135
6330
|
return;
|
|
5136
6331
|
}
|
|
5137
6332
|
try {
|
|
5138
|
-
const entries =
|
|
6333
|
+
const entries = import_node_fs20.default.readdirSync(abs, { withFileTypes: true });
|
|
5139
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();
|
|
5140
|
-
const parent = abs !== twoUp ?
|
|
6335
|
+
const parent = abs !== twoUp ? import_node_path22.default.dirname(abs) : null;
|
|
5141
6336
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5142
|
-
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) || "." }));
|
|
5143
6338
|
} catch (err) {
|
|
5144
6339
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
5145
6340
|
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
|
@@ -5165,8 +6360,8 @@ async function startChartServer(opts = {}) {
|
|
|
5165
6360
|
const { projects: newProjects } = JSON.parse(body);
|
|
5166
6361
|
const config2 = loadConfig(projectRoot);
|
|
5167
6362
|
config2.projects = newProjects.length > 0 ? newProjects : void 0;
|
|
5168
|
-
const configPath =
|
|
5169
|
-
|
|
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");
|
|
5170
6365
|
projects.length = 0;
|
|
5171
6366
|
if (config2.projects) projects.push(...config2.projects);
|
|
5172
6367
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -5179,7 +6374,7 @@ async function startChartServer(opts = {}) {
|
|
|
5179
6374
|
return;
|
|
5180
6375
|
}
|
|
5181
6376
|
if (url2.pathname !== "/") {
|
|
5182
|
-
const staticPath =
|
|
6377
|
+
const staticPath = import_node_path22.default.join(clientDir, url2.pathname);
|
|
5183
6378
|
if (serveStatic(res, staticPath)) return;
|
|
5184
6379
|
}
|
|
5185
6380
|
serveIndex(res, clientDir);
|