@feathq/web-sdk 0.1.0 → 0.2.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 CHANGED
@@ -1,4 +1,12 @@
1
- # @feathq/web-sdk
1
+ <p align="center">
2
+ <a href="https://feat.so">
3
+ <img src="https://feat.so/logo/wordmark.png" alt="feat.so" width="320" />
4
+ </a>
5
+ </p>
6
+
7
+ ---
8
+
9
+ # feat Web SDK
2
10
 
3
11
  Browser / client-side SDK for [feat](https://feat.so) feature flags. Polls a per-environment datafile to the browser and evaluates flags locally with a synchronous cache.
4
12
 
@@ -19,7 +27,7 @@ import { FeatWebClient } from "@feathq/web-sdk";
19
27
 
20
28
  const client = new FeatWebClient({
21
29
  apiKey: "feat_cs_…", // client-side ID key
22
- dataPlaneUrl: "https://data.feat.so",
30
+ url: "https://data-01.feat.so", // optional; this is the default
23
31
  anonymous: { storage: "localStorage" }, // optional: auto-mint a stable anonymous user
24
32
  cache: { storage: "localStorage" }, // optional: warm cache across page loads
25
33
  });
@@ -54,7 +62,7 @@ import { OpenFeature } from "@openfeature/web-sdk";
54
62
  import { FeatWebClient } from "@feathq/web-sdk";
55
63
  import { FeatWebProvider } from "@feathq/openfeature-web";
56
64
 
57
- const featClient = new FeatWebClient({ apiKey, dataPlaneUrl });
65
+ const featClient = new FeatWebClient({ apiKey });
58
66
  await OpenFeature.setProviderAndWait(new FeatWebProvider(featClient));
59
67
  await OpenFeature.setContext({ targetingKey: "user-123" });
60
68
 
@@ -66,7 +74,7 @@ const enabled = OpenFeature.getClient().getBooleanValue("checkout-v2", false);
66
74
  Fetch the datafile on the server and pass it through to the client to skip the first round trip:
67
75
 
68
76
  ```ts
69
- new FeatWebClient({ apiKey, dataPlaneUrl, bootstrap: serverProvidedDatafile });
77
+ new FeatWebClient({ apiKey, bootstrap: serverProvidedDatafile });
70
78
  ```
71
79
 
72
80
  ## How it works
@@ -75,7 +83,7 @@ new FeatWebClient({ apiKey, dataPlaneUrl, bootstrap: serverProvidedDatafile });
75
83
  - Polls every 30 s by default; pauses while the tab is hidden and force-refreshes on visibility restore. Floored at 5 s.
76
84
  - Cross-tab `BroadcastChannel` sync: when one tab fetches a new datafile, sibling tabs adopt it without their own network call.
77
85
  - 304-aware via `ETag` / `If-None-Match`.
78
- - `dataPlaneUrl` must use `https://` (the constructor rejects plaintext URLs except `http://localhost` for tests).
86
+ - `url` must use `https://` if you override it (the constructor rejects plaintext URLs except `http://localhost` for tests).
79
87
 
80
88
  ## Security notes
81
89
 
package/dist/index.cjs CHANGED
@@ -130,6 +130,112 @@ var Emitter = class {
130
130
  }
131
131
  };
132
132
 
