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/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(["/health", "/favicon.ico", "/_next", "/static", "/public"])
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
- setImmediate(function () {
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 (err) {
208
- console.error("[botversion] update failed:", err.message);
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
- console.error(
219
- "[botversion] failed to attach Next.js interceptor:",
220
- err.message,
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botversion-sdk",
3
- "version": "1.0.6",
3
+ "version": "1.1.6",
4
4
  "description": "BotVersion SDK — auto-detect and register your API endpoints",
5
5
  "main": "index.js",
6
6
  "bin": {