@mhosaic/feedback 0.16.2 → 0.18.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,50 @@
1
+ // src/capture/urlSanitizer.ts
2
+ var SENSITIVE_NAME = /token|key|password|secret|auth|session|sig|credential|bearer|cookie/i;
3
+ var SENSITIVE_VALUE_PATTERNS = [
4
+ // JWT: three base64url segments separated by dots, header begins with eyJ.
5
+ /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/,
6
+ // Mhosaic feedback keys (the project's own format).
7
+ /^(?:sk|pk)_proj_[A-Za-z0-9_-]{16,}$/,
8
+ // GitHub PATs (ghp/gho/ghu/ghs/ghr prefixes, 36+ chars).
9
+ /^gh[pousr]_[A-Za-z0-9]{30,}$/,
10
+ // Stripe live/test secret keys.
11
+ /^(?:sk|pk|rk|whsec)_(?:live|test)_[A-Za-z0-9]{16,}$/,
12
+ // AWS access key id.
13
+ /^AKIA[0-9A-Z]{12,}$/,
14
+ // Slack bot tokens.
15
+ /^xox[abprso]-[A-Za-z0-9-]{12,}$/,
16
+ // Generic high-entropy: hex 40+ chars (SHA-1, SHA-256) or long alnum 32+.
17
+ /^[a-f0-9]{40,}$/i,
18
+ // Long opaque base64-ish string (common for OAuth codes & session ids).
19
+ /^[A-Za-z0-9_-]{40,}$/
20
+ ];
21
+ function looksLikeCredential(value) {
22
+ return SENSITIVE_VALUE_PATTERNS.some((re) => re.test(value));
23
+ }
24
+ function sanitizeUrl(url) {
25
+ try {
26
+ const isAbsolute = /^https?:\/\//i.test(url);
27
+ const isRootRelative = url.startsWith("/");
28
+ if (!isAbsolute && !isRootRelative) return url;
29
+ const base = typeof window !== "undefined" ? window.location.origin : "http://localhost";
30
+ const u = new URL(url, base);
31
+ const clean = new URLSearchParams();
32
+ u.searchParams.forEach((value, name) => {
33
+ if (SENSITIVE_NAME.test(name) || looksLikeCredential(value)) {
34
+ clean.set(name, "[redacted]");
35
+ } else {
36
+ clean.set(name, value);
37
+ }
38
+ });
39
+ u.search = clean.toString();
40
+ u.hash = u.hash ? "#[redacted]" : "";
41
+ return u.toString();
42
+ } catch {
43
+ return url;
44
+ }
45
+ }
46
+
47
+ export {
48
+ sanitizeUrl
49
+ };
50
+ //# sourceMappingURL=chunk-4QOIFIT5.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/capture/urlSanitizer.ts"],"sourcesContent":["/**\n * URL sanitizer for captured network request URLs.\n *\n * Two layers of defense:\n *\n * 1) Param NAME match — anything whose name suggests a credential is\n * redacted unconditionally (token, key, password, secret, auth, etc.).\n *\n * 2) Param VALUE shape match — even when the name is innocent (\"id\",\n * \"redirect_uri\", \"context\"), if the VALUE looks like a JWT, a\n * Bearer token, an mhosaic-feedback API key, or a long opaque hex/b64\n * string we redact it. This catches the realistic case where a backend\n * routes credentials through generic-named params or where a careless\n * developer puts a token in a URL fragment for \"convenience.\"\n *\n * The path portion is preserved as-is — its purpose is operator-side\n * \"which page was this report filed from\", and stripping it would\n * destroy that signal. Hosts that route secrets through path segments\n * (uncommon) should mark those routes for the widget to skip via the\n * sanitizeUrl hook in createFeedback().\n */\n\nconst SENSITIVE_NAME = /token|key|password|secret|auth|session|sig|credential|bearer|cookie/i\n\nconst SENSITIVE_VALUE_PATTERNS: RegExp[] = [\n // JWT: three base64url segments separated by dots, header begins with eyJ.\n /^eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$/,\n // Mhosaic feedback keys (the project's own format).\n /^(?:sk|pk)_proj_[A-Za-z0-9_-]{16,}$/,\n // GitHub PATs (ghp/gho/ghu/ghs/ghr prefixes, 36+ chars).\n /^gh[pousr]_[A-Za-z0-9]{30,}$/,\n // Stripe live/test secret keys.\n /^(?:sk|pk|rk|whsec)_(?:live|test)_[A-Za-z0-9]{16,}$/,\n // AWS access key id.\n /^AKIA[0-9A-Z]{12,}$/,\n // Slack bot tokens.\n /^xox[abprso]-[A-Za-z0-9-]{12,}$/,\n // Generic high-entropy: hex 40+ chars (SHA-1, SHA-256) or long alnum 32+.\n /^[a-f0-9]{40,}$/i,\n // Long opaque base64-ish string (common for OAuth codes & session ids).\n /^[A-Za-z0-9_-]{40,}$/,\n]\n\nfunction looksLikeCredential(value: string): boolean {\n return SENSITIVE_VALUE_PATTERNS.some((re) => re.test(value))\n}\n\nexport function sanitizeUrl(url: string): string {\n try {\n // Accept absolute URLs or root-relative paths; reject everything else\n const isAbsolute = /^https?:\\/\\//i.test(url)\n const isRootRelative = url.startsWith('/')\n if (!isAbsolute && !isRootRelative) return url\n\n const base = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'\n const u = new URL(url, base)\n const clean = new URLSearchParams()\n u.searchParams.forEach((value, name) => {\n if (SENSITIVE_NAME.test(name) || looksLikeCredential(value)) {\n clean.set(name, '[redacted]')\n } else {\n clean.set(name, value)\n }\n })\n u.search = clean.toString()\n // Hash fragments occasionally carry credentials (OAuth implicit flow):\n // never log them.\n u.hash = u.hash ? '#[redacted]' : ''\n return u.toString()\n } catch {\n return url\n }\n}\n"],"mappings":";AAsBA,IAAM,iBAAiB;AAEvB,IAAM,2BAAqC;AAAA;AAAA,EAEzC;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AACF;AAEA,SAAS,oBAAoB,OAAwB;AACnD,SAAO,yBAAyB,KAAK,CAAC,OAAO,GAAG,KAAK,KAAK,CAAC;AAC7D;AAEO,SAAS,YAAY,KAAqB;AAC/C,MAAI;AAEF,UAAM,aAAa,gBAAgB,KAAK,GAAG;AAC3C,UAAM,iBAAiB,IAAI,WAAW,GAAG;AACzC,QAAI,CAAC,cAAc,CAAC,eAAgB,QAAO;AAE3C,UAAM,OAAO,OAAO,WAAW,cAAc,OAAO,SAAS,SAAS;AACtE,UAAM,IAAI,IAAI,IAAI,KAAK,IAAI;AAC3B,UAAM,QAAQ,IAAI,gBAAgB;AAClC,MAAE,aAAa,QAAQ,CAAC,OAAO,SAAS;AACtC,UAAI,eAAe,KAAK,IAAI,KAAK,oBAAoB,KAAK,GAAG;AAC3D,cAAM,IAAI,MAAM,YAAY;AAAA,MAC9B,OAAO;AACL,cAAM,IAAI,MAAM,KAAK;AAAA,MACvB;AAAA,IACF,CAAC;AACD,MAAE,SAAS,MAAM,SAAS;AAG1B,MAAE,OAAO,EAAE,OAAO,gBAAgB;AAClC,WAAO,EAAE,SAAS;AAAA,EACpB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
@@ -1,3 +1,6 @@
1
+ import {
2
+ sanitizeUrl
3
+ } from "./chunk-4QOIFIT5.mjs";
1
4
  import {
2
5
  scrubCredentials
3
6
  } from "./chunk-FGA63IEZ.mjs";
