botversion-sdk 1.1.7 → 2.1.0

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/interceptor.js CHANGED
@@ -46,6 +46,39 @@ function attachInterceptor(app, client, options) {
46
46
 
47
47
  app.use(function botVersionInterceptor(req, res, next) {
48
48
  const path = req.path || req.url || "";
49
+
50
+ // ── Scan trigger from BotVersion dashboard ───────────────────────────
51
+ if (path === "/__botversion/scan" && req.method === "POST") {
52
+ const providedKey = req.headers["x-botversion-scan-key"] || "";
53
+ if (
54
+ options.scanSecret &&
55
+ providedKey === options.scanSecret &&
56
+ typeof options.onScanRequested === "function"
57
+ ) {
58
+ Promise.resolve(options.onScanRequested())
59
+ .then(function (result) {
60
+ res.statusCode = 200;
61
+ res.setHeader("Content-Type", "application/json");
62
+ res.end(JSON.stringify({ success: true, result: result || null }));
63
+ })
64
+ .catch(function (err) {
65
+ res.statusCode = 500;
66
+ res.setHeader("Content-Type", "application/json");
67
+ res.end(
68
+ JSON.stringify({
69
+ success: false,
70
+ error: String((err && err.message) || err),
71
+ }),
72
+ );
73
+ });
74
+ } else {
75
+ res.statusCode = 401;
76
+ res.setHeader("Content-Type", "application/json");
77
+ res.end(JSON.stringify({ success: false, error: "Unauthorized" }));
78
+ }
79
+ return;
80
+ }
81
+
49
82
  const shouldIgnore = ignorePaths.some(function (p) {
50
83
  return path.startsWith(p);
51
84
  });
@@ -248,18 +281,70 @@ function attachNextJsInterceptor(client, options) {
248
281
  const path = req.url ? req.url.split("?")[0] : "";
249
282
  const method = req.method ? req.method.toUpperCase() : "";
250
283
 
251
- const shouldIgnore = (options.exclude || [])
252
- .concat([
253
- "/health",
254
- "/favicon.ico",
255
- "/_next",
256
- "/static",
257
- "/public",
258
- "/admin",
259
- ])
260
- .some(function (p) {
261
- return path.startsWith(p);
262
- });
284
+ // ── Scan trigger from BotVersion dashboard ───────────────────────
285
+ if (path === "/__botversion/scan" && method === "POST") {
286
+ const providedKey = req.headers["x-botversion-scan-key"] || "";
287
+ if (
288
+ options.scanSecret &&
289
+ providedKey === options.scanSecret &&
290
+ typeof options.onScanRequested === "function"
291
+ ) {
292
+ Promise.resolve(options.onScanRequested())
293
+ .then(function (result) {
294
+ res.statusCode = 200;
295
+ res.setHeader("Content-Type", "application/json");
296
+ res.end(
297
+ JSON.stringify({ success: true, result: result || null }),
298
+ );
299
+ })
300
+ .catch(function (err) {
301
+ res.statusCode = 500;
302
+ res.setHeader("Content-Type", "application/json");
303
+ res.end(
304
+ JSON.stringify({
305
+ success: false,
306
+ error: String((err && err.message) || err),
307
+ }),
308
+ );
309
+ });
310
+ } else {
311
+ res.statusCode = 401;
312
+ res.setHeader("Content-Type", "application/json");
313
+ res.end(JSON.stringify({ success: false, error: "Unauthorized" }));
314
+ }
315
+ return; // handled directly — don't pass through to the app
316
+ }
317
+
318
+ const DEV_ASSET_MARKERS = [
319
+ "/node_modules",
320
+ "/.svelte-kit",
321
+ "/@fs",
322
+ "/@vite",
323
+ "/@id",
324
+ "/debug-cors",
325
+ "/src/lib",
326
+ "/src/routes",
327
+ ];
328
+ const STATIC_ASSET_EXT =
329
+ /\.(svelte|vue|css|scss|map|woff2?|ttf|eot|ico|png|jpe?g|gif|svg|webp|mjs)(\?.*)?$/i;
330
+
331
+ const shouldIgnore =
332
+ (options.exclude || [])
333
+ .concat([
334
+ "/health",
335
+ "/favicon.ico",
336
+ "/_next",
337
+ "/static",
338
+ "/public",
339
+ "/admin",
340
+ ])
341
+ .some(function (p) {
342
+ return path.startsWith(p);
343
+ }) ||
344
+ DEV_ASSET_MARKERS.some(function (m) {
345
+ return path.includes(m);
346
+ }) ||
347
+ STATIC_ASSET_EXT.test(path);
263
348
 
264
349
  const isApiPath = path.startsWith(options.apiPrefix || "/api");
265
350
 
@@ -464,6 +549,29 @@ function attachFastifyInterceptor(fastify, client, options) {
464
549
  const path = request.url ? request.url.split("?")[0] : "";
465
550
  const method = request.method ? request.method.toUpperCase() : "";
466
551
 
552
+ // ── Scan trigger from BotVersion dashboard ───────────────────────────
553
+ if (path === "/__botversion/scan" && method === "POST") {
554
+ const providedKey = request.headers["x-botversion-scan-key"] || "";
555
+ if (
556
+ options.scanSecret &&
557
+ providedKey === options.scanSecret &&
558
+ typeof options.onScanRequested === "function"
559
+ ) {
560
+ try {
561
+ const result = await options.onScanRequested();
562
+ reply.code(200).send({ success: true, result: result || null });
563
+ } catch (err) {
564
+ reply.code(500).send({
565
+ success: false,
566
+ error: String((err && err.message) || err),
567
+ });
568
+ }
569
+ } else {
570
+ reply.code(401).send({ success: false, error: "Unauthorized" });
571
+ }
572
+ return reply;
573
+ }
574
+
467
575
  const shouldIgnore = ignorePaths.some(function (p) {
468
576
  return path.startsWith(p);
469
577
  });
@@ -541,11 +649,37 @@ function attachKoaInterceptor(app, client, options) {
541
649
  // IMPORTANT: This interceptor must be added AFTER koa-bodyparser middleware
542
650
  // so that ctx.request.body is already populated when we read it.
543
651
  app.use(async function botVersionKoaInterceptor(ctx, next) {
544
- await next();
545
-
546
652
  const path = ctx.path || "";
547
653
  const method = ctx.method ? ctx.method.toUpperCase() : "";
548
654
 
655
+ // ── Scan trigger from BotVersion dashboard ───────────────────────────
656
+ if (path === "/__botversion/scan" && method === "POST") {
657
+ const providedKey = ctx.headers["x-botversion-scan-key"] || "";
658
+ if (
659
+ options.scanSecret &&
660
+ providedKey === options.scanSecret &&
661
+ typeof options.onScanRequested === "function"
662
+ ) {
663
+ try {
664
+ const result = await options.onScanRequested();
665
+ ctx.status = 200;
666
+ ctx.body = { success: true, result: result || null };
667
+ } catch (err) {
668
+ ctx.status = 500;
669
+ ctx.body = {
670
+ success: false,
671
+ error: String((err && err.message) || err),
672
+ };
673
+ }
674
+ } else {
675
+ ctx.status = 401;
676
+ ctx.body = { success: false, error: "Unauthorized" };
677
+ }
678
+ return; // handled directly — don't call next()
679
+ }
680
+
681
+ await next();
682
+
549
683
  const shouldIgnore = ignorePaths.some(function (p) {
550
684
  return path.startsWith(p);
551
685
  });
@@ -605,6 +739,38 @@ function attachHapiInterceptor(server, client, options) {
605
739
  const path = request.path || "";
606
740
  const method = request.method ? request.method.toUpperCase() : "";
607
741
 
742
+ // ── Scan trigger from BotVersion dashboard ───────────────────────────
743
+ if (path === "/__botversion/scan" && method === "POST") {
744
+ const providedKey =
745
+ (request.headers && request.headers["x-botversion-scan-key"]) || "";
746
+ if (
747
+ options.scanSecret &&
748
+ providedKey === options.scanSecret &&
749
+ typeof options.onScanRequested === "function"
750
+ ) {
751
+ return Promise.resolve(options.onScanRequested())
752
+ .then(function (result) {
753
+ return h
754
+ .response({ success: true, result: result || null })
755
+ .code(200)
756
+ .takeover();
757
+ })
758
+ .catch(function (err) {
759
+ return h
760
+ .response({
761
+ success: false,
762
+ error: String((err && err.message) || err),
763
+ })
764
+ .code(500)
765
+ .takeover();
766
+ });
767
+ }
768
+ return h
769
+ .response({ success: false, error: "Unauthorized" })
770
+ .code(401)
771
+ .takeover();
772
+ }
773
+
608
774
  const shouldIgnore = ignorePaths.some(function (p) {
609
775
  return path.startsWith(p);
610
776
  });
@@ -659,6 +825,8 @@ function attachNestJsInterceptor(client, options) {
659
825
  exclude: (options || {}).exclude || [],
660
826
  apiPrefix: (options || {}).apiPrefix || "/",
661
827
  debug: (options || {}).debug || false,
828
+ scanSecret: (options || {}).scanSecret,
829
+ onScanRequested: (options || {}).onScanRequested,
662
830
  });
663
831
  }
664
832
 
@@ -671,6 +839,8 @@ function attachSvelteKitInterceptor(client, options) {
671
839
  exclude: (options || {}).exclude || [],
672
840
  apiPrefix: (options || {}).apiPrefix || "/",
673
841
  debug: (options || {}).debug || false,
842
+ scanSecret: (options || {}).scanSecret,
843
+ onScanRequested: (options || {}).onScanRequested,
674
844
  });
675
845
  }
676
846
 
@@ -679,6 +849,8 @@ function attachNuxtInterceptor(client, options) {
679
849
  exclude: (options || {}).exclude || [],
680
850
  apiPrefix: (options || {}).apiPrefix || "/api",
681
851
  debug: (options || {}).debug || false,
852
+ scanSecret: (options || {}).scanSecret,
853
+ onScanRequested: (options || {}).onScanRequested,
682
854
  });
683
855
  }
684
856
 
@@ -687,6 +859,8 @@ function attachRemixInterceptor(client, options) {
687
859
  exclude: (options || {}).exclude || [],
688
860
  apiPrefix: (options || {}).apiPrefix || "/",
689
861
  debug: (options || {}).debug || false,
862
+ scanSecret: (options || {}).scanSecret,
863
+ onScanRequested: (options || {}).onScanRequested,
690
864
  });
691
865
  }
692
866
 
@@ -695,6 +869,8 @@ function attachAdonisInterceptor(client, options) {
695
869
  exclude: (options || {}).exclude || [],
696
870
  apiPrefix: (options || {}).apiPrefix || "/",
697
871
  debug: (options || {}).debug || false,
872
+ scanSecret: (options || {}).scanSecret,
873
+ onScanRequested: (options || {}).onScanRequested,
698
874
  });