133
+ // src/events.ts
134
+ var DEFAULT_EVENTS_FLUSH_INTERVAL_MS = 6e4;
135
+ var MIN_EVENTS_FLUSH_INTERVAL_MS = 5e3;
136
+ var MAX_BATCH = 2e3;
137
+ var MAX_BUFFERED_PAIRS = 1e5;
138
+ function extractContextPairs(context) {
139
+ const out = [];
140
+ const seen = /* @__PURE__ */ new Set();
141
+ const push = (kind, key) => {
142
+ const id = `${kind} ${key}`;
143
+ if (seen.has(id)) return;
144
+ seen.add(id);
145
+ out.push({ kind, key });
146
+ };
147
+ const userObj = context.user;
148
+ if (userObj && typeof userObj === "object" && typeof userObj.key === "string") {
149
+ push("user", userObj.key);
150
+ } else if (typeof context.targetingKey === "string") {
151
+ push("user", context.targetingKey);
152
+ }
153
+ for (const [kind, value] of Object.entries(context)) {
154
+ if (kind === "targetingKey" || kind === "user") continue;
155
+ if (value && typeof value === "object" && typeof value.key === "string") {
156
+ push(kind, value.key);
157
+ }
158
+ }
159
+ return out;
160
+ }
161
+ var EventSummarizer = class {
162
+ constructor(opts) {
163
+ this.opts = opts;
164
+ this.pending = /* @__PURE__ */ new Map();
165
+ this.timer = null;
166
+ this.inFlight = false;
167
+ this.visibilityHandler = null;
168
+ this.endpoint = `${opts.url.replace(/\/$/, "")}/sdk/v1/events`;
169
+ }
170
+ // Synchronous, never throws: safe to call inline from setContext().
171
+ record(context) {
172
+ for (const pair of extractContextPairs(context)) {
173
+ const id = `${pair.kind} ${pair.key}`;
174
+ if (this.pending.has(id)) continue;
175
+ if (this.pending.size >= MAX_BUFFERED_PAIRS) return;
176
+ this.pending.set(id, pair);
177
+ }
178
+ }
179
+ start() {
180
+ if (this.timer) return;
181
+ this.timer = setInterval(() => {
182
+ void this.flush();
183
+ }, this.opts.flushIntervalMs);
184
+ if (typeof document !== "undefined") {
185
+ this.visibilityHandler = () => {
186
+ if (document.visibilityState === "hidden") void this.flush(true);
187
+ };
188
+ document.addEventListener("visibilitychange", this.visibilityHandler);
189
+ }
190
+ }
191
+ async flush(keepalive = false) {
192
+ if (this.inFlight || this.pending.size === 0) return;
193
+ const batch = [...this.pending.values()].slice(0, MAX_BATCH);
194
+ for (const c of batch) this.pending.delete(`${c.kind} ${c.key}`);
195
+ this.inFlight = true;
196
+ try {
197
+ const res = await this.opts.fetchImpl(this.endpoint, {
198
+ method: "POST",
199
+ headers: {
200
+ Authorization: `Bearer ${this.opts.apiKey}`,
201
+ "Content-Type": "application/json",
202
+ "X-Feat-Sdk": this.opts.sdkHeader
203
+ },
204
+ body: JSON.stringify({ contexts: batch }),
205
+ keepalive
206
+ });
207
+ if (!res.ok && (res.status >= 500 || res.status === 429)) {
208
+ this.requeue(batch);
209
+ }
210
+ } catch {
211
+ this.requeue(batch);
212
+ } finally {
213
+ this.inFlight = false;
214
+ }
215
+ }
216
+ // Detach the page-hide hook, stop the timer, and make a best-effort final
217
+ // flush. Fire-and-forget: the flush is not awaited so close() stays sync.
218
+ close() {
219
+ if (this.timer) {
220
+ clearInterval(this.timer);
221
+ this.timer = null;
222
+ }
223
+ if (this.visibilityHandler && typeof document !== "undefined") {
224
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
225
+ this.visibilityHandler = null;
226
+ }
227
+ void this.flush(true);
228
+ }
229
+ requeue(batch) {
230
+ for (const c of batch) {
231
+ const id = `${c.kind} ${c.key}`;
232
+ if (this.pending.has(id)) continue;
233
+ if (this.pending.size >= MAX_BUFFERED_PAIRS) return;
234
+ this.pending.set(id, c);
235
+ }
236
+ }
237
+ };
238
+
133
239
  // src/persistence.ts
