@launchsecure/launch-kit 0.0.24 → 0.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/chart-serve.js +347 -26
- package/dist/server/cli.js +347 -26
- package/dist/server/graph-mcp-entry.js +347 -26
- package/package.json +1 -1
|
@@ -58,11 +58,14 @@ var init_config = __esm({
|
|
|
58
58
|
var ts_extractor_exports = {};
|
|
59
59
|
__export(ts_extractor_exports, {
|
|
60
60
|
classifyFile: () => classifyFile,
|
|
61
|
+
classifyRouteAgainstMiddleware: () => classifyRouteAgainstMiddleware,
|
|
61
62
|
createQuery: () => createQuery,
|
|
62
63
|
extractAuthWrappersTS: () => extractAuthWrappersTS,
|
|
63
64
|
extractDbCallsTS: () => extractDbCallsTS,
|
|
64
65
|
extractDeep: () => extractDeep,
|
|
66
|
+
extractMiddlewareAuthTS: () => extractMiddlewareAuthTS,
|
|
65
67
|
initTreeSitter: () => initTreeSitter,
|
|
68
|
+
middlewarePatternToRegex: () => middlewarePatternToRegex,
|
|
66
69
|
parseCodeTS: () => parseCodeTS,
|
|
67
70
|
parseFileTS: () => parseFileTS,
|
|
68
71
|
setExtractorConfig: () => setExtractorConfig
|
|
@@ -456,6 +459,180 @@ function extractAuthWrappersTS(absPath) {
|
|
|
456
459
|
}
|
|
457
460
|
return wrappers;
|
|
458
461
|
}
|
|
462
|
+
function inferIntentFromName(name) {
|
|
463
|
+
for (const re of EXEMPT_NAME_PATTERNS) {
|
|
464
|
+
if (re.test(name)) return { intent: "exempt", hint: `name "${name}" matches /${re.source}/` };
|
|
465
|
+
}
|
|
466
|
+
for (const re of PROTECT_NAME_PATTERNS) {
|
|
467
|
+
if (re.test(name)) return { intent: "protect", hint: `name "${name}" matches /${re.source}/` };
|
|
468
|
+
}
|
|
469
|
+
return { intent: "ambiguous", hint: `name "${name}" has no exempt/protect signal` };
|
|
470
|
+
}
|
|
471
|
+
function looksLikeRoutePattern(s) {
|
|
472
|
+
return s.startsWith("/") && !s.startsWith("//");
|
|
473
|
+
}
|
|
474
|
+
function collectStringsFromArray(arrNode) {
|
|
475
|
+
const out = [];
|
|
476
|
+
for (const child of arrNode.children) {
|
|
477
|
+
if (child.type !== "string") continue;
|
|
478
|
+
const frag = childOfType(child, "string_fragment");
|
|
479
|
+
if (frag) out.push(frag.text);
|
|
480
|
+
}
|
|
481
|
+
return out;
|
|
482
|
+
}
|
|
483
|
+
function findArrayInValue(valueNode) {
|
|
484
|
+
if (!valueNode) return null;
|
|
485
|
+
if (valueNode.type === "array") return valueNode;
|
|
486
|
+
if (valueNode.type === "call_expression") {
|
|
487
|
+
const args = childOfType(valueNode, "arguments");
|
|
488
|
+
if (!args) return null;
|
|
489
|
+
for (const c of args.children) {
|
|
490
|
+
if (c.type === "array") return c;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
function extractMatcherFromDeclarator(decl) {
|
|
496
|
+
const nameNode = childOfType(decl, "identifier");
|
|
497
|
+
if (!nameNode) return null;
|
|
498
|
+
let valueNode;
|
|
499
|
+
for (const c of decl.children) {
|
|
500
|
+
if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
|
|
501
|
+
valueNode = c;
|
|
502
|
+
}
|
|
503
|
+
const arr = findArrayInValue(valueNode);
|
|
504
|
+
if (!arr) return null;
|
|
505
|
+
const strings = collectStringsFromArray(arr);
|
|
506
|
+
const routes = strings.filter(looksLikeRoutePattern);
|
|
507
|
+
if (routes.length === 0) return null;
|
|
508
|
+
const { intent, hint } = inferIntentFromName(nameNode.text);
|
|
509
|
+
return { name: nameNode.text, patterns: routes, intent, hint };
|
|
510
|
+
}
|
|
511
|
+
function extractMatchersFromObject(objNode) {
|
|
512
|
+
const out = [];
|
|
513
|
+
for (const pair of childrenOfType(objNode, "pair")) {
|
|
514
|
+
const key = childOfType(pair, "property_identifier");
|
|
515
|
+
if (!key) continue;
|
|
516
|
+
const arr = pair.children.find((c) => c.type === "array");
|
|
517
|
+
if (!arr) continue;
|
|
518
|
+
const routes = collectStringsFromArray(arr).filter(looksLikeRoutePattern);
|
|
519
|
+
if (routes.length === 0) continue;
|
|
520
|
+
const { intent, hint } = inferIntentFromName(key.text);
|
|
521
|
+
out.push({ name: key.text, patterns: routes, intent, hint });
|
|
522
|
+
}
|
|
523
|
+
return out;
|
|
524
|
+
}
|
|
525
|
+
function detectFallthroughProtect(root) {
|
|
526
|
+
const text = root.text;
|
|
527
|
+
const signals = [
|
|
528
|
+
/\bauth\.protect\s*\(/,
|
|
529
|
+
/\bauth\(\)\.protect\s*\(/,
|
|
530
|
+
/\bredirect\s*\(\s*['"`]\/(sign-?in|log-?in|auth)/i,
|
|
531
|
+
/\bNextResponse\.redirect\s*\(/,
|
|
532
|
+
/\bthrow\s+new\s+\w*Unauthorized/i
|
|
533
|
+
];
|
|
534
|
+
return signals.some((re) => re.test(text));
|
|
535
|
+
}
|
|
536
|
+
function extractMiddlewareAuthTS(absPath) {
|
|
537
|
+
if (!require("node:fs").existsSync(absPath)) return null;
|
|
538
|
+
const tree = parseSource(absPath);
|
|
539
|
+
const root = tree.rootNode;
|
|
540
|
+
const matchers = [];
|
|
541
|
+
for (const stmt of root.children) {
|
|
542
|
+
if (stmt.type !== "lexical_declaration" && stmt.type !== "variable_declaration") continue;
|
|
543
|
+
for (const decl of childrenOfType(stmt, "variable_declarator")) {
|
|
544
|
+
const m = extractMatcherFromDeclarator(decl);
|
|
545
|
+
if (m) matchers.push(m);
|
|
546
|
+
let valueNode;
|
|
547
|
+
for (const c of decl.children) {
|
|
548
|
+
if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
|
|
549
|
+
valueNode = c;
|
|
550
|
+
}
|
|
551
|
+
if (valueNode?.type === "object") {
|
|
552
|
+
for (const inner of extractMatchersFromObject(valueNode)) matchers.push(inner);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
for (const stmt of root.children) {
|
|
557
|
+
if (stmt.type !== "export_statement") continue;
|
|
558
|
+
const decl = childOfType(stmt, "lexical_declaration") ?? childOfType(stmt, "variable_declaration");
|
|
559
|
+
if (!decl) continue;
|
|
560
|
+
for (const d of childrenOfType(decl, "variable_declarator")) {
|
|
561
|
+
let valueNode;
|
|
562
|
+
for (const c of d.children) {
|
|
563
|
+
if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
|
|
564
|
+
valueNode = c;
|
|
565
|
+
}
|
|
566
|
+
if (valueNode?.type === "object") {
|
|
567
|
+
for (const inner of extractMatchersFromObject(valueNode)) matchers.push(inner);
|
|
568
|
+
}
|
|
569
|
+
const m = extractMatcherFromDeclarator(d);
|
|
570
|
+
if (m && !matchers.some((x) => x.name === m.name && x.patterns.join() === m.patterns.join())) {
|
|
571
|
+
matchers.push(m);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
file: absPath,
|
|
577
|
+
matchers,
|
|
578
|
+
hasFallthroughProtect: detectFallthroughProtect(root)
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
function middlewarePatternToRegex(pattern) {
|
|
582
|
+
if (/\(\?\!/.test(pattern)) return null;
|
|
583
|
+
if (pattern.startsWith("(")) return null;
|
|
584
|
+
let src = "^";
|
|
585
|
+
let i = 0;
|
|
586
|
+
while (i < pattern.length) {
|
|
587
|
+
const ch = pattern[i];
|
|
588
|
+
if (ch === "(" && pattern.slice(i, i + 4) === "(.*)") {
|
|
589
|
+
src += ".*";
|
|
590
|
+
i += 4;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (ch === ":") {
|
|
594
|
+
i++;
|
|
595
|
+
while (i < pattern.length && /[a-zA-Z0-9_]/.test(pattern[i])) i++;
|
|
596
|
+
src += "[^/]+";
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
if (ch === "*") {
|
|
600
|
+
i++;
|
|
601
|
+
while (i < pattern.length && /[a-zA-Z0-9_]/.test(pattern[i])) i++;
|
|
602
|
+
if (pattern[i] === "?") {
|
|
603
|
+
i++;
|
|
604
|
+
src += ".*";
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
src += ".+";
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
if (/[.\\+?^${}()|[\]]/.test(ch)) {
|
|
611
|
+
src += "\\" + ch;
|
|
612
|
+
i++;
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
src += ch;
|
|
616
|
+
i++;
|
|
617
|
+
}
|
|
618
|
+
src += "$";
|
|
619
|
+
try {
|
|
620
|
+
return new RegExp(src);
|
|
621
|
+
} catch {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
function classifyRouteAgainstMiddleware(routePath, info) {
|
|
626
|
+
for (const m of info.matchers) {
|
|
627
|
+
if (m.intent === "ambiguous") continue;
|
|
628
|
+
for (const pat of m.patterns) {
|
|
629
|
+
const re = middlewarePatternToRegex(pat);
|
|
630
|
+
if (!re) continue;
|
|
631
|
+
if (re.test(routePath)) return { intent: m.intent, matcher: m.name, pattern: pat };
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
459
636
|
function trunc(s, max = 120) {
|
|
460
637
|
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
461
638
|
}
|
|
@@ -612,7 +789,7 @@ function extractDeep(absPath) {
|
|
|
612
789
|
}
|
|
613
790
|
return { elements, stateVars, conditions, variables, responses, params };
|
|
614
791
|
}
|
|
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;
|
|
792
|
+
var import_node_fs4, import_node_path4, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, SUPABASE_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods, INLINE_AUTH_IMPORTS, EXEMPT_NAME_PATTERNS, PROTECT_NAME_PATTERNS;
|
|
616
793
|
var init_ts_extractor = __esm({
|
|
617
794
|
"src/server/graph/core/ts-extractor.ts"() {
|
|
618
795
|
"use strict";
|
|
@@ -651,6 +828,29 @@ var init_ts_extractor = __esm({
|
|
|
651
828
|
{ module: /^@auth\//, helpers: ["auth"] },
|
|
652
829
|
{ module: /^@supabase\/auth-helpers/, helpers: ["createServerClient"] }
|
|
653
830
|
];
|
|
831
|
+
EXEMPT_NAME_PATTERNS = [
|
|
832
|
+
/^is_?public/i,
|
|
833
|
+
/^public_?routes?/i,
|
|
834
|
+
/^public_?paths?/i,
|
|
835
|
+
/^whitelist/i,
|
|
836
|
+
/^allowlist/i,
|
|
837
|
+
/^unauthenticated/i,
|
|
838
|
+
/^anonymous/i,
|
|
839
|
+
/^guest/i,
|
|
840
|
+
/^skip_?auth/i,
|
|
841
|
+
/^bypass/i
|
|
842
|
+
];
|
|
843
|
+
PROTECT_NAME_PATTERNS = [
|
|
844
|
+
/^is_?protected/i,
|
|
845
|
+
/^protected_?routes?/i,
|
|
846
|
+
/^protected_?paths?/i,
|
|
847
|
+
/^require_?auth/i,
|
|
848
|
+
/^auth_?required/i,
|
|
849
|
+
/^private_?routes?/i,
|
|
850
|
+
/^is_?admin/i,
|
|
851
|
+
/^admin_?routes?/i,
|
|
852
|
+
/^secured/i
|
|
853
|
+
];
|
|
654
854
|
}
|
|
655
855
|
});
|
|
656
856
|
|
|
@@ -691,15 +891,9 @@ var import_node_fs2 = require("node:fs");
|
|
|
691
891
|
var import_node_path2 = require("node:path");
|
|
692
892
|
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
693
893
|
"node_modules",
|
|
694
|
-
".git",
|
|
695
|
-
".next",
|
|
696
|
-
".launchsecure",
|
|
697
|
-
".claude",
|
|
698
894
|
"dist",
|
|
699
895
|
"build",
|
|
700
896
|
"out",
|
|
701
|
-
".turbo",
|
|
702
|
-
".vercel",
|
|
703
897
|
"coverage"
|
|
704
898
|
]);
|
|
705
899
|
function walk(dir, exts) {
|
|
@@ -721,6 +915,7 @@ function walkWithIgnore(dir, exts, opts = {}) {
|
|
|
721
915
|
const skip = opts.extraIgnore ? /* @__PURE__ */ new Set([...DEFAULT_IGNORE_DIRS, ...opts.extraIgnore]) : DEFAULT_IGNORE_DIRS;
|
|
722
916
|
for (const entry of (0, import_node_fs2.readdirSync)(dir, { withFileTypes: true })) {
|
|
723
917
|
if (entry.isDirectory()) {
|
|
918
|
+
if (entry.name.startsWith(".")) continue;
|
|
724
919
|
if (skip.has(entry.name)) continue;
|
|
725
920
|
results.push(...walkWithIgnore((0, import_node_path2.join)(dir, entry.name), exts, opts));
|
|
726
921
|
} else if (exts.includes((0, import_node_path2.extname)(entry.name))) {
|
|
@@ -1472,43 +1667,169 @@ function generate(rootDir) {
|
|
|
1472
1667
|
nodeIdSet.add(externalId);
|
|
1473
1668
|
uiEdges.push(...edgesFromThis);
|
|
1474
1669
|
}
|
|
1670
|
+
const tablesByFile = /* @__PURE__ */ new Map();
|
|
1671
|
+
const allDbNodes = [...apiNodes, ...uiNodes];
|
|
1672
|
+
for (const node of allDbNodes) {
|
|
1673
|
+
const calls = node._dbCalls;
|
|
1674
|
+
if (!calls || calls.length === 0) continue;
|
|
1675
|
+
const map = /* @__PURE__ */ new Map();
|
|
1676
|
+
for (const c of calls) {
|
|
1677
|
+
const key = `${c.kind}:${c.model}:${c.isMutation ? "m" : "r"}`;
|
|
1678
|
+
if (!map.has(key)) {
|
|
1679
|
+
map.set(key, { model: c.model, method: c.method, isMutation: c.isMutation, kind: c.kind, via: [] });
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
tablesByFile.set(node.id, map);
|
|
1683
|
+
}
|
|
1684
|
+
const reverseRuntimeImports = /* @__PURE__ */ new Map();
|
|
1685
|
+
for (const edge of uiEdges) {
|
|
1686
|
+
if (edge.type !== "imports" && edge.type !== "renders") continue;
|
|
1687
|
+
if (!reverseRuntimeImports.has(edge.target)) {
|
|
1688
|
+
reverseRuntimeImports.set(edge.target, /* @__PURE__ */ new Set());
|
|
1689
|
+
}
|
|
1690
|
+
reverseRuntimeImports.get(edge.target).add(edge.source);
|
|
1691
|
+
}
|
|
1692
|
+
let changed = true;
|
|
1693
|
+
let iterations = 0;
|
|
1694
|
+
while (changed && iterations < 50) {
|
|
1695
|
+
changed = false;
|
|
1696
|
+
iterations++;
|
|
1697
|
+
for (const [target, tableMap] of [...tablesByFile]) {
|
|
1698
|
+
const importers = reverseRuntimeImports.get(target);
|
|
1699
|
+
if (!importers) continue;
|
|
1700
|
+
for (const importer of importers) {
|
|
1701
|
+
if (importer === target) continue;
|
|
1702
|
+
let importerMap = tablesByFile.get(importer);
|
|
1703
|
+
if (!importerMap) {
|
|
1704
|
+
importerMap = /* @__PURE__ */ new Map();
|
|
1705
|
+
tablesByFile.set(importer, importerMap);
|
|
1706
|
+
}
|
|
1707
|
+
for (const [key, call] of tableMap) {
|
|
1708
|
+
if (importerMap.has(key)) continue;
|
|
1709
|
+
importerMap.set(key, {
|
|
1710
|
+
model: call.model,
|
|
1711
|
+
method: null,
|
|
1712
|
+
isMutation: call.isMutation,
|
|
1713
|
+
kind: call.kind,
|
|
1714
|
+
via: [...call.via, target]
|
|
1715
|
+
});
|
|
1716
|
+
changed = true;
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
for (const node of apiNodes) {
|
|
1722
|
+
const map = tablesByFile.get(node.id);
|
|
1723
|
+
if (!map) continue;
|
|
1724
|
+
node.db_models = [...new Set([...map.values()].map((c) => c.model))];
|
|
1725
|
+
node.db_operations = [...new Set(
|
|
1726
|
+
[...map.values()].filter((c) => c.via.length === 0 && c.method).map((c) => `${c.model}.${c.method}`)
|
|
1727
|
+
)];
|
|
1728
|
+
node.mutates = [...map.values()].some((c) => c.isMutation);
|
|
1729
|
+
}
|
|
1475
1730
|
const apiCrossRefs = [];
|
|
1476
1731
|
for (const node of apiNodes) {
|
|
1477
|
-
const
|
|
1478
|
-
if (!
|
|
1479
|
-
|
|
1480
|
-
|
|
1732
|
+
const map = tablesByFile.get(node.id);
|
|
1733
|
+
if (!map) {
|
|
1734
|
+
delete node._dbCalls;
|
|
1735
|
+
continue;
|
|
1736
|
+
}
|
|
1737
|
+
const seenTargets = /* @__PURE__ */ new Set();
|
|
1738
|
+
for (const call of map.values()) {
|
|
1481
1739
|
const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
|
|
1482
|
-
if (
|
|
1483
|
-
|
|
1484
|
-
|
|
1740
|
+
if (seenTargets.has(target)) continue;
|
|
1741
|
+
seenTargets.add(target);
|
|
1742
|
+
const isTransitive = call.via.length > 0;
|
|
1743
|
+
const ref = {
|
|
1485
1744
|
source: node.id,
|
|
1486
1745
|
target,
|
|
1487
|
-
type: call.isMutation ? "mutates" : "reads",
|
|
1746
|
+
type: call.isMutation ? isTransitive ? "mutates_via" : "mutates" : isTransitive ? "reads_via" : "reads",
|
|
1488
1747
|
layer: "db"
|
|
1489
|
-
}
|
|
1748
|
+
};
|
|
1749
|
+
if (isTransitive) ref.via = call.via;
|
|
1750
|
+
apiCrossRefs.push(ref);
|
|
1490
1751
|
}
|
|
1491
1752
|
delete node._dbCalls;
|
|
1492
1753
|
}
|
|
1493
1754
|
const uiCrossRefs = [];
|
|
1494
1755
|
for (const node of uiNodes) {
|
|
1495
|
-
const
|
|
1496
|
-
if (!
|
|
1497
|
-
|
|
1498
|
-
|
|
1756
|
+
const map = tablesByFile.get(node.id);
|
|
1757
|
+
if (!map) {
|
|
1758
|
+
if (node._dbCalls) delete node._dbCalls;
|
|
1759
|
+
continue;
|
|
1760
|
+
}
|
|
1761
|
+
const seenTargets = /* @__PURE__ */ new Set();
|
|
1762
|
+
for (const call of map.values()) {
|
|
1499
1763
|
const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
|
|
1500
|
-
if (
|
|
1501
|
-
|
|
1502
|
-
|
|
1764
|
+
if (seenTargets.has(target)) continue;
|
|
1765
|
+
seenTargets.add(target);
|
|
1766
|
+
const isTransitive = call.via.length > 0;
|
|
1767
|
+
const ref = {
|
|
1503
1768
|
source: node.id,
|
|
1504
1769
|
target,
|
|
1505
|
-
type: call.isMutation ? "mutates" : "reads",
|
|
1770
|
+
type: call.isMutation ? isTransitive ? "mutates_via" : "mutates" : isTransitive ? "reads_via" : "reads",
|
|
1506
1771
|
layer: "db"
|
|
1507
|
-
}
|
|
1772
|
+
};
|
|
1773
|
+
if (isTransitive) ref.via = call.via;
|
|
1774
|
+
uiCrossRefs.push(ref);
|
|
1508
1775
|
}
|
|
1509
|
-
delete node._dbCalls;
|
|
1776
|
+
if (node._dbCalls) delete node._dbCalls;
|
|
1510
1777
|
}
|
|
1511
1778
|
uiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
1779
|
+
const middlewareInfos = [];
|
|
1780
|
+
for (const conv of paths.conventionFiles) {
|
|
1781
|
+
if (!/middleware\.tsx?$/.test(conv)) continue;
|
|
1782
|
+
try {
|
|
1783
|
+
const info = extractMiddlewareAuthTS(conv);
|
|
1784
|
+
if (info && info.matchers.length > 0) middlewareInfos.push(info);
|
|
1785
|
+
} catch {
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
if (middlewareInfos.length > 0) {
|
|
1789
|
+
let setAuth2 = function(node, newTags, replaceAll) {
|
|
1790
|
+
const existing = node.auth ?? [];
|
|
1791
|
+
const meaningful = existing.filter((a) => a !== "public");
|
|
1792
|
+
const merged = replaceAll ? newTags : [.../* @__PURE__ */ new Set([...newTags, ...meaningful])];
|
|
1793
|
+
node.auth = merged.length > 0 ? merged : ["public"];
|
|
1794
|
+
}, applyMiddleware2 = function(node, routePath) {
|
|
1795
|
+
let resolved = null;
|
|
1796
|
+
let label = "";
|
|
1797
|
+
let hasAnyExemptMatcher = false;
|
|
1798
|
+
let hasAnyFallthrough = false;
|
|
1799
|
+
for (const info of middlewareInfos) {
|
|
1800
|
+
if (info.hasFallthroughProtect) hasAnyFallthrough = true;
|
|
1801
|
+
if (info.matchers.some((m) => m.intent === "exempt")) hasAnyExemptMatcher = true;
|
|
1802
|
+
const c = classifyRouteAgainstMiddleware(routePath, info);
|
|
1803
|
+
if (!c) continue;
|
|
1804
|
+
if (!resolved) {
|
|
1805
|
+
resolved = c.intent;
|
|
1806
|
+
label = c.matcher;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
if (resolved === "exempt") {
|
|
1810
|
+
setAuth2(node, ["public"], true);
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
if (resolved === "protect") {
|
|
1814
|
+
setAuth2(node, [`middleware:${label}`], false);
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
if (hasAnyExemptMatcher && hasAnyFallthrough) {
|
|
1818
|
+
setAuth2(node, ["middleware-protected"], false);
|
|
1819
|
+
}
|
|
1820
|
+
};
|
|
1821
|
+
var setAuth = setAuth2, applyMiddleware = applyMiddleware2;
|
|
1822
|
+
for (const node of apiNodes) {
|
|
1823
|
+
const routePath = node.path;
|
|
1824
|
+
if (!routePath) continue;
|
|
1825
|
+
applyMiddleware2(node, routePath);
|
|
1826
|
+
}
|
|
1827
|
+
for (const node of uiNodes) {
|
|
1828
|
+
const route = node.route;
|
|
1829
|
+
if (!route) continue;
|
|
1830
|
+
applyMiddleware2(node, route);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1512
1833
|
const apiNodeIds = new Set(apiNodes.map((n) => n.id));
|
|
1513
1834
|
const apiEdges = [];
|
|
1514
1835
|
const uiOnlyEdges = [];
|
package/dist/server/cli.js
CHANGED
|
@@ -3733,11 +3733,14 @@ var init_config = __esm({
|
|
|
3733
3733
|
var ts_extractor_exports = {};
|
|
3734
3734
|
__export(ts_extractor_exports, {
|
|
3735
3735
|
classifyFile: () => classifyFile,
|
|
3736
|
+
classifyRouteAgainstMiddleware: () => classifyRouteAgainstMiddleware,
|
|
3736
3737
|
createQuery: () => createQuery,
|
|
3737
3738
|
extractAuthWrappersTS: () => extractAuthWrappersTS,
|
|
3738
3739
|
extractDbCallsTS: () => extractDbCallsTS,
|
|
3739
3740
|
extractDeep: () => extractDeep,
|
|
3741
|
+
extractMiddlewareAuthTS: () => extractMiddlewareAuthTS,
|
|
3740
3742
|
initTreeSitter: () => initTreeSitter,
|
|
3743
|
+
middlewarePatternToRegex: () => middlewarePatternToRegex,
|
|
3741
3744
|
parseCodeTS: () => parseCodeTS,
|
|
3742
3745
|
parseFileTS: () => parseFileTS,
|
|
3743
3746
|
setExtractorConfig: () => setExtractorConfig
|
|
@@ -4131,6 +4134,180 @@ function extractAuthWrappersTS(absPath) {
|
|
|
4131
4134
|
}
|
|
4132
4135
|
return wrappers;
|
|
4133
4136
|
}
|
|
4137
|
+
function inferIntentFromName(name) {
|
|
4138
|
+
for (const re of EXEMPT_NAME_PATTERNS) {
|
|
4139
|
+
if (re.test(name)) return { intent: "exempt", hint: `name "${name}" matches /${re.source}/` };
|
|
4140
|
+
}
|
|
4141
|
+
for (const re of PROTECT_NAME_PATTERNS) {
|
|
4142
|
+
if (re.test(name)) return { intent: "protect", hint: `name "${name}" matches /${re.source}/` };
|
|
4143
|
+
}
|
|
4144
|
+
return { intent: "ambiguous", hint: `name "${name}" has no exempt/protect signal` };
|
|
4145
|
+
}
|
|
4146
|
+
function looksLikeRoutePattern(s) {
|
|
4147
|
+
return s.startsWith("/") && !s.startsWith("//");
|
|
4148
|
+
}
|
|
4149
|
+
function collectStringsFromArray(arrNode) {
|
|
4150
|
+
const out = [];
|
|
4151
|
+
for (const child of arrNode.children) {
|
|
4152
|
+
if (child.type !== "string") continue;
|
|
4153
|
+
const frag = childOfType(child, "string_fragment");
|
|
4154
|
+
if (frag) out.push(frag.text);
|
|
4155
|
+
}
|
|
4156
|
+
return out;
|
|
4157
|
+
}
|
|
4158
|
+
function findArrayInValue(valueNode) {
|
|
4159
|
+
if (!valueNode) return null;
|
|
4160
|
+
if (valueNode.type === "array") return valueNode;
|
|
4161
|
+
if (valueNode.type === "call_expression") {
|
|
4162
|
+
const args = childOfType(valueNode, "arguments");
|
|
4163
|
+
if (!args) return null;
|
|
4164
|
+
for (const c of args.children) {
|
|
4165
|
+
if (c.type === "array") return c;
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
return null;
|
|
4169
|
+
}
|
|
4170
|
+
function extractMatcherFromDeclarator(decl) {
|
|
4171
|
+
const nameNode = childOfType(decl, "identifier");
|
|
4172
|
+
if (!nameNode) return null;
|
|
4173
|
+
let valueNode;
|
|
4174
|
+
for (const c of decl.children) {
|
|
4175
|
+
if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
|
|
4176
|
+
valueNode = c;
|
|
4177
|
+
}
|
|
4178
|
+
const arr = findArrayInValue(valueNode);
|
|
4179
|
+
if (!arr) return null;
|
|
4180
|
+
const strings = collectStringsFromArray(arr);
|
|
4181
|
+
const routes = strings.filter(looksLikeRoutePattern);
|
|
4182
|
+
if (routes.length === 0) return null;
|
|
4183
|
+
const { intent, hint } = inferIntentFromName(nameNode.text);
|
|
4184
|
+
return { name: nameNode.text, patterns: routes, intent, hint };
|
|
4185
|
+
}
|
|
4186
|
+
function extractMatchersFromObject(objNode) {
|
|
4187
|
+
const out = [];
|
|
4188
|
+
for (const pair of childrenOfType(objNode, "pair")) {
|
|
4189
|
+
const key = childOfType(pair, "property_identifier");
|
|
4190
|
+
if (!key) continue;
|
|
4191
|
+
const arr = pair.children.find((c) => c.type === "array");
|
|
4192
|
+
if (!arr) continue;
|
|
4193
|
+
const routes = collectStringsFromArray(arr).filter(looksLikeRoutePattern);
|
|
4194
|
+
if (routes.length === 0) continue;
|
|
4195
|
+
const { intent, hint } = inferIntentFromName(key.text);
|
|
4196
|
+
out.push({ name: key.text, patterns: routes, intent, hint });
|
|
4197
|
+
}
|
|
4198
|
+
return out;
|
|
4199
|
+
}
|
|
4200
|
+
function detectFallthroughProtect(root) {
|
|
4201
|
+
const text = root.text;
|
|
4202
|
+
const signals = [
|
|
4203
|
+
/\bauth\.protect\s*\(/,
|
|
4204
|
+
/\bauth\(\)\.protect\s*\(/,
|
|
4205
|
+
/\bredirect\s*\(\s*['"`]\/(sign-?in|log-?in|auth)/i,
|
|
4206
|
+
/\bNextResponse\.redirect\s*\(/,
|
|
4207
|
+
/\bthrow\s+new\s+\w*Unauthorized/i
|
|
4208
|
+
];
|
|
4209
|
+
return signals.some((re) => re.test(text));
|
|
4210
|
+
}
|
|
4211
|
+
function extractMiddlewareAuthTS(absPath) {
|
|
4212
|
+
if (!require("node:fs").existsSync(absPath)) return null;
|
|
4213
|
+
const tree = parseSource(absPath);
|
|
4214
|
+
const root = tree.rootNode;
|
|
4215
|
+
const matchers = [];
|
|
4216
|
+
for (const stmt of root.children) {
|
|
4217
|
+
if (stmt.type !== "lexical_declaration" && stmt.type !== "variable_declaration") continue;
|
|
4218
|
+
for (const decl of childrenOfType(stmt, "variable_declarator")) {
|
|
4219
|
+
const m = extractMatcherFromDeclarator(decl);
|
|
4220
|
+
if (m) matchers.push(m);
|
|
4221
|
+
let valueNode;
|
|
4222
|
+
for (const c of decl.children) {
|
|
4223
|
+
if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
|
|
4224
|
+
valueNode = c;
|
|
4225
|
+
}
|
|
4226
|
+
if (valueNode?.type === "object") {
|
|
4227
|
+
for (const inner of extractMatchersFromObject(valueNode)) matchers.push(inner);
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
for (const stmt of root.children) {
|
|
4232
|
+
if (stmt.type !== "export_statement") continue;
|
|
4233
|
+
const decl = childOfType(stmt, "lexical_declaration") ?? childOfType(stmt, "variable_declaration");
|
|
4234
|
+
if (!decl) continue;
|
|
4235
|
+
for (const d of childrenOfType(decl, "variable_declarator")) {
|
|
4236
|
+
let valueNode;
|
|
4237
|
+
for (const c of d.children) {
|
|
4238
|
+
if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
|
|
4239
|
+
valueNode = c;
|
|
4240
|
+
}
|
|
4241
|
+
if (valueNode?.type === "object") {
|
|
4242
|
+
for (const inner of extractMatchersFromObject(valueNode)) matchers.push(inner);
|
|
4243
|
+
}
|
|
4244
|
+
const m = extractMatcherFromDeclarator(d);
|
|
4245
|
+
if (m && !matchers.some((x) => x.name === m.name && x.patterns.join() === m.patterns.join())) {
|
|
4246
|
+
matchers.push(m);
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
return {
|
|
4251
|
+
file: absPath,
|
|
4252
|
+
matchers,
|
|
4253
|
+
hasFallthroughProtect: detectFallthroughProtect(root)
|
|
4254
|
+
};
|
|
4255
|
+
}
|
|
4256
|
+
function middlewarePatternToRegex(pattern) {
|
|
4257
|
+
if (/\(\?\!/.test(pattern)) return null;
|
|
4258
|
+
if (pattern.startsWith("(")) return null;
|
|
4259
|
+
let src = "^";
|
|
4260
|
+
let i = 0;
|
|
4261
|
+
while (i < pattern.length) {
|
|
4262
|
+
const ch = pattern[i];
|
|
4263
|
+
if (ch === "(" && pattern.slice(i, i + 4) === "(.*)") {
|
|
4264
|
+
src += ".*";
|
|
4265
|
+
i += 4;
|
|
4266
|
+
continue;
|
|
4267
|
+
}
|
|
4268
|
+
if (ch === ":") {
|
|
4269
|
+
i++;
|
|
4270
|
+
while (i < pattern.length && /[a-zA-Z0-9_]/.test(pattern[i])) i++;
|
|
4271
|
+
src += "[^/]+";
|
|
4272
|
+
continue;
|
|
4273
|
+
}
|
|
4274
|
+
if (ch === "*") {
|
|
4275
|
+
i++;
|
|
4276
|
+
while (i < pattern.length && /[a-zA-Z0-9_]/.test(pattern[i])) i++;
|
|
4277
|
+
if (pattern[i] === "?") {
|
|
4278
|
+
i++;
|
|
4279
|
+
src += ".*";
|
|
4280
|
+
continue;
|
|
4281
|
+
}
|
|
4282
|
+
src += ".+";
|
|
4283
|
+
continue;
|
|
4284
|
+
}
|
|
4285
|
+
if (/[.\\+?^${}()|[\]]/.test(ch)) {
|
|
4286
|
+
src += "\\" + ch;
|
|
4287
|
+
i++;
|
|
4288
|
+
continue;
|
|
4289
|
+
}
|
|
4290
|
+
src += ch;
|
|
4291
|
+
i++;
|
|
4292
|
+
}
|
|
4293
|
+
src += "$";
|
|
4294
|
+
try {
|
|
4295
|
+
return new RegExp(src);
|
|
4296
|
+
} catch {
|
|
4297
|
+
return null;
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
function classifyRouteAgainstMiddleware(routePath, info) {
|
|
4301
|
+
for (const m of info.matchers) {
|
|
4302
|
+
if (m.intent === "ambiguous") continue;
|
|
4303
|
+
for (const pat of m.patterns) {
|
|
4304
|
+
const re = middlewarePatternToRegex(pat);
|
|
4305
|
+
if (!re) continue;
|
|
4306
|
+
if (re.test(routePath)) return { intent: m.intent, matcher: m.name, pattern: pat };
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
return null;
|
|
4310
|
+
}
|
|
4134
4311
|
function trunc(s, max = 120) {
|
|
4135
4312
|
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
4136
4313
|
}
|
|
@@ -4287,7 +4464,7 @@ function extractDeep(absPath) {
|
|
|
4287
4464
|
}
|
|
4288
4465
|
return { elements, stateVars, conditions, variables, responses, params };
|
|
4289
4466
|
}
|
|
4290
|
-
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;
|
|
4467
|
+
var import_node_fs4, import_node_path4, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, SUPABASE_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods, INLINE_AUTH_IMPORTS, EXEMPT_NAME_PATTERNS, PROTECT_NAME_PATTERNS;
|
|
4291
4468
|
var init_ts_extractor = __esm({
|
|
4292
4469
|
"src/server/graph/core/ts-extractor.ts"() {
|
|
4293
4470
|
"use strict";
|
|
@@ -4326,6 +4503,29 @@ var init_ts_extractor = __esm({
|
|
|
4326
4503
|
{ module: /^@auth\//, helpers: ["auth"] },
|
|
4327
4504
|
{ module: /^@supabase\/auth-helpers/, helpers: ["createServerClient"] }
|
|
4328
4505
|
];
|
|
4506
|
+
EXEMPT_NAME_PATTERNS = [
|
|
4507
|
+
/^is_?public/i,
|
|
4508
|
+
/^public_?routes?/i,
|
|
4509
|
+
/^public_?paths?/i,
|
|
4510
|
+
/^whitelist/i,
|
|
4511
|
+
/^allowlist/i,
|
|
4512
|
+
/^unauthenticated/i,
|
|
4513
|
+
/^anonymous/i,
|
|
4514
|
+
/^guest/i,
|
|
4515
|
+
/^skip_?auth/i,
|
|
4516
|
+
/^bypass/i
|
|
4517
|
+
];
|
|
4518
|
+
PROTECT_NAME_PATTERNS = [
|
|
4519
|
+
/^is_?protected/i,
|
|
4520
|
+
/^protected_?routes?/i,
|
|
4521
|
+
/^protected_?paths?/i,
|
|
4522
|
+
/^require_?auth/i,
|
|
4523
|
+
/^auth_?required/i,
|
|
4524
|
+
/^private_?routes?/i,
|
|
4525
|
+
/^is_?admin/i,
|
|
4526
|
+
/^admin_?routes?/i,
|
|
4527
|
+
/^secured/i
|
|
4528
|
+
];
|
|
4329
4529
|
}
|
|
4330
4530
|
});
|
|
4331
4531
|
|
|
@@ -7048,15 +7248,9 @@ var import_node_fs2 = require("node:fs");
|
|
|
7048
7248
|
var import_node_path2 = require("node:path");
|
|
7049
7249
|
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
7050
7250
|
"node_modules",
|
|
7051
|
-
".git",
|
|
7052
|
-
".next",
|
|
7053
|
-
".launchsecure",
|
|
7054
|
-
".claude",
|
|
7055
7251
|
"dist",
|
|
7056
7252
|
"build",
|
|
7057
7253
|
"out",
|
|
7058
|
-
".turbo",
|
|
7059
|
-
".vercel",
|
|
7060
7254
|
"coverage"
|
|
7061
7255
|
]);
|
|
7062
7256
|
function walk(dir, exts) {
|
|
@@ -7078,6 +7272,7 @@ function walkWithIgnore(dir, exts, opts = {}) {
|
|
|
7078
7272
|
const skip = opts.extraIgnore ? /* @__PURE__ */ new Set([...DEFAULT_IGNORE_DIRS, ...opts.extraIgnore]) : DEFAULT_IGNORE_DIRS;
|
|
7079
7273
|
for (const entry of (0, import_node_fs2.readdirSync)(dir, { withFileTypes: true })) {
|
|
7080
7274
|
if (entry.isDirectory()) {
|
|
7275
|
+
if (entry.name.startsWith(".")) continue;
|
|
7081
7276
|
if (skip.has(entry.name)) continue;
|
|
7082
7277
|
results.push(...walkWithIgnore((0, import_node_path2.join)(dir, entry.name), exts, opts));
|
|
7083
7278
|
} else if (exts.includes((0, import_node_path2.extname)(entry.name))) {
|
|
@@ -7829,43 +8024,169 @@ function generate(rootDir) {
|
|
|
7829
8024
|
nodeIdSet.add(externalId);
|
|
7830
8025
|
uiEdges.push(...edgesFromThis);
|
|
7831
8026
|
}
|
|
8027
|
+
const tablesByFile = /* @__PURE__ */ new Map();
|
|
8028
|
+
const allDbNodes = [...apiNodes, ...uiNodes];
|
|
8029
|
+
for (const node of allDbNodes) {
|
|
8030
|
+
const calls = node._dbCalls;
|
|
8031
|
+
if (!calls || calls.length === 0) continue;
|
|
8032
|
+
const map = /* @__PURE__ */ new Map();
|
|
8033
|
+
for (const c of calls) {
|
|
8034
|
+
const key = `${c.kind}:${c.model}:${c.isMutation ? "m" : "r"}`;
|
|
8035
|
+
if (!map.has(key)) {
|
|
8036
|
+
map.set(key, { model: c.model, method: c.method, isMutation: c.isMutation, kind: c.kind, via: [] });
|
|
8037
|
+
}
|
|
8038
|
+
}
|
|
8039
|
+
tablesByFile.set(node.id, map);
|
|
8040
|
+
}
|
|
8041
|
+
const reverseRuntimeImports = /* @__PURE__ */ new Map();
|
|
8042
|
+
for (const edge of uiEdges) {
|
|
8043
|
+
if (edge.type !== "imports" && edge.type !== "renders") continue;
|
|
8044
|
+
if (!reverseRuntimeImports.has(edge.target)) {
|
|
8045
|
+
reverseRuntimeImports.set(edge.target, /* @__PURE__ */ new Set());
|
|
8046
|
+
}
|
|
8047
|
+
reverseRuntimeImports.get(edge.target).add(edge.source);
|
|
8048
|
+
}
|
|
8049
|
+
let changed = true;
|
|
8050
|
+
let iterations = 0;
|
|
8051
|
+
while (changed && iterations < 50) {
|
|
8052
|
+
changed = false;
|
|
8053
|
+
iterations++;
|
|
8054
|
+
for (const [target, tableMap] of [...tablesByFile]) {
|
|
8055
|
+
const importers = reverseRuntimeImports.get(target);
|
|
8056
|
+
if (!importers) continue;
|
|
8057
|
+
for (const importer of importers) {
|
|
8058
|
+
if (importer === target) continue;
|
|
8059
|
+
let importerMap = tablesByFile.get(importer);
|
|
8060
|
+
if (!importerMap) {
|
|
8061
|
+
importerMap = /* @__PURE__ */ new Map();
|
|
8062
|
+
tablesByFile.set(importer, importerMap);
|
|
8063
|
+
}
|
|
8064
|
+
for (const [key, call] of tableMap) {
|
|
8065
|
+
if (importerMap.has(key)) continue;
|
|
8066
|
+
importerMap.set(key, {
|
|
8067
|
+
model: call.model,
|
|
8068
|
+
method: null,
|
|
8069
|
+
isMutation: call.isMutation,
|
|
8070
|
+
kind: call.kind,
|
|
8071
|
+
via: [...call.via, target]
|
|
8072
|
+
});
|
|
8073
|
+
changed = true;
|
|
8074
|
+
}
|
|
8075
|
+
}
|
|
8076
|
+
}
|
|
8077
|
+
}
|
|
8078
|
+
for (const node of apiNodes) {
|
|
8079
|
+
const map = tablesByFile.get(node.id);
|
|
8080
|
+
if (!map) continue;
|
|
8081
|
+
node.db_models = [...new Set([...map.values()].map((c) => c.model))];
|
|
8082
|
+
node.db_operations = [...new Set(
|
|
8083
|
+
[...map.values()].filter((c) => c.via.length === 0 && c.method).map((c) => `${c.model}.${c.method}`)
|
|
8084
|
+
)];
|
|
8085
|
+
node.mutates = [...map.values()].some((c) => c.isMutation);
|
|
8086
|
+
}
|
|
7832
8087
|
const apiCrossRefs = [];
|
|
7833
8088
|
for (const node of apiNodes) {
|
|
7834
|
-
const
|
|
7835
|
-
if (!
|
|
7836
|
-
|
|
7837
|
-
|
|
8089
|
+
const map = tablesByFile.get(node.id);
|
|
8090
|
+
if (!map) {
|
|
8091
|
+
delete node._dbCalls;
|
|
8092
|
+
continue;
|
|
8093
|
+
}
|
|
8094
|
+
const seenTargets = /* @__PURE__ */ new Set();
|
|
8095
|
+
for (const call of map.values()) {
|
|
7838
8096
|
const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
|
|
7839
|
-
if (
|
|
7840
|
-
|
|
7841
|
-
|
|
8097
|
+
if (seenTargets.has(target)) continue;
|
|
8098
|
+
seenTargets.add(target);
|
|
8099
|
+
const isTransitive = call.via.length > 0;
|
|
8100
|
+
const ref = {
|
|
7842
8101
|
source: node.id,
|
|
7843
8102
|
target,
|
|
7844
|
-
type: call.isMutation ? "mutates" : "reads",
|
|
8103
|
+
type: call.isMutation ? isTransitive ? "mutates_via" : "mutates" : isTransitive ? "reads_via" : "reads",
|
|
7845
8104
|
layer: "db"
|
|
7846
|
-
}
|
|
8105
|
+
};
|
|
8106
|
+
if (isTransitive) ref.via = call.via;
|
|
8107
|
+
apiCrossRefs.push(ref);
|
|
7847
8108
|
}
|
|
7848
8109
|
delete node._dbCalls;
|
|
7849
8110
|
}
|
|
7850
8111
|
const uiCrossRefs = [];
|
|
7851
8112
|
for (const node of uiNodes) {
|
|
7852
|
-
const
|
|
7853
|
-
if (!
|
|
7854
|
-
|
|
7855
|
-
|
|
8113
|
+
const map = tablesByFile.get(node.id);
|
|
8114
|
+
if (!map) {
|
|
8115
|
+
if (node._dbCalls) delete node._dbCalls;
|
|
8116
|
+
continue;
|
|
8117
|
+
}
|
|
8118
|
+
const seenTargets = /* @__PURE__ */ new Set();
|
|
8119
|
+
for (const call of map.values()) {
|
|
7856
8120
|
const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
|
|
7857
|
-
if (
|
|
7858
|
-
|
|
7859
|
-
|
|
8121
|
+
if (seenTargets.has(target)) continue;
|
|
8122
|
+
seenTargets.add(target);
|
|
8123
|
+
const isTransitive = call.via.length > 0;
|
|
8124
|
+
const ref = {
|
|
7860
8125
|
source: node.id,
|
|
7861
8126
|
target,
|
|
7862
|
-
type: call.isMutation ? "mutates" : "reads",
|
|
8127
|
+
type: call.isMutation ? isTransitive ? "mutates_via" : "mutates" : isTransitive ? "reads_via" : "reads",
|
|
7863
8128
|
layer: "db"
|
|
7864
|
-
}
|
|
8129
|
+
};
|
|
8130
|
+
if (isTransitive) ref.via = call.via;
|
|
8131
|
+
uiCrossRefs.push(ref);
|
|
7865
8132
|
}
|
|
7866
|
-
delete node._dbCalls;
|
|
8133
|
+
if (node._dbCalls) delete node._dbCalls;
|
|
7867
8134
|
}
|
|
7868
8135
|
uiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
8136
|
+
const middlewareInfos = [];
|
|
8137
|
+
for (const conv of paths.conventionFiles) {
|
|
8138
|
+
if (!/middleware\.tsx?$/.test(conv)) continue;
|
|
8139
|
+
try {
|
|
8140
|
+
const info = extractMiddlewareAuthTS(conv);
|
|
8141
|
+
if (info && info.matchers.length > 0) middlewareInfos.push(info);
|
|
8142
|
+
} catch {
|
|
8143
|
+
}
|
|
8144
|
+
}
|
|
8145
|
+
if (middlewareInfos.length > 0) {
|
|
8146
|
+
let setAuth2 = function(node, newTags, replaceAll) {
|
|
8147
|
+
const existing = node.auth ?? [];
|
|
8148
|
+
const meaningful = existing.filter((a) => a !== "public");
|
|
8149
|
+
const merged = replaceAll ? newTags : [.../* @__PURE__ */ new Set([...newTags, ...meaningful])];
|
|
8150
|
+
node.auth = merged.length > 0 ? merged : ["public"];
|
|
8151
|
+
}, applyMiddleware2 = function(node, routePath) {
|
|
8152
|
+
let resolved = null;
|
|
8153
|
+
let label = "";
|
|
8154
|
+
let hasAnyExemptMatcher = false;
|
|
8155
|
+
let hasAnyFallthrough = false;
|
|
8156
|
+
for (const info of middlewareInfos) {
|
|
8157
|
+
if (info.hasFallthroughProtect) hasAnyFallthrough = true;
|
|
8158
|
+
if (info.matchers.some((m) => m.intent === "exempt")) hasAnyExemptMatcher = true;
|
|
8159
|
+
const c = classifyRouteAgainstMiddleware(routePath, info);
|
|
8160
|
+
if (!c) continue;
|
|
8161
|
+
if (!resolved) {
|
|
8162
|
+
resolved = c.intent;
|
|
8163
|
+
label = c.matcher;
|
|
8164
|
+
}
|
|
8165
|
+
}
|
|
8166
|
+
if (resolved === "exempt") {
|
|
8167
|
+
setAuth2(node, ["public"], true);
|
|
8168
|
+
return;
|
|
8169
|
+
}
|
|
8170
|
+
if (resolved === "protect") {
|
|
8171
|
+
setAuth2(node, [`middleware:${label}`], false);
|
|
8172
|
+
return;
|
|
8173
|
+
}
|
|
8174
|
+
if (hasAnyExemptMatcher && hasAnyFallthrough) {
|
|
8175
|
+
setAuth2(node, ["middleware-protected"], false);
|
|
8176
|
+
}
|
|
8177
|
+
};
|
|
8178
|
+
var setAuth = setAuth2, applyMiddleware = applyMiddleware2;
|
|
8179
|
+
for (const node of apiNodes) {
|
|
8180
|
+
const routePath = node.path;
|
|
8181
|
+
if (!routePath) continue;
|
|
8182
|
+
applyMiddleware2(node, routePath);
|
|
8183
|
+
}
|
|
8184
|
+
for (const node of uiNodes) {
|
|
8185
|
+
const route = node.route;
|
|
8186
|
+
if (!route) continue;
|
|
8187
|
+
applyMiddleware2(node, route);
|
|
8188
|
+
}
|
|
8189
|
+
}
|
|
7869
8190
|
const apiNodeIds = new Set(apiNodes.map((n) => n.id));
|
|
7870
8191
|
const apiEdges = [];
|
|
7871
8192
|
const uiOnlyEdges = [];
|
|
@@ -175,6 +175,7 @@ function walkWithIgnore(dir, exts, opts = {}) {
|
|
|
175
175
|
const skip = opts.extraIgnore ? /* @__PURE__ */ new Set([...DEFAULT_IGNORE_DIRS, ...opts.extraIgnore]) : DEFAULT_IGNORE_DIRS;
|
|
176
176
|
for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
|
|
177
177
|
if (entry.isDirectory()) {
|
|
178
|
+
if (entry.name.startsWith(".")) continue;
|
|
178
179
|
if (skip.has(entry.name)) continue;
|
|
179
180
|
results.push(...walkWithIgnore((0, import_node_path3.join)(dir, entry.name), exts, opts));
|
|
180
181
|
} else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
|
|
@@ -191,15 +192,9 @@ var init_walk = __esm({
|
|
|
191
192
|
import_node_path3 = require("node:path");
|
|
192
193
|
DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
193
194
|
"node_modules",
|
|
194
|
-
".git",
|
|
195
|
-
".next",
|
|
196
|
-
".launchsecure",
|
|
197
|
-
".claude",
|
|
198
195
|
"dist",
|
|
199
196
|
"build",
|
|
200
197
|
"out",
|
|
201
|
-
".turbo",
|
|
202
|
-
".vercel",
|
|
203
198
|
"coverage"
|
|
204
199
|
]);
|
|
205
200
|
}
|
|
@@ -404,11 +399,14 @@ var init_resolve_paths = __esm({
|
|
|
404
399
|
var ts_extractor_exports = {};
|
|
405
400
|
__export(ts_extractor_exports, {
|
|
406
401
|
classifyFile: () => classifyFile,
|
|
402
|
+
classifyRouteAgainstMiddleware: () => classifyRouteAgainstMiddleware,
|
|
407
403
|
createQuery: () => createQuery,
|
|
408
404
|
extractAuthWrappersTS: () => extractAuthWrappersTS,
|
|
409
405
|
extractDbCallsTS: () => extractDbCallsTS,
|
|
410
406
|
extractDeep: () => extractDeep,
|
|
407
|
+
extractMiddlewareAuthTS: () => extractMiddlewareAuthTS,
|
|
411
408
|
initTreeSitter: () => initTreeSitter,
|
|
409
|
+
middlewarePatternToRegex: () => middlewarePatternToRegex,
|
|
412
410
|
parseCodeTS: () => parseCodeTS,
|
|
413
411
|
parseFileTS: () => parseFileTS,
|
|
414
412
|
setExtractorConfig: () => setExtractorConfig
|
|
@@ -802,6 +800,180 @@ function extractAuthWrappersTS(absPath) {
|
|
|
802
800
|
}
|
|
803
801
|
return wrappers;
|
|
804
802
|
}
|
|
803
|
+
function inferIntentFromName(name) {
|
|
804
|
+
for (const re of EXEMPT_NAME_PATTERNS) {
|
|
805
|
+
if (re.test(name)) return { intent: "exempt", hint: `name "${name}" matches /${re.source}/` };
|
|
806
|
+
}
|
|
807
|
+
for (const re of PROTECT_NAME_PATTERNS) {
|
|
808
|
+
if (re.test(name)) return { intent: "protect", hint: `name "${name}" matches /${re.source}/` };
|
|
809
|
+
}
|
|
810
|
+
return { intent: "ambiguous", hint: `name "${name}" has no exempt/protect signal` };
|
|
811
|
+
}
|
|
812
|
+
function looksLikeRoutePattern(s) {
|
|
813
|
+
return s.startsWith("/") && !s.startsWith("//");
|
|
814
|
+
}
|
|
815
|
+
function collectStringsFromArray(arrNode) {
|
|
816
|
+
const out = [];
|
|
817
|
+
for (const child of arrNode.children) {
|
|
818
|
+
if (child.type !== "string") continue;
|
|
819
|
+
const frag = childOfType(child, "string_fragment");
|
|
820
|
+
if (frag) out.push(frag.text);
|
|
821
|
+
}
|
|
822
|
+
return out;
|
|
823
|
+
}
|
|
824
|
+
function findArrayInValue(valueNode) {
|
|
825
|
+
if (!valueNode) return null;
|
|
826
|
+
if (valueNode.type === "array") return valueNode;
|
|
827
|
+
if (valueNode.type === "call_expression") {
|
|
828
|
+
const args = childOfType(valueNode, "arguments");
|
|
829
|
+
if (!args) return null;
|
|
830
|
+
for (const c of args.children) {
|
|
831
|
+
if (c.type === "array") return c;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
function extractMatcherFromDeclarator(decl) {
|
|
837
|
+
const nameNode = childOfType(decl, "identifier");
|
|
838
|
+
if (!nameNode) return null;
|
|
839
|
+
let valueNode;
|
|
840
|
+
for (const c of decl.children) {
|
|
841
|
+
if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
|
|
842
|
+
valueNode = c;
|
|
843
|
+
}
|
|
844
|
+
const arr = findArrayInValue(valueNode);
|
|
845
|
+
if (!arr) return null;
|
|
846
|
+
const strings = collectStringsFromArray(arr);
|
|
847
|
+
const routes = strings.filter(looksLikeRoutePattern);
|
|
848
|
+
if (routes.length === 0) return null;
|
|
849
|
+
const { intent, hint } = inferIntentFromName(nameNode.text);
|
|
850
|
+
return { name: nameNode.text, patterns: routes, intent, hint };
|
|
851
|
+
}
|
|
852
|
+
function extractMatchersFromObject(objNode) {
|
|
853
|
+
const out = [];
|
|
854
|
+
for (const pair of childrenOfType(objNode, "pair")) {
|
|
855
|
+
const key = childOfType(pair, "property_identifier");
|
|
856
|
+
if (!key) continue;
|
|
857
|
+
const arr = pair.children.find((c) => c.type === "array");
|
|
858
|
+
if (!arr) continue;
|
|
859
|
+
const routes = collectStringsFromArray(arr).filter(looksLikeRoutePattern);
|
|
860
|
+
if (routes.length === 0) continue;
|
|
861
|
+
const { intent, hint } = inferIntentFromName(key.text);
|
|
862
|
+
out.push({ name: key.text, patterns: routes, intent, hint });
|
|
863
|
+
}
|
|
864
|
+
return out;
|
|
865
|
+
}
|
|
866
|
+
function detectFallthroughProtect(root) {
|
|
867
|
+
const text = root.text;
|
|
868
|
+
const signals = [
|
|
869
|
+
/\bauth\.protect\s*\(/,
|
|
870
|
+
/\bauth\(\)\.protect\s*\(/,
|
|
871
|
+
/\bredirect\s*\(\s*['"`]\/(sign-?in|log-?in|auth)/i,
|
|
872
|
+
/\bNextResponse\.redirect\s*\(/,
|
|
873
|
+
/\bthrow\s+new\s+\w*Unauthorized/i
|
|
874
|
+
];
|
|
875
|
+
return signals.some((re) => re.test(text));
|
|
876
|
+
}
|
|
877
|
+
function extractMiddlewareAuthTS(absPath) {
|
|
878
|
+
if (!require("node:fs").existsSync(absPath)) return null;
|
|
879
|
+
const tree = parseSource(absPath);
|
|
880
|
+
const root = tree.rootNode;
|
|
881
|
+
const matchers = [];
|
|
882
|
+
for (const stmt of root.children) {
|
|
883
|
+
if (stmt.type !== "lexical_declaration" && stmt.type !== "variable_declaration") continue;
|
|
884
|
+
for (const decl of childrenOfType(stmt, "variable_declarator")) {
|
|
885
|
+
const m = extractMatcherFromDeclarator(decl);
|
|
886
|
+
if (m) matchers.push(m);
|
|
887
|
+
let valueNode;
|
|
888
|
+
for (const c of decl.children) {
|
|
889
|
+
if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
|
|
890
|
+
valueNode = c;
|
|
891
|
+
}
|
|
892
|
+
if (valueNode?.type === "object") {
|
|
893
|
+
for (const inner of extractMatchersFromObject(valueNode)) matchers.push(inner);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
for (const stmt of root.children) {
|
|
898
|
+
if (stmt.type !== "export_statement") continue;
|
|
899
|
+
const decl = childOfType(stmt, "lexical_declaration") ?? childOfType(stmt, "variable_declaration");
|
|
900
|
+
if (!decl) continue;
|
|
901
|
+
for (const d of childrenOfType(decl, "variable_declarator")) {
|
|
902
|
+
let valueNode;
|
|
903
|
+
for (const c of d.children) {
|
|
904
|
+
if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
|
|
905
|
+
valueNode = c;
|
|
906
|
+
}
|
|
907
|
+
if (valueNode?.type === "object") {
|
|
908
|
+
for (const inner of extractMatchersFromObject(valueNode)) matchers.push(inner);
|
|
909
|
+
}
|
|
910
|
+
const m = extractMatcherFromDeclarator(d);
|
|
911
|
+
if (m && !matchers.some((x) => x.name === m.name && x.patterns.join() === m.patterns.join())) {
|
|
912
|
+
matchers.push(m);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return {
|
|
917
|
+
file: absPath,
|
|
918
|
+
matchers,
|
|
919
|
+
hasFallthroughProtect: detectFallthroughProtect(root)
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
function middlewarePatternToRegex(pattern) {
|
|
923
|
+
if (/\(\?\!/.test(pattern)) return null;
|
|
924
|
+
if (pattern.startsWith("(")) return null;
|
|
925
|
+
let src = "^";
|
|
926
|
+
let i = 0;
|
|
927
|
+
while (i < pattern.length) {
|
|
928
|
+
const ch = pattern[i];
|
|
929
|
+
if (ch === "(" && pattern.slice(i, i + 4) === "(.*)") {
|
|
930
|
+
src += ".*";
|
|
931
|
+
i += 4;
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
if (ch === ":") {
|
|
935
|
+
i++;
|
|
936
|
+
while (i < pattern.length && /[a-zA-Z0-9_]/.test(pattern[i])) i++;
|
|
937
|
+
src += "[^/]+";
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
if (ch === "*") {
|
|
941
|
+
i++;
|
|
942
|
+
while (i < pattern.length && /[a-zA-Z0-9_]/.test(pattern[i])) i++;
|
|
943
|
+
if (pattern[i] === "?") {
|
|
944
|
+
i++;
|
|
945
|
+
src += ".*";
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
src += ".+";
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
if (/[.\\+?^${}()|[\]]/.test(ch)) {
|
|
952
|
+
src += "\\" + ch;
|
|
953
|
+
i++;
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
src += ch;
|
|
957
|
+
i++;
|
|
958
|
+
}
|
|
959
|
+
src += "$";
|
|
960
|
+
try {
|
|
961
|
+
return new RegExp(src);
|
|
962
|
+
} catch {
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
function classifyRouteAgainstMiddleware(routePath, info) {
|
|
967
|
+
for (const m of info.matchers) {
|
|
968
|
+
if (m.intent === "ambiguous") continue;
|
|
969
|
+
for (const pat of m.patterns) {
|
|
970
|
+
const re = middlewarePatternToRegex(pat);
|
|
971
|
+
if (!re) continue;
|
|
972
|
+
if (re.test(routePath)) return { intent: m.intent, matcher: m.name, pattern: pat };
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
805
977
|
function trunc(s, max = 120) {
|
|
806
978
|
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
807
979
|
}
|
|
@@ -958,7 +1130,7 @@ function extractDeep(absPath) {
|
|
|
958
1130
|
}
|
|
959
1131
|
return { elements, stateVars, conditions, variables, responses, params };
|
|
960
1132
|
}
|
|
961
|
-
var import_node_fs5, import_node_path5, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, SUPABASE_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods, INLINE_AUTH_IMPORTS;
|
|
1133
|
+
var import_node_fs5, import_node_path5, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, SUPABASE_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods, INLINE_AUTH_IMPORTS, EXEMPT_NAME_PATTERNS, PROTECT_NAME_PATTERNS;
|
|
962
1134
|
var init_ts_extractor = __esm({
|
|
963
1135
|
"src/server/graph/core/ts-extractor.ts"() {
|
|
964
1136
|
"use strict";
|
|
@@ -997,6 +1169,29 @@ var init_ts_extractor = __esm({
|
|
|
997
1169
|
{ module: /^@auth\//, helpers: ["auth"] },
|
|
998
1170
|
{ module: /^@supabase\/auth-helpers/, helpers: ["createServerClient"] }
|
|
999
1171
|
];
|
|
1172
|
+
EXEMPT_NAME_PATTERNS = [
|
|
1173
|
+
/^is_?public/i,
|
|
1174
|
+
/^public_?routes?/i,
|
|
1175
|
+
/^public_?paths?/i,
|
|
1176
|
+
/^whitelist/i,
|
|
1177
|
+
/^allowlist/i,
|
|
1178
|
+
/^unauthenticated/i,
|
|
1179
|
+
/^anonymous/i,
|
|
1180
|
+
/^guest/i,
|
|
1181
|
+
/^skip_?auth/i,
|
|
1182
|
+
/^bypass/i
|
|
1183
|
+
];
|
|
1184
|
+
PROTECT_NAME_PATTERNS = [
|
|
1185
|
+
/^is_?protected/i,
|
|
1186
|
+
/^protected_?routes?/i,
|
|
1187
|
+
/^protected_?paths?/i,
|
|
1188
|
+
/^require_?auth/i,
|
|
1189
|
+
/^auth_?required/i,
|
|
1190
|
+
/^private_?routes?/i,
|
|
1191
|
+
/^is_?admin/i,
|
|
1192
|
+
/^admin_?routes?/i,
|
|
1193
|
+
/^secured/i
|
|
1194
|
+
];
|
|
1000
1195
|
}
|
|
1001
1196
|
});
|
|
1002
1197
|
|
|
@@ -1540,43 +1735,169 @@ function generate(rootDir) {
|
|
|
1540
1735
|
nodeIdSet.add(externalId);
|
|
1541
1736
|
uiEdges.push(...edgesFromThis);
|
|
1542
1737
|
}
|
|
1738
|
+
const tablesByFile = /* @__PURE__ */ new Map();
|
|
1739
|
+
const allDbNodes = [...apiNodes, ...uiNodes];
|
|
1740
|
+
for (const node of allDbNodes) {
|
|
1741
|
+
const calls = node._dbCalls;
|
|
1742
|
+
if (!calls || calls.length === 0) continue;
|
|
1743
|
+
const map = /* @__PURE__ */ new Map();
|
|
1744
|
+
for (const c of calls) {
|
|
1745
|
+
const key = `${c.kind}:${c.model}:${c.isMutation ? "m" : "r"}`;
|
|
1746
|
+
if (!map.has(key)) {
|
|
1747
|
+
map.set(key, { model: c.model, method: c.method, isMutation: c.isMutation, kind: c.kind, via: [] });
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
tablesByFile.set(node.id, map);
|
|
1751
|
+
}
|
|
1752
|
+
const reverseRuntimeImports = /* @__PURE__ */ new Map();
|
|
1753
|
+
for (const edge of uiEdges) {
|
|
1754
|
+
if (edge.type !== "imports" && edge.type !== "renders") continue;
|
|
1755
|
+
if (!reverseRuntimeImports.has(edge.target)) {
|
|
1756
|
+
reverseRuntimeImports.set(edge.target, /* @__PURE__ */ new Set());
|
|
1757
|
+
}
|
|
1758
|
+
reverseRuntimeImports.get(edge.target).add(edge.source);
|
|
1759
|
+
}
|
|
1760
|
+
let changed = true;
|
|
1761
|
+
let iterations = 0;
|
|
1762
|
+
while (changed && iterations < 50) {
|
|
1763
|
+
changed = false;
|
|
1764
|
+
iterations++;
|
|
1765
|
+
for (const [target, tableMap] of [...tablesByFile]) {
|
|
1766
|
+
const importers = reverseRuntimeImports.get(target);
|
|
1767
|
+
if (!importers) continue;
|
|
1768
|
+
for (const importer of importers) {
|
|
1769
|
+
if (importer === target) continue;
|
|
1770
|
+
let importerMap = tablesByFile.get(importer);
|
|
1771
|
+
if (!importerMap) {
|
|
1772
|
+
importerMap = /* @__PURE__ */ new Map();
|
|
1773
|
+
tablesByFile.set(importer, importerMap);
|
|
1774
|
+
}
|
|
1775
|
+
for (const [key, call] of tableMap) {
|
|
1776
|
+
if (importerMap.has(key)) continue;
|
|
1777
|
+
importerMap.set(key, {
|
|
1778
|
+
model: call.model,
|
|
1779
|
+
method: null,
|
|
1780
|
+
isMutation: call.isMutation,
|
|
1781
|
+
kind: call.kind,
|
|
1782
|
+
via: [...call.via, target]
|
|
1783
|
+
});
|
|
1784
|
+
changed = true;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
for (const node of apiNodes) {
|
|
1790
|
+
const map = tablesByFile.get(node.id);
|
|
1791
|
+
if (!map) continue;
|
|
1792
|
+
node.db_models = [...new Set([...map.values()].map((c) => c.model))];
|
|
1793
|
+
node.db_operations = [...new Set(
|
|
1794
|
+
[...map.values()].filter((c) => c.via.length === 0 && c.method).map((c) => `${c.model}.${c.method}`)
|
|
1795
|
+
)];
|
|
1796
|
+
node.mutates = [...map.values()].some((c) => c.isMutation);
|
|
1797
|
+
}
|
|
1543
1798
|
const apiCrossRefs = [];
|
|
1544
1799
|
for (const node of apiNodes) {
|
|
1545
|
-
const
|
|
1546
|
-
if (!
|
|
1547
|
-
|
|
1548
|
-
|
|
1800
|
+
const map = tablesByFile.get(node.id);
|
|
1801
|
+
if (!map) {
|
|
1802
|
+
delete node._dbCalls;
|
|
1803
|
+
continue;
|
|
1804
|
+
}
|
|
1805
|
+
const seenTargets = /* @__PURE__ */ new Set();
|
|
1806
|
+
for (const call of map.values()) {
|
|
1549
1807
|
const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
|
|
1550
|
-
if (
|
|
1551
|
-
|
|
1552
|
-
|
|
1808
|
+
if (seenTargets.has(target)) continue;
|
|
1809
|
+
seenTargets.add(target);
|
|
1810
|
+
const isTransitive = call.via.length > 0;
|
|
1811
|
+
const ref = {
|
|
1553
1812
|
source: node.id,
|
|
1554
1813
|
target,
|
|
1555
|
-
type: call.isMutation ? "mutates" : "reads",
|
|
1814
|
+
type: call.isMutation ? isTransitive ? "mutates_via" : "mutates" : isTransitive ? "reads_via" : "reads",
|
|
1556
1815
|
layer: "db"
|
|
1557
|
-
}
|
|
1816
|
+
};
|
|
1817
|
+
if (isTransitive) ref.via = call.via;
|
|
1818
|
+
apiCrossRefs.push(ref);
|
|
1558
1819
|
}
|
|
1559
1820
|
delete node._dbCalls;
|
|
1560
1821
|
}
|
|
1561
1822
|
const uiCrossRefs = [];
|
|
1562
1823
|
for (const node of uiNodes) {
|
|
1563
|
-
const
|
|
1564
|
-
if (!
|
|
1565
|
-
|
|
1566
|
-
|
|
1824
|
+
const map = tablesByFile.get(node.id);
|
|
1825
|
+
if (!map) {
|
|
1826
|
+
if (node._dbCalls) delete node._dbCalls;
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1829
|
+
const seenTargets = /* @__PURE__ */ new Set();
|
|
1830
|
+
for (const call of map.values()) {
|
|
1567
1831
|
const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
|
|
1568
|
-
if (
|
|
1569
|
-
|
|
1570
|
-
|
|
1832
|
+
if (seenTargets.has(target)) continue;
|
|
1833
|
+
seenTargets.add(target);
|
|
1834
|
+
const isTransitive = call.via.length > 0;
|
|
1835
|
+
const ref = {
|
|
1571
1836
|
source: node.id,
|
|
1572
1837
|
target,
|
|
1573
|
-
type: call.isMutation ? "mutates" : "reads",
|
|
1838
|
+
type: call.isMutation ? isTransitive ? "mutates_via" : "mutates" : isTransitive ? "reads_via" : "reads",
|
|
1574
1839
|
layer: "db"
|
|
1575
|
-
}
|
|
1840
|
+
};
|
|
1841
|
+
if (isTransitive) ref.via = call.via;
|
|
1842
|
+
uiCrossRefs.push(ref);
|
|
1576
1843
|
}
|
|
1577
|
-
delete node._dbCalls;
|
|
1844
|
+
if (node._dbCalls) delete node._dbCalls;
|
|
1578
1845
|
}
|
|
1579
1846
|
uiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
1847
|
+
const middlewareInfos = [];
|
|
1848
|
+
for (const conv of paths.conventionFiles) {
|
|
1849
|
+
if (!/middleware\.tsx?$/.test(conv)) continue;
|
|
1850
|
+
try {
|
|
1851
|
+
const info = extractMiddlewareAuthTS(conv);
|
|
1852
|
+
if (info && info.matchers.length > 0) middlewareInfos.push(info);
|
|
1853
|
+
} catch {
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
if (middlewareInfos.length > 0) {
|
|
1857
|
+
let setAuth2 = function(node, newTags, replaceAll) {
|
|
1858
|
+
const existing = node.auth ?? [];
|
|
1859
|
+
const meaningful = existing.filter((a) => a !== "public");
|
|
1860
|
+
const merged = replaceAll ? newTags : [.../* @__PURE__ */ new Set([...newTags, ...meaningful])];
|
|
1861
|
+
node.auth = merged.length > 0 ? merged : ["public"];
|
|
1862
|
+
}, applyMiddleware2 = function(node, routePath) {
|
|
1863
|
+
let resolved = null;
|
|
1864
|
+
let label = "";
|
|
1865
|
+
let hasAnyExemptMatcher = false;
|
|
1866
|
+
let hasAnyFallthrough = false;
|
|
1867
|
+
for (const info of middlewareInfos) {
|
|
1868
|
+
if (info.hasFallthroughProtect) hasAnyFallthrough = true;
|
|
1869
|
+
if (info.matchers.some((m) => m.intent === "exempt")) hasAnyExemptMatcher = true;
|
|
1870
|
+
const c = classifyRouteAgainstMiddleware(routePath, info);
|
|
1871
|
+
if (!c) continue;
|
|
1872
|
+
if (!resolved) {
|
|
1873
|
+
resolved = c.intent;
|
|
1874
|
+
label = c.matcher;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
if (resolved === "exempt") {
|
|
1878
|
+
setAuth2(node, ["public"], true);
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
if (resolved === "protect") {
|
|
1882
|
+
setAuth2(node, [`middleware:${label}`], false);
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
if (hasAnyExemptMatcher && hasAnyFallthrough) {
|
|
1886
|
+
setAuth2(node, ["middleware-protected"], false);
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1889
|
+
var setAuth = setAuth2, applyMiddleware = applyMiddleware2;
|
|
1890
|
+
for (const node of apiNodes) {
|
|
1891
|
+
const routePath = node.path;
|
|
1892
|
+
if (!routePath) continue;
|
|
1893
|
+
applyMiddleware2(node, routePath);
|
|
1894
|
+
}
|
|
1895
|
+
for (const node of uiNodes) {
|
|
1896
|
+
const route = node.route;
|
|
1897
|
+
if (!route) continue;
|
|
1898
|
+
applyMiddleware2(node, route);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1580
1901
|
const apiNodeIds = new Set(apiNodes.map((n) => n.id));
|
|
1581
1902
|
const apiEdges = [];
|
|
1582
1903
|
const uiOnlyEdges = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@launchsecure/launch-kit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.25",
|
|
4
4
|
"description": "LaunchSecure toolkit — launch-pod (pipeline), launch-chart (project graph MCP), launch-deck (visual playground MCP).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "LaunchSecure - AutomateWithUs",
|