@savvagent/sdk 1.0.0 → 1.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/dist/index.mjs CHANGED
@@ -120,15 +120,23 @@ var TelemetryService = class {
120
120
  }
121
121
  /**
122
122
  * Send evaluation events to backend
123
+ * Per SDK Developer Guide: POST /api/telemetry/evaluations with { "evaluations": [...] }
123
124
  */
124
125
  async sendEvaluations(events) {
126
+ const evaluations = events.map((e) => ({
127
+ flag_key: e.flagKey,
128
+ result: e.result,
129
+ user_id: e.context?.user_id,
130
+ context: e.context,
131
+ timestamp: Math.floor(new Date(e.timestamp).getTime() / 1e3)
132
+ }));
125
133
  const response = await fetch(`${this.baseUrl}/api/telemetry/evaluations`, {
126
134
  method: "POST",
127
135
  headers: {
128
136
  "Content-Type": "application/json",
129
137
  Authorization: `Bearer ${this.apiKey}`
130
138
  },
131
- body: JSON.stringify({ events })
139
+ body: JSON.stringify({ evaluations })
132
140
  });
133
141
  if (!response.ok) {
134
142
  throw new Error(`Failed to send evaluations: ${response.status}`);
@@ -136,15 +144,25 @@ var TelemetryService = class {
136
144
  }
137
145
  /**
138
146
  * Send error events to backend
147
+ * Per SDK Developer Guide: POST /api/telemetry/errors with { "errors": [...] }
139
148
  */
140
149
  async sendErrors(events) {
150
+ const errors = events.map((e) => ({
151
+ flag_key: e.flagKey,
152
+ flag_enabled: e.flagEnabled,
153
+ error_type: e.errorType,
154
+ error_message: e.errorMessage,
155
+ stack_trace: e.stackTrace,
156
+ context: e.context,
157
+ timestamp: Math.floor(new Date(e.timestamp).getTime() / 1e3)
158
+ }));
141
159
  const response = await fetch(`${this.baseUrl}/api/telemetry/errors`, {
142
160
  method: "POST",
143
161
  headers: {
144
162
  "Content-Type": "application/json",
145
163
  Authorization: `Bearer ${this.apiKey}`
146
164
  },
147
- body: JSON.stringify({ events })
165
+ body: JSON.stringify({ errors })
148
166
  });
149
167
  if (!response.ok) {
150
168
  throw new Error(`Failed to send errors: ${response.status}`);
@@ -169,88 +187,144 @@ var TelemetryService = class {
169
187
  };
170
188
 
171
189
  // src/realtime.ts
190
+ import { fetchEventSource } from "@microsoft/fetch-event-source";
191
+ var FatalError = class extends Error {
192
+ constructor(message) {
193
+ super(message);
194
+ this.name = "FatalError";
195
+ }
196
+ };
197
+ var RetriableError = class extends Error {
198
+ constructor(message) {
199
+ super(message);
200
+ this.name = "RetriableError";
201
+ }
202
+ };
172
203
  var RealtimeService = class {
204
+ // Track auth failures to prevent reconnection attempts
173
205
  constructor(baseUrl, apiKey, onConnectionChange) {
174
- this.eventSource = null;
206
+ this.abortController = null;
175
207
  this.reconnectAttempts = 0;
176
208
  this.maxReconnectAttempts = 10;
177
- // Increased from 5 to 10
178
209
  this.reconnectDelay = 1e3;
179
210
  // Start with 1 second
180
211
  this.maxReconnectDelay = 3e4;
181
212
  // Cap at 30 seconds
182
213
  this.listeners = /* @__PURE__ */ new Map();
214
+ this.connected = false;
215
+ this.authFailed = false;
183
216
  this.baseUrl = baseUrl;
184
217
  this.apiKey = apiKey;
185
218
  this.onConnectionChange = onConnectionChange;
186
219
  }
187
220
  /**
188
- * Connect to SSE stream
221
+ * Connect to SSE stream using header-based authentication
222
+ * Per SDK Developer Guide: "Never pass API keys as query parameters"
189
223
  */
190
224
  connect() {
191
- if (this.eventSource) {
225
+ if (this.abortController) {
192
226
  return;
193
227
  }
194
- const url = `${this.baseUrl}/api/flags/stream?apiKey=${encodeURIComponent(this.apiKey)}`;
195
- try {
196
- this.eventSource = new EventSource(url);
197
- this.eventSource.onopen = () => {
198
- console.log("[Savvagent] Real-time connection established");
199
- this.reconnectAttempts = 0;
200
- this.reconnectDelay = 1e3;
201
- this.onConnectionChange?.call(null, true);
202
- };
203
- this.eventSource.onerror = (error) => {
204
- console.error("[Savvagent] SSE connection error:", error);
205
- this.handleDisconnect();
206
- };
207
- this.eventSource.addEventListener("heartbeat", () => {
208
- });
209
- this.eventSource.addEventListener("flag.updated", (e) => {
210
- this.handleMessage("flag.updated", e);
211
- });
212
- this.eventSource.addEventListener("flag.deleted", (e) => {
213
- this.handleMessage("flag.deleted", e);
214
- });
215
- this.eventSource.addEventListener("flag.created", (e) => {
216
- this.handleMessage("flag.created", e);
217
- });
218
- } catch (error) {
219
- console.error("[Savvagent] Failed to create EventSource:", error);
220
- this.handleDisconnect();
228
+ if (this.authFailed) {
229
+ return;
221
230
  }
231
+ this.abortController = new AbortController();
232
+ const url = `${this.baseUrl}/api/flags/stream`;
233
+ fetchEventSource(url, {
234
+ method: "GET",
235
+ headers: {
236
+ "Authorization": `Bearer ${this.apiKey}`
237
+ },
238
+ signal: this.abortController.signal,
239
+ // Disable built-in retry behavior - we handle it ourselves
240
+ openWhenHidden: false,
241
+ onopen: async (response) => {
242
+ if (response.ok) {
243
+ console.log("[Savvagent] Real-time connection established");
244
+ this.reconnectAttempts = 0;
245
+ this.reconnectDelay = 1e3;
246
+ this.connected = true;
247
+ this.onConnectionChange?.(true);
248
+ } else if (response.status === 401 || response.status === 403) {
249
+ this.authFailed = true;
250
+ console.error(`[Savvagent] SSE authentication failed (${response.status}). Check your API key. Reconnection disabled.`);
251
+ throw new FatalError(`SSE authentication failed: ${response.status}`);
252
+ } else {
253
+ console.error(`[Savvagent] SSE connection failed: ${response.status}`);
254
+ throw new RetriableError(`SSE connection failed: ${response.status}`);
255
+ }
256
+ },
257
+ onmessage: (event) => {
258
+ this.handleMessage(event);
259
+ },
260
+ onerror: (err) => {
261
+ if (this.authFailed) {
262
+ throw err;
263
+ }
264
+ console.error("[Savvagent] SSE connection error:", err);
265
+ this.handleDisconnect();
266
+ },
267
+ onclose: () => {
268
+ console.log("[Savvagent] SSE connection closed");
269
+ if (!this.authFailed) {
270
+ this.handleDisconnect();
271
+ }
272
+ }
273
+ }).catch((error) => {
274
+ if (error.name !== "AbortError" && !(error instanceof FatalError)) {
275
+ console.error("[Savvagent] SSE connection error:", error);
276
+ if (!this.authFailed) {
277
+ this.handleDisconnect();
278
+ }
279
+ }
280
+ });
222
281
  }
223
282
  /**
224
283
  * Handle incoming SSE messages
225
284
  */
226
- handleMessage(type, event) {
285
+ handleMessage(event) {
286
+ if (event.event === "heartbeat") {
287
+ return;
288
+ }
289
+ if (event.event === "connected") {
290
+ return;
291
+ }
292
+ const eventType = event.event;
293
+ if (!["flag.updated", "flag.deleted", "flag.created"].includes(eventType)) {
294
+ return;
295
+ }
227
296
  try {
228
297
  const data = JSON.parse(event.data);
229
298
  const updateEvent = {
230
- type,
299
+ type: eventType,
231
300
  flagKey: data.key,
232
301
  data
233
302
  };
234
- const flagListeners = this.listeners.get(updateEvent.flagKey);
235
- if (flagListeners) {
236
- flagListeners.forEach((listener) => listener(updateEvent));
237
- }
238
303
  const wildcardListeners = this.listeners.get("*");
239
304
  if (wildcardListeners) {
240
305
  wildcardListeners.forEach((listener) => listener(updateEvent));
241
306
  }
307
+ const flagListeners = this.listeners.get(updateEvent.flagKey);
308
+ if (flagListeners) {
309
+ flagListeners.forEach((listener) => listener(updateEvent));
310
+ }
242
311
  } catch (error) {
243
312
  console.error("[Savvagent] Failed to parse SSE message:", error);
244
313
  }
245
314
  }
246
315
  /**
247
- * Handle disconnection and attempt reconnect
316
+ * Handle disconnection and attempt reconnect with exponential backoff
248
317
  */
249
318
  handleDisconnect() {
250
- this.onConnectionChange?.call(null, false);
251
- if (this.eventSource) {
252
- this.eventSource.close();
253
- this.eventSource = null;
319
+ this.connected = false;
320
+ this.onConnectionChange?.(false);
321
+ if (this.abortController) {
322
+ this.abortController.abort();
323
+ this.abortController = null;
324
+ }
325
+ if (this.authFailed) {
326
+ console.warn("[Savvagent] Authentication failed. Reconnection disabled.");
327
+ return;
254
328
  }
255
329
  if (this.reconnectAttempts < this.maxReconnectAttempts) {
256
330
  this.reconnectAttempts++;
@@ -288,39 +362,48 @@ var RealtimeService = class {
288
362
  * Disconnect from SSE stream
289
363
  */
290
364
  disconnect() {
291
- if (this.eventSource) {
292
- this.eventSource.close();
293
- this.eventSource = null;
294
- }
295
365
  this.reconnectAttempts = this.maxReconnectAttempts;
296
- this.onConnectionChange?.call(null, false);
366
+ if (this.abortController) {
367
+ this.abortController.abort();
368
+ this.abortController = null;
369
+ }
370
+ this.connected = false;
371
+ this.onConnectionChange?.(false);
297
372
  }
298
373
  /**
299
374
  * Check if connected
300
375
  */
301
376
  isConnected() {
302
- return this.eventSource !== null && this.eventSource.readyState === EventSource.OPEN;
377
+ return this.connected;
303
378
  }
304
379
  };
305
380
 
306
381
  // src/client.ts
307
382
  var FlagClient = class {
383
+ // Track auth failures to prevent request spam
308
384
  constructor(config) {
309
385
  this.realtime = null;
310
386
  this.anonymousId = null;
311
387
  this.userId = null;
312
388
  this.detectedLanguage = null;
389
+ this.overrides = /* @__PURE__ */ new Map();
390
+ this.overrideListeners = /* @__PURE__ */ new Set();
391
+ this.authFailed = false;
313
392
  this.config = {
314
393
  apiKey: config.apiKey,
315
394
  applicationId: config.applicationId || "",
316
395
  baseUrl: config.baseUrl || "http://localhost:8080",
396
+ environment: config.environment || "production",
317
397
  enableRealtime: config.enableRealtime ?? true,
318
398
  cacheTtl: config.cacheTtl || 6e4,
319
399
  enableTelemetry: config.enableTelemetry ?? true,
320
400
  defaults: config.defaults || {},
321
401
  onError: config.onError || ((error) => console.error("[Savvagent]", error)),
322
402
  defaultLanguage: config.defaultLanguage || "",
323
- disableLanguageDetection: config.disableLanguageDetection ?? false
403
+ disableLanguageDetection: config.disableLanguageDetection ?? false,
404
+ retryAttempts: config.retryAttempts ?? 3,
405
+ retryDelay: config.retryDelay ?? 1e3,
406
+ retryBackoff: config.retryBackoff ?? "exponential"
324
407
  };
325
408
  if (!this.config.disableLanguageDetection && typeof navigator !== "undefined") {
326
409
  this.detectedLanguage = this.config.defaultLanguage || navigator.language || navigator.userLanguage || null;
@@ -334,7 +417,7 @@ var FlagClient = class {
334
417
  this.config.apiKey,
335
418
  this.config.enableTelemetry
336
419
  );
337
- if (this.config.enableRealtime && typeof EventSource !== "undefined") {
420
+ if (this.config.enableRealtime && typeof fetch !== "undefined") {
338
421
  this.realtime = new RealtimeService(
339
422
  this.config.baseUrl,
340
423
  this.config.apiKey,
@@ -399,6 +482,21 @@ var FlagClient = class {
399
482
  getUserId() {
400
483
  return this.userId;
401
484
  }
485
+ /**
486
+ * Set the environment for flag evaluation
487
+ * Useful for dynamically switching environments (e.g., dev tools)
488
+ * @param environment - The environment name (e.g., "development", "staging", "production", "beta")
489
+ */
490
+ setEnvironment(environment) {
491
+ this.config.environment = environment;
492
+ this.cache.clear();
493
+ }
494
+ /**
495
+ * Get the current environment
496
+ */
497
+ getEnvironment() {
498
+ return this.config.environment;
499
+ }
402
500
  /**
403
501
  * Get the current anonymous ID
404
502
  */
@@ -413,8 +511,7 @@ var FlagClient = class {
413
511
  const context = {
414
512
  user_id: this.userId || void 0,
415
513
  anonymous_id: this.anonymousId || void 0,
416
- environment: "production",
417
- // TODO: Make configurable
514
+ environment: this.config.environment,
418
515
  ...overrides
419
516
  };
420
517
  if (!context.application_id && this.config.applicationId) {
@@ -425,6 +522,86 @@ var FlagClient = class {
425
522
  }
426
523
  return context;
427
524
  }
525
+ /**
526
+ * Check if an error is retryable (transient failure)
527
+ * @param error - The error to check
528
+ * @param status - HTTP status code (if available)
529
+ */
530
+ isRetryableError(error, status) {
531
+ if (status === 401 || status === 403) {
532
+ return false;
533
+ }
534
+ if (status && status >= 400 && status < 500 && status !== 408 && status !== 429) {
535
+ return false;
536
+ }
537
+ if (status && status >= 500) {
538
+ return true;
539
+ }
540
+ if (error.name === "AbortError" || error.name === "TypeError" || error.message.includes("network")) {
541
+ return true;
542
+ }
543
+ return false;
544
+ }
545
+ /**
546
+ * Calculate delay for retry attempt
547
+ * @param attempt - Current attempt number (1-based)
548
+ */
549
+ getRetryDelay(attempt) {
550
+ const baseDelay = this.config.retryDelay;
551
+ if (this.config.retryBackoff === "linear") {
552
+ return baseDelay * attempt;
553
+ }
554
+ const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
555
+ const jitter = Math.random() * 0.3 * exponentialDelay;
556
+ return exponentialDelay + jitter;
557
+ }
558
+ /**
559
+ * Execute a fetch request with retry logic
560
+ * @param requestFn - Function that returns a fetch promise
561
+ * @param operationName - Name of the operation for logging
562
+ */
563
+ async fetchWithRetry(requestFn, operationName) {
564
+ let lastError = null;
565
+ let lastStatus;
566
+ for (let attempt = 1; attempt <= this.config.retryAttempts; attempt++) {
567
+ try {
568
+ const response = await requestFn();
569
+ if (response.status === 401 || response.status === 403) {
570
+ this.authFailed = true;
571
+ this.realtime?.disconnect();
572
+ console.error(`[Savvagent] Authentication failed (${response.status}). Check your API key. Further requests disabled.`);
573
+ throw new Error(`Authentication failed: ${response.status}`);
574
+ }
575
+ if (response.ok) {
576
+ return response;
577
+ }
578
+ lastStatus = response.status;
579
+ lastError = new Error(`${operationName} failed: ${response.status}`);
580
+ if (!this.isRetryableError(lastError, response.status)) {
581
+ throw lastError;
582
+ }
583
+ if (attempt < this.config.retryAttempts) {
584
+ const delay = this.getRetryDelay(attempt);
585
+ console.warn(`[Savvagent] ${operationName} failed (${response.status}), retrying in ${Math.round(delay)}ms (attempt ${attempt}/${this.config.retryAttempts})`);
586
+ await new Promise((resolve) => setTimeout(resolve, delay));
587
+ }
588
+ } catch (error) {
589
+ lastError = error;
590
+ if (lastError.message.includes("Authentication failed")) {
591
+ throw lastError;
592
+ }
593
+ if (!this.isRetryableError(lastError, lastStatus)) {
594
+ throw lastError;
595
+ }
596
+ if (attempt < this.config.retryAttempts) {
597
+ const delay = this.getRetryDelay(attempt);
598
+ console.warn(`[Savvagent] ${operationName} error: ${lastError.message}, retrying in ${Math.round(delay)}ms (attempt ${attempt}/${this.config.retryAttempts})`);
599
+ await new Promise((resolve) => setTimeout(resolve, delay));
600
+ }
601
+ }
602
+ }
603
+ throw lastError || new Error(`${operationName} failed after ${this.config.retryAttempts} attempts`);
604
+ }
428
605
  /**
429
606
  * Check if a feature flag is enabled
430
607
  * @param flagKey - The flag key to evaluate
@@ -445,6 +622,29 @@ var FlagClient = class {
445
622
  const startTime = Date.now();
446
623
  const traceId = TelemetryService.generateTraceId();
447
624
  try {
625
+ if (this.overrides.has(flagKey)) {
626
+ const overrideValue = this.overrides.get(flagKey);
627
+ return {
628
+ key: flagKey,
629
+ value: overrideValue,
630
+ reason: "default",
631
+ // Using 'default' to indicate override
632
+ metadata: {
633
+ description: "Local override active"
634
+ }
635
+ };
636
+ }
637
+ if (this.authFailed) {
638
+ const defaultValue = this.config.defaults[flagKey] ?? false;
639
+ return {
640
+ key: flagKey,
641
+ value: defaultValue,
642
+ reason: "error",
643
+ metadata: {
644
+ description: "Authentication failed - using default value"
645
+ }
646
+ };
647
+ }
448
648
  const cachedValue = this.cache.get(flagKey);
449
649
  if (cachedValue !== null) {
450
650
  return {
@@ -457,17 +657,26 @@ var FlagClient = class {
457
657
  const requestBody = {
458
658
  context: evaluationContext
459
659
  };
460
- const response = await fetch(`${this.config.baseUrl}/api/flags/${flagKey}/evaluate`, {
461
- method: "POST",
462
- headers: {
463
- "Content-Type": "application/json",
464
- Authorization: `Bearer ${this.config.apiKey}`
660
+ const response = await this.fetchWithRetry(
661
+ () => {
662
+ const controller = new AbortController();
663
+ const timeoutId = setTimeout(() => {
664
+ controller.abort();
665
+ }, 1e4);
666
+ return fetch(`${this.config.baseUrl}/api/flags/${flagKey}/evaluate`, {
667
+ method: "POST",
668
+ headers: {
669
+ "Content-Type": "application/json",
670
+ Authorization: `Bearer ${this.config.apiKey}`
671
+ },
672
+ body: JSON.stringify(requestBody),
673
+ signal: controller.signal
674
+ }).finally(() => {
675
+ clearTimeout(timeoutId);
676
+ });
465
677
  },
466
- body: JSON.stringify(requestBody)
467
- });
468
- if (!response.ok) {
469
- throw new Error(`Flag evaluation failed: ${response.status}`);
470
- }
678
+ `Flag evaluation (${flagKey})`
679
+ );
471
680
  const data = await response.json();
472
681
  const value = data.enabled || false;
473
682
  this.cache.set(flagKey, value, data.key);
@@ -592,6 +801,217 @@ var FlagClient = class {
592
801
  this.realtime?.disconnect();
593
802
  this.cache.clear();
594
803
  }
804
+ // =====================
805
+ // Local Override Methods
806
+ // =====================
807
+ /**
808
+ * Set a local override for a flag.
809
+ * Overrides take precedence over server values and cache.
810
+ *
811
+ * @param flagKey - The flag key to override
812
+ * @param value - The override value (true/false)
813
+ *
814
+ * @example
815
+ * ```typescript
816
+ * // Force a flag to be enabled locally
817
+ * client.setOverride('new-feature', true);
818
+ * ```
819
+ */
820
+ setOverride(flagKey, value) {
821
+ this.overrides.set(flagKey, value);
822
+ this.notifyOverrideListeners();
823
+ }
824
+ /**
825
+ * Clear a local override for a flag.
826
+ * The flag will return to using server/cached values.
827
+ *
828
+ * @param flagKey - The flag key to clear override for
829
+ */
830
+ clearOverride(flagKey) {
831
+ this.overrides.delete(flagKey);
832
+ this.notifyOverrideListeners();
833
+ }
834
+ /**
835
+ * Clear all local overrides.
836
+ */
837
+ clearAllOverrides() {
838
+ this.overrides.clear();
839
+ this.notifyOverrideListeners();
840
+ }
841
+ /**
842
+ * Check if a flag has a local override.
843
+ *
844
+ * @param flagKey - The flag key to check
845
+ * @returns true if the flag has an override
846
+ */
847
+ hasOverride(flagKey) {
848
+ return this.overrides.has(flagKey);
849
+ }
850
+ /**
851
+ * Get the override value for a flag.
852
+ *
853
+ * @param flagKey - The flag key to get override for
854
+ * @returns The override value, or undefined if not set
855
+ */
856
+ getOverride(flagKey) {
857
+ return this.overrides.get(flagKey);
858
+ }
859
+ /**
860
+ * Get all current overrides.
861
+ *
862
+ * @returns Record of flag keys to override values
863
+ */
864
+ getOverrides() {
865
+ const result = {};
866
+ this.overrides.forEach((value, key) => {
867
+ result[key] = value;
868
+ });
869
+ return result;
870
+ }
871
+ /**
872
+ * Set multiple overrides at once.
873
+ *
874
+ * @param overrides - Record of flag keys to override values
875
+ */
876
+ setOverrides(overrides) {
877
+ Object.entries(overrides).forEach(([key, value]) => {
878
+ this.overrides.set(key, value);
879
+ });
880
+ this.notifyOverrideListeners();
881
+ }
882
+ /**
883
+ * Subscribe to override changes.
884
+ * Useful for React components to re-render when overrides change.
885
+ *
886
+ * @param callback - Function to call when overrides change
887
+ * @returns Unsubscribe function
888
+ */
889
+ onOverrideChange(callback) {
890
+ this.overrideListeners.add(callback);
891
+ return () => {
892
+ this.overrideListeners.delete(callback);
893
+ };
894
+ }
895
+ /**
896
+ * Notify all override listeners of a change.
897
+ */
898
+ notifyOverrideListeners() {
899
+ this.overrideListeners.forEach((callback) => {
900
+ try {
901
+ callback();
902
+ } catch (e) {
903
+ console.error("[Savvagent] Override listener error:", e);
904
+ }
905
+ });
906
+ }
907
+ /**
908
+ * Get all flags for the application (and enterprise-scoped flags).
909
+ * Per SDK Developer Guide: GET /api/sdk/flags
910
+ *
911
+ * Use cases:
912
+ * - Local override UI: Display all available flags for developers to toggle
913
+ * - Offline mode: Pre-fetch flags for mobile/desktop apps
914
+ * - SDK initialization: Bootstrap SDK with all flag values on startup
915
+ * - DevTools integration: Show available flags in browser dev panels
916
+ *
917
+ * @param environment - Environment to evaluate enabled state for (default: 'development')
918
+ * @returns Promise<FlagDefinition[]> - List of flag definitions
919
+ *
920
+ * @example
921
+ * ```typescript
922
+ * // Fetch all flags for development
923
+ * const flags = await client.getAllFlags('development');
924
+ *
925
+ * // Bootstrap local cache
926
+ * flags.forEach(flag => {
927
+ * console.log(`${flag.key}: ${flag.enabled}`);
928
+ * });
929
+ * ```
930
+ */
931
+ async getAllFlags(environment = "development") {
932
+ if (this.authFailed) {
933
+ return [];
934
+ }
935
+ try {
936
+ const response = await this.fetchWithRetry(
937
+ () => {
938
+ const controller = new AbortController();
939
+ const timeoutId = setTimeout(() => {
940
+ controller.abort();
941
+ }, 1e4);
942
+ return fetch(
943
+ `${this.config.baseUrl}/api/sdk/flags?environment=${encodeURIComponent(environment)}`,
944
+ {
945
+ method: "GET",
946
+ headers: {
947
+ Authorization: `Bearer ${this.config.apiKey}`
948
+ },
949
+ signal: controller.signal
950
+ }
951
+ ).finally(() => {
952
+ clearTimeout(timeoutId);
953
+ });
954
+ },
955
+ "Get all flags"
956
+ );
957
+ const data = await response.json();
958
+ data.flags.forEach((flag) => {
959
+ this.cache.set(flag.key, flag.enabled, flag.key);
960
+ });
961
+ return data.flags;
962
+ } catch (error) {
963
+ this.config.onError(error);
964
+ return [];
965
+ }
966
+ }
967
+ /**
968
+ * Get only enterprise-scoped flags for the organization.
969
+ * Per SDK Developer Guide: GET /api/sdk/enterprise-flags
970
+ *
971
+ * Enterprise flags are shared across all applications in the organization.
972
+ *
973
+ * @param environment - Environment to evaluate enabled state for (default: 'development')
974
+ * @returns Promise<FlagDefinition[]> - List of enterprise flag definitions
975
+ *
976
+ * @example
977
+ * ```typescript
978
+ * // Fetch enterprise-only flags
979
+ * const enterpriseFlags = await client.getEnterpriseFlags('production');
980
+ * ```
981
+ */
982
+ async getEnterpriseFlags(environment = "development") {
983
+ if (this.authFailed) {
984
+ return [];
985
+ }
986
+ try {
987
+ const response = await this.fetchWithRetry(
988
+ () => {
989
+ const controller = new AbortController();
990
+ const timeoutId = setTimeout(() => {
991
+ controller.abort();
992
+ }, 1e4);
993
+ return fetch(
994
+ `${this.config.baseUrl}/api/sdk/enterprise-flags?environment=${encodeURIComponent(environment)}`,
995
+ {
996
+ method: "GET",
997
+ headers: {
998
+ Authorization: `Bearer ${this.config.apiKey}`
999
+ },
1000
+ signal: controller.signal
1001
+ }
1002
+ ).finally(() => {
1003
+ clearTimeout(timeoutId);
1004
+ });
1005
+ },
1006
+ "Get enterprise flags"
1007
+ );
1008
+ const data = await response.json();
1009
+ return data.flags;
1010
+ } catch (error) {
1011
+ this.config.onError(error);
1012
+ return [];
1013
+ }
1014
+ }
595
1015
  };
596
1016
  export {
597
1017
  FlagCache,