botversion-sdk 2.2.1 → 2.2.3

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.
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
4
  var path = require("path");
@@ -29,7 +29,7 @@ var BotVersionClient = require("../client");
29
29
 
30
30
  var apiKey = process.env.BOTVERSION_API_KEY;
31
31
  var platformUrl =
32
- process.env.BOTVERSION_PLATFORM_URL || "http://localhost:3000";
32
+ process.env.BOTVERSION_PLATFORM_URL || "https://console.botversion.com";
33
33
 
34
34
  if (!apiKey) {
35
35
  console.error(
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
4
  var path = require("path");
@@ -30,7 +30,7 @@ var sdk = require("../index");
30
30
 
31
31
  var apiKey = process.env.BOTVERSION_API_KEY;
32
32
  var platformUrl =
33
- process.env.BOTVERSION_PLATFORM_URL || "http://localhost:3000";
33
+ process.env.BOTVERSION_PLATFORM_URL || "https://console.botversion.com";
34
34
 
35
35
  if (!apiKey) {
36
36
  console.error(
package/client.js CHANGED
@@ -11,7 +11,7 @@ const url = require("url");
11
11
  */
12
12
  function BotVersionClient(options) {
13
13
  this.apiKey = options.apiKey;
14
- this.platformUrl = options.platformUrl || "http://localhost:3000";
14
+ this.platformUrl = options.platformUrl || "https://console.botversion.com";
15
15
  this.debug = options.debug || false;
16
16
  this.timeout = options.timeout || 30000;
17
17
 
package/index.js CHANGED
@@ -424,7 +424,7 @@ var BotVersion = {
424
424
 
425
425
  this._client = new BotVersionClient({
426
426
  apiKey: options.apiKey,
427
- platformUrl: options.platformUrl || "http://localhost:3000",
427
+ platformUrl: options.platformUrl || "https://console.botversion.com",
428
428
  debug: options.debug || false,
429
429
  timeout: options.timeout || 30000,
430
430
  });
package/interceptor.js CHANGED
@@ -215,6 +215,20 @@ function parseCapturedBody(buffer, res) {
215
215
  return undefined;
216
216
  }
217
217
 
218
+ // True when the request carries an upload (multipart) body that cannot be read safely
219
+ function isMultipartContentType(value) {
220
+ return String(value || "")
221
+ .toLowerCase()
222
+ .includes("multipart/form-data");
223
+ }
224
+
225
+ // Adds the multipart marker to a request schema (creates an empty one if there are no fields)
226
+ function withMultipartMarker(schema) {
227
+ const out = schema || { type: "object", properties: {} };
228
+ out["x-content-type"] = "multipart/form-data";
229
+ return out;
230
+ }
231
+
218
232
  // Our own scan-trigger route must never be reported as one of the host's endpoints
219
233
  function isBotVersionInternalPath(path) {
220
234
  return String(path || "").indexOf("/__botversion/") === 0;
@@ -522,7 +536,16 @@ function attachInterceptor(app, client, options) {
522
536
  if (!reportedEndpoints.has(bodyKey)) {
523
537
  reportedEndpoints.add(bodyKey);
524
538
 
525
- const jsonSchema = structureToJsonSchema(bodyStructure);
539
+ let jsonSchema = structureToJsonSchema(bodyStructure);
540
+ // Upload bodies can't be read here, so flag them as multipart instead of "no input"
541
+ if (
542
+ String((req.headers && req.headers["content-type"]) || "")
543
+ .toLowerCase()
544
+ .includes("multipart/form-data")
545
+ ) {
546
+ jsonSchema = jsonSchema || { type: "object", properties: {} };
547
+ jsonSchema["x-content-type"] = "multipart/form-data";
548
+ }
526
549
 
527
550
  setImmediate(function () {
528
551
  client
@@ -852,14 +875,18 @@ function attachNextJsInterceptor(client, options) {
852
875
  if (!reportedEndpoints.has(endpointKey)) {
853
876
  reportedEndpoints.add(endpointKey);
854
877
 
855
- // Skip file uploads — too large and not JSON
856
878
  const contentType = req.headers["content-type"] || "";
857
879
  if (contentType.includes("multipart/form-data")) {
858
880
  client
859
881
  .updateEndpoint({
860
882
  method: method,
861
883
  path: normalizedPath,
862
- requestBody: null,
884
+ // Field names can't be read without consuming the upload stream, so flag it as multipart instead of "no input"
885
+ requestBody: {
886
+ type: "object",
887
+ properties: {},
888
+ "x-content-type": "multipart/form-data",
889
+ },
863
890
  detectedBy: "runtime",
864
891
  })
865
892
  .catch(function () {});
@@ -1078,9 +1105,13 @@ function attachFastifyInterceptor(fastify, client, options) {
1078
1105
  .updateEndpoint({
1079
1106
  method,
1080
1107
  path: normalizedPath,
1081
- requestBody: queryStructure
1082
- ? structureToJsonSchema(queryStructure)
1083
- : null,
1108
+ requestBody: isMultipartContentType(request.headers["content-type"])
1109
+ ? withMultipartMarker(
1110
+ queryStructure ? structureToJsonSchema(queryStructure) : null,
1111
+ )
1112
+ : queryStructure
1113
+ ? structureToJsonSchema(queryStructure)
1114
+ : null,
1084
1115
  detectedBy: "runtime-fastify",
1085
1116
  })
1086
1117
  .catch(function () {});
@@ -1100,6 +1131,8 @@ function attachFastifyInterceptor(fastify, client, options) {
1100
1131
 
1101
1132
  const body = request.body;
1102
1133
  if (!body) return;
1134
+ // Upload bodies are reported as multipart by the first hook; their parts are not plain fields
1135
+ if (isMultipartContentType(request.headers["content-type"])) return;
1103
1136
 
1104
1137
  const bodyStructure = buildBodyStructure(body);
1105
1138
  if (!bodyStructure) return;
@@ -1247,7 +1280,9 @@ function attachKoaInterceptor(app, client, options) {
1247
1280
  .updateEndpoint({
1248
1281
  method,
1249
1282
  path: normalizedPath,
1250
- requestBody: jsonSchema,
1283
+ requestBody: isMultipartContentType(ctx.headers["content-type"])
1284
+ ? withMultipartMarker(jsonSchema)
1285
+ : jsonSchema,
1251
1286
  detectedBy: "runtime-koa",
1252
1287
  })
1253
1288
  .catch(function () {});
@@ -1324,7 +1359,11 @@ function attachHapiInterceptor(server, client, options) {
1324
1359
  const normalizedPath = normalizePath(path);
1325
1360
  const endpointKey = method + ":" + normalizedPath;
1326
1361
 
1327
- const body = request.payload;
1362
+ // Upload payloads hold file streams, not plain fields, so they are never read as a body
1363
+ const isUpload = isMultipartContentType(
1364
+ request.headers && request.headers["content-type"],
1365
+ );
1366
+ const body = isUpload ? null : request.payload;
1328
1367
  // GET/DELETE inputs live in the query string, so their names are added too
1329
1368
  const bodyStructure = mergeQueryIntoStructure(
1330
1369
  buildBodyStructure(body),
@@ -1352,7 +1391,9 @@ function attachHapiInterceptor(server, client, options) {
1352
1391
  .updateEndpoint({
1353
1392
  method,
1354
1393
  path: normalizedPath,
1355
- requestBody: jsonSchema,
1394
+ requestBody: isUpload
1395
+ ? withMultipartMarker(jsonSchema)
1396
+ : jsonSchema,
1356
1397
  detectedBy: "runtime-hapi",
1357
1398
  })
1358
1399
  .catch(function () {});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botversion-sdk",
3
- "version": "2.2.1",
3
+ "version": "2.2.3",
4
4
  "description": "BotVersion SDK — auto-detect and register your API endpoints",
5
5
  "main": "index.js",
6
6
  "bin": {
package/scanner.js CHANGED
@@ -80,7 +80,19 @@ function scanExpressFileStatically(filePath, seen) {
80
80
  method: effectiveMethod,
81
81
  path: routePath,
82
82
  description: "",
83
- requestBody: bodyFields || queryFields,
83
+ requestBody:
84
+ mergeBodyWithMultipart(
85
+ bodyFields,
86
+ ["POST", "PUT", "PATCH"].includes(effectiveMethod)
87
+ ? extractMultipartFieldsFromFile(
88
+ sliceRouteSegment(content, match.index, routePattern.source),
89
+ ) ||
90
+ ((content.match(new RegExp(routePattern.source, "gi")) || [])
91
+ .length === 1
92
+ ? extractMultipartFieldsFromFile(content)
93
+ : null)
94
+ : null,
95
+ ) || queryFields,
84
96
  routeParamMap: routeParamMap,
85
97
  responseBody: extractResponseFieldsForRoute(
86
98
  content,
@@ -239,6 +251,13 @@ function scanNextJsRoutes(pagesDir) {
239
251
  : null);
240
252
  const queryFields = extractQueryFieldsFromFile(methodCode);
241
253
 
254
+ // Upload handlers read multipart fields, not req.body
255
+ const multipartFields =
256
+ method !== "GET" && method !== "DELETE"
257
+ ? extractMultipartFieldsFromFile(methodCode) ||
258
+ extractMultipartFieldsFromFile(fileContent)
259
+ : null;
260
+
242
261
  // GET inputs come from the query string; only this method's own code is read
243
262
  const getQueryFields =
244
263
  method === "GET"
@@ -252,7 +271,7 @@ function scanNextJsRoutes(pagesDir) {
252
271
 
253
272
  // For DELETE with no body fields, query params are the input
254
273
  const effectiveRequestBody =
255
- bodyFields ||
274
+ mergeBodyWithMultipart(bodyFields, multipartFields) ||
256
275
  (method === "DELETE" && queryFields ? queryFields : null) ||
257
276
  getQueryFields;
258
277
 
@@ -462,6 +481,197 @@ function extractBodyFieldsFromFile(content) {
462
481
  return { type: "object", properties };
463
482
  }
464
483
 
484
+ // Reads field names from multipart/file-upload handlers (formData, multer, formidable).
485
+ // Returns null when the file is not an upload handler, so other routes are unaffected.
486
+ function extractMultipartFieldsFromFile(content) {
487
+ const text = String(content || "");
488
+ const usesFormData =
489
+ /formData\s*\(|readFormData\s*\(|parseMultipartFormData\s*\(/.test(text);
490
+ const usesMulter = /multer|upload\.(single|array|fields|any)\s*\(/.test(text);
491
+ const usesFormidable = /formidable|koa-?body/i.test(text);
492
+ // NestJS interceptors, AdonisJS request.file("x"), Nuxt readMultipartFormData, Hapi multipart payloads
493
+ const usesNest =
494
+ /\b(?:FileInterceptor|FilesInterceptor|FileFieldsInterceptor|AnyFilesInterceptor)\s*\(/.test(
495
+ text,
496
+ );
497
+ const usesRequestFile = /\brequest\.(?:file|files)\s*\(\s*['"`]/.test(text);
498
+ const usesNuxtParts = /readMultipartFormData\s*\(/.test(text);
499
+ const usesHapiPayload = /\bmultipart\s*:\s*(?:true|\{)/.test(text);
500
+ const usesOtherUpload =
501
+ usesNest || usesRequestFile || usesNuxtParts || usesHapiPayload;
502
+ if (!usesFormData && !usesMulter && !usesFormidable && !usesOtherUpload)
503
+ return null;
504
+
505
+ const fields = {}; // name -> "file" | "string"
506
+
507
+ if (usesFormData) {
508
+ // Only read .get() calls on the variable that holds the parsed form,
509
+ // so headers.get(...) / searchParams.get(...) are never mistaken for form fields
510
+ const formVars = [];
511
+ for (const fv of text.matchAll(
512
+ /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:await\s+)?(?:[\w$.]+\.formData|readFormData|(?:unstable_)?parseMultipartFormData)\s*\(/g,
513
+ )) {
514
+ formVars.push(fv[1]);
515
+ }
516
+ const readFormGet = function (m) {
517
+ const varName = m[1];
518
+ const fieldName = m[2];
519
+ let kind = "string";
520
+ if (varName) {
521
+ const fileUse = new RegExp(
522
+ "\\b" +
523
+ varName +
524
+ "\\b\\s*(?:as\\s+File|instanceof\\s+File|\\.(?:arrayBuffer|stream|size|type|name)\\b)|instanceof\\s+File",
525
+ );
526
+ if (fileUse.test(text)) kind = "file";
527
+ }
528
+ if (fields[fieldName] !== "file") fields[fieldName] = kind;
529
+ };
530
+ formVars.forEach(function (formVar) {
531
+ const safeVar = formVar.replace(/\$/g, "\\$");
532
+ const getRe = new RegExp(
533
+ "(?:(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*)?\\b" +
534
+ safeVar +
535
+ "\\s*\\.(?:get|getAll)\\s*\\(\\s*['\"`]([^'\"`]+)['\"`]\\s*\\)",
536
+ "g",
537
+ );
538
+ for (const m of text.matchAll(getRe)) readFormGet(m);
539
+ });
540
+ // Inline form: (await request.formData()).get("file")
541
+ const inlineRe =
542
+ /(?:(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*)?\(\s*await\s+[\w$.]+\.formData\s*\(\s*\)\s*\)\s*\.(?:get|getAll)\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
543
+ for (const m of text.matchAll(inlineRe)) readFormGet(m);
544
+ }
545
+
546
+ if (usesMulter) {
547
+ // upload.single("file") / upload.array("files")
548
+ for (const m of text.matchAll(
549
+ /\.(?:single|array)\s*\(\s*['"`]([^'"`]+)['"`]/g,
550
+ )) {
551
+ fields[m[1]] = "file";
552
+ }
553
+ // upload.fields([{ name: "avatar" }, { name: "docs" }])
554
+ for (const block of text.matchAll(/\.fields\s*\(\s*\[([\s\S]*?)\]\s*\)/g)) {
555
+ for (const n of block[1].matchAll(/name\s*:\s*['"`]([^'"`]+)['"`]/g)) {
556
+ fields[n[1]] = "file";
557
+ }
558
+ }
559
+ }
560
+
561
+ if (usesFormidable) {
562
+ // files.file / files["file"] and fields.title / fields["title"]
563
+ for (const m of text.matchAll(
564
+ /\bfiles(?:\?\.|\.)([A-Za-z_$][\w$]*)|\bfiles\s*\[\s*['"`]([^'"`]+)['"`]\s*\]/g,
565
+ )) {
566
+ fields[m[1] || m[2]] = "file";
567
+ }
568
+ for (const m of text.matchAll(
569
+ /\bfields(?:\?\.|\.)([A-Za-z_$][\w$]*)|\bfields\s*\[\s*['"`]([^'"`]+)['"`]\s*\]/g,
570
+ )) {
571
+ const name = m[1] || m[2];
572
+ if (!fields[name]) fields[name] = "string";
573
+ }
574
+ }
575
+
576
+ if (usesNest) {
577
+ // @UseInterceptors(FileInterceptor("file")) / FilesInterceptor("files")
578
+ for (const m of text.matchAll(
579
+ /\b(?:FileInterceptor|FilesInterceptor)\s*\(\s*['"`]([^'"`]+)['"`]/g,
580
+ )) {
581
+ fields[m[1]] = "file";
582
+ }
583
+ // FileFieldsInterceptor([{ name: "avatar" }, { name: "docs" }])
584
+ for (const block of text.matchAll(
585
+ /\bFileFieldsInterceptor\s*\(\s*\[([\s\S]*?)\]/g,
586
+ )) {
587
+ for (const n of block[1].matchAll(/name\s*:\s*['"`]([^'"`]+)['"`]/g)) {
588
+ fields[n[1]] = "file";
589
+ }
590
+ }
591
+ }
592
+
593
+ if (usesRequestFile) {
594
+ // request.file("avatar") / request.files("docs")
595
+ for (const m of text.matchAll(
596
+ /\brequest\.(?:file|files)\s*\(\s*['"`]([^'"`]+)['"`]/g,
597
+ )) {
598
+ fields[m[1]] = "file";
599
+ }
600
+ }
601
+
602
+ if (usesNuxtParts) {
603
+ // readMultipartFormData(event) -> parts.find((p) => p.name === "file")
604
+ for (const m of text.matchAll(
605
+ /(?:(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*)?[^\n;]*?\.name\s*===?\s*['"`]([^'"`]+)['"`]/g,
606
+ )) {
607
+ let kind = "string";
608
+ if (m[1]) {
609
+ const safeVar = m[1].replace(/\$/g, "\\$");
610
+ const fileUse = new RegExp(
611
+ "\\b" + safeVar + "\\b\\s*(?:\\?\\.|\\.)\\s*(?:filename|type)\\b",
612
+ );
613
+ if (fileUse.test(text)) kind = "file";
614
+ }
615
+ if (fields[m[2]] !== "file") fields[m[2]] = kind;
616
+ }
617
+ }
618
+
619
+ if (usesHapiPayload) {
620
+ // request.payload.file / const { file, title } = request.payload
621
+ const addHapiField = function (name) {
622
+ if (!/^[A-Za-z_$][\w$]*$/.test(name) || fields[name]) return;
623
+ const safeName = name.replace(/\$/g, "\\$");
624
+ const fileLike = new RegExp(
625
+ "\\b" + safeName + "\\b\\s*(?:\\?\\.|\\.)\\s*(?:hapi|_data|pipe)\\b",
626
+ ).test(text);
627
+ fields[name] = fileLike ? "file" : "string";
628
+ };
629
+ for (const m of text.matchAll(/\bpayload(?:\?\.|\.)([A-Za-z_$][\w$]*)/g)) {
630
+ addHapiField(m[1]);
631
+ }
632
+ for (const d of text.matchAll(/\{([^}]+)\}\s*=\s*[\w$.]*\bpayload\b/g)) {
633
+ d[1].split(",").forEach(function (f) {
634
+ addHapiField(f.trim().split(":")[0].split("=")[0].trim());
635
+ });
636
+ }
637
+ }
638
+
639
+ const names = Object.keys(fields);
640
+ if (names.length === 0) return null;
641
+
642
+ const properties = {};
643
+ names.forEach(function (n) {
644
+ properties[n] =
645
+ fields[n] === "file"
646
+ ? { type: "string", format: "binary" }
647
+ : { type: "string" };
648
+ });
649
+ return {
650
+ type: "object",
651
+ properties: properties,
652
+ "x-content-type": "multipart/form-data",
653
+ };
654
+ }
655
+
656
+ // Start of any Fastify route definition, used to cut a file into per-route pieces
657
+ const FASTIFY_ROUTE_START =
658
+ "(?:fastify|app|server|router)\\.(?:get|post|put|delete|patch|route)\\s*\\(";
659
+
660
+ // Combines text fields read from req.body with file fields from the upload parser
661
+ function mergeBodyWithMultipart(bodyFields, multipartFields) {
662
+ if (!multipartFields) return bodyFields;
663
+ if (!bodyFields) return multipartFields;
664
+ return {
665
+ type: "object",
666
+ properties: Object.assign(
667
+ {},
668
+ bodyFields.properties,
669
+ multipartFields.properties,
670
+ ),
671
+ "x-content-type": "multipart/form-data",
672
+ };
673
+ }
674
+
465
675
  function scanNextJsAppRoutes(appDir) {
466
676
  const fs = require("fs");
467
677
  const path = require("path");
@@ -507,6 +717,11 @@ function scanNextJsAppRoutes(appDir) {
507
717
  ? extractAppRouterBodyFields(content)
508
718
  : null);
509
719
  const queryFields = extractQueryFieldsFromFile(methodCode);
720
+ const multipartFields =
721
+ method !== "GET" && method !== "DELETE"
722
+ ? extractMultipartFieldsFromFile(methodCode) ||
723
+ extractMultipartFieldsFromFile(content)
724
+ : null;
510
725
 
511
726
  const routeParamMap = buildRouteParamMap(routePath, []);
512
727
 
@@ -515,7 +730,7 @@ function scanNextJsAppRoutes(appDir) {
515
730
  path: routePath,
516
731
  description: "",
517
732
  requestBody:
518
- bodyFields ||
733
+ mergeBodyWithMultipart(bodyFields, multipartFields) ||
519
734
  (method === "DELETE" && queryFields ? queryFields : null) ||
520
735
  (method === "GET"
521
736
  ? removePathParamFields(
@@ -1548,7 +1763,14 @@ function scanFastifyRoutes(cwd) {
1548
1763
  method,
1549
1764
  path: routePath,
1550
1765
  description: "",
1551
- requestBody: bodyFields,
1766
+ requestBody: mergeBodyWithMultipart(
1767
+ bodyFields,
1768
+ needsBody
1769
+ ? extractMultipartFieldsFromFile(
1770
+ sliceRouteSegment(content, match.index, FASTIFY_ROUTE_START),
1771
+ )
1772
+ : null,
1773
+ ),
1552
1774
  routeParamMap,
1553
1775
  detectedBy: "static-scan-fastify",
1554
1776
  });
@@ -1577,7 +1799,14 @@ function scanFastifyRoutes(cwd) {
1577
1799
  method,
1578
1800
  path: routePath,
1579
1801
  description: "",
1580
- requestBody: bodyFields,
1802
+ requestBody: mergeBodyWithMultipart(
1803
+ bodyFields,
1804
+ needsBody
1805
+ ? extractMultipartFieldsFromFile(
1806
+ sliceRouteSegment(content, match.index, FASTIFY_ROUTE_START),
1807
+ )
1808
+ : null,
1809
+ ),
1581
1810
  routeParamMap,
1582
1811
  detectedBy: "static-scan-fastify",
1583
1812
  });
@@ -1613,7 +1842,18 @@ function scanFastifyRoutes(cwd) {
1613
1842
  method,
1614
1843
  path: routePath,
1615
1844
  description: "",
1616
- requestBody: bodyFields,
1845
+ requestBody: mergeBodyWithMultipart(
1846
+ bodyFields,
1847
+ needsBody
1848
+ ? extractMultipartFieldsFromFile(
1849
+ sliceRouteSegment(
1850
+ content,
1851
+ match.index,
1852
+ FASTIFY_ROUTE_START,
1853
+ ),
1854
+ )
1855
+ : null,
1856
+ ),
1617
1857
  routeParamMap,
1618
1858
  detectedBy: "static-scan-fastify",
1619
1859
  });
@@ -1744,7 +1984,14 @@ function scanNestJsRoutes(cwd) {
1744
1984
  method,
1745
1985
  path: normalizedPath,
1746
1986
  description: "",
1747
- requestBody: bodyFields,
1987
+ requestBody: mergeBodyWithMultipart(
1988
+ bodyFields,
1989
+ needsBody
1990
+ ? extractMultipartFieldsFromFile(
1991
+ sliceRouteSegment(content, match.index, methodPattern.source),
1992
+ )
1993
+ : null,
1994
+ ),
1748
1995
  routeParamMap,
1749
1996
  detectedBy: "static-scan-nestjs",
1750
1997
  });
@@ -1847,7 +2094,14 @@ function scanKoaRoutes(cwd) {
1847
2094
  method,
1848
2095
  path: routePath,
1849
2096
  description: "",
1850
- requestBody: bodyFields,
2097
+ requestBody: mergeBodyWithMultipart(
2098
+ bodyFields,
2099
+ needsBody
2100
+ ? extractMultipartFieldsFromFile(
2101
+ sliceRouteSegment(content, match.index, routePattern.source),
2102
+ )
2103
+ : null,
2104
+ ),
1851
2105
  routeParamMap,
1852
2106
  detectedBy: "static-scan-koa",
1853
2107
  });
@@ -1947,7 +2201,14 @@ function scanHapiRoutes(cwd) {
1947
2201
  method,
1948
2202
  path: routePath,
1949
2203
  description: "",
1950
- requestBody: bodyFields,
2204
+ requestBody: mergeBodyWithMultipart(
2205
+ bodyFields,
2206
+ needsBody
2207
+ ? extractMultipartFieldsFromFile(
2208
+ sliceRouteSegment(content, match.index, routePattern.source),
2209
+ )
2210
+ : null,
2211
+ ),
1951
2212
  routeParamMap,
1952
2213
  detectedBy: "static-scan-hapi",
1953
2214
  });
@@ -2048,7 +2309,14 @@ function scanAdonisRoutes(cwd) {
2048
2309
  method,
2049
2310
  path: routePath,
2050
2311
  description: "",
2051
- requestBody: bodyFields,
2312
+ requestBody: mergeBodyWithMultipart(
2313
+ bodyFields,
2314
+ needsBody
2315
+ ? extractMultipartFieldsFromFile(
2316
+ sliceRouteSegment(content, match.index, routePattern.source),
2317
+ )
2318
+ : null,
2319
+ ),
2052
2320
  routeParamMap,
2053
2321
  detectedBy: "static-scan-adonis",
2054
2322
  });
@@ -2134,7 +2402,18 @@ function scanNuxtServerRoutes(cwd) {
2134
2402
  method: m,
2135
2403
  path: finalPath,
2136
2404
  description: "",
2137
- requestBody: null,
2405
+ requestBody:
2406
+ m !== "GET" && m !== "DELETE"
2407
+ ? (function () {
2408
+ try {
2409
+ return extractMultipartFieldsFromFile(
2410
+ fs.readFileSync(fullPath, "utf8"),
2411
+ );
2412
+ } catch (e) {
2413
+ return null;
2414
+ }
2415
+ })()
2416
+ : null,
2138
2417
  routeParamMap,
2139
2418
  detectedBy: "static-scan-nuxt",
2140
2419
  });
@@ -2223,7 +2502,14 @@ function scanSvelteKitServerRoutes(cwd) {
2223
2502
  method,
2224
2503
  path: routePath || "/",
2225
2504
  description: "",
2226
- requestBody: bodyFields,
2505
+ requestBody: mergeBodyWithMultipart(
2506
+ bodyFields,
2507
+ needsBody
2508
+ ? extractMultipartFieldsFromFile(
2509
+ getMethodSegment(content, method, methods, "app") || content,
2510
+ )
2511
+ : null,
2512
+ ),
2227
2513
  routeParamMap,
2228
2514
  detectedBy: "static-scan-sveltekit",
2229
2515
  });
@@ -2340,7 +2626,10 @@ function scanRemixServerRoutes(cwd) {
2340
2626
  if (seen.has(key)) return;
2341
2627
  seen.add(key);
2342
2628
 
2343
- const bodyFields = extractBodyFieldsFromFile(content);
2629
+ const bodyFields = mergeBodyWithMultipart(
2630
+ extractBodyFieldsFromFile(content),
2631
+ extractMultipartFieldsFromFile(content),
2632
+ );
2344
2633
  endpoints.push({
2345
2634
  method,
2346
2635
  path: routePath,