botversion-sdk 1.1.7 → 2.0.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.
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ var scanner = require("../scanner");
5
+ var BotVersionClient = require("../client");
6
+ var sdk = require("../index");
7
+
8
+ var apiKey = process.env.BOTVERSION_API_KEY;
9
+ var platformUrl =
10
+ process.env.BOTVERSION_PLATFORM_URL || "https://botversion.com";
11
+
12
+ if (!apiKey) {
13
+ console.error(
14
+ "[BotVersion] BOTVERSION_API_KEY environment variable is not set. Skipping frontend route scan.",
15
+ );
16
+ process.exit(0);
17
+ }
18
+
19
+ var cwd = process.cwd();
20
+ var client = new BotVersionClient({ apiKey: apiKey, platformUrl: platformUrl });
21
+
22
+ var detected = sdk.detectFrameworks(cwd);
23
+ var projectType = sdk.classifyInstallation(detected.backend, detected.frontend);
24
+ var routePatterns = scanner.scanFrontendRoutes(cwd);
25
+
26
+ client
27
+ .registerRoutePatterns(routePatterns, {
28
+ source: "cli",
29
+ projectType: projectType,
30
+ detectedBackend: detected.backend,
31
+ detectedFrontend: detected.frontend,
32
+ sdkLanguage: "javascript",
33
+ })
34
+ .then(function () {
35
+ console.log(
36
+ "[BotVersion] Reported " +
37
+ routePatterns.length +
38
+ " frontend routes. projectType=" +
39
+ projectType,
40
+ );
41
+ process.exit(0);
42
+ })
43
+ .catch(function (err) {
44
+ console.error(
45
+ "[BotVersion] Failed to report frontend routes:",
46
+ err.message,
47
+ );
48
+ process.exit(0);
49
+ });
package/client.js CHANGED
@@ -99,14 +99,20 @@ BotVersionClient.prototype.updateEndpoint = function (endpoint) {
99
99
  });
100
100
  };
101
101
 
