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