@hyphen/sdk 1.12.2 → 2.0.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.js CHANGED
@@ -63,7 +63,7 @@ import { Buffer as Buffer2 } from "buffer";
63
63
  import process2 from "process";
64
64
 
65
65
  // src/base-service.ts
66
- import axios from "axios";
66
+ import { CacheableNet } from "@cacheable/net";
67
67
  import { Cacheable } from "cacheable";
68
68
  import { Hookified } from "hookified";
69
69
  import pino from "pino";
@@ -79,11 +79,15 @@ var BaseService = class extends Hookified {
79
79
  _log = pino();
80
80
  _cache = new Cacheable();
81
81
  _throwErrors = false;
82
+ _net;
82
83
  constructor(options) {
83
84
  super(options);
84
85
  if (options && options.throwErrors !== void 0) {
85
86
  this._throwErrors = options.throwErrors;
86
87
  }
88
+ this._net = new CacheableNet({
89
+ cache: this._cache
90
+ });
87
91
  }
88
92
  get log() {
89
93
  return this._log;
@@ -96,6 +100,7 @@ var BaseService = class extends Hookified {
96
100
  }
97
101
  set cache(value) {
98
102
  this._cache = value;
103
+ this._net.cache = value;
99
104
  }
100
105
  get throwErrors() {
101
106
  return this._throwErrors;
@@ -119,22 +124,96 @@ var BaseService = class extends Hookified {
119
124
  this.emit("info", message, ...args);
120
125
  }
121
126
  async get(url, config2) {
122
- return axios.get(url, config2);
127
+ let finalUrl = url;
128
+ if (config2?.params) {
129
+ const params = new URLSearchParams(config2.params);
130
+ finalUrl = `${url}?${params.toString()}`;
131
+ }
132
+ const { params: _, ...fetchConfig } = config2 || {};
133
+ const response = await this._net.get(finalUrl, fetchConfig);
134
+ return {
135
+ data: response.data,
136
+ status: response.response.status,
137
+ statusText: response.response.statusText,
138
+ headers: response.response.headers,
139
+ config: config2,
140
+ request: void 0
141
+ };
123
142
  }
124
143
  async post(url, data, config2) {
125
- return axios.post(url, data, config2);
144
+ const response = await this._net.post(url, data, config2);
145
+ return {
146
+ data: response.data,
147
+ status: response.response.status,
148
+ statusText: response.response.statusText,
149
+ headers: response.response.headers,
150
+ config: config2,
151
+ request: void 0
152
+ };
126
153
  }
127
154
  async put(url, data, config2) {
128
- return axios.put(url, data, config2);
155
+ const response = await this._net.put(url, data, config2);
156
+ return {
157
+ data: response.data,
158
+ status: response.response.status,
159
+ statusText: response.response.statusText,
160
+ headers: response.response.headers,
161
+ config: config2,
162
+ request: void 0
163
+ };
129
164
  }
130
165
  async delete(url, config2) {
131
- if (config2?.headers) {
132
- delete config2.headers["content-type"];
166
+ const headers = {
167
+ ...config2?.headers
168
+ };
169
+ if (headers) {
170
+ delete headers["content-type"];
133
171
  }
134
- return axios.delete(url, config2);
172
+ const { data: configData, ...restConfig } = config2 || {};
173
+ let body;
174
+ if (configData) {
175
+ body = typeof configData === "string" ? (
176
+ /* c8 ignore next */
177
+ configData
178
+ ) : JSON.stringify(configData);
179
+ if (!headers["content-type"] && !headers["Content-Type"]) {
180
+ headers["content-type"] = "application/json";
181
+ }
182
+ }
183
+ const response = await this._net.fetch(url, {
184
+ ...restConfig,
185
+ headers,
186
+ body,
187
+ method: "DELETE"
188
+ });
189
+ let data;
190
+ if (response.status !== 204) {
191
+ const text = await response.text();
192
+ try {
193
+ data = text ? JSON.parse(text) : void 0;
194
+ } catch {
195
+ data = text;
196
+ }
197
+ }
198
+ return {
199
+ data,
200
+ status: response.status,
201
+ statusText: response.statusText,
202
+ headers: response.headers,
203
+ config: config2,
204
+ request: void 0
205
+ };
135
206
  }
136
207
  async patch(url, data, config2) {
137
- return axios.patch(url, data, config2);
208
+ const response = await this._net.patch(url, data, config2);
209
+ return {
210
+ data: response.data,
211
+ status: response.response.status,
212
+ statusText: response.response.statusText,
213
+ headers: response.response.headers,
214
+ config: config2,
215
+ request: void 0
216
+ };
138
217
  }
139
218
  createHeaders(apiKey) {
140
219
  const headers = {
@@ -652,363 +731,606 @@ var NetInfo = class extends BaseService {
652
731
  };
653
732
 
654
733
  // src/toggle.ts
655
- import process4 from "process";
656
- import { HyphenProvider } from "@hyphen/openfeature-server-provider";
657
- import { OpenFeature } from "@openfeature/server-sdk";
658
- import dotenv from "dotenv";
734
+ import { CacheableNet as CacheableNet2 } from "@cacheable/net";
659
735
  import { Hookified as Hookified2 } from "hookified";
660
- dotenv.config();
661
- var ToggleHooks = /* @__PURE__ */ (function(ToggleHooks2) {
662
- ToggleHooks2["beforeGetBoolean"] = "beforeGetBoolean";
663
- ToggleHooks2["afterGetBoolean"] = "afterGetBoolean";
664
- ToggleHooks2["beforeGetString"] = "beforeGetString";
665
- ToggleHooks2["afterGetString"] = "afterGetString";
666
- ToggleHooks2["beforeGetNumber"] = "beforeGetNumber";
667
- ToggleHooks2["afterGetNumber"] = "afterGetNumber";
668
- ToggleHooks2["beforeGetObject"] = "beforeGetObject";
669
- ToggleHooks2["afterGetObject"] = "afterGetObject";
670
- return ToggleHooks2;
671
- })({});
672
736
  var Toggle = class extends Hookified2 {
673
737
  static {
674
738
  __name(this, "Toggle");
675
739
  }
676
- _applicationId = process4.env.HYPHEN_APPLICATION_ID;
677
- _publicApiKey = process4.env.HYPHEN_PUBLIC_API_KEY;
740
+ _publicApiKey;
741
+ _organizationId;
742
+ _applicationId;
678
743
  _environment;
679
- _client;
680
- _context;
681
- _throwErrors = false;
682
- _uris;
683
- _caching;
684
- /*
685
- * Create a new Toggle instance. This will create a new client and set the options.
686
- * @param {ToggleOptions}
744
+ _horizonUrls = [];
745
+ _net = new CacheableNet2();
746
+ _defaultContext;
747
+ _defaultTargetingKey = `${Math.random().toString(36).substring(7)}`;
748
+ /**
749
+ * Creates a new Toggle instance.
750
+ *
751
+ * @param options - Configuration options for the toggle client
752
+ *
753
+ * @example
754
+ * ```typescript
755
+ * // Minimal configuration
756
+ * const toggle = new Toggle({
757
+ * publicApiKey: 'public_your-key',
758
+ * applicationId: 'app-123'
759
+ * });
760
+ *
761
+ * // With full options
762
+ * const toggle = new Toggle({
763
+ * publicApiKey: 'public_your-key',
764
+ * applicationId: 'app-123',
765
+ * environment: 'production',
766
+ * defaultContext: { targetingKey: 'user-456' },
767
+ * horizonUrls: ['https://my-horizon.example.com']
768
+ * });
769
+ * ```
687
770
  */
688
771
  constructor(options) {
689
772
  super();
690
- this._throwErrors = options?.throwErrors ?? false;
691
- this._applicationId = options?.applicationId;
773
+ if (options?.applicationId) {
774
+ this._applicationId = options.applicationId;
775
+ }
776
+ if (options?.environment) {
777
+ this._environment = options.environment;
778
+ } else {
779
+ this._environment = "development";
780
+ }
781
+ if (options?.defaultContext) {
782
+ this._defaultContext = options.defaultContext;
783
+ }
692
784
  if (options?.publicApiKey) {
693
- this.setPublicApiKey(options.publicApiKey);
785
+ this._publicApiKey = options.publicApiKey;
786
+ this._organizationId = this.getOrgIdFromPublicKey(this._publicApiKey);
787
+ }
788
+ if (options?.horizonUrls) {
789
+ this._horizonUrls = options.horizonUrls;
790
+ } else {
791
+ this._horizonUrls = this.getDefaultHorizonUrls(this._publicApiKey);
792
+ }
793
+ if (options?.defaultTargetKey) {
794
+ this._defaultTargetingKey = options?.defaultTargetKey;
795
+ } else {
796
+ if (this._defaultContext) {
797
+ this._defaultTargetingKey = this.getTargetingKey(this._defaultContext);
798
+ } else {
799
+ this._defaultTargetingKey = this.generateTargetKey();
800
+ }
801
+ }
802
+ if (options?.cache) {
803
+ this._net.cache = options.cache;
694
804
  }
695
- this._environment = options?.environment ?? process4.env.NODE_ENV ?? "development";
696
- this._context = options?.context;
697
- this._uris = options?.uris;
698
- this._caching = options?.caching;
699
805
  }
700
806
  /**
701
- * Get the application ID
702
- * @returns {string | undefined}
807
+ * Gets the public API key used for authentication.
808
+ *
809
+ * @returns The current public API key or undefined if not set
703
810
  */
704
- get applicationId() {
705
- return this._applicationId;
811
+ get publicApiKey() {
812
+ return this._publicApiKey;
706
813
  }
707
814
  /**
708
- * Set the application ID
709
- * @param {string | undefined} value
815
+ * Sets the public API key used for authentication.
816
+ *
817
+ * @param value - The public API key string or undefined to clear
818
+ * @throws {Error} If the key doesn't start with "public_"
710
819
  */
711
- set applicationId(value) {
712
- this._applicationId = value;
820
+ set publicApiKey(value) {
821
+ this.setPublicKey(value);
713
822
  }
714
823
  /**
715
- * Get the public API key
716
- * @returns {string}
824
+ * Gets the default context used for toggle evaluations.
825
+ *
826
+ * @returns The current default ToggleContext
717
827
  */
718
- get publicApiKey() {
719
- return this._publicApiKey;
828
+ get defaultContext() {
829
+ return this._defaultContext;
720
830
  }
721
831
  /**
722
- * Set the public API key
723
- * @param {string} value
832
+ * Sets the default context used for toggle evaluations.
833
+ *
834
+ * @param value - The ToggleContext to use as default
724
835
  */
725
- set publicApiKey(value) {
726
- if (!value) {
727
- this._publicApiKey = void 0;
728
- this._client = void 0;
729
- return;
730
- }
731
- this.setPublicApiKey(value);
836
+ set defaultContext(value) {
837
+ this._defaultContext = value;
732
838
  }
733
839
  /**
734
- * Get the environment
735
- * @returns {string}
840
+ * Gets the organization ID extracted from the public API key.
841
+ *
842
+ * @returns The organization ID string or undefined if not available
736
843
  */
737
- get environment() {
738
- return this._environment;
844
+ get organizationId() {
845
+ return this._organizationId;
739
846
  }
740
847
  /**
741
- * Set the environment
742
- * @param {string} value
848
+ * Gets the Horizon endpoint URLs used for load balancing.
849
+ *
850
+ * These URLs are used to distribute requests across multiple Horizon endpoints.
851
+ * If endpoints fail, the system will attempt to use the default horizon endpoint service.
852
+ *
853
+ * @returns Array of Horizon endpoint URLs
854
+ * @see {@link https://hyphen.ai/horizon} for more information
743
855
  */
744
- set environment(value) {
745
- this._environment = value;
856
+ get horizonUrls() {
857
+ return this._horizonUrls;
746
858
  }
747
859
  /**
748
- * Get the throwErrors. If true, errors will be thrown in addition to being emitted.
749
- * @returns {boolean}
860
+ * Sets the Horizon endpoint URLs for load balancing.
861
+ *
862
+ * Configures multiple Horizon endpoints that will be used for load balancing.
863
+ * When endpoints fail, the system will fall back to the default horizon endpoint service.
864
+ *
865
+ * @param value - Array of Horizon endpoint URLs or empty array to clear
866
+ * @see {@link https://hyphen.ai/horizon} for more information
867
+ *
868
+ * @example
869
+ * ```typescript
870
+ * const toggle = new Toggle();
871
+ * toggle.horizonUrls = [
872
+ * 'https://org1.toggle.hyphen.cloud',
873
+ * 'https://org2.toggle.hyphen.cloud'
874
+ * ];
875
+ * ```
750
876
  */
751
- get throwErrors() {
752
- return this._throwErrors;
877
+ set horizonUrls(value) {
878
+ this._horizonUrls = value;
753
879
  }
754
880
  /**
755
- * Set the throwErrors. If true, errors will be thrown in addition to being emitted.
756
- * @param {boolean} value
881
+ * Gets the application ID used for toggle context.
882
+ *
883
+ * @returns The current application ID or undefined if not set
757
884
  */
758
- set throwErrors(value) {
759
- this._throwErrors = value;
885
+ get applicationId() {
886
+ return this._applicationId;
760
887
  }
761
888
  /**
762
- * Get the current context. This is the default context used. You can override this at the get function level.
763
- * @returns {ToggleContext}
889
+ * Sets the application ID used for toggle context.
890
+ *
891
+ * @param value - The application ID string or undefined to clear
764
892
  */
765
- get context() {
766
- return this._context;
893
+ set applicationId(value) {
894
+ this._applicationId = value;
767
895
  }
768
896
  /**
769
- * Set the context. This is the default context used. You can override this at the get function level.
770
- * @param {ToggleContext} value
897
+ * Gets the environment used for toggle context.
898
+ *
899
+ * @returns The current environment (defaults to 'development')
771
900
  */
772
- set context(value) {
773
- this._context = value;
901
+ get environment() {
902
+ return this._environment;
774
903
  }
775
904
  /**
776
- * Get the URIs. This is used to override the default URIs for testing or if you are using a self-hosted version.
777
- * @returns {Array<string>}
905
+ * Sets the environment used for toggle context.
906
+ *
907
+ * @param value - The environment string or undefined to clear
778
908
  */
779
- get uris() {
780
- return this._uris;
909
+ set environment(value) {
910
+ this._environment = value;
781
911
  }
782
912
  /**
783
- * Set the URIs. This is used to override the default URIs for testing or if you are using a self-hosted version.
784
- * @param {Array<string>} value
913
+ * Gets the default targeting key used for toggle evaluations.
914
+ *
915
+ * @returns The current default targeting key or undefined if not set
785
916
  */
786
- set uris(value) {
787
- this._uris = value;
917
+ get defaultTargetingKey() {
918
+ return this._defaultTargetingKey;
788
919
  }
789
920
  /**
790
- * Get the caching options.
791
- * @returns {ToggleCachingOptions | undefined}
921
+ * Sets the default targeting key used for toggle evaluations.
922
+ *
923
+ * @param value - The targeting key string or undefined to clear
792
924
  */
793
- get caching() {
794
- return this._caching;
925
+ set defaultTargetingKey(value) {
926
+ this._defaultTargetingKey = value;
927
+ }
928
+ /**
929
+ * Gets the Cacheable instance used for caching fetch operations.
930
+ *
931
+ * @returns The current Cacheable instance
932
+ */
933
+ get cache() {
934
+ return this._net.cache;
795
935
  }
796
936
  /**
797
- * Set the caching options.
798
- * @param {ToggleCachingOptions | undefined} value
937
+ * Sets the Cacheable instance for caching fetch operations.
938
+ *
939
+ * @param cache - The Cacheable instance to use for caching
799
940
  */
800
- set caching(value) {
801
- this._caching = value;
941
+ set cache(cache) {
942
+ this._net.cache = cache;
802
943
  }
803
944
  /**
804
- * This is a helper function to set the public API key. It will check if the key starts with public_ and set it. If it
805
- * does set it will also set the client to undefined to force a new one to be created. If it does not,
806
- * it will emit an error and console warning and not set the key. Used by the constructor and publicApiKey setter.
807
- * @param key
808
- * @returns
945
+ * Retrieves a toggle value with generic type support.
946
+ *
947
+ * This is the core method for fetching toggle values. All convenience methods
948
+ * (getBoolean, getString, getNumber, getObject) delegate to this method.
949
+ *
950
+ * @template T - The expected type of the toggle value
951
+ * @param toggleKey - The key of the toggle to retrieve
952
+ * @param defaultValue - The value to return if the toggle is not found or an error occurs
953
+ * @param options - Optional configuration including context override
954
+ * @returns Promise resolving to the toggle value or defaultValue
955
+ *
956
+ * @example
957
+ * ```typescript
958
+ * const toggle = new Toggle({ publicApiKey: 'public_key', applicationId: 'app-id' });
959
+ *
960
+ * // Get a boolean
961
+ * const enabled = await toggle.get<boolean>('feature-flag', false);
962
+ *
963
+ * // Get an object with type safety
964
+ * interface Config { theme: string; }
965
+ * const config = await toggle.get<Config>('app-config', { theme: 'light' });
966
+ *
967
+ * // Override context for a single request
968
+ * const value = await toggle.get('key', 'default', {
969
+ * context: { targetingKey: 'user-456' }
970
+ * });
971
+ * ```
809
972
  */
810
- setPublicApiKey(key) {
811
- if (!key.startsWith("public_")) {
812
- this.emit("error", new Error("Public API key should start with public_"));
813
- if (process4.env.NODE_ENV !== "production") {
814
- console.error("Public API key should start with public_");
973
+ async get(toggleKey, defaultValue, options) {
974
+ try {
975
+ const context = {
976
+ application: this._applicationId ?? "",
977
+ environment: this._environment ?? "development"
978
+ };
979
+ if (options?.context) {
980
+ context.targetingKey = options?.context.targetingKey;
981
+ context.ipAddress = options?.context.ipAddress;
982
+ context.user = options?.context.user;
983
+ context.customAttributes = options?.context.customAttributes;
984
+ } else {
985
+ context.targetingKey = this._defaultContext?.targetingKey;
986
+ context.ipAddress = this._defaultContext?.ipAddress;
987
+ context.user = this._defaultContext?.user;
988
+ context.customAttributes = this._defaultContext?.customAttributes;
815
989
  }
816
- return;
990
+ const fetchOptions = {
991
+ headers: {}
992
+ };
993
+ if (!context.targetingKey) {
994
+ context.targetingKey = this.getTargetingKey(context);
995
+ }
996
+ if (this._publicApiKey) {
997
+ fetchOptions.headers["x-api-key"] = this._publicApiKey;
998
+ } else {
999
+ throw new Error("You must set the publicApiKey");
1000
+ }
1001
+ if (context.application === "") {
1002
+ throw new Error("You must set the applicationId");
1003
+ }
1004
+ const result = await this.fetch("/toggle/evaluate", context, fetchOptions);
1005
+ if (result?.toggles) {
1006
+ return result.toggles[toggleKey].value;
1007
+ }
1008
+ } catch (error) {
1009
+ this.emit("error", error);
817
1010
  }
818
- this._publicApiKey = key;
819
- this._client = void 0;
1011
+ return defaultValue;
820
1012
  }
821
1013
  /**
822
- * Set the context. This is the default context used. You can override this at the get function level.
823
- * @param {ToggleContext} context
1014
+ * Retrieves a boolean toggle value.
1015
+ *
1016
+ * This is a convenience method that wraps the generic get() method with boolean type safety.
1017
+ *
1018
+ * @param toggleKey - The key of the toggle to retrieve
1019
+ * @param defaultValue - The boolean value to return if the toggle is not found or an error occurs
1020
+ * @param options - Optional configuration including context for toggle evaluation
1021
+ * @returns Promise resolving to the boolean toggle value or defaultValue
1022
+ *
1023
+ * @example
1024
+ * ```typescript
1025
+ * const toggle = new Toggle({ publicApiKey: 'public_key', applicationId: 'app-id' });
1026
+ * const isFeatureEnabled = await toggle.getBoolean('feature-flag', false);
1027
+ * console.log(isFeatureEnabled); // true or false
1028
+ * ```
824
1029
  */
825
- setContext(context) {
826
- this._context = context;
827
- this._client = void 0;
1030
+ async getBoolean(toggleKey, defaultValue, options) {
1031
+ return this.get(toggleKey, defaultValue, options);
828
1032
  }
829
1033
  /**
830
- * Helper function to get the client. This will create a new client if one does not exist. It will also set the
831
- * application ID, environment, and URIs if they are not set. This is used by the get function to get the client.
832
- * This is normally only used internally.
833
- * @returns {Promise<Client>}
1034
+ * Retrieves a string toggle value.
1035
+ *
1036
+ * This is a convenience method that wraps the generic get() method with string type safety.
1037
+ *
1038
+ * @param toggleKey - The key of the toggle to retrieve
1039
+ * @param defaultValue - The string value to return if the toggle is not found or an error occurs
1040
+ * @param options - Optional configuration including context for toggle evaluation
1041
+ * @returns Promise resolving to the string toggle value or defaultValue
1042
+ *
1043
+ * @example
1044
+ * ```typescript
1045
+ * const toggle = new Toggle({ publicApiKey: 'public_key', applicationId: 'app-id' });
1046
+ * const message = await toggle.getString('welcome-message', 'Hello World');
1047
+ * console.log(message); // 'Welcome to our app!' or 'Hello World'
1048
+ * ```
834
1049
  */
835
- async getClient() {
836
- if (!this._client) {
837
- if (this._applicationId === void 0 || this._applicationId.length === 0) {
838
- const errorMessage = "Application ID is not set. You must set it before using the client or have the HYPHEN_APPLICATION_ID environment variable set.";
839
- this.emit("error", new Error(errorMessage));
840
- if (this._throwErrors) {
841
- throw new Error(errorMessage);
842
- }
843
- }
844
- const options = {
845
- application: this._applicationId,
846
- environment: this._environment,
847
- horizonUrls: this._uris,
848
- cache: this._caching
849
- };
850
- if (this._publicApiKey && this._publicApiKey.length > 0) {
851
- await OpenFeature.setProviderAndWait(new HyphenProvider(this._publicApiKey, options));
1050
+ async getString(toggleKey, defaultValue, options) {
1051
+ return this.get(toggleKey, defaultValue, options);
1052
+ }
1053
+ /**
1054
+ * Retrieves an object toggle value.
1055
+ *
1056
+ * This is a convenience method that wraps the generic get() method with object type safety.
1057
+ * Note that the toggle service may return JSON as a string, which should be parsed if needed.
1058
+ *
1059
+ * @template T - The expected object type
1060
+ * @param toggleKey - The key of the toggle to retrieve
1061
+ * @param defaultValue - The object value to return if the toggle is not found or an error occurs
1062
+ * @param options - Optional configuration including context for toggle evaluation
1063
+ * @returns Promise resolving to the object toggle value or defaultValue
1064
+ *
1065
+ * @example
1066
+ * ```typescript
1067
+ * const toggle = new Toggle({ publicApiKey: 'public_key', applicationId: 'app-id' });
1068
+ * const config = await toggle.getObject('app-config', { theme: 'light' });
1069
+ * console.log(config); // { theme: 'dark', features: ['a', 'b'] } or { theme: 'light' }
1070
+ * ```
1071
+ */
1072
+ async getObject(toggleKey, defaultValue, options) {
1073
+ return this.get(toggleKey, defaultValue, options);
1074
+ }
1075
+ /**
1076
+ * Retrieves a number toggle value.
1077
+ *
1078
+ * This is a convenience method that wraps the generic get() method with number type safety.
1079
+ *
1080
+ * @param toggleKey - The key of the toggle to retrieve
1081
+ * @param defaultValue - The number value to return if the toggle is not found or an error occurs
1082
+ * @param options - Optional configuration including context for toggle evaluation
1083
+ * @returns Promise resolving to the number toggle value or defaultValue
1084
+ *
1085
+ * @example
1086
+ * ```typescript
1087
+ * const toggle = new Toggle({ publicApiKey: 'public_key', applicationId: 'app-id' });
1088
+ * const maxRetries = await toggle.getNumber('max-retries', 3);
1089
+ * console.log(maxRetries); // 5 or 3
1090
+ * ```
1091
+ */
1092
+ async getNumber(toggleKey, defaultValue, options) {
1093
+ return this.get(toggleKey, defaultValue, options);
1094
+ }
1095
+ /**
1096
+ * Makes an HTTP POST request to the specified URL with automatic authentication.
1097
+ *
1098
+ * This method uses browser-compatible fetch and automatically includes the
1099
+ * public API key in the x-api-key header if available. It supports load
1100
+ * balancing across multiple horizon URLs with fallback behavior.
1101
+ *
1102
+ * @template T - The expected response type
1103
+ * @param path - The API path to request (e.g., '/api/toggles')
1104
+ * @param payload - The JSON payload to send in the request body
1105
+ * @param options - Optional fetch configuration. Cache is set at .cache
1106
+ * @returns Promise resolving to the parsed JSON response
1107
+ * @throws {Error} If no horizon URLs are configured or all requests fail
1108
+ *
1109
+ * @example
1110
+ * ```typescript
1111
+ * const toggle = new Toggle({
1112
+ * publicApiKey: 'public_your-key-here',
1113
+ * horizonUrls: ['https://api.hyphen.cloud']
1114
+ * });
1115
+ *
1116
+ * interface ToggleResponse {
1117
+ * enabled: boolean;
1118
+ * value: string;
1119
+ * }
1120
+ *
1121
+ * const result = await toggle.fetch<ToggleResponse>('/api/toggle/feature-flag', {
1122
+ * context: { targetingKey: 'user-123' }
1123
+ * });
1124
+ * console.log(result.enabled); // true/false
1125
+ * ```
1126
+ */
1127
+ async fetch(path2, payload, options) {
1128
+ if (this._horizonUrls.length === 0) {
1129
+ throw new Error("No horizon URLs configured. Set horizonUrls or provide a valid publicApiKey.");
1130
+ }
1131
+ const headers = {
1132
+ "Content-Type": "application/json"
1133
+ };
1134
+ if (options?.headers) {
1135
+ if (options.headers instanceof Headers) {
1136
+ options.headers.forEach((value, key) => {
1137
+ headers[key] = value;
1138
+ });
1139
+ } else if (Array.isArray(options.headers)) {
1140
+ options.headers.forEach(([key, value]) => {
1141
+ headers[key] = value;
1142
+ });
852
1143
  } else {
853
- this.emit("error", new Error("Public API key is not set. You must set it before using the client or have the HYPHEN_PUBLIC_API_KEY environment variable set."));
854
- if (this._throwErrors) {
855
- throw new Error("Public API key is not set");
1144
+ Object.assign(headers, options.headers);
1145
+ }
1146
+ }
1147
+ if (this._publicApiKey) {
1148
+ headers["x-api-key"] = this._publicApiKey;
1149
+ }
1150
+ const fetchOptions = {
1151
+ method: "POST",
1152
+ ...options,
1153
+ headers,
1154
+ body: payload ? JSON.stringify(payload) : options?.body
1155
+ };
1156
+ const errors = [];
1157
+ for (const baseUrl of this._horizonUrls) {
1158
+ try {
1159
+ const url = `${baseUrl.replace(/\/$/, "")}${path2.startsWith("/") ? path2 : `/${path2}`}`;
1160
+ const response = await this._net.fetch(url, fetchOptions);
1161
+ if (!response.ok) {
1162
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1163
+ }
1164
+ const data = await response.json();
1165
+ return data;
1166
+ } catch (error) {
1167
+ const fetchError = error instanceof Error ? error : new Error("Unknown fetch error");
1168
+ const statusMatch = fetchError.message.match(/status (\d{3})/);
1169
+ if (statusMatch) {
1170
+ const status = statusMatch[1];
1171
+ errors.push(new Error(`HTTP ${status}: ${fetchError.message}`));
1172
+ } else {
1173
+ errors.push(fetchError);
856
1174
  }
857
1175
  }
858
- this._client = OpenFeature.getClient(this._context);
859
1176
  }
860
- return this._client;
1177
+ throw new Error(`All horizon URLs failed. Last errors: ${errors.map((e) => e.message).join(", ")}`);
861
1178
  }
862
1179
  /**
863
- * This is the main function to get a feature flag value. It will check the type of the default value and call the
864
- * appropriate function. It will also set the context if it is not set.
865
- * @param {string} key - The key of the feature flag
866
- * @param {T} defaultValue - The default value to return if the feature flag is not set or does not evaluate.
867
- * @param {ToggleRequestOptions} options - The options to use for the request. This can be used to override the context.
868
- * @returns {Promise<T>}
1180
+ * Validates and sets the public API key.
1181
+ *
1182
+ * This method is used internally by the publicApiKey setter to validate
1183
+ * that the key starts with "public_" prefix. If the key is invalid,
1184
+ * an error is thrown.
1185
+ *
1186
+ * @param key - The public API key string or undefined to clear
1187
+ * @throws {Error} If the key doesn't start with "public_"
1188
+ *
1189
+ * @example
1190
+ * ```typescript
1191
+ * const toggle = new Toggle();
1192
+ *
1193
+ * // Valid key
1194
+ * toggle.setPublicKey('public_abc123'); // OK
1195
+ *
1196
+ * // Invalid key
1197
+ * toggle.setPublicKey('invalid_key'); // Throws error
1198
+ *
1199
+ * // Clear key
1200
+ * toggle.setPublicKey(undefined); // OK
1201
+ * ```
869
1202
  */
870
- async get(key, defaultValue, options) {
871
- switch (typeof defaultValue) {
872
- case "boolean": {
873
- return this.getBoolean(key, defaultValue, options);
874
- }
875
- case "string": {
876
- return this.getString(key, defaultValue, options);
877
- }
878
- case "number": {
879
- return this.getNumber(key, defaultValue, options);
880
- }
881
- default: {
882
- return this.getObject(key, defaultValue, options);
883
- }
1203
+ setPublicKey(key) {
1204
+ if (key !== void 0 && !key.startsWith("public_")) {
1205
+ throw new Error("Public API key must start with 'public_'");
884
1206
  }
1207
+ this._publicApiKey = key;
885
1208
  }
886
1209
  /**
887
- * Get a boolean value from the feature flag. This will check the type of the default value and call the
888
- * appropriate function. It will also set the context if it is not set.
889
- * @param {string} key - The key of the feature flag
890
- * @param {boolean} defaultValue - The default value to return if the feature flag is not set or does not evaluate.
891
- * @param {ToggleRequestOptions} options - The options to use for the request. This can be used to override the context.
892
- * @returns {Promise<boolean>} - The value of the feature flag
1210
+ * Extracts the organization ID from a public API key.
1211
+ *
1212
+ * The public key format is: `public_<base64-encoded-data>`
1213
+ * The base64 data contains: `orgId:secretData`
1214
+ * Only alphanumeric characters, underscores, and hyphens are considered valid in org IDs.
1215
+ *
1216
+ * @param publicKey - The public API key to extract the organization ID from
1217
+ * @returns The organization ID if valid and extractable, undefined otherwise
1218
+ *
1219
+ * @example
1220
+ * ```typescript
1221
+ * const toggle = new Toggle();
1222
+ * const orgId = toggle.getOrgIdFromPublicKey('public_dGVzdC1vcmc6c2VjcmV0');
1223
+ * console.log(orgId); // 'test-org'
1224
+ * ```
893
1225
  */
894
- async getBoolean(key, defaultValue, options) {
1226
+ getOrgIdFromPublicKey(publicKey) {
895
1227
  try {
896
- const data = {
897
- key,
898
- defaultValue,
899
- options
900
- };
901
- await this.hook("beforeGetBoolean", data);
902
- const client = await this.getClient();
903
- const result = await client.getBooleanValue(data.key, data.defaultValue, data.options?.context);
904
- const resultData = {
905
- key,
906
- defaultValue,
907
- options,
908
- result
909
- };
910
- await this.hook("afterGetBoolean", resultData);
911
- return resultData.result;
912
- } catch (error) {
913
- this.emit("error", error);
914
- if (this._throwErrors) {
915
- throw error;
916
- }
1228
+ const keyWithoutPrefix = publicKey.replace(/^public_/, "");
1229
+ const decoded = globalThis.atob ? globalThis.atob(keyWithoutPrefix) : Buffer.from(keyWithoutPrefix, "base64").toString();
1230
+ const [orgId] = decoded.split(":");
1231
+ const isValidOrgId = /^[a-zA-Z0-9_-]+$/.test(orgId);
1232
+ return isValidOrgId ? orgId : void 0;
1233
+ } catch {
1234
+ return void 0;
917
1235
  }
918
- return defaultValue;
919
1236
  }
920
1237
  /**
921
- * Get a string value from the feature flag.
922
- * @param {string} key - The key of the feature flag
923
- * @param {string} defaultValue - The default value to return if the feature flag is not set or does not evaluate.
924
- * @param {ToggleRequestOptions} options - The options to use for the request. This can be used to override the context.
925
- * @returns {Promise<string>} - The value of the feature flag
1238
+ * Builds the default Horizon API URL for the given public key.
1239
+ *
1240
+ * If a valid organization ID can be extracted from the public key, returns an
1241
+ * organization-specific URL. Otherwise, returns the default fallback URL.
1242
+ *
1243
+ * @param publicKey - The public API key to build the URL for
1244
+ * @returns Organization-specific URL or default fallback URL
1245
+ *
1246
+ * @example
1247
+ * ```typescript
1248
+ * const toggle = new Toggle();
1249
+ *
1250
+ * // With valid org ID
1251
+ * const orgUrl = toggle.buildDefaultHorizonUrl('public_dGVzdC1vcmc6c2VjcmV0');
1252
+ * console.log(orgUrl); // 'https://test-org.toggle.hyphen.cloud'
1253
+ *
1254
+ * // With invalid key
1255
+ * const defaultUrl = toggle.buildDefaultHorizonUrl('invalid-key');
1256
+ * console.log(defaultUrl); // 'https://toggle.hyphen.cloud'
1257
+ * ```
926
1258
  */
927
- async getString(key, defaultValue, options) {
928
- try {
929
- const data = {
930
- key,
931
- defaultValue,
932
- options
933
- };
934
- await this.hook("beforeGetString", data);
935
- const client = await this.getClient();
936
- const result = await client.getStringValue(data.key, data.defaultValue, data.options?.context);
937
- const resultData = {
938
- key,
939
- defaultValue,
940
- options,
941
- result
942
- };
943
- await this.hook("afterGetString", resultData);
944
- return resultData.result;
945
- } catch (error) {
946
- this.emit("error", error);
947
- if (this._throwErrors) {
948
- throw error;
949
- }
1259
+ getDefaultHorizonUrl(publicKey) {
1260
+ if (publicKey) {
1261
+ const orgId = this.getOrgIdFromPublicKey(publicKey);
1262
+ return orgId ? `https://${orgId}.toggle.hyphen.cloud` : "https://toggle.hyphen.cloud";
950
1263
  }
951
- return defaultValue;
1264
+ return "https://toggle.hyphen.cloud";
952
1265
  }
953
- async getNumber(key, defaultValue, options) {
954
- try {
955
- const data = {
956
- key,
957
- defaultValue,
958
- options
959
- };
960
- await this.hook("beforeGetNumber", data);
961
- const client = await this.getClient();
962
- const result = await client.getNumberValue(data.key, data.defaultValue, data.options?.context);
963
- const resultData = {
964
- key,
965
- defaultValue,
966
- options,
967
- result
968
- };
969
- await this.hook("afterGetNumber", resultData);
970
- return resultData.result;
971
- } catch (error) {
972
- this.emit("error", error);
973
- if (this._throwErrors) {
974
- throw error;
1266
+ /**
1267
+ * Gets the default Horizon URLs for load balancing and failover.
1268
+ *
1269
+ * If a public key is provided, returns an array with the organization-specific
1270
+ * URL as primary and the default Hyphen URL as fallback. Without a public key,
1271
+ * returns only the default Hyphen URL.
1272
+ *
1273
+ * @param publicKey - Optional public API key to derive organization-specific URL
1274
+ * @returns Array of Horizon URLs for load balancing
1275
+ *
1276
+ * @example
1277
+ * ```typescript
1278
+ * const toggle = new Toggle();
1279
+ *
1280
+ * // Without public key - returns default only
1281
+ * const defaultUrls = toggle.getDefaultHorizonUrls();
1282
+ * // ['https://toggle.hyphen.cloud']
1283
+ *
1284
+ * // With public key - returns org-specific + fallback
1285
+ * const urls = toggle.getDefaultHorizonUrls('public_dGVzdC1vcmc6c2VjcmV0');
1286
+ * // ['https://test-org.toggle.hyphen.cloud', 'https://toggle.hyphen.cloud']
1287
+ * ```
1288
+ */
1289
+ getDefaultHorizonUrls(publicKey) {
1290
+ let result = [
1291
+ this.getDefaultHorizonUrl()
1292
+ ];
1293
+ if (publicKey) {
1294
+ const defaultUrl = result[0];
1295
+ const orgUrl = this.getDefaultHorizonUrl(publicKey);
1296
+ result = [];
1297
+ if (orgUrl !== defaultUrl) {
1298
+ result.push(orgUrl);
975
1299
  }
1300
+ result.push(defaultUrl);
976
1301
  }
977
- return defaultValue;
1302
+ return result;
978
1303
  }
979
1304
  /**
980
- * Get an object value from the feature flag. This will check the type of the default value and call the
981
- * appropriate function. It will also set the context if it is not set.
982
- * @param {string} key - The key of the feature flag
983
- * @param {T} defaultValue - The default value to return if the feature flag is not set or does not evaluate.
984
- * @param {ToggleRequestOptions} options - The options to use for the request. This can be used to override the context.
985
- * @returns {Promise<T>} - The value of the feature flag
1305
+ * Generates a unique targeting key based on available context.
1306
+ *
1307
+ * @returns A targeting key in the format: `[app]-[env]-[random]` or simplified versions
986
1308
  */
987
- async getObject(key, defaultValue, options) {
988
- try {
989
- const data = {
990
- key,
991
- defaultValue,
992
- options
993
- };
994
- await this.hook("beforeGetObject", data);
995
- const client = await this.getClient();
996
- const result = await client.getObjectValue(key, defaultValue, data.options?.context);
997
- const resultData = {
998
- key,
999
- defaultValue,
1000
- options,
1001
- result
1002
- };
1003
- await this.hook("afterGetObject", resultData);
1004
- return resultData.result;
1005
- } catch (error) {
1006
- this.emit("error", error);
1007
- if (this._throwErrors) {
1008
- throw error;
1009
- }
1309
+ generateTargetKey() {
1310
+ const randomSuffix = Math.random().toString(36).substring(7);
1311
+ const app = this._applicationId || "";
1312
+ const env2 = this._environment || "";
1313
+ const components = [
1314
+ app,
1315
+ env2,
1316
+ randomSuffix
1317
+ ].filter(Boolean);
1318
+ return components.join("-");
1319
+ }
1320
+ /**
1321
+ * Extracts targeting key from a toggle context with fallback logic.
1322
+ *
1323
+ * @param context - The toggle context to extract targeting key from
1324
+ * @returns The targeting key string
1325
+ */
1326
+ getTargetingKey(context) {
1327
+ if (context.targetingKey) {
1328
+ return context.targetingKey;
1010
1329
  }
1011
- return defaultValue;
1330
+ if (context.user) {
1331
+ return context.user.id;
1332
+ }
1333
+ return this._defaultTargetingKey;
1012
1334
  }
1013
1335
  };
1014
1336
 
@@ -1036,11 +1358,6 @@ var Hyphen = class extends Hookified3 {
1036
1358
  netInfoOptions.apiKey = options.apiKey;
1037
1359
  linkOptions.apiKey = options.apiKey;
1038
1360
  }
1039
- if (options?.throwErrors !== void 0) {
1040
- toggleOptions.throwErrors = options.throwErrors;
1041
- netInfoOptions.throwErrors = options.throwErrors;
1042
- linkOptions.throwErrors = options.throwErrors;
1043
- }
1044
1361
  this._netInfo = new NetInfo(netInfoOptions);
1045
1362
  this._netInfo.on("error", (message, ...args) => this.emit("error", message, ...args));
1046
1363
  this._netInfo.on("info", (message, ...args) => this.emit("info", message, ...args));
@@ -1110,29 +1427,10 @@ var Hyphen = class extends Hookified3 {
1110
1427
  this._netInfo.apiKey = value;
1111
1428
  this._link.apiKey = value;
1112
1429
  }
1113
- /**
1114
- * Get whether to throw errors or not.
1115
- * If set to true, errors will be thrown instead of logged.
1116
- * @returns {boolean} Whether to throw errors or not.
1117
- */
1118
- get throwErrors() {
1119
- return this._netInfo.throwErrors && this._toggle.throwErrors && this._link.throwErrors;
1120
- }
1121
- /**
1122
- * Set whether to throw errors or not. If set to true, errors will be thrown instead of logged.
1123
- * This will update the underlying services as well.
1124
- * @param {boolean} value - Whether to throw errors or not.
1125
- */
1126
- set throwErrors(value) {
1127
- this._netInfo.throwErrors = value;
1128
- this._toggle.throwErrors = value;
1129
- this._link.throwErrors = value;
1130
- }
1131
1430
  };
1132
1431
  export {
1133
1432
  Hyphen,
1134
1433
  Toggle,
1135
- ToggleHooks,
1136
1434
  env,
1137
1435
  loadEnv
1138
1436
  };