@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.
Files changed (126) hide show
  1. package/README.md +50 -0
  2. package/dist/beacon/beacon.mjs +1016 -0
  3. package/dist/beacon/beacon.mjs.map +1 -0
  4. package/dist/beacon/beacon.umd.js +87 -0
  5. package/dist/beacon/beacon.umd.js.map +1 -0
  6. package/dist/beacon/index-DAIDnjfR.mjs +513 -0
  7. package/dist/beacon/index-DAIDnjfR.mjs.map +1 -0
  8. package/dist/beacon/types/capture/element.d.ts +3 -0
  9. package/dist/beacon/types/capture/element.d.ts.map +1 -0
  10. package/dist/beacon/types/capture/framework.d.ts +3 -0
  11. package/dist/beacon/types/capture/framework.d.ts.map +1 -0
  12. package/dist/beacon/types/capture/metadata.d.ts +3 -0
  13. package/dist/beacon/types/capture/metadata.d.ts.map +1 -0
  14. package/dist/beacon/types/capture/overlay.d.ts +7 -0
  15. package/dist/beacon/types/capture/overlay.d.ts.map +1 -0
  16. package/dist/beacon/types/capture/picker.d.ts +12 -0
  17. package/dist/beacon/types/capture/picker.d.ts.map +1 -0
  18. package/dist/beacon/types/capture/screenshot.d.ts +7 -0
  19. package/dist/beacon/types/capture/screenshot.d.ts.map +1 -0
  20. package/dist/beacon/types/capture/selector.d.ts +2 -0
  21. package/dist/beacon/types/capture/selector.d.ts.map +1 -0
  22. package/dist/beacon/types/element.d.ts +50 -0
  23. package/dist/beacon/types/element.d.ts.map +1 -0
  24. package/dist/beacon/types/index.d.ts +4 -0
  25. package/dist/beacon/types/index.d.ts.map +1 -0
  26. package/dist/beacon/types/transport/submit.d.ts +3 -0
  27. package/dist/beacon/types/transport/submit.d.ts.map +1 -0
  28. package/dist/beacon/types/types.d.ts +88 -0
  29. package/dist/beacon/types/types.d.ts.map +1 -0
  30. package/dist/beacon/types/ui/button.d.ts +2 -0
  31. package/dist/beacon/types/ui/button.d.ts.map +1 -0
  32. package/dist/beacon/types/ui/drawer.d.ts +31 -0
  33. package/dist/beacon/types/ui/drawer.d.ts.map +1 -0
  34. package/dist/beacon/types/ui/icons.d.ts +9 -0
  35. package/dist/beacon/types/ui/icons.d.ts.map +1 -0
  36. package/dist/beacon/types/ui/pick-mode-overlay.d.ts +25 -0
  37. package/dist/beacon/types/ui/pick-mode-overlay.d.ts.map +1 -0
  38. package/dist/beacon/types/ui/pin-popover.d.ts +14 -0
  39. package/dist/beacon/types/ui/pin-popover.d.ts.map +1 -0
  40. package/dist/chart-client/assets/{index-C8ANseEa.js → index-Bk1hawjD.js} +63 -58
  41. package/dist/chart-client/assets/index-DpaGa3bY.css +1 -0
  42. package/dist/chart-client/index.html +2 -2
  43. package/dist/client/assets/index-Bfel4OQ5.css +32 -0
  44. package/dist/client/assets/{index-Ds9UP_cj.js → index-eC-WuUWB.js} +58 -58
  45. package/dist/client/index.html +2 -2
  46. package/dist/council-client/assets/{index-Dc41S-R2.js → index-Cs_MVXHf.js} +14 -14
  47. package/dist/council-client/assets/index-P5kMsT5a.css +1 -0
  48. package/dist/council-client/index.html +2 -2
  49. package/dist/deck-client/assets/{_baseUniq-2gclQXo7.js → _baseUniq-C2xT_eYu.js} +1 -1
  50. package/dist/deck-client/assets/{arc-DcMY5Wm0.js → arc-CmVL9pGd.js} +1 -1
  51. package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-B8iirmmJ.js → architectureDiagram-Q4EWVU46-BSFgdjve.js} +1 -1
  52. package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-B4JBLjmJ.js → blockDiagram-DXYQGD6D-DuLzscvP.js} +1 -1
  53. package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-CojrJAk8.js → c4Diagram-AHTNJAMY-CfCJB8eY.js} +1 -1
  54. package/dist/deck-client/assets/channel-B4aNO8ZB.js +1 -0
  55. package/dist/deck-client/assets/{chunk-4BX2VUAB-Bmb_BMDo.js → chunk-4BX2VUAB-DxmLYTWZ.js} +1 -1
  56. package/dist/deck-client/assets/{chunk-4TB4RGXK-CumBy8qe.js → chunk-4TB4RGXK-CCnf7GFE.js} +1 -1
  57. package/dist/deck-client/assets/{chunk-55IACEB6-Ka8Hb1wD.js → chunk-55IACEB6-Db9DApcj.js} +1 -1
  58. package/dist/deck-client/assets/{chunk-EDXVE4YY-B3sIPiQo.js → chunk-EDXVE4YY-DmYDq8ZI.js} +1 -1
  59. package/dist/deck-client/assets/{chunk-FMBD7UC4-C1tYkaqu.js → chunk-FMBD7UC4-BGhUlF20.js} +1 -1
  60. package/dist/deck-client/assets/{chunk-OYMX7WX6-D7Wacbky.js → chunk-OYMX7WX6-CpEnicQZ.js} +1 -1
  61. package/dist/deck-client/assets/{chunk-QZHKN3VN-ChXI0vO3.js → chunk-QZHKN3VN-Doa7LKwf.js} +1 -1
  62. package/dist/deck-client/assets/{chunk-YZCP3GAM-BXhiqf8u.js → chunk-YZCP3GAM-CpkIlH6V.js} +1 -1
  63. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-BHTI0yWz.js +1 -0
  64. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-BHTI0yWz.js +1 -0
  65. package/dist/deck-client/assets/clone-HduFm7qU.js +1 -0
  66. package/dist/deck-client/assets/{cose-bilkent-S5V4N54A-Bqp3p68D.js → cose-bilkent-S5V4N54A-Bkh8Bfcb.js} +1 -1
  67. package/dist/deck-client/assets/{dagre-KV5264BT-BS-rtyhZ.js → dagre-KV5264BT-Bp0XpTgH.js} +1 -1
  68. package/dist/deck-client/assets/{diagram-5BDNPKRD-BIrj9YGI.js → diagram-5BDNPKRD-ZHiyGYPQ.js} +1 -1
  69. package/dist/deck-client/assets/{diagram-G4DWMVQ6-noHWPIg4.js → diagram-G4DWMVQ6-BW-Q8_H5.js} +1 -1
  70. package/dist/deck-client/assets/{diagram-MMDJMWI5-C2qHxvqV.js → diagram-MMDJMWI5-6I3LTafu.js} +1 -1
  71. package/dist/deck-client/assets/{diagram-TYMM5635-BytnGQr-.js → diagram-TYMM5635-CyM5YK28.js} +1 -1
  72. package/dist/deck-client/assets/{erDiagram-SMLLAGMA-BfK5m2YQ.js → erDiagram-SMLLAGMA-CjNxVJHk.js} +1 -1
  73. package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-Cq925G1Z.js → flowDiagram-DWJPFMVM-BDQHuAJR.js} +1 -1
  74. package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-DhhHPAmj.js → ganttDiagram-T4ZO3ILL-B7MnkpbP.js} +1 -1
  75. package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-B3Lc0h9q.js → gitGraphDiagram-UUTBAWPF-C9dZAcYD.js} +1 -1
  76. package/dist/deck-client/assets/{graph-RTawgVWm.js → graph-CjdBnzUy.js} +1 -1
  77. package/dist/deck-client/assets/{index-BfIfJXmS.js → index-DeIVPW63.js} +68 -68
  78. package/dist/deck-client/assets/index-LKZDAS9S.css +1 -0
  79. package/dist/deck-client/assets/{infoDiagram-42DDH7IO-BlR584kX.js → infoDiagram-42DDH7IO-C7d3iRC3.js} +1 -1
  80. package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-DygKoNGY.js → ishikawaDiagram-UXIWVN3A-BcYGKj09.js} +1 -1
  81. package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-BnaiYp9N.js → journeyDiagram-VCZTEJTY-DqFlRrOL.js} +1 -1
  82. package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-BQBUBzJC.js → kanban-definition-6JOO6SKY-BJhPp1NR.js} +1 -1
  83. package/dist/deck-client/assets/{layout-DeZ8HI1T.js → layout-DIeS6GvK.js} +1 -1
  84. package/dist/deck-client/assets/{linear-C6roLi_9.js → linear-He_yJy5H.js} +1 -1
  85. package/dist/deck-client/assets/{min-CbUksbuI.js → min-DQ6Kx06t.js} +1 -1
  86. package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-iNxV62yN.js → mindmap-definition-QFDTVHPH-sQ62L8T2.js} +1 -1
  87. package/dist/deck-client/assets/{pieDiagram-DEJITSTG-DHVA0jaG.js → pieDiagram-DEJITSTG-BqCWmU2K.js} +1 -1
  88. package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-DBeKKLUQ.js → quadrantDiagram-34T5L4WZ-rQ1TJOoe.js} +1 -1
  89. package/dist/deck-client/assets/{requirementDiagram-MS252O5E-CBwITx7p.js → requirementDiagram-MS252O5E-BO2MPBOM.js} +1 -1
  90. package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-BtE-1YTU.js → sankeyDiagram-XADWPNL6-BgsHEVex.js} +1 -1
  91. package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-DN96yPP2.js → sequenceDiagram-FGHM5R23-B3j1yMLU.js} +1 -1
  92. package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-VUkKC2uJ.js → stateDiagram-FHFEXIEX-C8jFlZou.js} +1 -1
  93. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-BoqepHW0.js +1 -0
  94. package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-oUeZhRns.js → timeline-definition-GMOUNBTQ-tM-qo4Zk.js} +1 -1
  95. package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-D87fK90n.js → vennDiagram-DHZGUBPP-B0-6kOEu.js} +1 -1
  96. package/dist/deck-client/assets/wardley-RL74JXVD-HpBk07P-.js +162 -0
  97. package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-Ca_i0QRA.js → wardleyDiagram-NUSXRM2D-BkA1NLDE.js} +1 -1
  98. package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-CUOJVIvq.js → xychartDiagram-5P7HB3ND-CEKGSuI-.js} +1 -1
  99. package/dist/deck-client/index.html +2 -2
  100. package/dist/server/chart-serve.js +1336 -141
  101. package/dist/server/cli.js +28423 -6671
  102. package/dist/server/council-entry.js +0 -0
  103. package/dist/server/deck-mcp-entry.js +332 -3
  104. package/dist/server/deck-serve.js +288 -0
  105. package/dist/server/fb-wizard.js +0 -0
  106. package/dist/server/graph/queries/classify.scm +8 -0
  107. package/dist/server/graph/queries/exports.scm +7 -0
  108. package/dist/server/graph-mcp-entry.js +1987 -224
  109. package/dist/server/recall-entry.js +1112 -0
  110. package/package.json +47 -21
  111. package/dist/chart-client/assets/index--120d9P9.css +0 -1
  112. package/dist/client/assets/index-Bf8zdL3x.css +0 -32
  113. package/dist/council-client/assets/index-CofZh7pS.css +0 -1
  114. package/dist/deck-client/assets/channel-ERh5jKXV.js +0 -1
  115. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-CMi1Gaev.js +0 -1
  116. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-CMi1Gaev.js +0 -1
  117. package/dist/deck-client/assets/clone-DfWhlD4X.js +0 -1
  118. package/dist/deck-client/assets/index-765AIQ9z.css +0 -1
  119. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-CA0IjulK.js +0 -1
  120. package/dist/deck-client/assets/wardley-RL74JXVD-DYbYcpDp.js +0 -162
  121. package/dist/server/deck-server/deck-mcp-entry.js +0 -1789
  122. package/dist/server/deck-server/deck-serve.js +0 -1275
  123. package/dist/server/server/chart-serve.js +0 -4643
  124. package/dist/server/server/cli.js +0 -13360
  125. package/dist/server/server/fb-wizard.js +0 -136
  126. 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
