botversion-sdk 2.1.0 → 2.1.1

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.
Files changed (2) hide show
  1. package/interceptor.js +144 -88
  2. package/package.json +1 -1
package/interceptor.js CHANGED
@@ -92,34 +92,43 @@ function attachInterceptor(app, client, options) {
92
92
  }
93
93
 
94
94
  const method = req.method.toUpperCase();
95
- const normalizedPath = normalizePath(path);
96
- const endpointKey = method + ":" + normalizedPath;
97
-
98
- const bodyStructure = buildBodyStructure(req.body);
99
- const bodyKey =
100
- endpointKey +
101
- ":" +
102
- Object.keys(bodyStructure || {})
103
- .sort()
104
- .join(",");
105
-
106
- if (!reportedEndpoints.has(bodyKey)) {
107
- reportedEndpoints.add(bodyKey);
108
-
109
- const jsonSchema = structureToJsonSchema(bodyStructure);
110
95
 
111
- // Report async — never block the request
112
- setImmediate(function () {
113
- client
114
- .updateEndpoint({
115
- method: method,
116
- path: normalizedPath,
117
- requestBody: jsonSchema,
118
- detectedBy: "runtime",
119
- })
120
- .catch(function (err) {});
121
- });
122
- }
96
+ // Split batched tRPC paths into separate clean paths + matching body
97
+ // slots, so each real procedure is registered as its own endpoint.
98
+ const rawPaths = splitBatchPath(path);
99
+ const bodySlots =
100
+ rawPaths.length > 1 ? splitBatchBody(req.body) : [req.body];
101
+
102
+ rawPaths.forEach(function (singlePath, i) {
103
+ const normalizedPath = normalizePath(singlePath);
104
+ const endpointKey = method + ":" + normalizedPath;
105
+ const slotBody = bodySlots[i] || null;
106
+
107
+ const bodyStructure = buildBodyStructure(slotBody);
108
+ const bodyKey =
109
+ endpointKey +
110
+ ":" +
111
+ Object.keys(bodyStructure || {})
112
+ .sort()
113
+ .join(",");
114
+
115
+ if (!reportedEndpoints.has(bodyKey)) {
116
+ reportedEndpoints.add(bodyKey);
117
+
118
+ const jsonSchema = structureToJsonSchema(bodyStructure);
119
+
120
+ setImmediate(function () {
121
+ client
122
+ .updateEndpoint({
123
+ method: method,
124
+ path: normalizedPath,
125
+ requestBody: jsonSchema,
126
+ detectedBy: "runtime",
127
+ })
128
+ .catch(function (err) {});
129
+ });
130
+ }
131
+ });
123
132
 
124
133
  next();
125
134
  });
@@ -184,6 +193,74 @@ function unwrapTrpcJsonEnvelope(obj) {
184
193
  return obj;
185
194
  }
186
195
 
