@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 +29 -2
- package/dist/index.cjs +110 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +232 -3
- package/dist/index.d.ts +232 -3
- package/dist/index.js +109 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 {
|
|
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
|
|
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
|
|
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;
|