@mushi-mushi/react-native 0.8.1 → 0.11.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/SECURITY.md +167 -4
- package/dist/index.cjs +222 -36
- package/dist/index.d.cts +42 -2
- package/dist/index.d.ts +42 -2
- package/dist/index.js +230 -35
- package/package.json +11 -7
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/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
|
@@ -34,6 +34,7 @@ __export(index_exports, {
|
|
|
34
34
|
MushiBottomSheet: () => MushiBottomSheet,
|
|
35
35
|
MushiFloatingButton: () => MushiFloatingButton,
|
|
36
36
|
MushiProvider: () => MushiProvider,
|
|
37
|
+
MushiTrigger: () => MushiTrigger,
|
|
37
38
|
getDeviceInfo: () => getDeviceInfo,
|
|
38
39
|
setupConsoleCapture: () => setupConsoleCapture,
|
|
39
40
|
setupNetworkCapture: () => setupNetworkCapture,
|
|
@@ -138,13 +139,28 @@ function stripQueryParams(url) {
|
|
|
138
139
|
var import_react_native = require("react-native");
|
|
139
140
|
function getDeviceInfo() {
|
|
140
141
|
const { width, height } = import_react_native.Dimensions.get("window");
|
|
142
|
+
const locale = (
|
|
143
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
144
|
+
import_react_native.NativeModules.SettingsManager?.settings?.AppleLocale ?? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
145
|
+
import_react_native.NativeModules.I18nManager?.localeIdentifier ?? "en"
|
|
146
|
+
);
|
|
147
|
+
let timezone = "UTC";
|
|
148
|
+
try {
|
|
149
|
+
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
150
|
+
} catch {
|
|
151
|
+
}
|
|
152
|
+
const appVersion = import_react_native.NativeModules.RNDeviceInfo?.appVersion ?? import_react_native.NativeModules.RNBuildConfig?.VERSION_NAME ?? String(import_react_native.Platform.Version);
|
|
141
153
|
return {
|
|
142
154
|
platform: import_react_native.Platform.OS,
|
|
143
155
|
osVersion: import_react_native.Platform.Version,
|
|
144
156
|
screenWidth: width,
|
|
145
157
|
screenHeight: height,
|
|
146
158
|
pixelRatio: import_react_native.PixelRatio.get(),
|
|
147
|
-
fontScale: import_react_native.PixelRatio.getFontScale()
|
|
159
|
+
fontScale: import_react_native.PixelRatio.getFontScale(),
|
|
160
|
+
systemName: import_react_native.Platform.OS,
|
|
161
|
+
locale,
|
|
162
|
+
timezone,
|
|
163
|
+
appVersion
|
|
148
164
|
};
|
|
149
165
|
}
|
|
150
166
|
|
|
@@ -164,7 +180,8 @@ var AsyncStorageQueue = class {
|
|
|
164
180
|
if (!AsyncStorage) return;
|
|
165
181
|
const queue = await this.getQueue(AsyncStorage);
|
|
166
182
|
if (queue.length >= this.maxSize) queue.shift();
|
|
167
|
-
|
|
183
|
+
const scrubbedReport = this.scrubReportPii(report);
|
|
184
|
+
queue.push({ report: scrubbedReport, enqueuedAt: Date.now() });
|
|
168
185
|
await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(queue));
|
|
169
186
|
}
|
|
170
187
|
async flush() {
|
|
@@ -175,21 +192,10 @@ var AsyncStorageQueue = class {
|
|
|
175
192
|
let flushed = 0;
|
|
176
193
|
const remaining = [];
|
|
177
194
|
for (const item of queue) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
"Content-Type": "application/json",
|
|
183
|
-
"X-Mushi-Api-Key": this.apiKey
|
|
184
|
-
},
|
|
185
|
-
body: JSON.stringify(item.report)
|
|
186
|
-
});
|
|
187
|
-
if (res.ok) {
|
|
188
|
-
flushed++;
|
|
189
|
-
} else {
|
|
190
|
-
remaining.push(item);
|
|
191
|
-
}
|
|
192
|
-
} catch {
|
|
195
|
+
const ok = await sendWithRetry(item, this.apiEndpoint, this.apiKey);
|
|
196
|
+
if (ok) {
|
|
197
|
+
flushed++;
|
|
198
|
+
} else {
|
|
193
199
|
remaining.push(item);
|
|
194
200
|
break;
|
|
195
201
|
}
|
|
@@ -203,6 +209,27 @@ var AsyncStorageQueue = class {
|
|
|
203
209
|
const queue = await this.getQueue(AsyncStorage);
|
|
204
210
|
return queue.length;
|
|
205
211
|
}
|
|
212
|
+
// Added: PII scrubbing (Phase 2.4)
|
|
213
|
+
// Scrubs free-text fields where users typically type emails / phones /
|
|
214
|
+
// card numbers when describing a bug. We deliberately do NOT scrub
|
|
215
|
+
// structured fields like `metadata.userEmail` — those are explicitly
|
|
216
|
+
// captured by the host app via `setUser()` and the operator opts in to
|
|
217
|
+
// collecting them. Scrubbing them here would silently break attribution.
|
|
218
|
+
scrubReportPii(report) {
|
|
219
|
+
const next = { ...report };
|
|
220
|
+
if (typeof next.description === "string") {
|
|
221
|
+
next.description = scrubPii(next.description);
|
|
222
|
+
}
|
|
223
|
+
if (typeof next.summary === "string") {
|
|
224
|
+
next.summary = scrubPii(next.summary);
|
|
225
|
+
}
|
|
226
|
+
if (Array.isArray(next.breadcrumbs)) {
|
|
227
|
+
next.breadcrumbs = next.breadcrumbs.map(
|
|
228
|
+
(b) => typeof b?.message === "string" ? { ...b, message: scrubPii(b.message) } : b
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
return next;
|
|
232
|
+
}
|
|
206
233
|
async getQueue(storage) {
|
|
207
234
|
try {
|
|
208
235
|
const raw = await storage.getItem(QUEUE_KEY);
|
|
@@ -220,6 +247,63 @@ var AsyncStorageQueue = class {
|
|
|
220
247
|
}
|
|
221
248
|
}
|
|
222
249
|
};
|
|
250
|
+
var SCRUB_PATTERNS = [
|
|
251
|
+
[/\b\d{3}-\d{2}-\d{4}\b/g, "[REDACTED_SSN]"],
|
|
252
|
+
[/\b(?:\d[ -]*){12,18}\d\b/g, "[REDACTED_CC]"],
|
|
253
|
+
[/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g, "[REDACTED_AWS_KEY]"],
|
|
254
|
+
[
|
|
255
|
+
/(?:aws_secret_access_key|secret_access_key)["'\s:=]+[A-Za-z0-9/+=]{40}\b/gi,
|
|
256
|
+
"aws_secret_access_key=[REDACTED_AWS_SECRET]"
|
|
257
|
+
],
|
|
258
|
+
[/\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{24,}\b/g, "[REDACTED_STRIPE_KEY]"],
|
|
259
|
+
[/\bpk_(?:live|test)_[A-Za-z0-9]{24,}\b/g, "[REDACTED_STRIPE_PK]"],
|
|
260
|
+
[/\bxox[abpor]-[A-Za-z0-9-]{10,}\b/g, "[REDACTED_SLACK_TOKEN]"],
|
|
261
|
+
[/\bghp_[A-Za-z0-9]{36}\b/g, "[REDACTED_GITHUB_PAT]"],
|
|
262
|
+
[/\bgithub_pat_[A-Za-z0-9_]{80,}\b/g, "[REDACTED_GITHUB_PAT]"],
|
|
263
|
+
[/\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g, "[REDACTED_OPENAI_KEY]"],
|
|
264
|
+
[/\bsk-ant-[A-Za-z0-9_-]{20,}\b/g, "[REDACTED_ANTHROPIC_KEY]"],
|
|
265
|
+
[/\bAIza[0-9A-Za-z_-]{35}\b/g, "[REDACTED_GOOGLE_KEY]"],
|
|
266
|
+
[/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[REDACTED_JWT]"],
|
|
267
|
+
[/\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/g, "[REDACTED_EMAIL]"],
|
|
268
|
+
[
|
|
269
|
+
/(?:\+\d{1,3}[\s.\-])?\(?\d{2,4}\)?[\s.\-]\d{3,4}[\s.\-]\d{3,4}\b/g,
|
|
270
|
+
"[REDACTED_PHONE]"
|
|
271
|
+
]
|
|
272
|
+
];
|
|
273
|
+
function scrubPii(text) {
|
|
274
|
+
let result = text;
|
|
275
|
+
for (const [regex, replacement] of SCRUB_PATTERNS) {
|
|
276
|
+
result = result.replace(regex, replacement);
|
|
277
|
+
}
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
async function sendWithRetry(item, endpoint, apiKey, attempt = 0) {
|
|
281
|
+
const delay = Math.min(1e3 * Math.pow(2, attempt) + Math.random() * 500, 1e4);
|
|
282
|
+
try {
|
|
283
|
+
const res = await fetch(`${endpoint}/v1/reports`, {
|
|
284
|
+
method: "POST",
|
|
285
|
+
headers: {
|
|
286
|
+
"Content-Type": "application/json",
|
|
287
|
+
"X-Mushi-Api-Key": apiKey
|
|
288
|
+
},
|
|
289
|
+
body: JSON.stringify(item.report)
|
|
290
|
+
});
|
|
291
|
+
if ((res.status === 429 || res.status >= 500) && attempt < 3) {
|
|
292
|
+
await sleep(delay);
|
|
293
|
+
return sendWithRetry(item, endpoint, apiKey, attempt + 1);
|
|
294
|
+
}
|
|
295
|
+
return res.ok;
|
|
296
|
+
} catch {
|
|
297
|
+
if (attempt < 3) {
|
|
298
|
+
await sleep(delay);
|
|
299
|
+
return sendWithRetry(item, endpoint, apiKey, attempt + 1);
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function sleep(ms) {
|
|
305
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
306
|
+
}
|
|
223
307
|
|
|
224
308
|
// src/components/MushiBottomSheet.tsx
|
|
225
309
|
var import_react = require("react");
|
|
@@ -599,6 +683,11 @@ function MushiProvider({ children, ...config }) {
|
|
|
599
683
|
const queueRef = (0, import_react3.useRef)(null);
|
|
600
684
|
const apiClientRef = (0, import_react3.useRef)(null);
|
|
601
685
|
const [sheetVisible, setSheetVisible] = (0, import_react3.useState)(false);
|
|
686
|
+
if (config.endpoint !== void 0 && config.endpoint.trim() === "") {
|
|
687
|
+
throw new Error(
|
|
688
|
+
"[MushiProvider] endpoint is set to an empty string. Set endpoint to your Supabase edge function URL, e.g. https://xyz.supabase.co/functions/v1/api, or omit it to use the cloud default."
|
|
689
|
+
);
|
|
690
|
+
}
|
|
602
691
|
const apiEndpoint = config.endpoint ?? import_core.DEFAULT_API_ENDPOINT;
|
|
603
692
|
(0, import_react3.useEffect)(() => {
|
|
604
693
|
if (config.capture?.console !== false) {
|
|
@@ -624,6 +713,20 @@ function MushiProvider({ children, ...config }) {
|
|
|
624
713
|
networkRef.current?.restore();
|
|
625
714
|
};
|
|
626
715
|
}, []);
|
|
716
|
+
(0, import_react3.useEffect)(() => {
|
|
717
|
+
let unsubscribe;
|
|
718
|
+
try {
|
|
719
|
+
const NetInfo = require("@react-native-community/netinfo").default;
|
|
720
|
+
unsubscribe = NetInfo.addEventListener((state) => {
|
|
721
|
+
if (state.isConnected && state.isInternetReachable) {
|
|
722
|
+
queueRef.current?.flush().catch(() => {
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
} catch {
|
|
727
|
+
}
|
|
728
|
+
return () => unsubscribe?.();
|
|
729
|
+
}, []);
|
|
627
730
|
const open = (0, import_react3.useCallback)(() => setSheetVisible(true), []);
|
|
628
731
|
const close = (0, import_react3.useCallback)(() => setSheetVisible(false), []);
|
|
629
732
|
const attachTo = (0, import_react3.useCallback)(() => ({ onPress: open }), [open]);
|
|
@@ -634,20 +737,22 @@ function MushiProvider({ children, ...config }) {
|
|
|
634
737
|
let cleanup;
|
|
635
738
|
let lastShake = 0;
|
|
636
739
|
const threshold = config.widget?.shakeThreshold ?? 2.7;
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
740
|
+
try {
|
|
741
|
+
const mod = require("expo-sensors");
|
|
742
|
+
if (!disposed) {
|
|
743
|
+
mod.Accelerometer.setUpdateInterval(120);
|
|
744
|
+
const sub = mod.Accelerometer.addListener((evt) => {
|
|
745
|
+
const g = Math.sqrt(evt.x * evt.x + evt.y * evt.y + evt.z * evt.z);
|
|
746
|
+
if (g < threshold) return;
|
|
747
|
+
const now = Date.now();
|
|
748
|
+
if (now - lastShake < 1e3) return;
|
|
749
|
+
lastShake = now;
|
|
750
|
+
open();
|
|
751
|
+
});
|
|
752
|
+
cleanup = () => sub.remove();
|
|
753
|
+
}
|
|
754
|
+
} catch {
|
|
755
|
+
}
|
|
651
756
|
return () => {
|
|
652
757
|
disposed = true;
|
|
653
758
|
cleanup?.();
|
|
@@ -688,6 +793,31 @@ function MushiProvider({ children, ...config }) {
|
|
|
688
793
|
},
|
|
689
794
|
[config.projectId, config.apiKey, apiEndpoint]
|
|
690
795
|
);
|
|
796
|
+
const userRef = (0, import_react3.useRef)(null);
|
|
797
|
+
const metadataRef = (0, import_react3.useRef)({});
|
|
798
|
+
const screenRef = (0, import_react3.useRef)(null);
|
|
799
|
+
const activityQueueRef = (0, import_react3.useRef)([]);
|
|
800
|
+
const rewardsFlushRef = (0, import_react3.useRef)(null);
|
|
801
|
+
(0, import_react3.useEffect)(() => {
|
|
802
|
+
const rewards = config.rewards;
|
|
803
|
+
if (!rewards?.enabled || !rewards.trackActivity) return;
|
|
804
|
+
const flushMs = Math.max(3e4, rewards.flushIntervalMs ?? 3e5);
|
|
805
|
+
rewardsFlushRef.current = setInterval(async () => {
|
|
806
|
+
const userId = userRef.current?.id;
|
|
807
|
+
const client = apiClientRef.current;
|
|
808
|
+
if (!userId || !client || activityQueueRef.current.length === 0) return;
|
|
809
|
+
const batch = activityQueueRef.current.splice(0, 100);
|
|
810
|
+
await client.submitActivity(userId, batch, {
|
|
811
|
+
userTraits: userRef.current ?? void 0,
|
|
812
|
+
optedIn: true
|
|
813
|
+
}).catch(() => {
|
|
814
|
+
activityQueueRef.current.unshift(...batch.slice(0, 50));
|
|
815
|
+
});
|
|
816
|
+
}, flushMs);
|
|
817
|
+
return () => {
|
|
818
|
+
if (rewardsFlushRef.current) clearInterval(rewardsFlushRef.current);
|
|
819
|
+
};
|
|
820
|
+
}, [config.rewards?.enabled, config.rewards?.trackActivity, config.rewards?.flushIntervalMs]);
|
|
691
821
|
const instance = (0, import_react3.useMemo)(
|
|
692
822
|
() => ({
|
|
693
823
|
open,
|
|
@@ -696,9 +826,50 @@ function MushiProvider({ children, ...config }) {
|
|
|
696
826
|
submitReport,
|
|
697
827
|
getDeviceInfo,
|
|
698
828
|
getConsoleEntries: () => consoleRef.current?.getEntries() ?? [],
|
|
699
|
-
getNetworkEntries: () => networkRef.current?.getEntries() ?? []
|
|
829
|
+
getNetworkEntries: () => networkRef.current?.getEntries() ?? [],
|
|
830
|
+
// v0.10.0: new identity methods (these were causing workarounds in apps/mobile/src/native/mushi.ts)
|
|
831
|
+
identify(userId, traits) {
|
|
832
|
+
userRef.current = {
|
|
833
|
+
id: userId,
|
|
834
|
+
email: traits?.email,
|
|
835
|
+
name: traits?.name,
|
|
836
|
+
provider: traits?.provider
|
|
837
|
+
};
|
|
838
|
+
},
|
|
839
|
+
setMetadata(key, value) {
|
|
840
|
+
metadataRef.current[key] = value;
|
|
841
|
+
},
|
|
842
|
+
setScreen(screen) {
|
|
843
|
+
screenRef.current = screen;
|
|
844
|
+
},
|
|
845
|
+
// Rewards (P1)
|
|
846
|
+
recordActivity(action, metadata) {
|
|
847
|
+
if (!config.rewards?.enabled) return;
|
|
848
|
+
activityQueueRef.current.push({ action, metadata });
|
|
849
|
+
},
|
|
850
|
+
async getTier() {
|
|
851
|
+
const userId = userRef.current?.id;
|
|
852
|
+
const client = apiClientRef.current;
|
|
853
|
+
if (!userId || !client) return null;
|
|
854
|
+
const res = await client.getMyTier(userId);
|
|
855
|
+
return res.ok ? res.data : null;
|
|
856
|
+
},
|
|
857
|
+
async getReputation() {
|
|
858
|
+
const userId = userRef.current?.id;
|
|
859
|
+
const client = apiClientRef.current;
|
|
860
|
+
if (!userId || !client) return null;
|
|
861
|
+
const res = await client.getMyPoints(userId);
|
|
862
|
+
if (!res.ok) return null;
|
|
863
|
+
return {
|
|
864
|
+
totalPoints: res.data.total_points ?? 0,
|
|
865
|
+
points30d: res.data.points_30d ?? 0,
|
|
866
|
+
reputation: 1,
|
|
867
|
+
confirmedBugs: 0,
|
|
868
|
+
totalReports: 0
|
|
869
|
+
};
|
|
870
|
+
}
|
|
700
871
|
}),
|
|
701
|
-
[open, close, attachTo, submitReport]
|
|
872
|
+
[open, close, attachTo, submitReport, config.rewards?.enabled]
|
|
702
873
|
);
|
|
703
874
|
const trigger = config.widget?.trigger ?? "button";
|
|
704
875
|
const showFab = trigger === "button" || trigger === "both" || trigger === "auto" || trigger === "edge-tab";
|
|
@@ -748,11 +919,25 @@ function useMushiWidget() {
|
|
|
748
919
|
};
|
|
749
920
|
}
|
|
750
921
|
|
|
751
|
-
// src/
|
|
922
|
+
// src/components/MushiTrigger.tsx
|
|
752
923
|
var import_react6 = require("react");
|
|
924
|
+
function MushiTrigger({ children }) {
|
|
925
|
+
const mushi = useMushiContext();
|
|
926
|
+
if (!(0, import_react6.isValidElement)(children)) return children;
|
|
927
|
+
const originalOnPress = children.props.onPress;
|
|
928
|
+
return (0, import_react6.cloneElement)(children, {
|
|
929
|
+
onPress: () => {
|
|
930
|
+
originalOnPress?.();
|
|
931
|
+
mushi?.open();
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// src/capture/navigation-capture.ts
|
|
937
|
+
var import_react7 = require("react");
|
|
753
938
|
function useNavigationCapture(navigationRef) {
|
|
754
|
-
const historyRef = (0,
|
|
755
|
-
(0,
|
|
939
|
+
const historyRef = (0, import_react7.useRef)([]);
|
|
940
|
+
(0, import_react7.useEffect)(() => {
|
|
756
941
|
if (!navigationRef?.addListener || !navigationRef?.getCurrentRoute) return;
|
|
757
942
|
const unsubscribe = navigationRef.addListener("state", () => {
|
|
758
943
|
const route = navigationRef.getCurrentRoute?.();
|
|
@@ -771,6 +956,7 @@ function useNavigationCapture(navigationRef) {
|
|
|
771
956
|
MushiBottomSheet,
|
|
772
957
|
MushiFloatingButton,
|
|
773
958
|
MushiProvider,
|
|
959
|
+
MushiTrigger,
|
|
774
960
|
getDeviceInfo,
|
|
775
961
|
setupConsoleCapture,
|
|
776
962
|
setupNetworkCapture,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import
|
|
2
|
+
import * as react from 'react';
|
|
3
|
+
import { ReactNode, FC, ReactElement } from 'react';
|
|
4
|
+
import { MushiRewardsConfig, MushiTierResult, MushiReputationResult } from '@mushi-mushi/core';
|
|
3
5
|
|
|
4
6
|
interface ConsoleEntry {
|
|
5
7
|
level: 'log' | 'warn' | 'error';
|
|
@@ -36,6 +38,7 @@ interface DeviceInfo {
|
|
|
36
38
|
systemName?: string;
|
|
37
39
|
locale?: string;
|
|
38
40
|
timezone?: string;
|
|
41
|
+
appVersion?: string;
|
|
39
42
|
}
|
|
40
43
|
declare function getDeviceInfo(): DeviceInfo;
|
|
41
44
|
|
|
@@ -64,6 +67,7 @@ interface MushiRNConfig {
|
|
|
64
67
|
maxQueueSize?: number;
|
|
65
68
|
retryIntervalMs?: number;
|
|
66
69
|
};
|
|
70
|
+
rewards?: MushiRewardsConfig;
|
|
67
71
|
}
|
|
68
72
|
interface MushiRNInstance {
|
|
69
73
|
open(): void;
|
|
@@ -78,6 +82,27 @@ interface MushiRNInstance {
|
|
|
78
82
|
getDeviceInfo(): ReturnType<typeof getDeviceInfo>;
|
|
79
83
|
getConsoleEntries(): ReturnType<ReturnType<typeof setupConsoleCapture>['getEntries']>;
|
|
80
84
|
getNetworkEntries(): ReturnType<ReturnType<typeof setupNetworkCapture>['getEntries']>;
|
|
85
|
+
/** Set the current authenticated user. Equivalent to Mushi.identify() on web. */
|
|
86
|
+
identify(userId: string, traits?: {
|
|
87
|
+
email?: string;
|
|
88
|
+
name?: string;
|
|
89
|
+
provider?: string;
|
|
90
|
+
[k: string]: unknown;
|
|
91
|
+
}): void;
|
|
92
|
+
/** Attach arbitrary key/value metadata to subsequent reports. */
|
|
93
|
+
setMetadata(key: string, value: unknown): void;
|
|
94
|
+
/** Set the current screen context attached to subsequent reports. */
|
|
95
|
+
setScreen(screen: {
|
|
96
|
+
name: string;
|
|
97
|
+
route?: string;
|
|
98
|
+
feature?: string;
|
|
99
|
+
}): void;
|
|
100
|
+
/** Manually record a host-defined activity event. */
|
|
101
|
+
recordActivity(action: string, metadata?: Record<string, unknown>): void;
|
|
102
|
+
/** Returns the current user's tier. */
|
|
103
|
+
getTier(): Promise<MushiTierResult | null>;
|
|
104
|
+
/** Returns the current user's reputation + point totals. */
|
|
105
|
+
getReputation(): Promise<MushiReputationResult | null>;
|
|
81
106
|
}
|
|
82
107
|
declare function MushiProvider({ children, ...config }: MushiRNConfig & {
|
|
83
108
|
children: ReactNode;
|
|
@@ -166,6 +191,20 @@ interface MushiFloatingButtonProps {
|
|
|
166
191
|
}
|
|
167
192
|
declare const MushiFloatingButton: FC<MushiFloatingButtonProps>;
|
|
168
193
|
|
|
194
|
+
interface MushiTriggerProps {
|
|
195
|
+
/** A single React Native element that accepts `onPress`. */
|
|
196
|
+
children: ReactElement<{
|
|
197
|
+
onPress?: () => void;
|
|
198
|
+
}>;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Clones `children` and injects `onPress` → `mushi.open()`.
|
|
202
|
+
* The child is responsible for its own visual styling.
|
|
203
|
+
*/
|
|
204
|
+
declare function MushiTrigger({ children }: MushiTriggerProps): ReactElement<{
|
|
205
|
+
onPress?: () => void;
|
|
206
|
+
}, string | react.JSXElementConstructor<any>>;
|
|
207
|
+
|
|
169
208
|
interface NavigationEntry {
|
|
170
209
|
screen: string;
|
|
171
210
|
timestamp: number;
|
|
@@ -189,8 +228,9 @@ declare class AsyncStorageQueue {
|
|
|
189
228
|
enqueue(report: object): Promise<void>;
|
|
190
229
|
flush(): Promise<number>;
|
|
191
230
|
size(): Promise<number>;
|
|
231
|
+
private scrubReportPii;
|
|
192
232
|
private getQueue;
|
|
193
233
|
private getAsyncStorage;
|
|
194
234
|
}
|
|
195
235
|
|
|
196
|
-
export { AsyncStorageQueue, type ConsoleEntry, type DeviceInfo, MushiBottomSheet, type MushiBottomSheetProps, MushiFloatingButton, type MushiFloatingButtonProps, MushiProvider, type MushiRNConfig, type MushiRNInstance, type NavigationEntry, type NetworkEntry, getDeviceInfo, setupConsoleCapture, setupNetworkCapture, useMushi, useMushiReport, useMushiWidget, useNavigationCapture };
|
|
236
|
+
export { AsyncStorageQueue, type ConsoleEntry, type DeviceInfo, MushiBottomSheet, type MushiBottomSheetProps, MushiFloatingButton, type MushiFloatingButtonProps, MushiProvider, type MushiRNConfig, type MushiRNInstance, MushiTrigger, type MushiTriggerProps, type NavigationEntry, type NetworkEntry, getDeviceInfo, setupConsoleCapture, setupNetworkCapture, useMushi, useMushiReport, useMushiWidget, useNavigationCapture };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import
|
|
2
|
+
import * as react from 'react';
|
|
3
|
+
import { ReactNode, FC, ReactElement } from 'react';
|
|
4
|
+
import { MushiRewardsConfig, MushiTierResult, MushiReputationResult } from '@mushi-mushi/core';
|
|
3
5
|
|
|
4
6
|
interface ConsoleEntry {
|
|
5
7
|
level: 'log' | 'warn' | 'error';
|
|
@@ -36,6 +38,7 @@ interface DeviceInfo {
|
|
|
36
38
|
systemName?: string;
|
|
37
39
|
locale?: string;
|
|
38
40
|
timezone?: string;
|
|
41
|
+
appVersion?: string;
|
|
39
42
|
}
|
|
40
43
|
declare function getDeviceInfo(): DeviceInfo;
|
|
41
44
|
|
|
@@ -64,6 +67,7 @@ interface MushiRNConfig {
|
|
|
64
67
|
maxQueueSize?: number;
|
|
65
68
|
retryIntervalMs?: number;
|
|
66
69
|
};
|
|
70
|
+
rewards?: MushiRewardsConfig;
|
|
67
71
|
}
|
|
68
72
|
interface MushiRNInstance {
|
|
69
73
|
open(): void;
|
|
@@ -78,6 +82,27 @@ interface MushiRNInstance {
|
|
|
78
82
|
getDeviceInfo(): ReturnType<typeof getDeviceInfo>;
|
|
79
83
|
getConsoleEntries(): ReturnType<ReturnType<typeof setupConsoleCapture>['getEntries']>;
|
|
80
84
|
getNetworkEntries(): ReturnType<ReturnType<typeof setupNetworkCapture>['getEntries']>;
|
|
85
|
+
/** Set the current authenticated user. Equivalent to Mushi.identify() on web. */
|
|
86
|
+
identify(userId: string, traits?: {
|
|
87
|
+
email?: string;
|
|
88
|
+
name?: string;
|
|
89
|
+
provider?: string;
|
|
90
|
+
[k: string]: unknown;
|
|
91
|
+
}): void;
|
|
92
|
+
/** Attach arbitrary key/value metadata to subsequent reports. */
|
|
93
|
+
setMetadata(key: string, value: unknown): void;
|
|
94
|
+
/** Set the current screen context attached to subsequent reports. */
|
|
95
|
+
setScreen(screen: {
|
|
96
|
+
name: string;
|
|
97
|
+
route?: string;
|
|
98
|
+
feature?: string;
|
|
99
|
+
}): void;
|
|
100
|
+
/** Manually record a host-defined activity event. */
|
|
101
|
+
recordActivity(action: string, metadata?: Record<string, unknown>): void;
|
|
102
|
+
/** Returns the current user's tier. */
|
|
103
|
+
getTier(): Promise<MushiTierResult | null>;
|
|
104
|
+
/** Returns the current user's reputation + point totals. */
|
|
105
|
+
getReputation(): Promise<MushiReputationResult | null>;
|
|
81
106
|
}
|
|
82
107
|
declare function MushiProvider({ children, ...config }: MushiRNConfig & {
|
|
83
108
|
children: ReactNode;
|
|
@@ -166,6 +191,20 @@ interface MushiFloatingButtonProps {
|
|
|
166
191
|
}
|
|
167
192
|
declare const MushiFloatingButton: FC<MushiFloatingButtonProps>;
|
|
168
193
|
|
|
194
|
+
interface MushiTriggerProps {
|
|
195
|
+
/** A single React Native element that accepts `onPress`. */
|
|
196
|
+
children: ReactElement<{
|
|
197
|
+
onPress?: () => void;
|
|
198
|
+
}>;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Clones `children` and injects `onPress` → `mushi.open()`.
|
|
202
|
+
* The child is responsible for its own visual styling.
|
|
203
|
+
*/
|
|
204
|
+
declare function MushiTrigger({ children }: MushiTriggerProps): ReactElement<{
|
|
205
|
+
onPress?: () => void;
|
|
206
|
+
}, string | react.JSXElementConstructor<any>>;
|
|
207
|
+
|
|
169
208
|
interface NavigationEntry {
|
|
170
209
|
screen: string;
|
|
171
210
|
timestamp: number;
|
|
@@ -189,8 +228,9 @@ declare class AsyncStorageQueue {
|
|
|
189
228
|
enqueue(report: object): Promise<void>;
|
|
190
229
|
flush(): Promise<number>;
|
|
191
230
|
size(): Promise<number>;
|
|
231
|
+
private scrubReportPii;
|
|
192
232
|
private getQueue;
|
|
193
233
|
private getAsyncStorage;
|
|
194
234
|
}
|
|
195
235
|
|
|
196
|
-
export { AsyncStorageQueue, type ConsoleEntry, type DeviceInfo, MushiBottomSheet, type MushiBottomSheetProps, MushiFloatingButton, type MushiFloatingButtonProps, MushiProvider, type MushiRNConfig, type MushiRNInstance, type NavigationEntry, type NetworkEntry, getDeviceInfo, setupConsoleCapture, setupNetworkCapture, useMushi, useMushiReport, useMushiWidget, useNavigationCapture };
|
|
236
|
+
export { AsyncStorageQueue, type ConsoleEntry, type DeviceInfo, MushiBottomSheet, type MushiBottomSheetProps, MushiFloatingButton, type MushiFloatingButtonProps, MushiProvider, type MushiRNConfig, type MushiRNInstance, MushiTrigger, type MushiTriggerProps, type NavigationEntry, type NetworkEntry, getDeviceInfo, setupConsoleCapture, setupNetworkCapture, useMushi, useMushiReport, useMushiWidget, useNavigationCapture };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
1
8
|
// src/provider.tsx
|
|
2
9
|
import {
|
|
3
10
|
createContext,
|
|
@@ -8,7 +15,10 @@ import {
|
|
|
8
15
|
useCallback as useCallback3,
|
|
9
16
|
useMemo
|
|
10
17
|
} from "react";
|
|
11
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
createApiClient,
|
|
20
|
+
DEFAULT_API_ENDPOINT
|
|
21
|
+
} from "@mushi-mushi/core";
|
|
12
22
|
|
|
13
23
|
// src/capture/console-capture.ts
|
|
14
24
|
function setupConsoleCapture(maxEntries = 100) {
|
|
@@ -97,16 +107,31 @@ function stripQueryParams(url) {
|
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
// src/capture/device-info.ts
|
|
100
|
-
import { Platform, Dimensions, PixelRatio } from "react-native";
|
|
110
|
+
import { Platform, Dimensions, PixelRatio, NativeModules } from "react-native";
|
|
101
111
|
function getDeviceInfo() {
|
|
102
112
|
const { width, height } = Dimensions.get("window");
|
|
113
|
+
const locale = (
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
115
|
+
NativeModules.SettingsManager?.settings?.AppleLocale ?? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
116
|
+
NativeModules.I18nManager?.localeIdentifier ?? "en"
|
|
117
|
+
);
|
|
118
|
+
let timezone = "UTC";
|
|
119
|
+
try {
|
|
120
|
+
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
const appVersion = NativeModules.RNDeviceInfo?.appVersion ?? NativeModules.RNBuildConfig?.VERSION_NAME ?? String(Platform.Version);
|
|
103
124
|
return {
|
|
104
125
|
platform: Platform.OS,
|
|
105
126
|
osVersion: Platform.Version,
|
|
106
127
|
screenWidth: width,
|
|
107
128
|
screenHeight: height,
|
|
108
129
|
pixelRatio: PixelRatio.get(),
|
|
109
|
-
fontScale: PixelRatio.getFontScale()
|
|
130
|
+
fontScale: PixelRatio.getFontScale(),
|
|
131
|
+
systemName: Platform.OS,
|
|
132
|
+
locale,
|
|
133
|
+
timezone,
|
|
134
|
+
appVersion
|
|
110
135
|
};
|
|
111
136
|
}
|
|
112
137
|
|
|
@@ -126,7 +151,8 @@ var AsyncStorageQueue = class {
|
|
|
126
151
|
if (!AsyncStorage) return;
|
|
127
152
|
const queue = await this.getQueue(AsyncStorage);
|
|
128
153
|
if (queue.length >= this.maxSize) queue.shift();
|
|
129
|
-
|
|
154
|
+
const scrubbedReport = this.scrubReportPii(report);
|
|
155
|
+
queue.push({ report: scrubbedReport, enqueuedAt: Date.now() });
|
|
130
156
|
await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(queue));
|
|
131
157
|
}
|
|
132
158
|
async flush() {
|
|
@@ -137,21 +163,10 @@ var AsyncStorageQueue = class {
|
|
|
137
163
|
let flushed = 0;
|
|
138
164
|
const remaining = [];
|
|
139
165
|
for (const item of queue) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
"Content-Type": "application/json",
|
|
145
|
-
"X-Mushi-Api-Key": this.apiKey
|
|
146
|
-
},
|
|
147
|
-
body: JSON.stringify(item.report)
|
|
148
|
-
});
|
|
149
|
-
if (res.ok) {
|
|
150
|
-
flushed++;
|
|
151
|
-
} else {
|
|
152
|
-
remaining.push(item);
|
|
153
|
-
}
|
|
154
|
-
} catch {
|
|
166
|
+
const ok = await sendWithRetry(item, this.apiEndpoint, this.apiKey);
|
|
167
|
+
if (ok) {
|
|
168
|
+
flushed++;
|
|
169
|
+
} else {
|
|
155
170
|
remaining.push(item);
|
|
156
171
|
break;
|
|
157
172
|
}
|
|
@@ -165,6 +180,27 @@ var AsyncStorageQueue = class {
|
|
|
165
180
|
const queue = await this.getQueue(AsyncStorage);
|
|
166
181
|
return queue.length;
|
|
167
182
|
}
|
|
183
|
+
// Added: PII scrubbing (Phase 2.4)
|
|
184
|
+
// Scrubs free-text fields where users typically type emails / phones /
|
|
185
|
+
// card numbers when describing a bug. We deliberately do NOT scrub
|
|
186
|
+
// structured fields like `metadata.userEmail` — those are explicitly
|
|
187
|
+
// captured by the host app via `setUser()` and the operator opts in to
|
|
188
|
+
// collecting them. Scrubbing them here would silently break attribution.
|
|
189
|
+
scrubReportPii(report) {
|
|
190
|
+
const next = { ...report };
|
|
191
|
+
if (typeof next.description === "string") {
|
|
192
|
+
next.description = scrubPii(next.description);
|
|
193
|
+
}
|
|
194
|
+
if (typeof next.summary === "string") {
|
|
195
|
+
next.summary = scrubPii(next.summary);
|
|
196
|
+
}
|
|
197
|
+
if (Array.isArray(next.breadcrumbs)) {
|
|
198
|
+
next.breadcrumbs = next.breadcrumbs.map(
|
|
199
|
+
(b) => typeof b?.message === "string" ? { ...b, message: scrubPii(b.message) } : b
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return next;
|
|
203
|
+
}
|
|
168
204
|
async getQueue(storage) {
|
|
169
205
|
try {
|
|
170
206
|
const raw = await storage.getItem(QUEUE_KEY);
|
|
@@ -182,6 +218,63 @@ var AsyncStorageQueue = class {
|
|
|
182
218
|
}
|
|
183
219
|
}
|
|
184
220
|
};
|
|
221
|
+
var SCRUB_PATTERNS = [
|
|
222
|
+
[/\b\d{3}-\d{2}-\d{4}\b/g, "[REDACTED_SSN]"],
|
|
223
|
+
[/\b(?:\d[ -]*){12,18}\d\b/g, "[REDACTED_CC]"],
|
|
224
|
+
[/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g, "[REDACTED_AWS_KEY]"],
|
|
225
|
+
[
|
|
226
|
+
/(?:aws_secret_access_key|secret_access_key)["'\s:=]+[A-Za-z0-9/+=]{40}\b/gi,
|
|
227
|
+
"aws_secret_access_key=[REDACTED_AWS_SECRET]"
|
|
228
|
+
],
|
|
229
|
+
[/\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{24,}\b/g, "[REDACTED_STRIPE_KEY]"],
|
|
230
|
+
[/\bpk_(?:live|test)_[A-Za-z0-9]{24,}\b/g, "[REDACTED_STRIPE_PK]"],
|
|
231
|
+
[/\bxox[abpor]-[A-Za-z0-9-]{10,}\b/g, "[REDACTED_SLACK_TOKEN]"],
|
|
232
|
+
[/\bghp_[A-Za-z0-9]{36}\b/g, "[REDACTED_GITHUB_PAT]"],
|
|
233
|
+
[/\bgithub_pat_[A-Za-z0-9_]{80,}\b/g, "[REDACTED_GITHUB_PAT]"],
|
|
234
|
+
[/\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g, "[REDACTED_OPENAI_KEY]"],
|
|
235
|
+
[/\bsk-ant-[A-Za-z0-9_-]{20,}\b/g, "[REDACTED_ANTHROPIC_KEY]"],
|
|
236
|
+
[/\bAIza[0-9A-Za-z_-]{35}\b/g, "[REDACTED_GOOGLE_KEY]"],
|
|
237
|
+
[/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[REDACTED_JWT]"],
|
|
238
|
+
[/\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/g, "[REDACTED_EMAIL]"],
|
|
239
|
+
[
|
|
240
|
+
/(?:\+\d{1,3}[\s.\-])?\(?\d{2,4}\)?[\s.\-]\d{3,4}[\s.\-]\d{3,4}\b/g,
|
|
241
|
+
"[REDACTED_PHONE]"
|
|
242
|
+
]
|
|
243
|
+
];
|
|
244
|
+
function scrubPii(text) {
|
|
245
|
+
let result = text;
|
|
246
|
+
for (const [regex, replacement] of SCRUB_PATTERNS) {
|
|
247
|
+
result = result.replace(regex, replacement);
|
|
248
|
+
}
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
async function sendWithRetry(item, endpoint, apiKey, attempt = 0) {
|
|
252
|
+
const delay = Math.min(1e3 * Math.pow(2, attempt) + Math.random() * 500, 1e4);
|
|
253
|
+
try {
|
|
254
|
+
const res = await fetch(`${endpoint}/v1/reports`, {
|
|
255
|
+
method: "POST",
|
|
256
|
+
headers: {
|
|
257
|
+
"Content-Type": "application/json",
|
|
258
|
+
"X-Mushi-Api-Key": apiKey
|
|
259
|
+
},
|
|
260
|
+
body: JSON.stringify(item.report)
|
|
261
|
+
});
|
|
262
|
+
if ((res.status === 429 || res.status >= 500) && attempt < 3) {
|
|
263
|
+
await sleep(delay);
|
|
264
|
+
return sendWithRetry(item, endpoint, apiKey, attempt + 1);
|
|
265
|
+
}
|
|
266
|
+
return res.ok;
|
|
267
|
+
} catch {
|
|
268
|
+
if (attempt < 3) {
|
|
269
|
+
await sleep(delay);
|
|
270
|
+
return sendWithRetry(item, endpoint, apiKey, attempt + 1);
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function sleep(ms) {
|
|
276
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
277
|
+
}
|
|
185
278
|
|
|
186
279
|
// src/components/MushiBottomSheet.tsx
|
|
187
280
|
import {
|
|
@@ -585,6 +678,11 @@ function MushiProvider({ children, ...config }) {
|
|
|
585
678
|
const queueRef = useRef3(null);
|
|
586
679
|
const apiClientRef = useRef3(null);
|
|
587
680
|
const [sheetVisible, setSheetVisible] = useState2(false);
|
|
681
|
+
if (config.endpoint !== void 0 && config.endpoint.trim() === "") {
|
|
682
|
+
throw new Error(
|
|
683
|
+
"[MushiProvider] endpoint is set to an empty string. Set endpoint to your Supabase edge function URL, e.g. https://xyz.supabase.co/functions/v1/api, or omit it to use the cloud default."
|
|
684
|
+
);
|
|
685
|
+
}
|
|
588
686
|
const apiEndpoint = config.endpoint ?? DEFAULT_API_ENDPOINT;
|
|
589
687
|
useEffect2(() => {
|
|
590
688
|
if (config.capture?.console !== false) {
|
|
@@ -610,6 +708,20 @@ function MushiProvider({ children, ...config }) {
|
|
|
610
708
|
networkRef.current?.restore();
|
|
611
709
|
};
|
|
612
710
|
}, []);
|
|
711
|
+
useEffect2(() => {
|
|
712
|
+
let unsubscribe;
|
|
713
|
+
try {
|
|
714
|
+
const NetInfo = __require("@react-native-community/netinfo").default;
|
|
715
|
+
unsubscribe = NetInfo.addEventListener((state) => {
|
|
716
|
+
if (state.isConnected && state.isInternetReachable) {
|
|
717
|
+
queueRef.current?.flush().catch(() => {
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
} catch {
|
|
722
|
+
}
|
|
723
|
+
return () => unsubscribe?.();
|
|
724
|
+
}, []);
|
|
613
725
|
const open = useCallback3(() => setSheetVisible(true), []);
|
|
614
726
|
const close = useCallback3(() => setSheetVisible(false), []);
|
|
615
727
|
const attachTo = useCallback3(() => ({ onPress: open }), [open]);
|
|
@@ -620,20 +732,22 @@ function MushiProvider({ children, ...config }) {
|
|
|
620
732
|
let cleanup;
|
|
621
733
|
let lastShake = 0;
|
|
622
734
|
const threshold = config.widget?.shakeThreshold ?? 2.7;
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
735
|
+
try {
|
|
736
|
+
const mod = __require("expo-sensors");
|
|
737
|
+
if (!disposed) {
|
|
738
|
+
mod.Accelerometer.setUpdateInterval(120);
|
|
739
|
+
const sub = mod.Accelerometer.addListener((evt) => {
|
|
740
|
+
const g = Math.sqrt(evt.x * evt.x + evt.y * evt.y + evt.z * evt.z);
|
|
741
|
+
if (g < threshold) return;
|
|
742
|
+
const now = Date.now();
|
|
743
|
+
if (now - lastShake < 1e3) return;
|
|
744
|
+
lastShake = now;
|
|
745
|
+
open();
|
|
746
|
+
});
|
|
747
|
+
cleanup = () => sub.remove();
|
|
748
|
+
}
|
|
749
|
+
} catch {
|
|
750
|
+
}
|
|
637
751
|
return () => {
|
|
638
752
|
disposed = true;
|
|
639
753
|
cleanup?.();
|
|
@@ -674,6 +788,31 @@ function MushiProvider({ children, ...config }) {
|
|
|
674
788
|
},
|
|
675
789
|
[config.projectId, config.apiKey, apiEndpoint]
|
|
676
790
|
);
|
|
791
|
+
const userRef = useRef3(null);
|
|
792
|
+
const metadataRef = useRef3({});
|
|
793
|
+
const screenRef = useRef3(null);
|
|
794
|
+
const activityQueueRef = useRef3([]);
|
|
795
|
+
const rewardsFlushRef = useRef3(null);
|
|
796
|
+
useEffect2(() => {
|
|
797
|
+
const rewards = config.rewards;
|
|
798
|
+
if (!rewards?.enabled || !rewards.trackActivity) return;
|
|
799
|
+
const flushMs = Math.max(3e4, rewards.flushIntervalMs ?? 3e5);
|
|
800
|
+
rewardsFlushRef.current = setInterval(async () => {
|
|
801
|
+
const userId = userRef.current?.id;
|
|
802
|
+
const client = apiClientRef.current;
|
|
803
|
+
if (!userId || !client || activityQueueRef.current.length === 0) return;
|
|
804
|
+
const batch = activityQueueRef.current.splice(0, 100);
|
|
805
|
+
await client.submitActivity(userId, batch, {
|
|
806
|
+
userTraits: userRef.current ?? void 0,
|
|
807
|
+
optedIn: true
|
|
808
|
+
}).catch(() => {
|
|
809
|
+
activityQueueRef.current.unshift(...batch.slice(0, 50));
|
|
810
|
+
});
|
|
811
|
+
}, flushMs);
|
|
812
|
+
return () => {
|
|
813
|
+
if (rewardsFlushRef.current) clearInterval(rewardsFlushRef.current);
|
|
814
|
+
};
|
|
815
|
+
}, [config.rewards?.enabled, config.rewards?.trackActivity, config.rewards?.flushIntervalMs]);
|
|
677
816
|
const instance = useMemo(
|
|
678
817
|
() => ({
|
|
679
818
|
open,
|
|
@@ -682,9 +821,50 @@ function MushiProvider({ children, ...config }) {
|
|
|
682
821
|
submitReport,
|
|
683
822
|
getDeviceInfo,
|
|
684
823
|
getConsoleEntries: () => consoleRef.current?.getEntries() ?? [],
|
|
685
|
-
getNetworkEntries: () => networkRef.current?.getEntries() ?? []
|
|
824
|
+
getNetworkEntries: () => networkRef.current?.getEntries() ?? [],
|
|
825
|
+
// v0.10.0: new identity methods (these were causing workarounds in apps/mobile/src/native/mushi.ts)
|
|
826
|
+
identify(userId, traits) {
|
|
827
|
+
userRef.current = {
|
|
828
|
+
id: userId,
|
|
829
|
+
email: traits?.email,
|
|
830
|
+
name: traits?.name,
|
|
831
|
+
provider: traits?.provider
|
|
832
|
+
};
|
|
833
|
+
},
|
|
834
|
+
setMetadata(key, value) {
|
|
835
|
+
metadataRef.current[key] = value;
|
|
836
|
+
},
|
|
837
|
+
setScreen(screen) {
|
|
838
|
+
screenRef.current = screen;
|
|
839
|
+
},
|
|
840
|
+
// Rewards (P1)
|
|
841
|
+
recordActivity(action, metadata) {
|
|
842
|
+
if (!config.rewards?.enabled) return;
|
|
843
|
+
activityQueueRef.current.push({ action, metadata });
|
|
844
|
+
},
|
|
845
|
+
async getTier() {
|
|
846
|
+
const userId = userRef.current?.id;
|
|
847
|
+
const client = apiClientRef.current;
|
|
848
|
+
if (!userId || !client) return null;
|
|
849
|
+
const res = await client.getMyTier(userId);
|
|
850
|
+
return res.ok ? res.data : null;
|
|
851
|
+
},
|
|
852
|
+
async getReputation() {
|
|
853
|
+
const userId = userRef.current?.id;
|
|
854
|
+
const client = apiClientRef.current;
|
|
855
|
+
if (!userId || !client) return null;
|
|
856
|
+
const res = await client.getMyPoints(userId);
|
|
857
|
+
if (!res.ok) return null;
|
|
858
|
+
return {
|
|
859
|
+
totalPoints: res.data.total_points ?? 0,
|
|
860
|
+
points30d: res.data.points_30d ?? 0,
|
|
861
|
+
reputation: 1,
|
|
862
|
+
confirmedBugs: 0,
|
|
863
|
+
totalReports: 0
|
|
864
|
+
};
|
|
865
|
+
}
|
|
686
866
|
}),
|
|
687
|
-
[open, close, attachTo, submitReport]
|
|
867
|
+
[open, close, attachTo, submitReport, config.rewards?.enabled]
|
|
688
868
|
);
|
|
689
869
|
const trigger = config.widget?.trigger ?? "button";
|
|
690
870
|
const showFab = trigger === "button" || trigger === "both" || trigger === "auto" || trigger === "edge-tab";
|
|
@@ -734,6 +914,20 @@ function useMushiWidget() {
|
|
|
734
914
|
};
|
|
735
915
|
}
|
|
736
916
|
|
|
917
|
+
// src/components/MushiTrigger.tsx
|
|
918
|
+
import { cloneElement, isValidElement } from "react";
|
|
919
|
+
function MushiTrigger({ children }) {
|
|
920
|
+
const mushi = useMushiContext();
|
|
921
|
+
if (!isValidElement(children)) return children;
|
|
922
|
+
const originalOnPress = children.props.onPress;
|
|
923
|
+
return cloneElement(children, {
|
|
924
|
+
onPress: () => {
|
|
925
|
+
originalOnPress?.();
|
|
926
|
+
mushi?.open();
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
737
931
|
// src/capture/navigation-capture.ts
|
|
738
932
|
import { useEffect as useEffect3, useRef as useRef4 } from "react";
|
|
739
933
|
function useNavigationCapture(navigationRef) {
|
|
@@ -756,6 +950,7 @@ export {
|
|
|
756
950
|
MushiBottomSheet,
|
|
757
951
|
MushiFloatingButton,
|
|
758
952
|
MushiProvider,
|
|
953
|
+
MushiTrigger,
|
|
759
954
|
getDeviceInfo,
|
|
760
955
|
setupConsoleCapture,
|
|
761
956
|
setupNetworkCapture,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mushi-mushi/react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React Native SDK for Mushi Mushi — shake-to-report, bottom sheet widget, full context capture",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"peerDependencies": {
|
|
10
10
|
"@react-native-async-storage/async-storage": ">=1.19",
|
|
11
|
+
"@react-native-community/netinfo": ">=11",
|
|
11
12
|
"@react-navigation/native": ">=6",
|
|
12
13
|
"expo-sensors": ">=13",
|
|
13
14
|
"react": ">=18",
|
|
@@ -22,18 +23,21 @@
|
|
|
22
23
|
},
|
|
23
24
|
"expo-sensors": {
|
|
24
25
|
"optional": true
|
|
26
|
+
},
|
|
27
|
+
"@react-native-community/netinfo": {
|
|
28
|
+
"optional": true
|
|
25
29
|
}
|
|
26
30
|
},
|
|
27
31
|
"dependencies": {
|
|
28
|
-
"@mushi-mushi/core": "^
|
|
32
|
+
"@mushi-mushi/core": "^1.1.0"
|
|
29
33
|
},
|
|
30
34
|
"devDependencies": {
|
|
31
|
-
"@types/react": "^19.
|
|
32
|
-
"react": "^19.
|
|
33
|
-
"react-native": "^0.79.
|
|
35
|
+
"@types/react": "^19.2.14",
|
|
36
|
+
"react": "^19.2.5",
|
|
37
|
+
"react-native": "^0.79.7",
|
|
34
38
|
"tsup": "^8.5.1",
|
|
35
|
-
"typescript": "^6.0.
|
|
36
|
-
"vitest": "^4.1.
|
|
39
|
+
"typescript": "^6.0.3",
|
|
40
|
+
"vitest": "^4.1.5"
|
|
37
41
|
},
|
|
38
42
|
"author": "Kenji Sakuramoto",
|
|
39
43
|
"repository": {
|