@supashiphq/javascript-sdk 0.7.7

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Supaship
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,656 @@
1
+ # Supaship JavaScript SDK
2
+
3
+ A type-safe JavaScript SDK for Supaship that provides a simple way to manage feature flags in your JavaScript applications.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @supashiphq/javascript-sdk
9
+ # or
10
+ yarn add @supashiphq/javascript-sdk
11
+ # or
12
+ pnpm add @supashiphq/javascript-sdk
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```typescript
18
+ import { SupaClient, FeaturesWithFallbacks } from '@supashiphq/javascript-sdk'
19
+
20
+ // Define your features with fallback values
21
+ const features = {
22
+ 'new-ui': false,
23
+ 'premium-features': true,
24
+ 'theme-config': {
25
+ primaryColor: '#007bff',
26
+ darkMode: false,
27
+ },
28
+ } satisfies FeaturesWithFallbacks
29
+
30
+ // Create client with features
31
+ const client = new SupaClient({
32
+ apiKey: 'your-api-key',
33
+ environment: 'production',
34
+ features,
35
+ context: {
36
+ userID: '123',
37
+ email: 'user@example.com',
38
+ plan: 'premium',
39
+ },
40
+ })
41
+
42
+ // Get a single feature flag (fully typed!)
43
+ const isNewUIEnabled = await client.getFeature('new-ui')
44
+ // Type: boolean
45
+
46
+ // Get theme configuration (fully typed!)
47
+ const themeConfig = await client.getFeature('theme-config')
48
+ // Type: { primaryColor: string; darkMode: boolean; }
49
+
50
+ // Get multiple features at once
51
+ const allFeatures = await client.getFeatures(['new-ui', 'premium-features'])
52
+ // Type: Record<string, FeatureValue>
53
+ ```
54
+
55
+ ## Type-Safe Features
56
+
57
+ The SDK provides type safety through the `FeaturesWithFallbacks` type. This ensures:
58
+
59
+ - ✅ Features must be defined before use
60
+ - ✅ Feature names are validated at compile-time
61
+ - ✅ Feature values are properly typed
62
+ - ✅ No typos in feature names
63
+
64
+ ### Defining Features
65
+
66
+ ```typescript
67
+ import { FeaturesWithFallbacks } from '@supashiphq/javascript-sdk'
68
+
69
+ // ✅ Recommended: satisfies (preserves exact literal types)
70
+ const features = {
71
+ // Boolean flags
72
+ 'dark-mode': false,
73
+ 'beta-access': true,
74
+
75
+ // Object configurations
76
+ 'ui-config': {
77
+ theme: 'light' as const, // Preserves 'light' literal
78
+ maxItems: 100,
79
+ enableAnimations: true,
80
+ },
81
+
82
+ // Arrays
83
+ 'allowed-regions': ['us-east', 'eu-west'],
84
+
85
+ // Null for disabled/unset features
86
+ 'experimental-feature': null,
87
+ } satisfies FeaturesWithFallbacks
88
+
89
+ // ⚠️ Avoid: Type annotation (widens types, loses precision)
90
+ const features: FeaturesWithFallbacks = {
91
+ 'dark-mode': false,
92
+ 'ui-config': {
93
+ theme: 'light', // Widened to string, not 'light' literal
94
+ maxItems: 100,
95
+ },
96
+ }
97
+
98
+ const client = new SupaClient({
99
+ apiKey: 'your-api-key',
100
+ environment: 'production',
101
+ features,
102
+ context: {},
103
+ })
104
+
105
+ // ✅ TypeScript knows 'dark-mode' exists and returns boolean
106
+ const darkMode = await client.getFeature('dark-mode')
107
+
108
+ // ❌ TypeScript error: 'unknown-feature' doesn't exist
109
+ const value = await client.getFeature('unknown-feature')
110
+ ```
111
+
112
+ ## API Reference
113
+
114
+ ### SupaClient
115
+
116
+ #### Constructor
117
+
118
+ ```typescript
119
+ new SupaClient(config: SupaClientConfig)
120
+ ```
121
+
122
+ **Configuration Options:**
123
+
124
+ | Option | Type | Required | Description |
125
+ | --------------- | ----------------------- | -------- | --------------------------------------------------------------- |
126
+ | `apiKey` | `string` | Yes | Your Supaship API key (Project Settings -> API Keys) |
127
+ | `environment` | `string` | Yes | Environment slug (e.g., `production`, `staging`, `development`) |
128
+ | `features` | `FeaturesWithFallbacks` | Yes | Feature definitions with fallback values |
129
+ | `context` | `FeatureContext` | Yes | Default context for feature evaluation |
130
+ | `networkConfig` | `NetworkConfig` | No | Network settings (endpoints, retry, timeout, custom fetch) |
131
+ | `plugins` | `SupaPlugin[]` | No | Plugins for observability, caching, etc. |
132
+
133
+ **Feature Context:**
134
+
135
+ | Field | Type | Description |
136
+ | --------------- | ---------------------------------- | ------------------------------------------- |
137
+ | `[key: string]` | `string` `number` `boolean` `null` | Key value pairs for feature flag evaluation |
138
+
139
+ **Network Configuration:**
140
+
141
+ | Field | Type | Required | Default | Description |
142
+ | ------------------ | ---------------------------------------------------------------------- | -------- | --------------------------------------- | ------------------------------------------------------------- |
143
+ | `featuresAPIUrl` | `string` | No | `https://edge.supaship.com/v1/features` | Override features API URL |
144
+ | `eventsAPIUrl` | `string` | No | `https://edge.supaship.com/v1/events` | Override events/analytics API URL |
145
+ | `requestTimeoutMs` | `number` | No | `10000` | Abort requests after N ms (uses AbortController if available) |
146
+ | `fetchFn` | `(input: RequestInfo \| URL, init?: RequestInit) => Promise<Response>` | No | — | Custom fetch (pass in Node < 18 or specialized runtimes) |
147
+ | `retry` | `RetryConfig` | No | see below | Retry behavior for network requests |
148
+
149
+ Retry (networkConfig.retry):
150
+
151
+ | Field | Type | Required | Default | Description |
152
+ | ------------- | --------- | -------- | ------- | -------------------------------------- |
153
+ | `enabled` | `boolean` | No | `true` | Enable/disable retries |
154
+ | `maxAttempts` | `number` | No | `3` | Maximum retry attempts |
155
+ | `backoff` | `number` | No | `1000` | Base backoff delay in ms (exponential) |
156
+
157
+ #### Methods
158
+
159
+ ##### getFeature()
160
+
161
+ Retrieves a single feature flag value with full TypeScript type safety.
162
+
163
+ ```typescript
164
+ getFeature<TKey extends keyof TFeatures>(
165
+ featureName: TKey,
166
+ options?: { context?: FeatureContext }
167
+ ): Promise<TFeatures[TKey]>
168
+ ```
169
+
170
+ **Parameters:**
171
+
172
+ - `featureName`: The name of the feature flag (must be defined in your features config)
173
+ - `options.context`: Context override for this request
174
+
175
+ **Examples:**
176
+
177
+ ```typescript
178
+ const features = {
179
+ 'dark-mode': false,
180
+ 'theme-config': { primary: '#007bff' },
181
+ } satisfies FeaturesWithFallbacks
182
+
183
+ const client = new SupaClient({
184
+ apiKey: 'key',
185
+ environment: 'production',
186
+ features,
187
+ context: {},
188
+ })
189
+
190
+ // Get boolean feature
191
+ const darkMode = await client.getFeature('dark-mode')
192
+ // Type: boolean
193
+
194
+ // Get object feature
195
+ const theme = await client.getFeature('theme-config')
196
+ // Type: { primary: string }
197
+
198
+ // With context override
199
+ const darkMode = await client.getFeature('dark-mode', {
200
+ context: { userID: '123', plan: 'premium' },
201
+ })
202
+ ```
203
+
204
+ ##### getFeatures()
205
+
206
+ Retrieves multiple feature flags in a single request.
207
+
208
+ ```typescript
209
+ getFeatures(
210
+ featureNames: (keyof TFeatures)[],
211
+ options?: { context?: FeatureContext }
212
+ ): Promise<Record<string, FeatureValue>>
213
+ ```
214
+
215
+ **Parameters:**
216
+
217
+ - `featureNames`: Array of feature flag names (must be defined in your features config)
218
+ - `options.context`: Context override for this request
219
+
220
+ **Examples:**
221
+
222
+ ```typescript
223
+ const features = {
224
+ 'new-ui': false,
225
+ 'premium-content': false,
226
+ 'beta-mode': false,
227
+ } satisfies FeaturesWithFallbacks
228
+
229
+ const client = new SupaClient({
230
+ apiKey: 'key',
231
+ environment: 'prod',
232
+ features,
233
+ context: {},
234
+ })
235
+
236
+ // Get multiple features
237
+ const results = await client.getFeatures(['new-ui', 'premium-content'])
238
+ // { 'new-ui': true, 'premium-content': false }
239
+
240
+ // With context override
241
+ const results = await client.getFeatures(['new-ui', 'beta-mode'], {
242
+ context: { userID: '123', plan: 'premium' },
243
+ })
244
+ ```
245
+
246
+ ##### updateContext()
247
+
248
+ Updates the default context for the client.
249
+
250
+ ```typescript
251
+ updateContext(context: FeatureContext, mergeWithExisting?: boolean): void
252
+ ```
253
+
254
+ **Parameters:**
255
+
256
+ - `context`: New context data
257
+ - `mergeWithExisting`: Whether to merge with existing context (default: `true`)
258
+
259
+ **Examples:**
260
+
261
+ ```typescript
262
+ // Merge with existing context
263
+ client.updateContext({ userID: '456' })
264
+
265
+ // Replace entire context
266
+ client.updateContext({ userID: '456', newField: 'value' }, false)
267
+ ```
268
+
269
+ ##### getContext()
270
+
271
+ Retrieves the current default context.
272
+
273
+ ```typescript
274
+ getContext(): FeatureContext | undefined
275
+ ```
276
+
277
+ ## Types
278
+
279
+ ### FeatureValue
280
+
281
+ Supported feature flag value types:
282
+
283
+ ```typescript
284
+ type FeatureValue = boolean | null | Record<string, unknown> | unknown[]
285
+ ```
286
+
287
+ - **`boolean`** - Simple on/off flags
288
+ - **`object`** - Structured configuration data (e.g., `{ theme: 'dark', size: 'large' }`)
289
+ - **`array`** - Lists of values (e.g., `['feature-a', 'feature-b']`)
290
+ - **`null`** - Disabled or unset state
291
+
292
+ > **Note:** Strings and numbers are not supported as standalone feature values. Use objects or arrays for complex data.
293
+
294
+ ### FeatureContext
295
+
296
+ Context object for feature evaluation:
297
+
298
+ ```typescript
299
+ interface FeatureContext {
300
+ [key: string]: string | number | boolean | null | undefined
301
+ }
302
+ ```
303
+
304
+ Common context properties:
305
+
306
+ - `userID`: User identifier
307
+ - `email`: User email
308
+ - `plan`: Membership plan (e.g., 'premium', 'free')
309
+ - `version`: Application version (e.g., 1.0.0)
310
+
311
+ > Note: The above are just common examples. You can use any properties in your context object that make sense for your application's feature targeting needs.
312
+
313
+ ## Best Practices
314
+
315
+ ### 1. Define Features Centrally
316
+
317
+ ```typescript
318
+ // features.ts
319
+ import { FeaturesWithFallbacks } from '@supashiphq/javascript-sdk'
320
+
321
+ export const features = {
322
+ 'new-dashboard': false,
323
+ 'premium-features': false,
324
+ 'ui-settings': {
325
+ theme: 'light' as const,
326
+ sidebarCollapsed: false,
327
+ },
328
+ 'enabled-regions': ['us', 'eu'],
329
+ } satisfies FeaturesWithFallbacks
330
+
331
+ // client.ts
332
+ import { SupaClient } from '@supashiphq/javascript-sdk'
333
+ import { features } from './features'
334
+
335
+ export const client = new SupaClient({
336
+ apiKey: process.env.SUPASHIP_API_KEY!,
337
+ environment: process.env.ENVIRONMENT!,
338
+ features,
339
+ context: {},
340
+ })
341
+ ```
342
+
343
+ ### 2. Use `satisfies` for Type Safety
344
+
345
+ Always use `satisfies` instead of type annotations to preserve literal types:
346
+
347
+ ```typescript
348
+ // ✅ Good - preserves literal types
349
+ const features = {
350
+ 'dark-mode': false,
351
+ config: {
352
+ maxItems: 50,
353
+ theme: 'light' as const,
354
+ },
355
+ } satisfies FeaturesWithFallbacks
356
+
357
+ // ❌ Bad - loses literal types
358
+ const features: FeaturesWithFallbacks = {
359
+ 'dark-mode': false,
360
+ config: {
361
+ maxItems: 50,
362
+ theme: 'light', // Widened to string
363
+ },
364
+ }
365
+
366
+ const client = new SupaClient({
367
+ apiKey: 'key',
368
+ environment: 'prod',
369
+ features,
370
+ context: {},
371
+ })
372
+
373
+ // With satisfies: TypeScript knows the exact literal type
374
+ const config = await client.getFeature('config')
375
+ // Type: { maxItems: number; theme: 'light'; }
376
+
377
+ const maxItems: number = config.maxItems // ✅ Type-safe
378
+ const theme = config.theme // ✅ Type-safe, inferred as 'light' literal
379
+ ```
380
+
381
+ ### 3. Use Context for Targeting
382
+
383
+ ```typescript
384
+ const client = new SupaClient({
385
+ apiKey: 'your-api-key',
386
+ environment: 'production',
387
+ features,
388
+ context: {
389
+ userID: user.id,
390
+ email: user.email,
391
+ plan: user.subscriptionPlan,
392
+ version: process.env.APP_VERSION!,
393
+ },
394
+ })
395
+
396
+ // Update context when user state changes
397
+ function onUserLogin(user) {
398
+ client.updateContext({
399
+ userID: user.id,
400
+ email: user.email,
401
+ plan: user.plan,
402
+ })
403
+ }
404
+ ```
405
+
406
+ ### 4. Batch Feature Requests
407
+
408
+ ```typescript
409
+ // ✅ Good - single API call
410
+ const results = await client.getFeatures(['feature-1', 'feature-2', 'feature-3'])
411
+
412
+ // ❌ Less efficient - multiple API calls
413
+ const feature1 = await client.getFeature('feature-1')
414
+ const feature2 = await client.getFeature('feature-2')
415
+ const feature3 = await client.getFeature('feature-3')
416
+ ```
417
+
418
+ ## Plugin System
419
+
420
+ The SDK supports plugins for observability, caching, logging, and more.
421
+
422
+ ### Built-in Plugins
423
+
424
+ #### Toolbar Plugin
425
+
426
+ Visual toolbar for local development and testing.
427
+
428
+ **✨ Auto-enabled in browser environments!** The toolbar is automatically enabled in browsers with `enabled: 'auto'` (shows only on localhost). No manual configuration needed!
429
+
430
+ ```typescript
431
+ import { SupaClient, FeaturesWithFallbacks } from '@supashiphq/javascript-sdk'
432
+
433
+ const features = {
434
+ 'new-ui': false,
435
+ premium: false,
436
+ } satisfies FeaturesWithFallbacks
437
+
438
+ // ✅ Automatic (recommended) - Toolbar auto-enabled in browser with 'auto' mode
439
+ const client = new SupaClient({
440
+ apiKey: 'your-api-key',
441
+ environment: 'development',
442
+ features,
443
+ context: {},
444
+ })
445
+
446
+ // 🎨 Custom configuration
447
+ const client = new SupaClient({
448
+ apiKey: 'your-api-key',
449
+ environment: 'development',
450
+ features,
451
+ context: {},
452
+ toolbar: {
453
+ enabled: true, // Always show
454
+ position: {
455
+ placement: 'bottom-right',
456
+ offset: { x: '1rem', y: '1rem' },
457
+ },
458
+ },
459
+ })
460
+
461
+ // ❌ Opt-out (disable toolbar)
462
+ const client = new SupaClient({
463
+ apiKey: 'your-api-key',
464
+ environment: 'production',
465
+ features,
466
+ context: {},
467
+ toolbar: false,
468
+ })
469
+ ```
470
+
471
+ **Features:**
472
+
473
+ - ✨ **Auto-enabled in browser** - No configuration needed!
474
+ - 🎯 Visual interface showing all configured feature flags
475
+ - 🔄 Override feature flag values locally
476
+ - 💾 Persistent storage in localStorage
477
+ - 🎨 Customizable position
478
+ - 🏠 Smart detection (shows only on localhost by default)
479
+ - 🚫 Easy opt-out with `toolbar: false`
480
+
481
+ ## Examples
482
+
483
+ ### React Integration
484
+
485
+ For React applications, use our dedicated React SDK which provides hooks and components optimized for React:
486
+
487
+ 📦 **[@supashiphq/react-sdk](https://npmjs.com/package/@supashiphq/react-sdk)**
488
+
489
+ ### Node.js Server
490
+
491
+ ```typescript
492
+ import express from 'express'
493
+ import { SupaClient, FeaturesWithFallbacks } from '@supashiphq/javascript-sdk'
494
+
495
+ const features = {
496
+ 'new-api': false,
497
+ 'cache-enabled': true,
498
+ 'rate-limit': { maxRequests: 100, windowMs: 60000 },
499
+ } satisfies FeaturesWithFallbacks
500
+
501
+ const featureClient = new SupaClient({
502
+ apiKey: process.env.SUPASHIP_API_KEY!,
503
+ environment: 'production',
504
+ features,
505
+ context: {},
506
+ })
507
+
508
+ const app = express()
509
+
510
+ app.get('/api/config', async (req, res) => {
511
+ const rateLimit = await featureClient.getFeature('rate-limit')
512
+
513
+ res.json({
514
+ rateLimit: rateLimit,
515
+ newApiEnabled: await featureClient.getFeature('new-api'),
516
+ })
517
+ })
518
+
519
+ app.get('/api/user-features/:userId', async (req, res) => {
520
+ const results = await featureClient.getFeatures(['new-api', 'cache-enabled'], {
521
+ context: {
522
+ userID: req.params.userId,
523
+ plan: req.user.plan,
524
+ region: req.headers['cloudfront-viewer-country'],
525
+ },
526
+ })
527
+
528
+ res.json(results)
529
+ })
530
+ ```
531
+
532
+ ### Vue Integration
533
+
534
+ ```typescript
535
+ // plugins/feature-flags.ts
536
+ import { SupaClient, FeaturesWithFallbacks } from '@supashiphq/javascript-sdk'
537
+
538
+ const features = {
539
+ 'new-nav': false,
540
+ 'dark-mode': false,
541
+ 'premium-content': false,
542
+ } satisfies FeaturesWithFallbacks
543
+
544
+ const client = new SupaClient({
545
+ apiKey: import.meta.env.VITE_SUPASHIP_API_KEY,
546
+ environment: 'production',
547
+ features,
548
+ context: {},
549
+ })
550
+
551
+ export default {
552
+ install(app: App) {
553
+ app.config.globalProperties.$featureFlags = client
554
+ app.provide('featureFlags', client)
555
+ }
556
+ }
557
+
558
+ // components/MyComponent.vue
559
+ <script setup lang="ts">
560
+ import { inject, ref, onMounted } from 'vue'
561
+ import type { SupaClient } from '@supashiphq/javascript-sdk'
562
+
563
+ const featureFlags = inject<SupaClient>('featureFlags')!
564
+ const showNewNav = ref(false)
565
+ const darkMode = ref(false)
566
+
567
+ onMounted(async () => {
568
+ showNewNav.value = await featureFlags.getFeature('new-nav')
569
+ darkMode.value = await featureFlags.getFeature('dark-mode')
570
+ })
571
+ </script>
572
+
573
+ <template>
574
+ <div :class="{ dark: darkMode }">
575
+ <NewNav v-if="showNewNav" />
576
+ <OldNav v-else />
577
+ </div>
578
+ </template>
579
+ ```
580
+
581
+ ### Angular Integration
582
+
583
+ ```typescript
584
+ // feature-flag.service.ts
585
+ import { Injectable } from '@angular/core'
586
+ import { SupaClient, FeaturesWithFallbacks } from '@supashiphq/javascript-sdk'
587
+ import { environment } from '../environments/environment'
588
+
589
+ const features = {
590
+ 'new-dashboard': false,
591
+ analytics: true,
592
+ 'theme-config': { mode: 'light' as const },
593
+ } satisfies FeaturesWithFallbacks
594
+
595
+ @Injectable({
596
+ providedIn: 'root',
597
+ })
598
+ export class FeatureFlagService {
599
+ private client: SupaClient
600
+
601
+ constructor() {
602
+ this.client = new SupaClient({
603
+ apiKey: environment.SUPASHIP_API_KEY,
604
+ environment: 'production',
605
+ features,
606
+ context: {
607
+ userID: this.getCurrentUserId(),
608
+ version: environment.version,
609
+ },
610
+ })
611
+ }
612
+
613
+ async getFeature(featureName: string): Promise<any> {
614
+ return this.client.getFeature(featureName)
615
+ }
616
+
617
+ async getFeatures(featureNames: string[]): Promise<Record<string, any>> {
618
+ return this.client.getFeatures(featureNames)
619
+ }
620
+
621
+ updateContext(context: Record<string, any>) {
622
+ this.client.updateContext(context)
623
+ }
624
+
625
+ private getCurrentUserId(): string {
626
+ return localStorage.getItem('userID') || 'anonymous'
627
+ }
628
+ }
629
+
630
+ // app.component.ts
631
+ import { Component, OnInit } from '@angular/core'
632
+ import { FeatureFlagService } from './feature-flag.service'
633
+
634
+ @Component({
635
+ selector: 'app-root',
636
+ template: `
637
+ <div>
638
+ <new-dashboard *ngIf="showNewDashboard"></new-dashboard>
639
+ <old-dashboard *ngIf="!showNewDashboard"></old-dashboard>
640
+ </div>
641
+ `,
642
+ })
643
+ export class AppComponent implements OnInit {
644
+ showNewDashboard = false
645
+
646
+ constructor(private featureFlags: FeatureFlagService) {}
647
+
648
+ async ngOnInit() {
649
+ this.showNewDashboard = await this.featureFlags.getFeature('new-dashboard')
650
+ }
651
+ }
652
+ ```
653
+
654
+ ## License
655
+
656
+ This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.