@mhosaic/feedback 0.10.0 → 0.11.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.
@@ -1,3 +1,7 @@
1
+ import {
2
+ scrubCredentials
3
+ } from "./chunk-DKE6ELJ4.mjs";
4
+
1
5
  // src/api/client.ts
2
6
  var SCALAR_FIELDS = [
3
7
  "description",
@@ -33,9 +37,15 @@ function createApiClient(options) {
33
37
  if (payload.widget_version) {
34
38
  form.append("widget_version", payload.widget_version);
35
39
  }
40
+ const headers = {
41
+ Authorization: `Bearer ${options.apiKey}`
42
+ };
43
+ if (payload.synthetic) {
44
+ headers["X-Mhosaic-Capture-Source"] = "error-tracking";
45
+ }
36
46
  const response = await fetcher(`${endpoint}/api/feedback/v1/reports/`, {
37
47
  method: "POST",
38
- headers: { Authorization: `Bearer ${options.apiKey}` },
48
+ headers,
39
49
  body: form
40
50
  });
41
51
  if (!response.ok) {
@@ -128,7 +138,28 @@ function createApiClient(options) {
128
138
  }
129
139
 
130
140
  // src/capture/urlSanitizer.ts
131
- var SENSITIVE = /token|key|password|secret|auth|session|sig/i;
141
+ var SENSITIVE_NAME = /token|key|password|secret|auth|session|sig|credential|bearer|cookie/i;
142
+ var SENSITIVE_VALUE_PATTERNS = [
143
+ // JWT: three base64url segments separated by dots, header begins with eyJ.
144
+ /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/,
145
+ // Mhosaic feedback keys (the project's own format).
146
+ /^(?:sk|pk)_proj_[A-Za-z0-9_-]{16,}$/,
147
+ // GitHub PATs (ghp/gho/ghu/ghs/ghr prefixes, 36+ chars).
148
+ /^gh[pousr]_[A-Za-z0-9]{30,}$/,
149
+ // Stripe live/test secret keys.
150
+ /^(?:sk|pk|rk|whsec)_(?:live|test)_[A-Za-z0-9]{16,}$/,
151
+ // AWS access key id.
152
+ /^AKIA[0-9A-Z]{12,}$/,
153
+ // Slack bot tokens.
154
+ /^xox[abprso]-[A-Za-z0-9-]{12,}$/,
155
+ // Generic high-entropy: hex 40+ chars (SHA-1, SHA-256) or long alnum 32+.
156
+ /^[a-f0-9]{40,}$/i,
157
+ // Long opaque base64-ish string (common for OAuth codes & session ids).
158
+ /^[A-Za-z0-9_-]{40,}$/
159
+ ];
160
+ function looksLikeCredential(value) {
161
+ return SENSITIVE_VALUE_PATTERNS.some((re) => re.test(value));
162
+ }
132
163
  function sanitizeUrl(url) {
133
164
  try {
134
165
  const isAbsolute = /^https?:\/\//i.test(url);
@@ -138,9 +169,14 @@ function sanitizeUrl(url) {
138
169
  const u = new URL(url, base);
139
170
  const clean = new URLSearchParams();
140
171
  u.searchParams.forEach((value, name) => {
141
- clean.set(name, SENSITIVE.test(name) ? "[redacted]" : value);
172
+ if (SENSITIVE_NAME.test(name) || looksLikeCredential(value)) {
173
+ clean.set(name, "[redacted]");
174
+ } else {
175
+ clean.set(name, value);
176
+ }
142
177
  });
143
178
  u.search = clean.toString();
179
+ u.hash = u.hash ? "#[redacted]" : "";
144
180
  return u.toString();
145
181
  } catch {
146
182
  return url;
@@ -152,7 +188,16 @@ function collectDevice() {
152
188
  const nav = navigator;
153
189
  const connection = nav.connection?.effectiveType;
154
190
  const deviceMemory = nav.deviceMemory;
155
- const referrer = document.referrer || void 0;
191
+ const rawReferrer = document.referrer || void 0;
192
+ const referrer = rawReferrer !== void 0 ? sanitizeUrl(rawReferrer) : void 0;
193
+ const sanitizedHere = sanitizeUrl(window.location.pathname + window.location.search);
194
+ let pathname;
195
+ try {
196
+ const parsed = new URL(sanitizedHere, window.location.origin);
197
+ pathname = parsed.pathname + parsed.search;
198
+ } catch {
199
+ pathname = window.location.pathname;
200
+ }
156
201
  return {
157
202
  viewport: { w: window.innerWidth, h: window.innerHeight, dpr: window.devicePixelRatio || 1 },
158
203
  screen: { w: window.screen.width, h: window.screen.height },
@@ -165,8 +210,12 @@ function collectDevice() {
165
210
  ...deviceMemory !== void 0 && { deviceMemory },
166
211
  hardwareConcurrency: nav.hardwareConcurrency,
167
212
  ...referrer !== void 0 && { referrer },
168
- title: document.title,
169
- pathname: window.location.pathname
213
+ // Hosts commonly stuff credentials / PII into page titles
214
+ // ("Inbox (3) — bob@x.com", "Reset password — token=abc"). Cap to a
215
+ // sane length and run through the same scrubber the console/error
216
+ // paths use.
217
+ title: scrubCredentials(document.title || "").slice(0, 200),
218
+ pathname
170
219
  };
171
220
  }
172
221
 
@@ -196,7 +245,8 @@ function installConsolePatch(buffer) {
196
245
  originals[level] = original;
197
246
  console[level] = function patched(...args) {
198
247
  try {
199
- const message = args.map(safeStringify).join(" ").slice(0, 2e3);
248
+ const rawMessage = args.map(safeStringify).join(" ");
249
+ const message = scrubCredentials(rawMessage).slice(0, 2e3);
200
250
  const entry = { level, message, ts: Date.now() };
201
251
  if (level === "error") {
202
252
  const stack = new Error().stack;
@@ -229,13 +279,14 @@ function installFetchPatch(buffer, sanitize) {
229
279
  buffer.push({ url: sanitize(url), method, status: response.status, durationMs: Math.round(performance.now() - start), ts: Date.now() });
230
280
  return response;
231
281
  } catch (err) {
282
+ const rawErr = err instanceof Error ? err.message : String(err);
232
283
  buffer.push({
233
284
  url: sanitize(url),
234
285
  method,
235
286
  status: 0,
236
287
  durationMs: Math.round(performance.now() - start),
237
288
  ts: Date.now(),
238
- error: err instanceof Error ? err.message : String(err)
289
+ error: scrubCredentials(rawErr)
239
290
  });
240
291
  throw err;
241
292
  }
@@ -282,9 +333,10 @@ function installErrorHandlers(buffer) {
282
333
  if (typeof window === "undefined") return () => {
283
334
  };
284
335
  const onError = (e) => {
285
- const stack = e.error instanceof Error ? e.error.stack : void 0;
336
+ const rawStack = e.error instanceof Error ? e.error.stack : void 0;
337
+ const stack = rawStack !== void 0 ? scrubCredentials(rawStack) : void 0;
286
338
  buffer.push({
287
- message: e.message || "Unknown error",
339
+ message: scrubCredentials(e.message || "Unknown error"),
288
340
  ...stack !== void 0 && { stack },
289
341
  ts: Date.now(),
290
342
  source: "window.error"
@@ -292,16 +344,17 @@ function installErrorHandlers(buffer) {
292
344
  };
293
345
  const onRejection = (e) => {
294
346
  const reason = e.reason;
295
- const message = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : (() => {
347
+ const rawMessage = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : (() => {
296
348
  try {
297
349
  return JSON.stringify(reason);
298
350
  } catch {
299
351
  return String(reason);
300
352
  }
301
353
  })();
302
- const stack = reason instanceof Error ? reason.stack : void 0;
354
+ const rawStack = reason instanceof Error ? reason.stack : void 0;
355
+ const stack = rawStack !== void 0 ? scrubCredentials(rawStack) : void 0;
303
356
  buffer.push({
304
- message,
357
+ message: scrubCredentials(rawMessage),
305
358
  ...stack !== void 0 && { stack },
306
359
  ts: Date.now(),
307
360
  source: "unhandledrejection"
@@ -330,7 +383,7 @@ function createPerformanceCollector(slowResourceMs = 1e3) {
330
383
  } else if (entry.entryType === "resource") {
331
384
  const e = entry;
332
385
  if (e.duration > slowResourceMs) {
333
- slowResources.push({ name: e.name, duration: e.duration, initiatorType: e.initiatorType });
386
+ slowResources.push({ name: sanitizeUrl(e.name), duration: e.duration, initiatorType: e.initiatorType });
334
387
  while (slowResources.length > 20) slowResources.shift();
335
388
  }
336
389
  }
@@ -413,6 +466,19 @@ function installCapture(options = {}) {
413
466
  }
414
467
 
415
468
  // src/screenshot/index.ts
469
+ var DEFAULT_REDACTED_INPUT_SELECTORS = [
470
+ 'input[type="password"]',
471
+ 'input[type="tel"]',
472
+ 'input[autocomplete*="cc-"]',
473
+ 'input[autocomplete*="current-password"]',
474
+ 'input[autocomplete*="new-password"]',
475
+ 'input[autocomplete*="one-time-code"]',
476
+ 'input[name*="cvv" i]',
477
+ 'input[name*="ccv" i]',
478
+ 'input[name*="cardnum" i]',
479
+ 'input[name*="card-num" i]'
480
+ ];
481
+ var REDACTION_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
416
482
  function isMaskedElement(el) {
417
483
  return el.hasAttribute("data-mfb-mask") || el.classList.contains("mfb-mask");
418
484
  }
@@ -421,13 +487,33 @@ function prepareMaskMatcher(selectors) {
421
487
  if (!joined) return isMaskedElement;
422
488
  return (el) => isMaskedElement(el) || el.matches(joined);
423
489
  }
490
+ function redactSensitiveInputs(clonedDoc, extraSelectors) {
491
+ const selectors = [
492
+ ...DEFAULT_REDACTED_INPUT_SELECTORS,
493
+ ...extraSelectors,
494
+ "[data-mfb-mask]",
495
+ ".mfb-mask"
496
+ ].join(",");
497
+ const elements = clonedDoc.querySelectorAll(selectors);
498
+ elements.forEach((el) => {
499
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
500
+ const len = Math.min(Math.max(el.value.length || 8, 4), 16);
501
+ el.value = REDACTION_MASK.slice(0, len);
502
+ el.setAttribute("value", el.value);
503
+ } else if (el instanceof HTMLElement) {
504
+ el.textContent = REDACTION_MASK;
505
+ }
506
+ });
507
+ }
424
508
  async function takeScreenshot(target, options = {}) {
425
509
  if (typeof document === "undefined") return null;
426
510
  try {
427
511
  const html2canvas = (await import("html2canvas-pro")).default;
428
- const matcher = prepareMaskMatcher(options.mask ?? []);
512
+ const extraSelectors = options.mask ?? [];
513
+ const matcher = prepareMaskMatcher(extraSelectors);
429
514
  const canvas = await html2canvas(target, {
430
515
  ignoreElements: (el) => matcher(el),
516
+ onclone: (clonedDoc) => redactSensitiveInputs(clonedDoc, extraSelectors),
431
517
  useCORS: true,
432
518
  logging: false,
433
519
  backgroundColor: null
@@ -1181,28 +1267,36 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1181
1267
  }
1182
1268
  };
1183
1269
  const handleMouseMove = (e) => {
1184
- if (!isDrawingRef.current || !draftShape) return;
1270
+ if (!isDrawingRef.current) return;
1185
1271
  const { x, y } = getCanvasCoords(e);
1186
- if (draftShape.kind === "rectangle" || draftShape.kind === "highlight" || draftShape.kind === "blur") {
1187
- setDraftShape({ ...draftShape, w: x - draftShape.x, h: y - draftShape.y });
1188
- } else if (draftShape.kind === "arrow") {
1189
- setDraftShape({ ...draftShape, x2: x, y2: y });
1190
- } else if (draftShape.kind === "freehand") {
1191
- setDraftShape({ ...draftShape, points: [...draftShape.points, { x, y }] });
1192
- }
1272
+ setDraftShape((current) => {
1273
+ if (!current) return current;
1274
+ if (current.kind === "rectangle" || current.kind === "highlight" || current.kind === "blur") {
1275
+ return { ...current, w: x - current.x, h: y - current.y };
1276
+ }
1277
+ if (current.kind === "arrow") {
1278
+ return { ...current, x2: x, y2: y };
1279
+ }
1280
+ if (current.kind === "freehand") {
1281
+ return { ...current, points: [...current.points, { x, y }] };
1282
+ }
1283
+ return current;
1284
+ });
1193
1285
  };
1194
1286
  const handleMouseUp = () => {
1195
- if (isDrawingRef.current && draftShape) {
1196
- const isTiny = (draftShape.kind === "rectangle" || draftShape.kind === "highlight" || draftShape.kind === "blur") && Math.abs(draftShape.w) < 4 && Math.abs(draftShape.h) < 4 || draftShape.kind === "arrow" && Math.hypot(
1197
- draftShape.x2 - draftShape.x1,
1198
- draftShape.y2 - draftShape.y1
1199
- ) < 4 || draftShape.kind === "freehand" && draftShape.points.length < 3;
1200
- if (!isTiny) {
1201
- addShape(draftShape);
1287
+ setDraftShape((current) => {
1288
+ if (isDrawingRef.current && current) {
1289
+ const isTiny = (current.kind === "rectangle" || current.kind === "highlight" || current.kind === "blur") && Math.abs(current.w) < 4 && Math.abs(current.h) < 4 || current.kind === "arrow" && Math.hypot(
1290
+ current.x2 - current.x1,
1291
+ current.y2 - current.y1
1292
+ ) < 4 || current.kind === "freehand" && current.points.length < 3;
1293
+ if (!isTiny) {
1294
+ addShape(current);
1295
+ }
1202
1296
  }
1203
- }
1297
+ return null;
1298
+ });
1204
1299
  isDrawingRef.current = false;
1205
- setDraftShape(null);
1206
1300
  };
1207
1301
  const handleSave = async () => {
1208
1302
  const canvas = canvasRef.current;
@@ -3483,8 +3577,8 @@ function createFeedback(config) {
3483
3577
  capture_method: screenshot ? manualScreenshot ? "manual" : "html2canvas" : "none",
3484
3578
  technical_context
3485
3579
  };
3486
- if ("0.10.0") {
3487
- payload.widget_version = "0.10.0";
3580
+ if ("0.11.0") {
3581
+ payload.widget_version = "0.11.0";
3488
3582
  }
3489
3583
  if (screenshot) payload.screenshot = screenshot;
3490
3584
  if (values.synthetic) payload.synthetic = true;
@@ -3570,4 +3664,4 @@ function createFeedback(config) {
3570
3664
  export {
3571
3665
  createFeedback
3572
3666
  };
3573
- //# sourceMappingURL=chunk-XSAUJEJN.mjs.map
3667
+ //# sourceMappingURL=chunk-AO3BXEG5.mjs.map