botversion-sdk 1.1.6 → 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.
- package/bin/scan-frontend.js +49 -0
- package/client.js +12 -6
- package/index.js +150 -23
- package/interceptor.js +332 -86
- package/package.json +2 -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
|
@@ -3,6 +3,29 @@
|
|
|
3
3
|
|
|
4
4
|
const reportedEndpoints = new Set();
|
|
5
5
|
|
|
6
|
+
function structureToJsonSchema(bodyStructure) {
|
|
7
|
+
if (!bodyStructure) return null;
|
|
8
|
+
return {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: Object.fromEntries(
|
|
11
|
+
Object.entries(bodyStructure).map(function ([key, typeOrObj]) {
|
|
12
|
+
if (typeOrObj && typeof typeOrObj === "object" && typeOrObj.type) {
|
|
13
|
+
return [key, typeOrObj];
|
|
14
|
+
}
|
|
15
|
+
return [
|
|
16
|
+
key,
|
|
17
|
+
{
|
|
18
|
+
type:
|
|
19
|
+
typeOrObj === "null" || typeOrObj === "[redacted]"
|
|
20
|
+
? "string"
|
|
21
|
+
: typeOrObj,
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
}),
|
|
25
|
+
),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
6
29
|
/**
|
|
7
30
|
* Attaches a middleware to the Express app that
|
|
8
31
|
* silently intercepts every request and reports
|
|
@@ -23,6 +46,39 @@ function attachInterceptor(app, client, options) {
|
|
|
23
46
|
|
|
24
47
|
app.use(function botVersionInterceptor(req, res, next) {
|
|
25
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
|
+
|
|
26
82
|
const shouldIgnore = ignorePaths.some(function (p) {
|
|
27
83
|
return path.startsWith(p);
|
|
28
84
|
});
|
|
@@ -50,24 +106,7 @@ function attachInterceptor(app, client, options) {
|
|
|
50
106
|
if (!reportedEndpoints.has(bodyKey)) {
|
|
51
107
|
reportedEndpoints.add(bodyKey);
|
|
52
108
|
|
|
53
|
-
const jsonSchema = bodyStructure
|
|
54
|
-
? {
|
|
55
|
-
type: "object",
|
|
56
|
-
properties: Object.fromEntries(
|
|
57
|
-
Object.entries(bodyStructure).map(function ([key, type]) {
|
|
58
|
-
return [
|
|
59
|
-
key,
|
|
60
|
-
{
|
|
61
|
-
type:
|
|
62
|
-
type === "null" || type === "[redacted]"
|
|
63
|
-
? "string"
|
|
64
|
-
: type,
|
|
65
|
-
},
|
|
66
|
-
];
|
|
67
|
-
}),
|
|
68
|
-
),
|
|
69
|
-
}
|
|
70
|
-
: null;
|
|
109
|
+
const jsonSchema = structureToJsonSchema(bodyStructure);
|
|
71
110
|
|
|
72
111
|
// Report async — never block the request
|
|
73
112
|
setImmediate(function () {
|
|
@@ -126,6 +165,25 @@ function normalizePath(path) {
|
|
|
126
165
|
.join("/");
|
|
127
166
|
}
|
|
128
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Unwraps a single (non-batched) tRPC/superjson envelope.
|
|
170
|
+
* tRPC sends single calls as { json: {...realFields...} }
|
|
171
|
+
* or with superjson as { json: {...realFields...}, meta: {...} }
|
|
172
|
+
*/
|
|
173
|
+
function unwrapTrpcJsonEnvelope(obj) {
|
|
174
|
+
if (
|
|
175
|
+
obj &&
|
|
176
|
+
typeof obj === "object" &&
|
|
177
|
+
!Array.isArray(obj) &&
|
|
178
|
+
obj.json &&
|
|
179
|
+
typeof obj.json === "object" &&
|
|
180
|
+
!Array.isArray(obj.json)
|
|
181
|
+
) {
|
|
182
|
+
return obj.json;
|
|
183
|
+
}
|
|
184
|
+
return obj;
|
|
185
|
+
}
|
|
186
|
+
|
|
129
187
|
/**
|
|
130
188
|
* Extract just the structure of a request body
|
|
131
189
|
* (keys and value types — never actual values for security)
|
|
@@ -133,6 +191,37 @@ function normalizePath(path) {
|
|
|
133
191
|
function buildBodyStructure(body) {
|
|
134
192
|
if (!body || typeof body !== "object") return null;
|
|
135
193
|
|
|
194
|
+
// Unwrap tRPC envelope
|
|
195
|
+
// Case A: single tRPC call — { json: { ...realFields... } } (optionally with a "meta" sibling)
|
|
196
|
+
// Case B: batched tRPC call — { "0": { json: {...} }, "1": { json: {...} } } or { "0": {...realFields...} }
|
|
197
|
+
// We detect whichever shape is present and flatten it to the real fields before processing
|
|
198
|
+
let keys = Object.keys(body);
|
|
199
|
+
|
|
200
|
+
const isTrpcEnvelope =
|
|
201
|
+
keys.length > 0 &&
|
|
202
|
+
keys.every(function (k) {
|
|
203
|
+
return (
|
|
204
|
+
/^\d+$/.test(k) &&
|
|
205
|
+
body[k] !== null &&
|
|
206
|
+
typeof body[k] === "object" &&
|
|
207
|
+
!Array.isArray(body[k])
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (isTrpcEnvelope) {
|
|
212
|
+
const unwrapped = {};
|
|
213
|
+
keys.forEach(function (k) {
|
|
214
|
+
// Each batched entry may itself be a { json: {...} } envelope
|
|
215
|
+
Object.assign(unwrapped, unwrapTrpcJsonEnvelope(body[k]));
|
|
216
|
+
});
|
|
217
|
+
body = unwrapped;
|
|
218
|
+
} else {
|
|
219
|
+
// Not a numeric batch — check for the single { json: {...} } shape
|
|
220
|
+
body = unwrapTrpcJsonEnvelope(body);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
keys = Object.keys(body);
|
|
224
|
+
|
|
136
225
|
const structure = {};
|
|
137
226
|
|
|
138
227
|
Object.keys(body).forEach(function (key) {
|
|
@@ -163,6 +252,17 @@ function buildBodyStructure(body) {
|
|
|
163
252
|
structure[key] = "array";
|
|
164
253
|
} else if (val === null) {
|
|
165
254
|
structure[key] = "null";
|
|
255
|
+
} else if (typeof val === "object") {
|
|
256
|
+
const nestedProps = {};
|
|
257
|
+
Object.keys(val).forEach(function (nk) {
|
|
258
|
+
nestedProps[nk] = {
|
|
259
|
+
type: val[nk] === null ? "string" : typeof val[nk],
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
structure[key] =
|
|
263
|
+
Object.keys(nestedProps).length > 0
|
|
264
|
+
? { type: "object", properties: nestedProps }
|
|
265
|
+
: "object";
|
|
166
266
|
} else {
|
|
167
267
|
structure[key] = typeof val;
|
|
168
268
|
}
|
|
@@ -181,6 +281,40 @@ function attachNextJsInterceptor(client, options) {
|
|
|
181
281
|
const path = req.url ? req.url.split("?")[0] : "";
|
|
182
282
|
const method = req.method ? req.method.toUpperCase() : "";
|
|
183
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
|
+
|
|
184
318
|
const shouldIgnore = (options.exclude || [])
|
|
185
319
|
.concat([
|
|
186
320
|
"/health",
|
|
@@ -220,24 +354,91 @@ function attachNextJsInterceptor(client, options) {
|
|
|
220
354
|
// Helper to send body structure to platform
|
|
221
355
|
function reportBody(bodyObj) {
|
|
222
356
|
try {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
357
|
+
// ── tRPC GET input extraction ──────────────────────────────────────
|
|
358
|
+
// tRPC GET requests send their input as a URL-encoded JSON query
|
|
359
|
+
// parameter called "input". The body is always empty for GET requests.
|
|
360
|
+
// We only attempt this extraction when:
|
|
361
|
+
// 1. The body is empty (GET requests have no body)
|
|
362
|
+
// 2. The URL contains an "input" query parameter
|
|
363
|
+
// 3. The path looks like a tRPC endpoint (/trpc/)
|
|
364
|
+
// This prevents accidentally treating regular REST query params
|
|
365
|
+
// (e.g. ?filter=active&limit=10) as a body schema.
|
|
366
|
+
const isTrpcPath = path.includes("/trpc/");
|
|
367
|
+
const hasInputParam = req.url && req.url.includes("input=");
|
|
368
|
+
|
|
369
|
+
if (
|
|
370
|
+
isTrpcPath &&
|
|
371
|
+
hasInputParam &&
|
|
372
|
+
(!bodyObj || Object.keys(bodyObj).length === 0)
|
|
373
|
+
) {
|
|
374
|
+
try {
|
|
375
|
+
// Use URL constructor to safely parse query params
|
|
376
|
+
const urlObj = new URL(req.url, "http://localhost");
|
|
377
|
+
const inputParam = urlObj.searchParams.get("input");
|
|
378
|
+
|
|
379
|
+
if (inputParam) {
|
|
380
|
+
let decoded;
|
|
381
|
+
try {
|
|
382
|
+
decoded = JSON.parse(decodeURIComponent(inputParam));
|
|
383
|
+
} catch (e) {
|
|
384
|
+
// input param exists but is not valid JSON — skip
|
|
385
|
+
decoded = null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (
|
|
389
|
+
decoded &&
|
|
390
|
+
typeof decoded === "object" &&
|
|
391
|
+
!Array.isArray(decoded)
|
|
392
|
+
) {
|
|
393
|
+
const keys = Object.keys(decoded);
|
|
394
|
+
|
|
395
|
+
// Format 1 — batched: { "0": { json: {...} }, "1": { json: {...} } }
|
|
396
|
+
// All keys are numeric strings
|
|
397
|
+
const isBatch =
|
|
398
|
+
keys.length > 0 && keys.every((k) => /^\d+$/.test(k));
|
|
399
|
+
|
|
400
|
+
if (isBatch) {
|
|
401
|
+
// Merge all batch entries into one flat object
|
|
402
|
+
// so we capture the full schema across all procedures
|
|
403
|
+
const merged = {};
|
|
404
|
+
keys.forEach((k) => {
|
|
405
|
+
const entry = decoded[k];
|
|
406
|
+
if (entry && typeof entry === "object") {
|
|
407
|
+
// Unwrap { json: {...} } envelope if present
|
|
408
|
+
const unwrapped =
|
|
409
|
+
entry.json && typeof entry.json === "object"
|
|
410
|
+
? entry.json
|
|
411
|
+
: entry;
|
|
412
|
+
Object.assign(merged, unwrapped);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
if (Object.keys(merged).length > 0) {
|
|
416
|
+
bodyObj = merged;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// Format 2 — single with superjson: { json: {...}, meta: {...} }
|
|
420
|
+
else if (
|
|
421
|
+
decoded.json &&
|
|
422
|
+
typeof decoded.json === "object"
|
|
423
|
+
) {
|
|
424
|
+
bodyObj = decoded.json;
|
|
425
|
+
}
|
|
426
|
+
// Format 3 — already unwrapped: { group: {...}, limit: 10 }
|
|
427
|
+
// Only use this if it doesn't look like a tRPC envelope
|
|
428
|
+
else if (!decoded.meta && !decoded.json) {
|
|
429
|
+
bodyObj = decoded;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
} catch (e) {
|
|
434
|
+
// URL parsing failed — fall through to normal body handling
|
|
435
|
+
}
|
|
240
436
|
}
|
|
437
|
+
|
|
438
|
+
// ── Normal body processing (unchanged) ────────────────────────────
|
|
439
|
+
const parsedBody = structureToJsonSchema(
|
|
440
|
+
buildBodyStructure(bodyObj),
|
|
441
|
+
);
|
|
241
442
|
client
|
|
242
443
|
.updateEndpoint({
|
|
243
444
|
method: method,
|
|
@@ -330,6 +531,29 @@ function attachFastifyInterceptor(fastify, client, options) {
|
|
|
330
531
|
const path = request.url ? request.url.split("?")[0] : "";
|
|
331
532
|
const method = request.method ? request.method.toUpperCase() : "";
|
|
332
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
|
+
|
|
333
557
|
const shouldIgnore = ignorePaths.some(function (p) {
|
|
334
558
|
return path.startsWith(p);
|
|
335
559
|
});
|
|
@@ -373,19 +597,7 @@ function attachFastifyInterceptor(fastify, client, options) {
|
|
|
373
597
|
const bodyStructure = buildBodyStructure(body);
|
|
374
598
|
if (!bodyStructure) return;
|
|
375
599
|
|
|
376
|
-
const jsonSchema =
|
|
377
|
-
type: "object",
|
|
378
|
-
properties: Object.fromEntries(
|
|
379
|
-
Object.entries(bodyStructure).map(function ([key, type]) {
|
|
380
|
-
return [
|
|
381
|
-
key,
|
|
382
|
-
{
|
|
383
|
-
type: type === "null" || type === "[redacted]" ? "string" : type,
|
|
384
|
-
},
|
|
385
|
-
];
|
|
386
|
-
}),
|
|
387
|
-
),
|
|
388
|
-
};
|
|
600
|
+
const jsonSchema = structureToJsonSchema(bodyStructure);
|
|
389
601
|
|
|
390
602
|
setImmediate(function () {
|
|
391
603
|
client
|
|
@@ -419,11 +631,37 @@ function attachKoaInterceptor(app, client, options) {
|
|
|
419
631
|
// IMPORTANT: This interceptor must be added AFTER koa-bodyparser middleware
|
|
420
632
|
// so that ctx.request.body is already populated when we read it.
|
|
421
633
|
app.use(async function botVersionKoaInterceptor(ctx, next) {
|
|
422
|
-
await next();
|
|
423
|
-
|
|
424
634
|
const path = ctx.path || "";
|
|
425
635
|
const method = ctx.method ? ctx.method.toUpperCase() : "";
|
|
426
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
|
+
|
|
427
665
|
const shouldIgnore = ignorePaths.some(function (p) {
|
|
428
666
|
return path.startsWith(p);
|
|
429
667
|
});
|
|
@@ -447,24 +685,7 @@ function attachKoaInterceptor(app, client, options) {
|
|
|
447
685
|
if (!reportedEndpoints.has(bodyKey)) {
|
|
448
686
|
reportedEndpoints.add(bodyKey);
|
|
449
687
|
|
|
450
|
-
const jsonSchema = bodyStructure
|
|
451
|
-
? {
|
|
452
|
-
type: "object",
|
|
453
|
-
properties: Object.fromEntries(
|
|
454
|
-
Object.entries(bodyStructure).map(function ([key, type]) {
|
|
455
|
-
return [
|
|
456
|
-
key,
|
|
457
|
-
{
|
|
458
|
-
type:
|
|
459
|
-
type === "null" || type === "[redacted]"
|
|
460
|
-
? "string"
|
|
461
|
-
: type,
|
|
462
|
-
},
|
|
463
|
-
];
|
|
464
|
-
}),
|
|
465
|
-
),
|
|
466
|
-
}
|
|
467
|
-
: null;
|
|
688
|
+
const jsonSchema = structureToJsonSchema(bodyStructure);
|
|
468
689
|
|
|
469
690
|
setImmediate(function () {
|
|
470
691
|
client
|
|
@@ -500,6 +721,38 @@ function attachHapiInterceptor(server, client, options) {
|
|
|
500
721
|
const path = request.path || "";
|
|
501
722
|
const method = request.method ? request.method.toUpperCase() : "";
|
|
502
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
|
+
|
|
503
756
|
const shouldIgnore = ignorePaths.some(function (p) {
|
|
504
757
|
return path.startsWith(p);
|
|
505
758
|
});
|
|
@@ -524,24 +777,7 @@ function attachHapiInterceptor(server, client, options) {
|
|
|
524
777
|
if (!reportedEndpoints.has(bodyKey)) {
|
|
525
778
|
reportedEndpoints.add(bodyKey);
|
|
526
779
|
|
|
527
|
-
const jsonSchema = bodyStructure
|
|
528
|
-
? {
|
|
529
|
-
type: "object",
|
|
530
|
-
properties: Object.fromEntries(
|
|
531
|
-
Object.entries(bodyStructure).map(function ([key, type]) {
|
|
532
|
-
return [
|
|
533
|
-
key,
|
|
534
|
-
{
|
|
535
|
-
type:
|
|
536
|
-
type === "null" || type === "[redacted]"
|
|
537
|
-
? "string"
|
|
538
|
-
: type,
|
|
539
|
-
},
|
|
540
|
-
];
|
|
541
|
-
}),
|
|
542
|
-
),
|
|
543
|
-
}
|
|
544
|
-
: null;
|
|
780
|
+
const jsonSchema = structureToJsonSchema(bodyStructure);
|
|
545
781
|
|
|
546
782
|
setImmediate(function () {
|
|
547
783
|
client
|
|
@@ -571,6 +807,8 @@ function attachNestJsInterceptor(client, options) {
|
|
|
571
807
|
exclude: (options || {}).exclude || [],
|
|
572
808
|
apiPrefix: (options || {}).apiPrefix || "/",
|
|
573
809
|
debug: (options || {}).debug || false,
|
|
810
|
+
scanSecret: (options || {}).scanSecret,
|
|
811
|
+
onScanRequested: (options || {}).onScanRequested,
|
|
574
812
|
});
|
|
575
813
|
}
|
|
576
814
|
|
|
@@ -583,6 +821,8 @@ function attachSvelteKitInterceptor(client, options) {
|
|
|
583
821
|
exclude: (options || {}).exclude || [],
|
|
584
822
|
apiPrefix: (options || {}).apiPrefix || "/",
|
|
585
823
|
debug: (options || {}).debug || false,
|
|
824
|
+
scanSecret: (options || {}).scanSecret,
|
|
825
|
+
onScanRequested: (options || {}).onScanRequested,
|
|
586
826
|
});
|
|
587
827
|
}
|
|
588
828
|
|
|
@@ -591,6 +831,8 @@ function attachNuxtInterceptor(client, options) {
|
|
|
591
831
|
exclude: (options || {}).exclude || [],
|
|
592
832
|
apiPrefix: (options || {}).apiPrefix || "/api",
|
|
593
833
|
debug: (options || {}).debug || false,
|
|
834
|
+
scanSecret: (options || {}).scanSecret,
|
|
835
|
+
onScanRequested: (options || {}).onScanRequested,
|
|
594
836
|
});
|
|
595
837
|
}
|
|
596
838
|
|
|
@@ -599,6 +841,8 @@ function attachRemixInterceptor(client, options) {
|
|
|
599
841
|
exclude: (options || {}).exclude || [],
|
|
600
842
|
apiPrefix: (options || {}).apiPrefix || "/",
|
|
601
843
|
debug: (options || {}).debug || false,
|
|
844
|
+
scanSecret: (options || {}).scanSecret,
|
|
845
|
+
onScanRequested: (options || {}).onScanRequested,
|
|
602
846
|
});
|
|
603
847
|
}
|
|
604
848
|
|
|
@@ -607,6 +851,8 @@ function attachAdonisInterceptor(client, options) {
|
|
|
607
851
|
exclude: (options || {}).exclude || [],
|
|
608
852
|
apiPrefix: (options || {}).apiPrefix || "/",
|
|
609
853
|
debug: (options || {}).debug || false,
|
|
854
|
+
scanSecret: (options || {}).scanSecret,
|
|
855
|
+
onScanRequested: (options || {}).onScanRequested,
|
|
610
856
|
});
|
|
611
857
|
}
|
|
612
858
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "botversion-sdk",
|
|
3
|
-
"version": "
|
|
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-
|
|
7
|
+
"botversion-scan-frontend": "./bin/scan-frontend.js"
|
|
8
8
|
},
|
|
9
9
|
"keywords": [
|
|
10
10
|
"botversion",
|
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) {
|