@mushi-mushi/react-native 0.19.0 → 0.19.2
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/CHANGELOG.md +26 -0
- package/CONTRIBUTING.md +12 -0
- package/README.md +16 -2
- package/dist/index.cjs +229 -77
- package/dist/index.d.cts +22 -7
- package/dist/index.d.ts +22 -7
- package/dist/index.js +221 -64
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @mushi-mushi/react-native
|
|
2
2
|
|
|
3
|
+
## 0.19.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7b44c97: SDK fixes from automated code review.
|
|
8
|
+
|
|
9
|
+
- **Favicon trust boundary** (`@mushi-mushi/core`): `readPageFaviconHref` now returns only http(s) URLs, so a host page can't get a `data:` / `blob:` / `javascript:` favicon rendered into the widget's `<img src>`; anything else falls back to the default mark.
|
|
10
|
+
- **Self-hosted credential message** (`@mushi-mushi/core`, `@mushi-mushi/react-native`): the one-time 401/403 "credentials rejected" warning only links to the hosted console when the client is actually using the Cloud endpoint; self-hosted deployments get a console-agnostic message instead of a wrong domain.
|
|
11
|
+
- **Offline-queue data loss** (`@mushi-mushi/react-native`): `decryptQueueBlob` now decrypts any blob carrying the encrypted prefix regardless of the current `secureStorage` flag. Previously, toggling `secureStorage` from `true` to `false` returned the still-encrypted string, which failed `JSON.parse` and silently cleared the offline report queue.
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [7b44c97]
|
|
14
|
+
- @mushi-mushi/core@1.19.2
|
|
15
|
+
|
|
16
|
+
## 0.19.1
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- 08108e6: Setup UX overhaul: zero-paste `mushi login` browser device-auth, credential error visibility, and docs fixes.
|
|
21
|
+
|
|
22
|
+
- **CLI**: `mushi login` now implements RFC 8628 browser device-auth (zero copy-paste). Opens the console in the browser, user clicks Approve, CLI receives a session token automatically, then lists/creates a project and saves the API key. `--api-key` flag remains as the CI/non-interactive fallback.
|
|
23
|
+
- **Core SDK**: 401/403 responses now emit a one-time `console.error` with a clear credential-failure message and the console URL, instead of silently entering the offline retry queue.
|
|
24
|
+
- **React Native**: Same 401/403 credential-failure detection in `MushiProvider.submitReport` — skips enqueue and surfaces the error immediately.
|
|
25
|
+
|
|
26
|
+
- Updated dependencies [08108e6]
|
|
27
|
+
- @mushi-mushi/core@1.19.1
|
|
28
|
+
|
|
3
29
|
## 0.19.0
|
|
4
30
|
|
|
5
31
|
### Minor Changes
|
package/CONTRIBUTING.md
CHANGED
|
@@ -27,12 +27,24 @@ pnpm build
|
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
29
|
pnpm dev # Start all dev servers (admin on :6464)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Surface | URL |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| Admin console | http://localhost:6464 |
|
|
35
|
+
| Docs site | http://localhost:3000 |
|
|
36
|
+
| CLI console override | `MUSHI_CONSOLE_URL=http://localhost:6464` |
|
|
37
|
+
|
|
38
|
+
```bash
|
|
30
39
|
pnpm test # Run Vitest across all packages
|
|
31
40
|
pnpm typecheck # TypeScript checks
|
|
32
41
|
pnpm lint # ESLint
|
|
33
42
|
pnpm format # Prettier
|
|
43
|
+
pnpm smoke:cli-setup # Playwright: onboarding ?setup=cli + success panel (skips headless if unsigned)
|
|
34
44
|
```
|
|
35
45
|
|
|
46
|
+
Set `CLI_SETUP_SMOKE_STRICT=1` to fail the smoke script when Playwright is not signed in (CI gate).
|
|
47
|
+
|
|
36
48
|
Ad-hoc screenshots captured during UI reviews can live temporarily at the repo
|
|
37
49
|
root, but root-level `*.png` files are intentionally ignored. Canonical
|
|
38
50
|
screenshots that should be versioned belong under `docs/screenshots/`.
|
package/README.md
CHANGED
|
@@ -106,13 +106,27 @@ import { MushiBottomSheet, MushiFloatingButton } from '@mushi-mushi/react-native
|
|
|
106
106
|
|
|
107
107
|
## Screenshots
|
|
108
108
|
|
|
109
|
-
When `react-native-view-shot` is installed, the SDK captures the current screen **before** the report sheet overlays it, and shows the user a thumbnail they can remove before submitting. Enabled by default; turn off with `capture: { screenshot: false }`.
|
|
109
|
+
When `react-native-view-shot` is installed, the SDK captures the current screen **before** the report sheet overlays it, and shows the user a **thumbnail preview** they can remove before submitting. A configurable **privacy caption** (`widget.screenshotSensitiveHint`) appears under the preview — default copy reminds reporters to drop balances or PII. Enabled by default; turn off capture with `capture: { screenshot: false }`.
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
<MushiProvider
|
|
113
|
+
config={{
|
|
114
|
+
widget: {
|
|
115
|
+
// true (default) | "Custom compliance copy" | false (hide caption only)
|
|
116
|
+
screenshotSensitiveHint: true,
|
|
117
|
+
},
|
|
118
|
+
capture: { screenshot: true },
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
```
|
|
110
122
|
|
|
111
123
|
```bash
|
|
112
124
|
npx expo install react-native-view-shot # v4+ on Expo SDK 55; v5+ for Fabric / New Architecture
|
|
113
125
|
```
|
|
114
126
|
|
|
115
|
-
> **Masking sensitive screens.** `captureScreen()` grabs whatever is on screen
|
|
127
|
+
> **Masking sensitive screens.** `captureScreen()` grabs whatever is on screen. The preview + Remove control is the reporter's consent gate — finance apps can keep screenshots on with a custom caption instead of disabling capture entirely. For screens that must never be photographed, set `capture: { screenshot: false }` while they're focused, or use `expo-screen-capture` to mark the view secure. Full doc: [`docs/SDK_SCREENSHOT_PREVIEW.md`](../../docs/SDK_SCREENSHOT_PREVIEW.md).
|
|
128
|
+
|
|
129
|
+
> **Metro / Hermes hosts.** The published dist uses esbuild `__require()` for optional peers. Host apps may need a postinstall patch (see yen-yen `scripts/patch-mushi.mjs`) so Metro resolves `react-native-view-shot` and `@react-native-community/netinfo`.
|
|
116
130
|
|
|
117
131
|
## Identifying the reporter
|
|
118
132
|
|
package/dist/index.cjs
CHANGED
|
@@ -47,9 +47,10 @@ module.exports = __toCommonJS(index_exports);
|
|
|
47
47
|
|
|
48
48
|
// src/provider.tsx
|
|
49
49
|
var import_react3 = require("react");
|
|
50
|
-
var
|
|
50
|
+
var import_core5 = require("@mushi-mushi/core");
|
|
51
51
|
|
|
52
52
|
// src/capture/console-capture.ts
|
|
53
|
+
var import_core = require("@mushi-mushi/core");
|
|
53
54
|
function setupConsoleCapture(maxEntries = 100) {
|
|
54
55
|
const entries = [];
|
|
55
56
|
const methods = ["log", "warn", "error"];
|
|
@@ -60,7 +61,7 @@ function setupConsoleCapture(maxEntries = 100) {
|
|
|
60
61
|
console[method] = (...args) => {
|
|
61
62
|
entries.push({
|
|
62
63
|
level: method,
|
|
63
|
-
message: args.map(String).join(" ").slice(0, 500),
|
|
64
|
+
message: (0, import_core.scrubPii)(args.map(String).join(" ").slice(0, 500)),
|
|
64
65
|
timestamp: Date.now()
|
|
65
66
|
});
|
|
66
67
|
if (entries.length > maxEntries) entries.shift();
|
|
@@ -189,16 +190,153 @@ function getDeviceFingerprintHash(device) {
|
|
|
189
190
|
}
|
|
190
191
|
|
|
191
192
|
// src/storage/async-storage-queue.ts
|
|
192
|
-
var
|
|
193
|
+
var import_core2 = require("@mushi-mushi/core");
|
|
194
|
+
|
|
195
|
+
// src/storage/secure-storage.ts
|
|
196
|
+
var REPORTER_TOKEN_KEY = "@mushi:reporter_token";
|
|
197
|
+
var QUEUE_CRYPTO_KEY = "@mushi:queue_aes_key";
|
|
198
|
+
var LEGACY_QUEUE_KEY = "@mushi:offline_queue";
|
|
199
|
+
var ENCRYPTED_QUEUE_KEY = "@mushi:offline_queue_enc";
|
|
200
|
+
var ENCRYPTED_PREFIX = "mushi_enc_v1:";
|
|
201
|
+
async function loadAsyncStorage() {
|
|
202
|
+
try {
|
|
203
|
+
const mod = await import("@react-native-async-storage/async-storage");
|
|
204
|
+
return mod.default;
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function loadSecureStore() {
|
|
210
|
+
try {
|
|
211
|
+
const mod = await import("expo-secure-store");
|
|
212
|
+
return mod.default ?? mod;
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function bytesToBase64(bytes) {
|
|
218
|
+
let binary = "";
|
|
219
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
220
|
+
if (typeof btoa === "function") return btoa(binary);
|
|
221
|
+
const B = globalThis.Buffer;
|
|
222
|
+
if (B) return B.from(bytes).toString("base64");
|
|
223
|
+
return "";
|
|
224
|
+
}
|
|
225
|
+
function base64ToBytes(b64) {
|
|
226
|
+
if (typeof atob === "function") {
|
|
227
|
+
const binary = atob(b64);
|
|
228
|
+
const out = new Uint8Array(binary.length);
|
|
229
|
+
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
const B = globalThis.Buffer;
|
|
233
|
+
if (B) return new Uint8Array(B.from(b64, "base64"));
|
|
234
|
+
return new Uint8Array();
|
|
235
|
+
}
|
|
236
|
+
function hasWebCrypto() {
|
|
237
|
+
return typeof globalThis.crypto?.subtle?.importKey === "function";
|
|
238
|
+
}
|
|
239
|
+
async function getOrCreateQueueKey(secure) {
|
|
240
|
+
if (!hasWebCrypto()) return null;
|
|
241
|
+
let rawB64 = await secure.getItemAsync(QUEUE_CRYPTO_KEY);
|
|
242
|
+
if (!rawB64) {
|
|
243
|
+
const raw2 = new Uint8Array(32);
|
|
244
|
+
globalThis.crypto.getRandomValues(raw2);
|
|
245
|
+
rawB64 = bytesToBase64(raw2);
|
|
246
|
+
await secure.setItemAsync(QUEUE_CRYPTO_KEY, rawB64);
|
|
247
|
+
}
|
|
248
|
+
const raw = base64ToBytes(rawB64);
|
|
249
|
+
return globalThis.crypto.subtle.importKey("raw", raw, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
|
|
250
|
+
}
|
|
251
|
+
async function encryptQueueBlob(plaintext, secureStorage) {
|
|
252
|
+
if (!secureStorage) return plaintext;
|
|
253
|
+
const secure = await loadSecureStore();
|
|
254
|
+
if (!secure) return plaintext;
|
|
255
|
+
const key = await getOrCreateQueueKey(secure);
|
|
256
|
+
if (!key) return plaintext;
|
|
257
|
+
const iv = new Uint8Array(12);
|
|
258
|
+
globalThis.crypto.getRandomValues(iv);
|
|
259
|
+
const encoded = new TextEncoder().encode(plaintext);
|
|
260
|
+
const cipher = await globalThis.crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encoded);
|
|
261
|
+
const combined = new Uint8Array(iv.length + cipher.byteLength);
|
|
262
|
+
combined.set(iv, 0);
|
|
263
|
+
combined.set(new Uint8Array(cipher), iv.length);
|
|
264
|
+
return ENCRYPTED_PREFIX + bytesToBase64(combined);
|
|
265
|
+
}
|
|
266
|
+
async function decryptQueueBlob(stored, _secureStorage) {
|
|
267
|
+
if (!stored.startsWith(ENCRYPTED_PREFIX)) return stored;
|
|
268
|
+
const secure = await loadSecureStore();
|
|
269
|
+
if (!secure || !hasWebCrypto()) return stored;
|
|
270
|
+
const key = await getOrCreateQueueKey(secure);
|
|
271
|
+
if (!key) return stored;
|
|
272
|
+
const combined = base64ToBytes(stored.slice(ENCRYPTED_PREFIX.length));
|
|
273
|
+
if (combined.length < 13) return stored;
|
|
274
|
+
const iv = combined.slice(0, 12);
|
|
275
|
+
const cipher = combined.slice(12);
|
|
276
|
+
try {
|
|
277
|
+
const plain = await globalThis.crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, cipher);
|
|
278
|
+
return new TextDecoder().decode(plain);
|
|
279
|
+
} catch {
|
|
280
|
+
return stored;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function loadReporterToken(secureStorage) {
|
|
284
|
+
if (secureStorage) {
|
|
285
|
+
const secure = await loadSecureStore();
|
|
286
|
+
if (secure) {
|
|
287
|
+
const fromSecure = await secure.getItemAsync(REPORTER_TOKEN_KEY);
|
|
288
|
+
if (fromSecure?.startsWith("mushi_")) return fromSecure;
|
|
289
|
+
const storage2 = await loadAsyncStorage();
|
|
290
|
+
if (storage2) {
|
|
291
|
+
const legacy = await storage2.getItem(REPORTER_TOKEN_KEY);
|
|
292
|
+
if (legacy?.startsWith("mushi_")) {
|
|
293
|
+
await secure.setItemAsync(REPORTER_TOKEN_KEY, legacy);
|
|
294
|
+
await storage2.removeItem(REPORTER_TOKEN_KEY);
|
|
295
|
+
return legacy;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return fromSecure;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const storage = await loadAsyncStorage();
|
|
302
|
+
if (!storage) return null;
|
|
303
|
+
return storage.getItem(REPORTER_TOKEN_KEY);
|
|
304
|
+
}
|
|
305
|
+
async function saveReporterToken(token, secureStorage) {
|
|
306
|
+
if (secureStorage) {
|
|
307
|
+
const secure = await loadSecureStore();
|
|
308
|
+
if (secure) {
|
|
309
|
+
await secure.setItemAsync(REPORTER_TOKEN_KEY, token);
|
|
310
|
+
const storage2 = await loadAsyncStorage();
|
|
311
|
+
if (storage2) await storage2.removeItem(REPORTER_TOKEN_KEY);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const storage = await loadAsyncStorage();
|
|
316
|
+
if (storage) await storage.setItem(REPORTER_TOKEN_KEY, token);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/storage/async-storage-queue.ts
|
|
320
|
+
var QUEUE_KEY = ENCRYPTED_QUEUE_KEY;
|
|
193
321
|
var AsyncStorageQueue = class {
|
|
194
322
|
maxSize;
|
|
195
323
|
apiEndpoint;
|
|
196
324
|
apiKey;
|
|
325
|
+
projectId;
|
|
326
|
+
sdkPackage;
|
|
327
|
+
sdkVersion;
|
|
328
|
+
getUserToken;
|
|
329
|
+
secureStorage;
|
|
197
330
|
onSynced;
|
|
198
331
|
constructor(config) {
|
|
199
332
|
this.maxSize = config.maxSize ?? 50;
|
|
200
333
|
this.apiEndpoint = config.apiEndpoint;
|
|
201
334
|
this.apiKey = config.apiKey;
|
|
335
|
+
this.projectId = config.projectId;
|
|
336
|
+
this.sdkPackage = config.sdkPackage;
|
|
337
|
+
this.sdkVersion = config.sdkVersion;
|
|
338
|
+
this.getUserToken = config.getUserToken;
|
|
339
|
+
this.secureStorage = config.secureStorage !== false;
|
|
202
340
|
this.onSynced = config.onSynced;
|
|
203
341
|
}
|
|
204
342
|
async enqueue(report) {
|
|
@@ -208,7 +346,7 @@ var AsyncStorageQueue = class {
|
|
|
208
346
|
if (queue.length >= this.maxSize) queue.shift();
|
|
209
347
|
const scrubbedReport = this.scrubReportPii(report);
|
|
210
348
|
queue.push({ report: scrubbedReport, enqueuedAt: Date.now() });
|
|
211
|
-
await
|
|
349
|
+
await this.persistQueue(AsyncStorage, queue);
|
|
212
350
|
}
|
|
213
351
|
async flush() {
|
|
214
352
|
const AsyncStorage = await this.getAsyncStorage();
|
|
@@ -218,7 +356,14 @@ var AsyncStorageQueue = class {
|
|
|
218
356
|
let flushed = 0;
|
|
219
357
|
const remaining = [];
|
|
220
358
|
for (const item of queue) {
|
|
221
|
-
const ok = await sendWithRetry(item,
|
|
359
|
+
const ok = await sendWithRetry(item, {
|
|
360
|
+
apiEndpoint: this.apiEndpoint,
|
|
361
|
+
apiKey: this.apiKey,
|
|
362
|
+
projectId: this.projectId,
|
|
363
|
+
sdkPackage: this.sdkPackage,
|
|
364
|
+
sdkVersion: this.sdkVersion,
|
|
365
|
+
getUserToken: this.getUserToken
|
|
366
|
+
});
|
|
222
367
|
if (ok) {
|
|
223
368
|
flushed++;
|
|
224
369
|
const reportId = item.report.id;
|
|
@@ -233,7 +378,7 @@ var AsyncStorageQueue = class {
|
|
|
233
378
|
break;
|
|
234
379
|
}
|
|
235
380
|
}
|
|
236
|
-
await
|
|
381
|
+
await this.persistQueue(AsyncStorage, remaining);
|
|
237
382
|
return flushed;
|
|
238
383
|
}
|
|
239
384
|
async size() {
|
|
@@ -242,31 +387,43 @@ var AsyncStorageQueue = class {
|
|
|
242
387
|
const queue = await this.getQueue(AsyncStorage);
|
|
243
388
|
return queue.length;
|
|
244
389
|
}
|
|
245
|
-
// Added: PII scrubbing (Phase 2.4)
|
|
246
|
-
// Scrubs free-text fields where users typically type emails / phones /
|
|
247
|
-
// card numbers when describing a bug. We deliberately do NOT scrub
|
|
248
|
-
// structured fields like `metadata.userEmail` — those are explicitly
|
|
249
|
-
// captured by the host app via `setUser()` and the operator opts in to
|
|
250
|
-
// collecting them. Scrubbing them here would silently break attribution.
|
|
251
390
|
scrubReportPii(report) {
|
|
252
391
|
const next = { ...report };
|
|
253
392
|
if (typeof next.description === "string") {
|
|
254
|
-
next.description = scrubPii(next.description);
|
|
393
|
+
next.description = (0, import_core2.scrubPii)(next.description);
|
|
255
394
|
}
|
|
256
395
|
if (typeof next.summary === "string") {
|
|
257
|
-
next.summary = scrubPii(next.summary);
|
|
396
|
+
next.summary = (0, import_core2.scrubPii)(next.summary);
|
|
258
397
|
}
|
|
259
398
|
if (Array.isArray(next.breadcrumbs)) {
|
|
260
399
|
next.breadcrumbs = next.breadcrumbs.map(
|
|
261
|
-
(b) => typeof b?.message === "string" ? { ...b, message: scrubPii(b.message) } : b
|
|
400
|
+
(b) => typeof b?.message === "string" ? { ...b, message: (0, import_core2.scrubPii)(b.message) } : b
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
if (Array.isArray(next.consoleLogs)) {
|
|
404
|
+
next.consoleLogs = next.consoleLogs.map(
|
|
405
|
+
(entry) => typeof entry?.message === "string" ? { ...entry, message: (0, import_core2.scrubPii)(entry.message) } : entry
|
|
262
406
|
);
|
|
263
407
|
}
|
|
264
408
|
return next;
|
|
265
409
|
}
|
|
410
|
+
async persistQueue(storage, queue) {
|
|
411
|
+
const plaintext = JSON.stringify(queue);
|
|
412
|
+
const blob = await encryptQueueBlob(plaintext, this.secureStorage);
|
|
413
|
+
await storage.setItem(QUEUE_KEY, blob);
|
|
414
|
+
}
|
|
266
415
|
async getQueue(storage) {
|
|
267
416
|
try {
|
|
268
|
-
|
|
269
|
-
|
|
417
|
+
let raw = await storage.getItem(QUEUE_KEY);
|
|
418
|
+
if (!raw) {
|
|
419
|
+
raw = await storage.getItem(LEGACY_QUEUE_KEY);
|
|
420
|
+
if (raw) {
|
|
421
|
+
await storage.removeItem(LEGACY_QUEUE_KEY);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (!raw) return [];
|
|
425
|
+
const plaintext = await decryptQueueBlob(raw, this.secureStorage);
|
|
426
|
+
return JSON.parse(plaintext);
|
|
270
427
|
} catch {
|
|
271
428
|
return [];
|
|
272
429
|
}
|
|
@@ -280,56 +437,33 @@ var AsyncStorageQueue = class {
|
|
|
280
437
|
}
|
|
281
438
|
}
|
|
282
439
|
};
|
|
283
|
-
|
|
284
|
-
[/\b\d{3}-\d{2}-\d{4}\b/g, "[REDACTED_SSN]"],
|
|
285
|
-
[/\b(?:\d[ -]*){12,18}\d\b/g, "[REDACTED_CC]"],
|
|
286
|
-
[/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g, "[REDACTED_AWS_KEY]"],
|
|
287
|
-
[
|
|
288
|
-
/(?:aws_secret_access_key|secret_access_key)["'\s:=]+[A-Za-z0-9/+=]{40}\b/gi,
|
|
289
|
-
"aws_secret_access_key=[REDACTED_AWS_SECRET]"
|
|
290
|
-
],
|
|
291
|
-
[/\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{24,}\b/g, "[REDACTED_STRIPE_KEY]"],
|
|
292
|
-
[/\bpk_(?:live|test)_[A-Za-z0-9]{24,}\b/g, "[REDACTED_STRIPE_PK]"],
|
|
293
|
-
[/\bxox[abpor]-[A-Za-z0-9-]{10,}\b/g, "[REDACTED_SLACK_TOKEN]"],
|
|
294
|
-
[/\bghp_[A-Za-z0-9]{36}\b/g, "[REDACTED_GITHUB_PAT]"],
|
|
295
|
-
[/\bgithub_pat_[A-Za-z0-9_]{80,}\b/g, "[REDACTED_GITHUB_PAT]"],
|
|
296
|
-
[/\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g, "[REDACTED_OPENAI_KEY]"],
|
|
297
|
-
[/\bsk-ant-[A-Za-z0-9_-]{20,}\b/g, "[REDACTED_ANTHROPIC_KEY]"],
|
|
298
|
-
[/\bAIza[0-9A-Za-z_-]{35}\b/g, "[REDACTED_GOOGLE_KEY]"],
|
|
299
|
-
[/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[REDACTED_JWT]"],
|
|
300
|
-
[/\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/g, "[REDACTED_EMAIL]"],
|
|
301
|
-
[
|
|
302
|
-
/(?:\+\d{1,3}[\s.\-])?\(?\d{2,4}\)?[\s.\-]\d{3,4}[\s.\-]\d{3,4}\b/g,
|
|
303
|
-
"[REDACTED_PHONE]"
|
|
304
|
-
]
|
|
305
|
-
];
|
|
306
|
-
function scrubPii(text) {
|
|
307
|
-
let result = text;
|
|
308
|
-
for (const [regex, replacement] of SCRUB_PATTERNS) {
|
|
309
|
-
result = result.replace(regex, replacement);
|
|
310
|
-
}
|
|
311
|
-
return result;
|
|
312
|
-
}
|
|
313
|
-
async function sendWithRetry(item, endpoint, apiKey, attempt = 0) {
|
|
440
|
+
async function sendWithRetry(item, ctx, attempt = 0) {
|
|
314
441
|
const delay = Math.min(1e3 * Math.pow(2, attempt) + Math.random() * 500, 1e4);
|
|
442
|
+
const internalKind = "report-submit";
|
|
443
|
+
const userToken = ctx.getUserToken?.() ?? null;
|
|
315
444
|
try {
|
|
316
|
-
const res = await fetch(`${
|
|
445
|
+
const res = await fetch(`${ctx.apiEndpoint}/v1/reports`, {
|
|
317
446
|
method: "POST",
|
|
318
|
-
headers: {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
447
|
+
headers: (0, import_core2.buildSdkIngestHeaders)({
|
|
448
|
+
apiKey: ctx.apiKey,
|
|
449
|
+
projectId: ctx.projectId,
|
|
450
|
+
sdkPackage: ctx.sdkPackage,
|
|
451
|
+
sdkVersion: ctx.sdkVersion,
|
|
452
|
+
userToken,
|
|
453
|
+
internalKind
|
|
454
|
+
}),
|
|
455
|
+
body: JSON.stringify(item.report),
|
|
456
|
+
[import_core2.MUSHI_INTERNAL_INIT_MARKER]: internalKind
|
|
323
457
|
});
|
|
324
458
|
if ((res.status === 429 || res.status >= 500) && attempt < 3) {
|
|
325
459
|
await sleep(delay);
|
|
326
|
-
return sendWithRetry(item,
|
|
460
|
+
return sendWithRetry(item, ctx, attempt + 1);
|
|
327
461
|
}
|
|
328
462
|
return res.ok;
|
|
329
463
|
} catch {
|
|
330
464
|
if (attempt < 3) {
|
|
331
465
|
await sleep(delay);
|
|
332
|
-
return sendWithRetry(item,
|
|
466
|
+
return sendWithRetry(item, ctx, attempt + 1);
|
|
333
467
|
}
|
|
334
468
|
return false;
|
|
335
469
|
}
|
|
@@ -341,7 +475,7 @@ function sleep(ms) {
|
|
|
341
475
|
// src/components/MushiBottomSheet.tsx
|
|
342
476
|
var import_react = require("react");
|
|
343
477
|
var import_react_native2 = require("react-native");
|
|
344
|
-
var
|
|
478
|
+
var import_core3 = require("@mushi-mushi/core");
|
|
345
479
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
346
480
|
var { height: SCREEN_HEIGHT } = import_react_native2.Dimensions.get("window");
|
|
347
481
|
var SHEET_HEIGHT = SCREEN_HEIGHT * 0.55;
|
|
@@ -499,7 +633,7 @@ var MushiBottomSheet = ({
|
|
|
499
633
|
}
|
|
500
634
|
};
|
|
501
635
|
const activeScreenshot = screenshotDataUrl && screenshotAttached ? screenshotDataUrl : null;
|
|
502
|
-
const pal = (0,
|
|
636
|
+
const pal = (0, import_core3.mushiPalette)(dark ? "dark" : "light");
|
|
503
637
|
const colors = dark ? { bg: pal.paper, text: pal.ink, sub: pal.inkMuted, card: pal.paperRaised, accent: NEON.accent, accentInk: NEON.ink, border: pal.ruleStrong, backdrop: "rgba(0,0,0,0.6)", disabled: "#3a3a3c", disabledText: pal.inkFaint } : { bg: pal.paperRaised, text: pal.ink, sub: pal.inkMuted, card: pal.paper, accent: NEON.accent, accentInk: NEON.ink, border: pal.ruleStrong, backdrop: "rgba(0,0,0,0.35)", disabled: "#d1d1d6", disabledText: pal.inkFaint };
|
|
504
638
|
const canSubmit = !!category && description.trim().length > 0 && phase === "form";
|
|
505
639
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Modal, { visible, transparent: true, animationType: "none", statusBarTranslucent: true, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
@@ -894,7 +1028,7 @@ var s = import_react_native2.StyleSheet.create({
|
|
|
894
1028
|
// src/components/MushiFloatingButton.tsx
|
|
895
1029
|
var import_react2 = require("react");
|
|
896
1030
|
var import_react_native3 = require("react-native");
|
|
897
|
-
var
|
|
1031
|
+
var import_core4 = require("@mushi-mushi/core");
|
|
898
1032
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
899
1033
|
var FAB_SIZE = 56;
|
|
900
1034
|
var EDGE_PADDING = 12;
|
|
@@ -946,7 +1080,7 @@ var MushiFloatingButton = ({
|
|
|
946
1080
|
const currentX = (0, import_react2.useRef)(baseX);
|
|
947
1081
|
const currentY = (0, import_react2.useRef)(0);
|
|
948
1082
|
const isDragging = (0, import_react2.useRef)(false);
|
|
949
|
-
const accentColor = accent ?? (0,
|
|
1083
|
+
const accentColor = accent ?? (0, import_core4.mushiPalette)(dark ? "dark" : "light").accent;
|
|
950
1084
|
(0, import_react2.useEffect)(() => {
|
|
951
1085
|
if (!persist) return;
|
|
952
1086
|
void loadStoredPos().then((pos) => {
|
|
@@ -1056,7 +1190,7 @@ var MushiFloatingButton = ({
|
|
|
1056
1190
|
onStartShouldSetResponder: () => !draggable,
|
|
1057
1191
|
onResponderGrant: handlePressIn,
|
|
1058
1192
|
onResponderRelease: handlePressOut,
|
|
1059
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Animated.Text, { style: s2.emoji, children:
|
|
1193
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Animated.Text, { style: s2.emoji, children: import_core4.MUSHI_COPY.triggerText })
|
|
1060
1194
|
}
|
|
1061
1195
|
);
|
|
1062
1196
|
};
|
|
@@ -1081,7 +1215,7 @@ var s2 = import_react_native3.StyleSheet.create({
|
|
|
1081
1215
|
|
|
1082
1216
|
// src/version.ts
|
|
1083
1217
|
var MUSHI_SDK_PACKAGE = "@mushi-mushi/react-native";
|
|
1084
|
-
var MUSHI_SDK_VERSION = "0.19.
|
|
1218
|
+
var MUSHI_SDK_VERSION = "0.19.2" ? "0.19.2" : "0.17.0";
|
|
1085
1219
|
|
|
1086
1220
|
// src/provider.tsx
|
|
1087
1221
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
@@ -1128,9 +1262,9 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1128
1262
|
const networkRef = (0, import_react3.useRef)(null);
|
|
1129
1263
|
const queueRef = (0, import_react3.useRef)(null);
|
|
1130
1264
|
const apiClientRef = (0, import_react3.useRef)(null);
|
|
1131
|
-
const sessionIdRef = (0, import_react3.useRef)(`sess_${(0,
|
|
1265
|
+
const sessionIdRef = (0, import_react3.useRef)(`sess_${(0, import_core5.newUuid)()}`);
|
|
1132
1266
|
const breadcrumbsRef = (0, import_react3.useRef)(
|
|
1133
|
-
(0,
|
|
1267
|
+
(0, import_core5.createBreadcrumbBuffer)({ max: config.capture?.maxBreadcrumbs ?? 50 })
|
|
1134
1268
|
);
|
|
1135
1269
|
const [sheetVisible, setSheetVisible] = (0, import_react3.useState)(false);
|
|
1136
1270
|
const [sheetScreenshot, setSheetScreenshot] = (0, import_react3.useState)(null);
|
|
@@ -1153,7 +1287,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1153
1287
|
if (config.endpoint && !config.apiEndpoint && __DEV__) {
|
|
1154
1288
|
console.warn("[MushiProvider] `endpoint` is deprecated \u2014 use `apiEndpoint` instead.");
|
|
1155
1289
|
}
|
|
1156
|
-
const apiEndpoint = rawEndpoint ??
|
|
1290
|
+
const apiEndpoint = rawEndpoint ?? import_core5.DEFAULT_API_ENDPOINT;
|
|
1157
1291
|
(0, import_react3.useEffect)(() => {
|
|
1158
1292
|
if (config.capture?.console !== false) {
|
|
1159
1293
|
consoleRef.current = setupConsoleCapture(config.capture?.maxConsoleEntries);
|
|
@@ -1161,7 +1295,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1161
1295
|
if (config.capture?.network !== false) {
|
|
1162
1296
|
networkRef.current = setupNetworkCapture(config.capture?.maxNetworkEntries, apiEndpoint);
|
|
1163
1297
|
}
|
|
1164
|
-
apiClientRef.current = (0,
|
|
1298
|
+
apiClientRef.current = (0, import_core5.createApiClient)({
|
|
1165
1299
|
projectId: config.projectId,
|
|
1166
1300
|
apiKey: config.apiKey,
|
|
1167
1301
|
apiEndpoint,
|
|
@@ -1173,6 +1307,11 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1173
1307
|
maxSize: config.storage?.maxQueueSize,
|
|
1174
1308
|
apiEndpoint,
|
|
1175
1309
|
apiKey: config.apiKey,
|
|
1310
|
+
projectId: config.projectId,
|
|
1311
|
+
sdkPackage: MUSHI_SDK_PACKAGE,
|
|
1312
|
+
sdkVersion: MUSHI_SDK_VERSION,
|
|
1313
|
+
getUserToken: () => userTokenRef.current,
|
|
1314
|
+
secureStorage: config.storage?.secureStorage,
|
|
1176
1315
|
// `onReportSynced` fires here — when a previously-queued report actually
|
|
1177
1316
|
// drains to the server — not on the direct-submit path (where it would be
|
|
1178
1317
|
// indistinguishable from `onReportSubmitted` and thus meaningless).
|
|
@@ -1187,21 +1326,20 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1187
1326
|
data: { sdk: MUSHI_SDK_PACKAGE, version: MUSHI_SDK_VERSION }
|
|
1188
1327
|
});
|
|
1189
1328
|
(async () => {
|
|
1190
|
-
const TOKEN_KEY = "@mushi:reporter_token";
|
|
1191
1329
|
try {
|
|
1192
|
-
const
|
|
1193
|
-
const existing = await
|
|
1330
|
+
const secureStorage = config.storage?.secureStorage !== false;
|
|
1331
|
+
const existing = await loadReporterToken(secureStorage);
|
|
1194
1332
|
if (existing && existing.startsWith("mushi_")) {
|
|
1195
1333
|
reporterTokenRef.current = existing;
|
|
1196
1334
|
} else {
|
|
1197
|
-
const fresh = `mushi_${(0,
|
|
1335
|
+
const fresh = `mushi_${(0, import_core5.newUuid)()}`;
|
|
1198
1336
|
reporterTokenRef.current = fresh;
|
|
1199
|
-
await
|
|
1337
|
+
await saveReporterToken(fresh, secureStorage);
|
|
1200
1338
|
}
|
|
1201
1339
|
resolveReporterTokenReady();
|
|
1202
1340
|
} catch {
|
|
1203
1341
|
if (reporterTokenRef.current === `rn-${config.projectId}-anon`) {
|
|
1204
|
-
reporterTokenRef.current = `mushi_${(0,
|
|
1342
|
+
reporterTokenRef.current = `mushi_${(0, import_core5.newUuid)()}`;
|
|
1205
1343
|
}
|
|
1206
1344
|
resolveReporterTokenReady();
|
|
1207
1345
|
}
|
|
@@ -1311,7 +1449,10 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1311
1449
|
if (screenRef.current?.route) contextMeta.screenRoute = screenRef.current.route;
|
|
1312
1450
|
if (screenRef.current?.feature) contextMeta.screenFeature = screenRef.current.feature;
|
|
1313
1451
|
const merged = Object.keys(metadataRef.current).length > 0 ? { ...contextMeta, ...metadataRef.current } : Object.keys(contextMeta).length > 0 ? contextMeta : void 0;
|
|
1314
|
-
const breadcrumbs = breadcrumbsRef.current.getAll()
|
|
1452
|
+
const breadcrumbs = breadcrumbsRef.current.getAll().map((c) => ({
|
|
1453
|
+
...c,
|
|
1454
|
+
message: (0, import_core5.scrubPii)(c.message)
|
|
1455
|
+
}));
|
|
1315
1456
|
const timeline = breadcrumbs.map((c) => ({
|
|
1316
1457
|
ts: c.timestamp,
|
|
1317
1458
|
kind: breadcrumbToTimelineKind(c.category),
|
|
@@ -1319,10 +1460,10 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1319
1460
|
}));
|
|
1320
1461
|
const fingerprintHash = getDeviceFingerprintHash(deviceInfo);
|
|
1321
1462
|
const report = {
|
|
1322
|
-
id: (0,
|
|
1463
|
+
id: (0, import_core5.newUuid)(),
|
|
1323
1464
|
projectId: config.projectId,
|
|
1324
1465
|
category: data.category,
|
|
1325
|
-
description: data.description,
|
|
1466
|
+
description: (0, import_core5.scrubPii)(data.description),
|
|
1326
1467
|
environment: {
|
|
1327
1468
|
userAgent: deviceInfo.systemName ?? "ReactNative",
|
|
1328
1469
|
platform: deviceInfo.platform ?? "mobile",
|
|
@@ -1333,7 +1474,10 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1333
1474
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1334
1475
|
timezone: deviceInfo.timezone ?? "UTC"
|
|
1335
1476
|
},
|
|
1336
|
-
consoleLogs: consoleRef.current?.getEntries() ?? []
|
|
1477
|
+
consoleLogs: (consoleRef.current?.getEntries() ?? []).map((entry) => ({
|
|
1478
|
+
...entry,
|
|
1479
|
+
message: (0, import_core5.scrubPii)(entry.message)
|
|
1480
|
+
})),
|
|
1337
1481
|
networkLogs: networkRef.current?.getEntries().map((entry) => ({
|
|
1338
1482
|
...entry,
|
|
1339
1483
|
status: entry.status ?? 0
|
|
@@ -1354,6 +1498,14 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1354
1498
|
if (!client) return;
|
|
1355
1499
|
const result = await client.submitReport(report);
|
|
1356
1500
|
if (!result.ok) {
|
|
1501
|
+
const isCredentialError = result.error?.code === "HTTP_401" || result.error?.code === "HTTP_403" || result.error?.code?.includes("UNAUTHORIZED") || result.error?.code?.includes("FORBIDDEN");
|
|
1502
|
+
if (isCredentialError) {
|
|
1503
|
+
const where = apiEndpoint.replace(/\/$/, "") === import_core5.DEFAULT_API_ENDPOINT ? "Get the correct values at: https://kensaur.us/mushi-mushi/admin/projects" : "Get the correct values from your Mushi console's Projects page.";
|
|
1504
|
+
console.error(
|
|
1505
|
+
'[Mushi] Credentials rejected. Check your projectId and apiKey (must have "report:write" scope). ' + where
|
|
1506
|
+
);
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1357
1509
|
await queueRef.current?.enqueue(report);
|
|
1358
1510
|
config.callbacks?.onReportQueued?.(report.id);
|
|
1359
1511
|
} else {
|
|
@@ -1416,7 +1568,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1416
1568
|
identifyWithToken(token) {
|
|
1417
1569
|
userTokenRef.current = token && typeof token === "string" ? token : null;
|
|
1418
1570
|
if (userTokenRef.current) {
|
|
1419
|
-
const claims = (0,
|
|
1571
|
+
const claims = (0, import_core5.parseIdentityToken)(userTokenRef.current);
|
|
1420
1572
|
if (claims?.sub) {
|
|
1421
1573
|
userRef.current = {
|
|
1422
1574
|
id: claims.sub,
|
package/dist/index.d.cts
CHANGED
|
@@ -98,6 +98,8 @@ interface MushiRNConfig {
|
|
|
98
98
|
storage?: {
|
|
99
99
|
maxQueueSize?: number;
|
|
100
100
|
retryIntervalMs?: number;
|
|
101
|
+
/** Encrypt offline queue + store reporter token in SecureStore when available. Default true. */
|
|
102
|
+
secureStorage?: boolean;
|
|
101
103
|
};
|
|
102
104
|
rewards?: MushiRewardsConfig;
|
|
103
105
|
}
|
|
@@ -298,22 +300,35 @@ declare function useNavigationCapture(navigationRef: {
|
|
|
298
300
|
addListener?: (event: string, cb: () => void) => () => void;
|
|
299
301
|
} | null): () => NavigationEntry[];
|
|
300
302
|
|
|
303
|
+
interface AsyncStorageQueueConfig {
|
|
304
|
+
maxSize?: number;
|
|
305
|
+
apiEndpoint: string;
|
|
306
|
+
apiKey: string;
|
|
307
|
+
projectId: string;
|
|
308
|
+
sdkPackage?: string;
|
|
309
|
+
sdkVersion?: string;
|
|
310
|
+
getUserToken?: () => string | null | undefined;
|
|
311
|
+
/** When true (default), encrypt queue + store reporter token in SecureStore when available. */
|
|
312
|
+
secureStorage?: boolean;
|
|
313
|
+
/** Fired once per queued report that drains successfully to the server. */
|
|
314
|
+
onSynced?: (reportId: string) => void;
|
|
315
|
+
}
|
|
301
316
|
declare class AsyncStorageQueue {
|
|
302
317
|
private maxSize;
|
|
303
318
|
private apiEndpoint;
|
|
304
319
|
private apiKey;
|
|
320
|
+
private projectId;
|
|
321
|
+
private sdkPackage?;
|
|
322
|
+
private sdkVersion?;
|
|
323
|
+
private getUserToken?;
|
|
324
|
+
private secureStorage;
|
|
305
325
|
private onSynced?;
|
|
306
|
-
constructor(config:
|
|
307
|
-
maxSize?: number;
|
|
308
|
-
apiEndpoint: string;
|
|
309
|
-
apiKey: string;
|
|
310
|
-
/** Fired once per queued report that drains successfully to the server. */
|
|
311
|
-
onSynced?: (reportId: string) => void;
|
|
312
|
-
});
|
|
326
|
+
constructor(config: AsyncStorageQueueConfig);
|
|
313
327
|
enqueue(report: object): Promise<void>;
|
|
314
328
|
flush(): Promise<number>;
|
|
315
329
|
size(): Promise<number>;
|
|
316
330
|
private scrubReportPii;
|
|
331
|
+
private persistQueue;
|
|
317
332
|
private getQueue;
|
|
318
333
|
private getAsyncStorage;
|
|
319
334
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -98,6 +98,8 @@ interface MushiRNConfig {
|
|
|
98
98
|
storage?: {
|
|
99
99
|
maxQueueSize?: number;
|
|
100
100
|
retryIntervalMs?: number;
|
|
101
|
+
/** Encrypt offline queue + store reporter token in SecureStore when available. Default true. */
|
|
102
|
+
secureStorage?: boolean;
|
|
101
103
|
};
|
|
102
104
|
rewards?: MushiRewardsConfig;
|
|
103
105
|
}
|
|
@@ -298,22 +300,35 @@ declare function useNavigationCapture(navigationRef: {
|
|
|
298
300
|
addListener?: (event: string, cb: () => void) => () => void;
|
|
299
301
|
} | null): () => NavigationEntry[];
|
|
300
302
|
|
|
303
|
+
interface AsyncStorageQueueConfig {
|
|
304
|
+
maxSize?: number;
|
|
305
|
+
apiEndpoint: string;
|
|
306
|
+
apiKey: string;
|
|
307
|
+
projectId: string;
|
|
308
|
+
sdkPackage?: string;
|
|
309
|
+
sdkVersion?: string;
|
|
310
|
+
getUserToken?: () => string | null | undefined;
|
|
311
|
+
/** When true (default), encrypt queue + store reporter token in SecureStore when available. */
|
|
312
|
+
secureStorage?: boolean;
|
|
313
|
+
/** Fired once per queued report that drains successfully to the server. */
|
|
314
|
+
onSynced?: (reportId: string) => void;
|
|
315
|
+
}
|
|
301
316
|
declare class AsyncStorageQueue {
|
|
302
317
|
private maxSize;
|
|
303
318
|
private apiEndpoint;
|
|
304
319
|
private apiKey;
|
|
320
|
+
private projectId;
|
|
321
|
+
private sdkPackage?;
|
|
322
|
+
private sdkVersion?;
|
|
323
|
+
private getUserToken?;
|
|
324
|
+
private secureStorage;
|
|
305
325
|
private onSynced?;
|
|
306
|
-
constructor(config:
|
|
307
|
-
maxSize?: number;
|
|
308
|
-
apiEndpoint: string;
|
|
309
|
-
apiKey: string;
|
|
310
|
-
/** Fired once per queued report that drains successfully to the server. */
|
|
311
|
-
onSynced?: (reportId: string) => void;
|
|
312
|
-
});
|
|
326
|
+
constructor(config: AsyncStorageQueueConfig);
|
|
313
327
|
enqueue(report: object): Promise<void>;
|
|
314
328
|
flush(): Promise<number>;
|
|
315
329
|
size(): Promise<number>;
|
|
316
330
|
private scrubReportPii;
|
|
331
|
+
private persistQueue;
|
|
317
332
|
private getQueue;
|
|
318
333
|
private getAsyncStorage;
|
|
319
334
|
}
|
package/dist/index.js
CHANGED
|
@@ -20,10 +20,12 @@ import {
|
|
|
20
20
|
createBreadcrumbBuffer,
|
|
21
21
|
DEFAULT_API_ENDPOINT,
|
|
22
22
|
newUuid,
|
|
23
|
-
parseIdentityToken
|
|
23
|
+
parseIdentityToken,
|
|
24
|
+
scrubPii as scrubPii3
|
|
24
25
|
} from "@mushi-mushi/core";
|
|
25
26
|
|
|
26
27
|
// src/capture/console-capture.ts
|
|
28
|
+
import { scrubPii } from "@mushi-mushi/core";
|
|
27
29
|
function setupConsoleCapture(maxEntries = 100) {
|
|
28
30
|
const entries = [];
|
|
29
31
|
const methods = ["log", "warn", "error"];
|
|
@@ -34,7 +36,7 @@ function setupConsoleCapture(maxEntries = 100) {
|
|
|
34
36
|
console[method] = (...args) => {
|
|
35
37
|
entries.push({
|
|
36
38
|
level: method,
|
|
37
|
-
message: args.map(String).join(" ").slice(0, 500),
|
|
39
|
+
message: scrubPii(args.map(String).join(" ").slice(0, 500)),
|
|
38
40
|
timestamp: Date.now()
|
|
39
41
|
});
|
|
40
42
|
if (entries.length > maxEntries) entries.shift();
|
|
@@ -163,16 +165,157 @@ function getDeviceFingerprintHash(device) {
|
|
|
163
165
|
}
|
|
164
166
|
|
|
165
167
|
// src/storage/async-storage-queue.ts
|
|
166
|
-
|
|
168
|
+
import {
|
|
169
|
+
buildSdkIngestHeaders,
|
|
170
|
+
MUSHI_INTERNAL_INIT_MARKER,
|
|
171
|
+
scrubPii as scrubPii2
|
|
172
|
+
} from "@mushi-mushi/core";
|
|
173
|
+
|
|
174
|
+
// src/storage/secure-storage.ts
|
|
175
|
+
var REPORTER_TOKEN_KEY = "@mushi:reporter_token";
|
|
176
|
+
var QUEUE_CRYPTO_KEY = "@mushi:queue_aes_key";
|
|
177
|
+
var LEGACY_QUEUE_KEY = "@mushi:offline_queue";
|
|
178
|
+
var ENCRYPTED_QUEUE_KEY = "@mushi:offline_queue_enc";
|
|
179
|
+
var ENCRYPTED_PREFIX = "mushi_enc_v1:";
|
|
180
|
+
async function loadAsyncStorage() {
|
|
181
|
+
try {
|
|
182
|
+
const mod = await import("@react-native-async-storage/async-storage");
|
|
183
|
+
return mod.default;
|
|
184
|
+
} catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function loadSecureStore() {
|
|
189
|
+
try {
|
|
190
|
+
const mod = await import("expo-secure-store");
|
|
191
|
+
return mod.default ?? mod;
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function bytesToBase64(bytes) {
|
|
197
|
+
let binary = "";
|
|
198
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
199
|
+
if (typeof btoa === "function") return btoa(binary);
|
|
200
|
+
const B = globalThis.Buffer;
|
|
201
|
+
if (B) return B.from(bytes).toString("base64");
|
|
202
|
+
return "";
|
|
203
|
+
}
|
|
204
|
+
function base64ToBytes(b64) {
|
|
205
|
+
if (typeof atob === "function") {
|
|
206
|
+
const binary = atob(b64);
|
|
207
|
+
const out = new Uint8Array(binary.length);
|
|
208
|
+
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
const B = globalThis.Buffer;
|
|
212
|
+
if (B) return new Uint8Array(B.from(b64, "base64"));
|
|
213
|
+
return new Uint8Array();
|
|
214
|
+
}
|
|
215
|
+
function hasWebCrypto() {
|
|
216
|
+
return typeof globalThis.crypto?.subtle?.importKey === "function";
|
|
217
|
+
}
|
|
218
|
+
async function getOrCreateQueueKey(secure) {
|
|
219
|
+
if (!hasWebCrypto()) return null;
|
|
220
|
+
let rawB64 = await secure.getItemAsync(QUEUE_CRYPTO_KEY);
|
|
221
|
+
if (!rawB64) {
|
|
222
|
+
const raw2 = new Uint8Array(32);
|
|
223
|
+
globalThis.crypto.getRandomValues(raw2);
|
|
224
|
+
rawB64 = bytesToBase64(raw2);
|
|
225
|
+
await secure.setItemAsync(QUEUE_CRYPTO_KEY, rawB64);
|
|
226
|
+
}
|
|
227
|
+
const raw = base64ToBytes(rawB64);
|
|
228
|
+
return globalThis.crypto.subtle.importKey("raw", raw, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
|
|
229
|
+
}
|
|
230
|
+
async function encryptQueueBlob(plaintext, secureStorage) {
|
|
231
|
+
if (!secureStorage) return plaintext;
|
|
232
|
+
const secure = await loadSecureStore();
|
|
233
|
+
if (!secure) return plaintext;
|
|
234
|
+
const key = await getOrCreateQueueKey(secure);
|
|
235
|
+
if (!key) return plaintext;
|
|
236
|
+
const iv = new Uint8Array(12);
|
|
237
|
+
globalThis.crypto.getRandomValues(iv);
|
|
238
|
+
const encoded = new TextEncoder().encode(plaintext);
|
|
239
|
+
const cipher = await globalThis.crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encoded);
|
|
240
|
+
const combined = new Uint8Array(iv.length + cipher.byteLength);
|
|
241
|
+
combined.set(iv, 0);
|
|
242
|
+
combined.set(new Uint8Array(cipher), iv.length);
|
|
243
|
+
return ENCRYPTED_PREFIX + bytesToBase64(combined);
|
|
244
|
+
}
|
|
245
|
+
async function decryptQueueBlob(stored, _secureStorage) {
|
|
246
|
+
if (!stored.startsWith(ENCRYPTED_PREFIX)) return stored;
|
|
247
|
+
const secure = await loadSecureStore();
|
|
248
|
+
if (!secure || !hasWebCrypto()) return stored;
|
|
249
|
+
const key = await getOrCreateQueueKey(secure);
|
|
250
|
+
if (!key) return stored;
|
|
251
|
+
const combined = base64ToBytes(stored.slice(ENCRYPTED_PREFIX.length));
|
|
252
|
+
if (combined.length < 13) return stored;
|
|
253
|
+
const iv = combined.slice(0, 12);
|
|
254
|
+
const cipher = combined.slice(12);
|
|
255
|
+
try {
|
|
256
|
+
const plain = await globalThis.crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, cipher);
|
|
257
|
+
return new TextDecoder().decode(plain);
|
|
258
|
+
} catch {
|
|
259
|
+
return stored;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async function loadReporterToken(secureStorage) {
|
|
263
|
+
if (secureStorage) {
|
|
264
|
+
const secure = await loadSecureStore();
|
|
265
|
+
if (secure) {
|
|
266
|
+
const fromSecure = await secure.getItemAsync(REPORTER_TOKEN_KEY);
|
|
267
|
+
if (fromSecure?.startsWith("mushi_")) return fromSecure;
|
|
268
|
+
const storage2 = await loadAsyncStorage();
|
|
269
|
+
if (storage2) {
|
|
270
|
+
const legacy = await storage2.getItem(REPORTER_TOKEN_KEY);
|
|
271
|
+
if (legacy?.startsWith("mushi_")) {
|
|
272
|
+
await secure.setItemAsync(REPORTER_TOKEN_KEY, legacy);
|
|
273
|
+
await storage2.removeItem(REPORTER_TOKEN_KEY);
|
|
274
|
+
return legacy;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return fromSecure;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const storage = await loadAsyncStorage();
|
|
281
|
+
if (!storage) return null;
|
|
282
|
+
return storage.getItem(REPORTER_TOKEN_KEY);
|
|
283
|
+
}
|
|
284
|
+
async function saveReporterToken(token, secureStorage) {
|
|
285
|
+
if (secureStorage) {
|
|
286
|
+
const secure = await loadSecureStore();
|
|
287
|
+
if (secure) {
|
|
288
|
+
await secure.setItemAsync(REPORTER_TOKEN_KEY, token);
|
|
289
|
+
const storage2 = await loadAsyncStorage();
|
|
290
|
+
if (storage2) await storage2.removeItem(REPORTER_TOKEN_KEY);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const storage = await loadAsyncStorage();
|
|
295
|
+
if (storage) await storage.setItem(REPORTER_TOKEN_KEY, token);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/storage/async-storage-queue.ts
|
|
299
|
+
var QUEUE_KEY = ENCRYPTED_QUEUE_KEY;
|
|
167
300
|
var AsyncStorageQueue = class {
|
|
168
301
|
maxSize;
|
|
169
302
|
apiEndpoint;
|
|
170
303
|
apiKey;
|
|
304
|
+
projectId;
|
|
305
|
+
sdkPackage;
|
|
306
|
+
sdkVersion;
|
|
307
|
+
getUserToken;
|
|
308
|
+
secureStorage;
|
|
171
309
|
onSynced;
|
|
172
310
|
constructor(config) {
|
|
173
311
|
this.maxSize = config.maxSize ?? 50;
|
|
174
312
|
this.apiEndpoint = config.apiEndpoint;
|
|
175
313
|
this.apiKey = config.apiKey;
|
|
314
|
+
this.projectId = config.projectId;
|
|
315
|
+
this.sdkPackage = config.sdkPackage;
|
|
316
|
+
this.sdkVersion = config.sdkVersion;
|
|
317
|
+
this.getUserToken = config.getUserToken;
|
|
318
|
+
this.secureStorage = config.secureStorage !== false;
|
|
176
319
|
this.onSynced = config.onSynced;
|
|
177
320
|
}
|
|
178
321
|
async enqueue(report) {
|
|
@@ -182,7 +325,7 @@ var AsyncStorageQueue = class {
|
|
|
182
325
|
if (queue.length >= this.maxSize) queue.shift();
|
|
183
326
|
const scrubbedReport = this.scrubReportPii(report);
|
|
184
327
|
queue.push({ report: scrubbedReport, enqueuedAt: Date.now() });
|
|
185
|
-
await
|
|
328
|
+
await this.persistQueue(AsyncStorage, queue);
|
|
186
329
|
}
|
|
187
330
|
async flush() {
|
|
188
331
|
const AsyncStorage = await this.getAsyncStorage();
|
|
@@ -192,7 +335,14 @@ var AsyncStorageQueue = class {
|
|
|
192
335
|
let flushed = 0;
|
|
193
336
|
const remaining = [];
|
|
194
337
|
for (const item of queue) {
|
|
195
|
-
const ok = await sendWithRetry(item,
|
|
338
|
+
const ok = await sendWithRetry(item, {
|
|
339
|
+
apiEndpoint: this.apiEndpoint,
|
|
340
|
+
apiKey: this.apiKey,
|
|
341
|
+
projectId: this.projectId,
|
|
342
|
+
sdkPackage: this.sdkPackage,
|
|
343
|
+
sdkVersion: this.sdkVersion,
|
|
344
|
+
getUserToken: this.getUserToken
|
|
345
|
+
});
|
|
196
346
|
if (ok) {
|
|
197
347
|
flushed++;
|
|
198
348
|
const reportId = item.report.id;
|
|
@@ -207,7 +357,7 @@ var AsyncStorageQueue = class {
|
|
|
207
357
|
break;
|
|
208
358
|
}
|
|
209
359
|
}
|
|
210
|
-
await
|
|
360
|
+
await this.persistQueue(AsyncStorage, remaining);
|
|
211
361
|
return flushed;
|
|
212
362
|
}
|
|
213
363
|
async size() {
|
|
@@ -216,31 +366,43 @@ var AsyncStorageQueue = class {
|
|
|
216
366
|
const queue = await this.getQueue(AsyncStorage);
|
|
217
367
|
return queue.length;
|
|
218
368
|
}
|
|
219
|
-
// Added: PII scrubbing (Phase 2.4)
|
|
220
|
-
// Scrubs free-text fields where users typically type emails / phones /
|
|
221
|
-
// card numbers when describing a bug. We deliberately do NOT scrub
|
|
222
|
-
// structured fields like `metadata.userEmail` — those are explicitly
|
|
223
|
-
// captured by the host app via `setUser()` and the operator opts in to
|
|
224
|
-
// collecting them. Scrubbing them here would silently break attribution.
|
|
225
369
|
scrubReportPii(report) {
|
|
226
370
|
const next = { ...report };
|
|
227
371
|
if (typeof next.description === "string") {
|
|
228
|
-
next.description =
|
|
372
|
+
next.description = scrubPii2(next.description);
|
|
229
373
|
}
|
|
230
374
|
if (typeof next.summary === "string") {
|
|
231
|
-
next.summary =
|
|
375
|
+
next.summary = scrubPii2(next.summary);
|
|
232
376
|
}
|
|
233
377
|
if (Array.isArray(next.breadcrumbs)) {
|
|
234
378
|
next.breadcrumbs = next.breadcrumbs.map(
|
|
235
|
-
(b) => typeof b?.message === "string" ? { ...b, message:
|
|
379
|
+
(b) => typeof b?.message === "string" ? { ...b, message: scrubPii2(b.message) } : b
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
if (Array.isArray(next.consoleLogs)) {
|
|
383
|
+
next.consoleLogs = next.consoleLogs.map(
|
|
384
|
+
(entry) => typeof entry?.message === "string" ? { ...entry, message: scrubPii2(entry.message) } : entry
|
|
236
385
|
);
|
|
237
386
|
}
|
|
238
387
|
return next;
|
|
239
388
|
}
|
|
389
|
+
async persistQueue(storage, queue) {
|
|
390
|
+
const plaintext = JSON.stringify(queue);
|
|
391
|
+
const blob = await encryptQueueBlob(plaintext, this.secureStorage);
|
|
392
|
+
await storage.setItem(QUEUE_KEY, blob);
|
|
393
|
+
}
|
|
240
394
|
async getQueue(storage) {
|
|
241
395
|
try {
|
|
242
|
-
|
|
243
|
-
|
|
396
|
+
let raw = await storage.getItem(QUEUE_KEY);
|
|
397
|
+
if (!raw) {
|
|
398
|
+
raw = await storage.getItem(LEGACY_QUEUE_KEY);
|
|
399
|
+
if (raw) {
|
|
400
|
+
await storage.removeItem(LEGACY_QUEUE_KEY);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (!raw) return [];
|
|
404
|
+
const plaintext = await decryptQueueBlob(raw, this.secureStorage);
|
|
405
|
+
return JSON.parse(plaintext);
|
|
244
406
|
} catch {
|
|
245
407
|
return [];
|
|
246
408
|
}
|
|
@@ -254,56 +416,33 @@ var AsyncStorageQueue = class {
|
|
|
254
416
|
}
|
|
255
417
|
}
|
|
256
418
|
};
|
|
257
|
-
|
|
258
|
-
[/\b\d{3}-\d{2}-\d{4}\b/g, "[REDACTED_SSN]"],
|
|
259
|
-
[/\b(?:\d[ -]*){12,18}\d\b/g, "[REDACTED_CC]"],
|
|
260
|
-
[/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g, "[REDACTED_AWS_KEY]"],
|
|
261
|
-
[
|
|
262
|
-
/(?:aws_secret_access_key|secret_access_key)["'\s:=]+[A-Za-z0-9/+=]{40}\b/gi,
|
|
263
|
-
"aws_secret_access_key=[REDACTED_AWS_SECRET]"
|
|
264
|
-
],
|
|
265
|
-
[/\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{24,}\b/g, "[REDACTED_STRIPE_KEY]"],
|
|
266
|
-
[/\bpk_(?:live|test)_[A-Za-z0-9]{24,}\b/g, "[REDACTED_STRIPE_PK]"],
|
|
267
|
-
[/\bxox[abpor]-[A-Za-z0-9-]{10,}\b/g, "[REDACTED_SLACK_TOKEN]"],
|
|
268
|
-
[/\bghp_[A-Za-z0-9]{36}\b/g, "[REDACTED_GITHUB_PAT]"],
|
|
269
|
-
[/\bgithub_pat_[A-Za-z0-9_]{80,}\b/g, "[REDACTED_GITHUB_PAT]"],
|
|
270
|
-
[/\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g, "[REDACTED_OPENAI_KEY]"],
|
|
271
|
-
[/\bsk-ant-[A-Za-z0-9_-]{20,}\b/g, "[REDACTED_ANTHROPIC_KEY]"],
|
|
272
|
-
[/\bAIza[0-9A-Za-z_-]{35}\b/g, "[REDACTED_GOOGLE_KEY]"],
|
|
273
|
-
[/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[REDACTED_JWT]"],
|
|
274
|
-
[/\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/g, "[REDACTED_EMAIL]"],
|
|
275
|
-
[
|
|
276
|
-
/(?:\+\d{1,3}[\s.\-])?\(?\d{2,4}\)?[\s.\-]\d{3,4}[\s.\-]\d{3,4}\b/g,
|
|
277
|
-
"[REDACTED_PHONE]"
|
|
278
|
-
]
|
|
279
|
-
];
|
|
280
|
-
function scrubPii(text) {
|
|
281
|
-
let result = text;
|
|
282
|
-
for (const [regex, replacement] of SCRUB_PATTERNS) {
|
|
283
|
-
result = result.replace(regex, replacement);
|
|
284
|
-
}
|
|
285
|
-
return result;
|
|
286
|
-
}
|
|
287
|
-
async function sendWithRetry(item, endpoint, apiKey, attempt = 0) {
|
|
419
|
+
async function sendWithRetry(item, ctx, attempt = 0) {
|
|
288
420
|
const delay = Math.min(1e3 * Math.pow(2, attempt) + Math.random() * 500, 1e4);
|
|
421
|
+
const internalKind = "report-submit";
|
|
422
|
+
const userToken = ctx.getUserToken?.() ?? null;
|
|
289
423
|
try {
|
|
290
|
-
const res = await fetch(`${
|
|
424
|
+
const res = await fetch(`${ctx.apiEndpoint}/v1/reports`, {
|
|
291
425
|
method: "POST",
|
|
292
|
-
headers: {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
426
|
+
headers: buildSdkIngestHeaders({
|
|
427
|
+
apiKey: ctx.apiKey,
|
|
428
|
+
projectId: ctx.projectId,
|
|
429
|
+
sdkPackage: ctx.sdkPackage,
|
|
430
|
+
sdkVersion: ctx.sdkVersion,
|
|
431
|
+
userToken,
|
|
432
|
+
internalKind
|
|
433
|
+
}),
|
|
434
|
+
body: JSON.stringify(item.report),
|
|
435
|
+
[MUSHI_INTERNAL_INIT_MARKER]: internalKind
|
|
297
436
|
});
|
|
298
437
|
if ((res.status === 429 || res.status >= 500) && attempt < 3) {
|
|
299
438
|
await sleep(delay);
|
|
300
|
-
return sendWithRetry(item,
|
|
439
|
+
return sendWithRetry(item, ctx, attempt + 1);
|
|
301
440
|
}
|
|
302
441
|
return res.ok;
|
|
303
442
|
} catch {
|
|
304
443
|
if (attempt < 3) {
|
|
305
444
|
await sleep(delay);
|
|
306
|
-
return sendWithRetry(item,
|
|
445
|
+
return sendWithRetry(item, ctx, attempt + 1);
|
|
307
446
|
}
|
|
308
447
|
return false;
|
|
309
448
|
}
|
|
@@ -1082,7 +1221,7 @@ var s2 = StyleSheet2.create({
|
|
|
1082
1221
|
|
|
1083
1222
|
// src/version.ts
|
|
1084
1223
|
var MUSHI_SDK_PACKAGE = "@mushi-mushi/react-native";
|
|
1085
|
-
var MUSHI_SDK_VERSION = "0.19.
|
|
1224
|
+
var MUSHI_SDK_VERSION = "0.19.2" ? "0.19.2" : "0.17.0";
|
|
1086
1225
|
|
|
1087
1226
|
// src/provider.tsx
|
|
1088
1227
|
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
@@ -1174,6 +1313,11 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1174
1313
|
maxSize: config.storage?.maxQueueSize,
|
|
1175
1314
|
apiEndpoint,
|
|
1176
1315
|
apiKey: config.apiKey,
|
|
1316
|
+
projectId: config.projectId,
|
|
1317
|
+
sdkPackage: MUSHI_SDK_PACKAGE,
|
|
1318
|
+
sdkVersion: MUSHI_SDK_VERSION,
|
|
1319
|
+
getUserToken: () => userTokenRef.current,
|
|
1320
|
+
secureStorage: config.storage?.secureStorage,
|
|
1177
1321
|
// `onReportSynced` fires here — when a previously-queued report actually
|
|
1178
1322
|
// drains to the server — not on the direct-submit path (where it would be
|
|
1179
1323
|
// indistinguishable from `onReportSubmitted` and thus meaningless).
|
|
@@ -1188,16 +1332,15 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1188
1332
|
data: { sdk: MUSHI_SDK_PACKAGE, version: MUSHI_SDK_VERSION }
|
|
1189
1333
|
});
|
|
1190
1334
|
(async () => {
|
|
1191
|
-
const TOKEN_KEY = "@mushi:reporter_token";
|
|
1192
1335
|
try {
|
|
1193
|
-
const
|
|
1194
|
-
const existing = await
|
|
1336
|
+
const secureStorage = config.storage?.secureStorage !== false;
|
|
1337
|
+
const existing = await loadReporterToken(secureStorage);
|
|
1195
1338
|
if (existing && existing.startsWith("mushi_")) {
|
|
1196
1339
|
reporterTokenRef.current = existing;
|
|
1197
1340
|
} else {
|
|
1198
1341
|
const fresh = `mushi_${newUuid()}`;
|
|
1199
1342
|
reporterTokenRef.current = fresh;
|
|
1200
|
-
await
|
|
1343
|
+
await saveReporterToken(fresh, secureStorage);
|
|
1201
1344
|
}
|
|
1202
1345
|
resolveReporterTokenReady();
|
|
1203
1346
|
} catch {
|
|
@@ -1312,7 +1455,10 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1312
1455
|
if (screenRef.current?.route) contextMeta.screenRoute = screenRef.current.route;
|
|
1313
1456
|
if (screenRef.current?.feature) contextMeta.screenFeature = screenRef.current.feature;
|
|
1314
1457
|
const merged = Object.keys(metadataRef.current).length > 0 ? { ...contextMeta, ...metadataRef.current } : Object.keys(contextMeta).length > 0 ? contextMeta : void 0;
|
|
1315
|
-
const breadcrumbs = breadcrumbsRef.current.getAll()
|
|
1458
|
+
const breadcrumbs = breadcrumbsRef.current.getAll().map((c) => ({
|
|
1459
|
+
...c,
|
|
1460
|
+
message: scrubPii3(c.message)
|
|
1461
|
+
}));
|
|
1316
1462
|
const timeline = breadcrumbs.map((c) => ({
|
|
1317
1463
|
ts: c.timestamp,
|
|
1318
1464
|
kind: breadcrumbToTimelineKind(c.category),
|
|
@@ -1323,7 +1469,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1323
1469
|
id: newUuid(),
|
|
1324
1470
|
projectId: config.projectId,
|
|
1325
1471
|
category: data.category,
|
|
1326
|
-
description: data.description,
|
|
1472
|
+
description: scrubPii3(data.description),
|
|
1327
1473
|
environment: {
|
|
1328
1474
|
userAgent: deviceInfo.systemName ?? "ReactNative",
|
|
1329
1475
|
platform: deviceInfo.platform ?? "mobile",
|
|
@@ -1334,7 +1480,10 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1334
1480
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1335
1481
|
timezone: deviceInfo.timezone ?? "UTC"
|
|
1336
1482
|
},
|
|
1337
|
-
consoleLogs: consoleRef.current?.getEntries() ?? []
|
|
1483
|
+
consoleLogs: (consoleRef.current?.getEntries() ?? []).map((entry) => ({
|
|
1484
|
+
...entry,
|
|
1485
|
+
message: scrubPii3(entry.message)
|
|
1486
|
+
})),
|
|
1338
1487
|
networkLogs: networkRef.current?.getEntries().map((entry) => ({
|
|
1339
1488
|
...entry,
|
|
1340
1489
|
status: entry.status ?? 0
|
|
@@ -1355,6 +1504,14 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1355
1504
|
if (!client) return;
|
|
1356
1505
|
const result = await client.submitReport(report);
|
|
1357
1506
|
if (!result.ok) {
|
|
1507
|
+
const isCredentialError = result.error?.code === "HTTP_401" || result.error?.code === "HTTP_403" || result.error?.code?.includes("UNAUTHORIZED") || result.error?.code?.includes("FORBIDDEN");
|
|
1508
|
+
if (isCredentialError) {
|
|
1509
|
+
const where = apiEndpoint.replace(/\/$/, "") === DEFAULT_API_ENDPOINT ? "Get the correct values at: https://kensaur.us/mushi-mushi/admin/projects" : "Get the correct values from your Mushi console's Projects page.";
|
|
1510
|
+
console.error(
|
|
1511
|
+
'[Mushi] Credentials rejected. Check your projectId and apiKey (must have "report:write" scope). ' + where
|
|
1512
|
+
);
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1358
1515
|
await queueRef.current?.enqueue(report);
|
|
1359
1516
|
config.callbacks?.onReportQueued?.(report.id);
|
|
1360
1517
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mushi-mushi/react-native",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React Native SDK for Mushi Mushi — shake-to-report, bottom sheet widget, full context capture",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"@react-native-community/netinfo": ">=11",
|
|
12
12
|
"@react-navigation/native": ">=6",
|
|
13
13
|
"expo-sensors": ">=13",
|
|
14
|
+
"expo-secure-store": ">=13",
|
|
14
15
|
"react": ">=18",
|
|
15
16
|
"react-native": ">=0.72",
|
|
16
17
|
"react-native-view-shot": ">=3.8"
|
|
@@ -25,6 +26,9 @@
|
|
|
25
26
|
"expo-sensors": {
|
|
26
27
|
"optional": true
|
|
27
28
|
},
|
|
29
|
+
"expo-secure-store": {
|
|
30
|
+
"optional": true
|
|
31
|
+
},
|
|
28
32
|
"@react-native-community/netinfo": {
|
|
29
33
|
"optional": true
|
|
30
34
|
},
|
|
@@ -33,7 +37,7 @@
|
|
|
33
37
|
}
|
|
34
38
|
},
|
|
35
39
|
"dependencies": {
|
|
36
|
-
"@mushi-mushi/core": "^1.19.
|
|
40
|
+
"@mushi-mushi/core": "^1.19.2"
|
|
37
41
|
},
|
|
38
42
|
"devDependencies": {
|
|
39
43
|
"@types/react": "^19.2.17",
|