@mushi-mushi/core 1.17.0 → 1.19.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
@@ -102,6 +102,20 @@ interface MushiWidgetConfig {
102
102
  brandFooter?: boolean;
103
103
  /** How the widget should surface SDK freshness warnings. Defaults to auto. */
104
104
  outdatedBanner?: 'auto' | 'banner' | 'console-only' | 'off';
105
+ /**
106
+ * Privacy nudge shown beside an attached screenshot preview, reminding the
107
+ * reporter to remove anything sensitive (balances, PII, tokens) before they
108
+ * submit. The widget always renders the captured image as a visible preview
109
+ * so the user can see exactly what will be sent and remove it if needed; this
110
+ * flag only controls the accompanying caption.
111
+ * - `true` (default) — show the localized default caption.
112
+ * - string — show this custom caption verbatim.
113
+ * - `false` — hide the caption (the preview + remove control still show).
114
+ *
115
+ * Settable per-host via the SDK config and remotely via the Mushi console
116
+ * runtime config (it travels in the `widget` block of `GET /v1/sdk/config`).
117
+ */
118
+ screenshotSensitiveHint?: boolean | string;
105
119
  /**
106
120
  * Beta mode: injects discreet "early access" messaging into the widget panel.
107
121
  * Shows a beta strip on the category step and a contact footer on the success
package/dist/index.d.ts CHANGED
@@ -102,6 +102,20 @@ interface MushiWidgetConfig {
102
102
  brandFooter?: boolean;
103
103
  /** How the widget should surface SDK freshness warnings. Defaults to auto. */
104
104
  outdatedBanner?: 'auto' | 'banner' | 'console-only' | 'off';
105
+ /**
106
+ * Privacy nudge shown beside an attached screenshot preview, reminding the
107
+ * reporter to remove anything sensitive (balances, PII, tokens) before they
108
+ * submit. The widget always renders the captured image as a visible preview
109
+ * so the user can see exactly what will be sent and remove it if needed; this
110
+ * flag only controls the accompanying caption.
111
+ * - `true` (default) — show the localized default caption.
112
+ * - string — show this custom caption verbatim.
113
+ * - `false` — hide the caption (the preview + remove control still show).
114
+ *
115
+ * Settable per-host via the SDK config and remotely via the Mushi console
116
+ * runtime config (it travels in the `widget` block of `GET /v1/sdk/config`).
117
+ */
118
+ screenshotSensitiveHint?: boolean | string;
105
119
  /**
106
120
  * Beta mode: injects discreet "early access" messaging into the widget panel.
107
121
  * Shows a beta strip on the category step and a contact footer on the success
package/dist/index.js CHANGED
@@ -840,6 +840,8 @@ var LS_KEY = "mushi_offline_queue";
840
840
  var BATCH_SIZE = 10;
841
841
  var MAX_BACKOFF_MS = 6e4;
842
842
  var AUTO_FLUSH_INTERVAL_MS = 3e4;
843
+ var MAX_DELIVERY_ATTEMPTS = 8;
844
+ var MAX_QUEUE_AGE_MS = 24 * 60 * 60 * 1e3;
843
845
  function createOfflineQueue(config = {}) {
844
846
  const { enabled = true, maxQueueSize = 50, syncOnReconnect = true, encryptAtRest = true } = config;
845
847
  let syncCleanup = null;
@@ -872,6 +874,17 @@ function createOfflineQueue(config = {}) {
872
874
  function isEncryptedRecord(row) {
873
875
  return !!row.payload && isEncryptedPayload(row.payload);
874
876
  }
877
+ function rowAttempts(row) {
878
+ const n = row.attempts;
879
+ return typeof n === "number" && Number.isFinite(n) ? n : 0;
880
+ }
881
+ function isExpired(row, now) {
882
+ const queuedAt = row.queuedAt;
883
+ if (!queuedAt) return false;
884
+ const ts = Date.parse(queuedAt);
885
+ if (Number.isNaN(ts)) return false;
886
+ return now - ts > MAX_QUEUE_AGE_MS;
887
+ }
875
888
  function detectBackend() {
876
889
  if (backendType) return backendType;
877
890
  if (typeof indexedDB !== "undefined") {
@@ -927,6 +940,15 @@ function createOfflineQueue(config = {}) {
927
940
  tx.onerror = () => reject(tx.error);
928
941
  });
929
942
  }
943
+ async function idbPutRow(row) {
944
+ const db = await openDb();
945
+ return new Promise((resolve, reject) => {
946
+ const tx = db.transaction(STORE_NAME, "readwrite");
947
+ tx.objectStore(STORE_NAME).put(row);
948
+ tx.oncomplete = () => resolve();
949
+ tx.onerror = () => reject(tx.error);
950
+ });
951
+ }
930
952
  async function idbSize() {
931
953
  const db = await openDb();
932
954
  return new Promise((resolve, reject) => {
@@ -968,6 +990,38 @@ function createOfflineQueue(config = {}) {
968
990
  const rows = lsRead().filter((r) => r.id !== id);
969
991
  lsWrite(rows);
970
992
  }
993
+ function lsUpdateRow(row) {
994
+ const rows = lsRead();
995
+ const idx = rows.findIndex((r) => r.id === row.id);
996
+ if (idx >= 0) {
997
+ rows[idx] = row;
998
+ lsWrite(rows);
999
+ }
1000
+ }
1001
+ async function removeRow(backend, rowId) {
1002
+ try {
1003
+ if (backend === "indexeddb") await idbDelete(rowId);
1004
+ else lsDelete(rowId);
1005
+ } catch (err) {
1006
+ queueLog.debug("Offline queue: row removal failed (will age out)", {
1007
+ id: rowId,
1008
+ err: String(err)
1009
+ });
1010
+ }
1011
+ }
1012
+ async function persistRow(backend, row) {
1013
+ try {
1014
+ if (backend === "indexeddb") await idbPutRow(row);
1015
+ else lsUpdateRow(row);
1016
+ return true;
1017
+ } catch (err) {
1018
+ queueLog.debug("Offline queue: row persist failed", {
1019
+ id: row.id,
1020
+ err: String(err)
1021
+ });
1022
+ return false;
1023
+ }
1024
+ }
971
1025
  async function enqueue(report) {
972
1026
  if (!enabled) return;
973
1027
  const currentSize = await size();
@@ -1008,6 +1062,18 @@ function createOfflineQueue(config = {}) {
1008
1062
  } else {
1009
1063
  rows = lsRead();
1010
1064
  }
1065
+ const now = Date.now();
1066
+ const fresh = [];
1067
+ for (const row of rows) {
1068
+ if (isExpired(row, now)) {
1069
+ const rowId = row.id;
1070
+ await removeRow(backend, rowId);
1071
+ queueLog.debug("Offline queue: evicting stale report", { id: rowId });
1072
+ } else {
1073
+ fresh.push(row);
1074
+ }
1075
+ }
1076
+ rows = fresh;
1011
1077
  const batch = rows.slice(0, BATCH_SIZE);
1012
1078
  let sent = 0;
1013
1079
  let failed = 0;
@@ -1016,23 +1082,13 @@ function createOfflineQueue(config = {}) {
1016
1082
  const rowId = row.id;
1017
1083
  const report = await unwrapForSend(row);
1018
1084
  if (!report) {
1019
- try {
1020
- if (backend === "indexeddb") await idbDelete(rowId);
1021
- else lsDelete(rowId);
1022
- } catch {
1023
- lsDelete(rowId);
1024
- }
1085
+ await removeRow(backend, rowId);
1025
1086
  failed++;
1026
1087
  continue;
1027
1088
  }
1028
1089
  const result = await client.submitReport(report);
1029
1090
  if (result.ok) {
1030
- try {
1031
- if (backend === "indexeddb") await idbDelete(rowId);
1032
- else lsDelete(rowId);
1033
- } catch {
1034
- lsDelete(rowId);
1035
- }
1091
+ await removeRow(backend, rowId);
1036
1092
  sent++;
1037
1093
  } else {
1038
1094
  const permanent = result.error?.code === "HTTP_400" || result.error?.code === "HTTP_413" || result.error?.code === "HTTP_422" || result.error?.code === "INGEST_ERROR" || result.error?.code === "VALIDATION_ERROR" || // A payload that exceeds the size guard will never shrink on its own;
@@ -1044,17 +1100,33 @@ function createOfflineQueue(config = {}) {
1044
1100
  );
1045
1101
  const transient = !permanent && (result.error?.code === "NETWORK_ERROR" || result.error?.code === "HTTP_403" || result.error?.code === "HTTP_429" || result.error?.code === "HTTP_502" || result.error?.code === "HTTP_503" || result.error?.code === "HTTP_504" || typeof result.error?.code === "string" && result.error.code.startsWith("HTTP_5"));
1046
1102
  if (permanent) {
1047
- try {
1048
- if (backend === "indexeddb") await idbDelete(rowId);
1049
- else lsDelete(rowId);
1050
- } catch {
1051
- lsDelete(rowId);
1052
- }
1103
+ await removeRow(backend, rowId);
1053
1104
  } else if (transient) {
1054
- queueLog.debug("Offline queue: transient failure, will retry", {
1055
- id: rowId,
1056
- code: result.error?.code
1057
- });
1105
+ const nextAttempts = rowAttempts(row) + 1;
1106
+ if (nextAttempts >= MAX_DELIVERY_ATTEMPTS) {
1107
+ await removeRow(backend, rowId);
1108
+ queueLog.warn("Offline queue: giving up on report after repeated failures", {
1109
+ id: rowId,
1110
+ attempts: nextAttempts,
1111
+ code: result.error?.code
1112
+ });
1113
+ } else {
1114
+ row.attempts = nextAttempts;
1115
+ const persisted = await persistRow(backend, row);
1116
+ if (persisted) {
1117
+ queueLog.debug("Offline queue: transient failure, will retry", {
1118
+ id: rowId,
1119
+ attempts: nextAttempts,
1120
+ code: result.error?.code
1121
+ });
1122
+ } else {
1123
+ await removeRow(backend, rowId);
1124
+ queueLog.warn("Offline queue: dropping report (attempt counter unpersistable)", {
1125
+ id: rowId,
1126
+ code: result.error?.code
1127
+ });
1128
+ }
1129
+ }
1058
1130
  }
1059
1131
  failed++;
1060
1132
  if (i < batch.length - 1) {