botversion-sdk 1.1.6 → 1.1.7

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 +172 -84
  2. package/package.json +1 -1
package/interceptor.js CHANGED
@@ -3,6 +3,29 @@
3
3
 
4
4
  const reportedEndpoints = new Set();
5
5
 
6
+ function structureToJsonSchema(bodyStructure) {
7
+ if (!bodyStructure) return null;
8
+ return {
9
+ type: "object",
10
+ properties: Object.fromEntries(
11
+ Object.entries(bodyStructure).map(function ([key, typeOrObj]) {
12
+ if (typeOrObj && typeof typeOrObj === "object" && typeOrObj.type) {
13
+ return [key, typeOrObj];
14
+ }
15
+ return [
16
+ key,
17
+ {
18
+ type:
19
+ typeOrObj === "null" || typeOrObj === "[redacted]"
20
+ ? "string"
21
+ : typeOrObj,
22
+ },
23
+ ];
24
+ }),
25
+ ),
26
+ };
27
+ }
28
+
6
29
  /**
7
30
  * Attaches a middleware to the Express app that
8
31
  * silently intercepts every request and reports
@@ -50,24 +73,7 @@ function attachInterceptor(app, client, options) {
50
73
  if (!reportedEndpoints.has(bodyKey)) {
51
74
  reportedEndpoints.add(bodyKey);
52
75
 
53
- const jsonSchema = bodyStructure
54
- ? {
55
- type: "object",
56
- properties: Object.fromEntries(
57
- Object.entries(bodyStructure).map(function ([key, type]) {
58
- return [
59
- key,
60
- {
61
- type:
62
- type === "null" || type === "[redacted]"
63
- ? "string"
64
- : type,
65
- },
66
- ];
67
- }),
68
- ),
69
- }
70
- : null;
76
+ const jsonSchema = structureToJsonSchema(bodyStructure);
71
77
 
72
78
  // Report async — never block the request
73
79
  setImmediate(function () {
@@ -126,6 +132,25 @@ function normalizePath(path) {
126
132
  .join("/");
127
133
  }
128
134
 
135
+ /**
136
+ * Unwraps a single (non-batched) tRPC/superjson envelope.
137
+ * tRPC sends single calls as { json: {...realFields...} }
138
+ * or with superjson as { json: {...realFields...}, meta: {...} }
139
+ */
140
+ function unwrapTrpcJsonEnvelope(obj) {
141
+ if (
142
+ obj &&
143
+ typeof obj === "object" &&
144
+ !Array.isArray(obj) &&
145
+ obj.json &&
146
+ typeof obj.json === "object" &&
147
+ !Array.isArray(obj.json)
148
+ ) {
149
+ return obj.json;
150
+ }
151
+ return obj;
152
+ }
153
+
129
154
  /**
130
155
  * Extract just the structure of a request body
131
156
  * (keys and value types — never actual values for security)
@@ -133,6 +158,37 @@ function normalizePath(path) {
133
158
  function buildBodyStructure(body) {
134
159
  if (!body || typeof body !== "object") return null;
135
160
 
161
+ // Unwrap tRPC envelope
162
+ // Case A: single tRPC call — { json: { ...realFields... } } (optionally with a "meta" sibling)
163
+ // Case B: batched tRPC call — { "0": { json: {...} }, "1": { json: {...} } } or { "0": {...realFields...} }
164
+ // We detect whichever shape is present and flatten it to the real fields before processing
165
+ let keys = Object.keys(body);
166
+
167
+ const isTrpcEnvelope =
168
+ keys.length > 0 &&
169
+ keys.every(function (k) {
170
+ return (
171
+ /^\d+$/.test(k) &&
172
+ body[k] !== null &&
173
+ typeof body[k] === "object" &&
174
+ !Array.isArray(body[k])
175
+ );
176
+ });
177
+
178
+ if (isTrpcEnvelope) {
179
+ const unwrapped = {};
180
+ keys.forEach(function (k) {
181
+ // Each batched entry may itself be a { json: {...} } envelope
182
+ Object.assign(unwrapped, unwrapTrpcJsonEnvelope(body[k]));
183
+ });
184
+ body = unwrapped;
185
+ } else {
186
+ // Not a numeric batch — check for the single { json: {...} } shape
187
+ body = unwrapTrpcJsonEnvelope(body);
188
+ }
189
+
190
+ keys = Object.keys(body);
191
+
136
192
  const structure = {};
137
193
 
138
194
  Object.keys(body).forEach(function (key) {
@@ -163,6 +219,17 @@ function buildBodyStructure(body) {
163
219
  structure[key] = "array";
164
220
  } else if (val === null) {
165
221
  structure[key] = "null";
222
+ } else if (typeof val === "object") {
223
+ const nestedProps = {};
224
+ Object.keys(val).forEach(function (nk) {
225
+ nestedProps[nk] = {
226
+ type: val[nk] === null ? "string" : typeof val[nk],
227
+ };
228
+ });
229
+ structure[key] =
230
+ Object.keys(nestedProps).length > 0
231
+ ? { type: "object", properties: nestedProps }
232
+ : "object";
166
233
  } else {
167
234
  structure[key] = typeof val;
168
235
  }
@@ -220,24 +287,91 @@ function attachNextJsInterceptor(client, options) {
220
287
  // Helper to send body structure to platform
221
288
  function reportBody(bodyObj) {
222
289
  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
- };
290
+ // ── tRPC GET input extraction ──────────────────────────────────────
291
+ // tRPC GET requests send their input as a URL-encoded JSON query
292
+ // parameter called "input". The body is always empty for GET requests.
293
+ // We only attempt this extraction when:
294
+ // 1. The body is empty (GET requests have no body)
295
+ // 2. The URL contains an "input" query parameter
296
+ // 3. The path looks like a tRPC endpoint (/trpc/)
297
+ // This prevents accidentally treating regular REST query params
298
+ // (e.g. ?filter=active&limit=10) as a body schema.
299
+ const isTrpcPath = path.includes("/trpc/");
300
+ const hasInputParam = req.url && req.url.includes("input=");
301
+
302
+ if (
303
+ isTrpcPath &&
304
+ hasInputParam &&
305
+ (!bodyObj || Object.keys(bodyObj).length === 0)
306
+ ) {
307
+ try {
308
+ // Use URL constructor to safely parse query params
309
+ const urlObj = new URL(req.url, "http://localhost");
310
+ const inputParam = urlObj.searchParams.get("input");
311
+
312
+ if (inputParam) {
313
+ let decoded;
314
+ try {
315
+ decoded = JSON.parse(decodeURIComponent(inputParam));
316
+ } catch (e) {
317
+ // input param exists but is not valid JSON — skip
318
+ decoded = null;
319
+ }
320
+
321
+ if (
322
+ decoded &&
323
+ typeof decoded === "object" &&
324
+ !Array.isArray(decoded)
325
+ ) {
326
+ const keys = Object.keys(decoded);
327
+
328
+ // Format 1 — batched: { "0": { json: {...} }, "1": { json: {...} } }
329
+ // All keys are numeric strings
330
+ const isBatch =
331
+ keys.length > 0 && keys.every((k) => /^\d+$/.test(k));
332
+
333
+ if (isBatch) {
334
+ // Merge all batch entries into one flat object
335
+ // so we capture the full schema across all procedures
336
+ const merged = {};
337
+ keys.forEach((k) => {
338
+ const entry = decoded[k];
339
+ if (entry && typeof entry === "object") {
340
+ // Unwrap { json: {...} } envelope if present
341
+ const unwrapped =
342
+ entry.json && typeof entry.json === "object"
343
+ ? entry.json
344
+ : entry;
345
+ Object.assign(merged, unwrapped);
346
+ }
347
+ });
348
+ if (Object.keys(merged).length > 0) {
349
+ bodyObj = merged;
350
+ }
351
+ }
352
+ // Format 2 — single with superjson: { json: {...}, meta: {...} }
353
+ else if (
354
+ decoded.json &&
355
+ typeof decoded.json === "object"
356
+ ) {
357
+ bodyObj = decoded.json;
358
+ }
359
+ // Format 3 — already unwrapped: { group: {...}, limit: 10 }
360
+ // Only use this if it doesn't look like a tRPC envelope
361
+ else if (!decoded.meta && !decoded.json) {
362
+ bodyObj = decoded;
363
+ }
364
+ }
365
+ }
366
+ } catch (e) {
367
+ // URL parsing failed — fall through to normal body handling
368
+ }
240
369
  }
370
+
371
+ // ── Normal body processing (unchanged) ────────────────────────────
372
+ const parsedBody = structureToJsonSchema(
373
+ buildBodyStructure(bodyObj),
374
+ );
241
375
  client
242
376
  .updateEndpoint({
243
377
  method: method,
@@ -373,19 +507,7 @@ function attachFastifyInterceptor(fastify, client, options) {
373
507
  const bodyStructure = buildBodyStructure(body);
374
508
  if (!bodyStructure) return;
375
509
 
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
- };
510
+ const jsonSchema = structureToJsonSchema(bodyStructure);
389
511
 
390
512
  setImmediate(function () {
391
513
  client
@@ -447,24 +569,7 @@ function attachKoaInterceptor(app, client, options) {
447
569
  if (!reportedEndpoints.has(bodyKey)) {
448
570
  reportedEndpoints.add(bodyKey);
449
571
 
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;
572
+ const jsonSchema = structureToJsonSchema(bodyStructure);
468
573
 
469
574
  setImmediate(function () {
470
575
  client
@@ -524,24 +629,7 @@ function attachHapiInterceptor(server, client, options) {
524
629
  if (!reportedEndpoints.has(bodyKey)) {
525
630
  reportedEndpoints.add(bodyKey);
526
631
 
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;
632
+ const jsonSchema = structureToJsonSchema(bodyStructure);
545
633
 
546
634
  setImmediate(function () {
547
635
  client
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botversion-sdk",
3
- "version": "1.1.6",
3
+ "version": "1.1.7",
4
4
  "description": "BotVersion SDK — auto-detect and register your API endpoints",
5
5
  "main": "index.js",
6
6
  "bin": {