botversion-sdk 2.3.1 → 2.3.3

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/core.js CHANGED
@@ -18,6 +18,20 @@ function pickOptions(appOrOptions, optionsArg) {
18
18
  }
19
19
 
20
20
  function startUndo(options) {
21
+ // DEBUG: debug logs for Undo, on only with debug:true or env BOTVERSION_DEBUG=1
22
+ global._botVersionDebug =
23
+ !!(options && options.debug) || process.env.BOTVERSION_DEBUG === "1";
24
+ if (global._botVersionDebug)
25
+ console.log("[bv-undo-debug] startUndo called", {
26
+ hasOptions: !!options,
27
+ undoSetToFalse: !!options && options.undo === false,
28
+ hasApiKey: !!(options && options.apiKey),
29
+ alreadyStarted: !!global._botVersionUndoStarted,
30
+ onVercel: !!process.env.VERCEL,
31
+ nextRuntime: process.env.NEXT_RUNTIME || null,
32
+ pid: process.pid,
33
+ node: process.version,
34
+ });
21
35
  try {
22
36
  if (!options || options.undo === false || !options.apiKey) {
23
37
  return;
@@ -35,7 +49,13 @@ function startUndo(options) {
35
49
  } else {
36
50
  }
37
51
  undo.setApiKey(options.apiKey);
38
- } catch (e) {}
52
+ if (global._botVersionDebug)
53
+ console.log("[bv-undo-debug] startUndo finished without errors");
54
+ } catch (e) {
55
+ // DEBUG: this error used to be swallowed silently
56
+ if (global._botVersionDebug)
57
+ console.log("[bv-undo-debug] startUndo FAILED:", e && e.message);
58
+ }
39
59
  }
40
60
 
41
61
  function init(appOrOptions, optionsArg) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botversion-sdk",
3
- "version": "2.3.1",
3
+ "version": "2.3.3",
4
4
  "description": "BotVersion SDK: auto-detect and register your API endpoints, with undo for AI actions",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
4
  var path = require("path");
@@ -29,7 +29,7 @@ var BotVersionClient = require("../client");
29
29
 
30
30
  var apiKey = process.env.BOTVERSION_API_KEY;
31
31
  var platformUrl =
32
- process.env.BOTVERSION_PLATFORM_URL || "https://console.botversion.com";
32
+ process.env.BOTVERSION_PLATFORM_URL || "http://localhost:3000";
33
33
 
