@mhosaic/feedback 0.14.0 → 0.15.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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  scrubCredentials
3
- } from "./chunk-DKE6ELJ4.mjs";
3
+ } from "./chunk-FGA63IEZ.mjs";
4
4
 
5
5
  // src/api/client.ts
6
6
  var SCALAR_FIELDS = [
@@ -12,6 +12,23 @@ var SCALAR_FIELDS = [
12
12
  "user_agent",
13
13
  "capture_method"
14
14
  ];
15
+ function assertSafeEndpoint(endpoint) {
16
+ let parsed;
17
+ try {
18
+ parsed = new URL(endpoint);
19
+ } catch {
20
+ throw new Error(
21
+ `[mhosaic-feedback] \`endpoint\` is not a valid URL: ${endpoint}`
22
+ );
23
+ }
24
+ if (parsed.protocol === "https:") return;
25
+ if (parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]")) {
26
+ return;
27
+ }
28
+ throw new Error(
29
+ `[mhosaic-feedback] \`endpoint\` must use https:// (got ${parsed.protocol}//${parsed.hostname}). http:// is only allowed for localhost in dev.`
30
+ );
31
+ }
15
32
  function createApiClient(options) {
16
33
  const endpoint = (options.endpoint ?? "").replace(/\/+$/, "");
17
34
  if (!endpoint) {
@@ -19,6 +36,7 @@ function createApiClient(options) {
19
36
  '[mhosaic-feedback] `endpoint` is required (e.g. "https://feedback.example.com").'
20
37
  );
21
38
  }
39
+ assertSafeEndpoint(endpoint);
22
40
  const fetcher = options.fetch ?? globalThis.fetch;
23
41
  async function submitReport(input) {
24
42
  let payload = input;
@@ -872,6 +890,20 @@ function truncateUrl(url, maxLength = 80) {
872
890
  const keepEnd = maxLength - 1 - keepStart;
873
891
  return `${url.slice(0, keepStart)}\u2026${url.slice(url.length - keepEnd)}`;
874
892
  }
893
+ function safeExternalHref(url) {
894
+ if (!url) return void 0;
895
+ let parsed;
896
+ try {
897
+ parsed = new URL(url);
898
+ } catch {
899
+ return void 0;
900
+ }
901
+ if (parsed.protocol === "https:") return parsed.toString();
902
+ if (parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]")) {
903
+ return parsed.toString();
904
+ }
905
+ return void 0;
906
+ }
875
907
 
876
908
  // src/widget/ReportDetailView.tsx
877
909
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
@@ -980,16 +1012,19 @@ function ReportDetailView({
980
1012
  /* @__PURE__ */ jsxs2("div", { class: "report-detail-body", children: [
981
1013
  /* @__PURE__ */ jsx2(ContextBlock, { detail, strings }),
982
1014
  /* @__PURE__ */ jsx2("p", { class: "report-detail-description", children: detail.description }),
983
- detail.screenshot_url && /* @__PURE__ */ jsx2(
984
- "a",
985
- {
986
- class: "report-detail-screenshot",
987
- href: detail.screenshot_url,
988
- target: "_blank",
989
- rel: "noopener noreferrer",
990
- children: /* @__PURE__ */ jsx2("img", { src: detail.screenshot_url, alt: "", loading: "lazy" })
991
- }
992
- ),
1015
+ detail.screenshot_url && (() => {
1016
+ const safeHref = safeExternalHref(detail.screenshot_url);
1017
+ return safeHref ? /* @__PURE__ */ jsx2(
1018
+ "a",
1019
+ {
1020
+ class: "report-detail-screenshot",
1021
+ href: safeHref,
1022
+ target: "_blank",
1023
+ rel: "noopener noreferrer",
1024
+ children: /* @__PURE__ */ jsx2("img", { src: detail.screenshot_url, alt: "", loading: "lazy" })
1025
+ }
1026
+ ) : /* @__PURE__ */ jsx2("div", { class: "report-detail-screenshot", children: /* @__PURE__ */ jsx2("img", { src: detail.screenshot_url, alt: "", loading: "lazy" }) });
1027
+ })(),
993
1028
  /* @__PURE__ */ jsx2("h3", { class: "report-detail-section", children: strings["detail.thread"] }),
994
1029
  detail.comments.length === 0 ? /* @__PURE__ */ jsx2("p", { class: "report-detail-empty", children: strings["detail.no_replies"] }) : /* @__PURE__ */ jsx2("ul", { class: "report-comments", children: detail.comments.map((c) => /* @__PURE__ */ jsx2("li", { children: /* @__PURE__ */ jsx2(CommentBubble, { comment: c, strings }) })) }),
995
1030
  detail.status_history && detail.status_history.length > 0 && /* @__PURE__ */ jsx2(StatusHistorySection, { rows: detail.status_history, strings }),
@@ -1048,19 +1083,22 @@ function ContextBlock({ detail, strings }) {
1048
1083
  /* @__PURE__ */ jsx2("span", { class: "report-detail-context-label", children: strings["detail.context.submitted_at"] }),
1049
1084
  /* @__PURE__ */ jsx2("span", { children: formatSubmittedAt(detail.created_at) })
1050
1085
  ] }),
1051
- detail.page_url && /* @__PURE__ */ jsxs2("div", { class: "report-detail-context-line", title: detail.page_url, children: [
1052
- /* @__PURE__ */ jsx2("span", { class: "report-detail-context-label", children: strings["detail.context.page"] }),
1053
- /* @__PURE__ */ jsx2(
1054
- "a",
1055
- {
1056
- class: "report-detail-context-url",
1057
- href: detail.page_url,
1058
- target: "_blank",
1059
- rel: "noopener noreferrer",
1060
- children: truncateUrl(detail.page_url, 64)
1061
- }
1062
- )
1063
- ] })
1086
+ detail.page_url && (() => {
1087
+ const safeHref = safeExternalHref(detail.page_url);
1088
+ return /* @__PURE__ */ jsxs2("div", { class: "report-detail-context-line", title: detail.page_url, children: [
1089
+ /* @__PURE__ */ jsx2("span", { class: "report-detail-context-label", children: strings["detail.context.page"] }),
1090
+ safeHref ? /* @__PURE__ */ jsx2(
1091
+ "a",
1092
+ {
1093
+ class: "report-detail-context-url",
1094
+ href: safeHref,
1095
+ target: "_blank",
1096
+ rel: "noopener noreferrer",
1097
+ children: truncateUrl(detail.page_url, 64)
1098
+ }
1099
+ ) : /* @__PURE__ */ jsx2("span", { class: "report-detail-context-url", children: truncateUrl(detail.page_url, 64) })
1100
+ ] });
1101
+ })()
1064
1102
  ] });
1065
1103
  }
