@flagpool/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +258 -0
- package/dist/cache.d.ts +6 -0
- package/dist/cache.js +24 -0
- package/dist/client.d.ts +27 -0
- package/dist/client.js +171 -0
- package/dist/crypto.d.ts +107 -0
- package/dist/crypto.js +99 -0
- package/dist/evaluator.d.ts +26 -0
- package/dist/evaluator.js +107 -0
- package/dist/fetcher.d.ts +8 -0
- package/dist/fetcher.js +23 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/realtime.d.ts +23 -0
- package/dist/realtime.js +62 -0
- package/dist/stream.d.ts +3 -0
- package/dist/stream.js +6 -0
- package/dist/types.d.ts +55 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +8 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# Flagpool SDK - TypeScript / JavaScript
|
|
2
|
+
|
|
3
|
+
Official TypeScript/JavaScript SDK for Flagpool feature flags with local evaluation, deterministic rollouts, and encrypted target lists.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@flagpool/sdk)
|
|
6
|
+
[](https://www.npmjs.com/package/@flagpool/sdk)
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @flagpool/sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { FlagpoolClient } from '@flagpool/sdk';
|
|
18
|
+
|
|
19
|
+
const client = new FlagpoolClient({
|
|
20
|
+
apiKey: 'fp_production_xxx',
|
|
21
|
+
environment: 'production',
|
|
22
|
+
context: {
|
|
23
|
+
userId: 'user-123',
|
|
24
|
+
email: 'alice@example.com',
|
|
25
|
+
plan: 'pro',
|
|
26
|
+
country: 'US'
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await client.init();
|
|
31
|
+
|
|
32
|
+
// Boolean flag
|
|
33
|
+
if (client.isEnabled('new-dashboard')) {
|
|
34
|
+
showNewDashboard();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// String flag (A/B test)
|
|
38
|
+
const buttonColor = client.getValue('cta-button-color'); // 'blue' | 'green' | 'orange'
|
|
39
|
+
|
|
40
|
+
// Number flag
|
|
41
|
+
const maxUpload = client.getValue('max-upload-size-mb'); // 10 | 100 | 1000
|
|
42
|
+
|
|
43
|
+
// JSON flag
|
|
44
|
+
const config = client.getValue('checkout-config');
|
|
45
|
+
// { showCoupons: true, maxItems: 50, paymentMethods: [...] }
|
|
46
|
+
|
|
47
|
+
// Clean up when done
|
|
48
|
+
client.close();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
- ✅ **Local evaluation** - No server roundtrip per flag check
|
|
54
|
+
- ✅ **Deterministic rollouts** - Same user always gets same variation
|
|
55
|
+
- ✅ **Multiple flag types** - Boolean, string, number, JSON
|
|
56
|
+
- ✅ **8 targeting operators** - eq, neq, in, nin, contains, startsWith, inTargetList, notInTargetList
|
|
57
|
+
- ✅ **Priority-based rules** - First matching rule wins
|
|
58
|
+
- ✅ **Encrypted target lists** - Optional client-side decryption
|
|
59
|
+
- ✅ **Real-time updates** - Polling for flag changes
|
|
60
|
+
- ✅ **Zero dependencies** - Core SDK has no external dependencies
|
|
61
|
+
|
|
62
|
+
## API Reference
|
|
63
|
+
|
|
64
|
+
### `FlagpoolClient`
|
|
65
|
+
|
|
66
|
+
#### Constructor Options
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
interface FlagpoolClientOptions {
|
|
70
|
+
apiKey: string; // Environment-specific API key
|
|
71
|
+
environment: string; // Environment name (required)
|
|
72
|
+
context?: Record<string, any>; // User context for targeting
|
|
73
|
+
pollingInterval?: number; // Polling interval in ms (default: 30000)
|
|
74
|
+
streaming?: boolean; // Enable real-time updates (default: false)
|
|
75
|
+
urlOverride?: string; // Override API endpoint
|
|
76
|
+
decryptionKey?: string; // Key for client-side target list decryption
|
|
77
|
+
clientSideTargetLists?: boolean; // Enable client-side target list evaluation
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
#### Methods
|
|
82
|
+
|
|
83
|
+
| Method | Description |
|
|
84
|
+
|--------|-------------|
|
|
85
|
+
| `init()` | Initialize client and fetch flags |
|
|
86
|
+
| `isEnabled(key)` | Check if boolean flag is enabled |
|
|
87
|
+
| `getValue(key)` | Get flag value (any type) |
|
|
88
|
+
| `getVariation(key)` | Alias for getValue |
|
|
89
|
+
| `getAllFlags()` | Get all evaluated flag values |
|
|
90
|
+
| `updateContext(context)` | Update user context and re-evaluate |
|
|
91
|
+
| `onChange(callback)` | Subscribe to flag changes |
|
|
92
|
+
| `close()` | Clean up timers and connections |
|
|
93
|
+
|
|
94
|
+
## Targeting Operators
|
|
95
|
+
|
|
96
|
+
| Operator | Description | Example |
|
|
97
|
+
|----------|-------------|---------|
|
|
98
|
+
| `eq` | Equals | `plan == "enterprise"` |
|
|
99
|
+
| `neq` | Not equals | `plan != "free"` |
|
|
100
|
+
| `in` | In list | `country in ["US", "CA"]` |
|
|
101
|
+
| `nin` | Not in list | `country not in ["CN", "RU"]` |
|
|
102
|
+
| `contains` | String contains | `email contains "@company.com"` |
|
|
103
|
+
| `startsWith` | String starts with | `userId startsWith "admin-"` |
|
|
104
|
+
| `inTargetList` | In target list | `userId in beta-testers` |
|
|
105
|
+
| `notInTargetList` | Not in target list | `userId not in blocked-users` |
|
|
106
|
+
|
|
107
|
+
## Dynamic Context Updates
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// Initial context
|
|
111
|
+
const client = new FlagpoolClient({
|
|
112
|
+
apiKey: 'fp_prod_xxx',
|
|
113
|
+
environment: 'production',
|
|
114
|
+
context: { userId: 'user-1', plan: 'free' }
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await client.init();
|
|
118
|
+
|
|
119
|
+
console.log(client.getValue('max-upload-size-mb')); // 10
|
|
120
|
+
|
|
121
|
+
// User upgrades to pro
|
|
122
|
+
client.updateContext({ plan: 'pro' });
|
|
123
|
+
|
|
124
|
+
console.log(client.getValue('max-upload-size-mb')); // 100
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Encrypted Target Lists
|
|
128
|
+
|
|
129
|
+
For offline/edge scenarios, you can decrypt and evaluate target lists client-side.
|
|
130
|
+
|
|
131
|
+
> **Note:** The SDK has NO crypto dependencies. You provide the adapter.
|
|
132
|
+
|
|
133
|
+
### Step 1: Create a Crypto Adapter
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// crypto-adapter.ts (YOUR CODE)
|
|
137
|
+
import type { CryptoAdapter } from 'flagpool-sdk';
|
|
138
|
+
|
|
139
|
+
export async function createNodeCryptoAdapter(): Promise<CryptoAdapter> {
|
|
140
|
+
const crypto = await import('crypto');
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
async decrypt(ciphertext, key, iv, tag) {
|
|
144
|
+
// Your AES-256-GCM decryption implementation
|
|
145
|
+
// See examples/src/crypto-adapter.ts for full implementation
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Step 2: Register and Use
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { FlagpoolClient, setCryptoAdapter } from 'flagpool-sdk';
|
|
155
|
+
import { createNodeCryptoAdapter } from './crypto-adapter';
|
|
156
|
+
|
|
157
|
+
// 1. Create and register your adapter
|
|
158
|
+
const adapter = await createNodeCryptoAdapter();
|
|
159
|
+
setCryptoAdapter(adapter);
|
|
160
|
+
|
|
161
|
+
// 2. Create client with decryption enabled
|
|
162
|
+
const client = new FlagpoolClient({
|
|
163
|
+
apiKey: 'fp_staging_xxx',
|
|
164
|
+
environment: 'staging',
|
|
165
|
+
context: { userId: 'beta-user-1' },
|
|
166
|
+
decryptionKey: 'your-secret-key',
|
|
167
|
+
clientSideTargetLists: true,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await client.init();
|
|
171
|
+
|
|
172
|
+
// Target list rules are now evaluated locally
|
|
173
|
+
client.isEnabled('beta-feature'); // true if userId is in beta-testers
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Real-time Updates
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const client = new FlagpoolClient({
|
|
180
|
+
apiKey: 'fp_prod_xxx',
|
|
181
|
+
environment: 'production',
|
|
182
|
+
context: { userId: 'user-1' },
|
|
183
|
+
streaming: true,
|
|
184
|
+
pollingInterval: 30000 // Poll every 30 seconds
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await client.init();
|
|
188
|
+
|
|
189
|
+
// Listen to flag changes
|
|
190
|
+
client.onChange((flagKey, newValue) => {
|
|
191
|
+
console.log(`Flag ${flagKey} changed to:`, newValue);
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Examples
|
|
196
|
+
|
|
197
|
+
See the `examples/` directory for working examples:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
cd examples
|
|
201
|
+
npm install
|
|
202
|
+
|
|
203
|
+
# Start mock server (uses shared server from test-harness)
|
|
204
|
+
npm run server
|
|
205
|
+
|
|
206
|
+
# Run tests
|
|
207
|
+
npm run test:dev # Development environment
|
|
208
|
+
npm run test:staging # Staging environment
|
|
209
|
+
npm run test:evaluator # Comprehensive evaluator tests
|
|
210
|
+
npm run test:client-side # Client-side target list evaluation
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Flag Types
|
|
214
|
+
|
|
215
|
+
### Boolean Flags
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
if (client.isEnabled('feature-flag')) {
|
|
219
|
+
// Feature is enabled
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### String Flags (A/B Tests)
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
const variant = client.getValue('button-color');
|
|
227
|
+
// 'blue' | 'green' | 'orange'
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Number Flags
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
const limit = client.getValue('rate-limit');
|
|
234
|
+
// 100 | 1000 | 10000
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### JSON Flags
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
const config = client.getValue('checkout-config');
|
|
241
|
+
// { showCoupons: true, maxItems: 50, ... }
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Conformance
|
|
245
|
+
|
|
246
|
+
This SDK passes all 60 conformance tests defined in `/spec/test-vectors.json`.
|
|
247
|
+
|
|
248
|
+
Run conformance tests:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
cd ../../test-harness
|
|
252
|
+
npm run test:typescript
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## License
|
|
256
|
+
|
|
257
|
+
MIT
|
|
258
|
+
|
package/dist/cache.d.ts
ADDED
package/dist/cache.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class Cache {
|
|
2
|
+
constructor(key) {
|
|
3
|
+
this.key = key;
|
|
4
|
+
}
|
|
5
|
+
load() {
|
|
6
|
+
try {
|
|
7
|
+
if (typeof localStorage === "undefined")
|
|
8
|
+
return null;
|
|
9
|
+
const raw = localStorage.getItem(this.key);
|
|
10
|
+
return raw ? JSON.parse(raw) : null;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
save(data) {
|
|
17
|
+
try {
|
|
18
|
+
if (typeof localStorage === "undefined")
|
|
19
|
+
return;
|
|
20
|
+
localStorage.setItem(this.key, JSON.stringify(data));
|
|
21
|
+
}
|
|
22
|
+
catch { }
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { FlagpoolClientOptions, FlagChangedCallback } from "./types";
|
|
2
|
+
export declare class FlagpoolClient {
|
|
3
|
+
private options;
|
|
4
|
+
private flags;
|
|
5
|
+
private evaluator;
|
|
6
|
+
private cache;
|
|
7
|
+
private pollingTimer;
|
|
8
|
+
private realtimeConnection?;
|
|
9
|
+
private changeListeners;
|
|
10
|
+
private targetLists;
|
|
11
|
+
constructor(options: FlagpoolClientOptions);
|
|
12
|
+
init(): Promise<void>;
|
|
13
|
+
private refresh;
|
|
14
|
+
/**
|
|
15
|
+
* Process and decrypt target lists if needed
|
|
16
|
+
*/
|
|
17
|
+
private processTargetLists;
|
|
18
|
+
private setupRealtime;
|
|
19
|
+
isEnabled(key: string, defaultValue?: boolean): boolean;
|
|
20
|
+
getValue(key: string): any;
|
|
21
|
+
getVariation(key: string): any;
|
|
22
|
+
getAllFlags(): Record<string, any>;
|
|
23
|
+
updateContext(newContext: Record<string, any>): void;
|
|
24
|
+
onChange(callback: FlagChangedCallback): () => boolean;
|
|
25
|
+
private notifyChanges;
|
|
26
|
+
close(): void;
|
|
27
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { Evaluator } from "./evaluator";
|
|
2
|
+
import { fetchFlags } from "./fetcher";
|
|
3
|
+
import { Cache } from "./cache";
|
|
4
|
+
import { RealtimeConnection } from "./realtime";
|
|
5
|
+
import { decryptTargetLists, isEncryptedTargetLists } from "./crypto";
|
|
6
|
+
export class FlagpoolClient {
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.flags = new Map();
|
|
9
|
+
this.evaluator = new Evaluator();
|
|
10
|
+
this.cache = new Cache("flagpool_flags");
|
|
11
|
+
this.changeListeners = new Set();
|
|
12
|
+
this.targetLists = null;
|
|
13
|
+
if (!options.apiKey)
|
|
14
|
+
throw new Error("apiKey is required");
|
|
15
|
+
if (!options.environment)
|
|
16
|
+
throw new Error("environment is required");
|
|
17
|
+
// Validate client-side target list requirements
|
|
18
|
+
if (options.clientSideTargetLists && !options.decryptionKey) {
|
|
19
|
+
throw new Error("decryptionKey is required when clientSideTargetLists is enabled");
|
|
20
|
+
}
|
|
21
|
+
this.options = options;
|
|
22
|
+
}
|
|
23
|
+
async init() {
|
|
24
|
+
// Try loading from cache
|
|
25
|
+
const cached = this.cache.load();
|
|
26
|
+
if (cached === null || cached === void 0 ? void 0 : cached.flags) {
|
|
27
|
+
Object.entries(cached.flags).forEach(([key, flag]) => {
|
|
28
|
+
this.flags.set(key, flag);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// Fetch fresh flags
|
|
32
|
+
try {
|
|
33
|
+
await this.refresh();
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
// If fetch failed but we have cache, continue; otherwise throw
|
|
37
|
+
if (this.flags.size === 0)
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
// Setup polling if configured
|
|
41
|
+
if (this.options.pollingInterval && !this.options.streaming) {
|
|
42
|
+
this.pollingTimer = setInterval(() => this.refresh().catch(() => { }), this.options.pollingInterval);
|
|
43
|
+
}
|
|
44
|
+
// Setup realtime streaming if enabled (uses polling internally)
|
|
45
|
+
if (this.options.streaming) {
|
|
46
|
+
this.setupRealtime();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async refresh() {
|
|
50
|
+
const response = await fetchFlags({
|
|
51
|
+
apiKey: this.options.apiKey,
|
|
52
|
+
environment: this.options.environment,
|
|
53
|
+
context: this.options.context,
|
|
54
|
+
urlOverride: this.options.urlOverride
|
|
55
|
+
});
|
|
56
|
+
// Update flags map
|
|
57
|
+
this.flags.clear();
|
|
58
|
+
response.flags.forEach(flag => {
|
|
59
|
+
this.flags.set(flag.key, flag);
|
|
60
|
+
});
|
|
61
|
+
// Handle target lists if client-side evaluation is enabled
|
|
62
|
+
if (this.options.clientSideTargetLists && this.options.decryptionKey && response.targetLists) {
|
|
63
|
+
await this.processTargetLists(response.targetLists);
|
|
64
|
+
}
|
|
65
|
+
// Cache the response
|
|
66
|
+
this.cache.save({
|
|
67
|
+
flags: Object.fromEntries(this.flags),
|
|
68
|
+
timestamp: response.timestamp
|
|
69
|
+
});
|
|
70
|
+
// Notify listeners of changes
|
|
71
|
+
this.notifyChanges();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Process and decrypt target lists if needed
|
|
75
|
+
*/
|
|
76
|
+
async processTargetLists(targetLists) {
|
|
77
|
+
if (isEncryptedTargetLists(targetLists)) {
|
|
78
|
+
// Decrypt target lists
|
|
79
|
+
try {
|
|
80
|
+
this.targetLists = await decryptTargetLists(targetLists, this.options.decryptionKey);
|
|
81
|
+
this.evaluator.setTargetLists(this.targetLists, true);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
console.error("Failed to decrypt target lists:", err);
|
|
85
|
+
this.targetLists = null;
|
|
86
|
+
this.evaluator.setTargetLists(null, false);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
// Already decrypted (for testing/development)
|
|
91
|
+
this.targetLists = targetLists;
|
|
92
|
+
this.evaluator.setTargetLists(this.targetLists, true);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
setupRealtime() {
|
|
96
|
+
var _a;
|
|
97
|
+
const endpoint = (_a = this.options.urlOverride) !== null && _a !== void 0 ? _a : `${process.env.SUPABASE_URL || 'http://localhost:54321'}/functions/v1/get-flags`;
|
|
98
|
+
this.realtimeConnection = new RealtimeConnection({
|
|
99
|
+
endpoint,
|
|
100
|
+
apiKey: this.options.apiKey,
|
|
101
|
+
environment: this.options.environment,
|
|
102
|
+
context: this.options.context,
|
|
103
|
+
onUpdate: () => {
|
|
104
|
+
// Flags changed, refresh
|
|
105
|
+
this.refresh().catch(() => { });
|
|
106
|
+
},
|
|
107
|
+
pollInterval: this.options.pollingInterval || 30000
|
|
108
|
+
});
|
|
109
|
+
this.realtimeConnection.start();
|
|
110
|
+
}
|
|
111
|
+
isEnabled(key, defaultValue = false) {
|
|
112
|
+
const value = this.getValue(key);
|
|
113
|
+
if (value === null || value === undefined)
|
|
114
|
+
return defaultValue;
|
|
115
|
+
return Boolean(value);
|
|
116
|
+
}
|
|
117
|
+
getValue(key) {
|
|
118
|
+
const flag = this.flags.get(key);
|
|
119
|
+
if (!flag)
|
|
120
|
+
return null;
|
|
121
|
+
const context = {
|
|
122
|
+
environment: this.options.environment,
|
|
123
|
+
...this.options.context
|
|
124
|
+
};
|
|
125
|
+
return this.evaluator.evaluate(flag, context);
|
|
126
|
+
}
|
|
127
|
+
getVariation(key) {
|
|
128
|
+
return this.getValue(key);
|
|
129
|
+
}
|
|
130
|
+
getAllFlags() {
|
|
131
|
+
const result = {};
|
|
132
|
+
const context = {
|
|
133
|
+
environment: this.options.environment,
|
|
134
|
+
...this.options.context
|
|
135
|
+
};
|
|
136
|
+
this.flags.forEach((flag, key) => {
|
|
137
|
+
result[key] = this.evaluator.evaluate(flag, context);
|
|
138
|
+
});
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
updateContext(newContext) {
|
|
142
|
+
var _a;
|
|
143
|
+
this.options.context = {
|
|
144
|
+
...((_a = this.options.context) !== null && _a !== void 0 ? _a : {}),
|
|
145
|
+
...newContext
|
|
146
|
+
};
|
|
147
|
+
// Notify listeners of potential changes
|
|
148
|
+
this.notifyChanges();
|
|
149
|
+
}
|
|
150
|
+
onChange(callback) {
|
|
151
|
+
this.changeListeners.add(callback);
|
|
152
|
+
return () => this.changeListeners.delete(callback);
|
|
153
|
+
}
|
|
154
|
+
notifyChanges() {
|
|
155
|
+
const allFlags = this.getAllFlags();
|
|
156
|
+
this.changeListeners.forEach(listener => {
|
|
157
|
+
Object.entries(allFlags).forEach(([key, value]) => {
|
|
158
|
+
listener(key, value);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
close() {
|
|
163
|
+
if (this.pollingTimer) {
|
|
164
|
+
clearInterval(this.pollingTimer);
|
|
165
|
+
}
|
|
166
|
+
if (this.realtimeConnection) {
|
|
167
|
+
this.realtimeConnection.stop();
|
|
168
|
+
}
|
|
169
|
+
this.changeListeners.clear();
|
|
170
|
+
}
|
|
171
|
+
}
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-256-GCM Encryption/Decryption for Target Lists
|
|
3
|
+
*
|
|
4
|
+
* This module provides secure encryption for target lists, allowing
|
|
5
|
+
* flag configurations to be shared publicly while keeping sensitive
|
|
6
|
+
* user data (like user IDs, emails) encrypted.
|
|
7
|
+
*
|
|
8
|
+
* ARCHITECTURE NOTE:
|
|
9
|
+
* ------------------
|
|
10
|
+
* This crypto module is OPTIONAL and only needed for client-side target list
|
|
11
|
+
* evaluation in offline/edge scenarios. The recommended approach is:
|
|
12
|
+
*
|
|
13
|
+
* 1. SERVER-SIDE EVALUATION (Default): Server evaluates inTargetList rules
|
|
14
|
+
* before sending flags. SDK receives pre-filtered flags, no crypto needed.
|
|
15
|
+
*
|
|
16
|
+
* 2. CLIENT-SIDE EVALUATION (Optional): For offline/edge cases, inject a
|
|
17
|
+
* crypto adapter. This keeps the core SDK dependency-free.
|
|
18
|
+
*
|
|
19
|
+
* To use client-side decryption, provide a CryptoAdapter implementation
|
|
20
|
+
* for your platform (Node.js, Browser, etc.)
|
|
21
|
+
*/
|
|
22
|
+
export interface EncryptedData {
|
|
23
|
+
_encrypted: true;
|
|
24
|
+
_algorithm: 'AES-256-GCM';
|
|
25
|
+
_iv: string;
|
|
26
|
+
_data: string;
|
|
27
|
+
_tag: string;
|
|
28
|
+
}
|
|
29
|
+
export interface TargetList {
|
|
30
|
+
attributeKey: string;
|
|
31
|
+
values: string[];
|
|
32
|
+
}
|
|
33
|
+
export interface DecryptedTargetLists {
|
|
34
|
+
[key: string]: TargetList;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Crypto Adapter Interface
|
|
38
|
+
*
|
|
39
|
+
* Implement this interface for your platform to enable client-side
|
|
40
|
+
* target list decryption. This keeps the core SDK dependency-free.
|
|
41
|
+
*
|
|
42
|
+
* Example implementations:
|
|
43
|
+
* - Node.js: Use built-in 'crypto' module
|
|
44
|
+
* - Browser: Use Web Crypto API
|
|
45
|
+
* - React Native: Use expo-crypto or react-native-crypto
|
|
46
|
+
*/
|
|
47
|
+
export interface CryptoAdapter {
|
|
48
|
+
/**
|
|
49
|
+
* Decrypt AES-256-GCM encrypted data
|
|
50
|
+
* @param ciphertext - Base64 encoded ciphertext
|
|
51
|
+
* @param key - Decryption key string
|
|
52
|
+
* @param iv - Base64 encoded initialization vector
|
|
53
|
+
* @param tag - Base64 encoded authentication tag
|
|
54
|
+
* @returns Decrypted plaintext string
|
|
55
|
+
*/
|
|
56
|
+
decrypt(ciphertext: string, key: string, iv: string, tag: string): Promise<string>;
|
|
57
|
+
/**
|
|
58
|
+
* Encrypt plaintext with AES-256-GCM
|
|
59
|
+
* @param plaintext - String to encrypt
|
|
60
|
+
* @param key - Encryption key string
|
|
61
|
+
* @returns Encrypted data with IV and auth tag
|
|
62
|
+
*/
|
|
63
|
+
encrypt?(plaintext: string, key: string): Promise<{
|
|
64
|
+
ciphertext: string;
|
|
65
|
+
iv: string;
|
|
66
|
+
tag: string;
|
|
67
|
+
}>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Set the crypto adapter for client-side decryption
|
|
71
|
+
*
|
|
72
|
+
* Call this once at app startup if you need client-side target list evaluation.
|
|
73
|
+
* If not set, the SDK will rely on server-side evaluation (recommended).
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // Node.js
|
|
77
|
+
* import { setCryptoAdapter, createNodeCryptoAdapter } from 'flagpool-sdk';
|
|
78
|
+
* setCryptoAdapter(createNodeCryptoAdapter());
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* // Browser
|
|
82
|
+
* import { setCryptoAdapter, createWebCryptoAdapter } from 'flagpool-sdk';
|
|
83
|
+
* setCryptoAdapter(createWebCryptoAdapter());
|
|
84
|
+
*/
|
|
85
|
+
export declare function setCryptoAdapter(adapter: CryptoAdapter): void;
|
|
86
|
+
/**
|
|
87
|
+
* Get the current crypto adapter
|
|
88
|
+
*/
|
|
89
|
+
export declare function getCryptoAdapter(): CryptoAdapter | null;
|
|
90
|
+
/**
|
|
91
|
+
* Check if a crypto adapter is available
|
|
92
|
+
*/
|
|
93
|
+
export declare function hasCryptoAdapter(): boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Decrypt target lists with AES-256-GCM
|
|
96
|
+
* Requires a crypto adapter to be set via setCryptoAdapter()
|
|
97
|
+
*/
|
|
98
|
+
export declare function decryptTargetLists(encryptedData: EncryptedData, decryptionKey: string): Promise<DecryptedTargetLists>;
|
|
99
|
+
/**
|
|
100
|
+
* Encrypt target lists with AES-256-GCM
|
|
101
|
+
* Requires a crypto adapter with encrypt() support
|
|
102
|
+
*/
|
|
103
|
+
export declare function encryptTargetLists(targetLists: DecryptedTargetLists, encryptionKey: string): Promise<EncryptedData>;
|
|
104
|
+
/**
|
|
105
|
+
* Check if an object is encrypted target lists
|
|
106
|
+
*/
|
|
107
|
+
export declare function isEncryptedTargetLists(obj: any): obj is EncryptedData;
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-256-GCM Encryption/Decryption for Target Lists
|
|
3
|
+
*
|
|
4
|
+
* This module provides secure encryption for target lists, allowing
|
|
5
|
+
* flag configurations to be shared publicly while keeping sensitive
|
|
6
|
+
* user data (like user IDs, emails) encrypted.
|
|
7
|
+
*
|
|
8
|
+
* ARCHITECTURE NOTE:
|
|
9
|
+
* ------------------
|
|
10
|
+
* This crypto module is OPTIONAL and only needed for client-side target list
|
|
11
|
+
* evaluation in offline/edge scenarios. The recommended approach is:
|
|
12
|
+
*
|
|
13
|
+
* 1. SERVER-SIDE EVALUATION (Default): Server evaluates inTargetList rules
|
|
14
|
+
* before sending flags. SDK receives pre-filtered flags, no crypto needed.
|
|
15
|
+
*
|
|
16
|
+
* 2. CLIENT-SIDE EVALUATION (Optional): For offline/edge cases, inject a
|
|
17
|
+
* crypto adapter. This keeps the core SDK dependency-free.
|
|
18
|
+
*
|
|
19
|
+
* To use client-side decryption, provide a CryptoAdapter implementation
|
|
20
|
+
* for your platform (Node.js, Browser, etc.)
|
|
21
|
+
*/
|
|
22
|
+
// Global crypto adapter - set via setCryptoAdapter()
|
|
23
|
+
let cryptoAdapter = null;
|
|
24
|
+
/**
|
|
25
|
+
* Set the crypto adapter for client-side decryption
|
|
26
|
+
*
|
|
27
|
+
* Call this once at app startup if you need client-side target list evaluation.
|
|
28
|
+
* If not set, the SDK will rely on server-side evaluation (recommended).
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* // Node.js
|
|
32
|
+
* import { setCryptoAdapter, createNodeCryptoAdapter } from 'flagpool-sdk';
|
|
33
|
+
* setCryptoAdapter(createNodeCryptoAdapter());
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // Browser
|
|
37
|
+
* import { setCryptoAdapter, createWebCryptoAdapter } from 'flagpool-sdk';
|
|
38
|
+
* setCryptoAdapter(createWebCryptoAdapter());
|
|
39
|
+
*/
|
|
40
|
+
export function setCryptoAdapter(adapter) {
|
|
41
|
+
cryptoAdapter = adapter;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get the current crypto adapter
|
|
45
|
+
*/
|
|
46
|
+
export function getCryptoAdapter() {
|
|
47
|
+
return cryptoAdapter;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Check if a crypto adapter is available
|
|
51
|
+
*/
|
|
52
|
+
export function hasCryptoAdapter() {
|
|
53
|
+
return cryptoAdapter !== null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Decrypt target lists with AES-256-GCM
|
|
57
|
+
* Requires a crypto adapter to be set via setCryptoAdapter()
|
|
58
|
+
*/
|
|
59
|
+
export async function decryptTargetLists(encryptedData, decryptionKey) {
|
|
60
|
+
if (!cryptoAdapter) {
|
|
61
|
+
throw new Error('No crypto adapter configured. Either:\n' +
|
|
62
|
+
'1. Use server-side target list evaluation (recommended)\n' +
|
|
63
|
+
'2. Call setCryptoAdapter() with a platform-specific adapter\n' +
|
|
64
|
+
'\n' +
|
|
65
|
+
'See documentation for available adapters:\n' +
|
|
66
|
+
'- createNodeCryptoAdapter() for Node.js\n' +
|
|
67
|
+
'- createWebCryptoAdapter() for browsers');
|
|
68
|
+
}
|
|
69
|
+
if (!encryptedData._encrypted || encryptedData._algorithm !== 'AES-256-GCM') {
|
|
70
|
+
throw new Error('Invalid encrypted data format');
|
|
71
|
+
}
|
|
72
|
+
const plaintext = await cryptoAdapter.decrypt(encryptedData._data, decryptionKey, encryptedData._iv, encryptedData._tag);
|
|
73
|
+
return JSON.parse(plaintext);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Encrypt target lists with AES-256-GCM
|
|
77
|
+
* Requires a crypto adapter with encrypt() support
|
|
78
|
+
*/
|
|
79
|
+
export async function encryptTargetLists(targetLists, encryptionKey) {
|
|
80
|
+
if (!(cryptoAdapter === null || cryptoAdapter === void 0 ? void 0 : cryptoAdapter.encrypt)) {
|
|
81
|
+
throw new Error('No crypto adapter with encryption support configured.\n' +
|
|
82
|
+
'Call setCryptoAdapter() with an adapter that implements encrypt().');
|
|
83
|
+
}
|
|
84
|
+
const plaintext = JSON.stringify(targetLists);
|
|
85
|
+
const { ciphertext, iv, tag } = await cryptoAdapter.encrypt(plaintext, encryptionKey);
|
|
86
|
+
return {
|
|
87
|
+
_encrypted: true,
|
|
88
|
+
_algorithm: 'AES-256-GCM',
|
|
89
|
+
_iv: iv,
|
|
90
|
+
_data: ciphertext,
|
|
91
|
+
_tag: tag
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Check if an object is encrypted target lists
|
|
96
|
+
*/
|
|
97
|
+
export function isEncryptedTargetLists(obj) {
|
|
98
|
+
return obj && obj._encrypted === true && obj._algorithm === 'AES-256-GCM';
|
|
99
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { FlagDefinition, DecryptedTargetLists } from "./types";
|
|
2
|
+
export declare class Evaluator {
|
|
3
|
+
private targetLists;
|
|
4
|
+
private clientSideTargetLists;
|
|
5
|
+
/**
|
|
6
|
+
* Set target lists for client-side evaluation
|
|
7
|
+
*/
|
|
8
|
+
setTargetLists(targetLists: DecryptedTargetLists | null, clientSideEnabled?: boolean): void;
|
|
9
|
+
/**
|
|
10
|
+
* Check if client-side target list evaluation is enabled
|
|
11
|
+
*/
|
|
12
|
+
hasTargetLists(): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Evaluate a flag with priority-based rules.
|
|
15
|
+
* If clientSideTargetLists is enabled, inTargetList/notInTargetList rules
|
|
16
|
+
* are evaluated locally using decrypted target lists.
|
|
17
|
+
*/
|
|
18
|
+
evaluate(flag: FlagDefinition, context?: Record<string, any>): any;
|
|
19
|
+
private matchesRule;
|
|
20
|
+
/**
|
|
21
|
+
* Evaluate inTargetList/notInTargetList rules
|
|
22
|
+
*/
|
|
23
|
+
private evaluateTargetListRule;
|
|
24
|
+
private compare;
|
|
25
|
+
private inRollout;
|
|
26
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { hashString } from "./utils";
|
|
2
|
+
export class Evaluator {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.targetLists = null;
|
|
5
|
+
this.clientSideTargetLists = false;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Set target lists for client-side evaluation
|
|
9
|
+
*/
|
|
10
|
+
setTargetLists(targetLists, clientSideEnabled = false) {
|
|
11
|
+
this.targetLists = targetLists;
|
|
12
|
+
this.clientSideTargetLists = clientSideEnabled;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Check if client-side target list evaluation is enabled
|
|
16
|
+
*/
|
|
17
|
+
hasTargetLists() {
|
|
18
|
+
return this.clientSideTargetLists && this.targetLists !== null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Evaluate a flag with priority-based rules.
|
|
22
|
+
* If clientSideTargetLists is enabled, inTargetList/notInTargetList rules
|
|
23
|
+
* are evaluated locally using decrypted target lists.
|
|
24
|
+
*/
|
|
25
|
+
evaluate(flag, context = {}) {
|
|
26
|
+
var _a, _b, _c, _d, _e, _f;
|
|
27
|
+
// 1. Sort rules by priority (lower = higher priority)
|
|
28
|
+
const sortedRules = (flag.rules || []).sort((a, b) => a.priority - b.priority);
|
|
29
|
+
// 2. Evaluate rules in priority order
|
|
30
|
+
for (const rule of sortedRules) {
|
|
31
|
+
if (this.matchesRule(rule, context)) {
|
|
32
|
+
return (_b = (_a = flag.variations[rule.variation]) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : (_c = flag.variations[flag.defaultVariation]) === null || _c === void 0 ? void 0 : _c.value;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// 3. Check rollout percentage (if less than 100%)
|
|
36
|
+
if (flag.rolloutPercentage < 100) {
|
|
37
|
+
if (!this.inRollout(flag, context)) {
|
|
38
|
+
// Not in rollout - return "off" variation (assume index 1)
|
|
39
|
+
return (_e = (_d = flag.variations[1]) === null || _d === void 0 ? void 0 : _d.value) !== null && _e !== void 0 ? _e : false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// 4. Fall back to default variation
|
|
43
|
+
return (_f = flag.variations[flag.defaultVariation]) === null || _f === void 0 ? void 0 : _f.value;
|
|
44
|
+
}
|
|
45
|
+
matchesRule(rule, context) {
|
|
46
|
+
const contextValue = context[rule.attribute];
|
|
47
|
+
// Handle target list operators
|
|
48
|
+
if (rule.operator === 'inTargetList' || rule.operator === 'notInTargetList') {
|
|
49
|
+
return this.evaluateTargetListRule(rule, context);
|
|
50
|
+
}
|
|
51
|
+
// Standard operators
|
|
52
|
+
return this.compare(contextValue, rule.operator, rule.value);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Evaluate inTargetList/notInTargetList rules
|
|
56
|
+
*/
|
|
57
|
+
evaluateTargetListRule(rule, context) {
|
|
58
|
+
const targetListKey = rule.targetListKey;
|
|
59
|
+
if (!targetListKey) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
// If client-side target lists are enabled, evaluate locally
|
|
63
|
+
if (this.clientSideTargetLists && this.targetLists) {
|
|
64
|
+
const targetList = this.targetLists[targetListKey];
|
|
65
|
+
if (!targetList) {
|
|
66
|
+
// Target list not found - rule doesn't match
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
// Get the context value for the target list's attribute
|
|
70
|
+
const contextValue = context[targetList.attributeKey];
|
|
71
|
+
if (contextValue === undefined || contextValue === null) {
|
|
72
|
+
return rule.operator === 'notInTargetList';
|
|
73
|
+
}
|
|
74
|
+
const isInList = targetList.values.includes(String(contextValue));
|
|
75
|
+
return rule.operator === 'inTargetList' ? isInList : !isInList;
|
|
76
|
+
}
|
|
77
|
+
// Server-side evaluation fallback:
|
|
78
|
+
// If the rule exists here, assume server already filtered it
|
|
79
|
+
const contextValue = context[rule.attribute];
|
|
80
|
+
return contextValue !== undefined;
|
|
81
|
+
}
|
|
82
|
+
compare(contextValue, operator, ruleValue) {
|
|
83
|
+
switch (operator) {
|
|
84
|
+
case 'eq':
|
|
85
|
+
return contextValue === ruleValue;
|
|
86
|
+
case 'neq':
|
|
87
|
+
return contextValue !== ruleValue;
|
|
88
|
+
case 'in':
|
|
89
|
+
return Array.isArray(ruleValue) && ruleValue.includes(contextValue);
|
|
90
|
+
case 'nin':
|
|
91
|
+
return Array.isArray(ruleValue) && !ruleValue.includes(contextValue);
|
|
92
|
+
case 'contains':
|
|
93
|
+
return String(contextValue).includes(String(ruleValue));
|
|
94
|
+
case 'startsWith':
|
|
95
|
+
return String(contextValue).startsWith(String(ruleValue));
|
|
96
|
+
default:
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
inRollout(flag, context) {
|
|
101
|
+
const userId = context.userId || context.id;
|
|
102
|
+
if (!userId)
|
|
103
|
+
return true; // If no user ID, include in rollout
|
|
104
|
+
const hash = hashString(String(userId) + flag.key) % 100;
|
|
105
|
+
return hash < flag.rolloutPercentage;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { GetFlagsResponse } from "./types";
|
|
2
|
+
export interface FetchFlagsOptions {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
environment: string;
|
|
5
|
+
context?: Record<string, any>;
|
|
6
|
+
urlOverride?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function fetchFlags(options: FetchFlagsOptions): Promise<GetFlagsResponse>;
|
package/dist/fetcher.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export async function fetchFlags(options) {
|
|
2
|
+
var _a;
|
|
3
|
+
// Default to Supabase edge function endpoint
|
|
4
|
+
const url = (_a = options.urlOverride) !== null && _a !== void 0 ? _a : `${process.env.SUPABASE_URL || 'http://localhost:54321'}/functions/v1/get-flags`;
|
|
5
|
+
const res = await fetch(url, {
|
|
6
|
+
method: 'POST',
|
|
7
|
+
headers: {
|
|
8
|
+
'Content-Type': 'application/json',
|
|
9
|
+
},
|
|
10
|
+
body: JSON.stringify({
|
|
11
|
+
apiKey: options.apiKey,
|
|
12
|
+
context: {
|
|
13
|
+
environment: options.environment,
|
|
14
|
+
...options.context
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
});
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
const error = await res.json().catch(() => ({ error: 'Failed to fetch flags' }));
|
|
20
|
+
throw new Error(error.error || `Failed to fetch flags: ${res.status}`);
|
|
21
|
+
}
|
|
22
|
+
return res.json();
|
|
23
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export * from "./client";
|
|
2
|
+
export * from "./types";
|
|
3
|
+
export { encryptTargetLists, decryptTargetLists, isEncryptedTargetLists, setCryptoAdapter, getCryptoAdapter, hasCryptoAdapter } from "./crypto";
|
|
4
|
+
export type { EncryptedData, TargetList, DecryptedTargetLists, CryptoAdapter } from "./crypto";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal real-time connection for flag updates
|
|
3
|
+
* Uses polling as a simple, dependency-free solution
|
|
4
|
+
*/
|
|
5
|
+
export interface RealtimeOptions {
|
|
6
|
+
endpoint: string;
|
|
7
|
+
apiKey: string;
|
|
8
|
+
environment: string;
|
|
9
|
+
context?: Record<string, any>;
|
|
10
|
+
onUpdate: () => void;
|
|
11
|
+
pollInterval?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare class RealtimeConnection {
|
|
14
|
+
private options;
|
|
15
|
+
private pollTimer;
|
|
16
|
+
private lastTimestamp;
|
|
17
|
+
private isRunning;
|
|
18
|
+
constructor(options: RealtimeOptions);
|
|
19
|
+
start(): Promise<void>;
|
|
20
|
+
private poll;
|
|
21
|
+
stop(): void;
|
|
22
|
+
updateContext(context: Record<string, any>): void;
|
|
23
|
+
}
|
package/dist/realtime.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal real-time connection for flag updates
|
|
3
|
+
* Uses polling as a simple, dependency-free solution
|
|
4
|
+
*/
|
|
5
|
+
export class RealtimeConnection {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.lastTimestamp = null;
|
|
8
|
+
this.isRunning = false;
|
|
9
|
+
this.options = {
|
|
10
|
+
pollInterval: 30000, // Default: 30 seconds
|
|
11
|
+
...options
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
async start() {
|
|
15
|
+
if (this.isRunning)
|
|
16
|
+
return;
|
|
17
|
+
this.isRunning = true;
|
|
18
|
+
// Poll immediately
|
|
19
|
+
await this.poll();
|
|
20
|
+
// Setup polling interval
|
|
21
|
+
this.pollTimer = setInterval(() => this.poll().catch(() => { }), this.options.pollInterval);
|
|
22
|
+
}
|
|
23
|
+
async poll() {
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(this.options.endpoint, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: {
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
apiKey: this.options.apiKey,
|
|
32
|
+
context: {
|
|
33
|
+
environment: this.options.environment,
|
|
34
|
+
...this.options.context
|
|
35
|
+
},
|
|
36
|
+
since: this.lastTimestamp // Server can optimize based on this
|
|
37
|
+
})
|
|
38
|
+
});
|
|
39
|
+
if (!response.ok)
|
|
40
|
+
return;
|
|
41
|
+
const data = await response.json();
|
|
42
|
+
// Check if data has changed
|
|
43
|
+
if (data.timestamp && data.timestamp !== this.lastTimestamp) {
|
|
44
|
+
this.lastTimestamp = data.timestamp;
|
|
45
|
+
this.options.onUpdate();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
// Silently fail - will retry on next interval
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
stop() {
|
|
53
|
+
this.isRunning = false;
|
|
54
|
+
if (this.pollTimer) {
|
|
55
|
+
clearInterval(this.pollTimer);
|
|
56
|
+
this.pollTimer = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
updateContext(context) {
|
|
60
|
+
this.options.context = context;
|
|
61
|
+
}
|
|
62
|
+
}
|
package/dist/stream.d.ts
ADDED
package/dist/stream.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Legacy streaming support - replaced by Supabase Realtime
|
|
2
|
+
// This file is kept for backward compatibility but is no longer used
|
|
3
|
+
export function startStream() {
|
|
4
|
+
console.warn('startStream is deprecated - use Supabase Realtime streaming instead');
|
|
5
|
+
return { close: () => { } };
|
|
6
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type FlagValue = boolean | string | number | object | null;
|
|
2
|
+
export interface FlagpoolClientOptions {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
environment: string;
|
|
5
|
+
context?: Record<string, any>;
|
|
6
|
+
pollingInterval?: number;
|
|
7
|
+
streaming?: boolean;
|
|
8
|
+
urlOverride?: string;
|
|
9
|
+
decryptionKey?: string;
|
|
10
|
+
clientSideTargetLists?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface FlagVariation {
|
|
13
|
+
value: any;
|
|
14
|
+
name: string;
|
|
15
|
+
}
|
|
16
|
+
export interface FlagRule {
|
|
17
|
+
attribute: string;
|
|
18
|
+
operator: 'eq' | 'neq' | 'in' | 'nin' | 'contains' | 'startsWith' | 'inTargetList' | 'notInTargetList';
|
|
19
|
+
value: any;
|
|
20
|
+
targetListKey?: string;
|
|
21
|
+
variation: number;
|
|
22
|
+
priority: number;
|
|
23
|
+
}
|
|
24
|
+
export interface FlagDefinition {
|
|
25
|
+
key: string;
|
|
26
|
+
variations: FlagVariation[];
|
|
27
|
+
defaultVariation: number;
|
|
28
|
+
rolloutPercentage: number;
|
|
29
|
+
tags?: string[];
|
|
30
|
+
rules?: FlagRule[];
|
|
31
|
+
}
|
|
32
|
+
export interface GetFlagsResponse {
|
|
33
|
+
flags: FlagDefinition[];
|
|
34
|
+
environment: {
|
|
35
|
+
key: string;
|
|
36
|
+
name: string;
|
|
37
|
+
};
|
|
38
|
+
timestamp: string;
|
|
39
|
+
targetLists?: EncryptedTargetLists | DecryptedTargetLists;
|
|
40
|
+
}
|
|
41
|
+
export interface EncryptedTargetLists {
|
|
42
|
+
_encrypted: true;
|
|
43
|
+
_algorithm: 'AES-256-GCM';
|
|
44
|
+
_iv: string;
|
|
45
|
+
_data: string;
|
|
46
|
+
_tag: string;
|
|
47
|
+
}
|
|
48
|
+
export interface TargetList {
|
|
49
|
+
attributeKey: string;
|
|
50
|
+
values: string[];
|
|
51
|
+
}
|
|
52
|
+
export interface DecryptedTargetLists {
|
|
53
|
+
[key: string]: TargetList;
|
|
54
|
+
}
|
|
55
|
+
export type FlagChangedCallback = (key: string, newValue: any) => void;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function hashString(str: string): number;
|
package/dist/utils.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flagpool/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Official Flagpool SDK for TypeScript/JavaScript - feature flags with local evaluation, deterministic rollouts, and encrypted target lists",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/flagpool/flagpool-sdk.git",
|
|
26
|
+
"directory": "sdks/typescript"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/flagpool/flagpool-sdk/tree/main/sdks/typescript#readme",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/flagpool/flagpool-sdk/issues"
|
|
31
|
+
},
|
|
32
|
+
"author": "Flagpool",
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"test:watch": "vitest",
|
|
37
|
+
"test:ui": "vitest --ui",
|
|
38
|
+
"test:coverage": "vitest run --coverage",
|
|
39
|
+
"prepublishOnly": "npm run build && npm test"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"feature-flags",
|
|
43
|
+
"feature-toggles",
|
|
44
|
+
"sdk",
|
|
45
|
+
"flagpool",
|
|
46
|
+
"ab-testing",
|
|
47
|
+
"rollouts",
|
|
48
|
+
"targeting"
|
|
49
|
+
],
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18.0.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "^25.0.3",
|
|
56
|
+
"@vitest/coverage-v8": "^4.0.10",
|
|
57
|
+
"@vitest/ui": "^4.0.10",
|
|
58
|
+
"eventsource": "^4.1.0",
|
|
59
|
+
"typescript": "^5.5.2",
|
|
60
|
+
"vitest": "^4.0.10"
|
|
61
|
+
}
|
|
62
|
+
}
|