@botim/mp-debug-sdk 0.1.0

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.
@@ -0,0 +1,253 @@
1
+ declare const SCHEMA_VERSION: 2;
2
+ type EventLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal';
3
+ type EventType = 'console' | 'network' | 'error' | 'lifecycle' | 'bridge' | 'perf' | 'command-ack' | 'command-rejected';
4
+ interface DebugEventBase {
5
+ v: typeof SCHEMA_VERSION;
6
+ sid: string;
7
+ seq: number;
8
+ ts: number;
9
+ type: EventType;
10
+ level: EventLevel;
11
+ meta?: EventMeta;
12
+ }
13
+ interface EventMeta {
14
+ route?: string;
15
+ build?: string;
16
+ appVersion?: string;
17
+ deviceId?: string;
18
+ userIdHash?: string;
19
+ /** Set on command-ack / command-rejected events to correlate with the originating CommandRequest. */
20
+ commandId?: string;
21
+ /**
22
+ * Set ONLY on synthesized rollup events emitted by the SDK's deduper.
23
+ * Carries the count of suppressed identical events in the just-closed
24
+ * window. Original (non-suppressed) events do NOT carry this field.
25
+ */
26
+ dedup?: {
27
+ /** Stable signature used to bucket the suppressed events. */
28
+ signature: string;
29
+ /** Number of suppressed occurrences (excludes the leading event that was already emitted). */
30
+ count: number;
31
+ /** Timestamp of the first occurrence in the window. */
32
+ firstTs: number;
33
+ /** Timestamp of the last occurrence in the window. */
34
+ lastTs: number;
35
+ };
36
+ }
37
+ type SerializedValue = {
38
+ k: 'primitive';
39
+ v: string | number | boolean | null;
40
+ } | {
41
+ k: 'undefined';
42
+ } | {
43
+ k: 'string';
44
+ v: string;
45
+ truncated?: boolean;
46
+ } | {
47
+ k: 'json';
48
+ v: string;
49
+ truncated?: boolean;
50
+ } | {
51
+ k: 'error';
52
+ name: string;
53
+ message: string;
54
+ stack?: string;
55
+ } | {
56
+ k: 'circular';
57
+ } | {
58
+ k: 'function';
59
+ name?: string;
60
+ } | {
61
+ k: 'unserializable';
62
+ reason: string;
63
+ };
64
+ interface ConsolePayload {
65
+ method: 'log' | 'info' | 'warn' | 'error' | 'debug';
66
+ args: SerializedValue[];
67
+ }
68
+ type NetworkPhase = 'request' | 'response' | 'error';
69
+ interface NetworkPayload {
70
+ phase: NetworkPhase;
71
+ reqId: string;
72
+ method: string;
73
+ url: string;
74
+ status?: number;
75
+ durationMs?: number;
76
+ reqHeaders?: Record<string, string>;
77
+ reqBody?: string;
78
+ reqBodyTruncated?: boolean;
79
+ resHeaders?: Record<string, string>;
80
+ resBody?: string;
81
+ resBodyTruncated?: boolean;
82
+ errorMessage?: string;
83
+ errorName?: string;
84
+ }
85
+ type ErrorSource = 'window.error' | 'unhandledrejection' | 'manual';
86
+ interface ErrorPayload {
87
+ message: string;
88
+ name?: string;
89
+ stack?: string;
90
+ source: ErrorSource;
91
+ filename?: string;
92
+ lineno?: number;
93
+ colno?: number;
94
+ }
95
+ type LifecycleEvent = 'launch' | 'pageShow' | 'pageHide' | 'route' | 'background' | 'foreground' | 'visibilityChange';
96
+ interface LifecyclePayload {
97
+ event: LifecycleEvent;
98
+ data?: Record<string, unknown>;
99
+ }
100
+ interface BridgePayload {
101
+ fn: string;
102
+ argsPreview?: string;
103
+ result?: 'ok' | 'error';
104
+ durationMs?: number;
105
+ errorMessage?: string;
106
+ }
107
+ interface PerfPayload {
108
+ metric: string;
109
+ value: number;
110
+ unit?: string;
111
+ }
112
+ /** Payload for command-ack: the handler ran, here's its result. */
113
+ interface CommandAckPayload {
114
+ command: string;
115
+ ok: boolean;
116
+ /** Handler return value, JSON-serializable. */
117
+ result?: unknown;
118
+ durationMs?: number;
119
+ }
120
+ /** Payload for command-rejected: relay sent a command we won't run. */
121
+ interface CommandRejectedPayload {
122
+ command: string;
123
+ reason: 'unknown-command' | 'rate-limited' | 'invalid-args' | 'handler-threw' | 'overridden';
124
+ details?: string;
125
+ }
126
+ type DebugEvent = (DebugEventBase & {
127
+ type: 'console';
128
+ payload: ConsolePayload;
129
+ }) | (DebugEventBase & {
130
+ type: 'network';
131
+ payload: NetworkPayload;
132
+ }) | (DebugEventBase & {
133
+ type: 'error';
134
+ payload: ErrorPayload;
135
+ }) | (DebugEventBase & {
136
+ type: 'lifecycle';
137
+ payload: LifecyclePayload;
138
+ }) | (DebugEventBase & {
139
+ type: 'bridge';
140
+ payload: BridgePayload;
141
+ }) | (DebugEventBase & {
142
+ type: 'perf';
143
+ payload: PerfPayload;
144
+ }) | (DebugEventBase & {
145
+ type: 'command-ack';
146
+ payload: CommandAckPayload;
147
+ }) | (DebugEventBase & {
148
+ type: 'command-rejected';
149
+ payload: CommandRejectedPayload;
150
+ });
151
+ interface UploadBatch {
152
+ sessionToken: string;
153
+ events: DebugEvent[];
154
+ }
155
+ interface UploadAck {
156
+ acceptedSeq: number;
157
+ rejected?: Array<{
158
+ seq: number;
159
+ reason: string;
160
+ }>;
161
+ }
162
+ type StreamFrame = {
163
+ kind: 'hello';
164
+ sid: string;
165
+ devices: number;
166
+ } | {
167
+ kind: 'event';
168
+ event: DebugEvent;
169
+ } | {
170
+ kind: 'device-join';
171
+ deviceInfo: DeviceInfo;
172
+ } | {
173
+ kind: 'device-leave';
174
+ deviceId: string;
175
+ } | {
176
+ kind: 'gap';
177
+ deviceId: string;
178
+ expectedSeq: number;
179
+ gotSeq: number;
180
+ } | {
181
+ kind: 'session-end';
182
+ reason: string;
183
+ };
184
+ interface DeviceInfo {
185
+ deviceId: string;
186
+ platform: 'ios' | 'android' | 'web' | 'unknown';
187
+ osVersion?: string;
188
+ appName?: string;
189
+ appVersion?: string;
190
+ appBuild?: string;
191
+ userAgent?: string;
192
+ }
193
+ type BotimEnv = 'dev' | 'uat' | 'beta' | 'prod';
194
+ /**
195
+ * The contents of `botim.{env}.json`, resolved at build time and injected
196
+ * into the bundle via the virtual module `virtual:botim/config`.
197
+ */
198
+ interface BotimConfig {
199
+ miniProgramId: string;
200
+ env: BotimEnv;
201
+ /** Short hash of the resolved config or the build, set by the plugin. */
202
+ buildSignature: string;
203
+ appName?: string;
204
+ appVersion?: string;
205
+ /** Forward-compatible bag for fields the relay may add later. */
206
+ [extra: string]: unknown;
207
+ }
208
+ interface ConsentInput {
209
+ /** JWT issued by the BOTIM host runtime — strongest form. */
210
+ hostToken?: string;
211
+ /** Explicit user opt-in (e.g., set after a privacy dialog). */
212
+ userOptIn?: boolean;
213
+ }
214
+ interface AttachRequest {
215
+ miniProgramId: string;
216
+ env: BotimEnv;
217
+ buildSignature: string;
218
+ deviceInfo: DeviceInfo;
219
+ consent: ConsentInput;
220
+ schemaVersion: typeof SCHEMA_VERSION;
221
+ }
222
+ interface AttachResponse {
223
+ sid: string;
224
+ deviceToken: string;
225
+ expiresAt: number;
226
+ ingestUrl: string;
227
+ /** Endpoint the device long-polls for AI-issued commands. */
228
+ commandPollUrl: string;
229
+ }
230
+ interface CommandRequest {
231
+ /** Relay-issued unique id, echoed in the resulting ack/rejected event. */
232
+ id: string;
233
+ /** Registered command name (kebab-case). */
234
+ name: string;
235
+ /** Arbitrary JSON args, validated by the handler. */
236
+ args?: Record<string, unknown>;
237
+ /** Optional deadline; handlers SHOULD respect it. */
238
+ deadlineTs?: number;
239
+ }
240
+ /** Long-poll response shape from `commandPollUrl`. */
241
+ interface CommandPollResponse {
242
+ commands: CommandRequest[];
243
+ /** Suggested seconds until the next poll. */
244
+ nextDelayMs?: number;
245
+ }
246
+ type CommandHandler = (args: Record<string, unknown>, ctx: CommandContext) => unknown | Promise<unknown>;
247
+ interface CommandContext {
248
+ commandId: string;
249
+ command: string;
250
+ signal: AbortSignal;
251
+ }
252
+
253
+ export { type AttachRequest, type AttachResponse, type BotimConfig, type BotimEnv, type BridgePayload, type CommandAckPayload, type CommandContext, type CommandHandler, type CommandPollResponse, type CommandRejectedPayload, type CommandRequest, type ConsentInput, type ConsolePayload, type DebugEvent, type DebugEventBase, type DeviceInfo, type ErrorPayload, type ErrorSource, type EventLevel, type EventMeta, type EventType, type LifecycleEvent, type LifecyclePayload, type NetworkPayload, type NetworkPhase, type PerfPayload, SCHEMA_VERSION, type SerializedValue, type StreamFrame, type UploadAck, type UploadBatch };
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ // src/types.ts
2
+ var SCHEMA_VERSION = 2;
3
+
4
+ export { SCHEMA_VERSION };
5
+ //# sourceMappingURL=types.js.map
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts"],"names":[],"mappings":";AAAO,IAAM,cAAA,GAAiB","file":"types.js","sourcesContent":["export const SCHEMA_VERSION = 2 as const;\n\nexport type EventLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal';\n\nexport type EventType =\n | 'console'\n | 'network'\n | 'error'\n | 'lifecycle'\n | 'bridge'\n | 'perf'\n | 'command-ack'\n | 'command-rejected';\n\nexport interface DebugEventBase {\n v: typeof SCHEMA_VERSION;\n sid: string;\n seq: number;\n ts: number;\n type: EventType;\n level: EventLevel;\n meta?: EventMeta;\n}\n\nexport interface EventMeta {\n route?: string;\n build?: string;\n appVersion?: string;\n deviceId?: string;\n userIdHash?: string;\n /** Set on command-ack / command-rejected events to correlate with the originating CommandRequest. */\n commandId?: string;\n /**\n * Set ONLY on synthesized rollup events emitted by the SDK's deduper.\n * Carries the count of suppressed identical events in the just-closed\n * window. Original (non-suppressed) events do NOT carry this field.\n */\n dedup?: {\n /** Stable signature used to bucket the suppressed events. */\n signature: string;\n /** Number of suppressed occurrences (excludes the leading event that was already emitted). */\n count: number;\n /** Timestamp of the first occurrence in the window. */\n firstTs: number;\n /** Timestamp of the last occurrence in the window. */\n lastTs: number;\n };\n}\n\nexport type SerializedValue =\n | { k: 'primitive'; v: string | number | boolean | null }\n | { k: 'undefined' }\n | { k: 'string'; v: string; truncated?: boolean }\n | { k: 'json'; v: string; truncated?: boolean }\n | { k: 'error'; name: string; message: string; stack?: string }\n | { k: 'circular' }\n | { k: 'function'; name?: string }\n | { k: 'unserializable'; reason: string };\n\nexport interface ConsolePayload {\n method: 'log' | 'info' | 'warn' | 'error' | 'debug';\n args: SerializedValue[];\n}\n\nexport type NetworkPhase = 'request' | 'response' | 'error';\n\nexport interface NetworkPayload {\n phase: NetworkPhase;\n reqId: string;\n method: string;\n url: string;\n status?: number;\n durationMs?: number;\n reqHeaders?: Record<string, string>;\n reqBody?: string;\n reqBodyTruncated?: boolean;\n resHeaders?: Record<string, string>;\n resBody?: string;\n resBodyTruncated?: boolean;\n errorMessage?: string;\n errorName?: string;\n}\n\nexport type ErrorSource = 'window.error' | 'unhandledrejection' | 'manual';\n\nexport interface ErrorPayload {\n message: string;\n name?: string;\n stack?: string;\n source: ErrorSource;\n filename?: string;\n lineno?: number;\n colno?: number;\n}\n\nexport type LifecycleEvent =\n | 'launch'\n | 'pageShow'\n | 'pageHide'\n | 'route'\n | 'background'\n | 'foreground'\n | 'visibilityChange';\n\nexport interface LifecyclePayload {\n event: LifecycleEvent;\n data?: Record<string, unknown>;\n}\n\nexport interface BridgePayload {\n fn: string;\n argsPreview?: string;\n result?: 'ok' | 'error';\n durationMs?: number;\n errorMessage?: string;\n}\n\nexport interface PerfPayload {\n metric: string;\n value: number;\n unit?: string;\n}\n\n/** Payload for command-ack: the handler ran, here's its result. */\nexport interface CommandAckPayload {\n command: string;\n ok: boolean;\n /** Handler return value, JSON-serializable. */\n result?: unknown;\n durationMs?: number;\n}\n\n/** Payload for command-rejected: relay sent a command we won't run. */\nexport interface CommandRejectedPayload {\n command: string;\n reason:\n | 'unknown-command'\n | 'rate-limited'\n | 'invalid-args'\n | 'handler-threw'\n | 'overridden';\n details?: string;\n}\n\nexport type DebugEvent =\n | (DebugEventBase & { type: 'console'; payload: ConsolePayload })\n | (DebugEventBase & { type: 'network'; payload: NetworkPayload })\n | (DebugEventBase & { type: 'error'; payload: ErrorPayload })\n | (DebugEventBase & { type: 'lifecycle'; payload: LifecyclePayload })\n | (DebugEventBase & { type: 'bridge'; payload: BridgePayload })\n | (DebugEventBase & { type: 'perf'; payload: PerfPayload })\n | (DebugEventBase & { type: 'command-ack'; payload: CommandAckPayload })\n | (DebugEventBase & { type: 'command-rejected'; payload: CommandRejectedPayload });\n\nexport interface UploadBatch {\n sessionToken: string;\n events: DebugEvent[];\n}\n\nexport interface UploadAck {\n acceptedSeq: number;\n rejected?: Array<{ seq: number; reason: string }>;\n}\n\nexport type StreamFrame =\n | { kind: 'hello'; sid: string; devices: number }\n | { kind: 'event'; event: DebugEvent }\n | { kind: 'device-join'; deviceInfo: DeviceInfo }\n | { kind: 'device-leave'; deviceId: string }\n | { kind: 'gap'; deviceId: string; expectedSeq: number; gotSeq: number }\n | { kind: 'session-end'; reason: string };\n\nexport interface DeviceInfo {\n deviceId: string;\n platform: 'ios' | 'android' | 'web' | 'unknown';\n osVersion?: string;\n appName?: string;\n appVersion?: string;\n appBuild?: string;\n userAgent?: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Build-time identity (resolved by the Vite plugin from `botim.{env}.json`)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type BotimEnv = 'dev' | 'uat' | 'beta' | 'prod';\n\n/**\n * The contents of `botim.{env}.json`, resolved at build time and injected\n * into the bundle via the virtual module `virtual:botim/config`.\n */\nexport interface BotimConfig {\n miniProgramId: string;\n env: BotimEnv;\n /** Short hash of the resolved config or the build, set by the plugin. */\n buildSignature: string;\n appName?: string;\n appVersion?: string;\n /** Forward-compatible bag for fields the relay may add later. */\n [extra: string]: unknown;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Consent\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface ConsentInput {\n /** JWT issued by the BOTIM host runtime — strongest form. */\n hostToken?: string;\n /** Explicit user opt-in (e.g., set after a privacy dialog). */\n userOptIn?: boolean;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Wire protocol — attach\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface AttachRequest {\n miniProgramId: string;\n env: BotimEnv;\n buildSignature: string;\n deviceInfo: DeviceInfo;\n consent: ConsentInput;\n schemaVersion: typeof SCHEMA_VERSION;\n}\n\nexport interface AttachResponse {\n sid: string;\n deviceToken: string;\n expiresAt: number;\n ingestUrl: string;\n /** Endpoint the device long-polls for AI-issued commands. */\n commandPollUrl: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Wire protocol — commands (relay → device → ack)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface CommandRequest {\n /** Relay-issued unique id, echoed in the resulting ack/rejected event. */\n id: string;\n /** Registered command name (kebab-case). */\n name: string;\n /** Arbitrary JSON args, validated by the handler. */\n args?: Record<string, unknown>;\n /** Optional deadline; handlers SHOULD respect it. */\n deadlineTs?: number;\n}\n\n/** Long-poll response shape from `commandPollUrl`. */\nexport interface CommandPollResponse {\n commands: CommandRequest[];\n /** Suggested seconds until the next poll. */\n nextDelayMs?: number;\n}\n\nexport type CommandHandler = (\n args: Record<string, unknown>,\n ctx: CommandContext,\n) => unknown | Promise<unknown>;\n\nexport interface CommandContext {\n commandId: string;\n command: string;\n signal: AbortSignal;\n}\n"]}
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/vite/plugin.ts
21
+ var plugin_exports = {};
22
+ __export(plugin_exports, {
23
+ BotimConfigError: () => BotimConfigError,
24
+ botimDebug: () => botimDebug,
25
+ resolveBotimConfig: () => resolveBotimConfig
26
+ });
27
+ module.exports = __toCommonJS(plugin_exports);
28
+
29
+ // src/vite/resolve-config.ts
30
+ var import_node_fs = require("fs");
31
+ var import_node_path = require("path");
32
+ var import_node_crypto = require("crypto");
33
+
34
+ // src/errors.ts
35
+ var BRAND = "@botim/debug-sdk";
36
+ var BotimConfigError = class extends Error {
37
+ constructor(message, opts = { code: "invalid-config" }) {
38
+ super(`[${BRAND}] ${message}`);
39
+ this.name = "BotimConfigError";
40
+ this.code = opts.code;
41
+ this.path = opts.path;
42
+ }
43
+ };
44
+
45
+ // src/vite/resolve-config.ts
46
+ var VALID_ENVS = ["dev", "uat", "beta", "prod"];
47
+ var ID_PATTERN = /^[a-z][a-z0-9_-]{1,63}$/i;
48
+ function defaultMapMode(mode) {
49
+ if (mode === "development") return "dev";
50
+ if (mode === "production") return "prod";
51
+ return mode;
52
+ }
53
+ function isBotimEnv(s) {
54
+ return VALID_ENVS.includes(s);
55
+ }
56
+ function resolveBotimConfig(mode, root, opts = {}) {
57
+ if (!mode) {
58
+ throw new BotimConfigError("mode is required", { code: "no-mode" });
59
+ }
60
+ const mapped = (opts.mapMode ?? defaultMapMode)(mode);
61
+ if (typeof mapped !== "string" || !isBotimEnv(mapped)) {
62
+ throw new BotimConfigError(
63
+ `mode "${mode}" mapped to "${mapped}" which is not a valid BotimEnv (expected one of: ${VALID_ENVS.join(", ")})`,
64
+ { code: "invalid-env" }
65
+ );
66
+ }
67
+ const env = mapped;
68
+ const projectRoot = (0, import_node_path.isAbsolute)(root) ? root : (0, import_node_path.resolve)(process.cwd(), root);
69
+ const filePath = (0, import_node_path.resolve)(projectRoot, `botim.${env}.json`);
70
+ if (!(0, import_node_fs.existsSync)(filePath)) {
71
+ throw new BotimConfigError(
72
+ `expected config file not found: ${filePath}
73
+ Create botim.${env}.json at the project root with at least { "mp_id": "..." }.`,
74
+ { code: "config-missing", path: filePath }
75
+ );
76
+ }
77
+ const raw = (0, import_node_fs.readFileSync)(filePath, "utf8");
78
+ let parsed;
79
+ try {
80
+ parsed = JSON.parse(raw);
81
+ } catch (err) {
82
+ const detail = err instanceof Error ? err.message : String(err);
83
+ throw new BotimConfigError(`failed to parse JSON in ${filePath}: ${detail}`, {
84
+ code: "config-invalid-json",
85
+ path: filePath
86
+ });
87
+ }
88
+ if (parsed.deleted === 1) {
89
+ throw new BotimConfigError(
90
+ `${filePath} reports the mini-program as deleted (deleted: 1). Cannot ship debug SDK against a retired mini-program.`,
91
+ { code: "config-deleted", path: filePath }
92
+ );
93
+ }
94
+ const idValue = parsed.mp_id ?? parsed.miniProgramId;
95
+ if (typeof idValue !== "string" || idValue.length === 0) {
96
+ throw new BotimConfigError(
97
+ `${filePath} is missing required field "mp_id" (must be a non-empty string)`,
98
+ { code: "config-missing-field", path: filePath }
99
+ );
100
+ }
101
+ if (!ID_PATTERN.test(idValue)) {
102
+ throw new BotimConfigError(
103
+ `${filePath} field "mp_id" must match ${ID_PATTERN.toString()} (got: ${JSON.stringify(idValue)})`,
104
+ { code: "config-invalid-id", path: filePath }
105
+ );
106
+ }
107
+ const md5 = typeof parsed.app?.md5 === "string" ? parsed.app.md5 : typeof parsed.package_url?.md5 === "string" ? parsed.package_url.md5 : void 0;
108
+ const versionForSig = typeof parsed.version === "string" && parsed.version || typeof parsed.app?.version === "string" && parsed.app.version || void 0;
109
+ let buildSignature;
110
+ if (typeof parsed.buildSignature === "string" && parsed.buildSignature.length > 0) {
111
+ buildSignature = parsed.buildSignature;
112
+ } else if (versionForSig && md5) {
113
+ buildSignature = `v${versionForSig}+md5:${md5.slice(0, 12)}`;
114
+ } else {
115
+ buildSignature = "sha256:" + (0, import_node_crypto.createHash)("sha256").update(raw).digest("hex").slice(0, 12);
116
+ }
117
+ const cfg = {
118
+ // Spread first so the canonical fields below override anything inherited.
119
+ ...parsed,
120
+ miniProgramId: idValue,
121
+ env,
122
+ buildSignature,
123
+ appName: typeof parsed.app_id === "string" ? parsed.app_id : parsed.appName,
124
+ appVersion: versionForSig ?? (typeof parsed.appVersion === "string" ? parsed.appVersion : void 0)
125
+ };
126
+ return cfg;
127
+ }
128
+
129
+ // src/vite/plugin.ts
130
+ var VIRTUAL_ID = "virtual:botim/config";
131
+ var RESOLVED_VIRTUAL_ID = "\0" + VIRTUAL_ID;
132
+ function botimDebug(options = {}) {
133
+ let resolved = null;
134
+ let resolvedFromMode = null;
135
+ let resolvedError = null;
136
+ return {
137
+ name: "@botim/debug-sdk:vite",
138
+ enforce: "pre",
139
+ configResolved(viteConfig) {
140
+ const mode = options.mode ?? viteConfig.mode;
141
+ const root = options.root ?? viteConfig.root;
142
+ try {
143
+ resolved = resolveBotimConfig(mode, root, { mapMode: options.mapMode });
144
+ if (options.relayUrl) {
145
+ resolved.relayUrl = options.relayUrl.replace(/\/+$/, "");
146
+ }
147
+ resolvedFromMode = mode;
148
+ } catch (err) {
149
+ resolved = null;
150
+ resolvedError = err instanceof Error ? err : new Error(String(err));
151
+ resolvedFromMode = mode;
152
+ }
153
+ },
154
+ resolveId(id) {
155
+ if (id === VIRTUAL_ID) return RESOLVED_VIRTUAL_ID;
156
+ return null;
157
+ },
158
+ load(id) {
159
+ if (id !== RESOLVED_VIRTUAL_ID) return null;
160
+ if (!resolved) {
161
+ const message = resolvedError?.message ?? `[@botim/debug-sdk] virtual:botim/config requested but configResolved did not run (mode=${resolvedFromMode ?? "unknown"})`;
162
+ this.error(message);
163
+ }
164
+ const body = `// AUTO-GENERATED by @botim/debug-sdk:vite \u2014 do not edit.
165
+ export const botimConfig = Object.freeze(${JSON.stringify(resolved)});
166
+ export default botimConfig;
167
+ `;
168
+ return { code: body, map: null };
169
+ }
170
+ };
171
+ }
172
+ // Annotate the CommonJS export names for ESM import in node:
173
+ 0 && (module.exports = {
174
+ BotimConfigError,
175
+ botimDebug,
176
+ resolveBotimConfig
177
+ });
178
+ //# sourceMappingURL=plugin.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/vite/plugin.ts","../../src/vite/resolve-config.ts","../../src/errors.ts"],"sourcesContent":["/**\n * Vite plugin for `@botim/debug-sdk`.\n *\n * - Reads `botim.{mode}.json` from the consumer's project root at build time.\n * - Validates required fields; fails the build loudly if anything is wrong.\n * - Exposes the resolved config via the virtual module `virtual:botim/config`\n * as `export const botimConfig: Readonly<BotimConfig>`.\n *\n * The plugin's only Vite-specific surface is the `Plugin` type; we keep the\n * import as `type-only` so `vite` remains an *optional* peer dependency. A\n * consumer that never installs Vite will never load this file (it's only\n * reachable via the `./vite` subpath export).\n */\n\nimport type { Plugin } from 'vite';\nimport { resolveBotimConfig } from './resolve-config.js';\nimport type { BotimConfig, BotimEnv } from '../types.js';\n\nconst VIRTUAL_ID = 'virtual:botim/config';\nconst RESOLVED_VIRTUAL_ID = '\\0' + VIRTUAL_ID;\n\nexport interface BotimDebugPluginOptions {\n /**\n * Override Vite's `mode`. Useful when the build is driven outside Vite's\n * usual `--mode` flag, or in tests. If omitted, the plugin uses the mode\n * Vite resolves from the CLI / config.\n */\n mode?: string;\n /**\n * Map Vite's mode to a BotimEnv. Defaults: `development` → `dev`,\n * `production` → `prod`, others pass through verbatim.\n */\n mapMode?: (mode: string) => BotimEnv | string;\n /**\n * Project root override. Defaults to Vite's resolved `config.root`.\n */\n root?: string;\n /**\n * Relay endpoint to bake into `virtual:botim/config` as `relayUrl`. The\n * SDK reads this at runtime so the host app can call:\n *\n * enableRemoteDebug({ endpoint: botimConfig.relayUrl, … })\n *\n * Without this option, hosts must either set up a Vite proxy\n * (`server.proxy: { '/v1': 'http://localhost:8090' }`) and pass\n * `endpoint: location.origin`, OR pass an explicit URL at the call\n * site. Passing it through the plugin centralises the wiring and makes\n * mode-specific URLs trivial: read process.env.RELAY_URL in\n * vite.config and forward it here.\n *\n * The relay must allow the page's origin via CORS (CORS_ORIGINS env on\n * @botim/debug-relay defaults to \"*\"). Trailing slash is stripped at\n * resolve time so `/v1/attach` joins cleanly.\n */\n relayUrl?: string;\n}\n\nexport function botimDebug(options: BotimDebugPluginOptions = {}): Plugin {\n let resolved: BotimConfig | null = null;\n let resolvedFromMode: string | null = null;\n let resolvedError: Error | null = null;\n\n return {\n name: '@botim/debug-sdk:vite',\n enforce: 'pre',\n\n configResolved(viteConfig) {\n const mode = options.mode ?? viteConfig.mode;\n const root = options.root ?? viteConfig.root;\n try {\n resolved = resolveBotimConfig(mode, root, { mapMode: options.mapMode });\n // Merge the host-provided relay URL into the resolved config bag.\n // Strip trailing slash so callers don't get `/v1//attach` if they\n // pass `https://relay.example.com/`.\n if (options.relayUrl) {\n (resolved as BotimConfig & { relayUrl: string }).relayUrl =\n options.relayUrl.replace(/\\/+$/, '');\n }\n resolvedFromMode = mode;\n } catch (err) {\n // Stash the error; surface it the moment a build module imports the\n // virtual module. We don't throw from `configResolved` directly because\n // some consumers run Vite for non-build tasks (e.g. `vite-node` for\n // tooling) where the virtual module is never imported and a hard fail\n // would be a regression.\n resolved = null;\n resolvedError = err instanceof Error ? err : new Error(String(err));\n resolvedFromMode = mode;\n }\n },\n\n resolveId(id) {\n if (id === VIRTUAL_ID) return RESOLVED_VIRTUAL_ID;\n return null;\n },\n\n load(id) {\n if (id !== RESOLVED_VIRTUAL_ID) return null;\n\n if (!resolved) {\n const message =\n resolvedError?.message ??\n `[@botim/debug-sdk] virtual:botim/config requested but configResolved did not run (mode=${resolvedFromMode ?? 'unknown'})`;\n // `this.error` aborts the build with a properly attributed Vite error.\n this.error(message);\n }\n\n // Frozen so a runtime mutation can't accidentally taint the constant.\n const body =\n `// AUTO-GENERATED by @botim/debug-sdk:vite — do not edit.\\n` +\n `export const botimConfig = Object.freeze(${JSON.stringify(resolved)});\\n` +\n `export default botimConfig;\\n`;\n return { code: body, map: null };\n },\n };\n}\n\nexport { resolveBotimConfig } from './resolve-config.js';\nexport { BotimConfigError } from '../errors.js';\nexport type { BotimConfig, BotimEnv } from '../types.js';\n","/**\n * Build-time resolver for `botim.{env}.json`.\n *\n * The real BOTIM config schema is large and platform-managed (logos, package\n * URLs, framework metadata, grayscale rollout, etc). The SDK only needs a few\n * canonical fields, so this resolver acts as a *normalizer*:\n *\n * external schema → SDK-internal `BotimConfig`\n * ─────────────── ──────────────────────────\n * mp_id → miniProgramId\n * app_id → appId (extra; preserved)\n * version → appVersion\n * app.md5 | package_url.md5 → buildSignature (deterministic per release)\n * <env from filename> → env\n *\n * Reads `botim.{env}.json` from the consumer's project root. Build-time only:\n * uses Node `fs`/`crypto`. Must NEVER be imported from device-targeted code.\n */\n\nimport { readFileSync, existsSync } from 'node:fs';\nimport { resolve, isAbsolute } from 'node:path';\nimport { createHash } from 'node:crypto';\n\nimport { BotimConfigError } from '../errors.js';\nimport type { BotimConfig, BotimEnv } from '../types.js';\n\nconst VALID_ENVS: readonly BotimEnv[] = ['dev', 'uat', 'beta', 'prod'];\n/**\n * Mini-program ids in the real config are short kebab/snake-case slugs like\n * `mbrx_p2p` — case-insensitive, allows `_` and `-`, must start with a letter.\n */\nconst ID_PATTERN = /^[a-z][a-z0-9_-]{1,63}$/i;\n\nexport interface ResolveOptions {\n /** Map a Vite mode (e.g. \"development\", \"staging\") to a BotimEnv. */\n mapMode?: (mode: string) => BotimEnv | string;\n}\n\n/** Built-in mapping; consumers can override via `mapMode`. */\nfunction defaultMapMode(mode: string): string {\n if (mode === 'development') return 'dev';\n if (mode === 'production') return 'prod';\n return mode;\n}\n\nfunction isBotimEnv(s: string): s is BotimEnv {\n return (VALID_ENVS as readonly string[]).includes(s);\n}\n\ninterface RawBotimFile {\n /** Canonical mini-program id used by the BOTIM platform, e.g. \"mbrx_p2p\". */\n mp_id?: string;\n /** Reverse-DNS app id, e.g. \"me.botim.rd.p2p\". Different concept from mp_id. */\n app_id?: string;\n /** Release version of the mini-program package, e.g. \"0.0.127\". */\n version?: string;\n /** Optional override; otherwise we derive from version+md5. */\n buildSignature?: string;\n /** Per-package metadata; when present we use `app.md5` for the signature. */\n app?: { version?: string; md5?: string; updateUrl?: string };\n /** Older/alternate location for the same md5. */\n package_url?: { md5?: string; version?: string };\n /** Operational flags maintained by the platform. */\n disabled?: number;\n deleted?: number;\n mp_status?: number;\n /** Forward-compat for everything else (logos, framework, grayscale, ...). */\n [extra: string]: unknown;\n /** Legacy alias accepted for forward/back compat. */\n miniProgramId?: string;\n}\n\n/**\n * Resolve `botim.{env}.json` for the given Vite `mode` from `root`.\n *\n * Throws `BotimConfigError` synchronously on:\n * - unknown env after `mapMode` is applied\n * - missing or unreadable file\n * - invalid JSON\n * - missing/invalid `mp_id` (or its legacy `miniProgramId` alias)\n * - the platform marking the mini-program as deleted (`deleted: 1`)\n */\nexport function resolveBotimConfig(\n mode: string,\n root: string,\n opts: ResolveOptions = {},\n): BotimConfig {\n if (!mode) {\n throw new BotimConfigError('mode is required', { code: 'no-mode' });\n }\n\n const mapped = (opts.mapMode ?? defaultMapMode)(mode);\n if (typeof mapped !== 'string' || !isBotimEnv(mapped)) {\n throw new BotimConfigError(\n `mode \"${mode}\" mapped to \"${mapped}\" which is not a valid BotimEnv (expected one of: ${VALID_ENVS.join(', ')})`,\n { code: 'invalid-env' },\n );\n }\n const env: BotimEnv = mapped;\n\n const projectRoot = isAbsolute(root) ? root : resolve(process.cwd(), root);\n const filePath = resolve(projectRoot, `botim.${env}.json`);\n\n if (!existsSync(filePath)) {\n throw new BotimConfigError(\n `expected config file not found: ${filePath}\\n` +\n ` Create botim.${env}.json at the project root with at least { \"mp_id\": \"...\" }.`,\n { code: 'config-missing', path: filePath },\n );\n }\n\n const raw = readFileSync(filePath, 'utf8');\n let parsed: RawBotimFile;\n try {\n parsed = JSON.parse(raw) as RawBotimFile;\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new BotimConfigError(`failed to parse JSON in ${filePath}: ${detail}`, {\n code: 'config-invalid-json',\n path: filePath,\n });\n }\n\n // Hard-stop on a decommissioned mini-program — refusing the build is far\n // safer than shipping a debug SDK that targets a relay slot the platform\n // has retired.\n if (parsed.deleted === 1) {\n throw new BotimConfigError(\n `${filePath} reports the mini-program as deleted (deleted: 1). Cannot ship debug SDK against a retired mini-program.`,\n { code: 'config-deleted', path: filePath },\n );\n }\n\n // Read the canonical id; fall back to the SDK-internal alias for repos that\n // hand-write a config without the platform's `mp_id` field.\n const idValue = (parsed.mp_id ?? parsed.miniProgramId) as unknown;\n if (typeof idValue !== 'string' || idValue.length === 0) {\n throw new BotimConfigError(\n `${filePath} is missing required field \"mp_id\" (must be a non-empty string)`,\n { code: 'config-missing-field', path: filePath },\n );\n }\n if (!ID_PATTERN.test(idValue)) {\n throw new BotimConfigError(\n `${filePath} field \"mp_id\" must match ${ID_PATTERN.toString()} (got: ${JSON.stringify(idValue)})`,\n { code: 'config-invalid-id', path: filePath },\n );\n }\n\n // ── Derive a stable, release-correlated buildSignature ────────────────────\n // Preference order:\n // 1. explicit buildSignature in the file (consumers may pin one)\n // 2. version + package md5 (deterministic per release; survives unrelated\n // platform-side mutations like update_time / mp_status_time)\n // 3. sha256 of the file contents (last-resort fallback)\n const md5 =\n typeof parsed.app?.md5 === 'string'\n ? parsed.app.md5\n : typeof parsed.package_url?.md5 === 'string'\n ? parsed.package_url.md5\n : undefined;\n const versionForSig =\n (typeof parsed.version === 'string' && parsed.version) ||\n (typeof parsed.app?.version === 'string' && parsed.app.version) ||\n undefined;\n\n let buildSignature: string;\n if (typeof parsed.buildSignature === 'string' && parsed.buildSignature.length > 0) {\n buildSignature = parsed.buildSignature;\n } else if (versionForSig && md5) {\n buildSignature = `v${versionForSig}+md5:${md5.slice(0, 12)}`;\n } else {\n buildSignature = 'sha256:' + createHash('sha256').update(raw).digest('hex').slice(0, 12);\n }\n\n const cfg: BotimConfig = {\n // Spread first so the canonical fields below override anything inherited.\n ...parsed,\n miniProgramId: idValue,\n env,\n buildSignature,\n appName: typeof parsed.app_id === 'string' ? parsed.app_id : (parsed.appName as string | undefined),\n appVersion:\n versionForSig ?? (typeof parsed.appVersion === 'string' ? parsed.appVersion : undefined),\n };\n\n return cfg;\n}\n\nexport const __test__ = { defaultMapMode, ID_PATTERN, VALID_ENVS };\n","/**\n * Errors that escape the SDK into host code.\n *\n * The SDK's no-throw invariant has exactly two carve-outs:\n * - `BotimConfigError` — thrown synchronously by `enableRemoteDebug` when the\n * resolved `BotimConfig` is missing or invalid. Prevents any I/O.\n * - `BotimConsentError` — thrown synchronously by `enableRemoteDebug` when\n * consent has not been granted in a `prod` build. Prevents any I/O.\n *\n * Both are intentionally synchronous and pre-installation: at the moment they\n * fire, no globals have been wrapped, no listeners installed, no network sent.\n */\n\nconst BRAND = '@botim/debug-sdk' as const;\n\nexport class BotimConfigError extends Error {\n readonly name = 'BotimConfigError';\n readonly code: string;\n readonly path?: string;\n\n constructor(message: string, opts: { code: string; path?: string } = { code: 'invalid-config' }) {\n super(`[${BRAND}] ${message}`);\n this.code = opts.code;\n this.path = opts.path;\n }\n}\n\nexport class BotimConsentError extends Error {\n readonly name = 'BotimConsentError';\n constructor(message: string) {\n super(`[${BRAND}] ${message}`);\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,127 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ type BotimEnv = 'dev' | 'uat' | 'beta' | 'prod';
4
+ /**
5
+ * The contents of `botim.{env}.json`, resolved at build time and injected
6
+ * into the bundle via the virtual module `virtual:botim/config`.
7
+ */
8
+ interface BotimConfig {
9
+ miniProgramId: string;
10
+ env: BotimEnv;
11
+ /** Short hash of the resolved config or the build, set by the plugin. */
12
+ buildSignature: string;
13
+ appName?: string;
14
+ appVersion?: string;
15
+ /** Forward-compatible bag for fields the relay may add later. */
16
+ [extra: string]: unknown;
17
+ }
18
+
19
+ /**
20
+ * Build-time resolver for `botim.{env}.json`.
21
+ *
22
+ * The real BOTIM config schema is large and platform-managed (logos, package
23
+ * URLs, framework metadata, grayscale rollout, etc). The SDK only needs a few
24
+ * canonical fields, so this resolver acts as a *normalizer*:
25
+ *
26
+ * external schema → SDK-internal `BotimConfig`
27
+ * ─────────────── ──────────────────────────
28
+ * mp_id → miniProgramId
29
+ * app_id → appId (extra; preserved)
30
+ * version → appVersion
31
+ * app.md5 | package_url.md5 → buildSignature (deterministic per release)
32
+ * <env from filename> → env
33
+ *
34
+ * Reads `botim.{env}.json` from the consumer's project root. Build-time only:
35
+ * uses Node `fs`/`crypto`. Must NEVER be imported from device-targeted code.
36
+ */
37
+
38
+ interface ResolveOptions {
39
+ /** Map a Vite mode (e.g. "development", "staging") to a BotimEnv. */
40
+ mapMode?: (mode: string) => BotimEnv | string;
41
+ }
42
+ /**
43
+ * Resolve `botim.{env}.json` for the given Vite `mode` from `root`.
44
+ *
45
+ * Throws `BotimConfigError` synchronously on:
46
+ * - unknown env after `mapMode` is applied
47
+ * - missing or unreadable file
48
+ * - invalid JSON
49
+ * - missing/invalid `mp_id` (or its legacy `miniProgramId` alias)
50
+ * - the platform marking the mini-program as deleted (`deleted: 1`)
51
+ */
52
+ declare function resolveBotimConfig(mode: string, root: string, opts?: ResolveOptions): BotimConfig;
53
+
54
+ /**
55
+ * Errors that escape the SDK into host code.
56
+ *
57
+ * The SDK's no-throw invariant has exactly two carve-outs:
58
+ * - `BotimConfigError` — thrown synchronously by `enableRemoteDebug` when the
59
+ * resolved `BotimConfig` is missing or invalid. Prevents any I/O.
60
+ * - `BotimConsentError` — thrown synchronously by `enableRemoteDebug` when
61
+ * consent has not been granted in a `prod` build. Prevents any I/O.
62
+ *
63
+ * Both are intentionally synchronous and pre-installation: at the moment they
64
+ * fire, no globals have been wrapped, no listeners installed, no network sent.
65
+ */
66
+ declare class BotimConfigError extends Error {
67
+ readonly name = "BotimConfigError";
68
+ readonly code: string;
69
+ readonly path?: string;
70
+ constructor(message: string, opts?: {
71
+ code: string;
72
+ path?: string;
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Vite plugin for `@botim/debug-sdk`.
78
+ *
79
+ * - Reads `botim.{mode}.json` from the consumer's project root at build time.
80
+ * - Validates required fields; fails the build loudly if anything is wrong.
81
+ * - Exposes the resolved config via the virtual module `virtual:botim/config`
82
+ * as `export const botimConfig: Readonly<BotimConfig>`.
83
+ *
84
+ * The plugin's only Vite-specific surface is the `Plugin` type; we keep the
85
+ * import as `type-only` so `vite` remains an *optional* peer dependency. A
86
+ * consumer that never installs Vite will never load this file (it's only
87
+ * reachable via the `./vite` subpath export).
88
+ */
89
+
90
+ interface BotimDebugPluginOptions {
91
+ /**
92
+ * Override Vite's `mode`. Useful when the build is driven outside Vite's
93
+ * usual `--mode` flag, or in tests. If omitted, the plugin uses the mode
94
+ * Vite resolves from the CLI / config.
95
+ */
96
+ mode?: string;
97
+ /**
98
+ * Map Vite's mode to a BotimEnv. Defaults: `development` → `dev`,
99
+ * `production` → `prod`, others pass through verbatim.
100
+ */
101
+ mapMode?: (mode: string) => BotimEnv | string;
102
+ /**
103
+ * Project root override. Defaults to Vite's resolved `config.root`.
104
+ */
105
+ root?: string;
106
+ /**
107
+ * Relay endpoint to bake into `virtual:botim/config` as `relayUrl`. The
108
+ * SDK reads this at runtime so the host app can call:
109
+ *
110
+ * enableRemoteDebug({ endpoint: botimConfig.relayUrl, … })
111
+ *
112
+ * Without this option, hosts must either set up a Vite proxy
113
+ * (`server.proxy: { '/v1': 'http://localhost:8090' }`) and pass
114
+ * `endpoint: location.origin`, OR pass an explicit URL at the call
115
+ * site. Passing it through the plugin centralises the wiring and makes
116
+ * mode-specific URLs trivial: read process.env.RELAY_URL in
117
+ * vite.config and forward it here.
118
+ *
119
+ * The relay must allow the page's origin via CORS (CORS_ORIGINS env on
120
+ * @botim/debug-relay defaults to "*"). Trailing slash is stripped at
121
+ * resolve time so `/v1/attach` joins cleanly.
122
+ */
123
+ relayUrl?: string;
124
+ }
125
+ declare function botimDebug(options?: BotimDebugPluginOptions): Plugin;
126
+
127
+ export { type BotimConfig, BotimConfigError, type BotimDebugPluginOptions, type BotimEnv, botimDebug, resolveBotimConfig };