34
34
  if (!apiKey) {
35
35
  console.error(
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
4
  var path = require("path");
@@ -30,7 +30,7 @@ var sdk = require("../index");
30
30
 
31
31
  var apiKey = process.env.BOTVERSION_API_KEY;
32
32
  var platformUrl =
33
- process.env.BOTVERSION_PLATFORM_URL || "https://console.botversion.com";
33
+ process.env.BOTVERSION_PLATFORM_URL || "http://localhost:3000";
34
34
 
35
35
  if (!apiKey) {
36
36
  console.error(
@@ -11,7 +11,7 @@ const url = require("url");
11
11
  */
12
12
  function BotVersionClient(options) {
13
13
  this.apiKey = options.apiKey;
14
- this.platformUrl = options.platformUrl || "https://console.botversion.com";
14
+ this.platformUrl = options.platformUrl || "http://localhost:3000";
15
15
  this.debug = options.debug || false;
16
16
  this.timeout = options.timeout || 30000;
17
17
 
@@ -439,7 +439,7 @@ var BotVersion = {
439
439
 
440
440
  this._client = new BotVersionClient({
441
441
  apiKey: options.apiKey,
442
- platformUrl: options.platformUrl || "https://console.botversion.com",
442
+ platformUrl: options.platformUrl || "http://localhost:3000",
443
443
  debug: options.debug || false,
444
444
  timeout: options.timeout || 30000,
445
445
  });
package/undo/index.js CHANGED
@@ -27,6 +27,18 @@ const { printReport } = require("./lib/report");
27
27
 
28
28
  let started = false;
29
29
 
30
+ // DEBUG: prints only when debug is on
31
+ const dbg = (...a) => {
32
+ if (global._botVersionDebug) console.log("[bv-undo-debug]", ...a);
33
+ };
34
+ const brief = (v) => {
35
+ try {
36
+ return String(JSON.stringify(v)).slice(0, 150);
37
+ } catch (_) {
38
+ return String(v).slice(0, 150);
39
+ }
40
+ };
41
+
30
42
  /**
31
43
  * @param {object} [options]
32
44
  * @param {string} [options.testHeader] header that marks a request as an "AI request" (default x-botversion-action-id)
@@ -35,7 +47,11 @@ let started = false;
35
47
  */
36
48
  function init(options) {
37
49
  try {
38
- if (started) return { ok: true, alreadyStarted: true };
50
+ if (started) {
51
+ dbg("undo init skipped: already started in this process");
52
+ return { ok: true, alreadyStarted: true };
53
+ }
54
+ dbg("undo init running", { pid: process.pid });
39
55
  started = true;
40
56
 
41
57
  const opts = options && typeof options === "object" ? options : {};
@@ -46,9 +62,21 @@ function init(options) {
46
62
 
47
63
  // 1. Request tagger (framework independent).
48
64
  const taggerTargets = tagger.install();
65
+ dbg("request listener installed on:", taggerTargets, {
66
+ signedIds: tagger.hasApiKey(),
67
+ header: tagger.getHeaderName(),
68
+ });
49
69
 
50
70
  // 2. Look at the host project.
51
71
  const scan = scanner.scan();
72
+ dbg("project scan", {
73
+ hostRoot: scan.hostRoot,
74
+ hasPrisma: !!scan.prisma,
75
+ prismaProvider:
76
+ scan.prisma && scan.prisma.database
77
+ ? scan.prisma.database.provider
78
+ : null,
79
+ });
52
80
 
53
81
  // 3. Database hooks. Only the ones that match what the host has..
54
82
  // (Final SDK: more hooks are added to this same list.)
@@ -65,6 +93,16 @@ function init(options) {
65
93
  : null,
66
94
  );
67
95
 
96
+ dbg("database hooks attached:", {
97
+ pg: brief(pgCopies),
98
+ mysql2: brief(mysql2Copies),
99
+ mongodb: brief(mongodbCopies),
100
+ betterSqlite: brief(betterSqliteCopies),
101
+ sqlite3: brief(sqlite3Copies),
102
+ mssql: brief(mssqlCopies),
103
+ prisma: brief(prismaState),
104
+ });
105
+
68
106
  // Database structure (schema only). Runs AFTER the hooks attach so loading Prisma here cannot make the Prisma hook miss it.
69
107
  scan.structure = scan.prisma
70
108
  ? schemaReader.readPrismaStructure(scan.hostRoot)
@@ -84,8 +122,10 @@ function init(options) {
84
122
  prismaState,
85
123
  });
86
124
 
125
+ dbg("undo init finished without errors");
87
126
  return { ok: true };
88
127
  } catch (e) {
128
+ dbg("undo init FAILED:", e && e.message);
89
129
  return { ok: false };
90
130
  }
91
131
  }
@@ -26,6 +26,13 @@ let counter = 0;
26
26
  let signingKey = null; // derived from the host's API key; the raw key is never kept
27
27
  let lastIgnoredLog = 0;
28
28
 
29
+ // DEBUG: prints only when debug is on
30
+ const dbg = (...a) => {
31
+ if (global._botVersionDebug) console.log("[bv-undo-debug]", ...a);
32
+ };
33
+ const seenEvents = new Set(); // distinct server event names seen (capped)
34
+ let seenRequests = 0;
35
+
29
36
  function setApiKey(key) {
30
37
  try {
31
38
  signingKey = actionId.deriveSigningKey(key);
@@ -43,6 +50,7 @@ function noteIgnored(message) {
43
50
  const now = Date.now();
44
51
  if (now - lastIgnoredLog < 60000) return;
45
52
  lastIgnoredLog = now;
53
+ dbg("request ignored:", message);
46
54
  }
47
55
 
48
56
  function setHeaderName(name) {
@@ -146,6 +154,19 @@ function buildStore(req, res) {
146
154
  res.once("close", () => {
147
155
  try {
148
156
  const ms = Date.now() - store.startedAt;
157
+ // DEBUG: end-of-request summary, shows whether the database hooks saw the queries
158
+ dbg("AI request finished:", {
159
+ id: String(store.actionId).slice(0, 6),
160
+ method: store.method,
161
+ path: store.path,
162
+ status: res.statusCode,
163
+ ms,
164
+ queries: store.queries,
165
+ writes: store.writes,
166
+ savedSnapshots: store.savedIds.length,
167
+ unsaved: store.unsaved,
168
+ blocked: !!store.blocked,
169
+ });
149
170
  } catch (_) {
150
171
  /* never break the host */
151
172
  }
@@ -165,6 +186,7 @@ const OUR_HEADERS = [
165
186
  "x-botversion-requester",
166
187
  "x-botversion-visual",
167
188
  "x-botversion-confirmed",
189
+ "x-botversion-undo",
168
190
  ];
169
191
 
170
192
  // Returns the writeHead arguments, with our header names added to the host's allowed list only when the host already allows this origin.
@@ -268,7 +290,40 @@ function wrapEmit(proto) {
268
290
  const original = proto.emit;
269
291
  const wrapper = function emit(event, req, res) {
270
292
  let store = null;
293
+ // DEBUG: notes each distinct server event once (shows whether this server emits through the patched method)
294
+ if (
295
+ global._botVersionDebug &&
296
+ typeof event === "string" &&
297
+ seenEvents.size < 15 &&
298
+ !seenEvents.has(event)
299
+ ) {
300
+ seenEvents.add(event);
301
+ dbg("server event seen:", event, { pid: process.pid });
302
+ }
271
303
  if (event === "request") {
304
+ // DEBUG: shows whether requests reach this listener at all (first 3 requests, plus every botversion request)
305
+ let bvSeen = false;
306
+ try {
307
+ if (global._botVersionDebug) {
308
+ seenRequests++;
309
+ const hn = Object.keys((req && req.headers) || {}).filter(
310
+ (k) => k.indexOf("x-botversion-") === 0,
311
+ );
312
+ const p = String((req && req.url) || "")
313
+ .split("?")[0]
314
+ .slice(0, 120);
315
+ bvSeen = hn.length > 0 || p.indexOf("botversion") !== -1;
316
+ if (seenRequests <= 3 || bvSeen)
317
+ dbg("request seen:", {
318
+ n: seenRequests,
319
+ method: req && req.method,
320
+ path: p,
321
+ botversionHeaders: hn,
322
+ hasSigningKey: !!signingKey,
323
+ pid: process.pid,
324
+ });
325
+ }
326
+ } catch (_) {}
272
327
  try {
273
328
  // The Undo route is answered here, before the host's framework sees the request.
274
329
  if (
@@ -283,6 +338,13 @@ function wrapEmit(proto) {
283
338
  guardPreflight(req, res); // never throws; only touches preflight requests that ask for our headers
284
339
  try {
285
340
  store = buildStore(req, res);
341
+ // DEBUG: shows whether this request was accepted as an AI request
342
+ if (bvSeen)
343
+ dbg(
344
+ "accepted as AI request:",
345
+ !!store,
346
+ store ? { visual: store.visual, confirmed: store.confirmed } : "",
347
+ );
286
348
  } catch (_) {
287
349
  store = null; // tagging problems must never affect the host
288
350
  }
@@ -327,7 +389,9 @@ function install() {
327
389
  for (const proto of protos) {
328
390
  if (wrapEmit(proto) && !wrapped.includes(name)) wrapped.push(name);
329
391
  }
330
- } catch (e) {}
392
+ } catch (e) {
393
+ dbg("could not wrap", name, ":", e && e.message);
394
+ }
331
395
  }
332
396
  return wrapped;
333
397
  }
@@ -15,6 +15,11 @@ const undoMssql = require("./undo-mssql");
15
15
  const PATH = "/botversion-undo";
16
16
  const NOTE = "Emails, payments and other outside actions cannot be undone.";
17
17
 
18
+ // DEBUG: prints only when debug is on
19
+ const dbg = (...a) => {
20
+ if (global._botVersionDebug) console.log("[bv-undo-debug]", ...a);
21
+ };
22
+
18
23
  const STATUS_PATH = "/botversion-undo-status";
19
24
 
20
25
  // true only when something saved for this action can still be undone by this browser session.
@@ -33,6 +38,7 @@ async function statusOf(id, hash) {
33
38
  ]) {
34
39
  try {
35
40
  const ok = await u.statusAction(id, hash);
41
+ dbg("status check", name, "->", ok);
36
42
  if (ok) return true;
37
43
  } catch (e) {
38
44
  if (!(e instanceof undoPrisma.Refuse && e.code === "no-client"))
@@ -57,9 +63,15 @@ const CORS = {
57
63
  function send(res, status, obj) {
58
64
  try {
59
65
  res.statusCode = status;
66
+ dbg(
67
+ "undo route answered:",
68
+ status,
69
+ String((obj && (obj.code || obj.message)) || "").slice(0, 100),
70
+ );
60
71
  for (const k of Object.keys(CORS)) res.setHeader(k, CORS[k]);
61
72
  res.setHeader("Content-Type", "application/json");
62
- res.end(JSON.stringify(obj));
73
+ // marker lets the widget tell our answer apart from the host's own JSON
74
+ res.end(JSON.stringify(Object.assign({ botversion: true }, obj)));
63
75
  } catch (_) {
64
76
  /* ignore */
65
77
  }
@@ -68,8 +80,18 @@ function send(res, status, obj) {
68
80
  // Returns true when it answered the request.
69
81
  function handle(req, res, isValidId) {
70
82
  const urlPath = String(req.url || "").split("?")[0];
71
- const isStatus = urlPath === STATUS_PATH;
72
- if (urlPath !== PATH && !isStatus) return false;
83
+ // Also answers a request carrying the Undo label on any existing host address (serverless hosts never pass unknown addresses to the SDK).
84
+ const labelRaw = req.headers && req.headers["x-botversion-undo"];
85
+ const label = Array.isArray(labelRaw) ? labelRaw[0] : labelRaw;
86
+ const byLabel =
87
+ req.method === "POST" && (label === "status" || label === "undo");
88
+ const isStatus = byLabel ? label === "status" : urlPath === STATUS_PATH;
89
+ if (!byLabel && urlPath !== PATH && !isStatus) return false;
90
+ dbg("undo route matched:", {
91
+ method: req.method,
92
+ path: urlPath,
93
+ label: label || null,
94
+ });
73
95
  // Browser permission check (preflight) for cross-domain hosts.
74
96
  if (req.method === "OPTIONS") {
75
97
  try {