botversion-sdk 1.0.3 → 1.0.5
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/bin/init.js +184 -135
- package/cli/detector.js +360 -173
- package/cli/generator.js +18 -476
- package/cli/prompts.js +40 -42
- package/cli/writer.js +118 -42
- package/client.js +214 -36
- package/index.js +74 -391
- package/interceptor.js +112 -124
- package/package.json +3 -2
- package/scanner.js +792 -27
package/interceptor.js
CHANGED
|
@@ -1,16 +1,119 @@
|
|
|
1
1
|
// botversion-sdk/interceptor.js
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Makes an internal HTTP request to the user's own backend.
|
|
6
|
+
* Forwards cookies, auth headers, and CSRF tokens.
|
|
7
|
+
*/
|
|
8
|
+
function makeInternalRequest(method, path, body, cookies, headers, baseUrl) {
|
|
9
|
+
return new Promise(function (resolve) {
|
|
10
|
+
var http = require("http");
|
|
11
|
+
var https = require("https");
|
|
12
|
+
var url = require("url");
|
|
13
|
+
|
|
14
|
+
baseUrl = baseUrl || "http://127.0.0.1:8000";
|
|
15
|
+
var fullUrl = baseUrl + path;
|
|
16
|
+
var parsedUrl = url.parse(fullUrl);
|
|
17
|
+
var isHttps = parsedUrl.protocol === "https:";
|
|
18
|
+
var lib = isHttps ? https : http;
|
|
19
|
+
|
|
20
|
+
var bodyStr = body ? JSON.stringify(body) : null;
|
|
21
|
+
|
|
22
|
+
var reqHeaders = {
|
|
23
|
+
"Content-Type": "application/json",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if (bodyStr) {
|
|
27
|
+
reqHeaders["Content-Length"] = String(Buffer.byteLength(bodyStr));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Forward cookies — this is what identifies the user
|
|
31
|
+
if (cookies) reqHeaders["Cookie"] = cookies;
|
|
32
|
+
|
|
33
|
+
// Forward auth header
|
|
34
|
+
var authHeader =
|
|
35
|
+
headers && (headers["authorization"] || headers["Authorization"]);
|
|
36
|
+
if (authHeader) reqHeaders["Authorization"] = authHeader;
|
|
37
|
+
|
|
38
|
+
// Forward CSRF token
|
|
39
|
+
var csrf =
|
|
40
|
+
headers &&
|
|
41
|
+
(headers["x-csrftoken"] ||
|
|
42
|
+
headers["X-CSRFToken"] ||
|
|
43
|
+
headers["x-xsrf-token"] ||
|
|
44
|
+
headers["X-XSRF-TOKEN"]);
|
|
45
|
+
if (csrf) reqHeaders["X-CSRFToken"] = csrf;
|
|
46
|
+
|
|
47
|
+
var reqOptions = {
|
|
48
|
+
hostname: parsedUrl.hostname,
|
|
49
|
+
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
50
|
+
path: parsedUrl.path,
|
|
51
|
+
method: method.toUpperCase(),
|
|
52
|
+
headers: reqHeaders,
|
|
53
|
+
timeout: 30000,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
var req = lib.request(reqOptions, function (res) {
|
|
57
|
+
var data = "";
|
|
58
|
+
res.on("data", function (chunk) {
|
|
59
|
+
data += chunk;
|
|
60
|
+
});
|
|
61
|
+
res.on("end", function () {
|
|
62
|
+
var parsed;
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(data);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
parsed = { raw: data };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Handle redirects
|
|
70
|
+
if (
|
|
71
|
+
res.statusCode >= 300 &&
|
|
72
|
+
res.statusCode < 400 &&
|
|
73
|
+
res.headers.location
|
|
74
|
+
) {
|
|
75
|
+
makeInternalRequest(
|
|
76
|
+
res.statusCode === 303 ? "GET" : method,
|
|
77
|
+
res.headers.location,
|
|
78
|
+
res.statusCode === 303 ? null : body,
|
|
79
|
+
cookies,
|
|
80
|
+
headers,
|
|
81
|
+
baseUrl,
|
|
82
|
+
).then(resolve);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
resolve({
|
|
87
|
+
status: res.statusCode,
|
|
88
|
+
ok: res.statusCode >= 200 && res.statusCode < 300,
|
|
89
|
+
data: parsed,
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
req.on("error", function (err) {
|
|
95
|
+
resolve({ status: 500, ok: false, data: { error: err.message } });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
req.on("timeout", function () {
|
|
99
|
+
req.destroy();
|
|
100
|
+
resolve({ status: 500, ok: false, data: { error: "Request timed out" } });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (bodyStr) req.write(bodyStr);
|
|
104
|
+
req.end();
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
4
108
|
/**
|
|
5
109
|
* Attaches a middleware to the Express app that
|
|
6
110
|
* silently intercepts every request and reports
|
|
7
|
-
* new endpoints to BotVersion platform
|
|
111
|
+
* new endpoints to BotVersion platform.
|
|
112
|
+
* Auth and user context are handled client-side — not here.
|
|
8
113
|
*/
|
|
9
114
|
function attachInterceptor(app, client, options) {
|
|
10
115
|
options = options || {};
|
|
11
|
-
var getUserContext = options.getUserContext || null;
|
|
12
116
|
|
|
13
|
-
// Paths to always ignore
|
|
14
117
|
const ignorePaths = [
|
|
15
118
|
"/health",
|
|
16
119
|
"/favicon.ico",
|
|
@@ -20,11 +123,9 @@ function attachInterceptor(app, client, options) {
|
|
|
20
123
|
].concat(options.exclude || []);
|
|
21
124
|
|
|
22
125
|
// Track which endpoints we have already reported
|
|
23
|
-
// so we don't spam the platform on every request
|
|
24
126
|
const reportedEndpoints = new Set();
|
|
25
127
|
|
|
26
128
|
app.use(function botVersionInterceptor(req, res, next) {
|
|
27
|
-
// Skip ignored paths
|
|
28
129
|
const path = req.path || req.url || "";
|
|
29
130
|
const shouldIgnore = ignorePaths.some(function (p) {
|
|
30
131
|
return path.startsWith(p);
|
|
@@ -34,21 +135,15 @@ function attachInterceptor(app, client, options) {
|
|
|
34
135
|
return next();
|
|
35
136
|
}
|
|
36
137
|
|
|
37
|
-
// Skip non-API paths if configured
|
|
38
138
|
if (options.apiPrefix && !path.startsWith(options.apiPrefix)) {
|
|
39
139
|
return next();
|
|
40
140
|
}
|
|
41
141
|
|
|
42
142
|
const method = req.method.toUpperCase();
|
|
43
|
-
|
|
44
|
-
// Normalize path — replace IDs with :param
|
|
45
143
|
const normalizedPath = normalizePath(path);
|
|
46
144
|
const endpointKey = method + ":" + normalizedPath;
|
|
47
145
|
|
|
48
|
-
// Only report if we haven't seen this endpoint before
|
|
49
146
|
const bodyStructure = buildBodyStructure(req.body);
|
|
50
|
-
console.log("[DEBUG] endpoint:", endpointKey);
|
|
51
|
-
console.log("[DEBUG] bodyStructure:", JSON.stringify(bodyStructure));
|
|
52
147
|
const bodyKey =
|
|
53
148
|
endpointKey +
|
|
54
149
|
":" +
|
|
@@ -59,7 +154,6 @@ function attachInterceptor(app, client, options) {
|
|
|
59
154
|
if (!reportedEndpoints.has(bodyKey)) {
|
|
60
155
|
reportedEndpoints.add(bodyKey);
|
|
61
156
|
|
|
62
|
-
// Convert to JSON Schema format so Gemini can read field names
|
|
63
157
|
const jsonSchema = bodyStructure
|
|
64
158
|
? {
|
|
65
159
|
type: "object",
|
|
@@ -78,9 +172,8 @@ function attachInterceptor(app, client, options) {
|
|
|
78
172
|
),
|
|
79
173
|
}
|
|
80
174
|
: null;
|
|
81
|
-
console.log("[DEBUG] jsonSchema:", JSON.stringify(jsonSchema));
|
|
82
175
|
|
|
83
|
-
// Report async —
|
|
176
|
+
// Report async — never block the request
|
|
84
177
|
setImmediate(function () {
|
|
85
178
|
client
|
|
86
179
|
.updateEndpoint({
|
|
@@ -90,7 +183,6 @@ function attachInterceptor(app, client, options) {
|
|
|
90
183
|
detectedBy: "runtime",
|
|
91
184
|
})
|
|
92
185
|
.catch(function (err) {
|
|
93
|
-
// Silent fail — never crash customer's app
|
|
94
186
|
if (options.debug) {
|
|
95
187
|
console.warn(
|
|
96
188
|
"[BotVersion SDK] Failed to report endpoint:",
|
|
@@ -103,52 +195,10 @@ function attachInterceptor(app, client, options) {
|
|
|
103
195
|
|
|
104
196
|
next();
|
|
105
197
|
});
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
: extractDefaultContext(req);
|
|
111
|
-
|
|
112
|
-
return client
|
|
113
|
-
.agentChat({
|
|
114
|
-
message: req.body.message,
|
|
115
|
-
conversationHistory: req.body.conversationHistory || [],
|
|
116
|
-
pageContext: req.body.pageContext || {},
|
|
117
|
-
userContext: userContext,
|
|
118
|
-
})
|
|
119
|
-
.then(function (response) {
|
|
120
|
-
// Handle plain chat/greeting responses from ai-response.js
|
|
121
|
-
if (response.answer) {
|
|
122
|
-
return { action: "RESPOND", message: response.answer };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Handle agent action
|
|
126
|
-
if (response.action !== "EXECUTE_CALL") return response;
|
|
127
|
-
|
|
128
|
-
return makeLocalCall(req, response.call).then(function (result) {
|
|
129
|
-
return client
|
|
130
|
-
.agentToolResult(
|
|
131
|
-
response.sessionToken,
|
|
132
|
-
result,
|
|
133
|
-
response.sessionData,
|
|
134
|
-
)
|
|
135
|
-
.then(function (toolResponse) {
|
|
136
|
-
if (toolResponse.action === "EXECUTE_CALL") {
|
|
137
|
-
return makeLocalCall(req, toolResponse.call).then(
|
|
138
|
-
function (result2) {
|
|
139
|
-
return client.agentToolResult(
|
|
140
|
-
toolResponse.sessionToken,
|
|
141
|
-
result2,
|
|
142
|
-
toolResponse.sessionData,
|
|
143
|
-
);
|
|
144
|
-
},
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
return toolResponse;
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
};
|
|
198
|
+
// Register executor so WebSocket can forward calls to user's backend
|
|
199
|
+
client.setExecutor(function (method, path, body, cookies, headers, baseUrl) {
|
|
200
|
+
return makeInternalRequest(method, path, body, cookies, headers, baseUrl);
|
|
201
|
+
});
|
|
152
202
|
}
|
|
153
203
|
|
|
154
204
|
/**
|
|
@@ -172,7 +222,7 @@ function normalizePath(path) {
|
|
|
172
222
|
// Numeric ID
|
|
173
223
|
if (/^\d+$/.test(segment)) return ":id";
|
|
174
224
|
|
|
175
|
-
// cuid pattern
|
|
225
|
+
// cuid pattern
|
|
176
226
|
if (/^c[a-z0-9]{20,}$/i.test(segment)) return ":id";
|
|
177
227
|
|
|
178
228
|
// MongoDB ObjectId
|
|
@@ -201,7 +251,6 @@ function buildBodyStructure(body) {
|
|
|
201
251
|
const structure = {};
|
|
202
252
|
|
|
203
253
|
Object.keys(body).forEach(function (key) {
|
|
204
|
-
// Never store sensitive values
|
|
205
254
|
const sensitiveKeys = [
|
|
206
255
|
"password",
|
|
207
256
|
"token",
|
|
@@ -242,64 +291,3 @@ module.exports = {
|
|
|
242
291
|
normalizePath,
|
|
243
292
|
buildBodyStructure,
|
|
244
293
|
};
|
|
245
|
-
|
|
246
|
-
// Default userContext extraction — reads common patterns
|
|
247
|
-
// SaaS owner can override this with getUserContext option
|
|
248
|
-
function extractDefaultContext(req) {
|
|
249
|
-
var user = req.user || (req.session && req.session.user) || {};
|
|
250
|
-
return {
|
|
251
|
-
userId: user.id || user.userId || user._id || null,
|
|
252
|
-
email: user.email || null,
|
|
253
|
-
role: user.role || user.userRole || null,
|
|
254
|
-
name: user.name || user.username || null,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Makes the HTTP call locally on the SaaS owner's server
|
|
259
|
-
// forwarding the user's real auth headers
|
|
260
|
-
function makeLocalCall(req, call) {
|
|
261
|
-
return new Promise(function (resolve) {
|
|
262
|
-
var http = require("http");
|
|
263
|
-
var https = require("https");
|
|
264
|
-
|
|
265
|
-
var body = call.body ? JSON.stringify(call.body) : null;
|
|
266
|
-
var isHttps = req.protocol === "https" || req.secure;
|
|
267
|
-
var lib = isHttps ? https : http;
|
|
268
|
-
|
|
269
|
-
var options = {
|
|
270
|
-
hostname: "127.0.0.1",
|
|
271
|
-
port: process.env.PORT || 3000,
|
|
272
|
-
path: call.path,
|
|
273
|
-
method: call.method,
|
|
274
|
-
headers: {
|
|
275
|
-
"Content-Type": "application/json",
|
|
276
|
-
// Forward the user's real auth token — this is the key part
|
|
277
|
-
Authorization: req.headers["authorization"] || "",
|
|
278
|
-
Cookie: req.headers["cookie"] || "",
|
|
279
|
-
},
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
if (body) options.headers["Content-Length"] = Buffer.byteLength(body);
|
|
283
|
-
|
|
284
|
-
var apiReq = lib.request(options, function (apiRes) {
|
|
285
|
-
var data = "";
|
|
286
|
-
apiRes.on("data", function (chunk) {
|
|
287
|
-
data += chunk;
|
|
288
|
-
});
|
|
289
|
-
apiRes.on("end", function () {
|
|
290
|
-
try {
|
|
291
|
-
resolve({ status: apiRes.statusCode, data: JSON.parse(data) });
|
|
292
|
-
} catch {
|
|
293
|
-
resolve({ status: apiRes.statusCode, data: { raw: data } });
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
apiReq.on("error", function (err) {
|
|
299
|
-
resolve({ status: 500, error: err.message });
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
if (body) apiReq.write(body);
|
|
303
|
-
apiReq.end();
|
|
304
|
-
});
|
|
305
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "botversion-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "BotVersion SDK — auto-detect and register your API endpoints",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"author": "Saurav Dhakal",
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"express": "^4.18.2"
|
|
19
|
+
"express": "^4.18.2",
|
|
20
|
+
"ws": "^8.20.0"
|
|
20
21
|
}
|
|
21
22
|
}
|