botversion-sdk 1.0.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/client.js +32 -4
- package/index.js +285 -68
- package/interceptor.js +517 -33
- package/package.json +1 -1
- package/scanner.js +1279 -38
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
|
|
@@ -18,6 +41,7 @@ function attachInterceptor(app, client, options) {
|
|
|
18
41
|
"/_next",
|
|
19
42
|
"/static",
|
|
20
43
|
"/public",
|
|
44
|
+
"/admin",
|
|
21
45
|
].concat(options.exclude || []);
|
|
22
46
|
|
|
23
47
|
app.use(function botVersionInterceptor(req, res, next) {
|
|
@@ -49,24 +73,7 @@ function attachInterceptor(app, client, options) {
|
|
|
49
73
|
if (!reportedEndpoints.has(bodyKey)) {
|
|
50
74
|
reportedEndpoints.add(bodyKey);
|
|
51
75
|
|
|
52
|
-
const jsonSchema = bodyStructure
|
|
53
|
-
? {
|
|
54
|
-
type: "object",
|
|
55
|
-
properties: Object.fromEntries(
|
|
56
|
-
Object.entries(bodyStructure).map(function ([key, type]) {
|
|
57
|
-
return [
|
|
58
|
-
key,
|
|
59
|
-
{
|
|
60
|
-
type:
|
|
61
|
-
type === "null" || type === "[redacted]"
|
|
62
|
-
? "string"
|
|
63
|
-
: type,
|
|
64
|
-
},
|
|
65
|
-
];
|
|
66
|
-
}),
|
|
67
|
-
),
|
|
68
|
-
}
|
|
69
|
-
: null;
|
|
76
|
+
const jsonSchema = structureToJsonSchema(bodyStructure);
|
|
70
77
|
|
|
71
78
|
// Report async — never block the request
|
|
72
79
|
setImmediate(function () {
|
|
@@ -77,9 +84,7 @@ function attachInterceptor(app, client, options) {
|
|
|
77
84
|
requestBody: jsonSchema,
|
|
78
85
|
detectedBy: "runtime",
|
|
79
86
|
})
|
|
80
|
-
.catch(function (err) {
|
|
81
|
-
console.error("[botversion] update_endpoint failed:", err.message);
|
|
82
|
-
});
|
|
87
|
+
.catch(function (err) {});
|
|
83
88
|
});
|
|
84
89
|
}
|
|
85
90
|
|
|
@@ -127,6 +132,25 @@ function normalizePath(path) {
|
|
|
127
132
|
.join("/");
|
|
128
133
|
}
|
|
129
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
|
+
|
|
130
154
|
/**
|
|
131
155
|
* Extract just the structure of a request body
|
|
132
156
|
* (keys and value types — never actual values for security)
|
|
@@ -134,6 +158,37 @@ function normalizePath(path) {
|
|
|
134
158
|
function buildBodyStructure(body) {
|
|
135
159
|
if (!body || typeof body !== "object") return null;
|
|
136
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
|
+
|
|
137
192
|
const structure = {};
|
|
138
193
|
|
|
139
194
|
Object.keys(body).forEach(function (key) {
|
|
@@ -164,6 +219,17 @@ function buildBodyStructure(body) {
|
|
|
164
219
|
structure[key] = "array";
|
|
165
220
|
} else if (val === null) {
|
|
166
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";
|
|
167
233
|
} else {
|
|
168
234
|
structure[key] = typeof val;
|
|
169
235
|
}
|
|
@@ -183,7 +249,14 @@ function attachNextJsInterceptor(client, options) {
|
|
|
183
249
|
const method = req.method ? req.method.toUpperCase() : "";
|
|
184
250
|
|
|
185
251
|
const shouldIgnore = (options.exclude || [])
|
|
186
|
-
.concat([
|
|
252
|
+
.concat([
|
|
253
|
+
"/health",
|
|
254
|
+
"/favicon.ico",
|
|
255
|
+
"/_next",
|
|
256
|
+
"/static",
|
|
257
|
+
"/public",
|
|
258
|
+
"/admin",
|
|
259
|
+
])
|
|
187
260
|
.some(function (p) {
|
|
188
261
|
return path.startsWith(p);
|
|
189
262
|
});
|
|
@@ -197,34 +270,445 @@ function attachNextJsInterceptor(client, options) {
|
|
|
197
270
|
if (!reportedEndpoints.has(endpointKey)) {
|
|
198
271
|
reportedEndpoints.add(endpointKey);
|
|
199
272
|
|
|
200
|
-
|
|
273
|
+
// Skip file uploads — too large and not JSON
|
|
274
|
+
const contentType = req.headers["content-type"] || "";
|
|
275
|
+
if (contentType.includes("multipart/form-data")) {
|
|
201
276
|
client
|
|
202
277
|
.updateEndpoint({
|
|
203
278
|
method: method,
|
|
204
279
|
path: normalizedPath,
|
|
280
|
+
requestBody: null,
|
|
205
281
|
detectedBy: "runtime",
|
|
206
282
|
})
|
|
207
|
-
.catch(function (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
283
|
+
.catch(function () {});
|
|
284
|
+
return originalEmit.apply(this, arguments);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Helper to send body structure to platform
|
|
288
|
+
function reportBody(bodyObj) {
|
|
289
|
+
try {
|
|
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
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ── Normal body processing (unchanged) ────────────────────────────
|
|
372
|
+
const parsedBody = structureToJsonSchema(
|
|
373
|
+
buildBodyStructure(bodyObj),
|
|
374
|
+
);
|
|
375
|
+
client
|
|
376
|
+
.updateEndpoint({
|
|
377
|
+
method: method,
|
|
378
|
+
path: normalizedPath,
|
|
379
|
+
requestBody: parsedBody,
|
|
380
|
+
detectedBy: "runtime",
|
|
381
|
+
})
|
|
382
|
+
.catch(function () {});
|
|
383
|
+
} catch (e) {}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// APP ROUTER — req.body is a Web Stream, read it early
|
|
387
|
+
// and put it back as a new stream so App Router can still read it
|
|
388
|
+
if (
|
|
389
|
+
typeof req.body !== "undefined" &&
|
|
390
|
+
req.body &&
|
|
391
|
+
typeof req.body.getReader === "function"
|
|
392
|
+
) {
|
|
393
|
+
try {
|
|
394
|
+
const reader = req.body.getReader();
|
|
395
|
+
const chunks = [];
|
|
396
|
+
function readChunk() {
|
|
397
|
+
reader
|
|
398
|
+
.read()
|
|
399
|
+
.then(function (result) {
|
|
400
|
+
if (result.done) {
|
|
401
|
+
const fullBuffer = Buffer.concat(
|
|
402
|
+
chunks.map(function (c) {
|
|
403
|
+
return Buffer.from(c);
|
|
404
|
+
}),
|
|
405
|
+
);
|
|
406
|
+
// Put the body back as a new ReadableStream
|
|
407
|
+
const { ReadableStream } = require("stream/web");
|
|
408
|
+
req.body = new ReadableStream({
|
|
409
|
+
start: function (controller) {
|
|
410
|
+
controller.enqueue(fullBuffer);
|
|
411
|
+
controller.close();
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
// Parse and report
|
|
415
|
+
try {
|
|
416
|
+
const parsed = JSON.parse(fullBuffer.toString());
|
|
417
|
+
reportBody(parsed);
|
|
418
|
+
} catch (e) {}
|
|
419
|
+
} else {
|
|
420
|
+
chunks.push(result.value);
|
|
421
|
+
readChunk();
|
|
422
|
+
}
|
|
423
|
+
})
|
|
424
|
+
.catch(function () {});
|
|
425
|
+
}
|
|
426
|
+
readChunk();
|
|
427
|
+
} catch (e) {}
|
|
428
|
+
return originalEmit.apply(this, arguments);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// PAGES ROUTER — req.body is populated by Next.js after parsing
|
|
432
|
+
// We intercept res.end because by then req.body is fully available
|
|
433
|
+
const originalResEnd = res.end.bind(res);
|
|
434
|
+
res.end = function (chunk, encoding, callback) {
|
|
435
|
+
reportBody(req.body || null);
|
|
436
|
+
return originalResEnd(chunk, encoding, callback);
|
|
437
|
+
};
|
|
211
438
|
}
|
|
212
439
|
}
|
|
213
440
|
}
|
|
214
441
|
|
|
215
442
|
return originalEmit.apply(this, arguments);
|
|
216
443
|
};
|
|
217
|
-
} catch (err) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
444
|
+
} catch (err) {}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Attaches a Fastify hook that silently intercepts every request
|
|
449
|
+
* and reports new endpoints to BotVersion platform.
|
|
450
|
+
*/
|
|
451
|
+
function attachFastifyInterceptor(fastify, client, options) {
|
|
452
|
+
options = options || {};
|
|
453
|
+
|
|
454
|
+
const ignorePaths = [
|
|
455
|
+
"/health",
|
|
456
|
+
"/favicon.ico",
|
|
457
|
+
"/_next",
|
|
458
|
+
"/static",
|
|
459
|
+
"/public",
|
|
460
|
+
"/admin",
|
|
461
|
+
].concat(options.exclude || []);
|
|
462
|
+
|
|
463
|
+
fastify.addHook("onRequest", async function (request, reply) {
|
|
464
|
+
const path = request.url ? request.url.split("?")[0] : "";
|
|
465
|
+
const method = request.method ? request.method.toUpperCase() : "";
|
|
466
|
+
|
|
467
|
+
const shouldIgnore = ignorePaths.some(function (p) {
|
|
468
|
+
return path.startsWith(p);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
if (shouldIgnore) return;
|
|
472
|
+
|
|
473
|
+
if (options.apiPrefix && !path.startsWith(options.apiPrefix)) return;
|
|
474
|
+
|
|
475
|
+
const normalizedPath = normalizePath(path);
|
|
476
|
+
const endpointKey = method + ":" + normalizedPath;
|
|
477
|
+
|
|
478
|
+
if (!reportedEndpoints.has(endpointKey)) {
|
|
479
|
+
reportedEndpoints.add(endpointKey);
|
|
480
|
+
|
|
481
|
+
setImmediate(function () {
|
|
482
|
+
client
|
|
483
|
+
.updateEndpoint({
|
|
484
|
+
method,
|
|
485
|
+
path: normalizedPath,
|
|
486
|
+
requestBody: null,
|
|
487
|
+
detectedBy: "runtime-fastify",
|
|
488
|
+
})
|
|
489
|
+
.catch(function () {});
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Use onSend hook to capture body after parsing
|
|
495
|
+
fastify.addHook("preHandler", async function (request, reply) {
|
|
496
|
+
const path = request.url ? request.url.split("?")[0] : "";
|
|
497
|
+
const method = request.method ? request.method.toUpperCase() : "";
|
|
498
|
+
const normalizedPath = normalizePath(path);
|
|
499
|
+
const bodyKey = method + ":" + normalizedPath + ":body";
|
|
500
|
+
|
|
501
|
+
if (reportedEndpoints.has(bodyKey)) return;
|
|
502
|
+
reportedEndpoints.add(bodyKey);
|
|
503
|
+
|
|
504
|
+
const body = request.body;
|
|
505
|
+
if (!body) return;
|
|
506
|
+
|
|
507
|
+
const bodyStructure = buildBodyStructure(body);
|
|
508
|
+
if (!bodyStructure) return;
|
|
509
|
+
|
|
510
|
+
const jsonSchema = structureToJsonSchema(bodyStructure);
|
|
511
|
+
|
|
512
|
+
setImmediate(function () {
|
|
513
|
+
client
|
|
514
|
+
.updateEndpoint({
|
|
515
|
+
method,
|
|
516
|
+
path: normalizedPath,
|
|
517
|
+
requestBody: jsonSchema,
|
|
518
|
+
detectedBy: "runtime-fastify",
|
|
519
|
+
})
|
|
520
|
+
.catch(function () {});
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Attaches a Koa middleware that silently intercepts every request
|
|
527
|
+
* and reports new endpoints to BotVersion platform.
|
|
528
|
+
*/
|
|
529
|
+
function attachKoaInterceptor(app, client, options) {
|
|
530
|
+
options = options || {};
|
|
531
|
+
|
|
532
|
+
const ignorePaths = [
|
|
533
|
+
"/health",
|
|
534
|
+
"/favicon.ico",
|
|
535
|
+
"/_next",
|
|
536
|
+
"/static",
|
|
537
|
+
"/public",
|
|
538
|
+
"/admin",
|
|
539
|
+
].concat(options.exclude || []);
|
|
540
|
+
|
|
541
|
+
// IMPORTANT: This interceptor must be added AFTER koa-bodyparser middleware
|
|
542
|
+
// so that ctx.request.body is already populated when we read it.
|
|
543
|
+
app.use(async function botVersionKoaInterceptor(ctx, next) {
|
|
544
|
+
await next();
|
|
545
|
+
|
|
546
|
+
const path = ctx.path || "";
|
|
547
|
+
const method = ctx.method ? ctx.method.toUpperCase() : "";
|
|
548
|
+
|
|
549
|
+
const shouldIgnore = ignorePaths.some(function (p) {
|
|
550
|
+
return path.startsWith(p);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
if (shouldIgnore) return;
|
|
554
|
+
|
|
555
|
+
if (options.apiPrefix && !path.startsWith(options.apiPrefix)) return;
|
|
556
|
+
|
|
557
|
+
const normalizedPath = normalizePath(path);
|
|
558
|
+
const endpointKey = method + ":" + normalizedPath;
|
|
559
|
+
|
|
560
|
+
const body = ctx.request.body;
|
|
561
|
+
const bodyStructure = buildBodyStructure(body);
|
|
562
|
+
const bodyKey =
|
|
563
|
+
endpointKey +
|
|
564
|
+
":" +
|
|
565
|
+
Object.keys(bodyStructure || {})
|
|
566
|
+
.sort()
|
|
567
|
+
.join(",");
|
|
568
|
+
|
|
569
|
+
if (!reportedEndpoints.has(bodyKey)) {
|
|
570
|
+
reportedEndpoints.add(bodyKey);
|
|
571
|
+
|
|
572
|
+
const jsonSchema = structureToJsonSchema(bodyStructure);
|
|
573
|
+
|
|
574
|
+
setImmediate(function () {
|
|
575
|
+
client
|
|
576
|
+
.updateEndpoint({
|
|
577
|
+
method,
|
|
578
|
+
path: normalizedPath,
|
|
579
|
+
requestBody: jsonSchema,
|
|
580
|
+
detectedBy: "runtime-koa",
|
|
581
|
+
})
|
|
582
|
+
.catch(function () {});
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Attaches a Hapi lifecycle extension that silently intercepts every request
|
|
590
|
+
* and reports new endpoints to BotVersion platform.
|
|
591
|
+
*/
|
|
592
|
+
function attachHapiInterceptor(server, client, options) {
|
|
593
|
+
options = options || {};
|
|
594
|
+
|
|
595
|
+
const ignorePaths = [
|
|
596
|
+
"/health",
|
|
597
|
+
"/favicon.ico",
|
|
598
|
+
"/_next",
|
|
599
|
+
"/static",
|
|
600
|
+
"/public",
|
|
601
|
+
"/admin",
|
|
602
|
+
].concat(options.exclude || []);
|
|
603
|
+
|
|
604
|
+
server.ext("onPostAuth", function (request, h) {
|
|
605
|
+
const path = request.path || "";
|
|
606
|
+
const method = request.method ? request.method.toUpperCase() : "";
|
|
607
|
+
|
|
608
|
+
const shouldIgnore = ignorePaths.some(function (p) {
|
|
609
|
+
return path.startsWith(p);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
if (shouldIgnore) return h.continue;
|
|
613
|
+
|
|
614
|
+
if (options.apiPrefix && !path.startsWith(options.apiPrefix))
|
|
615
|
+
return h.continue;
|
|
616
|
+
|
|
617
|
+
const normalizedPath = normalizePath(path);
|
|
618
|
+
const endpointKey = method + ":" + normalizedPath;
|
|
619
|
+
|
|
620
|
+
const body = request.payload;
|
|
621
|
+
const bodyStructure = buildBodyStructure(body);
|
|
622
|
+
const bodyKey =
|
|
623
|
+
endpointKey +
|
|
624
|
+
":" +
|
|
625
|
+
Object.keys(bodyStructure || {})
|
|
626
|
+
.sort()
|
|
627
|
+
.join(",");
|
|
628
|
+
|
|
629
|
+
if (!reportedEndpoints.has(bodyKey)) {
|
|
630
|
+
reportedEndpoints.add(bodyKey);
|
|
631
|
+
|
|
632
|
+
const jsonSchema = structureToJsonSchema(bodyStructure);
|
|
633
|
+
|
|
634
|
+
setImmediate(function () {
|
|
635
|
+
client
|
|
636
|
+
.updateEndpoint({
|
|
637
|
+
method,
|
|
638
|
+
path: normalizedPath,
|
|
639
|
+
requestBody: jsonSchema,
|
|
640
|
+
detectedBy: "runtime-hapi",
|
|
641
|
+
})
|
|
642
|
+
.catch(function () {});
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return h.continue;
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Attaches a NestJS interceptor by patching the underlying http server.
|
|
652
|
+
* NestJS runs on top of Express or Fastify under the hood so we can
|
|
653
|
+
* patch the Node http server the same way we do for Next.js.
|
|
654
|
+
*/
|
|
655
|
+
function attachNestJsInterceptor(client, options) {
|
|
656
|
+
// NestJS runs on Node http server under the hood
|
|
657
|
+
// So we reuse the same approach as Next.js
|
|
658
|
+
attachNextJsInterceptor(client, {
|
|
659
|
+
exclude: (options || {}).exclude || [],
|
|
660
|
+
apiPrefix: (options || {}).apiPrefix || "/",
|
|
661
|
+
debug: (options || {}).debug || false,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* SvelteKit, Nuxt and Remix all run on Node http server
|
|
667
|
+
* so we reuse the Next.js interceptor approach for all of them.
|
|
668
|
+
*/
|
|
669
|
+
function attachSvelteKitInterceptor(client, options) {
|
|
670
|
+
attachNextJsInterceptor(client, {
|
|
671
|
+
exclude: (options || {}).exclude || [],
|
|
672
|
+
apiPrefix: (options || {}).apiPrefix || "/",
|
|
673
|
+
debug: (options || {}).debug || false,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function attachNuxtInterceptor(client, options) {
|
|
678
|
+
attachNextJsInterceptor(client, {
|
|
679
|
+
exclude: (options || {}).exclude || [],
|
|
680
|
+
apiPrefix: (options || {}).apiPrefix || "/api",
|
|
681
|
+
debug: (options || {}).debug || false,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function attachRemixInterceptor(client, options) {
|
|
686
|
+
attachNextJsInterceptor(client, {
|
|
687
|
+
exclude: (options || {}).exclude || [],
|
|
688
|
+
apiPrefix: (options || {}).apiPrefix || "/",
|
|
689
|
+
debug: (options || {}).debug || false,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function attachAdonisInterceptor(client, options) {
|
|
694
|
+
attachNextJsInterceptor(client, {
|
|
695
|
+
exclude: (options || {}).exclude || [],
|
|
696
|
+
apiPrefix: (options || {}).apiPrefix || "/",
|
|
697
|
+
debug: (options || {}).debug || false,
|
|
698
|
+
});
|
|
223
699
|
}
|
|
224
700
|
|
|
225
701
|
module.exports = {
|
|
226
702
|
attachInterceptor,
|
|
227
703
|
attachNextJsInterceptor,
|
|
704
|
+
attachFastifyInterceptor,
|
|
705
|
+
attachKoaInterceptor,
|
|
706
|
+
attachHapiInterceptor,
|
|
707
|
+
attachNestJsInterceptor,
|
|
708
|
+
attachSvelteKitInterceptor,
|
|
709
|
+
attachNuxtInterceptor,
|
|
710
|
+
attachRemixInterceptor,
|
|
711
|
+
attachAdonisInterceptor,
|
|
228
712
|
normalizePath,
|
|
229
713
|
buildBodyStructure,
|
|
230
714
|
};
|