134
240
  var STORAGE_KEY2 = "feat:datafile";
135
241
  function loadCachedDatafile(config) {
@@ -161,11 +267,15 @@ function hasLocalStorage2() {
161
267
  }
162
268
  }
163
269
 
270
+ // src/version.ts
271
+ var SDK_VERSION = "0.2.0";
272
+
164
273
  // src/client.ts
165
274
  var CLIENT_SIDE_PREFIX = "feat_cs_";
166
275
  var MIN_POLL_INTERVAL_MS = 5e3;
167
276
  var DEFAULT_POLL_INTERVAL_MS = 3e4;
168
277
  var MAX_DATAFILE_BYTES = 10 * 1024 * 1024;
278
+ var DEFAULT_URL = "https://data-01.feat.so";
169
279
  var FeatWebClient = class {
170
280
  constructor(config) {
171
281
  this.config = config;
@@ -184,17 +294,29 @@ var FeatWebClient = class {
184
294
  `FeatWebClient requires a client_side_id key (prefix "${CLIENT_SIDE_PREFIX}"). Server and mobile keys must never ship in browser code.`
185
295
  );
186
296
  }
187
- assertHttpsUrl(config.dataPlaneUrl);
297
+ this.url = config.url ?? DEFAULT_URL;
298
+ assertHttpsUrl(this.url);
188
299
  this.fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
189
300
  this.pollIntervalMs = Math.max(
190
301
  config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
191
302
  MIN_POLL_INTERVAL_MS
192
303
  );
304
+ this.summarizer = config.events === false ? null : new EventSummarizer({
305
+ url: this.url,
306
+ apiKey: config.apiKey,
307
+ fetchImpl: this.fetchImpl,
308
+ sdkHeader: `web/${SDK_VERSION}`,
309
+ flushIntervalMs: Math.max(
310
+ config.eventsFlushIntervalMs ?? DEFAULT_EVENTS_FLUSH_INTERVAL_MS,
311
+ MIN_EVENTS_FLUSH_INTERVAL_MS
312
+ )
313
+ });
193
314
  if (config.context) {
194
315
  this.context = config.context;
195
316
  } else if (config.anonymous) {
196
317
  this.context = buildAnonymousContext(config.anonymous);
197
318
  }
319
+ if (this.context) this.summarizer?.record(this.context);
198
320
  if (config.bootstrap) {
199
321
  this.datafile = config.bootstrap;
200
322
  this.etag = config.bootstrap.etag;
@@ -224,6 +346,7 @@ var FeatWebClient = class {
224
346
  // OpenFeature's `onContextChange` lifecycle hook bridges to this.
225
347
  async setContext(context) {
226
348
  this.context = context;
349
+ this.summarizer?.record(context);
227
350
  await this.recomputeCache();
228
351
  }
229
352
  currentContext() {
@@ -290,6 +413,7 @@ var FeatWebClient = class {
290
413
  }
291
414
  this.broadcast?.close();
292
415
  this.broadcast = null;
416
+ this.summarizer?.close();
293
417
  this.emitter.removeAll();
294
418
  }
295
419
  async bootstrap() {
@@ -299,6 +423,7 @@ var FeatWebClient = class {
299
423
  if (this.closed) return;
300
424
  this.startPolling();
301
425
  this.attachVisibilityHandler();
426
+ this.summarizer?.start();
302
427
  this.emitter.emit("ready", void 0);
303
428
  } catch (err) {
304
429
  this.emitter.emit("failed", err instanceof Error ? err : new Error(String(err)));
@@ -331,9 +456,11 @@ var FeatWebClient = class {
331
456
  document.addEventListener("visibilitychange", this.visibilityHandler);
332
457
  }
333
458
  async fetchDatafile() {
334
- const url = `${this.config.dataPlaneUrl.replace(/\/$/, "")}/sdk/v1/datafile`;
459
+ const url = `${this.url.replace(/\/$/, "")}/sdk/v1/datafile`;
335
460
  const headers = {
336
- Authorization: `Bearer ${this.config.apiKey}`
461
+ Authorization: `Bearer ${this.config.apiKey}`,
462
+ // Custom header because browsers forbid setting User-Agent on fetch.
463
+ "X-Feat-Sdk": `web/${SDK_VERSION}`
337
464
  };
338
465
  if (this.etag) headers["If-None-Match"] = this.etag;
339
466
  const res = await this.fetchImpl(url, { method: "GET", headers });
@@ -431,11 +558,11 @@ function assertHttpsUrl(url) {
431
558
  }
432
559
  } catch {
433
560
  }
434
- throw new Error("dataPlaneUrl must use https:// (http://localhost allowed for tests)");
561
+ throw new Error("url must use https:// (http://localhost allowed for tests)");
435
562
  }
436
563
 
437
564
  // src/index.ts
438
- var SDK_VERSION = "0.1.0";
565
+ var SDK_VERSION2 = "0.2.0";
439
566
 
440
567
  exports.FeatWebClient = FeatWebClient;
441
- exports.SDK_VERSION = SDK_VERSION;
568
+ exports.SDK_VERSION = SDK_VERSION2;
package/dist/index.d.cts CHANGED
@@ -15,13 +15,15 @@ interface DatafileCacheConfig {
15
15
 
16
16
  interface FeatWebClientConfig {
17
17
  apiKey: string;
18
- dataPlaneUrl: string;
18
+ url?: string;
19
19
  context?: EvalContext;
20
20
  anonymous?: AnonymousConfig;
21
21
  bootstrap?: Datafile;
22
22
  cache?: DatafileCacheConfig;
23
23
  pollIntervalMs?: number;
24
24
  crossTabSync?: boolean;
25
+ events?: boolean;
26
+ eventsFlushIntervalMs?: number;
25
27
  fetch?: typeof fetch;
26
28
  }
27
29
  interface ChangeEvent {
@@ -49,8 +51,10 @@ declare class FeatWebClient {
49
51
  private visibilityHandler;
50
52
  private emitter;
51
53
  private broadcast;
54
+ private readonly summarizer;
52
55
  private readonly fetchImpl;
53
56
  private readonly pollIntervalMs;
57
+ private readonly url;
54
58
  private closed;
55
59
  constructor(config: FeatWebClientConfig);
56
60
  ready(): Promise<void>;
@@ -76,6 +80,6 @@ declare class FeatWebClient {
76
80
  private recomputeCache;
77
81
  }
78
82
 
79
- declare const SDK_VERSION = "0.1.0";
83
+ declare const SDK_VERSION = "0.2.0";
80
84
 
81
85
  export { type ChangeEvent, FeatWebClient, type FeatWebClientConfig, type FlagEventMap, SDK_VERSION };
package/dist/index.d.ts CHANGED
@@ -15,13 +15,15 @@ interface DatafileCacheConfig {
15
15
 
16
16
  interface FeatWebClientConfig {
17
17
  apiKey: string;
18
- dataPlaneUrl: string;
18
+ url?: string;
19
19
  context?: EvalContext;
20
20
  anonymous?: AnonymousConfig;
21
21
  bootstrap?: Datafile;
22
22
  cache?: DatafileCacheConfig;
23
23
  pollIntervalMs?: number;
24
24
  crossTabSync?: boolean;
25
+ events?: boolean;
26
+ eventsFlushIntervalMs?: number;
25
27
  fetch?: typeof fetch;
26
28
  }
27
29
  interface ChangeEvent {
@@ -49,8 +51,10 @@ declare class FeatWebClient {
49
51
  private visibilityHandler;
50
52
  private emitter;
51
53
  private broadcast;
54
+ private readonly summarizer;
52
55
  private readonly fetchImpl;
53
56
  private readonly pollIntervalMs;
57
+ private readonly url;
54
58
  private closed;
55
59
  constructor(config: FeatWebClientConfig);
56
60
  ready(): Promise<void>;
@@ -76,6 +80,6 @@ declare class FeatWebClient {
76
80
  private recomputeCache;
77
81
  }
78
82
 
79
- declare const SDK_VERSION = "0.1.0";
83
+ declare const SDK_VERSION = "0.2.0";
80
84
 
81
85
  export { type ChangeEvent, FeatWebClient, type FeatWebClientConfig, type FlagEventMap, SDK_VERSION };
package/dist/index.js CHANGED
@@ -128,6 +128,112 @@ var Emitter = class {
128
128
  }
129
129
  };
130
130
 
131
+ // src/events.ts
132
+ var DEFAULT_EVENTS_FLUSH_INTERVAL_MS = 6e4;
133
+ var MIN_EVENTS_FLUSH_INTERVAL_MS = 5e3;
134
+ var MAX_BATCH = 2e3;
135
+ var MAX_BUFFERED_PAIRS = 1e5;
136
+ function extractContextPairs(context) {
137
+ const out = [];
138
+ const seen = /* @__PURE__ */ new Set();
139
+ const push = (kind, key) => {
140
+ const id = `${kind} ${key}`;
141
+ if (seen.has(id)) return;
142
+ seen.add(id);
143
+ out.push({ kind, key });
144
+ };
145
+ const userObj = context.user;
146
+ if (userObj && typeof userObj === "object" && typeof userObj.key === "string") {
147
+ push("user", userObj.key);
148
+ } else if (typeof context.targetingKey === "string") {
149
+ push("user", context.targetingKey);
150
+ }
151
+ for (const [kind, value] of Object.entries(context)) {
152
+ if (kind === "targetingKey" || kind === "user") continue;
153
+ if (value && typeof value === "object" && typeof value.key === "string") {
154
+ push(kind, value.key);
155
+ }
156
+ }
157
+ return out;
158
+ }
159
+ var EventSummarizer = class {
160
+ constructor(opts) {
161
+ this.opts = opts;
162
+ this.pending = /* @__PURE__ */ new Map();
163
+ this.timer = null;
164
+ this.inFlight = false;
165
+ this.visibilityHandler = null;
166
+ this.endpoint = `${opts.url.replace(/\/$/, "")}/sdk/v1/events`;
167
+ }
168
+ // Synchronous, never throws: safe to call inline from setContext().
169
+ record(context) {
170
+ for (const pair of extractContextPairs(context)) {
171
+ const id = `${pair.kind} ${pair.key}`;
172
+ if (this.pending.has(id)) continue;
173
+ if (this.pending.size >= MAX_BUFFERED_PAIRS) return;
174
+ this.pending.set(id, pair);
175
+ }
176
+ }
177
+ start() {
178
+ if (this.timer) return;
179
+ this.timer = setInterval(() => {
180
+ void this.flush();
181
+ }, this.opts.flushIntervalMs);
182
+ if (typeof document !== "undefined") {
183
+ this.visibilityHandler = () => {
184
+ if (document.visibilityState === "hidden") void this.flush(true);
185
+ };
186
+ document.addEventListener("visibilitychange", this.visibilityHandler);
187
+ }
188
+ }
189
+ async flush(keepalive = false) {
190
+ if (this.inFlight || this.pending.size === 0) return;
191
+ const batch = [...this.pending.values()].slice(0, MAX_BATCH);
192
+ for (const c of batch) this.pending.delete(`${c.kind} ${c.key}`);
193
+ this.inFlight = true;
194
+ try {
195
+ const res = await this.opts.fetchImpl(this.endpoint, {
196
+ method: "POST",
197
+ headers: {
198
+ Authorization: `Bearer ${this.opts.apiKey}`,
199
+ "Content-Type": "application/json",
200
+ "X-Feat-Sdk": this.opts.sdkHeader
201
+ },
202
+ body: JSON.stringify({ contexts: batch }),
203
+ keepalive
204
+ });
205
+ if (!res.ok && (res.status >= 500 || res.status === 429)) {
206
+ this.requeue(batch);
207
+ }
208
+ } catch {
209
+ this.requeue(batch);
210
+ } finally {
211
+ this.inFlight = false;
212
+ }
213
+ }
214
+ // Detach the page-hide hook, stop the timer, and make a best-effort final
215
+ // flush. Fire-and-forget: the flush is not awaited so close() stays sync.
216
+ close() {
217
+ if (this.timer) {
218
+ clearInterval(this.timer);
219
+ this.timer = null;
220
+ }
221
+ if (this.visibilityHandler && typeof document !== "undefined") {
222
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
223
+ this.visibilityHandler = null;
224
+ }
225
+ void this.flush(true);
226
+ }
227
+ requeue(batch) {
228
+ for (const c of batch) {
229
+ const id = `${c.kind} ${c.key}`;
230
+ if (this.pending.has(id)) continue;
231
+ if (this.pending.size >= MAX_BUFFERED_PAIRS) return;
232
+ this.pending.set(id, c);
233
+ }
234
+ }
235
+ };
236
+
131
237
  // src/persistence.ts
132
238
  var STORAGE_KEY2 = "feat:datafile";
133
239
  function loadCachedDatafile(config) {
@@ -159,11 +265,15 @@ function hasLocalStorage2() {
159
265
  }
160
266
  }
161
267
 
268
+ // src/version.ts
269
+ var SDK_VERSION = "0.2.0";
270
+
162
271
  // src/client.ts
163
272
  var CLIENT_SIDE_PREFIX = "feat_cs_";
164
273
  var MIN_POLL_INTERVAL_MS = 5e3;
165
274
  var DEFAULT_POLL_INTERVAL_MS = 3e4;
166
275
  var MAX_DATAFILE_BYTES = 10 * 1024 * 1024;
276
+ var DEFAULT_URL = "https://data-01.feat.so";
167
277
  var FeatWebClient = class {
168
278
  constructor(config) {
169
279
  this.config = config;
@@ -182,17 +292,29 @@ var FeatWebClient = class {
182
292
  `FeatWebClient requires a client_side_id key (prefix "${CLIENT_SIDE_PREFIX}"). Server and mobile keys must never ship in browser code.`
183
293
  );
184
294
  }
185
- assertHttpsUrl(config.dataPlaneUrl);
295
+ this.url = config.url ?? DEFAULT_URL;
296
+ assertHttpsUrl(this.url);
186
297
  this.fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
187
298
  this.pollIntervalMs = Math.max(
188
299
  config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
189
300
  MIN_POLL_INTERVAL_MS
190
301
  );
302
+ this.summarizer = config.events === false ? null : new EventSummarizer({
303
+ url: this.url,
304
+ apiKey: config.apiKey,
305
+ fetchImpl: this.fetchImpl,
306
+ sdkHeader: `web/${SDK_VERSION}`,
307
+ flushIntervalMs: Math.max(
308
+ config.eventsFlushIntervalMs ?? DEFAULT_EVENTS_FLUSH_INTERVAL_MS,
309
+ MIN_EVENTS_FLUSH_INTERVAL_MS
310
+ )
311
+ });
191
312
  if (config.context) {
192
313
  this.context = config.context;
193
314
  } else if (config.anonymous) {
194
315
  this.context = buildAnonymousContext(config.anonymous);
195
316
  }
317
+ if (this.context) this.summarizer?.record(this.context);
196
318
  if (config.bootstrap) {
197
319
  this.datafile = config.bootstrap;
198
320
  this.etag = config.bootstrap.etag;
@@ -222,6 +344,7 @@ var FeatWebClient = class {
222
344
  // OpenFeature's `onContextChange` lifecycle hook bridges to this.
223
345
  async setContext(context) {
224
346
  this.context = context;
347
+ this.summarizer?.record(context);
225
348
  await this.recomputeCache();
226
349
  }
227
350
  currentContext() {
@@ -288,6 +411,7 @@ var FeatWebClient = class {
288
411
  }
289
412
  this.broadcast?.close();
290
413
  this.broadcast = null;
414
+ this.summarizer?.close();
291
415
  this.emitter.removeAll();
292
416
  }
293
417
  async bootstrap() {
@@ -297,6 +421,7 @@ var FeatWebClient = class {
297
421
  if (this.closed) return;
298
422
  this.startPolling();
299
423
  this.attachVisibilityHandler();
424
+ this.summarizer?.start();
300
425
  this.emitter.emit("ready", void 0);
301
426
  } catch (err) {
302
427
  this.emitter.emit("failed", err instanceof Error ? err : new Error(String(err)));
@@ -329,9 +454,11 @@ var FeatWebClient = class {
329
454
  document.addEventListener("visibilitychange", this.visibilityHandler);
330
455
  }
331
456
  async fetchDatafile() {
332
- const url = `${this.config.dataPlaneUrl.replace(/\/$/, "")}/sdk/v1/datafile`;
457
+ const url = `${this.url.replace(/\/$/, "")}/sdk/v1/datafile`;
333
458
  const headers = {
334
- Authorization: `Bearer ${this.config.apiKey}`
459
+ Authorization: `Bearer ${this.config.apiKey}`,
460
+ // Custom header because browsers forbid setting User-Agent on fetch.
461
+ "X-Feat-Sdk": `web/${SDK_VERSION}`
335
462
  };
336
463
  if (this.etag) headers["If-None-Match"] = this.etag;
337
464
  const res = await this.fetchImpl(url, { method: "GET", headers });
@@ -429,10 +556,10 @@ function assertHttpsUrl(url) {
429
556
  }
430
557
  } catch {
431
558
  }
432
- throw new Error("dataPlaneUrl must use https:// (http://localhost allowed for tests)");
559
+ throw new Error("url must use https:// (http://localhost allowed for tests)");
433
560
  }
434
561
 
435
562
  // src/index.ts
436
- var SDK_VERSION = "0.1.0";
563
+ var SDK_VERSION2 = "0.2.0";
437
564
 
438
- export { FeatWebClient, SDK_VERSION };
565
+ export { FeatWebClient, SDK_VERSION2 as SDK_VERSION };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feathq/web-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "feat feature-flag SDK for browsers. Polling client + sync evaluation cache. Pair with @feathq/openfeature-web for OpenFeature.",
5
5
  "keywords": [
6
6
  "feature-flags",
@@ -11,7 +11,7 @@
11
11
  "client-side"
12
12
  ],
13
13
  "license": "MIT",
14
- "author": "feat HQ <engineering@feat.so>",
14
+ "author": "feat HQ <support@feat.so>",
15
15
  "homepage": "https://feat.so",
16
16
  "repository": {
17
17
  "type": "git",
@@ -48,11 +48,6 @@
48
48
  "@feathq/datafile-schema": "^0.1.0",
49
49
  "@feathq/feat-eval": "^0.1.0"
50
50
  },
51
- "resolutions": {
52
- "@feathq/datafile-schema": "portal:../../packages/datafile-schema",
53
- "@feathq/datafile-schema@workspace:*": "portal:../../packages/datafile-schema",
54
- "@feathq/feat-eval": "portal:../../packages/feat-eval"
55
- },
56
51
  "devDependencies": {
57
52
  "@types/node": "^25.6.2",
58
53
  "happy-dom": "^15.11.7",
@@ -60,4 +55,4 @@
60
55
  "typescript": "^6.0.3",
61
56
  "vitest": "^4.1.6"
62
57
  }
63
- }
58
+ }