@mushi-mushi/core 1.23.0 → 1.25.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/CONTRIBUTING.md CHANGED
@@ -76,6 +76,7 @@ packages/
76
76
  apps/
77
77
  admin/ Admin dashboard (React + Tailwind)
78
78
  docs/ Documentation site (Nextra — apps/docs)
79
+ testers/ Tester marketplace portal (Next.js)
79
80
  tooling/
80
81
  eslint-config/ Shared ESLint flat config
81
82
  tsconfig/ Shared TypeScript configs
@@ -118,15 +119,22 @@ If your PR changes **user-visible SDK or CLI behavior**, update docs in the **sa
118
119
  Guardrails (run locally before pushing):
119
120
 
120
121
  ```bash
121
- pnpm check:docs-stats # root README stat badges
122
+ pnpm check:docs-stats # root README stat badges + glance <sub> line
122
123
  pnpm check:sdk-version-matrix # apps/docs/content/sdks/index.mdx versions
123
124
  pnpm check:internal-doc-links # dead /foo links in MDX
124
- pnpm check:onboarding-drift # phantom MCP env vars / stale tool names
125
+ pnpm check:onboarding-drift # phantom MCP env vars / stale tool names / native install coords
126
+ pnpm check:catalog-sync # MCP catalog ↔ admin counts ↔ prose "N tools"
127
+ pnpm check:mcp-tools-doc # mcp-tools.generated.mdx stale check
125
128
  pnpm check:bundle-docs # @mushi-mushi/web size-limit vs README
129
+ pnpm check:docs-drift # versions + public docs links
126
130
  pnpm gen:llms-txt # regenerate apps/docs/public/llms.txt after nav changes
127
131
  pnpm gen:mcp-tools-doc # regenerate sdks/mcp-tools.generated.mdx from catalog.ts
128
132
  ```
129
133
 
