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.
- package/interceptor.js +172 -84
- 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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|