196
+ /**
197
+ * tRPC batches multiple procedure calls into one URL, e.g.
198
+ * /api/trpc/me.get,getUserTopBanners,bookingUnconfirmedCount
199
+ * The procedure names appear comma-separated, only in the LAST path segment.
200
+ * This splits that into separate, clean paths — one per real procedure —
201
+ * so each gets registered as its own endpoint instead of one garbled,
202
+ * comma-joined path. Non-batched paths are returned unchanged as a
203
+ * single-item array.
204
+ */
205
+ function splitBatchPath(rawPath) {
206
+ // tRPC is always mounted at a fixed base containing "/trpc/" — e.g.
207
+ // "/api/trpc/". Everything after that marker is the comma-separated
208
+ // procedure list, and each comma-separated piece is ALREADY a complete
209
+ // procedure path in its own right (it may contain further "/" for
210
+ // nested routers, e.g. "me/get") — it must never have another
211
+ // procedure's prefix re-attached to it.
212
+ const trpcMarker = "/trpc/";
213
+ const markerIndex = rawPath.indexOf(trpcMarker);
214
+ if (markerIndex === -1) return [rawPath];
215
+
216
+ const base = rawPath.slice(0, markerIndex + trpcMarker.length);
217
+ const tail = rawPath.slice(markerIndex + trpcMarker.length);
218
+
219
+ if (!tail.includes(",")) return [rawPath];
220
+
221
+ return tail
222
+ .split(",")
223
+ .map(function (proc) {
224
+ return proc.trim();
225
+ })
226
+ .filter(Boolean)
227
+ .map(function (proc) {
228
+ return base + proc;
229
+ });
230
+ }
231
+
232
+ /**
233
+ * Splits a batched tRPC request body/input — { "0": {...}, "1": {...} } —
234
+ * into an array of individual bodies, in the same order as the batch keys,
235
+ * which lines up with the same order splitBatchPath() returns procedure
236
+ * names in. Each slot is unwrapped from its own { json: {...} } envelope.
237
+ * Returns a single-item array unchanged if the body isn't actually batched,
238
+ * so callers can always treat the result uniformly.
239
+ */
240
+ function splitBatchBody(bodyObj) {
241
+ if (!bodyObj || typeof bodyObj !== "object") return [bodyObj || null];
242
+
243
+ const keys = Object.keys(bodyObj);
244
+ const isBatch =
245
+ keys.length > 0 &&
246
+ keys.every(function (k) {
247
+ return /^\d+$/.test(k);
248
+ });
249
+
250
+ if (!isBatch) return [bodyObj];
251
+
252
+ return keys
253
+ .sort(function (a, b) {
254
+ return Number(a) - Number(b);
255
+ })
256
+ .map(function (k) {
257
+ const entry = bodyObj[k];
258
+ return entry && typeof entry === "object"
259
+ ? unwrapTrpcJsonEnvelope(entry)
260
+ : entry || null;
261
+ });
262
+ }
263
+
187
264
  /**
188
265
  * Extract just the structure of a request body
189
266
  * (keys and value types — never actual values for security)
@@ -372,25 +449,28 @@ function attachNextJsInterceptor(client, options) {
372
449
  // Helper to send body structure to platform
373
450
  function reportBody(bodyObj) {
374
451
  try {
375
- // ── tRPC GET input extraction ──────────────────────────────────────
376
- // tRPC GET requests send their input as a URL-encoded JSON query
377
- // parameter called "input". The body is always empty for GET requests.
378
- // We only attempt this extraction when:
379
- // 1. The body is empty (GET requests have no body)
380
- // 2. The URL contains an "input" query parameter
381
- // 3. The path looks like a tRPC endpoint (/trpc/)
382
- // This prevents accidentally treating regular REST query params
383
- // (e.g. ?filter=active&limit=10) as a body schema.
452
+ // Split the (possibly batched) tRPC path into separate,
453
+ // clean paths — one per real procedure — instead of one
454
+ // garbled comma-joined path.
455
+ const splitPaths = splitBatchPath(path).map(normalizePath);
456
+
384
457
  const isTrpcPath = path.includes("/trpc/");
385
458
  const hasInputParam = req.url && req.url.includes("input=");
386
459
 
460
+ // ── tRPC GET input extraction ──────────────────────────────────────
461
+ // tRPC GET requests send their input as a URL-encoded JSON query
462
+ // parameter called "input". We only attempt this extraction when
463
+ // the body is empty, the URL has an "input" param, and the path
464
+ // looks like a tRPC endpoint — so regular REST query params are
465
+ // never mistaken for a body schema.
466
+ let bodySlots = null;
467
+
387
468
  if (
388
469
  isTrpcPath &&
389
470
  hasInputParam &&
390
471
  (!bodyObj || Object.keys(bodyObj).length === 0)
391
472
  ) {
392
473
  try {
393
- // Use URL constructor to safely parse query params
394
474
  const urlObj = new URL(req.url, "http://localhost");
395
475
  const inputParam = urlObj.searchParams.get("input");
396
476
 
@@ -399,7 +479,6 @@ function attachNextJsInterceptor(client, options) {
399
479
  try {
400
480
  decoded = JSON.parse(decodeURIComponent(inputParam));
401
481
  } catch (e) {
402
- // input param exists but is not valid JSON — skip
403
482
  decoded = null;
404
483
  }
405
484
 
@@ -408,44 +487,10 @@ function attachNextJsInterceptor(client, options) {
408
487
  typeof decoded === "object" &&
409
488
  !Array.isArray(decoded)
410
489
  ) {
411
- const keys = Object.keys(decoded);
412
-
413
- // Format 1 — batched: { "0": { json: {...} }, "1": { json: {...} } }
414
- // All keys are numeric strings
415
- const isBatch =
416
- keys.length > 0 && keys.every((k) => /^\d+$/.test(k));
417
-
418
- if (isBatch) {
419
- // Merge all batch entries into one flat object
420
- // so we capture the full schema across all procedures
421
- const merged = {};
422
- keys.forEach((k) => {
423
- const entry = decoded[k];
424
- if (entry && typeof entry === "object") {
425
- // Unwrap { json: {...} } envelope if present
426
- const unwrapped =
427
- entry.json && typeof entry.json === "object"
428
- ? entry.json
429
- : entry;
430
- Object.assign(merged, unwrapped);
431
- }
432
- });
433
- if (Object.keys(merged).length > 0) {
434
- bodyObj = merged;
435
- }
436
- }
437
- // Format 2 — single with superjson: { json: {...}, meta: {...} }
438
- else if (
439
- decoded.json &&
440
- typeof decoded.json === "object"
441
- ) {
442
- bodyObj = decoded.json;
443
- }
444
- // Format 3 — already unwrapped: { group: {...}, limit: 10 }
445
- // Only use this if it doesn't look like a tRPC envelope
446
- else if (!decoded.meta && !decoded.json) {
447
- bodyObj = decoded;
448
- }
490
+ // Splits batched input into one slot per procedure,
491
+ // in path order — never merges different procedures'
492
+ // fields together.
493
+ bodySlots = splitBatchBody(decoded);
449
494
  }
450
495
  }
451
496
  } catch (e) {
@@ -453,18 +498,29 @@ function attachNextJsInterceptor(client, options) {
453
498
  }
454
499
  }
455
500
 
456
- // ── Normal body processing (unchanged) ────────────────────────────
457
- const parsedBody = structureToJsonSchema(
458
- buildBodyStructure(bodyObj),
459
- );
460
- client
461
- .updateEndpoint({
462
- method: method,
463
- path: normalizedPath,
464
- requestBody: parsedBody,
465
- detectedBy: "runtime",
466
- })
467
- .catch(function () {});
501
+ // Normal (non-GET-input) body — split the same way if the
502
+ // path turned out to be a batch.
503
+ if (!bodySlots) {
504
+ bodySlots =
505
+ splitPaths.length > 1 ? splitBatchBody(bodyObj) : [bodyObj];
506
+ }
507
+
508
+ // Report each real procedure as its own endpoint, with only
509
+ // its own fields — never another procedure's fields mixed in.
510
+ splitPaths.forEach(function (singlePath, i) {
511
+ const slotBody = bodySlots[i] || null;
512
+ const parsedBody = structureToJsonSchema(
513
+ buildBodyStructure(slotBody),
514
+ );
515
+ client
516
+ .updateEndpoint({
517
+ method: method,
518
+ path: singlePath,
519
+ requestBody: parsedBody,
520
+ detectedBy: "runtime",
521
+ })
522
+ .catch(function () {});
523
+ });
468
524
  } catch (e) {}
469
525
  }
470
526
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botversion-sdk",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "BotVersion SDK — auto-detect and register your API endpoints",
5
5
  "main": "index.js",
6
6
  "bin": {