@botim/mp-debug-sdk 0.4.1 → 0.6.1

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/dist/index.d.cts CHANGED
@@ -52,6 +52,36 @@ interface BuiltinHostHooks {
52
52
  * The SDK enforces a 1 MB base64/JSON cap regardless.
53
53
  */
54
54
  screenshot?: () => ScreenshotResult | Promise<ScreenshotResult>;
55
+ /**
56
+ * Default-enabled remote-eval command (`exec`). Pass `false` to opt out
57
+ * of registration entirely; pass an object to inject extra locals into
58
+ * every snippet's scope.
59
+ *
60
+ * builtins: { exec: false }
61
+ * → exec NOT registered; agents calling exec see `unknown-command`.
62
+ *
63
+ * builtins: { exec: { globals: { app, store } } }
64
+ * → exec registered; snippets can reference `app` and `store` as
65
+ * bare identifiers. Default locals (`BOT`, `window`, `document`)
66
+ * are always present.
67
+ *
68
+ * builtins.exec is undefined / not set
69
+ * → default behaviour: exec registered with default locals only.
70
+ *
71
+ * See `openspec/changes/add-default-exec-builtin/design.md` for the
72
+ * security rationale (no prod-specific gating in MVP; relies on the
73
+ * existing `enableRemoteDebug` consent gate).
74
+ */
75
+ exec?: false | ExecOptions;
76
+ }
77
+ interface ExecOptions {
78
+ /**
79
+ * Additional locals bound into every snippet's scope. Keys MUST be
80
+ * valid identifier names (the SDK does not validate this — passing a
81
+ * key like "1foo" or "a-b" will throw at AsyncFunction construction
82
+ * time and surface as a `command-rejected`).
83
+ */
84
+ globals?: Record<string, unknown>;
55
85
  }
56
86
  type ScreenshotResult = string | {
57
87
  data: string;
@@ -71,6 +101,7 @@ type ScreenshotResult = string | {
71
101
  */
72
102
  | 'html-snapshot';
73
103
  };
104
+ declare function getBOT(): unknown;
74
105
 
75
106
  /**
76
107
  * Per-signature sliding-window event deduper.
@@ -211,4 +242,4 @@ declare const DEFAULT_BUFFER_SIZE = 1000;
211
242
  declare const DEFAULT_MAX_BATCH_SIZE = 50;
212
243
  declare function enableRemoteDebug(options: RemoteDebugOptions): Promise<RemoteDebugHandle>;
213
244
 
214
- export { type AppInfo, BotimConfig, BotimConfigError, BotimConsentError, type BuiltinHostHooks, CommandHandler, ConsentInput, ConsolePayload, DEFAULT_BODY_PATTERNS, DEFAULT_BUFFER_SIZE, DEFAULT_FLUSH_INTERVAL_MS, DEFAULT_MAX_BATCH_SIZE, DEFAULT_MAX_BODY_BYTES, DEFAULT_REDACT_HEADERS, type DedupOptions, DeviceInfo, ErrorPayload, EventLevel, EventType, NetworkPayload, type RedactionConfig, type RemoteDebugHandle, type RemoteDebugOptions, type SamplingConfig, type SuppressionSummary, enableRemoteDebug };
245
+ export { type AppInfo, BotimConfig, BotimConfigError, BotimConsentError, type BuiltinHostHooks, CommandHandler, ConsentInput, ConsolePayload, DEFAULT_BODY_PATTERNS, DEFAULT_BUFFER_SIZE, DEFAULT_FLUSH_INTERVAL_MS, DEFAULT_MAX_BATCH_SIZE, DEFAULT_MAX_BODY_BYTES, DEFAULT_REDACT_HEADERS, type DedupOptions, DeviceInfo, ErrorPayload, EventLevel, EventType, type ExecOptions, NetworkPayload, type RedactionConfig, type RemoteDebugHandle, type RemoteDebugOptions, type SamplingConfig, type SuppressionSummary, enableRemoteDebug, getBOT };
package/dist/index.d.ts CHANGED
@@ -52,6 +52,36 @@ interface BuiltinHostHooks {
52
52
  * The SDK enforces a 1 MB base64/JSON cap regardless.
53
53
  */
54
54
  screenshot?: () => ScreenshotResult | Promise<ScreenshotResult>;
55
+ /**
56
+ * Default-enabled remote-eval command (`exec`). Pass `false` to opt out
57
+ * of registration entirely; pass an object to inject extra locals into
58
+ * every snippet's scope.
59
+ *
60
+ * builtins: { exec: false }
61
+ * → exec NOT registered; agents calling exec see `unknown-command`.
62
+ *
63
+ * builtins: { exec: { globals: { app, store } } }
64
+ * → exec registered; snippets can reference `app` and `store` as
65
+ * bare identifiers. Default locals (`BOT`, `window`, `document`)
66
+ * are always present.
67
+ *
68
+ * builtins.exec is undefined / not set
69
+ * → default behaviour: exec registered with default locals only.
70
+ *
71
+ * See `openspec/changes/add-default-exec-builtin/design.md` for the
72
+ * security rationale (no prod-specific gating in MVP; relies on the
73
+ * existing `enableRemoteDebug` consent gate).
74
+ */
75
+ exec?: false | ExecOptions;
76
+ }
77
+ interface ExecOptions {
78
+ /**
79
+ * Additional locals bound into every snippet's scope. Keys MUST be
80
+ * valid identifier names (the SDK does not validate this — passing a
81
+ * key like "1foo" or "a-b" will throw at AsyncFunction construction
82
+ * time and surface as a `command-rejected`).
83
+ */
84
+ globals?: Record<string, unknown>;
55
85
  }
