botversion-sdk 1.0.4 → 1.0.5

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/scanner.js CHANGED
@@ -4,31 +4,94 @@
4
4
  /**
5
5
  * Scans an Express app object and extracts all registered routes
6
6
  */
7
- function scanExpressRoutes(app) {
7
+ function scanExpressRoutes(app, cwd) {
8
8
  const endpoints = [];
9
9
  const seen = new Set();
10
10
 
11
- // Force Express to initialize its router if it hasn't yet
12
- if (app.lazyrouter) app.lazyrouter();
13
- const router = app._router || app.router || (app.stack ? app : null);
11
+ // Build body map from ALL files upfront
12
+ const bodyMap = cwd ? buildBodyMap(cwd) : {};
14
13
 
15
- if (!router) {
16
- console.warn(
17
- "[BotVersion SDK] No router found — trying app._events fallback",
18
- );
14
+ if (app) {
15
+ if (app.lazyrouter) app.lazyrouter();
16
+ const router = app._router || app.router || (app.stack ? app : null);
17
+ if (router) {
18
+ const stack = router.stack || [];
19
+ extractRoutes(stack, "", endpoints, seen, bodyMap);
20
+ }
21
+ }
19
22
 
20
- if (app._events && app._events.request) {
21
- const handler = app._events.request;
22
- if (handler && handler.stack) {
23
- extractRoutes(handler.stack, "", endpoints, seen);
24
- return endpoints;
25
- }
23
+ // ALSO scan all JS/TS files statically for route definitions
24
+ if (cwd) {
25
+ const allFiles = scanAllExpressFiles(cwd);
26
+ for (const file of allFiles) {
27
+ const fileEndpoints = scanExpressFileStatically(file, seen);
28
+ endpoints.push(...fileEndpoints);
26
29
  }
30
+ }
31
+
32
+ console.log(`\n[DEBUG] ===== JS SCAN SUMMARY =====`);
33
+ endpoints.forEach((ep) => {
34
+ const status = ep.requestBody ? "✅" : "❌ NULL";
35
+ console.log(
36
+ `[DEBUG] ${status} ${ep.method.padEnd(6)} ${ep.path} → ${JSON.stringify(ep.requestBody)}`,
37
+ );
38
+ });
39
+ console.log(`[DEBUG] ================================\n`);
40
+
41
+ return endpoints;
42
+ }
43
+
44
+ function scanExpressFileStatically(filePath, seen) {
45
+ const fs = require("fs");
46
+ const endpoints = [];
47
+
48
+ let content;
49
+ try {
50
+ content = fs.readFileSync(filePath, "utf8");
51
+ } catch {
27
52
  return endpoints;
28
53
  }
54
+ console.log(`[DEBUG] Scanning file statically: ${filePath}`);
55
+
56
+ // Match patterns like:
57
+ // app.get('/path', ...)
58
+ // router.post('/path', ...)
59
+ // app.all('/curd.php', ...)
60
+ const routePattern =
61
+ /\.(get|post|put|delete|patch|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
62
+
63
+ let match;
64
+ while ((match = routePattern.exec(content)) !== null) {
65
+ const method = match[1].toUpperCase();
66
+ const routePath = match[2];
67
+
68
+ // Skip middleware patterns
69
+ if (routePath.includes("*")) continue;
70
+
71
+ const key = method + ":" + routePath;
72
+ if (seen.has(key)) continue;
73
+ seen.add(key);
74
+
75
+ const effectiveMethod = method === "ALL" ? "GET" : method;
76
+ const needsBody = ["POST", "PUT", "PATCH"].includes(effectiveMethod);
77
+ const bodyFields = needsBody ? extractBodyFieldsFromFile(content) : null;
78
+
79
+ console.log(
80
+ `[DEBUG] ${effectiveMethod} ${routePath} → bodyFields: ${JSON.stringify(bodyFields)}`,
81
+ );
82
+
83
+ const routeParamMap = buildRouteParamMap(routePath, []);
84
+
85
+ endpoints.push({
86
+ method: effectiveMethod,
87
+ path: routePath,
88
+ description: "",
89
+ requestBody: bodyFields,
90
+ routeParamMap: routeParamMap,
91
+ detectedBy: "static-scan-file",
92
+ });
93
+ }
29
94
 
30
- const stack = router.stack || [];
31
- extractRoutes(stack, "", endpoints, seen);
32
95
  return endpoints;
33
96
  }
34
97
 
@@ -40,7 +103,7 @@ function scanExpressRoutes(app) {
40
103
  * - a router layer → layer.name === 'router', has its own .handle.stack
41
104
  * - middleware → everything else (body-parser, cors, etc.) — skip these
42
105
  */
43
- function extractRoutes(stack, prefix, endpoints, seen) {
106
+ function extractRoutes(stack, prefix, endpoints, seen, bodyMap) {
44
107
  prefix = prefix || "";
45
108
 
46
109
  stack.forEach(function (layer) {
@@ -53,28 +116,54 @@ function extractRoutes(stack, prefix, endpoints, seen) {
53
116
 
54
117
  methods.forEach(function (method) {
55
118
  method = method.toUpperCase();
56
-
57
- // Skip Express internals
58
119
  if (method === "_ALL") return;
59
120
 
60
121
  var key = method + ":" + routePath;
61
122
  if (seen.has(key)) return;
62
123
  seen.add(key);
63
124
 
64
- var params = extractPathParams(routePath);
125
+ const needsBody = ["POST", "PUT", "PATCH"].includes(method);
126
+ let requestBody = null;
127
+
128
+ if (needsBody) {
129
+ // Strategy 1: scan inline handler directly via fn.toString() — most accurate
130
+ for (const handler of layer.route.stack) {
131
+ const fn = handler.handle || handler;
132
+ const fnStr = fn.toString();
133
+ const fields = extractBodyFieldsFromFile(fnStr);
134
+ if (fields) {
135
+ requestBody = fields;
136
+ console.log(
137
+ `[DEBUG] Found body from inline handler for ${method} ${routePath}`,
138
+ );
139
+ break;
140
+ }
141
+ }
142
+
143
+ // Strategy 2: fall back to bodyMap using handler name
144
+ if (!requestBody) {
145
+ const handlerName = extractHandlerName(layer);
146
+ console.log(
147
+ `[DEBUG] ${method} ${routePath} → handlerName: ${handlerName}`,
148
+ );
149
+ if (handlerName && bodyMap[handlerName]) {
150
+ requestBody = bodyMap[handlerName];
151
+ console.log(`[DEBUG] Found body from bodyMap for ${handlerName}`);
152
+ }
153
+ }
154
+ }
155
+
156
+ const routeParamMap = buildRouteParamMap(routePath, []);
65
157
 
66
158
  endpoints.push({
67
159
  method: method,
68
160
  path: routePath,
69
161
  description: "",
70
- requestBody:
71
- method !== "GET" && params.length > 0
72
- ? buildParamSchema(params)
73
- : null,
162
+ requestBody: requestBody,
163
+ routeParamMap: routeParamMap,
74
164
  detectedBy: "static-scan",
75
165
  });
76
166
  });
77
-
78
167
  return;
79
168
  }
80
169
 
@@ -82,7 +171,7 @@ function extractRoutes(stack, prefix, endpoints, seen) {
82
171
  if (layer.name === "router" && layer.handle && layer.handle.stack) {
83
172
  // Extract the mount path from the regexp
84
173
  var mountPath = prefix + regexpToPath(layer.regexp, layer.keys);
85
- extractRoutes(layer.handle.stack, mountPath, endpoints, seen);
174
+ extractRoutes(layer.handle.stack, mountPath, endpoints, seen, bodyMap);
86
175
  return;
87
176
  }
88
177
 
@@ -139,11 +228,14 @@ function scanNextJsRoutes(pagesDir) {
139
228
  bodyFields ||
140
229
  (method === "DELETE" && queryFields ? queryFields : null);
141
230
 
231
+ const routeParamMap = buildRouteParamMap(normalizedPath, []);
232
+
142
233
  endpoints.push({
143
234
  method: method,
144
235
  path: normalizedPath,
145
236
  description: "",
146
237
  requestBody: effectiveRequestBody,
238
+ routeParamMap: routeParamMap,
147
239
  detectedBy: "static-scan",
148
240
  });
149
241
  });
@@ -230,6 +322,35 @@ function buildParamSchema(params) {
230
322
  return schema;
231
323
  }
232
324
 
325
+ function inferFieldType(fieldName, content) {
326
+ const arrayPatterns = [
327
+ new RegExp(
328
+ `${fieldName}\\s*\\.\\s*(map|filter|forEach|push|reduce|find|some|every|includes|join|slice|splice|length)\\b`,
329
+ ),
330
+ new RegExp(`Array\\.isArray\\s*\\(\\s*${fieldName}\\s*\\)`),
331
+ new RegExp(`for\\s*\\(.*of\\s+${fieldName}\\b`),
332
+ new RegExp(`\\[\\s*\\.\\.\\.${fieldName}\\s*\\]`),
333
+ ];
334
+ if (arrayPatterns.some((p) => p.test(content))) return "array";
335
+
336
+ const numberPatterns = [
337
+ new RegExp(`${fieldName}\\s*[+\\-*/%]\\s*\\d`),
338
+ new RegExp(`parseInt\\s*\\(\\s*${fieldName}`),
339
+ new RegExp(`parseFloat\\s*\\(\\s*${fieldName}`),
340
+ new RegExp(`Number\\s*\\(\\s*${fieldName}`),
341
+ ];
342
+ if (numberPatterns.some((p) => p.test(content))) return "number";
343
+
344
+ const boolPatterns = [
345
+ new RegExp(`${fieldName}\\s*===?\\s*(true|false)`),
346
+ new RegExp(`(true|false)\\s*===?\\s*${fieldName}`),
347
+ new RegExp(`Boolean\\s*\\(\\s*${fieldName}`),
348
+ ];
349
+ if (boolPatterns.some((p) => p.test(content))) return "boolean";
350
+
351
+ return "string";
352
+ }
353
+
233
354
  function extractBodyFieldsFromFile(content) {
234
355
  const fields = new Set();
235
356
 
@@ -284,12 +405,192 @@ function extractBodyFieldsFromFile(content) {
284
405
 
285
406
  const properties = {};
286
407
  fields.forEach(function (field) {
287
- properties[field] = { type: "string" };
408
+ const type = inferFieldType(field, content);
409
+ properties[field] =
410
+ type === "array"
411
+ ? { type: "array", items: { type: "object" } }
412
+ : { type };
288
413
  });
414
+ console.log(
415
+ `[DEBUG] extractBodyFieldsFromFile found fields: ${JSON.stringify([...fields])}`,
416
+ );
289
417
 
290
418
  return { type: "object", properties };
291
419
  }
292
420
 
421
+ function scanNextJsAppRoutes(appDir) {
422
+ const fs = require("fs");
423
+ const path = require("path");
424
+ const endpoints = [];
425
+
426
+ const apiDir = path.join(appDir, "api");
427
+ if (!fs.existsSync(apiDir)) {
428
+ console.warn("[BotVersion SDK] No app/api directory found at:", apiDir);
429
+ return endpoints;
430
+ }
431
+
432
+ function walkDir(dir, routePath) {
433
+ routePath = routePath || "/api";
434
+ const files = fs.readdirSync(dir);
435
+
436
+ files.forEach(function (file) {
437
+ const fullPath = path.join(dir, file);
438
+ const stat = fs.statSync(fullPath);
439
+
440
+ if (stat.isDirectory()) {
441
+ // Convert [param] → :param
442
+ const segment = file.replace(/\[([^\]]+)\]/g, ":$1");
443
+ walkDir(fullPath, routePath + "/" + segment);
444
+ return;
445
+ }
446
+
447
+ // Only process route.ts / route.js
448
+ if (!/^route\.(js|ts)$/.test(file)) return;
449
+
450
+ const content = fs.readFileSync(fullPath, "utf8");
451
+ const methods = detectAppRouterMethods(content);
452
+
453
+ methods.forEach(function (method) {
454
+ const bodyFields =
455
+ method !== "GET" ? extractAppRouterBodyFields(content) : null;
456
+ const queryFields = extractQueryFieldsFromFile(content);
457
+
458
+ const routeParamMap = buildRouteParamMap(routePath, []);
459
+
460
+ endpoints.push({
461
+ method: method,
462
+ path: routePath,
463
+ description: "",
464
+ requestBody:
465
+ bodyFields ||
466
+ (method === "DELETE" && queryFields ? queryFields : null),
467
+ routeParamMap: routeParamMap,
468
+ detectedBy: "static-scan",
469
+ });
470
+ });
471
+ });
472
+ }
473
+
474
+ walkDir(apiDir);
475
+ return endpoints;
476
+ }
477
+
478
+ function detectAppRouterMethods(content) {
479
+ const methods = [];
480
+ const patterns = [
481
+ { pattern: /export\s+async\s+function\s+GET\b/, method: "GET" },
482
+ { pattern: /export\s+async\s+function\s+POST\b/, method: "POST" },
483
+ { pattern: /export\s+async\s+function\s+PUT\b/, method: "PUT" },
484
+ { pattern: /export\s+async\s+function\s+DELETE\b/, method: "DELETE" },
485
+ { pattern: /export\s+async\s+function\s+PATCH\b/, method: "PATCH" },
486
+ // named exports too: export { POST }
487
+ { pattern: /export\s+function\s+GET\b/, method: "GET" },
488
+ { pattern: /export\s+function\s+POST\b/, method: "POST" },
489
+ { pattern: /export\s+function\s+PUT\b/, method: "PUT" },
490
+ { pattern: /export\s+function\s+DELETE\b/, method: "DELETE" },
491
+ { pattern: /export\s+function\s+PATCH\b/, method: "PATCH" },
492
+ ];
493
+
494
+ patterns.forEach(function (p) {
495
+ if (p.pattern.test(content)) methods.push(p.method);
496
+ });
497
+
498
+ return methods.length > 0 ? methods : ["GET"];
499
+ }
500
+
501
+ function extractAppRouterBodyFields(content) {
502
+ const fields = new Set();
503
+
504
+ // Pattern 1: const { userId, tokens } = await request.json()
505
+ const destructureMatches = content.matchAll(
506
+ /const\s*\{([^}]+)\}\s*=\s*await\s+\w+\.json\(\)/g,
507
+ );
508
+ for (const match of destructureMatches) {
509
+ match[1].split(",").forEach(function (f) {
510
+ const clean = f.trim().split(":")[0].trim();
511
+ if (clean) fields.add(clean);
512
+ });
513
+ }
514
+
515
+ // Pattern 2: const body = await request.json() then body.userId
516
+ const bodyVarMatch = content.match(
517
+ /const\s+(\w+)\s*=\s*await\s+\w+\.json\(\)/,
518
+ );
519
+ if (bodyVarMatch) {
520
+ const varName = bodyVarMatch[1];
521
+ const varMatches = content.matchAll(
522
+ new RegExp(`${varName}\\.([a-zA-Z_][a-zA-Z0-9_]*)`, "g"),
523
+ );
524
+ for (const match of varMatches) {
525
+ fields.add(match[1]);
526
+ }
527
+ }
528
+
529
+ // Pattern 3: (await request.json()).userId
530
+ const inlineMatches = content.matchAll(
531
+ /\(await\s+\w+\.json\(\)\)\.([a-zA-Z_][a-zA-Z0-9_]*)/g,
532
+ );
533
+ for (const match of inlineMatches) {
534
+ fields.add(match[1]);
535
+ }
536
+
537
+ if (fields.size === 0) return null;
538
+
539
+ const properties = {};
540
+ fields.forEach(function (field) {
541
+ const type = inferFieldType(field, content);
542
+ properties[field] =
543
+ type === "array"
544
+ ? { type: "array", items: { type: "object" } }
545
+ : { type };
546
+ });
547
+
548
+ return { type: "object", properties };
549
+ }
550
+
551
+ function buildRouteParamMap(routePath, segments) {
552
+ const paramMap = {};
553
+ const parts = routePath.split("/").filter(Boolean);
554
+
555
+ for (let i = 0; i < parts.length; i++) {
556
+ const part = parts[i];
557
+
558
+ // Skip catch-all [...slug] and optional [[...slug]]
559
+ if (/^\[?\[\.\.\./.test(part)) continue;
560
+
561
+ // Check if this segment is a dynamic param
562
+ // Handles both Next.js style [id] and Express style :id
563
+ const nextParamMatch = part.match(/^\[([^\]]+)\]$/);
564
+ const expressParamMatch = part.match(/^:([a-zA-Z_][a-zA-Z0-9_]*)$/);
565
+ const paramMatch = nextParamMatch || expressParamMatch;
566
+ if (!paramMatch) continue;
567
+
568
+ const paramName = paramMatch[1];
569
+
570
+ // If param already has a descriptive name like [projectId], use it as-is
571
+ if (paramName !== "id" && paramName !== "slug" && paramName !== "param") {
572
+ paramMap[paramName] = paramName;
573
+ continue;
574
+ }
575
+
576
+ // If param is just [id], look at the parent folder name to figure out type
577
+ // e.g. projects/[id] → projectId
578
+ const parentSegment = parts[i - 1];
579
+ if (parentSegment) {
580
+ // Remove any dynamic brackets from parent if it's also a param
581
+ const cleanParent = parentSegment.replace(/^\[([^\]]+)\]$/, "$1");
582
+ // Singularize simple plural names: projects → project, users → user
583
+ const singular = cleanParent.replace(/ies$/, "y").replace(/s$/, "");
584
+ paramMap[paramName] = singular + "Id";
585
+ } else {
586
+ // No parent segment, just call it "id"
587
+ paramMap[paramName] = "id";
588
+ }
589
+ }
590
+
591
+ return paramMap;
592
+ }
593
+
293
594
  function extractQueryFieldsFromFile(content) {
294
595
  const fields = new Set();
295
596
 
@@ -355,8 +656,472 @@ function regexpToPath(regexp, keys) {
355
656
  }
356
657
  }
357
658
 
659
+ function scanAllExpressFiles(cwd) {
660
+ const fs = require("fs");
661
+ const path = require("path");
662
+
663
+ const SKIP_DIRS = [
664
+ "node_modules",
665
+ ".git",
666
+ ".next",
667
+ "dist",
668
+ "build",
669
+ ".cache",
670
+ "coverage",
671
+ "out",
672
+ ];
673
+
674
+ const routeFiles = [];
675
+
676
+ function walk(dir, depth) {
677
+ if (depth > 4) return;
678
+
679
+ let entries;
680
+ try {
681
+ entries = fs.readdirSync(dir);
682
+ } catch {
683
+ return;
684
+ }
685
+
686
+ for (const entry of entries) {
687
+ if (SKIP_DIRS.includes(entry)) continue;
688
+
689
+ const fullPath = path.join(dir, entry);
690
+ let stat;
691
+ try {
692
+ stat = fs.statSync(fullPath);
693
+ } catch {
694
+ continue;
695
+ }
696
+
697
+ if (stat.isDirectory()) {
698
+ walk(fullPath, depth + 1);
699
+ } else if (/\.(js|ts)$/.test(entry)) {
700
+ try {
701
+ const content = fs.readFileSync(fullPath, "utf8");
702
+ // Check if file contains Express route definitions
703
+ const isExpressFile =
704
+ content.includes("express()") ||
705
+ content.includes("express.Router()") ||
706
+ content.includes("Router()") ||
707
+ // Match only route-like patterns: app.get('/...) or router.post('/...)
708
+ /(?:app|router|server)\.(get|post|put|delete|patch|all)\s*\(\s*['"`]\//.test(
709
+ content,
710
+ );
711
+
712
+ if (isExpressFile) {
713
+ console.log(`[DEBUG] Found route file: ${fullPath}`); // already have this
714
+ routeFiles.push(fullPath);
715
+ }
716
+
717
+ // ADD THIS — log files that are being SKIPPED
718
+ else if (/\.(js|ts)$/.test(entry)) {
719
+ console.log(
720
+ `[DEBUG] Skipped (not detected as route file): ${fullPath}`,
721
+ );
722
+ }
723
+ } catch {
724
+ continue;
725
+ }
726
+ }
727
+ }
728
+ }
729
+
730
+ walk(cwd, 0);
731
+ return routeFiles;
732
+ }
733
+
734
+ function buildBodyMap(cwd) {
735
+ const fs = require("fs");
736
+ const path = require("path");
737
+ const bodyMap = {}; // { functionName: { type: "object", properties: {...} } }
738
+
739
+ const SKIP_DIRS = [
740
+ "node_modules",
741
+ ".git",
742
+ ".next",
743
+ "dist",
744
+ "build",
745
+ ".cache",
746
+ "coverage",
747
+ "out",
748
+ ];
749
+
750
+ function walk(dir, depth) {
751
+ if (depth > 4) return;
752
+ let entries;
753
+ try {
754
+ entries = fs.readdirSync(dir);
755
+ } catch {
756
+ return;
757
+ }
758
+
759
+ for (const entry of entries) {
760
+ if (SKIP_DIRS.includes(entry)) continue;
761
+ const fullPath = path.join(dir, entry);
762
+ let stat;
763
+ try {
764
+ stat = fs.statSync(fullPath);
765
+ } catch {
766
+ continue;
767
+ }
768
+
769
+ if (stat.isDirectory()) {
770
+ walk(fullPath, depth + 1);
771
+ continue;
772
+ }
773
+
774
+ if (!/\.(js|ts)$/.test(entry)) continue;
775
+
776
+ let content;
777
+ try {
778
+ content = fs.readFileSync(fullPath, "utf8");
779
+ } catch {
780
+ continue;
781
+ }
782
+
783
+ // Skip files with no req.body at all
784
+ // Split file into individual function chunks more reliably
785
+ // by finding each function and extracting a reasonable chunk after it
786
+
787
+ const fnPatterns = [
788
+ // function loginUser(req, res) {
789
+ /(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\([^)]*\)\s*\{/g,
790
+ // const loginUser = async (req, res) => {
791
+ /const\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{/g,
792
+ // exports.loginUser = async (req, res) => {
793
+ /exports\.([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{/g,
794
+ // export const loginUser = async (req, res) => {
795
+ /export\s+const\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{/g,
796
+ ];
797
+
798
+ if (content.includes("req.body")) {
799
+ console.log(`[DEBUG] bodyMap: found req.body in ${fullPath}`);
800
+ console.log(
801
+ `[DEBUG] bodyMap: fnNames found: ${JSON.stringify([
802
+ ...(() => {
803
+ const names = new Set();
804
+ for (const p of fnPatterns) {
805
+ p.lastIndex = 0;
806
+ let m;
807
+ while ((m = p.exec(content)) !== null) names.add(m[1]);
808
+ }
809
+ return names;
810
+ })(),
811
+ ])}`,
812
+ );
813
+ }
814
+
815
+ // Map every function name in this file to the whole file's body fields.
816
+ // Brace-counting is unreliable across JS/TS variations, so we use
817
+ // file-level field extraction here. Per-function accuracy is handled
818
+ // at runtime by Strategy 2 (fn.toString()) in extractRoutes.
819
+ const fileFields = extractBodyFieldsFromFile(content);
820
+ if (fileFields) {
821
+ const fnNames = new Set();
822
+ for (const pattern of fnPatterns) {
823
+ pattern.lastIndex = 0; // reset regex state before each use
824
+ let m;
825
+ while ((m = pattern.exec(content)) !== null) fnNames.add(m[1]);
826
+ }
827
+ for (const name of fnNames) {
828
+ if (!bodyMap[name]) {
829
+ // don't overwrite a more precise entry
830
+ bodyMap[name] = fileFields;
831
+ }
832
+ }
833
+ }
834
+ }
835
+ }
836
+
837
+ walk(cwd, 0);
838
+ console.log(`[DEBUG] bodyMap keys: ${Object.keys(bodyMap)}`);
839
+ return bodyMap;
840
+ }
841
+
842
+ function extractHandlerName(layer) {
843
+ const handlers = layer.route.stack.map((h) => h.handle || h);
844
+
845
+ const SKIP_NAMES = new Set([
846
+ "anonymous",
847
+ "",
848
+ "bound dispatch",
849
+ "middleware",
850
+ "protect",
851
+ "admin",
852
+ "auth",
853
+ "verify",
854
+ "validate",
855
+ "isAuth",
856
+ "isAdmin",
857
+ "checkAuth",
858
+ "authenticate",
859
+ ]);
860
+
861
+ // Try from last to first, skip known middleware names
862
+ for (let i = handlers.length - 1; i >= 0; i--) {
863
+ const fn = handlers[i];
864
+ const name = fn.name || "";
865
+ if (name && !SKIP_NAMES.has(name) && !name.startsWith("bound ")) {
866
+ return name;
867
+ }
868
+ }
869
+ return null;
870
+ }
871
+
872
+ function convertNextJsSegment(segment) {
873
+ // Skip catch-all [...slug] and optional [[...slug]]
874
+ if (/^\[?\[\.\.\./.test(segment)) return null;
875
+ // Convert [projectId] → :projectId (Next.js / Nuxt / SvelteKit)
876
+ if (/^\[([^\]]+)\]$/.test(segment))
877
+ return segment.replace(/^\[([^\]]+)\]$/, ":$1");
878
+ // Convert $projectId → :projectId (Remix)
879
+ if (/^\$([a-zA-Z_][a-zA-Z0-9_]*)$/.test(segment))
880
+ return segment.replace(/^\$/, ":");
881
+ return segment;
882
+ }
883
+
884
+ function extractParamPositions(segments) {
885
+ const paramMap = {};
886
+ segments.forEach(function (segment, index) {
887
+ if (segment && segment.startsWith(":")) {
888
+ const paramName = segment.slice(1);
889
+ paramMap[paramName] = index;
890
+ }
891
+ });
892
+ return paramMap;
893
+ }
894
+
895
+ function scanConfigBasedRoutes(cwd) {
896
+ const fs = require("fs");
897
+ const path = require("path");
898
+ const patterns = [];
899
+ const seen = new Set();
900
+
901
+ // Files to scan for route definitions
902
+ const filesToCheck = [
903
+ // React Router — common file names
904
+ path.join(cwd, "src", "App.jsx"),
905
+ path.join(cwd, "src", "App.tsx"),
906
+ path.join(cwd, "src", "App.js"),
907
+ path.join(cwd, "src", "router.jsx"),
908
+ path.join(cwd, "src", "router.tsx"),
909
+ path.join(cwd, "src", "router.js"),
910
+ path.join(cwd, "src", "routes.jsx"),
911
+ path.join(cwd, "src", "routes.tsx"),
912
+ path.join(cwd, "src", "routes.js"),
913
+ path.join(cwd, "src", "Router.jsx"),
914
+ path.join(cwd, "src", "Router.tsx"),
915
+ // Vue Router
916
+ path.join(cwd, "src", "router", "index.js"),
917
+ path.join(cwd, "src", "router", "index.ts"),
918
+ path.join(cwd, "src", "router.js"),
919
+ path.join(cwd, "src", "router.ts"),
920
+ // Angular
921
+ path.join(cwd, "src", "app", "app-routing.module.ts"),
922
+ path.join(cwd, "src", "app", "app.routes.ts"),
923
+ ];
924
+
925
+ for (const filePath of filesToCheck) {
926
+ if (!fs.existsSync(filePath)) continue;
927
+
928
+ let content;
929
+ try {
930
+ content = fs.readFileSync(filePath, "utf8");
931
+ } catch {
932
+ continue;
933
+ }
934
+
935
+ console.log("[BotVersion SDK] Scanning config-based routes in:", filePath);
936
+
937
+ // Pattern 1 — React Router JSX: <Route path="/:projectId/dashboard" />
938
+ const jsxRouteMatches = content.matchAll(
939
+ /<Route[^>]+path=["']([^"']+)["']/g,
940
+ );
941
+ for (const match of jsxRouteMatches) {
942
+ addConfigPattern(match[1], seen, patterns);
943
+ }
944
+
945
+ // Pattern 2 — React Router / Vue Router object: { path: '/:projectId/dashboard' }
946
+ const objectRouteMatches = content.matchAll(/path\s*:\s*["']([^"']+)["']/g);
947
+ for (const match of objectRouteMatches) {
948
+ addConfigPattern(match[1], seen, patterns);
949
+ }
950
+
951
+ // Pattern 3 — Angular: { path: ':projectId/dashboard' }
952
+ const angularRouteMatches = content.matchAll(
953
+ /\{\s*path\s*:\s*["']([^"']+)["']/g,
954
+ );
955
+ for (const match of angularRouteMatches) {
956
+ addConfigPattern(match[1], seen, patterns);
957
+ }
958
+ }
959
+
960
+ return patterns;
961
+ }
962
+
963
+ function addConfigPattern(routePath, seen, patterns) {
964
+ // Skip empty, wildcard and catch-all routes
965
+ if (!routePath || routePath === "*" || routePath === "**") return;
966
+ // Skip routes with no dynamic params
967
+ if (!routePath.includes(":") && !routePath.includes("$")) return;
968
+
969
+ // Normalize — ensure leading slash
970
+ const normalized = routePath.startsWith("/") ? routePath : "/" + routePath;
971
+
972
+ if (seen.has(normalized)) return;
973
+ seen.add(normalized);
974
+
975
+ // Extract params and their positions
976
+ const segments = normalized.split("/").filter(Boolean);
977
+ const paramMap = {};
978
+ segments.forEach(function (segment, index) {
979
+ if (segment.startsWith(":")) {
980
+ paramMap[segment.slice(1)] = index;
981
+ }
982
+ });
983
+
984
+ if (Object.keys(paramMap).length === 0) return;
985
+
986
+ patterns.push({ pattern: normalized, params: paramMap });
987
+ console.log(
988
+ "[BotVersion SDK] Found config-based route pattern:",
989
+ normalized,
990
+ "→",
991
+ paramMap,
992
+ );
993
+ }
994
+
995
+ function scanFrontendRoutes(cwd) {
996
+ const fs = require("fs");
997
+ const path = require("path");
998
+ const patterns = [];
999
+ const seen = new Set();
1000
+
1001
+ const dirsToScan = [
1002
+ // Next.js Pages Router
1003
+ path.join(cwd, "pages"),
1004
+ path.join(cwd, "src", "pages"),
1005
+ // Next.js App Router
1006
+ path.join(cwd, "app"),
1007
+ path.join(cwd, "src", "app"),
1008
+ // Nuxt.js
1009
+ path.join(cwd, "pages"),
1010
+ // SvelteKit
1011
+ path.join(cwd, "src", "routes"),
1012
+ path.join(cwd, "routes"),
1013
+ // Remix
1014
+ path.join(cwd, "app", "routes"),
1015
+ ];
1016
+
1017
+ function walkDir(dir, routeSegments) {
1018
+ if (!fs.existsSync(dir)) return;
1019
+ let entries;
1020
+ try {
1021
+ entries = fs.readdirSync(dir);
1022
+ } catch {
1023
+ return;
1024
+ }
1025
+
1026
+ entries.forEach(function (file) {
1027
+ const fullPath = path.join(dir, file);
1028
+ let stat;
1029
+ try {
1030
+ stat = fs.statSync(fullPath);
1031
+ } catch {
1032
+ return;
1033
+ }
1034
+
1035
+ if (stat.isDirectory()) {
1036
+ if (file === "api") return; // skip API routes
1037
+ if (file.startsWith("_")) return; // skip Next.js internals
1038
+ if (/^\(.*\)$/.test(file)) {
1039
+ // route groups like (marketing) — don't add to URL
1040
+ walkDir(fullPath, routeSegments);
1041
+ return;
1042
+ }
1043
+ const segment = convertNextJsSegment(file);
1044
+ if (!segment) return;
1045
+ walkDir(fullPath, routeSegments.concat(segment));
1046
+ return;
1047
+ }
1048
+
1049
+ if (!/\.(js|ts|jsx|tsx|vue|svelte)$/.test(file)) return;
1050
+ if (file.startsWith("_")) return;
1051
+
1052
+ const routeName = file.replace(/\.(js|ts|jsx|tsx|vue|svelte)$/, "");
1053
+
1054
+ // Skip non-page files
1055
+ if (
1056
+ ["layout", "loading", "error", "template", "not-found"].includes(
1057
+ routeName,
1058
+ )
1059
+ )
1060
+ return;
1061
+
1062
+ // SvelteKit uses +page.svelte — skip +layout, +error etc.
1063
+ if (routeName.startsWith("+") && routeName !== "+page") return;
1064
+
1065
+ // Remix uses dots as path separators — e.g. $projectId.dashboard.tsx → /:projectId/dashboard
1066
+ const isRemixRoute =
1067
+ routeName.includes(".") && !routeName.startsWith("+");
1068
+ let finalSegments;
1069
+
1070
+ if (isRemixRoute) {
1071
+ // Split by dots and convert each segment
1072
+ const remixSegments = routeName
1073
+ .split(".")
1074
+ .map((s) => convertNextJsSegment(s) || s);
1075
+ finalSegments = routeSegments.concat(remixSegments);
1076
+ } else if (
1077
+ routeName === "index" ||
1078
+ routeName === "page" ||
1079
+ routeName === "+page"
1080
+ ) {
1081
+ finalSegments = routeSegments;
1082
+ } else {
1083
+ finalSegments = routeSegments.concat(
1084
+ convertNextJsSegment(routeName) || routeName,
1085
+ );
1086
+ }
1087
+
1088
+ const pattern = "/" + finalSegments.filter(Boolean).join("/");
1089
+ if (seen.has(pattern)) return;
1090
+ seen.add(pattern);
1091
+
1092
+ const paramMap = extractParamPositions(finalSegments);
1093
+ if (Object.keys(paramMap).length === 0) return; // no dynamic params, skip static routes
1094
+
1095
+ patterns.push({ pattern, params: paramMap });
1096
+ console.log(
1097
+ "[BotVersion SDK] Found frontend route pattern:",
1098
+ pattern,
1099
+ "→",
1100
+ paramMap,
1101
+ );
1102
+ });
1103
+ }
1104
+
1105
+ dirsToScan.forEach(function (dir) {
1106
+ if (fs.existsSync(dir)) walkDir(dir, []);
1107
+ });
1108
+
1109
+ // Also scan config-based frameworks (React Router, Vue Router, Angular)
1110
+ const configPatterns = scanConfigBasedRoutes(cwd);
1111
+ configPatterns.forEach(function (p) {
1112
+ if (!seen.has(p.pattern)) {
1113
+ seen.add(p.pattern);
1114
+ patterns.push(p);
1115
+ }
1116
+ });
1117
+
1118
+ return patterns;
1119
+ }
1120
+
358
1121
  module.exports = {
359
1122
  scanExpressRoutes,
360
1123
  scanNextJsRoutes,
1124
+ scanNextJsAppRoutes,
361
1125
  extractPathParams,
1126
+ scanFrontendRoutes,
362
1127
  };