102
- BotVersionClient.prototype.registerRoutePatterns = function (patterns) {
102
+ BotVersionClient.prototype.registerRoutePatterns = function (
103
+ patterns,
104
+ classification,
105
+ ) {
103
106
  var self = this;
104
- if (!patterns || patterns.length === 0) return Promise.resolve();
105
-
106
- return self._post("/api/sdk/register-route-patterns", {
107
+ var body = {
107
108
  workspaceKey: self.apiKey,
108
- patterns: patterns,
109
- });
109
+ patterns: patterns || [],
110
+ classification: classification || null,
111
+ };
112
+ if ((!patterns || patterns.length === 0) && !classification) {
113
+ return Promise.resolve();
114
+ }
115
+ return self._post("/api/sdk/register-route-patterns", body);
110
116
  };
111
117
 
112
118
  /**
package/index.js CHANGED
@@ -161,6 +161,91 @@ function runBackendScanner(detectedBackend, app, options, cwd) {
161
161
  return endpoints;
162
162
  }
163
163
 
164
+ var FULLSTACK_FRAMEWORKS = ["nextjs", "nuxt", "sveltekit", "remix"];
165
+ var BACKEND_ONLY_FRAMEWORKS = [
166
+ "express",
167
+ "fastify",
168
+ "nestjs",
169
+ "koa",
170
+ "hapi",
171
+ "adonis",
172
+ ];
173
+
174
+ function classifyInstallation(detectedBackend, detectedFrontend) {
175
+ if (FULLSTACK_FRAMEWORKS.indexOf(detectedBackend) !== -1) {
176
+ return "fullstack";
177
+ }
178
+ if (
179
+ BACKEND_ONLY_FRAMEWORKS.indexOf(detectedBackend) !== -1 &&
180
+ detectedFrontend === "unknown"
181
+ ) {
182
+ return "backend-only";
183
+ }
184
+ if (detectedBackend === "unknown" && detectedFrontend !== "unknown") {
185
+ return "frontend-only";
186
+ }
187
+ if (detectedBackend === "unknown" && detectedFrontend === "unknown") {
188
+ return "unknown";
189
+ }
190
+ return "mixed";
191
+ }
192
+
193
+ // ── Run a full scan (backend endpoints + frontend routes) and report ────────
194
+ // This used to run automatically 3 seconds after server start. Now it only
195
+ // runs when the BotVersion dashboard's "Scan" button sends a request to this
196
+ // SDK's internal scan-trigger endpoint (see interceptor.js). This fixes scans
197
+ // not happening on serverless redeploys (Vercel, etc.) since there's no
198
+ // "restart" event to hook into there.
199
+ function runFullScan(
200
+ detectedBackend,
201
+ detectedFrontend,
202
+ app,
203
+ options,
204
+ cwd,
205
+ client,
206
+ ) {
207
+ var projectType = classifyInstallation(detectedBackend, detectedFrontend);
208
+
209
+ var result = {
210
+ endpointCount: 0,
211
+ routeCount: 0,
212
+ projectType: projectType,
213
+ detectedBackend: detectedBackend,
214
+ detectedFrontend: detectedFrontend,
215
+ sdkLanguage: "javascript",
216
+ };
217
+
218
+ var shouldScanBackend =
219
+ projectType === "fullstack" ||
220
+ projectType === "backend-only" ||
221
+ projectType === "mixed";
222
+
223
+ var shouldScanFrontend =
224
+ projectType === "fullstack" || projectType === "frontend-only";
225
+
226
+ if (shouldScanBackend) {
227
+ var endpoints = runBackendScanner(detectedBackend, app, options, cwd);
228
+ result.endpointCount = endpoints.length;
229
+
230
+ if (endpoints.length > 0) {
231
+ client.registerEndpoints(endpoints);
232
+ }
233
+ }
234
+
235
+ if (shouldScanFrontend) {
236
+ var routePatterns = scanner.scanFrontendRoutes(cwd);
237
+ result.routeCount = routePatterns.length;
238
+
239
+ if (routePatterns.length > 0) {
240
+ client.registerRoutePatterns(routePatterns).catch(function () {
241
+ // silently ignore — non-critical
242
+ });
243
+ }
244
+ }
245
+
246
+ return result;
247
+ }
248
+
164
249
  // ── Attach the right runtime interceptor ─────────────────────────────────────
165
250
  function attachRuntimeInterceptor(detectedBackend, app, client, options) {
166
251
  switch (detectedBackend) {
@@ -169,6 +254,8 @@ function attachRuntimeInterceptor(detectedBackend, app, client, options) {
169
254
  exclude: options.exclude || [],
170
255
  apiPrefix: options.apiPrefix || null,
171
256
  debug: options.debug || false,
257
+ scanSecret: options.scanSecret,
258
+ onScanRequested: options.onScanRequested,
172
259
  });
173
260
  break;
174
261
 
@@ -177,6 +264,8 @@ function attachRuntimeInterceptor(detectedBackend, app, client, options) {
177
264
  exclude: options.exclude || [],
178
265
  apiPrefix: options.apiPrefix || "/api",
179
266
  debug: options.debug || false,
267
+ scanSecret: options.scanSecret,
268
+ onScanRequested: options.onScanRequested,
180
269
  });
181
270
  break;
182
271
 
@@ -186,6 +275,8 @@ function attachRuntimeInterceptor(detectedBackend, app, client, options) {
186
275
  exclude: options.exclude || [],
187
276
  apiPrefix: options.apiPrefix || null,
188
277
  debug: options.debug || false,
278
+ scanSecret: options.scanSecret,
279
+ onScanRequested: options.onScanRequested,
189
280
  });
190
281
  }
191
282
  break;
@@ -196,6 +287,8 @@ function attachRuntimeInterceptor(detectedBackend, app, client, options) {
196
287
  exclude: options.exclude || [],
197
288
  apiPrefix: options.apiPrefix || null,
198
289
  debug: options.debug || false,
290
+ scanSecret: options.scanSecret,
291
+ onScanRequested: options.onScanRequested,
199
292
  });
200
293
  }
201
294
  break;
@@ -206,6 +299,8 @@ function attachRuntimeInterceptor(detectedBackend, app, client, options) {
206
299
  exclude: options.exclude || [],
207
300
  apiPrefix: options.apiPrefix || null,
208
301
  debug: options.debug || false,
302
+ scanSecret: options.scanSecret,
303
+ onScanRequested: options.onScanRequested,
209
304
  });
210
305
  }
211
306
  break;
@@ -215,6 +310,8 @@ function attachRuntimeInterceptor(detectedBackend, app, client, options) {
215
310
  exclude: options.exclude || [],
216
311
  apiPrefix: options.apiPrefix || "/",
217
312
  debug: options.debug || false,
313
+ scanSecret: options.scanSecret,
314
+ onScanRequested: options.onScanRequested,
218
315
  });
219
316
  break;
220
317
 
@@ -223,6 +320,8 @@ function attachRuntimeInterceptor(detectedBackend, app, client, options) {
223
320
  exclude: options.exclude || [],
224
321
  apiPrefix: options.apiPrefix || "/",
225
322
  debug: options.debug || false,
323
+ scanSecret: options.scanSecret,
324
+ onScanRequested: options.onScanRequested,
226
325
  });
227
326
  break;
228
327
 
@@ -231,6 +330,8 @@ function attachRuntimeInterceptor(detectedBackend, app, client, options) {
231
330
  exclude: options.exclude || [],
232
331
  apiPrefix: options.apiPrefix || "/api",
233
332
  debug: options.debug || false,
333
+ scanSecret: options.scanSecret,
334
+ onScanRequested: options.onScanRequested,
234
335
  });
235
336
  break;
236
337
 
@@ -239,6 +340,8 @@ function attachRuntimeInterceptor(detectedBackend, app, client, options) {
239
340
  exclude: options.exclude || [],
240
341
  apiPrefix: options.apiPrefix || "/",
241
342
  debug: options.debug || false,
343
+ scanSecret: options.scanSecret,
344
+ onScanRequested: options.onScanRequested,
242
345
  });
243
346
  break;
244
347
 
@@ -247,6 +350,8 @@ function attachRuntimeInterceptor(detectedBackend, app, client, options) {
247
350
  exclude: options.exclude || [],
248
351
  apiPrefix: options.apiPrefix || "/",
249
352
  debug: options.debug || false,
353
+ scanSecret: options.scanSecret,
354
+ onScanRequested: options.onScanRequested,
250
355
  });
251
356
  break;
252
357
 
@@ -256,6 +361,8 @@ function attachRuntimeInterceptor(detectedBackend, app, client, options) {
256
361
  exclude: options.exclude || [],
257
362
  apiPrefix: options.apiPrefix || "/",
258
363
  debug: options.debug || false,
364
+ scanSecret: options.scanSecret,
365
+ onScanRequested: options.onScanRequested,
259
366
  });
260
367
  break;
261
368
  }
@@ -289,7 +396,22 @@ var BotVersion = {
289
396
  this._initialized = true;
290
397
  const savedOptions = global._botVersionOptions || {};
291
398
  const savedFramework = global._botVersionFramework || "nextjs";
292
- attachRuntimeInterceptor(savedFramework, app, this._client, savedOptions);
399
+ const savedFrontend = global._botVersionFrontendFramework || "unknown";
400
+ const savedProjectType = classifyInstallation(
401
+ savedFramework,
402
+ savedFrontend,
403
+ );
404
+ if (
405
+ savedProjectType !== "frontend-only" &&
406
+ savedProjectType !== "unknown"
407
+ ) {
408
+ attachRuntimeInterceptor(
409
+ savedFramework,
410
+ app,
411
+ this._client,
412
+ savedOptions,
413
+ );
414
+ }
293
415
  return;
294
416
  }
295
417
 
@@ -319,33 +441,36 @@ var BotVersion = {
319
441
  var detectedFrontend = options.frontendFramework || detected.frontend;
320
442
 
321
443
  global._botVersionFramework = detectedBackend;
444
+ global._botVersionFrontendFramework = detectedFrontend;
445
+
446
+ var projectType = classifyInstallation(detectedBackend, detectedFrontend);
322
447
 
323
448
  if (debug) {
324
- // Detected: detectedBackend, detectedFrontend
449
+ // Detected: detectedBackend, detectedFrontend, projectType
325
450
  }
326
451
 
327
- // ── Attach runtime interceptor ───────────────────────────────────────────
328
- attachRuntimeInterceptor(detectedBackend, app, self._client, options);
329
-
330
- // ── Static scan ──────────────────────────────────────────────────────────
331
- setTimeout(function () {
332
- // Run backend scanner
333
- var endpoints = runBackendScanner(detectedBackend, app, options, cwd);
334
-
335
- // Send endpoints to platform
336
- if (endpoints.length > 0) {
337
- self._client.registerEndpoints(endpoints);
338
- }
339
-
340
- // Run frontend route scanner
341
- var routePatterns = scanner.scanFrontendRoutes(cwd);
452
+ // ── Wire up the on-demand scan trigger ───────────────────────────────────
453
+ // Instead of scanning automatically on restart, the interceptor now
454
+ // listens for a special request from the BotVersion dashboard and runs
455
+ // the scan only when the user clicks "Scan" there.
456
+ options.scanSecret = options.apiKey;
457
+ options.onScanRequested = function () {
458
+ return runFullScan(
459
+ detectedBackend,
460
+ detectedFrontend,
461
+ app,
462
+ options,
463
+ cwd,
464
+ self._client,
465
+ );
466
+ };
342
467
 
343
- if (routePatterns.length > 0) {
344
- self._client.registerRoutePatterns(routePatterns).catch(function () {
345
- // silently ignore — non-critical
346
- });
347
- }
348
- }, 3000);
468
+ // ── Attach runtime interceptor ───────────────────────────────────────────
469
+ // Skip for frontend-only or unknown projects — there's no backend
470
+ // server on this process worth intercepting live traffic on.
471
+ if (projectType !== "frontend-only" && projectType !== "unknown") {
472
+ attachRuntimeInterceptor(detectedBackend, app, self._client, options);
473
+ }
349
474
  },
350
475
 
351
476
  getEndpoints: function () {
@@ -377,3 +502,5 @@ module.exports.default = BotVersion;
377
502
  module.exports.init = BotVersion.init;
378
503
  module.exports.getEndpoints = BotVersion.getEndpoints;
379
504
  module.exports.registerEndpoint = BotVersion.registerEndpoint;
505
+ module.exports.detectFrameworks = detectFrameworks;
506
+ module.exports.classifyInstallation = classifyInstallation;
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,6 +281,40 @@ 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
 
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
+
251
318
  const shouldIgnore = (options.exclude || [])
252
319
  .concat([
253
320
  "/health",
@@ -464,6 +531,29 @@ function attachFastifyInterceptor(fastify, client, options) {
464
531
  const path = request.url ? request.url.split("?")[0] : "";
465
532
  const method = request.method ? request.method.toUpperCase() : "";
466
533
 
534
+ // ── Scan trigger from BotVersion dashboard ───────────────────────────
535
+ if (path === "/__botversion/scan" && method === "POST") {
536
+ const providedKey = request.headers["x-botversion-scan-key"] || "";
537
+ if (
538
+ options.scanSecret &&
539
+ providedKey === options.scanSecret &&
540
+ typeof options.onScanRequested === "function"
541
+ ) {
542
+ try {
543
+ const result = await options.onScanRequested();
544
+ reply.code(200).send({ success: true, result: result || null });
545
+ } catch (err) {
546
+ reply.code(500).send({
547
+ success: false,
548
+ error: String((err && err.message) || err),
549
+ });
550
+ }
551
+ } else {
552
+ reply.code(401).send({ success: false, error: "Unauthorized" });
553
+ }
554
+ return reply;
555
+ }
556
+
467
557
  const shouldIgnore = ignorePaths.some(function (p) {
468
558
  return path.startsWith(p);
469
559
  });
@@ -541,11 +631,37 @@ function attachKoaInterceptor(app, client, options) {
541
631
  // IMPORTANT: This interceptor must be added AFTER koa-bodyparser middleware
542
632
  // so that ctx.request.body is already populated when we read it.
543
633
  app.use(async function botVersionKoaInterceptor(ctx, next) {
544
- await next();
545
-
546
634
  const path = ctx.path || "";
547
635
  const method = ctx.method ? ctx.method.toUpperCase() : "";
548
636
 
637
+ // ── Scan trigger from BotVersion dashboard ───────────────────────────
638
+ if (path === "/__botversion/scan" && method === "POST") {
639
+ const providedKey = ctx.headers["x-botversion-scan-key"] || "";
640
+ if (
641
+ options.scanSecret &&
642
+ providedKey === options.scanSecret &&
643
+ typeof options.onScanRequested === "function"
644
+ ) {
645
+ try {
646
+ const result = await options.onScanRequested();
647
+ ctx.status = 200;
648
+ ctx.body = { success: true, result: result || null };
649
+ } catch (err) {
650
+ ctx.status = 500;
651
+ ctx.body = {
652
+ success: false,
653
+ error: String((err && err.message) || err),
654
+ };
655
+ }
656
+ } else {
657
+ ctx.status = 401;
658
+ ctx.body = { success: false, error: "Unauthorized" };
659
+ }
660
+ return; // handled directly — don't call next()
661
+ }
662
+
663
+ await next();
664
+
549
665
  const shouldIgnore = ignorePaths.some(function (p) {
550
666
  return path.startsWith(p);
551
667
  });
@@ -605,6 +721,38 @@ function attachHapiInterceptor(server, client, options) {
605
721
  const path = request.path || "";
606
722
  const method = request.method ? request.method.toUpperCase() : "";
607
723
 
724
+ // ── Scan trigger from BotVersion dashboard ───────────────────────────
725
+ if (path === "/__botversion/scan" && method === "POST") {
726
+ const providedKey =
727
+ (request.headers && request.headers["x-botversion-scan-key"]) || "";
728
+ if (
729
+ options.scanSecret &&
730
+ providedKey === options.scanSecret &&
731
+ typeof options.onScanRequested === "function"
732
+ ) {
733
+ return Promise.resolve(options.onScanRequested())
734
+ .then(function (result) {
735
+ return h
736
+ .response({ success: true, result: result || null })
737
+ .code(200)
738
+ .takeover();
739
+ })
740
+ .catch(function (err) {
741
+ return h
742
+ .response({
743
+ success: false,
744
+ error: String((err && err.message) || err),
745
+ })
746
+ .code(500)
747
+ .takeover();
748
+ });
749
+ }
750
+ return h
751
+ .response({ success: false, error: "Unauthorized" })
752
+ .code(401)
753
+ .takeover();
754
+ }
755
+
608
756
  const shouldIgnore = ignorePaths.some(function (p) {
609
757
  return path.startsWith(p);
610
758
  });
@@ -659,6 +807,8 @@ function attachNestJsInterceptor(client, options) {
659
807
  exclude: (options || {}).exclude || [],
660
808
  apiPrefix: (options || {}).apiPrefix || "/",
661
809
  debug: (options || {}).debug || false,
810
+ scanSecret: (options || {}).scanSecret,
811
+ onScanRequested: (options || {}).onScanRequested,
662
812
  });
663
813
  }
664
814
 
@@ -671,6 +821,8 @@ function attachSvelteKitInterceptor(client, options) {
671
821
  exclude: (options || {}).exclude || [],
672
822
  apiPrefix: (options || {}).apiPrefix || "/",
673
823
  debug: (options || {}).debug || false,
824
+ scanSecret: (options || {}).scanSecret,
825
+ onScanRequested: (options || {}).onScanRequested,
674
826
  });
675
827
  }
676
828
 
@@ -679,6 +831,8 @@ function attachNuxtInterceptor(client, options) {
679
831
  exclude: (options || {}).exclude || [],
680
832
  apiPrefix: (options || {}).apiPrefix || "/api",
681
833
  debug: (options || {}).debug || false,
834
+ scanSecret: (options || {}).scanSecret,
835
+ onScanRequested: (options || {}).onScanRequested,
682
836
  });
683
837
  }
684
838
 
@@ -687,6 +841,8 @@ function attachRemixInterceptor(client, options) {
687
841
  exclude: (options || {}).exclude || [],
688
842
  apiPrefix: (options || {}).apiPrefix || "/",
689
843
  debug: (options || {}).debug || false,
844
+ scanSecret: (options || {}).scanSecret,
845
+ onScanRequested: (options || {}).onScanRequested,
690
846
  });
691
847
  }
692
848
 
@@ -695,6 +851,8 @@ function attachAdonisInterceptor(client, options) {
695
851
  exclude: (options || {}).exclude || [],
696
852
  apiPrefix: (options || {}).apiPrefix || "/",
697
853
  debug: (options || {}).debug || false,
854
+ scanSecret: (options || {}).scanSecret,
855
+ onScanRequested: (options || {}).onScanRequested,
698
856
  });
699
857
  }
700
858
 
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "botversion-sdk",
3
- "version": "1.1.7",
3
+ "version": "2.0.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": "./bin/scan-frontend.js"
8
8
  },
9
9
  "keywords": [
10
10
  "botversion",