botversion-sdk 1.0.6 → 1.1.6
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/client.js +32 -4
- package/index.js +285 -68
- package/interceptor.js +411 -15
- package/package.json +1 -1
- package/scanner.js +1279 -38
package/interceptor.js
CHANGED
|
@@ -18,6 +18,7 @@ function attachInterceptor(app, client, options) {
|
|
|
18
18
|
"/_next",
|
|
19
19
|
"/static",
|
|
20
20
|
"/public",
|
|
21
|
+
"/admin",
|
|
21
22
|
].concat(options.exclude || []);
|
|
22
23
|
|
|
23
24
|
app.use(function botVersionInterceptor(req, res, next) {
|
|
@@ -77,9 +78,7 @@ function attachInterceptor(app, client, options) {
|
|
|
77
78
|
requestBody: jsonSchema,
|
|
78
79
|
detectedBy: "runtime",
|
|
79
80
|
})
|
|
80
|
-
.catch(function (err) {
|
|
81
|
-
console.error("[botversion] update_endpoint failed:", err.message);
|
|
82
|
-
});
|
|
81
|
+
.catch(function (err) {});
|
|
83
82
|
});
|
|
84
83
|
}
|
|
85
84
|
|
|
@@ -183,7 +182,14 @@ function attachNextJsInterceptor(client, options) {
|
|
|
183
182
|
const method = req.method ? req.method.toUpperCase() : "";
|
|
184
183
|
|
|
185
184
|
const shouldIgnore = (options.exclude || [])
|
|
186
|
-
.concat([
|
|
185
|
+
.concat([
|
|
186
|
+
"/health",
|
|
187
|
+
"/favicon.ico",
|
|
188
|
+
"/_next",
|
|
189
|
+
"/static",
|
|
190
|
+
"/public",
|
|
191
|
+
"/admin",
|
|
192
|
+
])
|
|
187
193
|
.some(function (p) {
|
|
188
194
|
return path.startsWith(p);
|
|
189
195
|
});
|
|
@@ -197,34 +203,424 @@ function attachNextJsInterceptor(client, options) {
|
|
|
197
203
|
if (!reportedEndpoints.has(endpointKey)) {
|
|
198
204
|
reportedEndpoints.add(endpointKey);
|
|
199
205
|
|
|
200
|
-
|
|
206
|
+
// Skip file uploads — too large and not JSON
|
|
207
|
+
const contentType = req.headers["content-type"] || "";
|
|
208
|
+
if (contentType.includes("multipart/form-data")) {
|
|
201
209
|
client
|
|
202
210
|
.updateEndpoint({
|
|
203
211
|
method: method,
|
|
204
212
|
path: normalizedPath,
|
|
213
|
+
requestBody: null,
|
|
205
214
|
detectedBy: "runtime",
|
|
206
215
|
})
|
|
207
|
-
.catch(function (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
216
|
+
.catch(function () {});
|
|
217
|
+
return originalEmit.apply(this, arguments);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Helper to send body structure to platform
|
|
221
|
+
function reportBody(bodyObj) {
|
|
222
|
+
try {
|
|
223
|
+
let parsedBody = null;
|
|
224
|
+
const structure = buildBodyStructure(bodyObj);
|
|
225
|
+
if (structure) {
|
|
226
|
+
parsedBody = {
|
|
227
|
+
type: "object",
|
|
228
|
+
properties: Object.fromEntries(
|
|
229
|
+
Object.entries(structure).map(([key, type]) => [
|
|
230
|
+
key,
|
|
231
|
+
{
|
|
232
|
+
type:
|
|
233
|
+
type === "null" || type === "[redacted]"
|
|
234
|
+
? "string"
|
|
235
|
+
: type,
|
|
236
|
+
},
|
|
237
|
+
]),
|
|
238
|
+
),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
client
|
|
242
|
+
.updateEndpoint({
|
|
243
|
+
method: method,
|
|
244
|
+
path: normalizedPath,
|
|
245
|
+
requestBody: parsedBody,
|
|
246
|
+
detectedBy: "runtime",
|
|
247
|
+
})
|
|
248
|
+
.catch(function () {});
|
|
249
|
+
} catch (e) {}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// APP ROUTER — req.body is a Web Stream, read it early
|
|
253
|
+
// and put it back as a new stream so App Router can still read it
|
|
254
|
+
if (
|
|
255
|
+
typeof req.body !== "undefined" &&
|
|
256
|
+
req.body &&
|
|
257
|
+
typeof req.body.getReader === "function"
|
|
258
|
+
) {
|
|
259
|
+
try {
|
|
260
|
+
const reader = req.body.getReader();
|
|
261
|
+
const chunks = [];
|
|
262
|
+
function readChunk() {
|
|
263
|
+
reader
|
|
264
|
+
.read()
|
|
265
|
+
.then(function (result) {
|
|
266
|
+
if (result.done) {
|
|
267
|
+
const fullBuffer = Buffer.concat(
|
|
268
|
+
chunks.map(function (c) {
|
|
269
|
+
return Buffer.from(c);
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
272
|
+
// Put the body back as a new ReadableStream
|
|
273
|
+
const { ReadableStream } = require("stream/web");
|
|
274
|
+
req.body = new ReadableStream({
|
|
275
|
+
start: function (controller) {
|
|
276
|
+
controller.enqueue(fullBuffer);
|
|
277
|
+
controller.close();
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
// Parse and report
|
|
281
|
+
try {
|
|
282
|
+
const parsed = JSON.parse(fullBuffer.toString());
|
|
283
|
+
reportBody(parsed);
|
|
284
|
+
} catch (e) {}
|
|
285
|
+
} else {
|
|
286
|
+
chunks.push(result.value);
|
|
287
|
+
readChunk();
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
.catch(function () {});
|
|
291
|
+
}
|
|
292
|
+
readChunk();
|
|
293
|
+
} catch (e) {}
|
|
294
|
+
return originalEmit.apply(this, arguments);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// PAGES ROUTER — req.body is populated by Next.js after parsing
|
|
298
|
+
// We intercept res.end because by then req.body is fully available
|
|
299
|
+
const originalResEnd = res.end.bind(res);
|
|
300
|
+
res.end = function (chunk, encoding, callback) {
|
|
301
|
+
reportBody(req.body || null);
|
|
302
|
+
return originalResEnd(chunk, encoding, callback);
|
|
303
|
+
};
|
|
211
304
|
}
|
|
212
305
|
}
|
|
213
306
|
}
|
|
214
307
|
|
|
215
308
|
return originalEmit.apply(this, arguments);
|
|
216
309
|
};
|
|
217
|
-
} catch (err) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
310
|
+
} catch (err) {}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Attaches a Fastify hook that silently intercepts every request
|
|
315
|
+
* and reports new endpoints to BotVersion platform.
|
|
316
|
+
*/
|
|
317
|
+
function attachFastifyInterceptor(fastify, client, options) {
|
|
318
|
+
options = options || {};
|
|
319
|
+
|
|
320
|
+
const ignorePaths = [
|
|
321
|
+
"/health",
|
|
322
|
+
"/favicon.ico",
|
|
323
|
+
"/_next",
|
|
324
|
+
"/static",
|
|
325
|
+
"/public",
|
|
326
|
+
"/admin",
|
|
327
|
+
].concat(options.exclude || []);
|
|
328
|
+
|
|
329
|
+
fastify.addHook("onRequest", async function (request, reply) {
|
|
330
|
+
const path = request.url ? request.url.split("?")[0] : "";
|
|
331
|
+
const method = request.method ? request.method.toUpperCase() : "";
|
|
332
|
+
|
|
333
|
+
const shouldIgnore = ignorePaths.some(function (p) {
|
|
334
|
+
return path.startsWith(p);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
if (shouldIgnore) return;
|
|
338
|
+
|
|
339
|
+
if (options.apiPrefix && !path.startsWith(options.apiPrefix)) return;
|
|
340
|
+
|
|
341
|
+
const normalizedPath = normalizePath(path);
|
|
342
|
+
const endpointKey = method + ":" + normalizedPath;
|
|
343
|
+
|
|
344
|
+
if (!reportedEndpoints.has(endpointKey)) {
|
|
345
|
+
reportedEndpoints.add(endpointKey);
|
|
346
|
+
|
|
347
|
+
setImmediate(function () {
|
|
348
|
+
client
|
|
349
|
+
.updateEndpoint({
|
|
350
|
+
method,
|
|
351
|
+
path: normalizedPath,
|
|
352
|
+
requestBody: null,
|
|
353
|
+
detectedBy: "runtime-fastify",
|
|
354
|
+
})
|
|
355
|
+
.catch(function () {});
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Use onSend hook to capture body after parsing
|
|
361
|
+
fastify.addHook("preHandler", async function (request, reply) {
|
|
362
|
+
const path = request.url ? request.url.split("?")[0] : "";
|
|
363
|
+
const method = request.method ? request.method.toUpperCase() : "";
|
|
364
|
+
const normalizedPath = normalizePath(path);
|
|
365
|
+
const bodyKey = method + ":" + normalizedPath + ":body";
|
|
366
|
+
|
|
367
|
+
if (reportedEndpoints.has(bodyKey)) return;
|
|
368
|
+
reportedEndpoints.add(bodyKey);
|
|
369
|
+
|
|
370
|
+
const body = request.body;
|
|
371
|
+
if (!body) return;
|
|
372
|
+
|
|
373
|
+
const bodyStructure = buildBodyStructure(body);
|
|
374
|
+
if (!bodyStructure) return;
|
|
375
|
+
|
|
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
|
+
};
|
|
389
|
+
|
|
390
|
+
setImmediate(function () {
|
|
391
|
+
client
|
|
392
|
+
.updateEndpoint({
|
|
393
|
+
method,
|
|
394
|
+
path: normalizedPath,
|
|
395
|
+
requestBody: jsonSchema,
|
|
396
|
+
detectedBy: "runtime-fastify",
|
|
397
|
+
})
|
|
398
|
+
.catch(function () {});
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Attaches a Koa middleware that silently intercepts every request
|
|
405
|
+
* and reports new endpoints to BotVersion platform.
|
|
406
|
+
*/
|
|
407
|
+
function attachKoaInterceptor(app, client, options) {
|
|
408
|
+
options = options || {};
|
|
409
|
+
|
|
410
|
+
const ignorePaths = [
|
|
411
|
+
"/health",
|
|
412
|
+
"/favicon.ico",
|
|
413
|
+
"/_next",
|
|
414
|
+
"/static",
|
|
415
|
+
"/public",
|
|
416
|
+
"/admin",
|
|
417
|
+
].concat(options.exclude || []);
|
|
418
|
+
|
|
419
|
+
// IMPORTANT: This interceptor must be added AFTER koa-bodyparser middleware
|
|
420
|
+
// so that ctx.request.body is already populated when we read it.
|
|
421
|
+
app.use(async function botVersionKoaInterceptor(ctx, next) {
|
|
422
|
+
await next();
|
|
423
|
+
|
|
424
|
+
const path = ctx.path || "";
|
|
425
|
+
const method = ctx.method ? ctx.method.toUpperCase() : "";
|
|
426
|
+
|
|
427
|
+
const shouldIgnore = ignorePaths.some(function (p) {
|
|
428
|
+
return path.startsWith(p);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
if (shouldIgnore) return;
|
|
432
|
+
|
|
433
|
+
if (options.apiPrefix && !path.startsWith(options.apiPrefix)) return;
|
|
434
|
+
|
|
435
|
+
const normalizedPath = normalizePath(path);
|
|
436
|
+
const endpointKey = method + ":" + normalizedPath;
|
|
437
|
+
|
|
438
|
+
const body = ctx.request.body;
|
|
439
|
+
const bodyStructure = buildBodyStructure(body);
|
|
440
|
+
const bodyKey =
|
|
441
|
+
endpointKey +
|
|
442
|
+
":" +
|
|
443
|
+
Object.keys(bodyStructure || {})
|
|
444
|
+
.sort()
|
|
445
|
+
.join(",");
|
|
446
|
+
|
|
447
|
+
if (!reportedEndpoints.has(bodyKey)) {
|
|
448
|
+
reportedEndpoints.add(bodyKey);
|
|
449
|
+
|
|
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;
|
|
468
|
+
|
|
469
|
+
setImmediate(function () {
|
|
470
|
+
client
|
|
471
|
+
.updateEndpoint({
|
|
472
|
+
method,
|
|
473
|
+
path: normalizedPath,
|
|
474
|
+
requestBody: jsonSchema,
|
|
475
|
+
detectedBy: "runtime-koa",
|
|
476
|
+
})
|
|
477
|
+
.catch(function () {});
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Attaches a Hapi lifecycle extension that silently intercepts every request
|
|
485
|
+
* and reports new endpoints to BotVersion platform.
|
|
486
|
+
*/
|
|
487
|
+
function attachHapiInterceptor(server, client, options) {
|
|
488
|
+
options = options || {};
|
|
489
|
+
|
|
490
|
+
const ignorePaths = [
|
|
491
|
+
"/health",
|
|
492
|
+
"/favicon.ico",
|
|
493
|
+
"/_next",
|
|
494
|
+
"/static",
|
|
495
|
+
"/public",
|
|
496
|
+
"/admin",
|
|
497
|
+
].concat(options.exclude || []);
|
|
498
|
+
|
|
499
|
+
server.ext("onPostAuth", function (request, h) {
|
|
500
|
+
const path = request.path || "";
|
|
501
|
+
const method = request.method ? request.method.toUpperCase() : "";
|
|
502
|
+
|
|
503
|
+
const shouldIgnore = ignorePaths.some(function (p) {
|
|
504
|
+
return path.startsWith(p);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
if (shouldIgnore) return h.continue;
|
|
508
|
+
|
|
509
|
+
if (options.apiPrefix && !path.startsWith(options.apiPrefix))
|
|
510
|
+
return h.continue;
|
|
511
|
+
|
|
512
|
+
const normalizedPath = normalizePath(path);
|
|
513
|
+
const endpointKey = method + ":" + normalizedPath;
|
|
514
|
+
|
|
515
|
+
const body = request.payload;
|
|
516
|
+
const bodyStructure = buildBodyStructure(body);
|
|
517
|
+
const bodyKey =
|
|
518
|
+
endpointKey +
|
|
519
|
+
":" +
|
|
520
|
+
Object.keys(bodyStructure || {})
|
|
521
|
+
.sort()
|
|
522
|
+
.join(",");
|
|
523
|
+
|
|
524
|
+
if (!reportedEndpoints.has(bodyKey)) {
|
|
525
|
+
reportedEndpoints.add(bodyKey);
|
|
526
|
+
|
|
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;
|
|
545
|
+
|
|
546
|
+
setImmediate(function () {
|
|
547
|
+
client
|
|
548
|
+
.updateEndpoint({
|
|
549
|
+
method,
|
|
550
|
+
path: normalizedPath,
|
|
551
|
+
requestBody: jsonSchema,
|
|
552
|
+
detectedBy: "runtime-hapi",
|
|
553
|
+
})
|
|
554
|
+
.catch(function () {});
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return h.continue;
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Attaches a NestJS interceptor by patching the underlying http server.
|
|
564
|
+
* NestJS runs on top of Express or Fastify under the hood so we can
|
|
565
|
+
* patch the Node http server the same way we do for Next.js.
|
|
566
|
+
*/
|
|
567
|
+
function attachNestJsInterceptor(client, options) {
|
|
568
|
+
// NestJS runs on Node http server under the hood
|
|
569
|
+
// So we reuse the same approach as Next.js
|
|
570
|
+
attachNextJsInterceptor(client, {
|
|
571
|
+
exclude: (options || {}).exclude || [],
|
|
572
|
+
apiPrefix: (options || {}).apiPrefix || "/",
|
|
573
|
+
debug: (options || {}).debug || false,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* SvelteKit, Nuxt and Remix all run on Node http server
|
|
579
|
+
* so we reuse the Next.js interceptor approach for all of them.
|
|
580
|
+
*/
|
|
581
|
+
function attachSvelteKitInterceptor(client, options) {
|
|
582
|
+
attachNextJsInterceptor(client, {
|
|
583
|
+
exclude: (options || {}).exclude || [],
|
|
584
|
+
apiPrefix: (options || {}).apiPrefix || "/",
|
|
585
|
+
debug: (options || {}).debug || false,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function attachNuxtInterceptor(client, options) {
|
|
590
|
+
attachNextJsInterceptor(client, {
|
|
591
|
+
exclude: (options || {}).exclude || [],
|
|
592
|
+
apiPrefix: (options || {}).apiPrefix || "/api",
|
|
593
|
+
debug: (options || {}).debug || false,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function attachRemixInterceptor(client, options) {
|
|
598
|
+
attachNextJsInterceptor(client, {
|
|
599
|
+
exclude: (options || {}).exclude || [],
|
|
600
|
+
apiPrefix: (options || {}).apiPrefix || "/",
|
|
601
|
+
debug: (options || {}).debug || false,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function attachAdonisInterceptor(client, options) {
|
|
606
|
+
attachNextJsInterceptor(client, {
|
|
607
|
+
exclude: (options || {}).exclude || [],
|
|
608
|
+
apiPrefix: (options || {}).apiPrefix || "/",
|
|
609
|
+
debug: (options || {}).debug || false,
|
|
610
|
+
});
|
|
223
611
|
}
|
|
224
612
|
|
|
225
613
|
module.exports = {
|
|
226
614
|
attachInterceptor,
|
|
227
615
|
attachNextJsInterceptor,
|
|
616
|
+
attachFastifyInterceptor,
|
|
617
|
+
attachKoaInterceptor,
|
|
618
|
+
attachHapiInterceptor,
|
|
619
|
+
attachNestJsInterceptor,
|
|
620
|
+
attachSvelteKitInterceptor,
|
|
621
|
+
attachNuxtInterceptor,
|
|
622
|
+
attachRemixInterceptor,
|
|
623
|
+
attachAdonisInterceptor,
|
|
228
624
|
normalizePath,
|
|
229
625
|
buildBodyStructure,
|
|
230
626
|
};
|