@mushi-mushi/core 0.2.1 → 0.3.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.
@@ -0,0 +1,51 @@
1
+ <!--
2
+ AUTO-SYNCED from repo root by scripts/sync-community-files.mjs.
3
+ Do not edit here — edit the canonical file at the repository root and
4
+ re-run `node scripts/sync-community-files.mjs` (pre-commit hook does this
5
+ automatically).
6
+ -->
7
+
8
+ # Contributor Covenant Code of Conduct
9
+
10
+ ## Our Pledge
11
+
12
+ We as members, contributors, and leaders pledge to make participation in our
13
+ community a harassment-free experience for everyone, regardless of age, body
14
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
15
+ identity and expression, level of experience, education, socio-economic status,
16
+ nationality, personal appearance, race, caste, color, religion, or sexual
17
+ identity and orientation.
18
+
19
+ We pledge to act and interact in ways that contribute to an open, welcoming,
20
+ diverse, inclusive, and healthy community.
21
+
22
+ ## Our Standards
23
+
24
+ Examples of behavior that contributes to a positive environment:
25
+
26
+ - Using welcoming and inclusive language
27
+ - Being respectful of differing viewpoints and experiences
28
+ - Gracefully accepting constructive criticism
29
+ - Focusing on what is best for the community
30
+ - Showing empathy towards other community members
31
+
32
+ Examples of unacceptable behavior:
33
+
34
+ - The use of sexualized language or imagery, and sexual attention or advances of any kind
35
+ - Trolling, insulting or derogatory comments, and personal or political attacks
36
+ - Public or private harassment
37
+ - Publishing others' private information without explicit permission
38
+ - Other conduct which could reasonably be considered inappropriate in a professional setting
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
43
+ reported to the project team at **security@mushimushi.dev**.
44
+
45
+ All complaints will be reviewed and investigated promptly and fairly.
46
+
47
+ ## Attribution
48
+
49
+ This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/),
50
+ version 2.1, available at
51
+ https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
@@ -0,0 +1,122 @@
1
+ <!--
2
+ AUTO-SYNCED from repo root by scripts/sync-community-files.mjs.
3
+ Do not edit here — edit the canonical file at the repository root and
4
+ re-run `node scripts/sync-community-files.mjs` (pre-commit hook does this
5
+ automatically).
6
+ -->
7
+
8
+ # Contributing to Mushi Mushi
9
+
10
+ Thanks for wanting to help. Here's everything you need to get started.
11
+
12
+ ## Prerequisites
13
+
14
+ - **Node.js >= 22** (see `.node-version`)
15
+ - **pnpm >= 10** — install with `corepack enable`
16
+
17
+ ## Setup
18
+
19
+ ```bash
20
+ git clone https://github.com/kensaurus/mushi-mushi.git
21
+ cd mushi-mushi
22
+ pnpm install
23
+ pnpm build
24
+ ```
25
+
26
+ ## Development
27
+
28
+ ```bash
29
+ pnpm dev # Start all dev servers (admin on :6464)
30
+ pnpm test # Run Vitest across all packages
31
+ pnpm typecheck # TypeScript checks
32
+ pnpm lint # ESLint
33
+ pnpm format # Prettier
34
+ ```
35
+
36
+ ### Working on a single package
37
+
38
+ ```bash
39
+ cd packages/core
40
+ pnpm dev # Watch mode
41
+ pnpm test # Tests for this package only
42
+ ```
43
+
44
+ ## Project Structure
45
+
46
+ ```
47
+ packages/
48
+ core/ Types, API client, offline queue (MIT)
49
+ web/ Browser SDK — widget, capture (MIT)
50
+ react/ React bindings (MIT)
51
+ vue/ Vue 3 plugin (MIT)
52
+ svelte/ Svelte SDK (MIT)
53
+ angular/ Angular SDK (MIT)
54
+ react-native/ React Native SDK (MIT)
55
+ cli/ CLI tool (MIT)
56
+ mcp/ MCP server for coding agents (MIT)
57
+ server/ Supabase Edge Functions (BSL)
58
+ agents/ Agentic fix pipeline (BSL)
59
+ verify/ Fix verification (BSL)
60
+ apps/
61
+ admin/ Admin dashboard (React + Tailwind)
62
+ docs/ Documentation site (planned)
63
+ tooling/
64
+ eslint-config/ Shared ESLint flat config
65
+ tsconfig/ Shared TypeScript configs
66
+ ```
67
+
68
+ ## Making Changes
69
+
70
+ 1. Create a feature branch from `master`
71
+ 2. Make your changes
72
+ 3. Add tests for new functionality
73
+ 4. Run `pnpm typecheck && pnpm lint && pnpm test` to verify
74
+ 5. Create a changeset if your change affects published packages:
75
+ ```bash
76
+ pnpm changeset
77
+ ```
78
+ 6. Open a pull request
79
+
80
+ ## Changesets
81
+
82
+ We use [Changesets](https://github.com/changesets/changesets) for versioning. If your PR modifies a published package (`core`, `web`, `react`, `vue`, `svelte`, `angular`, `react-native`, `cli`, `mcp`), add a changeset:
83
+
84
+ ```bash
85
+ pnpm changeset
86
+ ```
87
+
88
+ Select the affected packages, the semver bump type, and write a summary. The changeset file gets committed with your PR.
89
+
90
+ ## Code Style
91
+
92
+ - **TypeScript strict mode** — no `any` unless absolutely necessary
93
+ - **Prettier** formats everything — run `pnpm format` before committing
94
+ - **ESLint** catches bugs — `pnpm lint` must pass
95
+ - **No default exports** in library packages — use named exports
96
+ - **Dual ESM/CJS** builds via tsup for all SDK packages
97
+
98
+ ## Commit Messages
99
+
100
+ Use conventional commits:
101
+
102
+ ```
103
+ feat(core): add batch report submission
104
+ fix(web): prevent widget from opening during screenshot
105
+ docs(react): update provider usage example
106
+ chore: bump dependencies
107
+ ```
108
+
109
+ ## Tests
110
+
111
+ - **Framework:** Vitest
112
+ - **Location:** Co-located with source (`src/foo.test.ts`)
113
+ - **Coverage:** Required for `core`, `web`, `react` — encouraged for all packages
114
+
115
+ ## License
116
+
117
+ - SDK packages are MIT — your contributions will be MIT-licensed
118
+ - Server/agents/verify are BSL 1.1 — contributions to those packages fall under BSL
119
+
120
+ ## Questions?
121
+
122
+ Open an issue or start a discussion. We're happy to help.
package/SECURITY.md ADDED
@@ -0,0 +1,50 @@
1
+ <!--
2
+ AUTO-SYNCED from repo root by scripts/sync-community-files.mjs.
3
+ Do not edit here — edit the canonical file at the repository root and
4
+ re-run `node scripts/sync-community-files.mjs` (pre-commit hook does this
5
+ automatically).
6
+ -->
7
+
8
+ # Security Policy
9
+
10
+ ## Supported Versions
11
+
12
+ | Version | Supported |
13
+ |---------|-----------|
14
+ | 0.x | Yes |
15
+
16
+ ## Reporting a Vulnerability
17
+
18
+ If you discover a security vulnerability, please report it responsibly.
19
+
20
+ **Do NOT open a public GitHub issue.**
21
+
22
+ Instead, email: **security@mushimushi.dev**
23
+
24
+ Include:
25
+ - Description of the vulnerability
26
+ - Steps to reproduce
27
+ - Impact assessment
28
+ - Suggested fix (if any)
29
+
30
+ We will acknowledge receipt within 48 hours and aim to release a patch within 7 days for critical issues.
31
+
32
+ ## Scope
33
+
34
+ - All `@mushi-mushi/*` npm packages
35
+ - Supabase Edge Functions (server-side)
36
+ - Admin console application
37
+ - CLI tool
38
+
39
+ ## Out of Scope
40
+
41
+ - Self-hosted deployments configured by the user
42
+ - Third-party integrations (Jira, Linear, PagerDuty)
43
+ - Vulnerabilities requiring physical access
44
+
45
+ ## Security Best Practices for Users
46
+
47
+ - **Never commit your API keys** — use environment variables
48
+ - **Rotate API keys** regularly via the admin console
49
+ - **Enable SSO** for team projects (Enterprise tier)
50
+ - **Review audit logs** periodically for suspicious activity
package/dist/index.cjs CHANGED
@@ -350,6 +350,98 @@ var noopLogger = {
350
350
  }
351
351
  };
