@mushi-mushi/core 0.7.0 → 0.9.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/README.md CHANGED
@@ -7,7 +7,7 @@ Core types, API client, and utilities for the Mushi Mushi SDK.
7
7
  ## What's Inside
8
8
 
9
9
  - **Types**: `MushiConfig`, `MushiReport`, `MushiEnvironment`, and all shared interfaces
10
- - **API Client**: Fetch-based HTTP client with retry and exponential backoff
10
+ - **API Client**: Fetch-based HTTP client with retry and exponential backoff. Tags every internal request with `X-Mushi-Internal: <kind>` so framework SDKs can filter their own traffic out of network capture and `apiCascade`. Ships HMAC-signed reporter helpers (`getLatestSdkVersion`, `listReporterReports`, `listReporterComments`, `replyToReporterReport`) for the two-way reply pipeline, plus `postDiscoveryEvent` (v2.1) for the passive inventory channel
11
11
  - **Pre-Filter**: On-device Stage 0 spam/gibberish filter (runs client-side, zero server cost)
12
12
  - **Offline Queue**: IndexedDB-backed queue with auto-sync on reconnect
13
13
  - **Environment Capture**: Browser/device snapshot (viewport, user agent, connection info)
@@ -15,10 +15,37 @@ Core types, API client, and utilities for the Mushi Mushi SDK.
15
15
  - **Session ID**: Tab-scoped session correlation
16
16
  - **Rate Limiter**: Token bucket self-throttle to prevent API flooding
17
17
 
18
+ ## Public types added in 0.7 → 0.11
19
+
20
+ | Type | Purpose |
21
+ |----------------------------|-----------------------------------------------------------------------------------------------|
22
+ | `MushiPreset` | `'production-calm' \| 'beta-loud' \| 'internal-debug' \| 'manual-only'` posture bundles. |
23
+ | `MushiWidgetAnchor` | Raw-CSS positioning (`top` / `right` / `bottom` / `left`) for the widget launcher. |
24
+ | `MushiPrivacyConfig` | `maskSelectors`, `blockSelectors`, `allowUserRemoveScreenshot` for screenshot redaction. |
25
+ | `MushiUrlMatcher` | `string \| RegExp` element used by `capture.ignoreUrls` and `apiCascade.ignoreUrls`. |
26
+ | `MushiApiCascadeConfig` | Object form of `proactive.apiCascade` so URL filters can be declared per-host-app. |
27
+ | `MushiDiagnosticsResult` | Return shape of `Mushi.diagnose()` (CSP, runtime-config, capture, widget health). |
28
+ | `MushiSdkVersionInfo` | Response shape for `getLatestSdkVersion(packageName)`; powers the outdated-banner UI. |
29
+ | `MushiTimelineEntry` | `{ ts, kind: 'route' \| 'click' \| 'request' \| 'log' \| 'screen', payload }` repro entries. |
30
+ | `MushiReporterReport` | Reporter-facing report row (HMAC-authed) with `unread_count` for the widget badge. |
31
+ | `MushiReporterComment` | Reporter-facing comment row (HMAC-authed) tagged `author_kind: 'admin' \| 'reporter'`. |
32
+ | `MushiDiscoverInventoryConfig` | Mushi v2.1 — fine-grained controls for `capture.discoverInventory` (`enabled`, `throttleMs`, `routeTemplates`, `userIdSource`, `captureDomSummary`). Pass `true` for defaults. |
33
+ | `MushiDiscoveryEventPayload` | Mushi v2.1 — wire shape for `POST /v1/sdk/discovery`. Mirrored server-side by `_shared/schemas.ts::discoveryEventSchema`; route + page title + testids + network paths + query-param **keys only** + sha256 user id hash. |
34
+
35
+ Constants: `MUSHI_INTERNAL_HEADER` (`'X-Mushi-Internal'`),
36
+ `MUSHI_INTERNAL_INIT_MARKER`, and the `MushiInternalRequestKind` literal union
37
+ are re-exported so framework adapters can build their own self-noise filters.
38
+
18
39
  ## Usage
19
40
 
20
41
  ```typescript
21
- import { createApiClient, createPreFilter, captureEnvironment, createRateLimiter } from '@mushi-mushi/core';
42
+ import {
43
+ createApiClient,
44
+ createPreFilter,
45
+ captureEnvironment,
46
+ createRateLimiter,
47
+ MUSHI_INTERNAL_HEADER,
48
+ } from '@mushi-mushi/core';
22
49
  ```
23
50
 
24
51
  This package is used internally by `@mushi-mushi/web` and `@mushi-mushi/react`. Most consumers should use those packages instead.
package/dist/index.cjs CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  // src/api-client.ts
4
4
  var DEFAULT_API_ENDPOINT = "https://dxptnwrhwsqckaftyymj.supabase.co/functions/v1/api";
