@mushi-mushi/core 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +171 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +69 -4
- package/dist/index.d.ts +69 -4
- package/dist/index.js +171 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -95,6 +95,21 @@ interface MushiOfflineConfig {
|
|
|
95
95
|
enabled?: boolean;
|
|
96
96
|
maxQueueSize?: number;
|
|
97
97
|
syncOnReconnect?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Encrypt queued reports at rest (IndexedDB + localStorage) with AES-GCM.
|
|
100
|
+
*
|
|
101
|
+
* Wave S1 / D-16: on a shared device (kiosks, iPads, support-agent
|
|
102
|
+
* laptops) any queued report sits in plaintext until the next online flush.
|
|
103
|
+
* With this flag set we generate a non-extractable AES-GCM key at first use,
|
|
104
|
+
* stash it in a single tightly-scoped IndexedDB record, and wrap every
|
|
105
|
+
* queued report payload under it. The key never leaves the origin's
|
|
106
|
+
* IndexedDB; stealing the DB file on disk still requires the OS-level
|
|
107
|
+
* Web Crypto keystore to decrypt.
|
|
108
|
+
*
|
|
109
|
+
* Defaults to `true`. Set false only if you need to inspect raw queue
|
|
110
|
+
* contents during local dev.
|
|
111
|
+
*/
|
|
112
|
+
encryptAtRest?: boolean;
|
|
98
113
|
}
|
|
99
114
|
interface MushiRewardsConfig {
|
|
100
115
|
enabled?: boolean;
|
|
@@ -120,7 +135,7 @@ interface MushiReport {
|
|
|
120
135
|
sessionId?: string;
|
|
121
136
|
reporterToken: string;
|
|
122
137
|
/**
|
|
123
|
-
*
|
|
138
|
+
* §3c — stable per-device hash from `getDeviceFingerprintHash()`.
|
|
124
139
|
* Sent so the server can run the cross-account anti-gaming check; falls
|
|
125
140
|
* back to IP+UA fingerprinting when omitted.
|
|
126
141
|
*/
|
|
@@ -215,6 +230,43 @@ interface MushiSDKInstance {
|
|
|
215
230
|
open(): void;
|
|
216
231
|
close(): void;
|
|
217
232
|
destroy(): void;
|
|
233
|
+
/**
|
|
234
|
+
* Wave G4 — unified `captureEvent` API. Submits a bug report
|
|
235
|
+
* programmatically without opening the widget. Useful for adapters
|
|
236
|
+
* that translate errors from Datadog / Honeycomb / Sentry /
|
|
237
|
+
* Grafana into Mushi reports.
|
|
238
|
+
*
|
|
239
|
+
* Returns the server-assigned report id when the submit succeeds.
|
|
240
|
+
*/
|
|
241
|
+
captureEvent(event: MushiCaptureEventInput): Promise<string | null>;
|
|
242
|
+
/**
|
|
243
|
+
* Wave G4 — sugar alias for `setUser()`. Name mirrors the
|
|
244
|
+
* identify/track/capture vocabulary that PostHog/Segment/Mixpanel
|
|
245
|
+
* users already know.
|
|
246
|
+
*/
|
|
247
|
+
identify(userId: string, traits?: {
|
|
248
|
+
email?: string;
|
|
249
|
+
name?: string;
|
|
250
|
+
[k: string]: unknown;
|
|
251
|
+
}): void;
|
|
252
|
+
}
|
|
253
|
+
interface MushiCaptureEventInput {
|
|
254
|
+
/** Human-readable summary; becomes `reports.description`. */
|
|
255
|
+
description: string;
|
|
256
|
+
category?: MushiReportCategory;
|
|
257
|
+
severity?: 'critical' | 'high' | 'medium' | 'low';
|
|
258
|
+
component?: string;
|
|
259
|
+
/** Arbitrary tags merged into `reports.metadata.tags`. */
|
|
260
|
+
tags?: Record<string, string | number | boolean>;
|
|
261
|
+
/** Source-of-truth adapter that produced this event (e.g. `'datadog'`). */
|
|
262
|
+
source?: string;
|
|
263
|
+
/** Optional error payload (name/message/stack) captured from the host app. */
|
|
264
|
+
error?: {
|
|
265
|
+
name?: string;
|
|
266
|
+
message?: string;
|
|
267
|
+
stack?: string;
|
|
268
|
+
};
|
|
269
|
+
metadata?: Record<string, unknown>;
|
|
218
270
|
}
|
|
219
271
|
interface MushiApiClient {
|
|
220
272
|
submitReport(report: MushiReport): Promise<MushiApiResponse<{
|
|
@@ -248,7 +300,7 @@ declare const DEFAULT_API_ENDPOINT = "https://dxptnwrhwsqckaftyymj.supabase.co/f
|
|
|
248
300
|
declare function createApiClient(options: ApiClientOptions): MushiApiClient;
|
|
249
301
|
|
|
250
302
|
/**
|
|
251
|
-
*
|
|
303
|
+
* C7: Data residency region resolution.
|
|
252
304
|
*
|
|
253
305
|
* The SDK supports four regional clouds:
|
|
254
306
|
* - 'us' → United States (default; legacy `dxptnwrhwsqckaftyymj`)
|
|
@@ -306,7 +358,7 @@ declare function captureEnvironment(): MushiEnvironment;
|
|
|
306
358
|
declare function getReporterToken(): string;
|
|
307
359
|
|
|
308
360
|
/**
|
|
309
|
-
*
|
|
361
|
+
* §3c — stable device fingerprint hash.
|
|
310
362
|
*
|
|
311
363
|
* Hashes a deliberately small set of long-lived device characteristics so
|
|
312
364
|
* the same browser keeps the same hash across sessions, but moving to a
|
|
@@ -350,6 +402,19 @@ interface PiiScrubberConfig {
|
|
|
350
402
|
creditCards?: boolean;
|
|
351
403
|
ssns?: boolean;
|
|
352
404
|
ipAddresses?: boolean;
|
|
405
|
+
/**
|
|
406
|
+
* Scrub vendor-shaped secret tokens (AWS access keys, Stripe keys,
|
|
407
|
+
* Slack/GitHub PATs, OpenAI/Anthropic/Google keys, JWTs).
|
|
408
|
+
*
|
|
409
|
+
* Wave S1 / D-15: SDK parity with the server-side scrubber. The server
|
|
410
|
+
* scrubs these on every LLM invocation; the SDK now scrubs them at
|
|
411
|
+
* capture so they never hit the wire in the first place — important for
|
|
412
|
+
* users who `console.log(stripeKey)` during dev and later ship bug
|
|
413
|
+
* reports with the error text attached.
|
|
414
|
+
*/
|
|
415
|
+
secretTokens?: boolean;
|
|
416
|
+
/** IPv6 addresses. Defaults off for the same reason IPv4 does. */
|
|
417
|
+
ipv6?: boolean;
|
|
353
418
|
}
|
|
354
419
|
declare function createPiiScrubber(config?: PiiScrubberConfig): {
|
|
355
420
|
scrub: (text: string) => string;
|
|
@@ -423,4 +488,4 @@ declare function createLogger(options: LoggerOptions): Logger;
|
|
|
423
488
|
*/
|
|
424
489
|
declare const noopLogger: Logger;
|
|
425
490
|
|
|
426
|
-
export { type ApiClientOptions, DEFAULT_API_ENDPOINT, type LogEntry, type LogFormat, type LogLevel, type Logger, type LoggerOptions, type MushiApiClient, type MushiApiResponse, type MushiCaptureConfig, type MushiConfig, type MushiConsoleEntry, type MushiCooldownConfig, type MushiEnvironment, type MushiEventHandler, type MushiEventType, type MushiIntegrationsConfig, type MushiNetworkEntry, type MushiOfflineConfig, type MushiOnDeviceClassifier, type MushiOnDeviceClassifierInput, type MushiOnDeviceClassifierResult, type MushiPerformanceMetrics, type MushiPreFilterConfig, type MushiProactiveConfig, type MushiRegion, type MushiReport, type MushiReportBuilder, type MushiReportCategory, type MushiReportStatus, type MushiRewardsConfig, type MushiSDKInstance, type MushiSelectedElement, type MushiSentryConfig, type MushiWidgetConfig, type OfflineQueue, type PiiScrubberConfig, type PreFilterResult, REGION_ENDPOINTS, type RateLimiter, type RateLimiterConfig, captureEnvironment, createApiClient, createLogger, createOfflineQueue, createPiiScrubber, createPreFilter, createRateLimiter, getDeviceFingerprintHash, getReporterToken, getSessionId, noopLogger, resolveRegionEndpoint, scrubPii };
|
|
491
|
+
export { type ApiClientOptions, DEFAULT_API_ENDPOINT, type LogEntry, type LogFormat, type LogLevel, type Logger, type LoggerOptions, type MushiApiClient, type MushiApiResponse, type MushiCaptureConfig, type MushiCaptureEventInput, type MushiConfig, type MushiConsoleEntry, type MushiCooldownConfig, type MushiEnvironment, type MushiEventHandler, type MushiEventType, type MushiIntegrationsConfig, type MushiNetworkEntry, type MushiOfflineConfig, type MushiOnDeviceClassifier, type MushiOnDeviceClassifierInput, type MushiOnDeviceClassifierResult, type MushiPerformanceMetrics, type MushiPreFilterConfig, type MushiProactiveConfig, type MushiRegion, type MushiReport, type MushiReportBuilder, type MushiReportCategory, type MushiReportStatus, type MushiRewardsConfig, type MushiSDKInstance, type MushiSelectedElement, type MushiSentryConfig, type MushiWidgetConfig, type OfflineQueue, type PiiScrubberConfig, type PreFilterResult, REGION_ENDPOINTS, type RateLimiter, type RateLimiterConfig, captureEnvironment, createApiClient, createLogger, createOfflineQueue, createPiiScrubber, createPreFilter, createRateLimiter, getDeviceFingerprintHash, getReporterToken, getSessionId, noopLogger, resolveRegionEndpoint, scrubPii };
|
package/dist/index.d.ts
CHANGED
|
@@ -95,6 +95,21 @@ interface MushiOfflineConfig {
|
|
|
95
95
|
enabled?: boolean;
|
|
96
96
|
maxQueueSize?: number;
|
|
97
97
|
syncOnReconnect?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Encrypt queued reports at rest (IndexedDB + localStorage) with AES-GCM.
|
|
100
|
+
*
|
|
101
|
+
* Wave S1 / D-16: on a shared device (kiosks, iPads, support-agent
|
|
102
|
+
* laptops) any queued report sits in plaintext until the next online flush.
|
|
103
|
+
* With this flag set we generate a non-extractable AES-GCM key at first use,
|
|
104
|
+
* stash it in a single tightly-scoped IndexedDB record, and wrap every
|
|
105
|
+
* queued report payload under it. The key never leaves the origin's
|
|
106
|
+
* IndexedDB; stealing the DB file on disk still requires the OS-level
|
|
107
|
+
* Web Crypto keystore to decrypt.
|
|
108
|
+
*
|
|
109
|
+
* Defaults to `true`. Set false only if you need to inspect raw queue
|
|
110
|
+
* contents during local dev.
|
|
111
|
+
*/
|
|
112
|
+
encryptAtRest?: boolean;
|
|
98
113
|
}
|
|
99
114
|
interface MushiRewardsConfig {
|
|
100
115
|
enabled?: boolean;
|
|
@@ -120,7 +135,7 @@ interface MushiReport {
|
|
|
120
135
|
sessionId?: string;
|
|
121
136
|
reporterToken: string;
|
|
122
137
|
/**
|
|
123
|
-
*
|
|
138
|
+
* §3c — stable per-device hash from `getDeviceFingerprintHash()`.
|
|
124
139
|
* Sent so the server can run the cross-account anti-gaming check; falls
|
|
125
140
|
* back to IP+UA fingerprinting when omitted.
|
|
126
141
|
*/
|
|
@@ -215,6 +230,43 @@ interface MushiSDKInstance {
|
|
|
215
230
|
open(): void;
|
|
216
231
|
close(): void;
|
|
217
232
|
destroy(): void;
|
|
233
|
+
/**
|
|
234
|
+
* Wave G4 — unified `captureEvent` API. Submits a bug report
|
|
235
|
+
* programmatically without opening the widget. Useful for adapters
|
|
236
|
+
* that translate errors from Datadog / Honeycomb / Sentry /
|
|
237
|
+
* Grafana into Mushi reports.
|
|
238
|
+
*
|
|
239
|
+
* Returns the server-assigned report id when the submit succeeds.
|
|
240
|
+
*/
|
|
241
|
+
captureEvent(event: MushiCaptureEventInput): Promise<string | null>;
|
|
242
|
+
/**
|
|
243
|
+
* Wave G4 — sugar alias for `setUser()`. Name mirrors the
|
|
244
|
+
* identify/track/capture vocabulary that PostHog/Segment/Mixpanel
|
|
245
|
+
* users already know.
|
|
246
|
+
*/
|
|
247
|
+
identify(userId: string, traits?: {
|
|
248
|
+
email?: string;
|
|
249
|
+
name?: string;
|
|
250
|
+
[k: string]: unknown;
|
|
251
|
+
}): void;
|
|
252
|
+
}
|
|
253
|
+
interface MushiCaptureEventInput {
|
|
254
|
+
/** Human-readable summary; becomes `reports.description`. */
|
|
255
|
+
description: string;
|
|
256
|
+
category?: MushiReportCategory;
|
|
257
|
+
severity?: 'critical' | 'high' | 'medium' | 'low';
|
|
258
|
+
component?: string;
|
|
259
|
+
/** Arbitrary tags merged into `reports.metadata.tags`. */
|
|
260
|
+
tags?: Record<string, string | number | boolean>;
|
|
261
|
+
/** Source-of-truth adapter that produced this event (e.g. `'datadog'`). */
|
|
262
|
+
source?: string;
|
|
263
|
+
/** Optional error payload (name/message/stack) captured from the host app. */
|
|
264
|
+
error?: {
|
|
265
|
+
name?: string;
|
|
266
|
+
message?: string;
|
|
267
|
+
stack?: string;
|
|
268
|
+
};
|
|
269
|
+
metadata?: Record<string, unknown>;
|
|
218
270
|
}
|
|
219
271
|
interface MushiApiClient {
|
|
220
272
|
submitReport(report: MushiReport): Promise<MushiApiResponse<{
|
|
@@ -248,7 +300,7 @@ declare const DEFAULT_API_ENDPOINT = "https://dxptnwrhwsqckaftyymj.supabase.co/f
|
|
|
248
300
|
declare function createApiClient(options: ApiClientOptions): MushiApiClient;
|
|
249
301
|
|
|
250
302
|
/**
|
|
251
|
-
*
|
|
303
|
+
* C7: Data residency region resolution.
|
|
252
304
|
*
|
|
253
305
|
* The SDK supports four regional clouds:
|
|
254
306
|
* - 'us' → United States (default; legacy `dxptnwrhwsqckaftyymj`)
|
|
@@ -306,7 +358,7 @@ declare function captureEnvironment(): MushiEnvironment;
|
|
|
306
358
|
declare function getReporterToken(): string;
|
|
307
359
|
|
|
308
360
|
/**
|
|
309
|
-
*
|
|
361
|
+
* §3c — stable device fingerprint hash.
|
|
310
362
|
*
|
|
311
363
|
* Hashes a deliberately small set of long-lived device characteristics so
|
|
312
364
|
* the same browser keeps the same hash across sessions, but moving to a
|
|
@@ -350,6 +402,19 @@ interface PiiScrubberConfig {
|
|
|
350
402
|
creditCards?: boolean;
|
|
351
403
|
ssns?: boolean;
|
|
352
404
|
ipAddresses?: boolean;
|
|
405
|
+
/**
|
|
406
|
+
* Scrub vendor-shaped secret tokens (AWS access keys, Stripe keys,
|
|
407
|
+
* Slack/GitHub PATs, OpenAI/Anthropic/Google keys, JWTs).
|
|
408
|
+
*
|
|
409
|
+
* Wave S1 / D-15: SDK parity with the server-side scrubber. The server
|
|
410
|
+
* scrubs these on every LLM invocation; the SDK now scrubs them at
|
|
411
|
+
* capture so they never hit the wire in the first place — important for
|
|
412
|
+
* users who `console.log(stripeKey)` during dev and later ship bug
|
|
413
|
+
* reports with the error text attached.
|
|
414
|
+
*/
|
|
415
|
+
secretTokens?: boolean;
|
|
416
|
+
/** IPv6 addresses. Defaults off for the same reason IPv4 does. */
|
|
417
|
+
ipv6?: boolean;
|
|
353
418
|
}
|
|
354
419
|
declare function createPiiScrubber(config?: PiiScrubberConfig): {
|
|
355
420
|
scrub: (text: string) => string;
|
|
@@ -423,4 +488,4 @@ declare function createLogger(options: LoggerOptions): Logger;
|
|
|
423
488
|
*/
|
|
424
489
|
declare const noopLogger: Logger;
|
|
425
490
|
|
|
426
|
-
export { type ApiClientOptions, DEFAULT_API_ENDPOINT, type LogEntry, type LogFormat, type LogLevel, type Logger, type LoggerOptions, type MushiApiClient, type MushiApiResponse, type MushiCaptureConfig, type MushiConfig, type MushiConsoleEntry, type MushiCooldownConfig, type MushiEnvironment, type MushiEventHandler, type MushiEventType, type MushiIntegrationsConfig, type MushiNetworkEntry, type MushiOfflineConfig, type MushiOnDeviceClassifier, type MushiOnDeviceClassifierInput, type MushiOnDeviceClassifierResult, type MushiPerformanceMetrics, type MushiPreFilterConfig, type MushiProactiveConfig, type MushiRegion, type MushiReport, type MushiReportBuilder, type MushiReportCategory, type MushiReportStatus, type MushiRewardsConfig, type MushiSDKInstance, type MushiSelectedElement, type MushiSentryConfig, type MushiWidgetConfig, type OfflineQueue, type PiiScrubberConfig, type PreFilterResult, REGION_ENDPOINTS, type RateLimiter, type RateLimiterConfig, captureEnvironment, createApiClient, createLogger, createOfflineQueue, createPiiScrubber, createPreFilter, createRateLimiter, getDeviceFingerprintHash, getReporterToken, getSessionId, noopLogger, resolveRegionEndpoint, scrubPii };
|
|
491
|
+
export { type ApiClientOptions, DEFAULT_API_ENDPOINT, type LogEntry, type LogFormat, type LogLevel, type Logger, type LoggerOptions, type MushiApiClient, type MushiApiResponse, type MushiCaptureConfig, type MushiCaptureEventInput, type MushiConfig, type MushiConsoleEntry, type MushiCooldownConfig, type MushiEnvironment, type MushiEventHandler, type MushiEventType, type MushiIntegrationsConfig, type MushiNetworkEntry, type MushiOfflineConfig, type MushiOnDeviceClassifier, type MushiOnDeviceClassifierInput, type MushiOnDeviceClassifierResult, type MushiPerformanceMetrics, type MushiPreFilterConfig, type MushiProactiveConfig, type MushiRegion, type MushiReport, type MushiReportBuilder, type MushiReportCategory, type MushiReportStatus, type MushiRewardsConfig, type MushiSDKInstance, type MushiSelectedElement, type MushiSentryConfig, type MushiWidgetConfig, type OfflineQueue, type PiiScrubberConfig, type PreFilterResult, REGION_ENDPOINTS, type RateLimiter, type RateLimiterConfig, captureEnvironment, createApiClient, createLogger, createOfflineQueue, createPiiScrubber, createPreFilter, createRateLimiter, getDeviceFingerprintHash, getReporterToken, getSessionId, noopLogger, resolveRegionEndpoint, scrubPii };
|
package/dist/index.js
CHANGED
|
@@ -348,6 +348,98 @@ var noopLogger = {
|
|
|
348
348
|
}
|
|
349
349
|
};
|
|
350
350
|
|
|
351
|
+
// src/queue-crypto.ts
|
|
352
|
+
var KEY_DB = "mushi-mushi-keyring";
|
|
353
|
+
var KEY_STORE = "keys";
|
|
354
|
+
var KEY_RECORD_ID = "offline-queue/v1";
|
|
355
|
+
var cachedKey = null;
|
|
356
|
+
var cachedKeyPromise = null;
|
|
357
|
+
function hasWebCrypto() {
|
|
358
|
+
return typeof globalThis !== "undefined" && typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined" && typeof indexedDB !== "undefined";
|
|
359
|
+
}
|
|
360
|
+
function openKeyDb() {
|
|
361
|
+
return new Promise((resolve, reject) => {
|
|
362
|
+
const req = indexedDB.open(KEY_DB, 1);
|
|
363
|
+
req.onupgradeneeded = () => {
|
|
364
|
+
const db = req.result;
|
|
365
|
+
if (!db.objectStoreNames.contains(KEY_STORE)) {
|
|
366
|
+
db.createObjectStore(KEY_STORE);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
req.onsuccess = () => resolve(req.result);
|
|
370
|
+
req.onerror = () => reject(req.error);
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
async function loadKey() {
|
|
374
|
+
const db = await openKeyDb();
|
|
375
|
+
return new Promise((resolve, reject) => {
|
|
376
|
+
const tx = db.transaction(KEY_STORE, "readonly");
|
|
377
|
+
const req = tx.objectStore(KEY_STORE).get(KEY_RECORD_ID);
|
|
378
|
+
req.onsuccess = () => resolve(req.result ?? null);
|
|
379
|
+
req.onerror = () => reject(req.error);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
async function storeKey(key) {
|
|
383
|
+
const db = await openKeyDb();
|
|
384
|
+
return new Promise((resolve, reject) => {
|
|
385
|
+
const tx = db.transaction(KEY_STORE, "readwrite");
|
|
386
|
+
tx.objectStore(KEY_STORE).put(key, KEY_RECORD_ID);
|
|
387
|
+
tx.oncomplete = () => resolve();
|
|
388
|
+
tx.onerror = () => reject(tx.error);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
async function getOfflineQueueKey() {
|
|
392
|
+
if (cachedKey) return cachedKey;
|
|
393
|
+
if (cachedKeyPromise) return cachedKeyPromise;
|
|
394
|
+
if (!hasWebCrypto()) {
|
|
395
|
+
throw new Error("Web Crypto + IndexedDB required for offline queue encryption");
|
|
396
|
+
}
|
|
397
|
+
cachedKeyPromise = (async () => {
|
|
398
|
+
const existing = await loadKey();
|
|
399
|
+
if (existing) {
|
|
400
|
+
cachedKey = existing;
|
|
401
|
+
return existing;
|
|
402
|
+
}
|
|
403
|
+
const key = await crypto.subtle.generateKey(
|
|
404
|
+
{ name: "AES-GCM", length: 256 },
|
|
405
|
+
false,
|
|
406
|
+
["encrypt", "decrypt"]
|
|
407
|
+
);
|
|
408
|
+
await storeKey(key);
|
|
409
|
+
cachedKey = key;
|
|
410
|
+
return key;
|
|
411
|
+
})();
|
|
412
|
+
return cachedKeyPromise;
|
|
413
|
+
}
|
|
414
|
+
function bytesToB64(bytes) {
|
|
415
|
+
let s = "";
|
|
416
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
417
|
+
return btoa(s);
|
|
418
|
+
}
|
|
419
|
+
function b64ToBytes(s) {
|
|
420
|
+
const bin = atob(s);
|
|
421
|
+
const out = new Uint8Array(new ArrayBuffer(bin.length));
|
|
422
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
423
|
+
return out;
|
|
424
|
+
}
|
|
425
|
+
async function encryptJson(plain) {
|
|
426
|
+
const key = await getOfflineQueueKey();
|
|
427
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
428
|
+
const data = new TextEncoder().encode(JSON.stringify(plain));
|
|
429
|
+
const cipher = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, data));
|
|
430
|
+
return { _mme: 1, iv: bytesToB64(iv), ct: bytesToB64(cipher) };
|
|
431
|
+
}
|
|
432
|
+
function isEncryptedPayload(v) {
|
|
433
|
+
return !!v && typeof v === "object" && v._mme === 1 && typeof v.iv === "string" && typeof v.ct === "string";
|
|
434
|
+
}
|
|
435
|
+
async function decryptJson(payload) {
|
|
436
|
+
const key = await getOfflineQueueKey();
|
|
437
|
+
const iv = b64ToBytes(payload.iv);
|
|
438
|
+
const ct = b64ToBytes(payload.ct);
|
|
439
|
+
const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct);
|
|
440
|
+
return JSON.parse(new TextDecoder().decode(plain));
|
|
441
|
+
}
|
|
442
|
+
|
|
351
443
|
// src/queue.ts
|
|
352
444
|
var queueLog = createLogger({ scope: "mushi:queue", level: "warn" });
|
|
353
445
|
var DB_NAME = "mushi-mushi";
|
|
@@ -357,9 +449,36 @@ var LS_KEY = "mushi_offline_queue";
|
|
|
357
449
|
var BATCH_SIZE = 10;
|
|
358
450
|
var MAX_BACKOFF_MS = 6e4;
|
|
359
451
|
function createOfflineQueue(config = {}) {
|
|
360
|
-
const { enabled = true, maxQueueSize = 50, syncOnReconnect = true } = config;
|
|
452
|
+
const { enabled = true, maxQueueSize = 50, syncOnReconnect = true, encryptAtRest = true } = config;
|
|
361
453
|
let syncCleanup = null;
|
|
362
454
|
let backendType = null;
|
|
455
|
+
async function wrapForStorage(report) {
|
|
456
|
+
const queuedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
457
|
+
if (!encryptAtRest) {
|
|
458
|
+
return { ...report, queuedAt };
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
const payload = await encryptJson(report);
|
|
462
|
+
return { id: report.id, queuedAt, payload };
|
|
463
|
+
} catch (err) {
|
|
464
|
+
queueLog.warn("Offline queue: encryption failed, storing plaintext", { err: String(err) });
|
|
465
|
+
return { ...report, queuedAt };
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
async function unwrapForSend(row) {
|
|
469
|
+
if (isEncryptedRecord(row)) {
|
|
470
|
+
try {
|
|
471
|
+
return await decryptJson(row.payload);
|
|
472
|
+
} catch (err) {
|
|
473
|
+
queueLog.warn("Offline queue: decrypt failed, dropping row", { err: String(err), id: row.id });
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return row;
|
|
478
|
+
}
|
|
479
|
+
function isEncryptedRecord(row) {
|
|
480
|
+
return !!row.payload && isEncryptedPayload(row.payload);
|
|
481
|
+
}
|
|
363
482
|
function detectBackend() {
|
|
364
483
|
if (backendType) return backendType;
|
|
365
484
|
if (typeof indexedDB !== "undefined") {
|
|
@@ -389,9 +508,10 @@ function createOfflineQueue(config = {}) {
|
|
|
389
508
|
}
|
|
390
509
|
async function idbEnqueue(report) {
|
|
391
510
|
const db = await openDb();
|
|
511
|
+
const row = await wrapForStorage(report);
|
|
392
512
|
return new Promise((resolve, reject) => {
|
|
393
513
|
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
394
|
-
tx.objectStore(STORE_NAME).put(
|
|
514
|
+
tx.objectStore(STORE_NAME).put(row);
|
|
395
515
|
tx.oncomplete = () => resolve();
|
|
396
516
|
tx.onerror = () => reject(tx.error);
|
|
397
517
|
});
|
|
@@ -440,20 +560,20 @@ function createOfflineQueue(config = {}) {
|
|
|
440
560
|
return [];
|
|
441
561
|
}
|
|
442
562
|
}
|
|
443
|
-
function lsWrite(
|
|
563
|
+
function lsWrite(rows) {
|
|
444
564
|
try {
|
|
445
|
-
localStorage.setItem(LS_KEY, JSON.stringify(
|
|
565
|
+
localStorage.setItem(LS_KEY, JSON.stringify(rows));
|
|
446
566
|
} catch {
|
|
447
567
|
}
|
|
448
568
|
}
|
|
449
|
-
function lsEnqueue(report) {
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
lsWrite(
|
|
569
|
+
async function lsEnqueue(report) {
|
|
570
|
+
const rows = lsRead();
|
|
571
|
+
rows.push(await wrapForStorage(report));
|
|
572
|
+
lsWrite(rows);
|
|
453
573
|
}
|
|
454
574
|
function lsDelete(id) {
|
|
455
|
-
const
|
|
456
|
-
lsWrite(
|
|
575
|
+
const rows = lsRead().filter((r) => r.id !== id);
|
|
576
|
+
lsWrite(rows);
|
|
457
577
|
}
|
|
458
578
|
async function enqueue(report) {
|
|
459
579
|
if (!enabled) return;
|
|
@@ -472,7 +592,7 @@ function createOfflineQueue(config = {}) {
|
|
|
472
592
|
}
|
|
473
593
|
}
|
|
474
594
|
if (backend === "localstorage" || backendType === "localstorage") {
|
|
475
|
-
lsEnqueue(report);
|
|
595
|
+
await lsEnqueue(report);
|
|
476
596
|
return;
|
|
477
597
|
}
|
|
478
598
|
}
|
|
@@ -484,29 +604,41 @@ function createOfflineQueue(config = {}) {
|
|
|
484
604
|
}
|
|
485
605
|
async function flush(client) {
|
|
486
606
|
if (!enabled) return { sent: 0, failed: 0 };
|
|
487
|
-
let
|
|
607
|
+
let rows;
|
|
488
608
|
const backend = detectBackend();
|
|
489
609
|
if (backend === "indexeddb") {
|
|
490
610
|
try {
|
|
491
|
-
|
|
611
|
+
rows = await idbGetAll();
|
|
492
612
|
} catch {
|
|
493
|
-
|
|
613
|
+
rows = lsRead();
|
|
494
614
|
}
|
|
495
615
|
} else {
|
|
496
|
-
|
|
616
|
+
rows = lsRead();
|
|
497
617
|
}
|
|
498
|
-
const batch =
|
|
618
|
+
const batch = rows.slice(0, BATCH_SIZE);
|
|
499
619
|
let sent = 0;
|
|
500
620
|
let failed = 0;
|
|
501
621
|
for (let i = 0; i < batch.length; i++) {
|
|
502
|
-
const
|
|
622
|
+
const row = batch[i];
|
|
623
|
+
const rowId = row.id;
|
|
624
|
+
const report = await unwrapForSend(row);
|
|
625
|
+
if (!report) {
|
|
626
|
+
try {
|
|
627
|
+
if (backend === "indexeddb") await idbDelete(rowId);
|
|
628
|
+
else lsDelete(rowId);
|
|
629
|
+
} catch {
|
|
630
|
+
lsDelete(rowId);
|
|
631
|
+
}
|
|
632
|
+
failed++;
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
503
635
|
const result = await client.submitReport(report);
|
|
504
636
|
if (result.ok) {
|
|
505
637
|
try {
|
|
506
|
-
if (backend === "indexeddb") await idbDelete(
|
|
507
|
-
else lsDelete(
|
|
638
|
+
if (backend === "indexeddb") await idbDelete(rowId);
|
|
639
|
+
else lsDelete(rowId);
|
|
508
640
|
} catch {
|
|
509
|
-
lsDelete(
|
|
641
|
+
lsDelete(rowId);
|
|
510
642
|
}
|
|
511
643
|
sent++;
|
|
512
644
|
} else {
|
|
@@ -740,16 +872,33 @@ function createRateLimiter(config = {}) {
|
|
|
740
872
|
var ORDERED_PATTERNS = [
|
|
741
873
|
{ key: "ssns", regex: /\b\d{3}-\d{2}-\d{4}\b/g, replacement: "[REDACTED_SSN]" },
|
|
742
874
|
{ key: "creditCards", regex: /\b(?:\d[ -]*){12,18}\d\b/g, replacement: "[REDACTED_CC]" },
|
|
875
|
+
// Vendor secret tokens — mirrors packages/server/.../pii-scrubber.ts exactly.
|
|
876
|
+
{ key: "secretTokens", regex: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
|
|
877
|
+
{ key: "secretTokens", regex: /(?:aws_secret_access_key|secret_access_key)["'\s:=]+[A-Za-z0-9/+=]{40}\b/gi, replacement: "aws_secret_access_key=[REDACTED_AWS_SECRET]" },
|
|
878
|
+
{ key: "secretTokens", regex: /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{24,}\b/g, replacement: "[REDACTED_STRIPE_KEY]" },
|
|
879
|
+
{ key: "secretTokens", regex: /\bpk_(?:live|test)_[A-Za-z0-9]{24,}\b/g, replacement: "[REDACTED_STRIPE_PK]" },
|
|
880
|
+
{ key: "secretTokens", regex: /\bxox[abpor]-[A-Za-z0-9-]{10,}\b/g, replacement: "[REDACTED_SLACK_TOKEN]" },
|
|
881
|
+
{ key: "secretTokens", regex: /\bghp_[A-Za-z0-9]{36}\b/g, replacement: "[REDACTED_GITHUB_PAT]" },
|
|
882
|
+
{ key: "secretTokens", regex: /\bgithub_pat_[A-Za-z0-9_]{80,}\b/g, replacement: "[REDACTED_GITHUB_PAT]" },
|
|
883
|
+
{ key: "secretTokens", regex: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g, replacement: "[REDACTED_OPENAI_KEY]" },
|
|
884
|
+
{ key: "secretTokens", regex: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g, replacement: "[REDACTED_ANTHROPIC_KEY]" },
|
|
885
|
+
{ key: "secretTokens", regex: /\bAIza[0-9A-Za-z_-]{35}\b/g, replacement: "[REDACTED_GOOGLE_KEY]" },
|
|
886
|
+
{ key: "secretTokens", regex: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, replacement: "[REDACTED_JWT]" },
|
|
743
887
|
{ key: "emails", regex: /\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/g, replacement: "[REDACTED_EMAIL]" },
|
|
744
888
|
{ key: "phones", regex: /(?:\+\d{1,3}[\s.-])?\(?\d{2,4}\)?[\s.-]\d{3,4}[\s.-]\d{3,4}\b/g, replacement: "[REDACTED_PHONE]" },
|
|
745
|
-
{ key: "ipAddresses", regex: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, replacement: "[REDACTED_IP]" }
|
|
889
|
+
{ key: "ipAddresses", regex: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, replacement: "[REDACTED_IP]" },
|
|
890
|
+
{ key: "ipv6", regex: /\b(?:[A-Fa-f0-9]{1,4}:){2,7}[A-Fa-f0-9]{0,4}\b/g, replacement: "[REDACTED_IPV6]" }
|
|
746
891
|
];
|
|
747
892
|
var DEFAULT_CONFIG = {
|
|
748
893
|
emails: true,
|
|
749
894
|
phones: true,
|
|
750
895
|
creditCards: true,
|
|
751
896
|
ssns: true,
|
|
752
|
-
ipAddresses: false
|
|
897
|
+
ipAddresses: false,
|
|
898
|
+
// Secret tokens default ON — if they leak into a bug report there's no
|
|
899
|
+
// good reason to ship them to our servers. Cheaper to scrub client-side.
|
|
900
|
+
secretTokens: true,
|
|
901
|
+
ipv6: false
|
|
753
902
|
};
|
|
754
903
|
function createPiiScrubber(config = {}) {
|
|
755
904
|
const merged = { ...DEFAULT_CONFIG, ...config };
|