699
875
  }
700
876
 
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "botversion-sdk",
3
- "version": "1.1.7",
3
+ "version": "2.1.0",
4
4
  "description": "BotVersion SDK — auto-detect and register your API endpoints",
5
5
  "main": "index.js",
6
6
  "bin": {
7
- "botversion-sdk": "bin/init.js"
7
+ "botversion-scan-frontend-routes": "bin/scan-frontend.js",
8
+ "botversion-scan-endpoints": "bin/scan-backend.js"
8
9
  },
9
10
  "keywords": [
10
11
  "botversion",
@@ -18,6 +19,7 @@
18
19
  "dependencies": {
19
20
  "@babel/parser": "^7.29.3",
20
21
  "@babel/traverse": "^7.29.0",
22
+ "dotenv": "^16.4.5",
21
23
  "express": "^4.18.2"
22
24
  }
23
25
  }
package/scanner.js CHANGED
@@ -1146,112 +1146,13 @@ function findAllFrontendDirs(cwd) {
1146
1146
  }
1147
1147
  }
1148
1148
 
1149
- // ── Step 2: Walk up exactly 1 level to find sibling folders ─────────
1150
- // Handles the case where SDK is installed in backend/ but
1151
- // frontend/ is a sibling folder at the same level
1152
- const parentDir = path.dirname(cwd);
1153
- if (parentDir !== cwd) {
1154
- const UNSAFE_PARENTS = new Set([
1155
- "Desktop",
1156
- "Documents",
1157
- "Downloads",
1158
- "Pictures",
1159
- "Videos",
1160
- "Music",
1161
- "home",
1162
- "users",
1163
- "Users",
1164
- "var",
1165
- "www",
1166
- "srv",
1167
- "opt",
1168
- "tmp",
1169
- "workspace",
1170
- "Workspace",
1171
- "projects",
1172
- "Projects",
1173
- "code",
1174
- "Code",
1175
- "sites",
1176
- "Sites",
1177
- "dev",
1178
- "Dev",
1179
- "work",
1180
- "Work",
1181
- ]);
1182
-
1183
- if (UNSAFE_PARENTS.has(path.basename(parentDir))) {
1184
- if (found.length === 0) found.push(cwd);
1185
- return found;
1186
- }
1187
- // Safety check — only scan siblings if the parent folder
1188
- // looks like a project root (has package.json OR common project folders)
1189
- // This prevents scanning unrelated projects on the Desktop
1190
- const parentHasPackageJson = fs.existsSync(
1191
- path.join(parentDir, "package.json"),
1192
- );
1193
- const parentHasCommonProjectFiles =
1194
- fs.existsSync(path.join(parentDir, "docker-compose.yml")) ||
1195
- fs.existsSync(path.join(parentDir, "docker-compose.yaml")) ||
1196
- fs.existsSync(path.join(parentDir, ".env")) ||
1197
- fs.existsSync(path.join(parentDir, "turbo.json")) ||
1198
- fs.existsSync(path.join(parentDir, "pnpm-workspace.yaml"));
1199
-
1200
- // If parent has no signs of being a project root, skip sibling scanning
1201
- if (!parentHasPackageJson && !parentHasCommonProjectFiles) {
1202
- // skip — parent is probably just a random folder like Desktop
1203
- } else {
1204
- let parentEntries;
1205
- try {
1206
- parentEntries = fs.readdirSync(parentDir);
1207
- } catch {
1208
- parentEntries = [];
1209
- }
1210
-
1211
- for (const entry of parentEntries) {
1212
- if (SKIP_DIRS.has(entry)) continue;
1213
-
1214
- const sibling = path.join(parentDir, entry);
1215
- if (sibling === cwd) continue; // skip cwd itself
1216
-
1217
- let stat;
1218
- try {
1219
- stat = fs.statSync(sibling);
1220
- } catch {
1221
- continue;
1222
- }
1223
- if (!stat.isDirectory()) continue;
1224
-
1225
- // Only add if it actually looks like a frontend project
1226
- if (isFrontendDir(sibling) && !found.includes(sibling)) {
1227
- found.push(sibling);
1228
- }
1229
-
1230
- // Also check one level inside the sibling
1231
- let siblingEntries;
1232
- try {
1233
- siblingEntries = fs.readdirSync(sibling);
1234
- } catch {
1235
- continue;
1236
- }
1237
-
1238
- for (const siblingEntry of siblingEntries) {
1239
- if (SKIP_DIRS.has(siblingEntry)) continue;
1240
- const siblingChild = path.join(sibling, siblingEntry);
1241
- let siblingChildStat;
1242
- try {
1243
- siblingChildStat = fs.statSync(siblingChild);
1244
- } catch {
1245
- continue;
1246
- }
1247
- if (!siblingChildStat.isDirectory()) continue;
1248
- if (isFrontendDir(siblingChild) && !found.includes(siblingChild)) {
1249
- found.push(siblingChild);
1250
- }
1251
- }
1252
- }
1253
- }
1254
- }
1149
+ // Note: we intentionally do NOT walk up to sibling folders (e.g. a
1150
+ // separate frontend/ folder next to backend/) anymore. That only worked
1151
+ // in local dev monorepos where both folders live on the same disk. In
1152
+ // production, frontend and backend are frequently on completely separate
1153
+ // servers, so there is nothing to find outside cwd. If this install has
1154
+ // no frontend, classification (in index.js) reports that to the
1155
+ // dashboard instead of guessing at a folder that may not exist here.
1255
1156
 
1256
1157
  // If nothing found, fall back to cwd
1257
1158
  if (found.length === 0) {