@@ -206,8 +209,27 @@ function createApiClient(options) {
206
209
  }
207
210
  return response.json();
208
211
  }
212
+ async function checkVisibility(externalId) {
213
+ const response = await fetcher(
214
+ `${endpoint}/api/feedback/v1/widget/visibility/`,
215
+ {
216
+ method: "POST",
217
+ headers: {
218
+ ...widgetHeaders(externalId),
219
+ "Content-Type": "application/json"
220
+ },
221
+ body: JSON.stringify({ external_id: externalId })
222
+ }
223
+ );
224
+ if (!response.ok) {
225
+ throw new Error(`checkVisibility failed: ${response.status}`);
226
+ }
227
+ const data = await response.json();
228
+ return Boolean(data.show);
229
+ }
209
230
  return {
210
231
  submitReport,
232
+ checkVisibility,
211
233
  listMine,
212
234
  listChangelog,
213
235
  getReport,
@@ -218,52 +240,6 @@ function createApiClient(options) {
218
240
  };
219
241
  }
220
242
 
221
- // src/capture/urlSanitizer.ts
222
- var SENSITIVE_NAME = /token|key|password|secret|auth|session|sig|credential|bearer|cookie/i;
223
- var SENSITIVE_VALUE_PATTERNS = [
224
- // JWT: three base64url segments separated by dots, header begins with eyJ.
225
- /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/,
226
- // Mhosaic feedback keys (the project's own format).
227
- /^(?:sk|pk)_proj_[A-Za-z0-9_-]{16,}$/,
228
- // GitHub PATs (ghp/gho/ghu/ghs/ghr prefixes, 36+ chars).
229
- /^gh[pousr]_[A-Za-z0-9]{30,}$/,
230
- // Stripe live/test secret keys.
231
- /^(?:sk|pk|rk|whsec)_(?:live|test)_[A-Za-z0-9]{16,}$/,
232
- // AWS access key id.
233
- /^AKIA[0-9A-Z]{12,}$/,
234
- // Slack bot tokens.
235
- /^xox[abprso]-[A-Za-z0-9-]{12,}$/,
236
- // Generic high-entropy: hex 40+ chars (SHA-1, SHA-256) or long alnum 32+.
237
- /^[a-f0-9]{40,}$/i,
238
- // Long opaque base64-ish string (common for OAuth codes & session ids).
239
- /^[A-Za-z0-9_-]{40,}$/
240
- ];
241
- function looksLikeCredential(value) {
242
- return SENSITIVE_VALUE_PATTERNS.some((re) => re.test(value));
243
- }
244
- function sanitizeUrl(url) {
245
- try {
246
- const isAbsolute = /^https?:\/\//i.test(url);
247
- const isRootRelative = url.startsWith("/");
248
- if (!isAbsolute && !isRootRelative) return url;
249
- const base = typeof window !== "undefined" ? window.location.origin : "http://localhost";
250
- const u = new URL(url, base);
251
- const clean = new URLSearchParams();
252
- u.searchParams.forEach((value, name) => {
253
- if (SENSITIVE_NAME.test(name) || looksLikeCredential(value)) {
254
- clean.set(name, "[redacted]");
255
- } else {
256
- clean.set(name, value);
257
- }
258
- });
259
- u.search = clean.toString();
260
- u.hash = u.hash ? "#[redacted]" : "";
261
- return u.toString();
262
- } catch {
263
- return url;
264
- }
265
- }
266
-
267
243
  // src/capture/device.ts
