botversion-sdk 1.0.5 → 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
@@ -1,109 +1,7 @@
1
1
  // botversion-sdk/interceptor.js
2
2
  "use strict";
3
3
 
4
- /**
5
- * Makes an internal HTTP request to the user's own backend.
6
- * Forwards cookies, auth headers, and CSRF tokens.
7
- */
8
- function makeInternalRequest(method, path, body, cookies, headers, baseUrl) {
9
- return new Promise(function (resolve) {
10
- var http = require("http");
11
- var https = require("https");
12
- var url = require("url");
13
-
14
- baseUrl = baseUrl || "http://127.0.0.1:8000";
15
- var fullUrl = baseUrl + path;
16
- var parsedUrl = url.parse(fullUrl);
17
- var isHttps = parsedUrl.protocol === "https:";
18
- var lib = isHttps ? https : http;
19
-
20
- var bodyStr = body ? JSON.stringify(body) : null;
21
-
22
- var reqHeaders = {
23
- "Content-Type": "application/json",
24
- };
25
-
26
- if (bodyStr) {
27
- reqHeaders["Content-Length"] = String(Buffer.byteLength(bodyStr));
28
- }
29
-
30
- // Forward cookies — this is what identifies the user
31
- if (cookies) reqHeaders["Cookie"] = cookies;
32
-
33
- // Forward auth header
34
- var authHeader =
35
- headers && (headers["authorization"] || headers["Authorization"]);
36
- if (authHeader) reqHeaders["Authorization"] = authHeader;
37
-
38
- // Forward CSRF token
39
- var csrf =
40
- headers &&
41
- (headers["x-csrftoken"] ||
42
- headers["X-CSRFToken"] ||
43
- headers["x-xsrf-token"] ||
44
- headers["X-XSRF-TOKEN"]);
45
- if (csrf) reqHeaders["X-CSRFToken"] = csrf;
46
-
47
- var reqOptions = {
48
- hostname: parsedUrl.hostname,
49
- port: parsedUrl.port || (isHttps ? 443 : 80),
50
- path: parsedUrl.path,
51
- method: method.toUpperCase(),
52
- headers: reqHeaders,
53
- timeout: 30000,
54
- };
55
-
56
- var req = lib.request(reqOptions, function (res) {
57
- var data = "";
58
- res.on("data", function (chunk) {
59
- data += chunk;
60
- });
61
- res.on("end", function () {
62
- var parsed;
63
- try {
64
- parsed = JSON.parse(data);
65
- } catch (e) {
66
- parsed = { raw: data };
67
- }
68
-
69
- // Handle redirects
70
- if (
71
- res.statusCode >= 300 &&
72
- res.statusCode < 400 &&
73
- res.headers.location
74
- ) {
75
- makeInternalRequest(
76
- res.statusCode === 303 ? "GET" : method,
77
- res.headers.location,
78
- res.statusCode === 303 ? null : body,
79
- cookies,
80
- headers,
81
- baseUrl,
82
- ).then(resolve);
83
- return;
84
- }
85
-
86
- resolve({
87
- status: res.statusCode,
88
- ok: res.statusCode >= 200 && res.statusCode < 300,
89
- data: parsed,
90
- });
91
- });
92
- });
93
-
94
- req.on("error", function (err) {
95
- resolve({ status: 500, ok: false, data: { error: err.message } });
96
- });
97
-
98
- req.on("timeout", function () {
99
- req.destroy();
100
- resolve({ status: 500, ok: false, data: { error: "Request timed out" } });
101
- });
102
-
103
- if (bodyStr) req.write(bodyStr);
104
- req.end();
105
- });
106
- }
4
+ const reportedEndpoints = new Set();
107
5
 