- const content = (0, import_node_fs4.readFileSync)(absPath, "utf-8");
102
- return parserInstance.parse(content);
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
- variables.push({
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
- return { elements, stateVars, conditions, variables, responses, params };
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 import_node_fs18 = __toESM(require("node:fs"));
666
- var import_node_path19 = __toESM(require("node:path"));
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 import_node_fs15 = require("node:fs");
670
- var import_node_path16 = require("node:path");
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 import_node_path12 = require("node:path");
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 import_node_path11 = require("node:path");
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 dbCalls = node._dbCalls;
1478
- if (!dbCalls) continue;
1479
- const seenModels = /* @__PURE__ */ new Set();
1480
- for (const call of dbCalls) {
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 (seenModels.has(target)) continue;
1483
- seenModels.add(target);
1484
- apiCrossRefs.push({
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 dbCalls = node._dbCalls;
1496
- if (!dbCalls) continue;
1497
- const seenModels = /* @__PURE__ */ new Set();
1498
- for (const call of dbCalls) {
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 (seenModels.has(target)) continue;
1501
- seenModels.add(target);
1502
- uiCrossRefs.push({
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, import_node_path11.resolve)(rootDir, entry.path);
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, import_node_path12.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
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 import_node_path14 = require("node:path");
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 import_node_path13 = require("node:path");
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, import_node_path13.join)(rootDir, "src"),
3837
- (0, import_node_path13.join)(rootDir, "app"),
3838
- (0, import_node_path13.join)(rootDir, "lib")
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, import_node_path13.join)(base, convention);
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, import_node_path14.resolve)(rootDir, entry.path);
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 import_node_path15 = require("node:path");
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, import_node_path15.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
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, import_node_path15.dirname)(filePath);
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, import_node_path16.join)(rootDir, GRAPHS_DIR2);
4230
- if (!(0, import_node_fs15.existsSync)(dir)) return [];
4231
- return (0, import_node_fs15.readdirSync)(dir).filter((f) => f.endsWith(".json") && f !== "tags.json").map((f) => f.replace(".json", ""));
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, import_node_path16.join)(rootDir, GRAPHS_DIR2);
5026
+ return (0, import_node_path18.join)(rootDir, GRAPHS_DIR2);
4237
5027
  }