268
244
  function collectDevice() {
269
245
  const nav = navigator;
@@ -844,7 +820,7 @@ function resolveStrings(overrides, options = {}) {
844
820
 
845
821
  // src/widget/mount.tsx
846
822
  import { h, render } from "preact";
847
- import { useCallback } from "preact/hooks";
823
+ import { useCallback, useEffect as useEffect8, useState as useState7 } from "preact/hooks";
848
824
 
849
825
  // src/widget/BoardView.tsx
850
826
  import { useEffect as useEffect2, useMemo, useState as useState2 } from "preact/hooks";
@@ -4505,6 +4481,12 @@ var WIDGET_STYLES = `
4505
4481
 
4506
4482
  // src/widget/mount.tsx
4507
4483
  import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs12 } from "preact/jsx-runtime";
4484
+ function computeFabVisible(opts, externalId, visibilityAllowed) {
4485
+ if (!opts.showFAB) return false;
4486
+ if (opts.getExternalId !== void 0 && !externalId) return false;
4487
+ if (opts.requiresVisibilityCheck && !visibilityAllowed) return false;
4488
+ return true;
4489
+ }
4508
4490
  function mountWidget(options) {
4509
4491
  const shadow = options.host.attachShadow({ mode: "open" });
4510
4492
  const style = document.createElement("style");
@@ -4553,7 +4535,29 @@ function mountWidget(options) {
4553
4535
  }
4554
4536
  }, []);
4555
4537
  const externalId = options.getExternalId?.();
4556
- const fabVisible = options.showFAB && (options.getExternalId === void 0 ? true : Boolean(externalId));
4538
+ const [visibilityAllowed, setVisibilityAllowed] = useState7(
4539
+ !options.requiresVisibilityCheck
4540
+ );
4541
+ useEffect8(() => {
4542
+ if (!options.requiresVisibilityCheck) {
4543
+ setVisibilityAllowed(true);
4544
+ return;
4545
+ }
4546
+ if (!externalId || !options.checkVisibility) {
4547
+ setVisibilityAllowed(false);
4548
+ return;
4549
+ }
4550
+ let cancelled = false;
4551
+ options.checkVisibility(externalId).then((show) => {
4552
+ if (!cancelled) setVisibilityAllowed(show);
4553
+ }).catch(() => {
4554
+ if (!cancelled) setVisibilityAllowed(false);
4555
+ });
4556
+ return () => {
4557
+ cancelled = true;
4558
+ };
4559
+ }, [externalId]);
4560
+ const fabVisible = computeFabVisible(options, externalId, visibilityAllowed);
4557
4561
  const showMineTab = Boolean(options.api && externalId);
4558
4562
  return /* @__PURE__ */ jsxs12(Fragment3, { children: [
4559
4563
  fabVisible && /* @__PURE__ */ jsx12(
@@ -4752,8 +4756,8 @@ function createFeedback(config) {
4752
4756
  capture_method: captureMethod,
4753
4757
  technical_context
4754
4758
  };
4755
- if ("0.16.2") {
4756
- payload.widget_version = "0.16.2";
4759
+ if ("0.18.0") {
4760
+ payload.widget_version = "0.18.0";
4757
4761
  }
4758
4762
  if (manualScreenshot) payload.screenshot = manualScreenshot;
4759
4763
  if (values.synthetic) payload.synthetic = true;
@@ -4790,7 +4794,11 @@ function createFeedback(config) {
4790
4794
  // Keep this a callback (not a snapshot) so the mount picks up identity
4791
4795
  // changes that happen after createFeedback() — `notifyIdentityChanged()`
4792
4796
  // is the trigger for a re-render.
4793
- getExternalId: () => user?.id !== void 0 && user.id !== null && user.id !== "" ? String(user.id) : void 0
4797
+ getExternalId: () => user?.id !== void 0 && user.id !== null && user.id !== "" ? String(user.id) : void 0,
4798
+ // Phase 4: the manifest tells the loader whether this project gates the
4799
+ // widget per end-user; the loader forwards it as `requiresVisibilityCheck`.
4800
+ requiresVisibilityCheck: config.requiresVisibilityCheck ?? false,
4801
+ checkVisibility: (externalId) => api.checkVisibility(externalId)
4794
4802
  });
4795
4803
  const instance = {
4796
4804
  show() {
@@ -4839,4 +4847,4 @@ function createFeedback(config) {
4839
4847
  export {
4840
4848
  createFeedback
4841
4849
  };
4842
- //# sourceMappingURL=chunk-I4GWTJ6Q.mjs.map
4850
+ //# sourceMappingURL=chunk-4QQI5ZYD.mjs.map