108
6
  /**
109
7
  * Attaches a middleware to the Express app that
@@ -120,11 +18,9 @@ function attachInterceptor(app, client, options) {
120
18
  "/_next",
121
19
  "/static",
122
20
  "/public",
21
+ "/admin",
123
22
  ].concat(options.exclude || []);
124
23
 
125
- // Track which endpoints we have already reported
126
- const reportedEndpoints = new Set();
127
-
128
24
  app.use(function botVersionInterceptor(req, res, next) {
129
25
  const path = req.path || req.url || "";
130
26
  const shouldIgnore = ignorePaths.some(function (p) {
@@ -182,23 +78,12 @@ function attachInterceptor(app, client, options) {
182
78
  requestBody: jsonSchema,
183
79
  detectedBy: "runtime",
184
80
  })
185
- .catch(function (err) {
186
- if (options.debug) {
187
- console.warn(
188
- "[BotVersion SDK] Failed to report endpoint:",
189
- err.message,
190
- );
191
- }
192
- });
81
+ .catch(function (err) {});
193
82
  });
194
83
  }
195
84
 
196
85
  next();
197
86
  });
198
- // Register executor so WebSocket can forward calls to user's backend
199
- client.setExecutor(function (method, path, body, cookies, headers, baseUrl) {
200
- return makeInternalRequest(method, path, body, cookies, headers, baseUrl);
201
- });
202
87
  }
203
88
 
204
89
  /**
@@ -286,8 +171,456 @@ function buildBodyStructure(body) {
286
171
  return structure;
287
172
  }
288
173
 
174
+ function attachNextJsInterceptor(client, options) {
175
+ try {
176
+ const http = require("http");
177
+ const originalEmit = http.Server.prototype.emit;
178
+
179
+ http.Server.prototype.emit = function (event, req, res) {
180
+ if (event === "request") {
181
+ const path = req.url ? req.url.split("?")[0] : "";
182
+ const method = req.method ? req.method.toUpperCase() : "";
183
+
184
+ const shouldIgnore = (options.exclude || [])
185
+ .concat([
186
+ "/health",
187
+ "/favicon.ico",
188
+ "/_next",
189
+ "/static",
190
+ "/public",
191
+ "/admin",
192
+ ])
193
+ .some(function (p) {
194
+ return path.startsWith(p);
195
+ });
196
+
197
+ const isApiPath = path.startsWith(options.apiPrefix || "/api");
198
+
199
+ if (!shouldIgnore && isApiPath) {
200
+ const normalizedPath = normalizePath(path);
201
+ const endpointKey = method + ":" + normalizedPath;
202
+
203
+ if (!reportedEndpoints.has(endpointKey)) {
204
+ reportedEndpoints.add(endpointKey);
205
+
206
+ // Skip file uploads — too large and not JSON
207
+ const contentType = req.headers["content-type"] || "";
208
+ if (contentType.includes("multipart/form-data")) {
209
+ client
210
+ .updateEndpoint({
211
+ method: method,
212
+ path: normalizedPath,
213
+ requestBody: null,
214
+ detectedBy: "runtime",
215
+ })
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
+ };
304
+ }
305
+ }
306
+ }
307
+
308
+ return originalEmit.apply(this, arguments);
309
+ };
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
+ });
611
+ }
612
+
289
613
  module.exports = {
290
614
  attachInterceptor,
615
+ attachNextJsInterceptor,
616
+ attachFastifyInterceptor,
617
+ attachKoaInterceptor,
618
+ attachHapiInterceptor,
619
+ attachNestJsInterceptor,
620
+ attachSvelteKitInterceptor,
621
+ attachNuxtInterceptor,
622
+ attachRemixInterceptor,
623
+ attachAdonisInterceptor,
291
624
  normalizePath,
292
625
  buildBodyStructure,
293
626
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botversion-sdk",
3
- "version": "1.0.5",
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": {
@@ -16,7 +16,8 @@
16
16
  "author": "Saurav Dhakal",
17
17
  "license": "MIT",
18
18
  "dependencies": {
19
- "express": "^4.18.2",
20
- "ws": "^8.20.0"
19
+ "@babel/parser": "^7.29.3",
20
+ "@babel/traverse": "^7.29.0",
21
+ "express": "^4.18.2"
21
22
  }
22
23
  }