5
+ var MUSHI_INTERNAL_HEADER = "X-Mushi-Internal";
6
+ var MUSHI_INTERNAL_INIT_MARKER = "__mushiInternal";
5
7
  var DEFAULT_TIMEOUT = 1e4;
6
8
  var DEFAULT_MAX_RETRIES = 2;
7
9
  function createApiClient(options) {
@@ -13,7 +15,7 @@ function createApiClient(options) {
13
15
  maxRetries = DEFAULT_MAX_RETRIES
14
16
  } = options;
15
17
  let baseUrl = apiEndpoint.replace(/\/$/, "");
16
- async function request(method, path, body, retries = maxRetries) {
18
+ async function request(method, path, body, retries = maxRetries, internalKind) {
17
19
  const url = `${baseUrl}${path}`;
18
20
  const controller = new AbortController();
19
21
  const timer = setTimeout(() => controller.abort(), timeout);
@@ -23,10 +25,12 @@ function createApiClient(options) {
23
25
  headers: {
24
26
  "Content-Type": "application/json",
25
27
  "X-Mushi-Api-Key": apiKey,
26
- "X-Mushi-Project": projectId
28
+ "X-Mushi-Project": projectId,
29
+ ...internalKind ? { [MUSHI_INTERNAL_HEADER]: internalKind } : {}
27
30
  },
28
31
  body: body ? JSON.stringify(body) : void 0,
29
- signal: controller.signal
32
+ signal: controller.signal,
33
+ ...internalKind ? { [MUSHI_INTERNAL_INIT_MARKER]: internalKind } : {}
30
34
  });
31
35
  clearTimeout(timer);
32
36
  if (response.status === 307 || response.status === 308) {
@@ -35,7 +39,7 @@ function createApiClient(options) {
35
39
  const targetBase = target.replace(/\/v1\/.*$/, "").replace(/\/$/, "");
36
40
  if (targetBase !== baseUrl) {
37
41
  baseUrl = targetBase;
38
- return request(method, path, body, retries - 1);
42
+ return request(method, path, body, retries - 1, internalKind);
39
43
  }
40
44
  }
41
45
  }
@@ -43,7 +47,7 @@ function createApiClient(options) {
43
47
  const errorBody = await response.json().catch(() => ({}));
44
48
  if (response.status >= 500 && retries > 0) {
45
49
  await sleep(getBackoffDelay(maxRetries - retries));
46
- return request(method, path, body, retries - 1);
50
+ return request(method, path, body, retries - 1, internalKind);
47
51
  }
48
52
  return {
49
53
  ok: false,
@@ -60,7 +64,7 @@ function createApiClient(options) {
60
64
  clearTimeout(timer);
61
65
  if (retries > 0 && isRetryable(error)) {
62
66
  await sleep(getBackoffDelay(maxRetries - retries));
63
- return request(method, path, body, retries - 1);
67
+ return request(method, path, body, retries - 1, internalKind);
64
68
  }
65
69
  return {
66
70
  ok: false,
@@ -71,18 +75,96 @@ function createApiClient(options) {
71
75
  };
72
76
  }
73
77
  }
78
+ async function requestForReporter(method, path, reporterToken, body) {
79
+ const tokenHash = await sha256Hex(reporterToken);
80
+ const ts = String(Date.now());
81
+ const hmac = await hmacSha256Hex(apiKey, `${projectId}.${ts}.${tokenHash}`);
82
+ const url = `${baseUrl}${path}`;
83
+ const response = await fetch(url, {
84
+ method,
85
+ headers: {
86
+ "Content-Type": "application/json",
87
+ "X-Mushi-Api-Key": apiKey,
88
+ "X-Mushi-Project": projectId,
89
+ [MUSHI_INTERNAL_HEADER]: "reporter-poll",
90
+ "X-Reporter-Token-Hash": tokenHash,
91
+ "X-Reporter-Ts": ts,
92
+ "X-Reporter-Hmac": hmac
93
+ },
94
+ body: body ? JSON.stringify(body) : void 0,
95
+ [MUSHI_INTERNAL_INIT_MARKER]: "reporter-poll"
96
+ });
97
+ if (!response.ok) {
98
+ const errorBody = await response.json().catch(() => ({}));
99
+ return {
100
+ ok: false,
101
+ error: {
102
+ code: `HTTP_${response.status}`,
103
+ message: errorBody.error?.message ?? errorBody.message ?? `HTTP ${response.status} error`
104
+ }
105
+ };
106
+ }
107
+ const payload = await response.json();
108
+ return { ok: true, data: payload.data ?? payload };
109
+ }
74
110
  return {
75
111
  async submitReport(report) {
76
- return request("POST", "/v1/reports", report);
112
+ return request("POST", "/v1/reports", report, maxRetries, "report-submit");
77
113
  },
78
114
  async getReportStatus(reportId) {
79
- return request("GET", `/v1/reports/${reportId}/status`);
115
+ return request("GET", `/v1/reports/${reportId}/status`, void 0, maxRetries, "report-status");
80
116
  },
81
117
  async getSdkConfig() {
82
- return request("GET", "/v1/sdk/config");
118
+ return request("GET", "/v1/sdk/config", void 0, maxRetries, "sdk-config");
119
+ },
120
+ async getLatestSdkVersion(packageName) {
121
+ const query = new URLSearchParams({ package: packageName }).toString();
122
+ return request("GET", `/v1/sdk/latest-version?${query}`, void 0, maxRetries, "sdk-config");
123
+ },
124
+ async postDiscoveryEvent(event) {
125
+ return request(
126
+ "POST",
127
+ "/v1/sdk/discovery",
128
+ event,
129
+ 1,
130
+ "discovery"
131
+ );
132
+ },
133
+ async listReporterReports(reporterToken) {
134
+ return requestForReporter("GET", "/v1/reporter/reports", reporterToken);
135
+ },
136
+ async listReporterComments(reportId, reporterToken) {
137
+ return requestForReporter(
138
+ "GET",
139
+ `/v1/reporter/reports/${reportId}/comments`,
140
+ reporterToken
141
+ );
142
+ },
143
+ async replyToReporterReport(reportId, reporterToken, body) {
144
+ return requestForReporter(
145
+ "POST",
146
+ `/v1/reporter/reports/${reportId}/reply`,
147
+ reporterToken,
148
+ { body }
149
+ );
83
150
  }
84
151
  };
85
152
  }
153
+ async function sha256Hex(value) {
154
+ const buffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
155
+ return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
156
+ }
157
+ async function hmacSha256Hex(secret, value) {
158
+ const key = await crypto.subtle.importKey(
159
+ "raw",
160
+ new TextEncoder().encode(secret),
161
+ { name: "HMAC", hash: "SHA-256" },
162
+ false,
163
+ ["sign"]
164
+ );
165
+ const buffer = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(value));
166
+ return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
167
+ }
86
168
  function sleep(ms) {
87
169
  return new Promise((resolve) => setTimeout(resolve, ms));
88
170
  }
@@ -722,9 +804,23 @@ function captureEnvironment() {
722
804
  rtt: connection.rtt
723
805
  } : void 0,
724
806
  deviceMemory: nav?.deviceMemory,
725
- hardwareConcurrency: nav?.hardwareConcurrency
807
+ hardwareConcurrency: nav?.hardwareConcurrency,
808
+ route: win?.location?.pathname,
809
+ nearestTestid: findNearestTestidFromActive(doc)
726
810
  };
727
811
  }