4238
5028
  function graphFilePath(rootDir, layer) {
4239
- return (0, import_node_path16.join)(graphsDir(rootDir), `${layer}.json`);
5029
+ return (0, import_node_path18.join)(graphsDir(rootDir), `${layer}.json`);
4240
5030
  }
4241
5031
  function tagsFilePath2(rootDir) {
4242
- return (0, import_node_path16.join)(graphsDir(rootDir), "tags.json");
5032
+ return (0, import_node_path18.join)(graphsDir(rootDir), "tags.json");
4243
5033
  }
4244
5034
  function getMtimeMs(filePath) {
4245
- if (!(0, import_node_fs15.existsSync)(filePath)) return 0;
4246
- return (0, import_node_fs15.statSync)(filePath).mtimeMs;
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, import_node_fs15.existsSync)(filePath)) return null;
4286
- const stat = (0, import_node_fs15.statSync)(filePath);
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, import_node_fs15.readFileSync)(filePath, "utf-8");
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, import_node_fs15.existsSync)(rawFilePath)) return null;
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, import_node_fs15.mkdirSync)(dir, { recursive: true });
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, import_node_fs15.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
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, import_node_fs15.readdirSync)(dir)) {
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, import_node_path16.join)(dir, f);
5133
+ const orphan = (0, import_node_path18.join)(dir, f);
4344
5134
  try {
4345
- (0, import_node_fs15.unlinkSync)(orphan);
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 import_node_fs16 = require("node:fs");
5169
+ var import_node_fs17 = require("node:fs");
4362
5170
  var import_node_os = require("node:os");
4363
- var import_node_path17 = require("node:path");
5171
+ var import_node_path19 = require("node:path");
4364
5172
  function lockDir(projectRoot) {
4365
5173
  if (projectRoot) {
4366
- return (0, import_node_path17.join)(projectRoot, ".launchsecure");
5174
+ return (0, import_node_path19.join)(projectRoot, ".launchsecure");
4367
5175
  }
4368
- return (0, import_node_path17.join)((0, import_node_os.homedir)(), ".launchsecure");
5176
+ return (0, import_node_path19.join)((0, import_node_os.homedir)(), ".launchsecure");
4369
5177
  }
4370
5178
  function lockPath(projectRoot) {
4371
- return (0, import_node_path17.join)(lockDir(projectRoot), "launch-chart.lock");
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, import_node_fs16.existsSync)(p)) {
5185
+ if (!(0, import_node_fs17.existsSync)(p)) {
4378
5186
  if (root) {
4379
5187
  const globalP = lockPath();
4380
- if ((0, import_node_fs16.existsSync)(globalP)) {
5188
+ if ((0, import_node_fs17.existsSync)(globalP)) {
4381
5189
  try {
4382
- const data = JSON.parse((0, import_node_fs16.readFileSync)(globalP, "utf-8"));
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, import_node_fs16.readFileSync)(p, "utf-8"));
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, import_node_fs16.unlinkSync)(lockPath(root));
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, import_node_fs16.mkdirSync)(lockDir(root), { recursive: true });
4440
- (0, import_node_fs16.writeFileSync)(lockPath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
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, import_node_fs16.unlinkSync)(lockPath(root));
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 import_node_fs17 = require("node:fs");
4456
- var import_node_path18 = require("node:path");
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, import_node_path18.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
4459
- if (!(0, import_node_fs17.existsSync)(filePath)) return null;
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, import_node_fs17.readFileSync)(filePath, "utf-8"));
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
- findings.push({ id: "no-db-graph", severity: "info", category: "schema_drift", title: "No DB graph", detail: "Run generate_graph first to populate the DB layer." });
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 buildReport("db", "orphan_fks", findings);
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
- const staticGraph = readGraphFile(rootDir, "static");
4504
- if (!api) return buildReport("api", "unprotected_routes", findings);
4505
- const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
4506
- let routePermsContent = "";
4507
- if ((0, import_node_fs17.existsSync)(routePermsPath)) {
4508
- routePermsContent = (0, import_node_fs17.readFileSync)(routePermsPath, "utf-8");
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 buildReport("ui", "dead_screens", findings);
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 buildReport("static", "unenforced_permissions", findings);
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
- const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
4588
- let routePermsContent = "";
4589
- if ((0, import_node_fs17.existsSync)(routePermsPath)) {
4590
- routePermsContent = (0, import_node_fs17.readFileSync)(routePermsPath, "utf-8");
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 buildReport("static", "hardcoded_values", findings);
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 buildReport("static", "hardcoded_values", findings);
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, import_node_path18.join)(rootDir, "src", node.id);
4621
- if (!(0, import_node_fs17.existsSync)(filePath)) continue;
4622
- const content = (0, import_node_fs17.readFileSync)(filePath, "utf-8");
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 = import_node_path19.default.join(dir, ".launchsecure", "graphs");
4753
- if (import_node_fs18.default.existsSync(import_node_path19.default.join(graphsDir2, "ui.json")) || import_node_fs18.default.existsSync(import_node_path19.default.join(graphsDir2, "api.json")) || import_node_fs18.default.existsSync(import_node_path19.default.join(graphsDir2, "db.json"))) return dir;
4754
- const parent = import_node_path19.default.dirname(dir);
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 (import_node_fs18.default.existsSync(import_node_path19.default.join(dir, ".git"))) return dir;
4761
- const parent = import_node_path19.default.dirname(dir);
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 = import_node_path19.default.resolve(monorepoRoot, projectParam);
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 (!import_node_fs18.default.existsSync(filePath) || !import_node_fs18.default.statSync(filePath).isFile()) return false;
4822
- const ext = import_node_path19.default.extname(filePath).toLowerCase();
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
- import_node_fs18.default.createReadStream(filePath).pipe(res);
6020
+ import_node_fs20.default.createReadStream(filePath).pipe(res);
4826
6021
  return true;
4827
6022
  }