352
352
 
353
+ // src/queue-crypto.ts
354
+ var KEY_DB = "mushi-mushi-keyring";
355
+ var KEY_STORE = "keys";
356
+ var KEY_RECORD_ID = "offline-queue/v1";
357
+ var cachedKey = null;
358
+ var cachedKeyPromise = null;
359
+ function hasWebCrypto() {
360
+ return typeof globalThis !== "undefined" && typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined" && typeof indexedDB !== "undefined";
361
+ }
362
+ function openKeyDb() {
363
+ return new Promise((resolve, reject) => {
364
+ const req = indexedDB.open(KEY_DB, 1);
365
+ req.onupgradeneeded = () => {
366
+ const db = req.result;
367
+ if (!db.objectStoreNames.contains(KEY_STORE)) {
368
+ db.createObjectStore(KEY_STORE);
369
+ }
370
+ };
371
+ req.onsuccess = () => resolve(req.result);
372
+ req.onerror = () => reject(req.error);
373
+ });
374
+ }
375
+ async function loadKey() {
376
+ const db = await openKeyDb();
377
+ return new Promise((resolve, reject) => {
378
+ const tx = db.transaction(KEY_STORE, "readonly");
379
+ const req = tx.objectStore(KEY_STORE).get(KEY_RECORD_ID);
380
+ req.onsuccess = () => resolve(req.result ?? null);
381
+ req.onerror = () => reject(req.error);
382
+ });
383
+ }
384
+ async function storeKey(key) {
385
+ const db = await openKeyDb();
386
+ return new Promise((resolve, reject) => {
387
+ const tx = db.transaction(KEY_STORE, "readwrite");
388
+ tx.objectStore(KEY_STORE).put(key, KEY_RECORD_ID);
389
+ tx.oncomplete = () => resolve();
390
+ tx.onerror = () => reject(tx.error);
391
+ });
392
+ }
393
+ async function getOfflineQueueKey() {
394
+ if (cachedKey) return cachedKey;
395
+ if (cachedKeyPromise) return cachedKeyPromise;
396
+ if (!hasWebCrypto()) {
397
+ throw new Error("Web Crypto + IndexedDB required for offline queue encryption");
398
+ }
399
+ cachedKeyPromise = (async () => {
400
+ const existing = await loadKey();
401
+ if (existing) {
402
+ cachedKey = existing;
403
+ return existing;
404
+ }
405
+ const key = await crypto.subtle.generateKey(
406
+ { name: "AES-GCM", length: 256 },
407
+ false,
408
+ ["encrypt", "decrypt"]
409
+ );
410
+ await storeKey(key);
411
+ cachedKey = key;
412
+ return key;
413
+ })();
414
+ return cachedKeyPromise;
415
+ }
416
+ function bytesToB64(bytes) {
417
+ let s = "";
418
+ for (const b of bytes) s += String.fromCharCode(b);
419
+ return btoa(s);
420
+ }
421
+ function b64ToBytes(s) {
422
+ const bin = atob(s);
423
+ const out = new Uint8Array(new ArrayBuffer(bin.length));
424
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
425
+ return out;
426
+ }
427
+ async function encryptJson(plain) {
428
+ const key = await getOfflineQueueKey();
429
+ const iv = crypto.getRandomValues(new Uint8Array(12));
430
+ const data = new TextEncoder().encode(JSON.stringify(plain));
431
+ const cipher = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, data));
432
+ return { _mme: 1, iv: bytesToB64(iv), ct: bytesToB64(cipher) };
433
+ }
434
+ function isEncryptedPayload(v) {
435
+ return !!v && typeof v === "object" && v._mme === 1 && typeof v.iv === "string" && typeof v.ct === "string";
436
+ }
437
+ async function decryptJson(payload) {
438
+ const key = await getOfflineQueueKey();
439
+ const iv = b64ToBytes(payload.iv);
440
+ const ct = b64ToBytes(payload.ct);
441
+ const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct);
442
+ return JSON.parse(new TextDecoder().decode(plain));
443
+ }
444
+
353
445
  // src/queue.ts