56
86
  type ScreenshotResult = string | {
57
87
  data: string;
@@ -71,6 +101,7 @@ type ScreenshotResult = string | {
71
101
  */
72
102
  | 'html-snapshot';
73
103
  };
104
+ declare function getBOT(): unknown;
74
105
 
75
106
  /**
76
107
  * Per-signature sliding-window event deduper.
@@ -211,4 +242,4 @@ declare const DEFAULT_BUFFER_SIZE = 1000;
211
242
  declare const DEFAULT_MAX_BATCH_SIZE = 50;
212
243
  declare function enableRemoteDebug(options: RemoteDebugOptions): Promise<RemoteDebugHandle>;
213
244
 
214
- export { type AppInfo, BotimConfig, BotimConfigError, BotimConsentError, type BuiltinHostHooks, CommandHandler, ConsentInput, ConsolePayload, DEFAULT_BODY_PATTERNS, DEFAULT_BUFFER_SIZE, DEFAULT_FLUSH_INTERVAL_MS, DEFAULT_MAX_BATCH_SIZE, DEFAULT_MAX_BODY_BYTES, DEFAULT_REDACT_HEADERS, type DedupOptions, DeviceInfo, ErrorPayload, EventLevel, EventType, NetworkPayload, type RedactionConfig, type RemoteDebugHandle, type RemoteDebugOptions, type SamplingConfig, type SuppressionSummary, enableRemoteDebug };
245
+ export { type AppInfo, BotimConfig, BotimConfigError, BotimConsentError, type BuiltinHostHooks, CommandHandler, ConsentInput, ConsolePayload, DEFAULT_BODY_PATTERNS, DEFAULT_BUFFER_SIZE, DEFAULT_FLUSH_INTERVAL_MS, DEFAULT_MAX_BATCH_SIZE, DEFAULT_MAX_BODY_BYTES, DEFAULT_REDACT_HEADERS, type DedupOptions, DeviceInfo, ErrorPayload, EventLevel, EventType, type ExecOptions, NetworkPayload, type RedactionConfig, type RemoteDebugHandle, type RemoteDebugOptions, type SamplingConfig, type SuppressionSummary, enableRemoteDebug, getBOT };
package/dist/index.js CHANGED
@@ -270,7 +270,7 @@ function resolveAgainstEndpoint(url, base) {
270
270
  function detectDeviceInfo(app, override) {
271
271
  const ua = typeof navigator !== "undefined" ? navigator.userAgent : void 0;
272
272
  return {
273
- deviceId: override?.deviceId ?? generateDeviceId(),
273
+ deviceId: override?.deviceId ?? loadOrCreateDeviceId(),
274
274
  platform: override?.platform ?? detectPlatform(ua),
275
275
  osVersion: override?.osVersion,
276
276
  appName: override?.appName ?? app.name,
@@ -286,6 +286,21 @@ function detectPlatform(ua) {
286
286
  if (/Mozilla|Chrome|Safari|Firefox/i.test(ua)) return "web";
287
287
  return "unknown";
288
288
  }
289
+ var DEVICE_ID_STORAGE_KEY = "botim-debug-sdk:device-id";
290
+ function loadOrCreateDeviceId() {
291
+ try {
292
+ const ls = typeof localStorage !== "undefined" ? localStorage : null;
293
+ if (ls) {
294
+ const stored = ls.getItem(DEVICE_ID_STORAGE_KEY);
295
+ if (stored && stored.length > 0) return stored;
296
+ const fresh = generateDeviceId();
297
+ ls.setItem(DEVICE_ID_STORAGE_KEY, fresh);
298
+ return fresh;
299
+ }
300
+ } catch {
301
+ }
302
+ return generateDeviceId();
303
+ }
289
304
  function generateDeviceId() {
290
305
  const c = typeof crypto !== "undefined" ? crypto : void 0;
291
306
  if (c?.randomUUID) return c.randomUUID();
@@ -560,6 +575,11 @@ function wrapFetch(opts) {
560
575
  method,
561
576
  url,
562
577
  status: res.status,
578
+ // statusText carries the human label ("OK", "Not Found"). Pre-HTTP/2
579
+ // responses always have it; HTTP/2+ defines it as empty by spec but
580
+ // most browsers synthesize one from the code, so this is reliable
581
+ // enough to display alongside the status code.
582
+ statusText: res.statusText || void 0,
563
583
  durationMs: Date.now() - start,
564
584
  resHeaders: headersFromResponse(res),
565
585
  resBody
@@ -573,7 +593,14 @@ function wrapFetch(opts) {
573
593
  url,
574
594
  durationMs: Date.now() - start,
575
595
  errorMessage: err instanceof Error ? err.message : String(err),
576
- errorName: err instanceof Error ? err.name : void 0
596
+ errorName: err instanceof Error ? err.name : void 0,
597
+ // Stack from the rejected promise — points into fetch internals
598
+ // and (when present) the call site that issued the request.
599
+ errorStack: err instanceof Error ? err.stack : void 0,
600
+ // undici frequently wraps the real reason in `cause` (e.g.
601
+ // `TypeError: fetch failed` outside, `Error: ECONNREFUSED` inside).
602
+ // Flatten the chain so operators don't have to dig.
603
+ errorCause: collectCauseChain(err)
577
604
  });
578
605
  throw err;
579
606
  }
@@ -582,6 +609,24 @@ function wrapFetch(opts) {
582
609
  target.fetch = original;
583
610
  };
584
611
  }
612
+ function collectCauseChain(err) {
613
+ if (!err || typeof err !== "object") return void 0;
614
+ const lines = [];
615
+ let cur = err.cause;
616
+ const seen = /* @__PURE__ */ new Set();
617
+ while (cur && lines.length < 5) {
618
+ if (seen.has(cur)) break;
619
+ seen.add(cur);
620
+ if (cur instanceof Error) {
621
+ lines.push(`${cur.name}: ${cur.message}`);
622
+ cur = cur.cause;
623
+ } else {
624
+ lines.push(String(cur));
625
+ cur = cur?.cause;
626
+ }
627
+ }
628
+ return lines.length ? lines.join("\n") : void 0;
629
+ }
585
630
  function wrapXHR(opts) {
586
631
  if (typeof XMLHttpRequest === "undefined") return () => {
587
632
  };
@@ -617,6 +662,7 @@ function wrapXHR(opts) {
617
662
  }
618
663
  s.start = Date.now();
619
664
  s.reqBody = typeof body === "string" ? body : void 0;
665
+ const sendSiteStack = captureCallSiteStack();
620
666
  opts.emit({
621
667
  phase: "request",
622
668
  reqId: s.reqId,
@@ -634,25 +680,32 @@ function wrapXHR(opts) {
634
680
  method: s.method,
635
681
  url: s.url,
636
682
  status: this.status,
683
+ // XHR exposes statusText directly; same display purpose as fetch.
684
+ statusText: this.statusText || void 0,
637
685
  durationMs: Date.now() - s.start,
638
686
  resHeaders: headers,
639
687
  resBody
640
688
  });
641
689
  };
642
- const onError = () => {
690
+ const onError = (kind) => () => {
643
691
  opts.emit({
644
692
  phase: "error",
645
693
  reqId: s.reqId,
646
694
  method: s.method,
647
695
  url: s.url,
648
696
  durationMs: Date.now() - s.start,
649
- errorMessage: this.statusText || "xhr error"
697
+ // Distinguish error/timeout/abort in the message — the standard
698
+ // XHR `statusText` is empty for `error` and unhelpful for the
699
+ // others, so we synthesise a clear label.
700
+ errorMessage: this.statusText || `xhr ${kind}`,
701
+ errorName: `XHR${kind[0].toUpperCase()}${kind.slice(1)}`,
702
+ errorStack: sendSiteStack
650
703
  });
651
704
  };
652
705
  this.addEventListener("load", onLoad);
653
- this.addEventListener("error", onError);
654
- this.addEventListener("timeout", onError);
655
- this.addEventListener("abort", onError);
706
+ this.addEventListener("error", onError("error"));
707
+ this.addEventListener("timeout", onError("timeout"));
708
+ this.addEventListener("abort", onError("abort"));
656
709
  return origSend.apply(this, [body]);
657
710
  };
658
711
  return () => {
@@ -661,6 +714,15 @@ function wrapXHR(opts) {
661
714
  proto.setRequestHeader = origSetReqHeader;
662
715
  };
663
716
  }
717
+ function captureCallSiteStack() {
718
+ try {
719
+ throw new Error("xhr-callsite");
720
+ } catch (err) {
721
+ if (!(err instanceof Error) || !err.stack) return void 0;
722
+ const lines = err.stack.split("\n");
723
+ return lines.slice(2).join("\n") || void 0;
724
+ }
725
+ }
664
726
  function parseXhrHeaders(raw) {
665
727
  const out = {};
666
728
  if (!raw) return out;
@@ -755,6 +817,10 @@ var CommandRegistry = class {
755
817
  };
756
818
  var MAX_DUMP_BYTES = 64 * 1024;
757
819
  var MAX_SCREENSHOT_BYTES = 1024 * 1024;
820
+ var MAX_EXEC_CODE_BYTES = 8 * 1024;
821
+ var DEFAULT_EXEC_TIMEOUT_MS = 5e3;
822
+ var MIN_EXEC_TIMEOUT_MS = 250;
823
+ var MAX_EXEC_TIMEOUT_MS = 3e4;
758
824
  async function defaultDomScreenshot() {
759
825
  if (typeof document === "undefined" || typeof window === "undefined") {
760
826
  throw new Error(
@@ -869,6 +935,9 @@ function registerBuiltins(registry, hooks = {}) {
869
935
  registry.register("dump-state", makeDumpState(hooks.getState));
870
936
  registry.register("set-feature-flag", makeSetFlag(hooks.setFeatureFlag));
871
937
  registry.register("screenshot", makeScreenshot(hooks.screenshot));
938
+ if (hooks.exec !== false) {
939
+ registry.register("exec", makeExec(hooks.exec ?? {}));
940
+ }
872
941
  }
873
942
  var ping = () => ({ ok: true, ts: Date.now() });
874
943
  function makeReload(reload) {
@@ -931,6 +1000,168 @@ function makeScreenshot(screenshot) {
931
1000
  return { format, data, bytes: data.length };
932
1001
  };
933
1002
  }
1003
+ var cachedBOT = void 0;
1004
+ function getBOT() {
1005
+ if (cachedBOT !== void 0) return cachedBOT;
1006
+ if (typeof window === "undefined") {
1007
+ cachedBOT = null;
1008
+ return null;
1009
+ }
1010
+ try {
1011
+ const w = window;
1012
+ if (w.BOT) {
1013
+ cachedBOT = w.BOT;
1014
+ return cachedBOT;
1015
+ }
1016
+ } catch {
1017
+ }
1018
+ try {
1019
+ const w = window;
1020
+ const ng = w.angular;
1021
+ if (ng?.element && typeof document !== "undefined") {
1022
+ const injector = ng.element(document).injector?.();
1023
+ const bot = injector?.get?.("BOT");
1024
+ if (bot != null) {
1025
+ cachedBOT = bot;
1026
+ return cachedBOT;
1027
+ }
1028
+ }
1029
+ } catch {
1030
+ }
1031
+ return null;
1032
+ }
1033
+ function makeExec(opts) {
1034
+ const extraGlobalNames = Object.keys(opts.globals ?? {});
1035
+ const extraGlobalValues = extraGlobalNames.map((k) => opts.globals[k]);
1036
+ return async (args, ctx) => {
1037
+ if (typeof args.code !== "string" || args.code.length === 0) {
1038
+ throw new Error("args.code (non-empty string) is required");
1039
+ }
1040
+ if (args.code.length > MAX_EXEC_CODE_BYTES) {
1041
+ throw new Error(`args.code ${args.code.length} bytes exceeds limit ${MAX_EXEC_CODE_BYTES}`);
1042
+ }
1043
+ const timeoutMs = clamp(
1044
+ typeof args.timeoutMs === "number" ? args.timeoutMs : DEFAULT_EXEC_TIMEOUT_MS,
1045
+ MIN_EXEC_TIMEOUT_MS,
1046
+ MAX_EXEC_TIMEOUT_MS
1047
+ );
1048
+ const start = Date.now();
1049
+ const logs = [];
1050
+ const consoleMethods = ["log", "info", "warn", "error", "debug"];
1051
+ const originals = {};
1052
+ if (typeof console !== "undefined") {
1053
+ for (const m of consoleMethods) {
1054
+ const orig = console[m];
1055
+ if (typeof orig !== "function") continue;
1056
+ originals[m] = orig;
1057
+ const captured = (...callArgs) => {
1058
+ logs.push({ method: m, args: callArgs.map(execSafeClone), ts: Date.now() });
1059
+ try {
1060
+ orig.call(console, ...callArgs);
1061
+ } catch {
1062
+ }
1063
+ };
1064
+ console[m] = captured;
1065
+ }
1066
+ }
1067
+ let value;
1068
+ let threw = void 0;
1069
+ try {
1070
+ const AsyncFunction = Object.getPrototypeOf(async function() {
1071
+ }).constructor;
1072
+ const localNames = ["BOT", "window", "document", ...extraGlobalNames];
1073
+ const localValues = [
1074
+ getBOT(),
1075
+ typeof window !== "undefined" ? window : void 0,
1076
+ typeof document !== "undefined" ? document : void 0,
1077
+ ...extraGlobalValues
1078
+ ];
1079
+ let fnTry = null;
1080
+ try {
1081
+ fnTry = new AsyncFunction(...localNames, `return (${args.code});`);
1082
+ } catch (e) {
1083
+ if (!(e instanceof SyntaxError)) throw e;
1084
+ }
1085
+ const fn = fnTry ?? new AsyncFunction(...localNames, args.code);
1086
+ let timer = null;
1087
+ let abortReject = null;
1088
+ const onAbort = () => abortReject?.(new Error("exec cancelled"));
1089
+ ctx.signal?.addEventListener?.("abort", onAbort);
1090
+ try {
1091
+ value = await Promise.race([
1092
+ fn(...localValues),
1093
+ new Promise((_, reject) => {
1094
+ timer = setTimeout(
1095
+ () => reject(new Error(`exec exceeded ${timeoutMs}ms`)),
1096
+ timeoutMs
1097
+ );
1098
+ }),
1099
+ new Promise((_, reject) => {
1100
+ abortReject = reject;
1101
+ })
1102
+ ]);
1103
+ } finally {
1104
+ if (timer) clearTimeout(timer);
1105
+ ctx.signal?.removeEventListener?.("abort", onAbort);
1106
+ }
1107
+ } catch (err) {
1108
+ threw = err;
1109
+ } finally {
1110
+ if (typeof console !== "undefined") {
1111
+ for (const m of consoleMethods) {
1112
+ if (originals[m]) {
1113
+ console[m] = originals[m];
1114
+ }
1115
+ }
1116
+ }
1117
+ }
1118
+ if (threw !== void 0) {
1119
+ const e = threw instanceof Error ? threw : new Error(String(threw));
1120
+ const stackTruncated = typeof e.stack === "string" ? e.stack.split("\n").slice(0, 20).join("\n") : void 0;
1121
+ const detail = {
1122
+ name: e.name,
1123
+ message: e.message,
1124
+ stack: stackTruncated,
1125
+ logs,
1126
+ durationMs: Date.now() - start
1127
+ };
1128
+ const wrapped = new Error(JSON.stringify(detail));
1129
+ wrapped.detail = detail;
1130
+ throw wrapped;
1131
+ }
1132
+ return {
1133
+ ok: true,
1134
+ value: execSafeClone(value),
1135
+ logs,
1136
+ durationMs: Date.now() - start
1137
+ };
1138
+ };
1139
+ }
1140
+ function clamp(n, lo, hi) {
1141
+ if (!Number.isFinite(n)) return lo;
1142
+ return Math.min(Math.max(n, lo), hi);
1143
+ }
1144
+ function execSafeClone(v) {
1145
+ try {
1146
+ return JSON.parse(JSON.stringify(v, (_k, val) => {
1147
+ if (typeof val === "function") {
1148
+ return `[Function: ${val.name || "anonymous"}]`;
1149
+ }
1150
+ if (val instanceof Error) {
1151
+ return { name: val.name, message: val.message, stack: val.stack };
1152
+ }
1153
+ if (typeof val === "bigint") return val.toString() + "n";
1154
+ if (typeof val === "undefined") return null;
1155
+ if (val && typeof val === "object" && val.nodeType === 1) {
1156
+ const el = val;
1157
+ return `[${el.tagName ?? "Element"}${el.id ? "#" + el.id : ""}]`;
1158
+ }
1159
+ return val;
1160
+ }));
1161
+ } catch {
1162
+ return { __unserializable: typeof v };
1163
+ }
1164
+ }
934
1165
  function safeStringify(v) {
935
1166
  try {
936
1167
  const seen = /* @__PURE__ */ new WeakSet();
@@ -1368,6 +1599,6 @@ async function enableRemoteDebug(options) {
1368
1599
  };
1369
1600
  }
1370
1601
 
1371
- export { BotimConfigError, BotimConsentError, DEFAULT_BODY_PATTERNS, DEFAULT_BUFFER_SIZE, DEFAULT_FLUSH_INTERVAL_MS, DEFAULT_MAX_BATCH_SIZE, DEFAULT_MAX_BODY_BYTES, DEFAULT_REDACT_HEADERS, SCHEMA_VERSION, enableRemoteDebug };
1602
+ export { BotimConfigError, BotimConsentError, DEFAULT_BODY_PATTERNS, DEFAULT_BUFFER_SIZE, DEFAULT_FLUSH_INTERVAL_MS, DEFAULT_MAX_BATCH_SIZE, DEFAULT_MAX_BODY_BYTES, DEFAULT_REDACT_HEADERS, SCHEMA_VERSION, enableRemoteDebug, getBOT };
1372
1603
  //# sourceMappingURL=index.js.map
1373
1604
  //# sourceMappingURL=index.js.map