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/bin/scan-backend.js +75 -0
- package/bin/scan-frontend.js +66 -0
- package/client.js +13 -7
- package/index.js +152 -24
- package/interceptor.js +190 -14
- package/package.json +4 -2
- package/scanner.js +7 -106
- package/bin/init.js +0 -605
- package/cli/detector.js +0 -1149
- package/cli/generator.js +0 -187
- package/cli/prompts.js +0 -163
- package/cli/writer.js +0 -511
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
"
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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": "
|
|
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-
|
|
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
|
-
//
|
|
1150
|
-
//
|
|
1151
|
-
//
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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) {
|