4828
6023
  function serveIndex(res, clientDir) {
4829
- const indexPath = import_node_path19.default.join(clientDir, "index.html");
4830
- if (!import_node_fs18.default.existsSync(indexPath)) {
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 ?? import_node_path19.default.join(__dirname, "..", "chart-client");
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 = import_node_path19.default.resolve(projectRoot, p.root);
4899
- const hasGraphs = import_node_fs18.default.existsSync(import_node_path19.default.join(absRoot, ".launchsecure", "graphs"));
4900
- const hasNextConfig = import_node_fs18.default.existsSync(import_node_path19.default.join(absRoot, "next.config.ts")) || import_node_fs18.default.existsSync(import_node_path19.default.join(absRoot, "next.config.js")) || import_node_fs18.default.existsSync(import_node_path19.default.join(absRoot, "next.config.mjs"));
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: import_node_path19.default.basename(projectRoot), root: ".", hasGraphs: import_node_fs18.default.existsSync(import_node_path19.default.join(projectRoot, ".launchsecure", "graphs")), hasNextConfig: true }];
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("..") || import_node_path19.default.isAbsolute(relPath)) {
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 = import_node_path19.default.join(reqRoot, relPath);
4954
- if (!filePath.startsWith(reqRoot) || !import_node_fs18.default.existsSync(filePath) || !import_node_fs18.default.statSync(filePath).isFile()) {
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 = import_node_path19.default.extname(filePath).toLowerCase();
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 = import_node_fs18.default.readFileSync(filePath, "utf-8");
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 = import_node_path19.default.join(reqRoot, ".launchchart.json");
5004
- import_node_fs18.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
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 = import_node_path19.default.join(reqRoot, ".launchchart.json");
5038
- import_node_fs18.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
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) => import_node_path19.default.relative(reqRoot, 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 = import_node_path19.default.resolve(browsePath);
5131
- const twoUp = import_node_path19.default.resolve(projectRoot, "..", "..");
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 = import_node_fs18.default.readdirSync(abs, { withFileTypes: true });
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 ? import_node_path19.default.dirname(abs) : null;
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: import_node_path19.default.relative(projectRoot, abs) || "." }));
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 = import_node_path19.default.join(projectRoot, ".launchchart.json");
5169
- import_node_fs18.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
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 = import_node_path19.default.join(clientDir, url2.pathname);
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);