@botim/mp-debug-sdk 0.6.2 → 0.7.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.
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { EventType, EventLevel, ConsolePayload, NetworkPayload, ErrorPayload, CommandHandler, BotimConfig, ConsentInput, DeviceInfo } from './types.cjs';
1
+ import { ConsentPromptCopy, EventType, EventLevel, ConsolePayload, NetworkPayload, ErrorPayload, CommandHandler, BotimConfig, ConsentInput, DeviceInfo } from './types.cjs';
2
2
  export { AttachRequest, AttachResponse, BotimEnv, BridgePayload, CommandAckPayload, CommandContext, CommandPollResponse, CommandRejectedPayload, CommandRequest, DebugEvent, DebugEventBase, ErrorSource, EventMeta, LifecycleEvent, LifecyclePayload, NetworkPhase, PerfPayload, SCHEMA_VERSION, SerializedValue, StreamFrame, UploadAck, UploadBatch } from './types.cjs';
3
3
 
4
4
  /**
@@ -27,6 +27,62 @@ declare class BotimConsentError extends Error {
27
27
  constructor(message: string);
28
28
  }
29
29
 
30
+ /**
31
+ * In-app consent modal for the debug SDK.
32
+ *
33
+ * Shown on first load when the host opts in via `consent.promptUser:
34
+ * true`. Persists the user's choice in `localStorage` so subsequent
35
+ * loads attach silently (or stay disabled, if declined).
36
+ *
37
+ * Why hand-rolled DOM/CSS instead of pulling in a UI library:
38
+ * • The SDK ships into BOTIM mini-programs whose own bundles already
39
+ * include framework code — adding React/Vue/Lit just for this one
40
+ * dialog would inflate the SDK by an order of magnitude.
41
+ * • The modal must work on any host (Angular, vanilla, Web
42
+ * Components, future frameworks). Plain DOM is the universal
43
+ * baseline.
44
+ * • Style isolation via inline styles + a unique class prefix
45
+ * (`__botim-debug-consent-`) avoids collisions with whatever
46
+ * stylesheet the host has loaded. We deliberately avoid the
47
+ * simpler `style=""` attribute on every element — bundles using
48
+ * CSP `style-src` directives without `'unsafe-inline'` would
49
+ * reject those, breaking the very debug flow we're trying to
50
+ * enable. Instead we inject one <style> with `!important` rules
51
+ * into <head>, which CSP `style-src 'self'` allows.
52
+ *
53
+ * Trust model: this UI is for user-facing transparency, NOT a
54
+ * cryptographic gate. A motivated attacker who controls the page can
55
+ * trivially write to `localStorage` and fake `granted`. The actual
56
+ * defence against unauthorized debug attach is "the SDK isn't shipped
57
+ * in production builds." Treat the consent record like a "remember
58
+ * me" cookie, not a session token.
59
+ */
60
+
61
+ type ConsentDecision = 'granted' | 'denied';
62
+ /**
63
+ * Read the cached consent decision for this mp_id. Returns `null` when
64
+ * no decision has been recorded — the caller should prompt.
65
+ *
66
+ * `localStorage.getItem` can throw in some sandboxed contexts (Safari
67
+ * Private Mode historically; some embedded WebViews). Failures are
68
+ * coerced to `null` so the prompt re-shows rather than crashing the
69
+ * SDK on storage hiccups.
70
+ */
71
+ declare function readConsentDecision(mpId: string): ConsentDecision | null;
72
+ /**
73
+ * Persist a decision. Failures are swallowed for the same reason as
74
+ * `readConsentDecision` — on storage-hostile platforms the SDK should
75
+ * still function for the current session, even if the prompt re-shows
76
+ * next time.
77
+ */
78
+ declare function writeConsentDecision(mpId: string, decision: ConsentDecision): void;
79
+ /**
80
+ * Clear the cached decision for an mp_id. Exposed so a host can wire
81
+ * a "Revoke debug consent" button into its own settings UI.
82
+ */
83
+ declare function clearConsentDecision(mpId: string): void;
84
+ declare function promptForConsent(mpId: string, copy?: ConsentPromptCopy): Promise<ConsentDecision>;
85
+
30
86
  /**
31
87
  * Built-in commands registered automatically when `enableRemoteDebug` runs.
32
88
  *
@@ -242,4 +298,4 @@ declare const DEFAULT_BUFFER_SIZE = 1000;
242
298
  declare const DEFAULT_MAX_BATCH_SIZE = 50;
243
299
  declare function enableRemoteDebug(options: RemoteDebugOptions): Promise<RemoteDebugHandle>;
244
300
 
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 };
301
+ export { type AppInfo, BotimConfig, BotimConfigError, BotimConsentError, type BuiltinHostHooks, CommandHandler, type ConsentDecision, ConsentInput, ConsentPromptCopy, 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, clearConsentDecision, enableRemoteDebug, getBOT, promptForConsent, readConsentDecision, writeConsentDecision };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { EventType, EventLevel, ConsolePayload, NetworkPayload, ErrorPayload, CommandHandler, BotimConfig, ConsentInput, DeviceInfo } from './types.js';
1
+ import { ConsentPromptCopy, EventType, EventLevel, ConsolePayload, NetworkPayload, ErrorPayload, CommandHandler, BotimConfig, ConsentInput, DeviceInfo } from './types.js';
2
2
  export { AttachRequest, AttachResponse, BotimEnv, BridgePayload, CommandAckPayload, CommandContext, CommandPollResponse, CommandRejectedPayload, CommandRequest, DebugEvent, DebugEventBase, ErrorSource, EventMeta, LifecycleEvent, LifecyclePayload, NetworkPhase, PerfPayload, SCHEMA_VERSION, SerializedValue, StreamFrame, UploadAck, UploadBatch } from './types.js';
3
3
 
4
4
  /**
@@ -27,6 +27,62 @@ declare class BotimConsentError extends Error {
27
27
  constructor(message: string);
28
28
  }
29
29
 
30
+ /**
31
+ * In-app consent modal for the debug SDK.
32
+ *
33
+ * Shown on first load when the host opts in via `consent.promptUser:
34
+ * true`. Persists the user's choice in `localStorage` so subsequent
35
+ * loads attach silently (or stay disabled, if declined).
36
+ *
37
+ * Why hand-rolled DOM/CSS instead of pulling in a UI library:
38
+ * • The SDK ships into BOTIM mini-programs whose own bundles already
39
+ * include framework code — adding React/Vue/Lit just for this one
40
+ * dialog would inflate the SDK by an order of magnitude.
41
+ * • The modal must work on any host (Angular, vanilla, Web
42
+ * Components, future frameworks). Plain DOM is the universal
43
+ * baseline.
44
+ * • Style isolation via inline styles + a unique class prefix
45
+ * (`__botim-debug-consent-`) avoids collisions with whatever
46
+ * stylesheet the host has loaded. We deliberately avoid the
47
+ * simpler `style=""` attribute on every element — bundles using
48
+ * CSP `style-src` directives without `'unsafe-inline'` would
49
+ * reject those, breaking the very debug flow we're trying to
50
+ * enable. Instead we inject one <style> with `!important` rules
51
+ * into <head>, which CSP `style-src 'self'` allows.
52
+ *
53
+ * Trust model: this UI is for user-facing transparency, NOT a
54
+ * cryptographic gate. A motivated attacker who controls the page can
55
+ * trivially write to `localStorage` and fake `granted`. The actual
56
+ * defence against unauthorized debug attach is "the SDK isn't shipped
57
+ * in production builds." Treat the consent record like a "remember
58
+ * me" cookie, not a session token.
59
+ */
60
+
61
+ type ConsentDecision = 'granted' | 'denied';
62
+ /**
63
+ * Read the cached consent decision for this mp_id. Returns `null` when
64
+ * no decision has been recorded — the caller should prompt.
65
+ *
66
+ * `localStorage.getItem` can throw in some sandboxed contexts (Safari
67
+ * Private Mode historically; some embedded WebViews). Failures are
68
+ * coerced to `null` so the prompt re-shows rather than crashing the
69
+ * SDK on storage hiccups.
70
+ */
71
+ declare function readConsentDecision(mpId: string): ConsentDecision | null;
72
+ /**
73
+ * Persist a decision. Failures are swallowed for the same reason as
74
+ * `readConsentDecision` — on storage-hostile platforms the SDK should
75
+ * still function for the current session, even if the prompt re-shows
76
+ * next time.
77
+ */
78
+ declare function writeConsentDecision(mpId: string, decision: ConsentDecision): void;
79
+ /**
80
+ * Clear the cached decision for an mp_id. Exposed so a host can wire
81
+ * a "Revoke debug consent" button into its own settings UI.
82
+ */
83
+ declare function clearConsentDecision(mpId: string): void;
84
+ declare function promptForConsent(mpId: string, copy?: ConsentPromptCopy): Promise<ConsentDecision>;
85
+
30
86
  /**
31
87
  * Built-in commands registered automatically when `enableRemoteDebug` runs.
32
88
  *
@@ -242,4 +298,4 @@ declare const DEFAULT_BUFFER_SIZE = 1000;
242
298
  declare const DEFAULT_MAX_BATCH_SIZE = 50;
243
299
  declare function enableRemoteDebug(options: RemoteDebugOptions): Promise<RemoteDebugHandle>;
244
300
 
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 };
301
+ export { type AppInfo, BotimConfig, BotimConfigError, BotimConsentError, type BuiltinHostHooks, CommandHandler, type ConsentDecision, ConsentInput, ConsentPromptCopy, 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, clearConsentDecision, enableRemoteDebug, getBOT, promptForConsent, readConsentDecision, writeConsentDecision };
package/dist/index.js CHANGED
@@ -20,6 +20,214 @@ var BotimConsentError = class extends Error {
20
20
  }
21
21
  };
22
22
 
23
+ // src/consent-ui.ts
24
+ var STORAGE_PREFIX = "__botim_debug_consent_";
25
+ var CSS_PREFIX = "__botim-debug-consent";
26
+ var STYLE_ELEMENT_ID = `${CSS_PREFIX}-styles`;
27
+ var DEFAULT_COPY = {
28
+ title: "Enable BOTIM debug logging?",
29
+ body: [
30
+ "This shares your screen content, console logs, and network calls with the BOTIM team to help debug this mini-program.",
31
+ "No data leaves your device unless you agree. You can revoke this anytime by clearing site data or via developer tools."
32
+ ],
33
+ acceptLabel: "Allow",
34
+ declineLabel: "Skip"
35
+ };
36
+ function readConsentDecision(mpId) {
37
+ if (typeof localStorage === "undefined") return null;
38
+ try {
39
+ const raw = localStorage.getItem(STORAGE_PREFIX + mpId);
40
+ if (raw === "granted" || raw === "denied") return raw;
41
+ return null;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+ function writeConsentDecision(mpId, decision) {
47
+ if (typeof localStorage === "undefined") return;
48
+ try {
49
+ localStorage.setItem(STORAGE_PREFIX + mpId, decision);
50
+ } catch {
51
+ }
52
+ }
53
+ function clearConsentDecision(mpId) {
54
+ if (typeof localStorage === "undefined") return;
55
+ try {
56
+ localStorage.removeItem(STORAGE_PREFIX + mpId);
57
+ } catch {
58
+ }
59
+ }
60
+ var inflightPrompt = null;
61
+ async function promptForConsent(mpId, copy = {}) {
62
+ if (inflightPrompt) return inflightPrompt;
63
+ if (typeof document === "undefined" || typeof window === "undefined") {
64
+ return "denied";
65
+ }
66
+ const merged = {
67
+ title: copy.title ?? DEFAULT_COPY.title,
68
+ body: copy.body ?? DEFAULT_COPY.body,
69
+ acceptLabel: copy.acceptLabel ?? DEFAULT_COPY.acceptLabel,
70
+ declineLabel: copy.declineLabel ?? DEFAULT_COPY.declineLabel
71
+ };
72
+ inflightPrompt = new Promise((resolve) => {
73
+ injectStylesheet();
74
+ const overlay = buildOverlay(merged, (decision) => {
75
+ writeConsentDecision(mpId, decision);
76
+ try {
77
+ overlay.remove();
78
+ } catch {
79
+ }
80
+ inflightPrompt = null;
81
+ resolve(decision);
82
+ });
83
+ document.body.appendChild(overlay);
84
+ });
85
+ return inflightPrompt;
86
+ }
87
+ function injectStylesheet() {
88
+ if (document.getElementById(STYLE_ELEMENT_ID)) return;
89
+ const style = document.createElement("style");
90
+ style.id = STYLE_ELEMENT_ID;
91
+ style.textContent = `
92
+ .${CSS_PREFIX}-overlay {
93
+ position: fixed !important;
94
+ inset: 0 !important;
95
+ z-index: 2147483647 !important;
96
+ display: flex !important;
97
+ align-items: flex-end !important;
98
+ justify-content: center !important;
99
+ background: rgba(0, 0, 0, 0.45) !important;
100
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
101
+ Roboto, "Helvetica Neue", sans-serif !important;
102
+ animation: ${CSS_PREFIX}-fade-in 160ms ease-out !important;
103
+ box-sizing: border-box !important;
104
+ padding: 16px !important;
105
+ }
106
+ @media (min-width: 480px) {
107
+ .${CSS_PREFIX}-overlay {
108
+ align-items: center !important;
109
+ }
110
+ }
111
+ @keyframes ${CSS_PREFIX}-fade-in {
112
+ from { opacity: 0; }
113
+ to { opacity: 1; }
114
+ }
115
+ .${CSS_PREFIX}-card {
116
+ background: #ffffff !important;
117
+ color: #0e1116 !important;
118
+ border-radius: 16px !important;
119
+ max-width: 420px !important;
120
+ width: 100% !important;
121
+ padding: 20px 20px 16px !important;
122
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18) !important;
123
+ box-sizing: border-box !important;
124
+ }
125
+ .${CSS_PREFIX}-title {
126
+ font-size: 17px !important;
127
+ font-weight: 600 !important;
128
+ margin: 0 0 12px !important;
129
+ line-height: 1.3 !important;
130
+ }
131
+ .${CSS_PREFIX}-body {
132
+ font-size: 14px !important;
133
+ line-height: 1.5 !important;
134
+ color: #4a5160 !important;
135
+ margin: 0 0 8px !important;
136
+ }
137
+ .${CSS_PREFIX}-actions {
138
+ display: flex !important;
139
+ justify-content: flex-end !important;
140
+ gap: 8px !important;
141
+ margin-top: 16px !important;
142
+ }
143
+ .${CSS_PREFIX}-btn {
144
+ font-family: inherit !important;
145
+ font-size: 14px !important;
146
+ font-weight: 500 !important;
147
+ padding: 9px 16px !important;
148
+ border-radius: 10px !important;
149
+ border: none !important;
150
+ cursor: pointer !important;
151
+ line-height: 1 !important;
152
+ min-height: 36px !important;
153
+ box-sizing: border-box !important;
154
+ }
155
+ .${CSS_PREFIX}-btn-decline {
156
+ background: transparent !important;
157
+ color: #4a5160 !important;
158
+ }
159
+ .${CSS_PREFIX}-btn-decline:hover {
160
+ background: #f3f4f6 !important;
161
+ }
162
+ .${CSS_PREFIX}-btn-accept {
163
+ background: #0066ff !important;
164
+ color: #ffffff !important;
165
+ }
166
+ .${CSS_PREFIX}-btn-accept:hover {
167
+ background: #0055d4 !important;
168
+ }
169
+ @media (prefers-color-scheme: dark) {
170
+ .${CSS_PREFIX}-card {
171
+ background: #1c1f24 !important;
172
+ color: #e8eaed !important;
173
+ }
174
+ .${CSS_PREFIX}-body {
175
+ color: #b9bec7 !important;
176
+ }
177
+ .${CSS_PREFIX}-btn-decline {
178
+ color: #b9bec7 !important;
179
+ }
180
+ .${CSS_PREFIX}-btn-decline:hover {
181
+ background: #2a2e35 !important;
182
+ }
183
+ }
184
+ `;
185
+ document.head.appendChild(style);
186
+ }
187
+ function buildOverlay(copy, onDecision) {
188
+ const overlay = document.createElement("div");
189
+ overlay.className = `${CSS_PREFIX}-overlay`;
190
+ overlay.setAttribute("role", "dialog");
191
+ overlay.setAttribute("aria-modal", "true");
192
+ overlay.setAttribute("aria-labelledby", `${CSS_PREFIX}-title`);
193
+ const card = document.createElement("div");
194
+ card.className = `${CSS_PREFIX}-card`;
195
+ const title = document.createElement("h2");
196
+ title.id = `${CSS_PREFIX}-title`;
197
+ title.className = `${CSS_PREFIX}-title`;
198
+ title.textContent = copy.title;
199
+ card.appendChild(title);
200
+ for (const para of copy.body) {
201
+ const p = document.createElement("p");
202
+ p.className = `${CSS_PREFIX}-body`;
203
+ p.textContent = para;
204
+ card.appendChild(p);
205
+ }
206
+ const actions = document.createElement("div");
207
+ actions.className = `${CSS_PREFIX}-actions`;
208
+ const declineBtn = document.createElement("button");
209
+ declineBtn.type = "button";
210
+ declineBtn.className = `${CSS_PREFIX}-btn ${CSS_PREFIX}-btn-decline`;
211
+ declineBtn.textContent = copy.declineLabel;
212
+ declineBtn.addEventListener("click", () => onDecision("denied"));
213
+ const acceptBtn = document.createElement("button");
214
+ acceptBtn.type = "button";
215
+ acceptBtn.className = `${CSS_PREFIX}-btn ${CSS_PREFIX}-btn-accept`;
216
+ acceptBtn.textContent = copy.acceptLabel;
217
+ acceptBtn.addEventListener("click", () => onDecision("granted"));
218
+ actions.appendChild(declineBtn);
219
+ actions.appendChild(acceptBtn);
220
+ card.appendChild(actions);
221
+ overlay.appendChild(card);
222
+ requestAnimationFrame(() => {
223
+ try {
224
+ acceptBtn.focus();
225
+ } catch {
226
+ }
227
+ });
228
+ return overlay;
229
+ }
230
+
23
231
  // src/buffer.ts
24
232
  var RingBuffer = class {
25
233
  constructor(opts) {
@@ -1406,6 +1614,21 @@ function assertConfig(config) {
1406
1614
  async function enableRemoteDebug(options) {
1407
1615
  if (options.enabled === false) return NOOP_HANDLE;
1408
1616
  assertConfig(options.config);
1617
+ if (options.consent?.promptUser) {
1618
+ const cached = readConsentDecision(options.config.miniProgramId);
1619
+ let decision = cached;
1620
+ if (decision === null) {
1621
+ decision = await promptForConsent(
1622
+ options.config.miniProgramId,
1623
+ options.consent.promptCopy
1624
+ );
1625
+ }
1626
+ if (decision === "denied") return NOOP_HANDLE;
1627
+ options = {
1628
+ ...options,
1629
+ consent: { ...options.consent, userOptIn: true }
1630
+ };
1631
+ }
1409
1632
  assertConsent(options.config, options.consent);
1410
1633
  const onError = options.onError ?? (() => {
1411
1634
  });
@@ -1599,6 +1822,6 @@ async function enableRemoteDebug(options) {
1599
1822
  };
1600
1823
  }
1601
1824
 
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 };
1825
+ 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, clearConsentDecision, enableRemoteDebug, getBOT, promptForConsent, readConsentDecision, writeConsentDecision };
1603
1826
  //# sourceMappingURL=index.js.map
1604
1827
  //# sourceMappingURL=index.js.map