@variantlab/core 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,179 @@
1
+ # Origin story: the small-phone card problem
2
+
3
+ variantlab exists because of a very specific real-world problem we failed to solve cleanly in another project: Drishtikon Mobile, a Bengali news aggregation app.
4
+
5
+ ## Table of contents
6
+
7
+ - [The problem](#the-problem)
8
+ - [What we tried](#what-we-tried)
9
+ - [Why the ad-hoc solution doesn't scale](#why-the-ad-hoc-solution-doesnt-scale)
10
+ - [The realization](#the-realization)
11
+ - [Why this is generalizable](#why-this-is-generalizable)
12
+ - [What we kept](#what-we-kept)
13
+
14
+ ---
15
+
16
+ ## The problem
17
+
18
+ Drishtikon has a "detailed news card" component in a vertical feed. Each card shows:
19
+
20
+ - A hero image (roughly 50% of the card height)
21
+ - The article title (2 lines max)
22
+ - A bias visualization bar (1 line, 24 px)
23
+ - 3 claims (roughly 130 px)
24
+ - 5 source avatars (roughly 90 px)
25
+ - An action bar (44 px — bookmark, share, AI chat)
26
+
27
+ On larger phones (iPhone 15 Pro, Pixel 8) the card fits comfortably. On smaller devices (iPhone SE, older Android) the card height shrinks to ~280-360 px and the content gets clipped or forces scrolling.
28
+
29
+ The "obvious fix" — shrink the image — works up to a point, but on the smallest devices the image becomes uselessly tiny and the card still clips.
30
+
31
+ We needed to test **many** layout strategies to pick the right one. We couldn't just ship one and hope. We had to put multiple side-by-side, try each on a real phone, and pick the best.
32
+
33
+ ---
34
+
35
+ ## What we tried
36
+
37
+ We built a homebrew runtime mode switcher:
38
+
39
+ ```
40
+ src/context/CardResizeModeContext.tsx — React Context + AsyncStorage persistence
41
+ src/components/ui/CardResizeModePicker.tsx — floating button + bottom sheet picker
42
+ src/components/feature/news-card/DetailedCardBody.tsx — dispatcher with 30 mode components
43
+ ```
44
+
45
+ Over several hours we iterated on this:
46
+
47
+ 1. First pass: 12 layout modes (responsive image, no-scroll, tap-collapse, drag-handle, bottom-sheet, parallax, title-overlay, accordion, horizontal, drag-snap, minimal, swipe-resize)
48
+ 2. "Think out of the box" pass: +6 modes (auto-fit, swipe-pages, adaptive-density, focus-peek, ambient-bias, sticky-parallax)
49
+ 3. "Impress me" pass: +6 modes (RSVP reader, 3D flip, liquid morph, radial wheel, breathing pulse, marquee ticker)
50
+ 4. "Those don't solve my issue" pass: +6 modes (scale-to-fit, fixed-ratio-tabs, measure-drop, text-first, pip-thumbnail, single-section carousel)
51
+
52
+ Total: 30 modes switchable at runtime via a floating purple button.
53
+
54
+ All 30 modes persist to AsyncStorage. The picker is route-scoped (only shows on the feed). The current mode is surfaced in the debug overlay as a number badge.
55
+
56
+ **It works.** We can test every layout on every phone in the palm of our hand. The developer workflow is magical: change modes, feel the difference, pick a winner.
57
+
58
+ ---
59
+
60
+ ## Why the ad-hoc solution doesn't scale
61
+
62
+ The problem is that everything we built is:
63
+
64
+ 1. **Hand-rolled** — every new experiment needs new boilerplate
65
+ 2. **Single-app** — the `CardResizeModeContext` only knows about card resize modes
66
+ 3. **Single-experiment** — we can't test card layout AND onboarding flow AND CTA copy at the same time
67
+ 4. **Not typed** — mode strings are stringly-typed throughout the app
68
+ 5. **Not persisted across reinstalls** — we lose our picks when wiping the app
69
+ 6. **Not shareable** — we can't send a QA team "try mode 27 on mode 15" in a message
70
+ 7. **Tied to the Drishtikon codebase** — the next app will have to build this again
71
+
72
+ And crucially:
73
+
74
+ > **The picker is better than any existing A/B testing tool we've used.** Firebase Remote Config has nothing like it. GrowthBook's dashboard is nice but doesn't let you try variants on a real device. LaunchDarkly's targeting UI is powerful but living in a browser tab instead of the app.
75
+
76
+ The *experience* of opening a real phone, shaking it, and cycling through 30 UI variants with one tap — that is the killer feature no paid tool offers.
77
+
78
+ ---
79
+
80
+ ## The realization
81
+
82
+ If this UX is so valuable, it shouldn't be locked to one app. It should be:
83
+
84
+ 1. **Generalized** — any experiment, any variant, any framework
85
+ 2. **Typed** — codegen'd IDs from a JSON config
86
+ 3. **Portable** — the same picker UX on Next.js, Remix, Vue, Svelte, React Native
87
+ 4. **Lightweight** — small enough to include in production builds without regret
88
+ 5. **Secure** — safe enough to ship remote config over the wire
89
+ 6. **Free** — because no one should pay $8.33/seat/month for what we just built in a weekend
90
+
91
+ And so: **variantlab**.
92
+
93
+ ---
94
+
95
+ ## Why this is generalizable
96
+
97
+ The Drishtikon problem was about layout. But every app has problems that benefit from the same workflow:
98
+
99
+ - **Onboarding flow A/B tests**: "3-step vs 1-page"
100
+ - **CTA copy tests**: "Buy now vs Get started vs Try free"
101
+ - **Pricing tests**: "$9.99 vs $14.99"
102
+ - **Color scheme tests**: "orange vs blue"
103
+ - **Feature rollouts**: "new chat UI, 10% of users"
104
+ - **Kill switches**: "disable the experimental AI tab if it crashes"
105
+ - **Copy localization tests**: "formal vs casual Bengali"
106
+ - **Layout experiments**: exactly Drishtikon's original problem
107
+
108
+ Every single one of these is a variant experiment. Every single one benefits from:
109
+
110
+ - A hand-switchable debug overlay on the device
111
+ - Type-safe IDs so typos become compile errors
112
+ - Route-aware filtering so the picker shows only what matters
113
+ - Deep-link + QR override for QA
114
+ - Crash-triggered rollback for safety
115
+
116
+ Drishtikon forced us to invent the UX. variantlab is the refactor where we unlock it for everyone.
117
+
118
+ ---
119
+
120
+ ## What we kept
121
+
122
+ Several design decisions in variantlab come directly from lessons learned building the ad-hoc solution:
123
+
124
+ ### 1. AsyncStorage-based persistence
125
+
126
+ The ad-hoc version used AsyncStorage. variantlab ships AsyncStorage, MMKV, and SecureStore adapters out of the box, with `createMemoryStorage()` for tests.
127
+
128
+ ### 2. Floating debug button
129
+
130
+ The Drishtikon picker has a floating purple button in the corner. variantlab ships the same pattern, configurable, tree-shakable, with shake-to-open as an opt-in.
131
+
132
+ ### 3. Route-scoped pickers
133
+
134
+ Drishtikon's picker only appears on the feed route. variantlab generalizes this into the `routes` field on experiments and the `useRouteExperiments()` hook.
135
+
136
+ ### 4. Bottom-sheet modal with rich descriptions
137
+
138
+ The Drishtikon picker bottom sheet shows the mode label + description. variantlab ships the same UX, plus search, favorites, and QR share.
139
+
140
+ ### 5. One component per variant
141
+
142
+ Drishtikon's `DetailedCardBody.tsx` has a switch statement dispatching to 30 mode components. variantlab's `<Variant>` render-prop component is the generic version of this pattern.
143
+
144
+ ### 6. Developer-first ergonomics
145
+
146
+ The Drishtikon picker is *fun* to use. It feels like Storybook for layouts. variantlab preserves that feel — the debug overlay is a first-class product, not an afterthought.
147
+
148
+ ---
149
+
150
+ ## The first migration
151
+
152
+ The first real-world user of variantlab will be Drishtikon itself. We will:
153
+
154
+ 1. Replace `CardResizeModeContext` with `@variantlab/core` + `@variantlab/react-native`
155
+ 2. Replace `CardResizeModePicker` with `<VariantDebugOverlay>`
156
+ 3. Move the 30 mode definitions into `experiments.json`
157
+ 4. Replace the dispatcher switch with `<Variant experimentId="news-card-layout">`
158
+ 5. Delete ~2000 lines of hand-rolled code
159
+
160
+ This migration is the single most valuable reality check for the variantlab API. If it doesn't feel cleaner than the ad-hoc version, the API is wrong and we iterate.
161
+
162
+ ---
163
+
164
+ ## Lessons that shaped the architecture
165
+
166
+ 1. **Runtime picker UX is the product.** Everything else — remote config, dashboards, analytics — is secondary.
167
+ 2. **Route-scoping is essential.** On a big app with many experiments, showing all of them at all times is overwhelming. Filter by current route.
168
+ 3. **Persistence must survive hot reload.** The developer's flow is "make a change, hot reload, re-check the mode". Lost state kills the flow.
169
+ 4. **Configs are documents, not databases.** Treat `experiments.json` like a README — versioned, reviewed, diffable.
170
+ 5. **Never break production by accident.** Crash-triggered rollback was born from shipping a card mode that crashed on certain article data. We want the safety net built in.
171
+ 6. **Frameworks are plural now.** A React-only solution is a dead end in 2026. We use Next.js, React Native, and Vite-React in the same organization. Every real team does.
172
+
173
+ ---
174
+
175
+ ## See also
176
+
177
+ - [`README.md`](../../README.md) — the public pitch
178
+ - [`ROADMAP.md`](../../ROADMAP.md) — the phased plan
179
+ - [`docs/features/debug-overlay.md`](../features/debug-overlay.md) — the debug overlay design
@@ -0,0 +1,312 @@
1
+ # Security threat research
2
+
3
+ Extended research behind the threats and mitigations documented in [`SECURITY.md`](../../SECURITY.md). This file goes deeper into the reasoning, references, and historical incidents that inform each decision.
4
+
5
+ ## Table of contents
6
+
7
+ - [Scope](#scope)
8
+ - [Historical incidents we learned from](#historical-incidents-we-learned-from)
9
+ - [Threat categories](#threat-categories)
10
+ - [Deep-dives](#deep-dives)
11
+ - [Open questions](#open-questions)
12
+ - [References](#references)
13
+
14
+ ---
15
+
16
+ ## Scope
17
+
18
+ variantlab is a client-side configuration and variant-resolution library. Its threat surface includes:
19
+
20
+ 1. **Config integrity** — can the config be tampered with in transit or at rest?
21
+ 2. **Code execution** — can a malicious config execute code?
22
+ 3. **Data exfiltration** — can variantlab leak user data?
23
+ 4. **Storage integrity** — can local storage be tampered with?
24
+ 5. **Supply chain** — can an upstream compromise affect downstream users?
25
+ 6. **Denial of service** — can a malicious config crash or exhaust the runtime?
26
+
27
+ Out of scope (the user's responsibility):
28
+
29
+ - Authentication of users (we don't do auth)
30
+ - Securing the backend that serves the config
31
+ - Encrypting user data at rest (handled by the user's backend)
32
+ - Protecting secrets used to sign configs (key management is user-owned)
33
+
34
+ ---
35
+
36
+ ## Historical incidents we learned from
37
+
38
+ ### 1. The `event-stream` incident (2018)
39
+
40
+ **What happened**: A maintainer handed control of a popular npm package (`event-stream`) to an anonymous contributor who added a malicious dependency targeting cryptocurrency wallets.
41
+
42
+ **Lesson**: Every runtime dependency is a trust decision. Even seemingly benign packages can be compromised. Our response: zero runtime deps in `@variantlab/core`.
43
+
44
+ **Reference**: https://github.com/dominictarr/event-stream/issues/116
45
+
46
+ ### 2. The `ua-parser-js` hijack (2021)
47
+
48
+ **What happened**: A compromised maintainer account published a malicious version of `ua-parser-js`, a package with 7M+ weekly downloads.
49
+
50
+ **Lesson**: Even with 2FA, account compromise is possible. Defense in depth via signed releases and provenance attestations.
51
+
52
+ **Reference**: https://github.com/advisories/GHSA-pjwm-rvh2-c87w
53
+
54
+ ### 3. The `node-ipc` protestware incident (2022)
55
+
56
+ **What happened**: A maintainer added code that wiped files on machines with specific geolocation.
57
+
58
+ **Lesson**: Trust of maintainers cannot be absolute. Our response: reproducible builds so any divergence is detectable.
59
+
60
+ **Reference**: https://github.com/advisories/GHSA-97m3-w2cp-4xx6
61
+
62
+ ### 4. The `xz-utils` backdoor (2024)
63
+
64
+ **What happened**: A multi-year social engineering campaign introduced a backdoor into a popular upstream project via a seemingly legitimate contributor.
65
+
66
+ **Lesson**: Review every contribution with paranoia. Multi-reviewer approval on sensitive changes. Reproducible builds.
67
+
68
+ **Reference**: https://research.swtch.com/xz-timeline
69
+
70
+ ### 5. Prototype pollution in `lodash.merge` (2018-2020)
71
+
72
+ **What happened**: Multiple CVEs against `lodash.merge` for prototype pollution via crafted input.
73
+
74
+ **Lesson**: Don't deep-merge untrusted objects. Our response: no deep merge in config processing; whitelist-only object parsing.
75
+
76
+ **Reference**: CVE-2019-10744, CVE-2020-8203
77
+
78
+ ### 6. Firebase Remote Config bypass (hypothetical, not a specific CVE)
79
+
80
+ **What it illustrates**: Any client-controlled config can be tampered with by a sufficiently motivated user. The mitigation is to never trust config for security decisions.
81
+
82
+ **Lesson**: Our docs must clearly warn users not to use feature flags for access control.
83
+
84
+ ---
85
+
86
+ ## Threat categories
87
+
88
+ ### A. Integrity threats
89
+
90
+ Threats that allow an attacker to change what config the client sees.
91
+
92
+ - **A1**: MITM on remote config endpoint
93
+ - **A2**: Compromised CDN origin
94
+ - **A3**: DNS hijack
95
+ - **A4**: BGP hijack
96
+ - **A5**: Compromised storage bucket
97
+ - **A6**: Compromised npm package
98
+ - **A7**: Compromised build pipeline
99
+ - **A8**: Local storage tampering
100
+
101
+ ### B. Confidentiality threats
102
+
103
+ Threats that leak user data or secrets.
104
+
105
+ - **B1**: Telemetry leaking PII
106
+ - **B2**: Analytics integration leaking variant assignments to third parties
107
+ - **B3**: Debug overlay shown in production
108
+ - **B4**: Error messages revealing experiment structure
109
+ - **B5**: Timing attacks on HMAC verification
110
+ - **B6**: Cache poisoning allowing observation of other users' states
111
+
112
+ ### C. Availability threats
113
+
114
+ Threats that cause service degradation or denial of service.
115
+
116
+ - **C1**: Oversized config
117
+ - **C2**: Exponential regex / ReDoS
118
+ - **C3**: Deeply nested targeting predicates
119
+ - **C4**: Infinite polling loops
120
+ - **C5**: Memory exhaustion via time-travel history
121
+ - **C6**: Crash storm triggering rollback fatigue
122
+
123
+ ### D. Code execution threats
124
+
125
+ Threats that achieve arbitrary code execution.
126
+
127
+ - **D1**: Config containing `eval`-able payloads
128
+ - **D2**: Prototype pollution leading to method hijacking
129
+ - **D3**: Dynamic import of attacker-controlled URLs
130
+ - **D4**: Regex with code execution side effects (does not exist in JS, but in case we ever bind to a native engine)
131
+
132
+ ### E. Misuse threats
133
+
134
+ Threats that come from mis-configuration by the user.
135
+
136
+ - **E1**: Using variantlab for authorization decisions
137
+ - **E2**: Storing secrets in variant values
138
+ - **E3**: Exposing debug overlay in production
139
+ - **E4**: Forgetting to verify HMAC on remote configs
140
+ - **E5**: Using a weak HMAC key
141
+
142
+ ---
143
+
144
+ ## Deep-dives
145
+
146
+ ### Deep-dive: prototype pollution
147
+
148
+ JavaScript's prototype chain is a known attack vector. When parsing untrusted JSON:
149
+
150
+ ```js
151
+ // UNSAFE
152
+ const config = JSON.parse(untrustedInput);
153
+ const merged = { ...defaults, ...config };
154
+ // If untrustedInput contains `{"__proto__": {"admin": true}}`,
155
+ // every object in the program now has `.admin === true`.
156
+ ```
157
+
158
+ **Our mitigations**:
159
+
160
+ 1. **Never spread untrusted objects into others**. Use property-by-property allow-listing.
161
+ 2. **Use `Object.create(null)`** for all parsed data structures. These objects have no prototype.
162
+ 3. **Explicitly reject keys**: `__proto__`, `constructor`, `prototype`, `__defineGetter__`, `__defineSetter__`.
163
+ 4. **Freeze the config** after loading so accidental mutations throw in strict mode.
164
+
165
+ Implementation sketch:
166
+
167
+ ```ts
168
+ function safeParse(input: string): unknown {
169
+ const parsed = JSON.parse(input, (key, value) => {
170
+ if (
171
+ key === "__proto__" ||
172
+ key === "constructor" ||
173
+ key === "prototype"
174
+ ) {
175
+ return undefined; // strip it
176
+ }
177
+ return value;
178
+ });
179
+ return parsed;
180
+ }
181
+ ```
182
+
183
+ ### Deep-dive: ReDoS (Regex Denial of Service)
184
+
185
+ A crafted regex or input can cause catastrophic backtracking, consuming CPU indefinitely.
186
+
187
+ **Our mitigations**:
188
+
189
+ 1. **Avoid regex in hot paths**. Route globs and semver matching use purpose-built parsers, not regex.
190
+ 2. **If we must use regex**, only use constant patterns defined by us, never user-supplied patterns.
191
+ 3. **Input length limits**: config size capped at 1 MB, individual strings capped at 256-512 bytes.
192
+ 4. **Linear-time matchers**: all targeting predicates are O(n) in input size.
193
+
194
+ ### Deep-dive: HMAC timing attacks
195
+
196
+ Naive HMAC comparison:
197
+
198
+ ```ts
199
+ // UNSAFE
200
+ function verify(sig: Uint8Array, expected: Uint8Array): boolean {
201
+ for (let i = 0; i < expected.length; i++) {
202
+ if (sig[i] !== expected[i]) return false;
203
+ }
204
+ return true;
205
+ }
206
+ ```
207
+
208
+ This is timing-dependent. An attacker measuring verification time can guess the HMAC byte-by-byte.
209
+
210
+ **Our mitigation**: Use `crypto.subtle.verify` exclusively. It is specified to be constant-time in conforming implementations of the Web Crypto API.
211
+
212
+ ```ts
213
+ const valid = await crypto.subtle.verify("HMAC", key, signature, data);
214
+ ```
215
+
216
+ We never implement HMAC comparison ourselves.
217
+
218
+ ### Deep-dive: CSP compatibility
219
+
220
+ Content Security Policy is a browser feature that restricts what code can execute. The strictest policies reject:
221
+
222
+ - Inline scripts (`'unsafe-inline'`)
223
+ - `eval` and friends (`'unsafe-eval'`)
224
+ - External scripts not from explicit origins
225
+ - `data:` URIs in script-src
226
+
227
+ **variantlab is CSP-strict compatible**:
228
+
229
+ - Zero uses of `eval`, `Function()`, `setTimeout("string")`, `setInterval("string")`
230
+ - Zero inline `<script>` injection
231
+ - Debug overlay CSS is applied via inline styles only in dev mode (guarded by `__DEV__`)
232
+
233
+ We test CSP compatibility via a Playwright test that loads an example app under `Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'`.
234
+
235
+ ### Deep-dive: supply chain attack surface
236
+
237
+ Even with zero runtime dependencies, our supply chain includes:
238
+
239
+ 1. **Dev dependencies** (tsup, typescript, biome, etc.) used at build time
240
+ 2. **The build pipeline** (GitHub Actions runners, tools)
241
+ 3. **The publish pipeline** (npm registry, our tokens)
242
+ 4. **The end-user's install process** (lockfile, post-install scripts)
243
+
244
+ **Our mitigations**:
245
+
246
+ - **Pin dev dependencies to exact versions** in `pnpm-lock.yaml`
247
+ - **Regular `pnpm audit`** in CI
248
+ - **`socket.dev` checks** on every PR for known bad packages
249
+ - **Sigstore signing** on every release
250
+ - **npm provenance** attesting to the exact GitHub Actions workflow run
251
+ - **Hardware-key-protected maintainer accounts**
252
+ - **No post-install scripts** in any published package
253
+ - **Reproducible build** documentation so anyone can verify tarballs
254
+
255
+ ### Deep-dive: debug overlay in production
256
+
257
+ A developer copies `<VariantDebugOverlay />` into `app.tsx` for dev work, forgets to gate it, and ships it to production. End users can now toggle experiments.
258
+
259
+ **Our mitigations**:
260
+
261
+ 1. **Tree-shake by default**. The overlay lives in a separate entry point (`@variantlab/react-native/debug`) that production builds can skip.
262
+ 2. **Runtime guard**. The overlay component checks `process.env.NODE_ENV !== "production"` and throws a loud error in prod unless an explicit `__forceDevOverlay` prop is set.
263
+ 3. **ESLint rule** (Phase 3) that warns when the overlay is imported without a `__DEV__` guard.
264
+ 4. **Documentation** — every example shows the guard.
265
+ 5. **Documentation alert** — a big warning banner on the overlay docs page.
266
+
267
+ ### Deep-dive: variantlab is not a security control
268
+
269
+ Feature flags are often misused as access control. A flag `isAdmin: true` is trivially flippable by the user via local storage tampering or debug overlay.
270
+
271
+ **Our docs must state prominently**:
272
+
273
+ > **variantlab is not an authorization mechanism. Do not use feature flags to control access to sensitive data or operations. All security decisions must be enforced server-side.**
274
+
275
+ We include this warning in:
276
+
277
+ - `README.md`
278
+ - `SECURITY.md`
279
+ - `docs/features/killer-features.md`
280
+ - The debug overlay itself (a small warning next to the pick buttons)
281
+
282
+ ---
283
+
284
+ ## Open questions
285
+
286
+ 1. **Should we ship a reference HMAC-signing Cloudflare Worker?** Yes — it demonstrates the pattern and gives users a working starting point. Target: Phase 4.
287
+ 2. **Should we support encryption at rest for local storage?** Debated. Most cases don't need it. We'll ship an `EncryptedStorageAdapter` as an optional entry point in Phase 4.
288
+ 3. **Should we have a bug bounty?** Not until post-v1.0. Until then, rely on responsible disclosure.
289
+ 4. **Should core run in a Web Worker?** Interesting but not required. Users can instantiate the engine in a worker themselves if desired.
290
+ 5. **Do we need a Content-Security-Policy reporting endpoint?** No — we don't process reports, users do.
291
+
292
+ ---
293
+
294
+ ## References
295
+
296
+ - Web Crypto API: https://www.w3.org/TR/WebCryptoAPI/
297
+ - OWASP Top 10 for LLMs: https://owasp.org/www-project-top-10-for-large-language-model-applications/
298
+ - CSP Level 3: https://www.w3.org/TR/CSP3/
299
+ - Sigstore: https://www.sigstore.dev/
300
+ - npm Provenance: https://docs.npmjs.com/generating-provenance-statements
301
+ - SLSA: https://slsa.dev/
302
+ - CycloneDX SBOM: https://cyclonedx.org/
303
+ - Prototype Pollution primer: https://github.com/BlackFan/client-side-prototype-pollution
304
+ - ReDoS primer: https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
305
+
306
+ ---
307
+
308
+ ## See also
309
+
310
+ - [`SECURITY.md`](../../SECURITY.md) — the canonical threat model and mitigations
311
+ - [`docs/features/hmac-signing.md`](../features/hmac-signing.md) — HMAC signing implementation
312
+ - [`docs/research/bundle-size-analysis.md`](./bundle-size-analysis.md) — why zero deps matters for security too
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@variantlab/core",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "The framework-agnostic variantlab engine. Zero dependencies, runs anywhere.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,6 +23,7 @@
23
23
  },
24
24
  "files": [
25
25
  "dist",
26
+ "docs",
26
27
  "README.md"
27
28
  ],
28
29
  "publishConfig": {