812
+ function findNearestTestidFromActive(doc) {
813
+ if (!doc) return void 0;
814
+ let cur = doc.activeElement ?? null;
815
+ let hops = 0;
816
+ while (cur && hops < 20) {
817
+ const tid = cur.getAttribute?.("data-testid");
818
+ if (tid) return tid;
819
+ cur = cur.parentElement;
820
+ hops++;
821
+ }
822
+ return void 0;
823
+ }
728
824
 
729
825
  // src/reporter-token.ts
730
826
  var STORAGE_KEY = "mushi_reporter_token";
@@ -776,7 +872,7 @@ function collectInputs() {
776
872
  hardwareConcurrency: nav?.hardwareConcurrency
777
873
  };
778
874
  }
779
- async function sha256Hex(input) {
875
+ async function sha256Hex2(input) {
780
876
  if (typeof crypto !== "undefined" && crypto.subtle) {
781
877
  const buf = new TextEncoder().encode(input);
782
878
  const digest = await crypto.subtle.digest("SHA-256", buf);
@@ -796,7 +892,7 @@ async function getDeviceFingerprintHash() {
796
892
  }
797
893
  const inputs = collectInputs();
798
894
  const serialised = JSON.stringify(inputs);
799
- const hash = await sha256Hex(serialised);
895
+ const hash = await sha256Hex2(serialised);
800
896
  if (typeof localStorage !== "undefined") {
801
897
  try {
802
898
  localStorage.setItem(CACHE_KEY, hash);
@@ -933,6 +1029,8 @@ function scrubPii(text, config) {
933
1029
  }
934
1030
 
935
1031
  exports.DEFAULT_API_ENDPOINT = DEFAULT_API_ENDPOINT;
1032
+ exports.MUSHI_INTERNAL_HEADER = MUSHI_INTERNAL_HEADER;
1033
+ exports.MUSHI_INTERNAL_INIT_MARKER = MUSHI_INTERNAL_INIT_MARKER;
936
1034
  exports.REGION_ENDPOINTS = REGION_ENDPOINTS;
937
1035
  exports.captureEnvironment = captureEnvironment;
938
1036
  exports.createApiClient = createApiClient;