@botim/mp-debug-sdk 0.5.2 → 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/README.md +54 -3
- package/dist/index.cjs +170 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -1
- package/dist/index.d.ts +32 -1
- package/dist/index.js +170 -1
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Mini-programs run on user devices in environments you can't easily attach a debu
|
|
|
8
8
|
|
|
9
9
|
- **Live logs** of `console.*`, `fetch`, `XMLHttpRequest`, and uncaught errors.
|
|
10
10
|
- **AI-observable sessions** — every event indexed by `(miniProgramId, deviceId, sid)` so resolver agents can pull errors and reproduce bugs without a human in the loop.
|
|
11
|
-
- **Safe AI command channel** — agents can `reload`, `dump-state`, `set-feature-flag`, `screenshot`, `ping`, or any custom command you allow. Anything not registered is rejected.
|
|
11
|
+
- **Safe AI command channel** — agents can `reload`, `dump-state`, `set-feature-flag`, `screenshot`, `ping`, `exec` (default-on remote REPL), or any custom command you allow. Anything not registered is rejected.
|
|
12
12
|
- **Built-in redaction** before the event ever enters the in-memory buffer.
|
|
13
13
|
|
|
14
14
|
## Install
|
|
@@ -128,7 +128,58 @@ The SDK posts directly to `endpoint` — there's no proxy required. Your relay m
|
|
|
128
128
|
CORS_ORIGINS=https://my-mp.example.com,https://staging.example.com
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
## 5.
|
|
131
|
+
## 5. Remote REPL (`exec`) — default-on
|
|
132
|
+
|
|
133
|
+
Once `enableRemoteDebug` returns, an attached agent can send a JS snippet to the device and read back the value, captured `console.*` output, or thrown error. No host wiring needed.
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# From any terminal that can reach your relay:
|
|
137
|
+
curl -sX POST "https://debug.botim.dev/v1/mp/<MP_ID>/devices/<DEVICE_ID>/commands" \
|
|
138
|
+
-H 'content-type: application/json' \
|
|
139
|
+
-d '{"name":"exec","args":{"code":"console.log(\"hi\"); return window.location.href"}}'
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Result event:
|
|
143
|
+
|
|
144
|
+
```jsonc
|
|
145
|
+
{
|
|
146
|
+
"type": "command-ack",
|
|
147
|
+
"payload": {
|
|
148
|
+
"command": "exec",
|
|
149
|
+
"ok": true,
|
|
150
|
+
"result": {
|
|
151
|
+
"value": "https://my-mp.example.com/page",
|
|
152
|
+
"logs": [{"method":"log","args":["hi"],"ts": 1735324800123}],
|
|
153
|
+
"durationMs": 2
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Inside the snippet, `BOT`, `window`, and `document` are bound as locals (with `BOT` resolved best-effort from `window.BOT` or Angular DI; `null` if unavailable). Top-level `await` works. Code is capped at 8 KB and 30 s.
|
|
160
|
+
|
|
161
|
+
**To opt out** (e.g. you ship a non-debug release that still uses the SDK for telemetry only):
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
await enableRemoteDebug({
|
|
165
|
+
// ...,
|
|
166
|
+
builtins: { exec: false },
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**To inject extra locals** for your app:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
await enableRemoteDebug({
|
|
174
|
+
// ...,
|
|
175
|
+
builtins: { exec: { globals: { app: myApp, store: myStore } } },
|
|
176
|
+
});
|
|
177
|
+
// agent can then call: { code: "return store.getState().user" }
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
> Pre-buffer redaction (headers, JWT/long-token regex) is applied to `result.value` and `result.logs` before they leave the device buffer. The existing prod consent gate (`hostToken` or `userOptIn`) gates whether attach happens at all. See `docs/recipes/exec.md` for deeper notes.
|
|
181
|
+
|
|
182
|
+
## 6. Custom commands (optional)
|
|
132
183
|
|
|
133
184
|
Any AI agent with the right scope can request a command. The SDK only runs commands registered via `registerCommand`:
|
|
134
185
|
|
|
@@ -147,7 +198,7 @@ handle.registerCommand('set-locale', async (args) => {
|
|
|
147
198
|
|
|
148
199
|
Anything not registered gets a `command-rejected` event with reason `unknown-command`.
|
|
149
200
|
|
|
150
|
-
##
|
|
201
|
+
## 7. Lifecycle
|
|
151
202
|
|
|
152
203
|
```ts
|
|
153
204
|
await handle.flush(); // force-send buffered events
|
package/dist/index.cjs
CHANGED
|
@@ -819,6 +819,10 @@ var CommandRegistry = class {
|
|
|
819
819
|
};
|
|
820
820
|
var MAX_DUMP_BYTES = 64 * 1024;
|
|
821
821
|
var MAX_SCREENSHOT_BYTES = 1024 * 1024;
|
|
822
|
+
var MAX_EXEC_CODE_BYTES = 8 * 1024;
|
|
823
|
+
var DEFAULT_EXEC_TIMEOUT_MS = 5e3;
|
|
824
|
+
var MIN_EXEC_TIMEOUT_MS = 250;
|
|
825
|
+
var MAX_EXEC_TIMEOUT_MS = 3e4;
|
|
822
826
|
async function defaultDomScreenshot() {
|
|
823
827
|
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
824
828
|
throw new Error(
|
|
@@ -933,6 +937,9 @@ function registerBuiltins(registry, hooks = {}) {
|
|
|
933
937
|
registry.register("dump-state", makeDumpState(hooks.getState));
|
|
934
938
|
registry.register("set-feature-flag", makeSetFlag(hooks.setFeatureFlag));
|
|
935
939
|
registry.register("screenshot", makeScreenshot(hooks.screenshot));
|
|
940
|
+
if (hooks.exec !== false) {
|
|
941
|
+
registry.register("exec", makeExec(hooks.exec ?? {}));
|
|
942
|
+
}
|
|
936
943
|
}
|
|
937
944
|
var ping = () => ({ ok: true, ts: Date.now() });
|
|
938
945
|
function makeReload(reload) {
|
|
@@ -995,6 +1002,168 @@ function makeScreenshot(screenshot) {
|
|
|
995
1002
|
return { format, data, bytes: data.length };
|
|
996
1003
|
};
|
|
997
1004
|
}
|
|
1005
|
+
var cachedBOT = void 0;
|
|
1006
|
+
function getBOT() {
|
|
1007
|
+
if (cachedBOT !== void 0) return cachedBOT;
|
|
1008
|
+
if (typeof window === "undefined") {
|
|
1009
|
+
cachedBOT = null;
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
try {
|
|
1013
|
+
const w = window;
|
|
1014
|
+
if (w.BOT) {
|
|
1015
|
+
cachedBOT = w.BOT;
|
|
1016
|
+
return cachedBOT;
|
|
1017
|
+
}
|
|
1018
|
+
} catch {
|
|
1019
|
+
}
|
|
1020
|
+
try {
|
|
1021
|
+
const w = window;
|
|
1022
|
+
const ng = w.angular;
|
|
1023
|
+
if (ng?.element && typeof document !== "undefined") {
|
|
1024
|
+
const injector = ng.element(document).injector?.();
|
|
1025
|
+
const bot = injector?.get?.("BOT");
|
|
1026
|
+
if (bot != null) {
|
|
1027
|
+
cachedBOT = bot;
|
|
1028
|
+
return cachedBOT;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
} catch {
|
|
1032
|
+
}
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
function makeExec(opts) {
|
|
1036
|
+
const extraGlobalNames = Object.keys(opts.globals ?? {});
|
|
1037
|
+
const extraGlobalValues = extraGlobalNames.map((k) => opts.globals[k]);
|
|
1038
|
+
return async (args, ctx) => {
|
|
1039
|
+
if (typeof args.code !== "string" || args.code.length === 0) {
|
|
1040
|
+
throw new Error("args.code (non-empty string) is required");
|
|
1041
|
+
}
|
|
1042
|
+
if (args.code.length > MAX_EXEC_CODE_BYTES) {
|
|
1043
|
+
throw new Error(`args.code ${args.code.length} bytes exceeds limit ${MAX_EXEC_CODE_BYTES}`);
|
|
1044
|
+
}
|
|
1045
|
+
const timeoutMs = clamp(
|
|
1046
|
+
typeof args.timeoutMs === "number" ? args.timeoutMs : DEFAULT_EXEC_TIMEOUT_MS,
|
|
1047
|
+
MIN_EXEC_TIMEOUT_MS,
|
|
1048
|
+
MAX_EXEC_TIMEOUT_MS
|
|
1049
|
+
);
|
|
1050
|
+
const start = Date.now();
|
|
1051
|
+
const logs = [];
|
|
1052
|
+
const consoleMethods = ["log", "info", "warn", "error", "debug"];
|
|
1053
|
+
const originals = {};
|
|
1054
|
+
if (typeof console !== "undefined") {
|
|
1055
|
+
for (const m of consoleMethods) {
|
|
1056
|
+
const orig = console[m];
|
|
1057
|
+
if (typeof orig !== "function") continue;
|
|
1058
|
+
originals[m] = orig;
|
|
1059
|
+
const captured = (...callArgs) => {
|
|
1060
|
+
logs.push({ method: m, args: callArgs.map(execSafeClone), ts: Date.now() });
|
|
1061
|
+
try {
|
|
1062
|
+
orig.call(console, ...callArgs);
|
|
1063
|
+
} catch {
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
console[m] = captured;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
let value;
|
|
1070
|
+
let threw = void 0;
|
|
1071
|
+
try {
|
|
1072
|
+
const AsyncFunction = Object.getPrototypeOf(async function() {
|
|
1073
|
+
}).constructor;
|
|
1074
|
+
const localNames = ["BOT", "window", "document", ...extraGlobalNames];
|
|
1075
|
+
const localValues = [
|
|
1076
|
+
getBOT(),
|
|
1077
|
+
typeof window !== "undefined" ? window : void 0,
|
|
1078
|
+
typeof document !== "undefined" ? document : void 0,
|
|
1079
|
+
...extraGlobalValues
|
|
1080
|
+
];
|
|
1081
|
+
let fnTry = null;
|
|
1082
|
+
try {
|
|
1083
|
+
fnTry = new AsyncFunction(...localNames, `return (${args.code});`);
|
|
1084
|
+
} catch (e) {
|
|
1085
|
+
if (!(e instanceof SyntaxError)) throw e;
|
|
1086
|
+
}
|
|
1087
|
+
const fn = fnTry ?? new AsyncFunction(...localNames, args.code);
|
|
1088
|
+
let timer = null;
|
|
1089
|
+
let abortReject = null;
|
|
1090
|
+
const onAbort = () => abortReject?.(new Error("exec cancelled"));
|
|
1091
|
+
ctx.signal?.addEventListener?.("abort", onAbort);
|
|
1092
|
+
try {
|
|
1093
|
+
value = await Promise.race([
|
|
1094
|
+
fn(...localValues),
|
|
1095
|
+
new Promise((_, reject) => {
|
|
1096
|
+
timer = setTimeout(
|
|
1097
|
+
() => reject(new Error(`exec exceeded ${timeoutMs}ms`)),
|
|
1098
|
+
timeoutMs
|
|
1099
|
+
);
|
|
1100
|
+
}),
|
|
1101
|
+
new Promise((_, reject) => {
|
|
1102
|
+
abortReject = reject;
|
|
1103
|
+
})
|
|
1104
|
+
]);
|
|
1105
|
+
} finally {
|
|
1106
|
+
if (timer) clearTimeout(timer);
|
|
1107
|
+
ctx.signal?.removeEventListener?.("abort", onAbort);
|
|
1108
|
+
}
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
threw = err;
|
|
1111
|
+
} finally {
|
|
1112
|
+
if (typeof console !== "undefined") {
|
|
1113
|
+
for (const m of consoleMethods) {
|
|
1114
|
+
if (originals[m]) {
|
|
1115
|
+
console[m] = originals[m];
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
if (threw !== void 0) {
|
|
1121
|
+
const e = threw instanceof Error ? threw : new Error(String(threw));
|
|
1122
|
+
const stackTruncated = typeof e.stack === "string" ? e.stack.split("\n").slice(0, 20).join("\n") : void 0;
|
|
1123
|
+
const detail = {
|
|
1124
|
+
name: e.name,
|
|
1125
|
+
message: e.message,
|
|
1126
|
+
stack: stackTruncated,
|
|
1127
|
+
logs,
|
|
1128
|
+
durationMs: Date.now() - start
|
|
1129
|
+
};
|
|
1130
|
+
const wrapped = new Error(JSON.stringify(detail));
|
|
1131
|
+
wrapped.detail = detail;
|
|
1132
|
+
throw wrapped;
|
|
1133
|
+
}
|
|
1134
|
+
return {
|
|
1135
|
+
ok: true,
|
|
1136
|
+
value: execSafeClone(value),
|
|
1137
|
+
logs,
|
|
1138
|
+
durationMs: Date.now() - start
|
|
1139
|
+
};
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
function clamp(n, lo, hi) {
|
|
1143
|
+
if (!Number.isFinite(n)) return lo;
|
|
1144
|
+
return Math.min(Math.max(n, lo), hi);
|
|
1145
|
+
}
|
|
1146
|
+
function execSafeClone(v) {
|
|
1147
|
+
try {
|
|
1148
|
+
return JSON.parse(JSON.stringify(v, (_k, val) => {
|
|
1149
|
+
if (typeof val === "function") {
|
|
1150
|
+
return `[Function: ${val.name || "anonymous"}]`;
|
|
1151
|
+
}
|
|
1152
|
+
if (val instanceof Error) {
|
|
1153
|
+
return { name: val.name, message: val.message, stack: val.stack };
|
|
1154
|
+
}
|
|
1155
|
+
if (typeof val === "bigint") return val.toString() + "n";
|
|
1156
|
+
if (typeof val === "undefined") return null;
|
|
1157
|
+
if (val && typeof val === "object" && val.nodeType === 1) {
|
|
1158
|
+
const el = val;
|
|
1159
|
+
return `[${el.tagName ?? "Element"}${el.id ? "#" + el.id : ""}]`;
|
|
1160
|
+
}
|
|
1161
|
+
return val;
|
|
1162
|
+
}));
|
|
1163
|
+
} catch {
|
|
1164
|
+
return { __unserializable: typeof v };
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
998
1167
|
function safeStringify(v) {
|
|
999
1168
|
try {
|
|
1000
1169
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
@@ -1442,5 +1611,6 @@ exports.DEFAULT_MAX_BODY_BYTES = DEFAULT_MAX_BODY_BYTES;
|
|
|
1442
1611
|
exports.DEFAULT_REDACT_HEADERS = DEFAULT_REDACT_HEADERS;
|
|
1443
1612
|
exports.SCHEMA_VERSION = SCHEMA_VERSION;
|
|
1444
1613
|
exports.enableRemoteDebug = enableRemoteDebug;
|
|
1614
|
+
exports.getBOT = getBOT;
|
|
1445
1615
|
//# sourceMappingURL=index.cjs.map
|
|
1446
1616
|
//# sourceMappingURL=index.cjs.map
|