354
446
  var queueLog = createLogger({ scope: "mushi:queue", level: "warn" });
355
447
  var DB_NAME = "mushi-mushi";
@@ -359,9 +451,36 @@ var LS_KEY = "mushi_offline_queue";
359
451
  var BATCH_SIZE = 10;
360
452
  var MAX_BACKOFF_MS = 6e4;
361
453
  function createOfflineQueue(config = {}) {
362
- const { enabled = true, maxQueueSize = 50, syncOnReconnect = true } = config;
454
+ const { enabled = true, maxQueueSize = 50, syncOnReconnect = true, encryptAtRest = true } = config;
363
455
  let syncCleanup = null;
364
456
  let backendType = null;
457
+ async function wrapForStorage(report) {
458
+ const queuedAt = (/* @__PURE__ */ new Date()).toISOString();
459
+ if (!encryptAtRest) {
460
+ return { ...report, queuedAt };
461
+ }
462
+ try {
463
+ const payload = await encryptJson(report);
464
+ return { id: report.id, queuedAt, payload };
465
+ } catch (err) {
466
+ queueLog.warn("Offline queue: encryption failed, storing plaintext", { err: String(err) });
467
+ return { ...report, queuedAt };
468
+ }
469
+ }
470
+ async function unwrapForSend(row) {
471
+ if (isEncryptedRecord(row)) {
472
+ try {
473
+ return await decryptJson(row.payload);
474
+ } catch (err) {
475
+ queueLog.warn("Offline queue: decrypt failed, dropping row", { err: String(err), id: row.id });
476
+ return null;
477
+ }
478
+ }
479
+ return row;
480
+ }
481
+ function isEncryptedRecord(row) {
482
+ return !!row.payload && isEncryptedPayload(row.payload);
483
+ }
365
484
  function detectBackend() {
366
485
  if (backendType) return backendType;
367
486
  if (typeof indexedDB !== "undefined") {
@@ -391,9 +510,10 @@ function createOfflineQueue(config = {}) {
391
510
  }
392
511
  async function idbEnqueue(report) {
393
512
  const db = await openDb();
513
+ const row = await wrapForStorage(report);
394
514
  return new Promise((resolve, reject) => {
395
515
  const tx = db.transaction(STORE_NAME, "readwrite");
396
- tx.objectStore(STORE_NAME).put({ ...report, queuedAt: (/* @__PURE__ */ new Date()).toISOString() });
516
+ tx.objectStore(STORE_NAME).put(row);
397
517
  tx.oncomplete = () => resolve();
398
518
  tx.onerror = () => reject(tx.error);
399
519
  });
@@ -442,20 +562,20 @@ function createOfflineQueue(config = {}) {
442
562
  return [];
443
563
  }
444
564
  }
445
- function lsWrite(reports) {
565
+ function lsWrite(rows) {
446
566
  try {
447
- localStorage.setItem(LS_KEY, JSON.stringify(reports));
567
+ localStorage.setItem(LS_KEY, JSON.stringify(rows));
448
568
  } catch {
449
569
  }
450
570
  }
451
- function lsEnqueue(report) {
452
- const reports = lsRead();
453
- reports.push({ ...report, queuedAt: (/* @__PURE__ */ new Date()).toISOString() });
454
- lsWrite(reports);
571
+ async function lsEnqueue(report) {
572
+ const rows = lsRead();
573
+ rows.push(await wrapForStorage(report));
574
+ lsWrite(rows);
455
575
  }
456
576
  function lsDelete(id) {
457
- const reports = lsRead().filter((r) => r.id !== id);
458
- lsWrite(reports);
577
+ const rows = lsRead().filter((r) => r.id !== id);
578
+ lsWrite(rows);
459
579
  }
460
580
  async function enqueue(report) {
461
581
  if (!enabled) return;
@@ -474,7 +594,7 @@ function createOfflineQueue(config = {}) {
474
594
  }
475
595
  }
476
596
  if (backend === "localstorage" || backendType === "localstorage") {
477
- lsEnqueue(report);
597
+ await lsEnqueue(report);
478
598
  return;
479
599
  }
480
600
  }
@@ -486,29 +606,41 @@ function createOfflineQueue(config = {}) {
486
606
  }
487
607
  async function flush(client) {
488
608
  if (!enabled) return { sent: 0, failed: 0 };
489
- let reports;
609
+ let rows;
490
610
  const backend = detectBackend();
491
611
  if (backend === "indexeddb") {
492
612
  try {
493
- reports = await idbGetAll();
613
+ rows = await idbGetAll();
494
614
  } catch {
495
- reports = lsRead();
615
+ rows = lsRead();
496
616
  }
497
617
  } else {
498
- reports = lsRead();
618
+ rows = lsRead();
499
619
  }
500
- const batch = reports.slice(0, BATCH_SIZE);
620
+ const batch = rows.slice(0, BATCH_SIZE);
501
621
  let sent = 0;
502
622
  let failed = 0;
503
623
  for (let i = 0; i < batch.length; i++) {
504
- const report = batch[i];
624
+ const row = batch[i];
625
+ const rowId = row.id;
626
+ const report = await unwrapForSend(row);
627
+ if (!report) {
628
+ try {
629
+ if (backend === "indexeddb") await idbDelete(rowId);
630
+ else lsDelete(rowId);
631
+ } catch {
632
+ lsDelete(rowId);
633
+ }
634
+ failed++;
635
+ continue;
636
+ }
505
637
  const result = await client.submitReport(report);
506
638
  if (result.ok) {
507
639
  try {
508
- if (backend === "indexeddb") await idbDelete(report.id);
509
- else lsDelete(report.id);
640
+ if (backend === "indexeddb") await idbDelete(rowId);
641
+ else lsDelete(rowId);
510
642
  } catch {
511
- lsDelete(report.id);
643
+ lsDelete(rowId);
512
644
  }
513
645
  sent++;
514
646
  } else {
@@ -742,16 +874,33 @@ function createRateLimiter(config = {}) {
742
874
  var ORDERED_PATTERNS = [
743
875
  { key: "ssns", regex: /\b\d{3}-\d{2}-\d{4}\b/g, replacement: "[REDACTED_SSN]" },
744
876
  { key: "creditCards", regex: /\b(?:\d[ -]*){12,18}\d\b/g, replacement: "[REDACTED_CC]" },
877
+ // Vendor secret tokens — mirrors packages/server/.../pii-scrubber.ts exactly.
878
+ { key: "secretTokens", regex: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
879
+ { 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]" },
880
+ { key: "secretTokens", regex: /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{24,}\b/g, replacement: "[REDACTED_STRIPE_KEY]" },
881
+ { key: "secretTokens", regex: /\bpk_(?:live|test)_[A-Za-z0-9]{24,}\b/g, replacement: "[REDACTED_STRIPE_PK]" },
882
+ { key: "secretTokens", regex: /\bxox[abpor]-[A-Za-z0-9-]{10,}\b/g, replacement: "[REDACTED_SLACK_TOKEN]" },
883
+ { key: "secretTokens", regex: /\bghp_[A-Za-z0-9]{36}\b/g, replacement: "[REDACTED_GITHUB_PAT]" },
884
+ { key: "secretTokens", regex: /\bgithub_pat_[A-Za-z0-9_]{80,}\b/g, replacement: "[REDACTED_GITHUB_PAT]" },
885
+ { key: "secretTokens", regex: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g, replacement: "[REDACTED_OPENAI_KEY]" },
886
+ { key: "secretTokens", regex: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g, replacement: "[REDACTED_ANTHROPIC_KEY]" },
887
+ { key: "secretTokens", regex: /\bAIza[0-9A-Za-z_-]{35}\b/g, replacement: "[REDACTED_GOOGLE_KEY]" },
888
+ { key: "secretTokens", regex: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, replacement: "[REDACTED_JWT]" },
745
889
  { key: "emails", regex: /\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/g, replacement: "[REDACTED_EMAIL]" },
746
890
  { key: "phones", regex: /(?:\+\d{1,3}[\s.-])?\(?\d{2,4}\)?[\s.-]\d{3,4}[\s.-]\d{3,4}\b/g, replacement: "[REDACTED_PHONE]" },
747
- { key: "ipAddresses", regex: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, replacement: "[REDACTED_IP]" }
891
+ { key: "ipAddresses", regex: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, replacement: "[REDACTED_IP]" },
892
+ { key: "ipv6", regex: /\b(?:[A-Fa-f0-9]{1,4}:){2,7}[A-Fa-f0-9]{0,4}\b/g, replacement: "[REDACTED_IPV6]" }
748
893
  ];
749
894
  var DEFAULT_CONFIG = {
750
895
  emails: true,
751
896
  phones: true,
752
897
  creditCards: true,
753
898
  ssns: true,
754
- ipAddresses: false
899
+ ipAddresses: false,
900
+ // Secret tokens default ON — if they leak into a bug report there's no
901
+ // good reason to ship them to our servers. Cheaper to scrub client-side.
902
+ secretTokens: true,
903
+ ipv6: false
755
904
  };
756
905
  function createPiiScrubber(config = {}) {
757
906
  const merged = { ...DEFAULT_CONFIG, ...config };