@mushi-mushi/react-native 0.19.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -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,18 @@
|
|
|
1
1
|
# @mushi-mushi/react-native
|
|
2
2
|
|
|
3
|
+
## 0.19.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 08108e6: Setup UX overhaul: zero-paste `mushi login` browser device-auth, credential error visibility, and docs fixes.
|
|
8
|
+
|
|
9
|
+
- **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.
|
|
10
|
+
- **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.
|
|
11
|
+
- **React Native**: Same 401/403 credential-failure detection in `MushiProvider.submitReport` — skips enqueue and surfaces the error immediately.
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [08108e6]
|
|
14
|
+
- @mushi-mushi/core@1.19.1
|
|
15
|
+
|
|
3
16
|
## 0.19.0
|
|
4
17
|
|
|
5
18
|
### 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,154 @@ 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
|
+
if (!secureStorage) return stored;
|
|
269
|
+
const secure = await loadSecureStore();
|
|
270
|
+
if (!secure || !hasWebCrypto()) return stored;
|
|
271
|
+
const key = await getOrCreateQueueKey(secure);
|
|
272
|
+
if (!key) return stored;
|
|
273
|
+
const combined = base64ToBytes(stored.slice(ENCRYPTED_PREFIX.length));
|
|
274
|
+
if (combined.length < 13) return stored;
|
|
275
|
+
const iv = combined.slice(0, 12);
|
|
276
|
+
const cipher = combined.slice(12);
|
|
277
|
+
try {
|
|
278
|
+
const plain = await globalThis.crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, cipher);
|
|
279
|
+
return new TextDecoder().decode(plain);
|
|
280
|
+
} catch {
|
|
281
|
+
return stored;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async function loadReporterToken(secureStorage) {
|
|
285
|
+
if (secureStorage) {
|
|
286
|
+
const secure = await loadSecureStore();
|
|
287
|
+
if (secure) {
|
|
288
|
+
const fromSecure = await secure.getItemAsync(REPORTER_TOKEN_KEY);
|
|
289
|
+
if (fromSecure?.startsWith("mushi_")) return fromSecure;
|
|
290
|
+
const storage2 = await loadAsyncStorage();
|
|
291
|
+
if (storage2) {
|
|
292
|
+
const legacy = await storage2.getItem(REPORTER_TOKEN_KEY);
|
|
293
|
+
if (legacy?.startsWith("mushi_")) {
|
|
294
|
+
await secure.setItemAsync(REPORTER_TOKEN_KEY, legacy);
|
|
295
|
+
await storage2.removeItem(REPORTER_TOKEN_KEY);
|
|
296
|
+
return legacy;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return fromSecure;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const storage = await loadAsyncStorage();
|
|
303
|
+
if (!storage) return null;
|
|
304
|
+
return storage.getItem(REPORTER_TOKEN_KEY);
|
|
305
|
+
}
|
|
306
|
+
async function saveReporterToken(token, secureStorage) {
|
|
307
|
+
if (secureStorage) {
|
|
308
|
+
const secure = await loadSecureStore();
|
|
309
|
+
if (secure) {
|
|
310
|
+
await secure.setItemAsync(REPORTER_TOKEN_KEY, token);
|
|
311
|
+
const storage2 = await loadAsyncStorage();
|
|
312
|
+
if (storage2) await storage2.removeItem(REPORTER_TOKEN_KEY);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const storage = await loadAsyncStorage();
|
|
317
|
+
if (storage) await storage.setItem(REPORTER_TOKEN_KEY, token);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/storage/async-storage-queue.ts
|
|
321
|
+
var QUEUE_KEY = ENCRYPTED_QUEUE_KEY;
|
|
193
322
|
var AsyncStorageQueue = class {
|
|
194
323
|
maxSize;
|
|
195
324
|
apiEndpoint;
|
|
196
325
|
apiKey;
|
|
326
|
+
projectId;
|
|
327
|
+
sdkPackage;
|
|
328
|
+
sdkVersion;
|
|
329
|
+
getUserToken;
|
|
330
|
+
secureStorage;
|
|
197
331
|
onSynced;
|
|
198
332
|
constructor(config) {
|
|
199
333
|
this.maxSize = config.maxSize ?? 50;
|
|
200
334
|
this.apiEndpoint = config.apiEndpoint;
|
|
201
335
|
this.apiKey = config.apiKey;
|
|
336
|
+
this.projectId = config.projectId;
|
|
337
|
+
this.sdkPackage = config.sdkPackage;
|
|
338
|
+
this.sdkVersion = config.sdkVersion;
|
|
339
|
+
this.getUserToken = config.getUserToken;
|
|
340
|
+
this.secureStorage = config.secureStorage !== false;
|
|
202
341
|
this.onSynced = config.onSynced;
|
|
203
342
|
}
|
|
204
343
|
async enqueue(report) {
|
|
@@ -208,7 +347,7 @@ var AsyncStorageQueue = class {
|
|
|
208
347
|
if (queue.length >= this.maxSize) queue.shift();
|
|
209
348
|
const scrubbedReport = this.scrubReportPii(report);
|
|
210
349
|
queue.push({ report: scrubbedReport, enqueuedAt: Date.now() });
|
|
211
|
-
await
|
|
350
|
+
await this.persistQueue(AsyncStorage, queue);
|
|
212
351
|
}
|
|
213
352
|
async flush() {
|
|
214
353
|
const AsyncStorage = await this.getAsyncStorage();
|
|
@@ -218,7 +357,14 @@ var AsyncStorageQueue = class {
|
|
|
218
357
|
let flushed = 0;
|
|
219
358
|
const remaining = [];
|
|
220
359
|
for (const item of queue) {
|
|
221
|
-
const ok = await sendWithRetry(item,
|
|
360
|
+
const ok = await sendWithRetry(item, {
|
|
361
|
+
apiEndpoint: this.apiEndpoint,
|
|
362
|
+
apiKey: this.apiKey,
|
|
363
|
+
projectId: this.projectId,
|
|
364
|
+
sdkPackage: this.sdkPackage,
|
|
365
|
+
sdkVersion: this.sdkVersion,
|
|
366
|
+
getUserToken: this.getUserToken
|
|
367
|
+
});
|
|
222
368
|
if (ok) {
|
|
223
369
|
flushed++;
|
|
224
370
|
const reportId = item.report.id;
|
|
@@ -233,7 +379,7 @@ var AsyncStorageQueue = class {
|
|
|
233
379
|
break;
|
|
234
380
|
}
|
|
235
381
|
}
|
|
236
|
-
await
|
|
382
|
+
await this.persistQueue(AsyncStorage, remaining);
|
|
237
383
|
return flushed;
|
|
238
384
|
}
|
|
239
385
|
async size() {
|
|
@@ -242,31 +388,43 @@ var AsyncStorageQueue = class {
|
|
|
242
388
|
const queue = await this.getQueue(AsyncStorage);
|
|
243
389
|
return queue.length;
|
|
244
390
|
}
|
|
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
391
|
scrubReportPii(report) {
|
|
252
392
|
const next = { ...report };
|
|
253
393
|
if (typeof next.description === "string") {
|
|
254
|
-
next.description = scrubPii(next.description);
|
|
394
|
+
next.description = (0, import_core2.scrubPii)(next.description);
|
|
255
395
|
}
|
|
256
396
|
if (typeof next.summary === "string") {
|
|
257
|
-
next.summary = scrubPii(next.summary);
|
|
397
|
+
next.summary = (0, import_core2.scrubPii)(next.summary);
|
|
258
398
|
}
|
|
259
399
|
if (Array.isArray(next.breadcrumbs)) {
|
|
260
400
|
next.breadcrumbs = next.breadcrumbs.map(
|
|
261
|
-
(b) => typeof b?.message === "string" ? { ...b, message: scrubPii(b.message) } : b
|
|
401
|
+
(b) => typeof b?.message === "string" ? { ...b, message: (0, import_core2.scrubPii)(b.message) } : b
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
if (Array.isArray(next.consoleLogs)) {
|
|
405
|
+
next.consoleLogs = next.consoleLogs.map(
|
|
406
|
+
(entry) => typeof entry?.message === "string" ? { ...entry, message: (0, import_core2.scrubPii)(entry.message) } : entry
|
|
262
407
|
);
|
|
263
408
|
}
|
|
264
409
|
return next;
|
|
265
410
|
}
|
|
411
|
+
async persistQueue(storage, queue) {
|
|
412
|
+
const plaintext = JSON.stringify(queue);
|
|
413
|
+
const blob = await encryptQueueBlob(plaintext, this.secureStorage);
|
|
414
|
+
await storage.setItem(QUEUE_KEY, blob);
|
|
415
|
+
}
|
|
266
416
|
async getQueue(storage) {
|
|
267
417
|
try {
|
|
268
|
-
|
|
269
|
-
|
|
418
|
+
let raw = await storage.getItem(QUEUE_KEY);
|
|
419
|
+
if (!raw) {
|
|
420
|
+
raw = await storage.getItem(LEGACY_QUEUE_KEY);
|
|
421
|
+
if (raw) {
|
|
422
|
+
await storage.removeItem(LEGACY_QUEUE_KEY);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (!raw) return [];
|
|
426
|
+
const plaintext = await decryptQueueBlob(raw, this.secureStorage);
|
|
427
|
+
return JSON.parse(plaintext);
|
|
270
428
|
} catch {
|
|
271
429
|
return [];
|
|
272
430
|
}
|
|
@@ -280,56 +438,33 @@ var AsyncStorageQueue = class {
|
|
|
280
438
|
}
|
|
281
439
|
}
|
|
282
440
|
};
|
|
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) {
|
|
441
|
+
async function sendWithRetry(item, ctx, attempt = 0) {
|
|
314
442
|
const delay = Math.min(1e3 * Math.pow(2, attempt) + Math.random() * 500, 1e4);
|
|
443
|
+
const internalKind = "report-submit";
|
|
444
|
+
const userToken = ctx.getUserToken?.() ?? null;
|
|
315
445
|
try {
|
|
316
|
-
const res = await fetch(`${
|
|
446
|
+
const res = await fetch(`${ctx.apiEndpoint}/v1/reports`, {
|
|
317
447
|
method: "POST",
|
|
318
|
-
headers: {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
448
|
+
headers: (0, import_core2.buildSdkIngestHeaders)({
|
|
449
|
+
apiKey: ctx.apiKey,
|
|
450
|
+
projectId: ctx.projectId,
|
|
451
|
+
sdkPackage: ctx.sdkPackage,
|
|
452
|
+
sdkVersion: ctx.sdkVersion,
|
|
453
|
+
userToken,
|
|
454
|
+
internalKind
|
|
455
|
+
}),
|
|
456
|
+
body: JSON.stringify(item.report),
|
|
457
|
+
[import_core2.MUSHI_INTERNAL_INIT_MARKER]: internalKind
|
|
323
458
|
});
|
|
324
459
|
if ((res.status === 429 || res.status >= 500) && attempt < 3) {
|
|
325
460
|
await sleep(delay);
|
|
326
|
-
return sendWithRetry(item,
|
|
461
|
+
return sendWithRetry(item, ctx, attempt + 1);
|
|
327
462
|
}
|
|
328
463
|
return res.ok;
|
|
329
464
|
} catch {
|
|
330
465
|
if (attempt < 3) {
|
|
331
466
|
await sleep(delay);
|
|
332
|
-
return sendWithRetry(item,
|
|
467
|
+
return sendWithRetry(item, ctx, attempt + 1);
|
|
333
468
|
}
|
|
334
469
|
return false;
|
|
335
470
|
}
|
|
@@ -341,7 +476,7 @@ function sleep(ms) {
|
|
|
341
476
|
// src/components/MushiBottomSheet.tsx
|
|
342
477
|
var import_react = require("react");
|
|
343
478
|
var import_react_native2 = require("react-native");
|
|
344
|
-
var
|
|
479
|
+
var import_core3 = require("@mushi-mushi/core");
|
|
345
480
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
346
481
|
var { height: SCREEN_HEIGHT } = import_react_native2.Dimensions.get("window");
|
|
347
482
|
var SHEET_HEIGHT = SCREEN_HEIGHT * 0.55;
|
|
@@ -499,7 +634,7 @@ var MushiBottomSheet = ({
|
|
|
499
634
|
}
|
|
500
635
|
};
|
|
501
636
|
const activeScreenshot = screenshotDataUrl && screenshotAttached ? screenshotDataUrl : null;
|
|
502
|
-
const pal = (0,
|
|
637
|
+
const pal = (0, import_core3.mushiPalette)(dark ? "dark" : "light");
|
|
503
638
|
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
639
|
const canSubmit = !!category && description.trim().length > 0 && phase === "form";
|
|
505
640
|
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 +1029,7 @@ var s = import_react_native2.StyleSheet.create({
|
|
|
894
1029
|
// src/components/MushiFloatingButton.tsx
|
|
895
1030
|
var import_react2 = require("react");
|
|
896
1031
|
var import_react_native3 = require("react-native");
|
|
897
|
-
var
|
|
1032
|
+
var import_core4 = require("@mushi-mushi/core");
|
|
898
1033
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
899
1034
|
var FAB_SIZE = 56;
|
|
900
1035
|
var EDGE_PADDING = 12;
|
|
@@ -946,7 +1081,7 @@ var MushiFloatingButton = ({
|
|
|
946
1081
|
const currentX = (0, import_react2.useRef)(baseX);
|
|
947
1082
|
const currentY = (0, import_react2.useRef)(0);
|
|
948
1083
|
const isDragging = (0, import_react2.useRef)(false);
|
|
949
|
-
const accentColor = accent ?? (0,
|
|
1084
|
+
const accentColor = accent ?? (0, import_core4.mushiPalette)(dark ? "dark" : "light").accent;
|
|
950
1085
|
(0, import_react2.useEffect)(() => {
|
|
951
1086
|
if (!persist) return;
|
|
952
1087
|
void loadStoredPos().then((pos) => {
|
|
@@ -1056,7 +1191,7 @@ var MushiFloatingButton = ({
|
|
|
1056
1191
|
onStartShouldSetResponder: () => !draggable,
|
|
1057
1192
|
onResponderGrant: handlePressIn,
|
|
1058
1193
|
onResponderRelease: handlePressOut,
|
|
1059
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Animated.Text, { style: s2.emoji, children:
|
|
1194
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Animated.Text, { style: s2.emoji, children: import_core4.MUSHI_COPY.triggerText })
|
|
1060
1195
|
}
|
|
1061
1196
|
);
|
|
1062
1197
|
};
|
|
@@ -1081,7 +1216,7 @@ var s2 = import_react_native3.StyleSheet.create({
|
|
|
1081
1216
|
|
|
1082
1217
|
// src/version.ts
|
|
1083
1218
|
var MUSHI_SDK_PACKAGE = "@mushi-mushi/react-native";
|
|
1084
|
-
var MUSHI_SDK_VERSION = "0.19.
|
|
1219
|
+
var MUSHI_SDK_VERSION = "0.19.1" ? "0.19.1" : "0.17.0";
|
|
1085
1220
|
|
|
1086
1221
|
// src/provider.tsx
|
|
1087
1222
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
@@ -1128,9 +1263,9 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1128
1263
|
const networkRef = (0, import_react3.useRef)(null);
|
|
1129
1264
|
const queueRef = (0, import_react3.useRef)(null);
|
|
1130
1265
|
const apiClientRef = (0, import_react3.useRef)(null);
|
|
1131
|
-
const sessionIdRef = (0, import_react3.useRef)(`sess_${(0,
|
|
1266
|
+
const sessionIdRef = (0, import_react3.useRef)(`sess_${(0, import_core5.newUuid)()}`);
|
|
1132
1267
|
const breadcrumbsRef = (0, import_react3.useRef)(
|
|
1133
|
-
(0,
|
|
1268
|
+
(0, import_core5.createBreadcrumbBuffer)({ max: config.capture?.maxBreadcrumbs ?? 50 })
|
|
1134
1269
|
);
|
|
1135
1270
|
const [sheetVisible, setSheetVisible] = (0, import_react3.useState)(false);
|
|
1136
1271
|
const [sheetScreenshot, setSheetScreenshot] = (0, import_react3.useState)(null);
|
|
@@ -1153,7 +1288,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1153
1288
|
if (config.endpoint && !config.apiEndpoint && __DEV__) {
|
|
1154
1289
|
console.warn("[MushiProvider] `endpoint` is deprecated \u2014 use `apiEndpoint` instead.");
|
|
1155
1290
|
}
|
|
1156
|
-
const apiEndpoint = rawEndpoint ??
|
|
1291
|
+
const apiEndpoint = rawEndpoint ?? import_core5.DEFAULT_API_ENDPOINT;
|
|
1157
1292
|
(0, import_react3.useEffect)(() => {
|
|
1158
1293
|
if (config.capture?.console !== false) {
|
|
1159
1294
|
consoleRef.current = setupConsoleCapture(config.capture?.maxConsoleEntries);
|
|
@@ -1161,7 +1296,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1161
1296
|
if (config.capture?.network !== false) {
|
|
1162
1297
|
networkRef.current = setupNetworkCapture(config.capture?.maxNetworkEntries, apiEndpoint);
|
|
1163
1298
|
}
|
|
1164
|
-
apiClientRef.current = (0,
|
|
1299
|
+
apiClientRef.current = (0, import_core5.createApiClient)({
|
|
1165
1300
|
projectId: config.projectId,
|
|
1166
1301
|
apiKey: config.apiKey,
|
|
1167
1302
|
apiEndpoint,
|
|
@@ -1173,6 +1308,11 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1173
1308
|
maxSize: config.storage?.maxQueueSize,
|
|
1174
1309
|
apiEndpoint,
|
|
1175
1310
|
apiKey: config.apiKey,
|
|
1311
|
+
projectId: config.projectId,
|
|
1312
|
+
sdkPackage: MUSHI_SDK_PACKAGE,
|
|
1313
|
+
sdkVersion: MUSHI_SDK_VERSION,
|
|
1314
|
+
getUserToken: () => userTokenRef.current,
|
|
1315
|
+
secureStorage: config.storage?.secureStorage,
|
|
1176
1316
|
// `onReportSynced` fires here — when a previously-queued report actually
|
|
1177
1317
|
// drains to the server — not on the direct-submit path (where it would be
|
|
1178
1318
|
// indistinguishable from `onReportSubmitted` and thus meaningless).
|
|
@@ -1187,21 +1327,20 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1187
1327
|
data: { sdk: MUSHI_SDK_PACKAGE, version: MUSHI_SDK_VERSION }
|
|
1188
1328
|
});
|
|
1189
1329
|
(async () => {
|
|
1190
|
-
const TOKEN_KEY = "@mushi:reporter_token";
|
|
1191
1330
|
try {
|
|
1192
|
-
const
|
|
1193
|
-
const existing = await
|
|
1331
|
+
const secureStorage = config.storage?.secureStorage !== false;
|
|
1332
|
+
const existing = await loadReporterToken(secureStorage);
|
|
1194
1333
|
if (existing && existing.startsWith("mushi_")) {
|
|
1195
1334
|
reporterTokenRef.current = existing;
|
|
1196
1335
|
} else {
|
|
1197
|
-
const fresh = `mushi_${(0,
|
|
1336
|
+
const fresh = `mushi_${(0, import_core5.newUuid)()}`;
|
|
1198
1337
|
reporterTokenRef.current = fresh;
|
|
1199
|
-
await
|
|
1338
|
+
await saveReporterToken(fresh, secureStorage);
|
|
1200
1339
|
}
|
|
1201
1340
|
resolveReporterTokenReady();
|
|
1202
1341
|
} catch {
|
|
1203
1342
|
if (reporterTokenRef.current === `rn-${config.projectId}-anon`) {
|
|
1204
|
-
reporterTokenRef.current = `mushi_${(0,
|
|
1343
|
+
reporterTokenRef.current = `mushi_${(0, import_core5.newUuid)()}`;
|
|
1205
1344
|
}
|
|
1206
1345
|
resolveReporterTokenReady();
|
|
1207
1346
|
}
|
|
@@ -1311,7 +1450,10 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1311
1450
|
if (screenRef.current?.route) contextMeta.screenRoute = screenRef.current.route;
|
|
1312
1451
|
if (screenRef.current?.feature) contextMeta.screenFeature = screenRef.current.feature;
|
|
1313
1452
|
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()
|
|
1453
|
+
const breadcrumbs = breadcrumbsRef.current.getAll().map((c) => ({
|
|
1454
|
+
...c,
|
|
1455
|
+
message: (0, import_core5.scrubPii)(c.message)
|
|
1456
|
+
}));
|
|
1315
1457
|
const timeline = breadcrumbs.map((c) => ({
|
|
1316
1458
|
ts: c.timestamp,
|
|
1317
1459
|
kind: breadcrumbToTimelineKind(c.category),
|
|
@@ -1319,10 +1461,10 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1319
1461
|
}));
|
|
1320
1462
|
const fingerprintHash = getDeviceFingerprintHash(deviceInfo);
|
|
1321
1463
|
const report = {
|
|
1322
|
-
id: (0,
|
|
1464
|
+
id: (0, import_core5.newUuid)(),
|
|
1323
1465
|
projectId: config.projectId,
|
|
1324
1466
|
category: data.category,
|
|
1325
|
-
description: data.description,
|
|
1467
|
+
description: (0, import_core5.scrubPii)(data.description),
|
|
1326
1468
|
environment: {
|
|
1327
1469
|
userAgent: deviceInfo.systemName ?? "ReactNative",
|
|
1328
1470
|
platform: deviceInfo.platform ?? "mobile",
|
|
@@ -1333,7 +1475,10 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1333
1475
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1334
1476
|
timezone: deviceInfo.timezone ?? "UTC"
|
|
1335
1477
|
},
|
|
1336
|
-
consoleLogs: consoleRef.current?.getEntries() ?? []
|
|
1478
|
+
consoleLogs: (consoleRef.current?.getEntries() ?? []).map((entry) => ({
|
|
1479
|
+
...entry,
|
|
1480
|
+
message: (0, import_core5.scrubPii)(entry.message)
|
|
1481
|
+
})),
|
|
1337
1482
|
networkLogs: networkRef.current?.getEntries().map((entry) => ({
|
|
1338
1483
|
...entry,
|
|
1339
1484
|
status: entry.status ?? 0
|
|
@@ -1354,6 +1499,13 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1354
1499
|
if (!client) return;
|
|
1355
1500
|
const result = await client.submitReport(report);
|
|
1356
1501
|
if (!result.ok) {
|
|
1502
|
+
const isCredentialError = result.error?.code === "HTTP_401" || result.error?.code === "HTTP_403" || result.error?.code?.includes("UNAUTHORIZED") || result.error?.code?.includes("FORBIDDEN");
|
|
1503
|
+
if (isCredentialError) {
|
|
1504
|
+
console.error(
|
|
1505
|
+
'[Mushi] Credentials rejected. Check your projectId and apiKey (must have "report:write" scope). Get the correct values at: https://kensaur.us/mushi-mushi/admin/projects'
|
|
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,158 @@ 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
|
+
if (!secureStorage) return stored;
|
|
248
|
+
const secure = await loadSecureStore();
|
|
249
|
+
if (!secure || !hasWebCrypto()) return stored;
|
|
250
|
+
const key = await getOrCreateQueueKey(secure);
|
|
251
|
+
if (!key) return stored;
|
|
252
|
+
const combined = base64ToBytes(stored.slice(ENCRYPTED_PREFIX.length));
|
|
253
|
+
if (combined.length < 13) return stored;
|
|
254
|
+
const iv = combined.slice(0, 12);
|
|
255
|
+
const cipher = combined.slice(12);
|
|
256
|
+
try {
|
|
257
|
+
const plain = await globalThis.crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, cipher);
|
|
258
|
+
return new TextDecoder().decode(plain);
|
|
259
|
+
} catch {
|
|
260
|
+
return stored;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async function loadReporterToken(secureStorage) {
|
|
264
|
+
if (secureStorage) {
|
|
265
|
+
const secure = await loadSecureStore();
|
|
266
|
+
if (secure) {
|
|
267
|
+
const fromSecure = await secure.getItemAsync(REPORTER_TOKEN_KEY);
|
|
268
|
+
if (fromSecure?.startsWith("mushi_")) return fromSecure;
|
|
269
|
+
const storage2 = await loadAsyncStorage();
|
|
270
|
+
if (storage2) {
|
|
271
|
+
const legacy = await storage2.getItem(REPORTER_TOKEN_KEY);
|
|
272
|
+
if (legacy?.startsWith("mushi_")) {
|
|
273
|
+
await secure.setItemAsync(REPORTER_TOKEN_KEY, legacy);
|
|
274
|
+
await storage2.removeItem(REPORTER_TOKEN_KEY);
|
|
275
|
+
return legacy;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return fromSecure;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const storage = await loadAsyncStorage();
|
|
282
|
+
if (!storage) return null;
|
|
283
|
+
return storage.getItem(REPORTER_TOKEN_KEY);
|
|
284
|
+
}
|
|
285
|
+
async function saveReporterToken(token, secureStorage) {
|
|
286
|
+
if (secureStorage) {
|
|
287
|
+
const secure = await loadSecureStore();
|
|
288
|
+
if (secure) {
|
|
289
|
+
await secure.setItemAsync(REPORTER_TOKEN_KEY, token);
|
|
290
|
+
const storage2 = await loadAsyncStorage();
|
|
291
|
+
if (storage2) await storage2.removeItem(REPORTER_TOKEN_KEY);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const storage = await loadAsyncStorage();
|
|
296
|
+
if (storage) await storage.setItem(REPORTER_TOKEN_KEY, token);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/storage/async-storage-queue.ts
|
|
300
|
+
var QUEUE_KEY = ENCRYPTED_QUEUE_KEY;
|
|
167
301
|
var AsyncStorageQueue = class {
|
|
168
302
|
maxSize;
|
|
169
303
|
apiEndpoint;
|
|
170
304
|
apiKey;
|
|
305
|
+
projectId;
|
|
306
|
+
sdkPackage;
|
|
307
|
+
sdkVersion;
|
|
308
|
+
getUserToken;
|
|
309
|
+
secureStorage;
|
|
171
310
|
onSynced;
|
|
172
311
|
constructor(config) {
|
|
173
312
|
this.maxSize = config.maxSize ?? 50;
|
|
174
313
|
this.apiEndpoint = config.apiEndpoint;
|
|
175
314
|
this.apiKey = config.apiKey;
|
|
315
|
+
this.projectId = config.projectId;
|
|
316
|
+
this.sdkPackage = config.sdkPackage;
|
|
317
|
+
this.sdkVersion = config.sdkVersion;
|
|
318
|
+
this.getUserToken = config.getUserToken;
|
|
319
|
+
this.secureStorage = config.secureStorage !== false;
|
|
176
320
|
this.onSynced = config.onSynced;
|
|
177
321
|
}
|
|
178
322
|
async enqueue(report) {
|
|
@@ -182,7 +326,7 @@ var AsyncStorageQueue = class {
|
|
|
182
326
|
if (queue.length >= this.maxSize) queue.shift();
|
|
183
327
|
const scrubbedReport = this.scrubReportPii(report);
|
|
184
328
|
queue.push({ report: scrubbedReport, enqueuedAt: Date.now() });
|
|
185
|
-
await
|
|
329
|
+
await this.persistQueue(AsyncStorage, queue);
|
|
186
330
|
}
|
|
187
331
|
async flush() {
|
|
188
332
|
const AsyncStorage = await this.getAsyncStorage();
|
|
@@ -192,7 +336,14 @@ var AsyncStorageQueue = class {
|
|
|
192
336
|
let flushed = 0;
|
|
193
337
|
const remaining = [];
|
|
194
338
|
for (const item of queue) {
|
|
195
|
-
const ok = await sendWithRetry(item,
|
|
339
|
+
const ok = await sendWithRetry(item, {
|
|
340
|
+
apiEndpoint: this.apiEndpoint,
|
|
341
|
+
apiKey: this.apiKey,
|
|
342
|
+
projectId: this.projectId,
|
|
343
|
+
sdkPackage: this.sdkPackage,
|
|
344
|
+
sdkVersion: this.sdkVersion,
|
|
345
|
+
getUserToken: this.getUserToken
|
|
346
|
+
});
|
|
196
347
|
if (ok) {
|
|
197
348
|
flushed++;
|
|
198
349
|
const reportId = item.report.id;
|
|
@@ -207,7 +358,7 @@ var AsyncStorageQueue = class {
|
|
|
207
358
|
break;
|
|
208
359
|
}
|
|
209
360
|
}
|
|
210
|
-
await
|
|
361
|
+
await this.persistQueue(AsyncStorage, remaining);
|
|
211
362
|
return flushed;
|
|
212
363
|
}
|
|
213
364
|
async size() {
|
|
@@ -216,31 +367,43 @@ var AsyncStorageQueue = class {
|
|
|
216
367
|
const queue = await this.getQueue(AsyncStorage);
|
|
217
368
|
return queue.length;
|
|
218
369
|
}
|
|
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
370
|
scrubReportPii(report) {
|
|
226
371
|
const next = { ...report };
|
|
227
372
|
if (typeof next.description === "string") {
|
|
228
|
-
next.description =
|
|
373
|
+
next.description = scrubPii2(next.description);
|
|
229
374
|
}
|
|
230
375
|
if (typeof next.summary === "string") {
|
|
231
|
-
next.summary =
|
|
376
|
+
next.summary = scrubPii2(next.summary);
|
|
232
377
|
}
|
|
233
378
|
if (Array.isArray(next.breadcrumbs)) {
|
|
234
379
|
next.breadcrumbs = next.breadcrumbs.map(
|
|
235
|
-
(b) => typeof b?.message === "string" ? { ...b, message:
|
|
380
|
+
(b) => typeof b?.message === "string" ? { ...b, message: scrubPii2(b.message) } : b
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
if (Array.isArray(next.consoleLogs)) {
|
|
384
|
+
next.consoleLogs = next.consoleLogs.map(
|
|
385
|
+
(entry) => typeof entry?.message === "string" ? { ...entry, message: scrubPii2(entry.message) } : entry
|
|
236
386
|
);
|
|
237
387
|
}
|
|
238
388
|
return next;
|
|
239
389
|
}
|
|
390
|
+
async persistQueue(storage, queue) {
|
|
391
|
+
const plaintext = JSON.stringify(queue);
|
|
392
|
+
const blob = await encryptQueueBlob(plaintext, this.secureStorage);
|
|
393
|
+
await storage.setItem(QUEUE_KEY, blob);
|
|
394
|
+
}
|
|
240
395
|
async getQueue(storage) {
|
|
241
396
|
try {
|
|
242
|
-
|
|
243
|
-
|
|
397
|
+
let raw = await storage.getItem(QUEUE_KEY);
|
|
398
|
+
if (!raw) {
|
|
399
|
+
raw = await storage.getItem(LEGACY_QUEUE_KEY);
|
|
400
|
+
if (raw) {
|
|
401
|
+
await storage.removeItem(LEGACY_QUEUE_KEY);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (!raw) return [];
|
|
405
|
+
const plaintext = await decryptQueueBlob(raw, this.secureStorage);
|
|
406
|
+
return JSON.parse(plaintext);
|
|
244
407
|
} catch {
|
|
245
408
|
return [];
|
|
246
409
|
}
|
|
@@ -254,56 +417,33 @@ var AsyncStorageQueue = class {
|
|
|
254
417
|
}
|
|
255
418
|
}
|
|
256
419
|
};
|
|
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) {
|
|
420
|
+
async function sendWithRetry(item, ctx, attempt = 0) {
|
|
288
421
|
const delay = Math.min(1e3 * Math.pow(2, attempt) + Math.random() * 500, 1e4);
|
|
422
|
+
const internalKind = "report-submit";
|
|
423
|
+
const userToken = ctx.getUserToken?.() ?? null;
|
|
289
424
|
try {
|
|
290
|
-
const res = await fetch(`${
|
|
425
|
+
const res = await fetch(`${ctx.apiEndpoint}/v1/reports`, {
|
|
291
426
|
method: "POST",
|
|
292
|
-
headers: {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
427
|
+
headers: buildSdkIngestHeaders({
|
|
428
|
+
apiKey: ctx.apiKey,
|
|
429
|
+
projectId: ctx.projectId,
|
|
430
|
+
sdkPackage: ctx.sdkPackage,
|
|
431
|
+
sdkVersion: ctx.sdkVersion,
|
|
432
|
+
userToken,
|
|
433
|
+
internalKind
|
|
434
|
+
}),
|
|
435
|
+
body: JSON.stringify(item.report),
|
|
436
|
+
[MUSHI_INTERNAL_INIT_MARKER]: internalKind
|
|
297
437
|
});
|
|
298
438
|
if ((res.status === 429 || res.status >= 500) && attempt < 3) {
|
|
299
439
|
await sleep(delay);
|
|
300
|
-
return sendWithRetry(item,
|
|
440
|
+
return sendWithRetry(item, ctx, attempt + 1);
|
|
301
441
|
}
|
|
302
442
|
return res.ok;
|
|
303
443
|
} catch {
|
|
304
444
|
if (attempt < 3) {
|
|
305
445
|
await sleep(delay);
|
|
306
|
-
return sendWithRetry(item,
|
|
446
|
+
return sendWithRetry(item, ctx, attempt + 1);
|
|
307
447
|
}
|
|
308
448
|
return false;
|
|
309
449
|
}
|
|
@@ -1082,7 +1222,7 @@ var s2 = StyleSheet2.create({
|
|
|
1082
1222
|
|
|
1083
1223
|
// src/version.ts
|
|
1084
1224
|
var MUSHI_SDK_PACKAGE = "@mushi-mushi/react-native";
|
|
1085
|
-
var MUSHI_SDK_VERSION = "0.19.
|
|
1225
|
+
var MUSHI_SDK_VERSION = "0.19.1" ? "0.19.1" : "0.17.0";
|
|
1086
1226
|
|
|
1087
1227
|
// src/provider.tsx
|
|
1088
1228
|
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
@@ -1174,6 +1314,11 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1174
1314
|
maxSize: config.storage?.maxQueueSize,
|
|
1175
1315
|
apiEndpoint,
|
|
1176
1316
|
apiKey: config.apiKey,
|
|
1317
|
+
projectId: config.projectId,
|
|
1318
|
+
sdkPackage: MUSHI_SDK_PACKAGE,
|
|
1319
|
+
sdkVersion: MUSHI_SDK_VERSION,
|
|
1320
|
+
getUserToken: () => userTokenRef.current,
|
|
1321
|
+
secureStorage: config.storage?.secureStorage,
|
|
1177
1322
|
// `onReportSynced` fires here — when a previously-queued report actually
|
|
1178
1323
|
// drains to the server — not on the direct-submit path (where it would be
|
|
1179
1324
|
// indistinguishable from `onReportSubmitted` and thus meaningless).
|
|
@@ -1188,16 +1333,15 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1188
1333
|
data: { sdk: MUSHI_SDK_PACKAGE, version: MUSHI_SDK_VERSION }
|
|
1189
1334
|
});
|
|
1190
1335
|
(async () => {
|
|
1191
|
-
const TOKEN_KEY = "@mushi:reporter_token";
|
|
1192
1336
|
try {
|
|
1193
|
-
const
|
|
1194
|
-
const existing = await
|
|
1337
|
+
const secureStorage = config.storage?.secureStorage !== false;
|
|
1338
|
+
const existing = await loadReporterToken(secureStorage);
|
|
1195
1339
|
if (existing && existing.startsWith("mushi_")) {
|
|
1196
1340
|
reporterTokenRef.current = existing;
|
|
1197
1341
|
} else {
|
|
1198
1342
|
const fresh = `mushi_${newUuid()}`;
|
|
1199
1343
|
reporterTokenRef.current = fresh;
|
|
1200
|
-
await
|
|
1344
|
+
await saveReporterToken(fresh, secureStorage);
|
|
1201
1345
|
}
|
|
1202
1346
|
resolveReporterTokenReady();
|
|
1203
1347
|
} catch {
|
|
@@ -1312,7 +1456,10 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1312
1456
|
if (screenRef.current?.route) contextMeta.screenRoute = screenRef.current.route;
|
|
1313
1457
|
if (screenRef.current?.feature) contextMeta.screenFeature = screenRef.current.feature;
|
|
1314
1458
|
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()
|
|
1459
|
+
const breadcrumbs = breadcrumbsRef.current.getAll().map((c) => ({
|
|
1460
|
+
...c,
|
|
1461
|
+
message: scrubPii3(c.message)
|
|
1462
|
+
}));
|
|
1316
1463
|
const timeline = breadcrumbs.map((c) => ({
|
|
1317
1464
|
ts: c.timestamp,
|
|
1318
1465
|
kind: breadcrumbToTimelineKind(c.category),
|
|
@@ -1323,7 +1470,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1323
1470
|
id: newUuid(),
|
|
1324
1471
|
projectId: config.projectId,
|
|
1325
1472
|
category: data.category,
|
|
1326
|
-
description: data.description,
|
|
1473
|
+
description: scrubPii3(data.description),
|
|
1327
1474
|
environment: {
|
|
1328
1475
|
userAgent: deviceInfo.systemName ?? "ReactNative",
|
|
1329
1476
|
platform: deviceInfo.platform ?? "mobile",
|
|
@@ -1334,7 +1481,10 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1334
1481
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1335
1482
|
timezone: deviceInfo.timezone ?? "UTC"
|
|
1336
1483
|
},
|
|
1337
|
-
consoleLogs: consoleRef.current?.getEntries() ?? []
|
|
1484
|
+
consoleLogs: (consoleRef.current?.getEntries() ?? []).map((entry) => ({
|
|
1485
|
+
...entry,
|
|
1486
|
+
message: scrubPii3(entry.message)
|
|
1487
|
+
})),
|
|
1338
1488
|
networkLogs: networkRef.current?.getEntries().map((entry) => ({
|
|
1339
1489
|
...entry,
|
|
1340
1490
|
status: entry.status ?? 0
|
|
@@ -1355,6 +1505,13 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1355
1505
|
if (!client) return;
|
|
1356
1506
|
const result = await client.submitReport(report);
|
|
1357
1507
|
if (!result.ok) {
|
|
1508
|
+
const isCredentialError = result.error?.code === "HTTP_401" || result.error?.code === "HTTP_403" || result.error?.code?.includes("UNAUTHORIZED") || result.error?.code?.includes("FORBIDDEN");
|
|
1509
|
+
if (isCredentialError) {
|
|
1510
|
+
console.error(
|
|
1511
|
+
'[Mushi] Credentials rejected. Check your projectId and apiKey (must have "report:write" scope). Get the correct values at: https://kensaur.us/mushi-mushi/admin/projects'
|
|
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.1",
|
|
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.1"
|
|
37
41
|
},
|
|
38
42
|
"devDependencies": {
|
|
39
43
|
"@types/react": "^19.2.17",
|