@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 +10 -2
- package/SECURITY.md +24 -1
- package/dist/index.cjs +132 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +69 -1
- package/dist/index.d.ts +69 -1
- package/dist/index.js +129 -13
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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,
|
|
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
|
|
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
|
|
1367
|
+
const now2 = Date.now();
|
|
1357
1368
|
const fresh = [];
|
|
1358
1369
|
for (const row of rows) {
|
|
1359
|
-
if (isExpired(row,
|
|
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
|
|
1838
|
-
const elapsed =
|
|
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 =
|
|
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
|
|
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:
|
|
2164
|
-
exp:
|
|
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": "
|
|
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://
|
|
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
|