134
+ **Same-PR rule:** if behavior, env vars, CLI commands, MCP tools, or install
135
+ coordinates change, update the matching docs in this PR and run
136
+ `pnpm check:docs-drift && pnpm check:onboarding-drift && pnpm check:catalog-sync`.
137
+
130
138
  README changes on npm packages only appear on [npmjs.com](https://www.npmjs.com) after the next **Changesets publish**.
131
139
 
132
140
  ## Changesets
package/SECURITY.md CHANGED
@@ -111,7 +111,7 @@ What we treat as in-scope attacker capabilities, and what we don't.
111
111
  | User pasting a Stripe / OpenAI / GitHub PAT into a bug report | ✅ | PII scrubber redacts ~15 vendor token formats client-side before the report leaves the device. Mirrors `packages/core/src/pii-scrubber.ts` across iOS, Android, Flutter, React Native. |
112
112
  | Stolen end-user device with the SDK installed | ⚠️ partial | Offline queue is AsyncStorage / Keychain / SharedPreferences — no app-level encryption beyond the OS default. Reports waiting to flush are vulnerable to a forensic attacker. |
113
113
  | Compromised Supabase service-role key | ❌ | Treated as a tier-0 incident; would require key rotation and audit-log forensics. Not defendable in software. |
114
- | Compromise of `kensaurus@gmail.com` | ❌ | Treated as a project-fork event; downstream consumers should pin to the last known-good version and follow the new release channel. |
114
+ | Compromise of `kensaurus@gmail.com` mail account | ❌ | Treated as a project-fork event; downstream consumers should pin to the last known-good version and follow the new release channel. |
115
115
  | Physical / OS-level attacker on an end-user device | ❌ | Out of scope. |
116
116
  | Malicious fork using the Mushi name to ship malware | ❌ (technical) ✅ (legal) | The MIT / AGPLv3 grant lets the fork exist; the trademark policy (`TRADEMARK.md`) makes shipping it under the Mushi name an infringement we will pursue. |
117
117
 
@@ -157,6 +157,29 @@ Structured fields you set explicitly (`metadata.userEmail`,
157
157
  opt-in attribution data, and silently rewriting them would break
158
158
  support workflows.
159
159
 
160
+ ### CLI funnel telemetry
161
+
162
+ The Mushi CLI sends two fire-and-forget signals to `/v1/cli/funnel` on your
163
+ project's endpoint to help us detect where the onboarding wizard drops off:
164
+
165
+ | Signal | When | What is sent |
166
+ |--------|------|-------------|
167
+ | `wizard_env_written` | After `npx mushi-mushi` writes `.env.local` | `event`, `source: "cli"` |
168
+ | `mcp_setup_done` | After `mushi setup --ide` verifies the MCP key | `event`, `source: "cli"` |
169
+
170
+ No file contents, stack traces, or identifiers beyond your project ID and API
171
+ key are sent. The key is already on the endpoint — this adds no new disclosure.
172
+
173
+ **Opt out** by setting `MUSHI_NO_TELEMETRY=1` in your shell or CI environment:
174
+
175
+ ```bash
176
+ MUSHI_NO_TELEMETRY=1 npx mushi-mushi
177
+ MUSHI_NO_TELEMETRY=1 mushi setup --ide cursor
178
+ ```
179
+
180
+ These signals never block or delay the wizard — they time out silently after
181
+ 4 seconds and errors are discarded.
182
+
160
183
  ### Where data lives
161
184
 
162
185
  - **Reports & telemetry** — Supabase Postgres in the `us-west-1` region.
package/dist/index.cjs CHANGED
@@ -360,6 +360,17 @@ function createApiClient(options) {
360
360
  );
361
361
  },
362
362
  // ─── Rewards program (P1) ──────────────────────────────────
363
+ /** POST /v1/sdk/session — lightweight session lifecycle events.
364
+ * Best-effort (1 retry, no offline queue) — mirrors postDiscoveryEvent. */
365
+ async postSessionEvent(payload) {
366
+ return request(
367
+ "POST",
368
+ "/v1/sdk/session",
369
+ payload,
370
+ 1,
371
+ "discovery"
372
+ );
373
+ },
363
374
  async submitActivity(userId, events, opts) {
364
375
  return request(
365
376
  "POST",
@@ -1169,12 +1180,12 @@ function createOfflineQueue(config = {}) {
1169
1180
  const n = row.attempts;
1170
1181
  return typeof n === "number" && Number.isFinite(n) ? n : 0;
1171
1182
  }
1172
- function isExpired(row, now) {
1183
+ function isExpired(row, now2) {
1173
1184
  const queuedAt = row.queuedAt;
1174
1185
  if (!queuedAt) return false;
1175
1186
  const ts = Date.parse(queuedAt);
1176
1187
  if (Number.isNaN(ts)) return false;
1177
- return now - ts > MAX_QUEUE_AGE_MS;
1188
+ return now2 - ts > MAX_QUEUE_AGE_MS;
1178
1189
  }
1179
1190
  function detectBackend() {
1180
1191
  if (backendType) return backendType;
@@ -1353,10 +1364,10 @@ function createOfflineQueue(config = {}) {
1353
1364
  } else {
1354
1365
  rows = lsRead();
1355
1366
  }
1356
- const now = Date.now();
1367
+ const now2 = Date.now();
1357
1368
  const fresh = [];
1358
1369
  for (const row of rows) {
1359
- if (isExpired(row, now)) {
1370
+ if (isExpired(row, now2)) {
1360
1371
  const rowId = row.id;
1361
1372
  await removeRow(backend, rowId);
1362
1373
  queueLog.debug("Offline queue: evicting stale report", { id: rowId });
@@ -1821,6 +1832,111 @@ function randomSuffix() {
1821
1832
  return n.toString(36).padStart(6, "0").slice(0, 6);
1822
1833
  }
1823
1834
 
1835
+ // src/session-tracker.ts
1836
+ var HEARTBEAT_INTERVAL_MS = 6e4;
1837
+ var _client = null;
1838
+ var _sdkVersion;
1839
+ var _userIdHash = null;
1840
+ var _reporterTokenHash = null;
1841
+ var _pageViewCount = 0;
1842
+ var _heartbeatTimer = null;
1843
+ var _initialized = false;
1844
+ function now() {
1845
+ return (/* @__PURE__ */ new Date()).toISOString();
1846
+ }
1847
+ function currentRoute() {
1848
+ if (typeof window === "undefined") return "";
1849
+ return window.location.pathname + window.location.search;
1850
+ }
1851
+ function userAgent() {
1852
+ if (typeof navigator === "undefined") return null;
1853
+ return navigator.userAgent ?? null;
1854
+ }
1855
+ function buildPayload(kind, extra) {
1856
+ return {
1857
+ kind,
1858
+ session_id: getSessionId(),
1859
+ ts: now(),
1860
+ page_view_count: _pageViewCount,
1861
+ reporter_token_hash: _reporterTokenHash,
1862
+ user_id_hash: _userIdHash,
1863
+ user_agent: userAgent(),
1864
+ sdk_version: _sdkVersion,
1865
+ ...extra
1866
+ };
1867
+ }
1868
+ function send(payload) {
1869
+ if (!_client) return;
1870
+ _client.postSessionEvent(payload).catch(() => {
1871
+ });
1872
+ }
1873
+ function initSessionTracker(opts) {
1874
+ if (_initialized || typeof window === "undefined") return;
1875
+ _initialized = true;
1876
+ _client = opts.client;
1877
+ _sdkVersion = opts.sdkVersion;
1878
+ _reporterTokenHash = opts.reporterTokenHash ?? null;
1879
+ _userIdHash = opts.userIdHash ?? null;
1880
+ const entryRoute = currentRoute();
1881
+ _pageViewCount = 1;
1882
+ send(buildPayload("session_start", {
1883
+ route: entryRoute,
1884
+ referrer: typeof document !== "undefined" ? document.referrer ?? null : null
1885
+ }));
1886
+ _heartbeatTimer = setInterval(() => {
1887
+ send(buildPayload("session_heartbeat", { route: currentRoute() }));
1888
+ }, HEARTBEAT_INTERVAL_MS);
1889
+ const onHide = () => {
1890
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
1891
+ send(buildPayload("session_end", { route: currentRoute() }));
1892
+ }
1893
+ };
1894
+ if (typeof document !== "undefined") {
1895
+ document.addEventListener("visibilitychange", onHide, { passive: true });
1896
+ }
1897
+ if (typeof window !== "undefined") {
1898
+ window.addEventListener("pagehide", () => {
1899
+ send(buildPayload("session_end", { route: currentRoute() }));
1900
+ }, { passive: true });
1901
+ }
1902
+ patchHistoryForPageViews();
1903
+ }
1904
+ function trackPageView(route) {
1905
+ if (!_initialized || !_client) return;
1906
+ _pageViewCount += 1;
1907
+ send(buildPayload("page_view", { route: route ?? currentRoute() }));
1908
+ }
1909
+ function updateSessionIdentity(userIdHash) {
1910
+ _userIdHash = userIdHash;
1911
+ }
1912
+ function destroySessionTracker() {
1913
+ if (_heartbeatTimer != null) {
1914
+ clearInterval(_heartbeatTimer);
1915
+ _heartbeatTimer = null;
1916
+ }
1917
+ _initialized = false;
1918
+ _client = null;
1919
+ }
1920
+ var _historyPatched = false;
1921
+ function patchHistoryForPageViews() {
1922
+ if (_historyPatched || typeof history === "undefined") return;
1923
+ _historyPatched = true;
1924
+ const wrap = (original) => function(...args) {
1925
+ const result = original.apply(this, args);
1926
+ if (original === history.pushState) {
1927
+ _pageViewCount += 1;
1928
+ send(buildPayload("page_view", { route: currentRoute() }));
1929
+ }
1930
+ return result;
1931
+ };
1932
+ history.pushState = wrap(history.pushState);
1933
+ history.replaceState = wrap(history.replaceState);
1934
+ window.addEventListener("popstate", () => {
1935
+ _pageViewCount += 1;
1936
+ send(buildPayload("page_view", { route: currentRoute() }));
1937
+ }, { passive: true });
1938
+ }
1939
+
1824
1940
  // src/rate-limiter.ts
1825
1941
  var DEFAULT_MAX_BURST = 10;
1826
1942
  var DEFAULT_REFILL_RATE = 1;
@@ -1834,12 +1950,12 @@ function createRateLimiter(config = {}) {
1834
1950
  let tokens = maxBurst;
1835
1951
  let lastRefill = Date.now();
1836
1952
  function refill() {
1837
- const now = Date.now();
1838
- const elapsed = now - lastRefill;
1953
+ const now2 = Date.now();
1954
+ const elapsed = now2 - lastRefill;
1839
1955
  const refills = Math.floor(elapsed / refillIntervalMs);
1840
1956
  if (refills > 0) {
1841
1957
  tokens = Math.min(maxBurst, tokens + refills * refillRate);
1842
- lastRefill = now;
1958
+ lastRefill = now2;
1843
1959
  }
1844
1960
  }
1845
1961
  function tryConsume() {
@@ -2153,15 +2269,15 @@ function mushiTokens(mode) {
2153
2269
  // src/identity.ts
2154
2270
  var MUSHI_IDENTITY_TOKEN_PREFIX = "mushi.identity";
2155
2271
  function buildIdentityClaims(input) {
2156
- const now = Math.floor(Date.now() / 1e3);
2272
+ const now2 = Math.floor(Date.now() / 1e3);
2157
2273
  const ttl = input.ttlSeconds && input.ttlSeconds > 0 ? Math.min(input.ttlSeconds, 3600) : 600;
2158
2274
  return {
2159
2275
  projectId: input.projectId,
2160
2276
  sub: input.userId,
2161
2277
  ...input.email ? { email: input.email } : {},
2162
2278
  ...input.name ? { name: input.name } : {},
2163
- iat: now,
2164
- exp: now + ttl
2279
+ iat: now2,
2280
+ exp: now2 + ttl
2165
2281
  };
2166
2282
  }
2167
2283
  function base64UrlDecode(segment) {
@@ -2200,12 +2316,12 @@ var MULTI_TENANT_PARENT_HOSTS = /* @__PURE__ */ new Set(["kensaur.us", "www.kens
2200
2316
  var SLUG_DOMAIN_HINTS = {
2201
2317
  "glot-it": "glot.it",
2202
2318
  glotit: "glot.it",
2203
- "solo-boss-cloud": "sbc-front.vercel.app",
2319
+ "solo-boss-cloud": "solo-boss.kensaur.us",
2204
2320
  "mushi-mushi": "kensaur.us"
2205
2321
  };
2206
2322
  var SLUG_ICON_URL_HINTS = {
2207
2323
  "mushi-mushi": "https://kensaur.us/mushi-mushi/admin/favicon.svg",
2208
- "solo-boss-cloud": "https://sbc-front.vercel.app/favicon.svg",
2324
+ "solo-boss-cloud": "https://solo-boss.kensaur.us/favicon.svg",
2209
2325
  "glot-it": "https://kensaur.us/glot-it/apple-touch-icon.png",
2210
2326
  glotit: "https://kensaur.us/glot-it/apple-touch-icon.png"
2211
2327
  };
@@ -2419,6 +2535,7 @@ exports.createOfflineQueue = createOfflineQueue;
2419
2535
  exports.createPiiScrubber = createPiiScrubber;
2420
2536
  exports.createPreFilter = createPreFilter;
2421
2537
  exports.createRateLimiter = createRateLimiter;
2538
+ exports.destroySessionTracker = destroySessionTracker;
2422
2539
  exports.diagnoseEnvConfig = diagnoseEnvConfig;
2423
2540
  exports.estimateJsonBytes = estimateJsonBytes;
2424
2541
  exports.expandPreset = expandPreset;
@@ -2429,6 +2546,7 @@ exports.getReporterToken = getReporterToken;
2429
2546
  exports.getSessionId = getSessionId;
2430
2547
  exports.githubRepoDomainHint = githubRepoDomainHint;
2431
2548
  exports.hmacSha256Hex = hmacSha256Hex;
2549
+ exports.initSessionTracker = initSessionTracker;
2432
2550
  exports.isLikelyGenericFavicon = isLikelyGenericFavicon;
2433
2551
  exports.isUntrustedFaviconUrl = isUntrustedFaviconUrl;
2434
2552
  exports.mushiPalette = mushiPalette;
@@ -2450,6 +2568,8 @@ exports.resolveWidgetAccent = resolveWidgetAccent;
2450
2568
  exports.safeWidgetHex = safeWidgetHex;
2451
2569
  exports.scrubPii = scrubPii;
2452
2570
  exports.sha256Hex = sha256Hex;
2571
+ exports.trackPageView = trackPageView;
2572
+ exports.updateSessionIdentity = updateSessionIdentity;
2453
2573
  exports.validateConfig = validateConfig;
2454
2574
  //# sourceMappingURL=index.cjs.map
2455
2575
  //# sourceMappingURL=index.cjs.map