@mushi-mushi/core 0.9.0 → 1.1.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 +27 -0
- package/README.md +7 -1
- package/SECURITY.md +167 -4
- package/dist/index.cjs +244 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +497 -2
- package/dist/index.d.ts +497 -2
- package/dist/index.js +243 -2
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/CONTRIBUTING.md
CHANGED
|
@@ -91,6 +91,33 @@ pnpm changeset
|
|
|
91
91
|
|
|
92
92
|
Select the affected packages, the semver bump type, and write a summary. The changeset file gets committed with your PR.
|
|
93
93
|
|
|
94
|
+
## Release flow
|
|
95
|
+
|
|
96
|
+
Releases are fully automated. Maintainers don't run `npm publish` by hand.
|
|
97
|
+
|
|
98
|
+
1. PRs land on `master` with one or more changeset files in `.changeset/`.
|
|
99
|
+
2. `release.yml` runs on every push to `master`. It opens (or updates) a `chore: version packages` PR that bumps every affected `package.json`, rolls up the changelogs, and deletes the consumed changesets.
|
|
100
|
+
3. Merging that "Version Packages" PR re-fires `release.yml`. The publish step authenticates to npm via **OpenID Connect (OIDC) Trusted Publishers** — no long-lived `NPM_TOKEN` is exchanged — and every tarball ships with a **Sigstore provenance attestation** uploaded to the public transparency log.
|
|
101
|
+
|
|
102
|
+
If GitHub's anti-loop protection suppresses the auto re-fire (the squash merge can be attributed to `github-actions[bot]`), trigger the workflow manually: **Actions → release → Run workflow → master**.
|
|
103
|
+
|
|
104
|
+
### Adding a brand-new publishable package
|
|
105
|
+
|
|
106
|
+
Trusted Publisher bindings are configured **per package** on `npmjs.com` and require the package to already exist on the registry. New packages therefore need a one-time bootstrap before OIDC can take over.
|
|
107
|
+
|
|
108
|
+
1. Add the package under `packages/<name>/` with a real `version`, `files`, `publishConfig.access: "public"`, `LICENSE`, and the standard fields enforced by `pnpm check:publish-readiness`.
|
|
109
|
+
2. Build it locally: `pnpm install && pnpm -r build`.
|
|
110
|
+
3. Mint a short-lived granular access token at `https://www.npmjs.com/settings/<your-user>/tokens/granular-access-tokens/new` — **Bypass 2FA: ON**, **Read and write: All packages**, **Expiration: 7 days**.
|
|
111
|
+
4. Bootstrap-publish:
|
|
112
|
+
```bash
|
|
113
|
+
NPM_TOKEN=npm_xxx pnpm bootstrap:new-package
|
|
114
|
+
```
|
|
115
|
+
The script auto-detects which workspace packages are missing on npm and publishes them via `pnpm publish --no-provenance` (so `workspace:^` specifiers get rewritten to real semver in the tarball).
|
|
116
|
+
5. The script prints one URL per freshly-published package. Open each, click **GitHub Actions** under "Trusted Publisher", confirm the auto-filled fields (`<owner>` / `<repo>` / `release.yml`), and tap your security key.
|
|
117
|
+
6. Revoke the bootstrap token at `https://www.npmjs.com/settings/<your-user>/tokens`.
|
|
118
|
+
|
|
119
|
+
From the next changeset bump onward, that package publishes through the normal `release.yml` flow with full OIDC provenance — same as the rest.
|
|
120
|
+
|
|
94
121
|
## Code Style
|
|
95
122
|
|
|
96
123
|
- **TypeScript strict mode** — no `any` unless absolutely necessary
|
package/README.md
CHANGED
|
@@ -10,10 +10,12 @@ Core types, API client, and utilities for the Mushi Mushi SDK.
|
|
|
10
10
|
- **API Client**: Fetch-based HTTP client with retry and exponential backoff. Tags every internal request with `X-Mushi-Internal: <kind>` so framework SDKs can filter their own traffic out of network capture and `apiCascade`. Ships HMAC-signed reporter helpers (`getLatestSdkVersion`, `listReporterReports`, `listReporterComments`, `replyToReporterReport`) for the two-way reply pipeline, plus `postDiscoveryEvent` (v2.1) for the passive inventory channel
|
|
11
11
|
- **Pre-Filter**: On-device Stage 0 spam/gibberish filter (runs client-side, zero server cost)
|
|
12
12
|
- **Offline Queue**: IndexedDB-backed queue with auto-sync on reconnect
|
|
13
|
-
- **Environment Capture**: Browser/device snapshot
|
|
13
|
+
- **Environment Capture**: Browser/device snapshot — viewport, user agent (with **User-Agent Client Hints** when supported), connection info, screen + DPR, accessibility prefs (`prefers-reduced-motion`, `prefers-color-scheme`, `prefers-contrast`), online/displayMode/title, and a one-shot **page load timing** read from `PerformanceNavigationTiming` (TTFB, DOMContentLoaded, FCP, LCP)
|
|
14
14
|
- **Reporter Token**: Anonymous persistent identity for report attribution
|
|
15
15
|
- **Session ID**: Tab-scoped session correlation
|
|
16
16
|
- **Rate Limiter**: Token bucket self-throttle to prevent API flooding
|
|
17
|
+
- **Breadcrumb Buffer** (1.0+ — `createBreadcrumbBuffer`): 50-entry ring of `{timestamp, category, level, message, data?}` rows; framework SDKs auto-capture route changes, `console.error/warn`, `[data-testid]` clicks, and SDK lifecycle events. Snapshot is attached to every `MushiReport` (server promotes it to a dedicated `reports.breadcrumbs` jsonb column for GIN-indexed filtering)
|
|
18
|
+
- **Exception Normaliser** (1.0+ — `normaliseThrown`): turns any thrown value (`Error`, string, plain object, `null`, `undefined`) into a stable `{ name, message, stack?, cause? }` shape with truncated stacks and cyclic-cause guards. Powers `Mushi.captureException()` in `@mushi-mushi/web`
|
|
17
19
|
|
|
18
20
|
## Public types added in 0.7 → 0.11
|
|
19
21
|
|
|
@@ -31,6 +33,10 @@ Core types, API client, and utilities for the Mushi Mushi SDK.
|
|
|
31
33
|
| `MushiReporterComment` | Reporter-facing comment row (HMAC-authed) tagged `author_kind: 'admin' \| 'reporter'`. |
|
|
32
34
|
| `MushiDiscoverInventoryConfig` | Mushi v2.1 — fine-grained controls for `capture.discoverInventory` (`enabled`, `throttleMs`, `routeTemplates`, `userIdSource`, `captureDomSummary`). Pass `true` for defaults. |
|
|
33
35
|
| `MushiDiscoveryEventPayload` | Mushi v2.1 — wire shape for `POST /v1/sdk/discovery`. Mirrored server-side by `_shared/schemas.ts::discoveryEventSchema`; route + page title + testids + network paths + query-param **keys only** + sha256 user id hash. |
|
|
36
|
+
| `MushiBreadcrumb` | 1.0+ — `{ timestamp, category, level: 'debug' \| 'info' \| 'warning' \| 'error' \| 'critical', message, data? }`. Mirrors the Sentry breadcrumb shape so the admin can interleave Mushi + Sentry breadcrumbs on one timeline. |
|
|
37
|
+
| `MushiSentryContext` | 1.0+ — rich Sentry handshake the SDK captures via `@sentry/browser` v7/v8/v9: `eventId`, `replayId`, `traceId`, `spanId`, `transaction`, `release`, `environment`, `user`, `tags`, `breadcrumbs`, `issueUrl`, `mushiReportId` (bidirectional). |
|
|
38
|
+
| `MushiCaptureExceptionOptions` | 1.0+ — options for `Mushi.captureException(err, opts)`: `level`, `tags`, `extras`, `category`, `userIntent` overrides for the structured report. |
|
|
39
|
+
| `NormalisedException` | 1.0+ — return type of `normaliseThrown(err)` (`{ name, message, stack?, cause? }`); used internally by `captureException` and exposed for adapters that want to ship their own thin wrappers. |
|
|
34
40
|
|
|
35
41
|
Constants: `MUSHI_INTERNAL_HEADER` (`'X-Mushi-Internal'`),
|
|
36
42
|
`MUSHI_INTERNAL_INIT_MARKER`, and the `MushiInternalRequestKind` literal union
|
package/SECURITY.md
CHANGED
|
@@ -19,15 +19,59 @@ If you discover a security vulnerability, please report it responsibly.
|
|
|
19
19
|
|
|
20
20
|
**Do NOT open a public GitHub issue.**
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
Use either channel below:
|
|
23
|
+
|
|
24
|
+
1. **GitHub Private Vulnerability Reporting** — strongly preferred.
|
|
25
|
+
<https://github.com/kensaurus/mushi-mushi/security/advisories/new>
|
|
26
|
+
Routes the report into a private advisory with built-in CVE issuance,
|
|
27
|
+
patch coordination, and contributor-credit workflow.
|
|
28
|
+
2. **Email** — `kensaurus@gmail.com`, subject prefix `[mushi-security]`.
|
|
29
|
+
PGP welcome but not required.
|
|
23
30
|
|
|
24
31
|
Include:
|
|
25
32
|
- Description of the vulnerability
|
|
26
|
-
- Steps to reproduce
|
|
27
|
-
- Impact assessment
|
|
33
|
+
- Steps to reproduce (smallest reproducer wins)
|
|
34
|
+
- Impact assessment (what an attacker gains)
|
|
28
35
|
- Suggested fix (if any)
|
|
36
|
+
- Whether you want public credit (and how to spell your name)
|
|
37
|
+
|
|
38
|
+
### Coordinated-disclosure timeline
|
|
39
|
+
|
|
40
|
+
| Day | Action |
|
|
41
|
+
|-----|--------|
|
|
42
|
+
| 0 | Report received |
|
|
43
|
+
| ≤ 2 | Acknowledgment + assigned a tracking ID |
|
|
44
|
+
| ≤ 7 | Triage complete: severity assigned (CVSS 3.1) and target patch date communicated |
|
|
45
|
+
| ≤ 30 | Patch released for critical / high (CVSS ≥ 7.0); ≤ 60 days for medium; best-effort for low |
|
|
46
|
+
| Patch + 7 | Public advisory + CVE published; reporter credited unless they declined |
|
|
47
|
+
| Patch + 90 | Embargo expires regardless; if upstream is unresponsive, the reporter is free to publish |
|
|
48
|
+
|
|
49
|
+
### Safe harbor
|
|
50
|
+
|
|
51
|
+
Good-faith security research on Mushi Mushi is welcome. If you stay
|
|
52
|
+
within the rules below, we will not pursue legal action, will not ask
|
|
53
|
+
your hosting provider to take you offline, and will publicly credit your
|
|
54
|
+
work:
|
|
55
|
+
|
|
56
|
+
- Test only against your own self-hosted instance, the public demo at
|
|
57
|
+
<https://kensaur.us/mushi-mushi/admin/>, or accounts you own.
|
|
58
|
+
- Do not access, exfiltrate, or modify data belonging to other users.
|
|
59
|
+
- Do not run automated scanning that affects availability for others
|
|
60
|
+
(rate-limit your tooling, exclude `/health`).
|
|
61
|
+
- Disclose privately first (channels above); do not publish before the
|
|
62
|
+
embargo above expires.
|
|
63
|
+
- Do not intentionally exploit a finding to escalate beyond proving it
|
|
64
|
+
exists.
|
|
65
|
+
|
|
66
|
+
If a finding requires touching production data to confirm, **stop and
|
|
67
|
+
ask first** — describe what you'd need to do and we'll spin up a sandbox.
|
|
68
|
+
|
|
69
|
+
### Hall of fame
|
|
29
70
|
|
|
30
|
-
|
|
71
|
+
Researchers who report a confirmed vulnerability are credited in the
|
|
72
|
+
release notes for the patched version and added to
|
|
73
|
+
[`docs/SECURITY_HALL_OF_FAME.md`](./docs/SECURITY_HALL_OF_FAME.md) (with
|
|
74
|
+
permission).
|
|
31
75
|
|
|
32
76
|
## Scope
|
|
33
77
|
|
|
@@ -48,6 +92,125 @@ We will acknowledge receipt within 48 hours and aim to release a patch within 7
|
|
|
48
92
|
- **Rotate API keys** regularly via the admin console
|
|
49
93
|
- **Enable SSO** for team projects (Enterprise tier)
|
|
50
94
|
- **Review audit logs** periodically for suspicious activity
|
|
95
|
+
- **Verify SDK integrity** with `npm audit signatures` after install
|
|
96
|
+
- **Set `Content-Security-Policy`** on any page embedding the Mushi widget;
|
|
97
|
+
the widget itself ships with `script-src 'self'` and does not load
|
|
98
|
+
remote scripts.
|
|
99
|
+
|
|
100
|
+
## Threat model
|
|
101
|
+
|
|
102
|
+
What we treat as in-scope attacker capabilities, and what we don't.
|
|
103
|
+
|
|
104
|
+
| Capability | In scope | Notes |
|
|
105
|
+
|-----------|----------|-------|
|
|
106
|
+
| Unauthenticated network attacker hitting public endpoints | ✅ | Rate-limit + HMAC + replay protection on every webhook endpoint (`packages/server/supabase/functions/_shared/webhook-middleware.ts`). |
|
|
107
|
+
| Authenticated user trying to read another tenant's data | ✅ | Postgres RLS on every `public.*` table; advisor lints reviewed monthly. |
|
|
108
|
+
| Authenticated user trying to escalate to super-admin | ✅ | Role lives in `auth.users.raw_app_meta_data.role`; cannot be self-edited via PostgREST. |
|
|
109
|
+
| Compromised dependency (npm supply-chain attack) | ✅ | 7-day cooldown + provenance + Harden-Runner + pinned SHAs (see "Supply-chain hardening" below). |
|
|
110
|
+
| Stolen API key | ✅ | Per-key scopes (`api_key_has_scope`), revocation via admin console, audit log of every use. |
|
|
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
|
+
| 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
|
+
| 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. |
|
|
115
|
+
| Physical / OS-level attacker on an end-user device | ❌ | Out of scope. |
|
|
116
|
+
| Malicious fork using the Mushi name to ship malware | ❌ (technical) ✅ (legal) | The MIT/BSL grant lets the fork exist; the trademark policy (`TRADEMARK.md`) makes shipping it under the Mushi name an infringement we will pursue. |
|
|
117
|
+
|
|
118
|
+
## Data handling and PII
|
|
119
|
+
|
|
120
|
+
### What the SDK collects by default
|
|
121
|
+
|
|
122
|
+
| Field | Scope | PII risk |
|
|
123
|
+
|-------|-------|----------|
|
|
124
|
+
| URL / route the user was on | Always | Low — strip query strings if your routes encode user IDs. |
|
|
125
|
+
| Browser / OS / device | Always | None |
|
|
126
|
+
| Console errors (last 50) | Opt-in via `captureConsole: true` | Medium — can include user data your code logs. |
|
|
127
|
+
| Network failures (URL + status) | Opt-in via `captureNetwork: true` | Medium — query params logged as-is unless you redact in-app. |
|
|
128
|
+
| User id / email / role | Only if you call `setUser()` | High — only set what you need; we do not auto-discover. |
|
|
129
|
+
| Session replay frames | Off by default | High — handled by the masking layer; passwords / cards / opted-out elements never leave the page. |
|
|
130
|
+
| Free-text bug description | Always | Medium — passed through the PII scrubber (see below). |
|
|
131
|
+
|
|
132
|
+
### What the PII scrubber redacts before send
|
|
133
|
+
|
|
134
|
+
Implemented identically across `@mushi-mushi/core`, the iOS, Android,
|
|
135
|
+
Flutter, and React Native SDKs. Defaults are below — every category can
|
|
136
|
+
be toggled off, but `secretTokens` is on by default and we recommend
|
|
137
|
+
keeping it that way.
|
|
138
|
+
|
|
139
|
+
| Category | Default | Patterns |
|
|
140
|
+
|----------|---------|----------|
|
|
141
|
+
| `ssns` | on | `123-45-6789` |
|
|
142
|
+
| `creditCards` | on | 12–19 digit Luhn-shaped sequences with optional separators |
|
|
143
|
+
| `secretTokens` | on | AWS access key (`AKIA…` / `ASIA…`), AWS secret (`aws_secret_access_key=…`), Stripe (`sk_live_…`, `sk_test_…`, `rk_…`, `pk_…`), Slack (`xox[abpor]-…`), GitHub PAT (`ghp_…`, `github_pat_…`), OpenAI (`sk-…`, `sk-proj-…`), Anthropic (`sk-ant-…`), Google API (`AIza…`), JWT (`eyJ…` 3-segment) |
|
|
144
|
+
| `emails` | on | RFC-5322 lite |
|
|
145
|
+
| `phones` | on | E.164 with optional country code |
|
|
146
|
+
| `ipAddresses` | off | IPv4 (off because internal IPs are usually not PII and noise hurts triage) |
|
|
147
|
+
| `ipv6` | off | Same |
|
|
148
|
+
|
|
149
|
+
The fields scrubbed are:
|
|
150
|
+
|
|
151
|
+
- `description` — primary free-text field of every bug report
|
|
152
|
+
- `summary` — short summary, in the same composer
|
|
153
|
+
- `breadcrumbs[].message` — auto-captured user-action trail (clicks, route changes, console messages)
|
|
154
|
+
|
|
155
|
+
Structured fields you set explicitly (`metadata.userEmail`,
|
|
156
|
+
`metadata.userId`, etc.) are intentionally **not** scrubbed — those are
|
|
157
|
+
opt-in attribution data, and silently rewriting them would break
|
|
158
|
+
support workflows.
|
|
159
|
+
|
|
160
|
+
### Where data lives
|
|
161
|
+
|
|
162
|
+
- **Reports & telemetry** — Supabase Postgres in the `us-west-1` region.
|
|
163
|
+
- **Session replays** — Supabase Storage, same region. Lifecycle policy
|
|
164
|
+
trims replays older than 30 days unless explicitly retained from the
|
|
165
|
+
admin console.
|
|
166
|
+
- **Inbound webhook bodies** — only a SHA-256 hash + `delivery_id` of
|
|
167
|
+
the body is persisted (`webhook_audit_log`). The full body is
|
|
168
|
+
processed in memory and discarded.
|
|
169
|
+
- **Outbound integrations** (Slack, Jira, …) — Mushi is a sender only;
|
|
170
|
+
the receiving system's retention applies.
|
|
171
|
+
|
|
172
|
+
### Encryption
|
|
173
|
+
|
|
174
|
+
| Surface | At rest | In transit |
|
|
175
|
+
|---------|---------|------------|
|
|
176
|
+
| Postgres (Supabase) | AES-256 (Supabase default) | TLS 1.2+ |
|
|
177
|
+
| Supabase Storage (replays) | AES-256 | TLS 1.2+ |
|
|
178
|
+
| Edge Function ↔ Postgres | — | TLS via the Supavisor pooler |
|
|
179
|
+
| SDK ↔ ingest endpoint | — | TLS 1.2+ enforced; HSTS preload on `kensaur.us` |
|
|
180
|
+
| Inbound webhooks | — | TLS terminated at CloudFront / Supabase edge |
|
|
181
|
+
| Audit log integrity | append-only by RLS; no in-row signing | — |
|
|
182
|
+
|
|
183
|
+
### Cryptographic primitives
|
|
184
|
+
|
|
185
|
+
| Use | Algorithm | Implementation |
|
|
186
|
+
|-----|-----------|---------------|
|
|
187
|
+
| Webhook HMAC verification (Sentry, GitHub, Datadog, Honeycomb, Grafana, Bugsnag, Rollbar, Crashlytics) | HMAC-SHA256, constant-time compare | Web Crypto in Deno; `crypto.subtle.timingSafeEqual` analogue |
|
|
188
|
+
| AWS SNS subscription confirmation | RSA-SHA1 / RSA-SHA256 | Deno `crypto.subtle.verify` with the cert from `SigningCertURL` (URL allow-listed to `*.sns.*.amazonaws.com`) |
|
|
189
|
+
| Opsgenie JWT shared-token | HS256 with `aud` claim verification | `jose` (Deno-compatible) |
|
|
190
|
+
| API-key hashing (database) | SHA-256 prefix + bcrypt secret half | `pgcrypto` |
|
|
191
|
+
| Provenance attestations (npm) | Sigstore (Fulcio + Rekor) | `npm publish --provenance` |
|
|
192
|
+
|
|
193
|
+
We deliberately do not roll our own crypto. If you find an algorithm or
|
|
194
|
+
library above that has been deprecated, please file a security advisory.
|
|
195
|
+
|
|
196
|
+
### Operator security checklist
|
|
197
|
+
|
|
198
|
+
When you provision a new self-hosted Mushi instance:
|
|
199
|
+
|
|
200
|
+
- [ ] Set `auth_leaked_password_protection = true` in Supabase Auth
|
|
201
|
+
(HaveIBeenPwned blocklist; flagged as `auth_leaked_password_protection`
|
|
202
|
+
in the security advisor).
|
|
203
|
+
- [ ] Enable at least two MFA factors in Supabase Auth (`auth_insufficient_mfa_options`).
|
|
204
|
+
- [ ] Rotate the service-role key on day 1, then quarterly.
|
|
205
|
+
- [ ] Restrict Postgres direct connections to your CI / migration runners
|
|
206
|
+
via Supabase network restrictions.
|
|
207
|
+
- [ ] Run `pnpm dlx supabase advisors --project-ref <ref>` after every
|
|
208
|
+
migration; aim for zero ERROR-level findings.
|
|
209
|
+
- [ ] Configure a Supabase log drain to your SIEM if you are subject to
|
|
210
|
+
SOC 2 / ISO 27001.
|
|
211
|
+
- [ ] Set CSP `frame-ancestors` on the host page if you embed the Mushi
|
|
212
|
+
widget (the widget is iframe-friendly but does not enforce
|
|
213
|
+
framing constraints itself).
|
|
51
214
|
|
|
52
215
|
## Supply-chain hardening (how this package is protected)
|
|
53
216
|
|
package/dist/index.cjs
CHANGED
|
@@ -147,6 +147,53 @@ function createApiClient(options) {
|
|
|
147
147
|
reporterToken,
|
|
148
148
|
{ body }
|
|
149
149
|
);
|
|
150
|
+
},
|
|
151
|
+
// ─── Rewards program (P1) ──────────────────────────────────
|
|
152
|
+
async submitActivity(userId, events, opts) {
|
|
153
|
+
return request(
|
|
154
|
+
"POST",
|
|
155
|
+
"/v1/sdk/activity",
|
|
156
|
+
{
|
|
157
|
+
user_id: userId,
|
|
158
|
+
user_traits: opts?.userTraits,
|
|
159
|
+
opted_in: opts?.optedIn,
|
|
160
|
+
reporter_token_hash: opts?.reporterTokenHash,
|
|
161
|
+
// P2: JWT for monetary verification; omitted when null
|
|
162
|
+
...opts?.hostJwt ? { host_jwt: opts.hostJwt } : {},
|
|
163
|
+
events
|
|
164
|
+
},
|
|
165
|
+
1,
|
|
166
|
+
// best-effort, 1 retry
|
|
167
|
+
"discovery"
|
|
168
|
+
);
|
|
169
|
+
},
|
|
170
|
+
async getMyPoints(userId) {
|
|
171
|
+
return request(
|
|
172
|
+
"GET",
|
|
173
|
+
`/v1/sdk/me/points?userId=${encodeURIComponent(userId)}`,
|
|
174
|
+
void 0,
|
|
175
|
+
1,
|
|
176
|
+
"reporter-poll"
|
|
177
|
+
);
|
|
178
|
+
},
|
|
179
|
+
async getMyTier(userId) {
|
|
180
|
+
return request(
|
|
181
|
+
"GET",
|
|
182
|
+
`/v1/sdk/me/tier?userId=${encodeURIComponent(userId)}`,
|
|
183
|
+
void 0,
|
|
184
|
+
1,
|
|
185
|
+
"reporter-poll"
|
|
186
|
+
);
|
|
187
|
+
},
|
|
188
|
+
async getMyHistory(userId, opts) {
|
|
189
|
+
const qs = new URLSearchParams({ userId, ...opts?.limit ? { limit: String(opts.limit) } : {} });
|
|
190
|
+
return request(
|
|
191
|
+
"GET",
|
|
192
|
+
`/v1/sdk/me/history?${qs}`,
|
|
193
|
+
void 0,
|
|
194
|
+
1,
|
|
195
|
+
"reporter-poll"
|
|
196
|
+
);
|
|
150
197
|
}
|
|
151
198
|
};
|
|
152
199
|
}
|
|
@@ -785,6 +832,8 @@ function captureEnvironment() {
|
|
|
785
832
|
const nav = typeof navigator !== "undefined" ? navigator : void 0;
|
|
786
833
|
const win = typeof window !== "undefined" ? window : void 0;
|
|
787
834
|
const doc = typeof document !== "undefined" ? document : void 0;
|
|
835
|
+
const scr = typeof screen !== "undefined" ? screen : void 0;
|
|
836
|
+
void kickOffUserAgentData(nav);
|
|
788
837
|
const connection = nav && "connection" in nav ? nav.connection : void 0;
|
|
789
838
|
return {
|
|
790
839
|
userAgent: nav?.userAgent ?? "unknown",
|
|
@@ -806,7 +855,19 @@ function captureEnvironment() {
|
|
|
806
855
|
deviceMemory: nav?.deviceMemory,
|
|
807
856
|
hardwareConcurrency: nav?.hardwareConcurrency,
|
|
808
857
|
route: win?.location?.pathname,
|
|
809
|
-
nearestTestid: findNearestTestidFromActive(doc)
|
|
858
|
+
nearestTestid: findNearestTestidFromActive(doc),
|
|
859
|
+
userAgentData: captureUserAgentData(nav),
|
|
860
|
+
screen: captureScreen(scr, win),
|
|
861
|
+
prefersColorScheme: matchScheme(win),
|
|
862
|
+
prefersReducedMotion: matchMedia(win, "(prefers-reduced-motion: reduce)"),
|
|
863
|
+
prefersReducedData: matchMedia(win, "(prefers-reduced-data: reduce)"),
|
|
864
|
+
prefersContrast: matchContrast(win),
|
|
865
|
+
forcedColors: matchMedia(win, "(forced-colors: active)"),
|
|
866
|
+
online: typeof nav?.onLine === "boolean" ? nav.onLine : void 0,
|
|
867
|
+
displayMode: matchDisplayMode(win),
|
|
868
|
+
documentTitle: doc?.title?.slice(0, 200),
|
|
869
|
+
buildId: readBuildIdMeta(doc),
|
|
870
|
+
pageLoadTiming: capturePageLoadTiming(win)
|
|
810
871
|
};
|
|
811
872
|
}
|
|
812
873
|
function findNearestTestidFromActive(doc) {
|
|
@@ -821,6 +882,117 @@ function findNearestTestidFromActive(doc) {
|
|
|
821
882
|
}
|
|
822
883
|
return void 0;
|
|
823
884
|
}
|
|
885
|
+
var cachedHighEntropy = null;
|
|
886
|
+
var highEntropyKickedOff = false;
|
|
887
|
+
function kickOffUserAgentData(nav) {
|
|
888
|
+
if (highEntropyKickedOff) return;
|
|
889
|
+
const ua = nav?.userAgentData;
|
|
890
|
+
if (!ua?.getHighEntropyValues) return;
|
|
891
|
+
highEntropyKickedOff = true;
|
|
892
|
+
ua.getHighEntropyValues([
|
|
893
|
+
"architecture",
|
|
894
|
+
"bitness",
|
|
895
|
+
"model",
|
|
896
|
+
"platformVersion",
|
|
897
|
+
"uaFullVersion",
|
|
898
|
+
"fullVersionList"
|
|
899
|
+
]).then((v) => {
|
|
900
|
+
cachedHighEntropy = v;
|
|
901
|
+
}).catch(() => {
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
function pickBrand(brands) {
|
|
905
|
+
if (!brands?.length) return void 0;
|
|
906
|
+
const real = brands.filter((b) => !/not.?a.?brand/i.test(b.brand));
|
|
907
|
+
if (real.length === 0) return void 0;
|
|
908
|
+
const named = real.find((b) => !/chromium|google chrome/i.test(b.brand));
|
|
909
|
+
return named ?? real[0];
|
|
910
|
+
}
|
|
911
|
+
function captureUserAgentData(nav) {
|
|
912
|
+
const low = nav?.userAgentData;
|
|
913
|
+
if (!low && !cachedHighEntropy) return void 0;
|
|
914
|
+
const fullList = cachedHighEntropy?.fullVersionList;
|
|
915
|
+
const brand = pickBrand(fullList ?? low?.brands);
|
|
916
|
+
const out = {};
|
|
917
|
+
if (brand) {
|
|
918
|
+
out.browser = brand.brand;
|
|
919
|
+
out.browserVersion = brand.version;
|
|
920
|
+
}
|
|
921
|
+
if (low?.platform) out.os = low.platform;
|
|
922
|
+
if (cachedHighEntropy?.platformVersion) out.osVersion = cachedHighEntropy.platformVersion;
|
|
923
|
+
if (typeof low?.mobile === "boolean") out.mobile = low.mobile;
|
|
924
|
+
if (cachedHighEntropy?.model) out.model = cachedHighEntropy.model;
|
|
925
|
+
if (cachedHighEntropy?.architecture) out.architecture = cachedHighEntropy.architecture;
|
|
926
|
+
if (cachedHighEntropy?.bitness) out.bitness = cachedHighEntropy.bitness;
|
|
927
|
+
return Object.keys(out).length === 0 ? void 0 : out;
|
|
928
|
+
}
|
|
929
|
+
function captureScreen(scr, win) {
|
|
930
|
+
if (!scr && !win) return void 0;
|
|
931
|
+
const out = {};
|
|
932
|
+
if (typeof scr?.width === "number") out.width = scr.width;
|
|
933
|
+
if (typeof scr?.height === "number") out.height = scr.height;
|
|
934
|
+
if (typeof win?.devicePixelRatio === "number") out.devicePixelRatio = win.devicePixelRatio;
|
|
935
|
+
if (typeof scr?.colorDepth === "number") out.colorDepth = scr.colorDepth;
|
|
936
|
+
const orientationType = scr?.orientation?.type;
|
|
937
|
+
if (orientationType) out.orientation = orientationType;
|
|
938
|
+
return Object.keys(out).length === 0 ? void 0 : out;
|
|
939
|
+
}
|
|
940
|
+
function matchMedia(win, query) {
|
|
941
|
+
if (!win?.matchMedia) return void 0;
|
|
942
|
+
try {
|
|
943
|
+
return win.matchMedia(query).matches;
|
|
944
|
+
} catch {
|
|
945
|
+
return void 0;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
function matchScheme(win) {
|
|
949
|
+
if (!win?.matchMedia) return void 0;
|
|
950
|
+
if (matchMedia(win, "(prefers-color-scheme: dark)")) return "dark";
|
|
951
|
+
if (matchMedia(win, "(prefers-color-scheme: light)")) return "light";
|
|
952
|
+
return "no-preference";
|
|
953
|
+
}
|
|
954
|
+
function matchContrast(win) {
|
|
955
|
+
if (!win?.matchMedia) return void 0;
|
|
956
|
+
if (matchMedia(win, "(prefers-contrast: more)")) return "more";
|
|
957
|
+
if (matchMedia(win, "(prefers-contrast: less)")) return "less";
|
|
958
|
+
if (matchMedia(win, "(prefers-contrast: custom)")) return "custom";
|
|
959
|
+
return "no-preference";
|
|
960
|
+
}
|
|
961
|
+
function matchDisplayMode(win) {
|
|
962
|
+
if (!win?.matchMedia) return void 0;
|
|
963
|
+
if (matchMedia(win, "(display-mode: fullscreen)")) return "fullscreen";
|
|
964
|
+
if (matchMedia(win, "(display-mode: standalone)")) return "standalone";
|
|
965
|
+
if (matchMedia(win, "(display-mode: minimal-ui)")) return "minimal-ui";
|
|
966
|
+
if (matchMedia(win, "(display-mode: browser)")) return "browser";
|
|
967
|
+
return void 0;
|
|
968
|
+
}
|
|
969
|
+
function readBuildIdMeta(doc) {
|
|
970
|
+
if (!doc) return void 0;
|
|
971
|
+
const el = doc.querySelector?.('meta[name="mushi:build"]');
|
|
972
|
+
const v = el?.content?.trim();
|
|
973
|
+
if (!v) return void 0;
|
|
974
|
+
return v.slice(0, 64);
|
|
975
|
+
}
|
|
976
|
+
function capturePageLoadTiming(win) {
|
|
977
|
+
const perf = win?.performance;
|
|
978
|
+
if (!perf?.getEntriesByType) return void 0;
|
|
979
|
+
let entry;
|
|
980
|
+
try {
|
|
981
|
+
const entries = perf.getEntriesByType("navigation");
|
|
982
|
+
entry = entries[0];
|
|
983
|
+
} catch {
|
|
984
|
+
return void 0;
|
|
985
|
+
}
|
|
986
|
+
if (!entry) return void 0;
|
|
987
|
+
const start = entry.startTime ?? 0;
|
|
988
|
+
const out = {};
|
|
989
|
+
if (entry.domContentLoadedEventEnd > 0)
|
|
990
|
+
out.domContentLoadedMs = Math.round(entry.domContentLoadedEventEnd - start);
|
|
991
|
+
if (entry.loadEventEnd > 0) out.loadCompleteMs = Math.round(entry.loadEventEnd - start);
|
|
992
|
+
if (entry.responseStart > 0) out.timeToFirstByteMs = Math.round(entry.responseStart - start);
|
|
993
|
+
if (typeof entry.type === "string") out.navigationType = entry.type;
|
|
994
|
+
return Object.keys(out).length === 0 ? void 0 : out;
|
|
995
|
+
}
|
|
824
996
|
|
|
825
997
|
// src/reporter-token.ts
|
|
826
998
|
var STORAGE_KEY = "mushi_reporter_token";
|
|
@@ -1028,12 +1200,82 @@ function scrubPii(text, config) {
|
|
|
1028
1200
|
return createPiiScrubber(config).scrub(text);
|
|
1029
1201
|
}
|
|
1030
1202
|
|
|
1203
|
+
// src/breadcrumbs.ts
|
|
1204
|
+
var DEFAULT_MAX = 50;
|
|
1205
|
+
var DEFAULT_MAX_MESSAGE = 500;
|
|
1206
|
+
function createBreadcrumbBuffer(options = {}) {
|
|
1207
|
+
const max = Math.max(1, options.max ?? DEFAULT_MAX);
|
|
1208
|
+
const maxMsg = Math.max(50, options.maxMessageLength ?? DEFAULT_MAX_MESSAGE);
|
|
1209
|
+
let entries = [];
|
|
1210
|
+
return {
|
|
1211
|
+
add(input) {
|
|
1212
|
+
const ts = typeof input.timestamp === "number" ? input.timestamp : Date.now();
|
|
1213
|
+
const message = typeof input.message === "string" && input.message.length > maxMsg ? `${input.message.slice(0, maxMsg)}\u2026` : input.message;
|
|
1214
|
+
const crumb = {
|
|
1215
|
+
timestamp: ts,
|
|
1216
|
+
category: input.category,
|
|
1217
|
+
level: input.level ?? "info",
|
|
1218
|
+
message: message ?? "",
|
|
1219
|
+
...input.data ? { data: input.data } : {}
|
|
1220
|
+
};
|
|
1221
|
+
entries.push(crumb);
|
|
1222
|
+
while (entries.length > max) entries.shift();
|
|
1223
|
+
},
|
|
1224
|
+
getAll() {
|
|
1225
|
+
return entries.slice();
|
|
1226
|
+
},
|
|
1227
|
+
clear() {
|
|
1228
|
+
entries = [];
|
|
1229
|
+
},
|
|
1230
|
+
size() {
|
|
1231
|
+
return entries.length;
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// src/exception-normaliser.ts
|
|
1237
|
+
var STACK_LIMIT = 8 * 1024;
|
|
1238
|
+
var FALLBACK_JSON_LIMIT = 1e3;
|
|
1239
|
+
function normaliseThrown(thrown) {
|
|
1240
|
+
if (thrown instanceof Error) {
|
|
1241
|
+
const name = thrown.name || "Error";
|
|
1242
|
+
const message = thrown.message || String(thrown);
|
|
1243
|
+
const stack = typeof thrown.stack === "string" && thrown.stack.length > 0 ? thrown.stack.slice(0, STACK_LIMIT) : void 0;
|
|
1244
|
+
const cause = thrown.cause;
|
|
1245
|
+
return {
|
|
1246
|
+
name,
|
|
1247
|
+
message,
|
|
1248
|
+
...stack ? { stack } : {},
|
|
1249
|
+
...cause !== void 0 ? { cause: cause instanceof Error ? cause.message : cause } : {}
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
if (typeof thrown === "string") {
|
|
1253
|
+
return { name: "Error", message: thrown };
|
|
1254
|
+
}
|
|
1255
|
+
if (thrown && typeof thrown === "object") {
|
|
1256
|
+
const obj = thrown;
|
|
1257
|
+
const name = typeof obj.name === "string" ? obj.name : "Error";
|
|
1258
|
+
const message = typeof obj.message === "string" ? obj.message : (() => {
|
|
1259
|
+
try {
|
|
1260
|
+
return JSON.stringify(obj).slice(0, FALLBACK_JSON_LIMIT);
|
|
1261
|
+
} catch {
|
|
1262
|
+
return String(obj);
|
|
1263
|
+
}
|
|
1264
|
+
})();
|
|
1265
|
+
const stack = typeof obj.stack === "string" ? obj.stack.slice(0, STACK_LIMIT) : void 0;
|
|
1266
|
+
return { name, message, ...stack ? { stack } : {} };
|
|
1267
|
+
}
|
|
1268
|
+
if (thrown === void 0) return { name: "Error", message: "unknown" };
|
|
1269
|
+
return { name: "Error", message: String(thrown) };
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1031
1272
|
exports.DEFAULT_API_ENDPOINT = DEFAULT_API_ENDPOINT;
|
|
1032
1273
|
exports.MUSHI_INTERNAL_HEADER = MUSHI_INTERNAL_HEADER;
|
|
1033
1274
|
exports.MUSHI_INTERNAL_INIT_MARKER = MUSHI_INTERNAL_INIT_MARKER;
|
|
1034
1275
|
exports.REGION_ENDPOINTS = REGION_ENDPOINTS;
|
|
1035
1276
|
exports.captureEnvironment = captureEnvironment;
|
|
1036
1277
|
exports.createApiClient = createApiClient;
|
|
1278
|
+
exports.createBreadcrumbBuffer = createBreadcrumbBuffer;
|
|
1037
1279
|
exports.createLogger = createLogger;
|
|
1038
1280
|
exports.createOfflineQueue = createOfflineQueue;
|
|
1039
1281
|
exports.createPiiScrubber = createPiiScrubber;
|
|
@@ -1043,6 +1285,7 @@ exports.getDeviceFingerprintHash = getDeviceFingerprintHash;
|
|
|
1043
1285
|
exports.getReporterToken = getReporterToken;
|
|
1044
1286
|
exports.getSessionId = getSessionId;
|
|
1045
1287
|
exports.noopLogger = noopLogger;
|
|
1288
|
+
exports.normaliseThrown = normaliseThrown;
|
|
1046
1289
|
exports.resolveRegionEndpoint = resolveRegionEndpoint;
|
|
1047
1290
|
exports.scrubPii = scrubPii;
|
|
1048
1291
|
//# sourceMappingURL=index.cjs.map
|