1066
1104
  function formatSubmittedAt(iso) {
@@ -3934,9 +3972,16 @@ var WIDGET_STYLES = `
3934
3972
  * Width/height/max-* are animated together; padding too, since the
3935
3973
  * board's denser layout needs less of the modal's default padding. */
3936
3974
  .modal.is-expanded {
3937
- width: min(1280px, calc(100vw - var(--mfb-space-5)));
3938
- max-height: calc(100vh - var(--mfb-space-5));
3939
- height: calc(100vh - var(--mfb-space-5));
3975
+ /* Margin around the expanded board view. --mfb-space-7 (48px) matches
3976
+ * the breathing room of the default modal \u2014 24px top + 24px bottom +
3977
+ * 24px on each side \u2014 so the panel reads as "near-fullscreen with a
3978
+ * visible frame" instead of "the whole viewport, content cut off".
3979
+ * Was --mfb-space-5 (24px total = 12px each side), which felt
3980
+ * claustrophobic and tricked users into expecting more vertical
3981
+ * scroll under the viewport edge. */
3982
+ width: min(1280px, calc(100vw - var(--mfb-space-7)));
3983
+ max-height: calc(100vh - var(--mfb-space-7));
3984
+ height: calc(100vh - var(--mfb-space-7));
3940
3985
  padding: var(--mfb-space-5);
3941
3986
  transition:
3942
3987
  width 320ms cubic-bezier(0.22, 1, 0.36, 1),
@@ -3950,6 +3995,10 @@ var WIDGET_STYLES = `
3950
3995
  .backdrop.is-expanded { /* hook for any backdrop-level overrides */ }
3951
3996
 
3952
3997
  @media (max-width: 640px) {
3998
+ /* On mobile the sheet still snaps to the bottom edge and stretches
3999
+ * almost the full viewport \u2014 24px gap from the top is enough to
4000
+ * convey "this is a panel, not a takeover", but we don't pull margin
4001
+ * off the sides where users expect the sheet to fill the viewport. */
3953
4002
  .modal.is-expanded {
3954
4003
  width: 100vw;
3955
4004
  height: calc(100vh - var(--mfb-space-5));
@@ -4583,7 +4632,10 @@ function createFeedback(config) {
4583
4632
  async function buildAndSubmit(values) {
4584
4633
  const manualScreenshot = values.synthetic ? void 0 : values.screenshot;
4585
4634
  const technical_context = capture.snapshot();
4586
- if (user) technical_context.user = user;
4635
+ if (user) {
4636
+ const { userHash: _hash, exp: _exp, ...safeUser } = user;
4637
+ technical_context.user = safeUser;
4638
+ }
4587
4639
  if (metadata && Object.keys(metadata).length > 0) {
4588
4640
  technical_context.metadata = { ...metadata };
4589
4641
  }
@@ -4598,8 +4650,8 @@ function createFeedback(config) {
4598
4650
  capture_method: captureMethod,
4599
4651
  technical_context
4600
4652
  };
4601
- if ("0.14.0") {
4602
- payload.widget_version = "0.14.0";
4653
+ if ("0.15.1") {
4654
+ payload.widget_version = "0.15.1";
4603
4655
  }
4604
4656
  if (manualScreenshot) payload.screenshot = manualScreenshot;
4605
4657
  if (values.synthetic) payload.synthetic = true;
@@ -4685,4 +4737,4 @@ function createFeedback(config) {
4685
4737
  export {
4686
4738
  createFeedback
4687
4739
  };
4688
- //# sourceMappingURL=chunk-A4MBOFRR.mjs.map
4740
+ //# sourceMappingURL=chunk-3YLD2AJI.mjs.map