@hyphen/sdk 1.13.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -99
- package/dist/index.cjs +522 -314
- package/dist/index.d.cts +509 -158
- package/dist/index.d.ts +509 -158
- package/dist/index.js +522 -313
- package/package.json +8 -11
package/dist/index.js
CHANGED
|
@@ -86,8 +86,7 @@ var BaseService = class extends Hookified {
|
|
|
86
86
|
this._throwErrors = options.throwErrors;
|
|
87
87
|
}
|
|
88
88
|
this._net = new CacheableNet({
|
|
89
|
-
cache: this._cache
|
|
90
|
-
useHttpCache: false
|
|
89
|
+
cache: this._cache
|
|
91
90
|
});
|
|
92
91
|
}
|
|
93
92
|
get log() {
|
|
@@ -153,22 +152,12 @@ var BaseService = class extends Hookified {
|
|
|
153
152
|
};
|
|
154
153
|
}
|
|
155
154
|
async put(url, data, config2) {
|
|
156
|
-
const
|
|
157
|
-
...config2,
|
|
158
|
-
method: "PUT",
|
|
159
|
-
body: typeof data === "string" ? data : JSON.stringify(data),
|
|
160
|
-
headers: {
|
|
161
|
-
"Content-Type": "application/json",
|
|
162
|
-
...config2?.headers
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
const response = await this._net.fetch(url, requestInit);
|
|
166
|
-
const responseData = await response.json();
|
|
155
|
+
const response = await this._net.put(url, data, config2);
|
|
167
156
|
return {
|
|
168
|
-
data:
|
|
169
|
-
status: response.status,
|
|
170
|
-
statusText: response.statusText,
|
|
171
|
-
headers: response.headers,
|
|
157
|
+
data: response.data,
|
|
158
|
+
status: response.response.status,
|
|
159
|
+
statusText: response.response.statusText,
|
|
160
|
+
headers: response.response.headers,
|
|
172
161
|
config: config2,
|
|
173
162
|
request: void 0
|
|
174
163
|
};
|
|
@@ -183,7 +172,10 @@ var BaseService = class extends Hookified {
|
|
|
183
172
|
const { data: configData, ...restConfig } = config2 || {};
|
|
184
173
|
let body;
|
|
185
174
|
if (configData) {
|
|
186
|
-
body = typeof configData === "string" ?
|
|
175
|
+
body = typeof configData === "string" ? (
|
|
176
|
+
/* c8 ignore next */
|
|
177
|
+
configData
|
|
178
|
+
) : JSON.stringify(configData);
|
|
187
179
|
if (!headers["content-type"] && !headers["Content-Type"]) {
|
|
188
180
|
headers["content-type"] = "application/json";
|
|
189
181
|
}
|
|
@@ -739,365 +731,606 @@ var NetInfo = class extends BaseService {
|
|
|
739
731
|
};
|
|
740
732
|
|
|
741
733
|
// src/toggle.ts
|
|
742
|
-
import
|
|
743
|
-
import { HyphenProvider } from "@hyphen/openfeature-server-provider";
|
|
744
|
-
import { OpenFeature } from "@openfeature/server-sdk";
|
|
745
|
-
import dotenv from "dotenv";
|
|
734
|
+
import { CacheableNet as CacheableNet2 } from "@cacheable/net";
|
|
746
735
|
import { Hookified as Hookified2 } from "hookified";
|
|
747
|
-
dotenv.config({
|
|
748
|
-
quiet: true
|
|
749
|
-
});
|
|
750
|
-
var ToggleHooks = /* @__PURE__ */ (function(ToggleHooks2) {
|
|
751
|
-
ToggleHooks2["beforeGetBoolean"] = "beforeGetBoolean";
|
|
752
|
-
ToggleHooks2["afterGetBoolean"] = "afterGetBoolean";
|
|
753
|
-
ToggleHooks2["beforeGetString"] = "beforeGetString";
|
|
754
|
-
ToggleHooks2["afterGetString"] = "afterGetString";
|
|
755
|
-
ToggleHooks2["beforeGetNumber"] = "beforeGetNumber";
|
|
756
|
-
ToggleHooks2["afterGetNumber"] = "afterGetNumber";
|
|
757
|
-
ToggleHooks2["beforeGetObject"] = "beforeGetObject";
|
|
758
|
-
ToggleHooks2["afterGetObject"] = "afterGetObject";
|
|
759
|
-
return ToggleHooks2;
|
|
760
|
-
})({});
|
|
761
736
|
var Toggle = class extends Hookified2 {
|
|
762
737
|
static {
|
|
763
738
|
__name(this, "Toggle");
|
|
764
739
|
}
|
|
765
|
-
|
|
766
|
-
|
|
740
|
+
_publicApiKey;
|
|
741
|
+
_organizationId;
|
|
742
|
+
_applicationId;
|
|
767
743
|
_environment;
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
*
|
|
775
|
-
* @param
|
|
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
|
+
* ```
|
|
776
770
|
*/
|
|
777
771
|
constructor(options) {
|
|
778
772
|
super();
|
|
779
|
-
|
|
780
|
-
|
|
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
|
+
}
|
|
781
784
|
if (options?.publicApiKey) {
|
|
782
|
-
this.
|
|
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;
|
|
783
804
|
}
|
|
784
|
-
this._environment = options?.environment ?? process4.env.NODE_ENV ?? "development";
|
|
785
|
-
this._context = options?.context;
|
|
786
|
-
this._uris = options?.uris;
|
|
787
|
-
this._caching = options?.caching;
|
|
788
805
|
}
|
|
789
806
|
/**
|
|
790
|
-
*
|
|
791
|
-
*
|
|
807
|
+
* Gets the public API key used for authentication.
|
|
808
|
+
*
|
|
809
|
+
* @returns The current public API key or undefined if not set
|
|
792
810
|
*/
|
|
793
|
-
get
|
|
794
|
-
return this.
|
|
811
|
+
get publicApiKey() {
|
|
812
|
+
return this._publicApiKey;
|
|
795
813
|
}
|
|
796
814
|
/**
|
|
797
|
-
*
|
|
798
|
-
*
|
|
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_"
|
|
799
819
|
*/
|
|
800
|
-
set
|
|
801
|
-
this.
|
|
820
|
+
set publicApiKey(value) {
|
|
821
|
+
this.setPublicKey(value);
|
|
802
822
|
}
|
|
803
823
|
/**
|
|
804
|
-
*
|
|
805
|
-
*
|
|
824
|
+
* Gets the default context used for toggle evaluations.
|
|
825
|
+
*
|
|
826
|
+
* @returns The current default ToggleContext
|
|
806
827
|
*/
|
|
807
|
-
get
|
|
808
|
-
return this.
|
|
828
|
+
get defaultContext() {
|
|
829
|
+
return this._defaultContext;
|
|
809
830
|
}
|
|
810
831
|
/**
|
|
811
|
-
*
|
|
812
|
-
*
|
|
832
|
+
* Sets the default context used for toggle evaluations.
|
|
833
|
+
*
|
|
834
|
+
* @param value - The ToggleContext to use as default
|
|
813
835
|
*/
|
|
814
|
-
set
|
|
815
|
-
|
|
816
|
-
this._publicApiKey = void 0;
|
|
817
|
-
this._client = void 0;
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
this.setPublicApiKey(value);
|
|
836
|
+
set defaultContext(value) {
|
|
837
|
+
this._defaultContext = value;
|
|
821
838
|
}
|
|
822
839
|
/**
|
|
823
|
-
*
|
|
824
|
-
*
|
|
840
|
+
* Gets the organization ID extracted from the public API key.
|
|
841
|
+
*
|
|
842
|
+
* @returns The organization ID string or undefined if not available
|
|
825
843
|
*/
|
|
826
|
-
get
|
|
827
|
-
return this.
|
|
844
|
+
get organizationId() {
|
|
845
|
+
return this._organizationId;
|
|
828
846
|
}
|
|
829
847
|
/**
|
|
830
|
-
*
|
|
831
|
-
*
|
|
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
|
|
832
855
|
*/
|
|
833
|
-
|
|
834
|
-
this.
|
|
856
|
+
get horizonUrls() {
|
|
857
|
+
return this._horizonUrls;
|
|
835
858
|
}
|
|
836
859
|
/**
|
|
837
|
-
*
|
|
838
|
-
*
|
|
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
|
+
* ```
|
|
839
876
|
*/
|
|
840
|
-
|
|
841
|
-
|
|
877
|
+
set horizonUrls(value) {
|
|
878
|
+
this._horizonUrls = value;
|
|
842
879
|
}
|
|
843
880
|
/**
|
|
844
|
-
*
|
|
845
|
-
*
|
|
881
|
+
* Gets the application ID used for toggle context.
|
|
882
|
+
*
|
|
883
|
+
* @returns The current application ID or undefined if not set
|
|
846
884
|
*/
|
|
847
|
-
|
|
848
|
-
this.
|
|
885
|
+
get applicationId() {
|
|
886
|
+
return this._applicationId;
|
|
849
887
|
}
|
|
850
888
|
/**
|
|
851
|
-
*
|
|
852
|
-
*
|
|
889
|
+
* Sets the application ID used for toggle context.
|
|
890
|
+
*
|
|
891
|
+
* @param value - The application ID string or undefined to clear
|
|
853
892
|
*/
|
|
854
|
-
|
|
855
|
-
|
|
893
|
+
set applicationId(value) {
|
|
894
|
+
this._applicationId = value;
|
|
856
895
|
}
|
|
857
896
|
/**
|
|
858
|
-
*
|
|
859
|
-
*
|
|
897
|
+
* Gets the environment used for toggle context.
|
|
898
|
+
*
|
|
899
|
+
* @returns The current environment (defaults to 'development')
|
|
860
900
|
*/
|
|
861
|
-
|
|
862
|
-
this.
|
|
901
|
+
get environment() {
|
|
902
|
+
return this._environment;
|
|
863
903
|
}
|
|
864
904
|
/**
|
|
865
|
-
*
|
|
866
|
-
*
|
|
905
|
+
* Sets the environment used for toggle context.
|
|
906
|
+
*
|
|
907
|
+
* @param value - The environment string or undefined to clear
|
|
867
908
|
*/
|
|
868
|
-
|
|
869
|
-
|
|
909
|
+
set environment(value) {
|
|
910
|
+
this._environment = value;
|
|
870
911
|
}
|
|
871
912
|
/**
|
|
872
|
-
*
|
|
873
|
-
*
|
|
913
|
+
* Gets the default targeting key used for toggle evaluations.
|
|
914
|
+
*
|
|
915
|
+
* @returns The current default targeting key or undefined if not set
|
|
874
916
|
*/
|
|
875
|
-
|
|
876
|
-
this.
|
|
917
|
+
get defaultTargetingKey() {
|
|
918
|
+
return this._defaultTargetingKey;
|
|
877
919
|
}
|
|
878
920
|
/**
|
|
879
|
-
*
|
|
880
|
-
*
|
|
921
|
+
* Sets the default targeting key used for toggle evaluations.
|
|
922
|
+
*
|
|
923
|
+
* @param value - The targeting key string or undefined to clear
|
|
881
924
|
*/
|
|
882
|
-
|
|
883
|
-
|
|
925
|
+
set defaultTargetingKey(value) {
|
|
926
|
+
this._defaultTargetingKey = value;
|
|
884
927
|
}
|
|
885
928
|
/**
|
|
886
|
-
*
|
|
887
|
-
*
|
|
929
|
+
* Gets the Cacheable instance used for caching fetch operations.
|
|
930
|
+
*
|
|
931
|
+
* @returns The current Cacheable instance
|
|
888
932
|
*/
|
|
889
|
-
|
|
890
|
-
this.
|
|
933
|
+
get cache() {
|
|
934
|
+
return this._net.cache;
|
|
891
935
|
}
|
|
892
936
|
/**
|
|
893
|
-
*
|
|
894
|
-
*
|
|
895
|
-
*
|
|
896
|
-
* @param key
|
|
897
|
-
* @returns
|
|
937
|
+
* Sets the Cacheable instance for caching fetch operations.
|
|
938
|
+
*
|
|
939
|
+
* @param cache - The Cacheable instance to use for caching
|
|
898
940
|
*/
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
941
|
+
set cache(cache) {
|
|
942
|
+
this._net.cache = cache;
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
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
|
+
* ```
|
|
972
|
+
*/
|
|
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;
|
|
904
989
|
}
|
|
905
|
-
|
|
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);
|
|
906
1010
|
}
|
|
907
|
-
|
|
908
|
-
this._client = void 0;
|
|
1011
|
+
return defaultValue;
|
|
909
1012
|
}
|
|
910
1013
|
/**
|
|
911
|
-
*
|
|
912
|
-
*
|
|
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
|
+
* ```
|
|
913
1029
|
*/
|
|
914
|
-
|
|
915
|
-
this.
|
|
916
|
-
this._client = void 0;
|
|
1030
|
+
async getBoolean(toggleKey, defaultValue, options) {
|
|
1031
|
+
return this.get(toggleKey, defaultValue, options);
|
|
917
1032
|
}
|
|
918
1033
|
/**
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
* This is
|
|
922
|
-
*
|
|
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
|
+
* ```
|
|
923
1049
|
*/
|
|
924
|
-
async
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
+
});
|
|
941
1143
|
} else {
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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);
|
|
945
1174
|
}
|
|
946
1175
|
}
|
|
947
|
-
this._client = OpenFeature.getClient(this._context);
|
|
948
1176
|
}
|
|
949
|
-
|
|
1177
|
+
throw new Error(`All horizon URLs failed. Last errors: ${errors.map((e) => e.message).join(", ")}`);
|
|
950
1178
|
}
|
|
951
1179
|
/**
|
|
952
|
-
*
|
|
953
|
-
*
|
|
954
|
-
*
|
|
955
|
-
*
|
|
956
|
-
*
|
|
957
|
-
*
|
|
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
|
+
* ```
|
|
958
1202
|
*/
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
return this.getBoolean(key, defaultValue, options);
|
|
963
|
-
}
|
|
964
|
-
case "string": {
|
|
965
|
-
return this.getString(key, defaultValue, options);
|
|
966
|
-
}
|
|
967
|
-
case "number": {
|
|
968
|
-
return this.getNumber(key, defaultValue, options);
|
|
969
|
-
}
|
|
970
|
-
default: {
|
|
971
|
-
return this.getObject(key, defaultValue, options);
|
|
972
|
-
}
|
|
1203
|
+
setPublicKey(key) {
|
|
1204
|
+
if (key !== void 0 && !key.startsWith("public_")) {
|
|
1205
|
+
throw new Error("Public API key must start with 'public_'");
|
|
973
1206
|
}
|
|
1207
|
+
this._publicApiKey = key;
|
|
974
1208
|
}
|
|
975
1209
|
/**
|
|
976
|
-
*
|
|
977
|
-
*
|
|
978
|
-
*
|
|
979
|
-
*
|
|
980
|
-
*
|
|
981
|
-
*
|
|
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
|
+
* ```
|
|
982
1225
|
*/
|
|
983
|
-
|
|
1226
|
+
getOrgIdFromPublicKey(publicKey) {
|
|
984
1227
|
try {
|
|
985
|
-
const
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
const result = await client.getBooleanValue(data.key, data.defaultValue, data.options?.context);
|
|
993
|
-
const resultData = {
|
|
994
|
-
key,
|
|
995
|
-
defaultValue,
|
|
996
|
-
options,
|
|
997
|
-
result
|
|
998
|
-
};
|
|
999
|
-
await this.hook("afterGetBoolean", resultData);
|
|
1000
|
-
return resultData.result;
|
|
1001
|
-
} catch (error) {
|
|
1002
|
-
this.emit("error", error);
|
|
1003
|
-
if (this._throwErrors) {
|
|
1004
|
-
throw error;
|
|
1005
|
-
}
|
|
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;
|
|
1006
1235
|
}
|
|
1007
|
-
return defaultValue;
|
|
1008
1236
|
}
|
|
1009
1237
|
/**
|
|
1010
|
-
*
|
|
1011
|
-
*
|
|
1012
|
-
*
|
|
1013
|
-
*
|
|
1014
|
-
*
|
|
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
|
+
* ```
|
|
1015
1258
|
*/
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
defaultValue,
|
|
1021
|
-
options
|
|
1022
|
-
};
|
|
1023
|
-
await this.hook("beforeGetString", data);
|
|
1024
|
-
const client = await this.getClient();
|
|
1025
|
-
const result = await client.getStringValue(data.key, data.defaultValue, data.options?.context);
|
|
1026
|
-
const resultData = {
|
|
1027
|
-
key,
|
|
1028
|
-
defaultValue,
|
|
1029
|
-
options,
|
|
1030
|
-
result
|
|
1031
|
-
};
|
|
1032
|
-
await this.hook("afterGetString", resultData);
|
|
1033
|
-
return resultData.result;
|
|
1034
|
-
} catch (error) {
|
|
1035
|
-
this.emit("error", error);
|
|
1036
|
-
if (this._throwErrors) {
|
|
1037
|
-
throw error;
|
|
1038
|
-
}
|
|
1259
|
+
getDefaultHorizonUrl(publicKey) {
|
|
1260
|
+
if (publicKey) {
|
|
1261
|
+
const orgId = this.getOrgIdFromPublicKey(publicKey);
|
|
1262
|
+
return orgId ? `https://${orgId}.toggle.hyphen.cloud` : "https://toggle.hyphen.cloud";
|
|
1039
1263
|
}
|
|
1040
|
-
return
|
|
1264
|
+
return "https://toggle.hyphen.cloud";
|
|
1041
1265
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
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);
|
|
1064
1299
|
}
|
|
1300
|
+
result.push(defaultUrl);
|
|
1065
1301
|
}
|
|
1066
|
-
return
|
|
1302
|
+
return result;
|
|
1067
1303
|
}
|
|
1068
1304
|
/**
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
* @
|
|
1072
|
-
* @param {T} defaultValue - The default value to return if the feature flag is not set or does not evaluate.
|
|
1073
|
-
* @param {ToggleRequestOptions} options - The options to use for the request. This can be used to override the context.
|
|
1074
|
-
* @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
|
|
1075
1308
|
*/
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
if (this._throwErrors) {
|
|
1097
|
-
throw error;
|
|
1098
|
-
}
|
|
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;
|
|
1099
1329
|
}
|
|
1100
|
-
|
|
1330
|
+
if (context.user) {
|
|
1331
|
+
return context.user.id;
|
|
1332
|
+
}
|
|
1333
|
+
return this._defaultTargetingKey;
|
|
1101
1334
|
}
|
|
1102
1335
|
};
|
|
1103
1336
|
|
|
@@ -1125,11 +1358,6 @@ var Hyphen = class extends Hookified3 {
|
|
|
1125
1358
|
netInfoOptions.apiKey = options.apiKey;
|
|
1126
1359
|
linkOptions.apiKey = options.apiKey;
|
|
1127
1360
|
}
|
|
1128
|
-
if (options?.throwErrors !== void 0) {
|
|
1129
|
-
toggleOptions.throwErrors = options.throwErrors;
|
|
1130
|
-
netInfoOptions.throwErrors = options.throwErrors;
|
|
1131
|
-
linkOptions.throwErrors = options.throwErrors;
|
|
1132
|
-
}
|
|
1133
1361
|
this._netInfo = new NetInfo(netInfoOptions);
|
|
1134
1362
|
this._netInfo.on("error", (message, ...args) => this.emit("error", message, ...args));
|
|
1135
1363
|
this._netInfo.on("info", (message, ...args) => this.emit("info", message, ...args));
|
|
@@ -1199,29 +1427,10 @@ var Hyphen = class extends Hookified3 {
|
|
|
1199
1427
|
this._netInfo.apiKey = value;
|
|
1200
1428
|
this._link.apiKey = value;
|
|
1201
1429
|
}
|
|
1202
|
-
/**
|
|
1203
|
-
* Get whether to throw errors or not.
|
|
1204
|
-
* If set to true, errors will be thrown instead of logged.
|
|
1205
|
-
* @returns {boolean} Whether to throw errors or not.
|
|
1206
|
-
*/
|
|
1207
|
-
get throwErrors() {
|
|
1208
|
-
return this._netInfo.throwErrors && this._toggle.throwErrors && this._link.throwErrors;
|
|
1209
|
-
}
|
|
1210
|
-
/**
|
|
1211
|
-
* Set whether to throw errors or not. If set to true, errors will be thrown instead of logged.
|
|
1212
|
-
* This will update the underlying services as well.
|
|
1213
|
-
* @param {boolean} value - Whether to throw errors or not.
|
|
1214
|
-
*/
|
|
1215
|
-
set throwErrors(value) {
|
|
1216
|
-
this._netInfo.throwErrors = value;
|
|
1217
|
-
this._toggle.throwErrors = value;
|
|
1218
|
-
this._link.throwErrors = value;
|
|
1219
|
-
}
|
|
1220
1430
|
};
|
|
1221
1431
|
export {
|
|
1222
1432
|
Hyphen,
|
|
1223
1433
|
Toggle,
|
|
1224
|
-
ToggleHooks,
|
|
1225
1434
|
env,
|
|
1226
1435
|
loadEnv
|
|
1227
1436
|
};
|