@hyphen/react-sdk 1.0.2 → 1.0.4
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 +597 -0
- package/dist/index.d.cts +71 -0
- package/dist/index.js +497 -4
- package/package.json +14 -12
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Toggle: () => Toggle,
|
|
24
|
+
ToggleContext: () => ToggleContext,
|
|
25
|
+
ToggleProvider: () => ToggleProvider,
|
|
26
|
+
useToggle: () => useToggle,
|
|
27
|
+
withToggleProvider: () => withToggleProvider
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// node_modules/.pnpm/@hyphen+browser-sdk@1.0.6/node_modules/@hyphen/browser-sdk/dist/index.js
|
|
32
|
+
var import_hookified = require("hookified");
|
|
33
|
+
var __defProp2 = Object.defineProperty;
|
|
34
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
35
|
+
var __name = (target, value) => __defProp2(target, "name", { value, configurable: true });
|
|
36
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
37
|
+
var ToggleEvents = /* @__PURE__ */ (function(ToggleEvents2) {
|
|
38
|
+
ToggleEvents2["Error"] = "error";
|
|
39
|
+
return ToggleEvents2;
|
|
40
|
+
})({});
|
|
41
|
+
var _Toggle = class _Toggle2 extends import_hookified.Hookified {
|
|
42
|
+
constructor(options) {
|
|
43
|
+
super();
|
|
44
|
+
__publicField(this, "_publicApiKey");
|
|
45
|
+
__publicField(this, "_organizationId");
|
|
46
|
+
__publicField(this, "_applicationId");
|
|
47
|
+
__publicField(this, "_environment");
|
|
48
|
+
__publicField(this, "_horizonUrls", []);
|
|
49
|
+
__publicField(this, "_defaultContext");
|
|
50
|
+
__publicField(this, "_defaultTargetingKey", `${Math.random().toString(36).substring(7)}`);
|
|
51
|
+
if (options == null ? void 0 : options.applicationId) {
|
|
52
|
+
this._applicationId = options.applicationId;
|
|
53
|
+
}
|
|
54
|
+
if (options == null ? void 0 : options.environment) {
|
|
55
|
+
this._environment = options.environment;
|
|
56
|
+
} else {
|
|
57
|
+
this._environment = "development";
|
|
58
|
+
}
|
|
59
|
+
if (options == null ? void 0 : options.defaultContext) {
|
|
60
|
+
this._defaultContext = options.defaultContext;
|
|
61
|
+
}
|
|
62
|
+
if (options == null ? void 0 : options.publicApiKey) {
|
|
63
|
+
this._publicApiKey = options.publicApiKey;
|
|
64
|
+
this._organizationId = this.getOrgIdFromPublicKey(this._publicApiKey);
|
|
65
|
+
}
|
|
66
|
+
if (options == null ? void 0 : options.horizonUrls) {
|
|
67
|
+
this._horizonUrls = options.horizonUrls;
|
|
68
|
+
} else {
|
|
69
|
+
this._horizonUrls = this.getDefaultHorizonUrls(this._publicApiKey);
|
|
70
|
+
}
|
|
71
|
+
if (options == null ? void 0 : options.defaultTargetKey) {
|
|
72
|
+
this._defaultTargetingKey = options == null ? void 0 : options.defaultTargetKey;
|
|
73
|
+
} else {
|
|
74
|
+
if (this._defaultContext) {
|
|
75
|
+
this._defaultTargetingKey = this.getTargetingKey(this._defaultContext);
|
|
76
|
+
} else {
|
|
77
|
+
this._defaultTargetingKey = this.generateTargetKey();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Gets the public API key used for authentication.
|
|
83
|
+
*
|
|
84
|
+
* @returns The current public API key or undefined if not set
|
|
85
|
+
*/
|
|
86
|
+
get publicApiKey() {
|
|
87
|
+
return this._publicApiKey;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Sets the public API key used for authentication.
|
|
91
|
+
*
|
|
92
|
+
* @param value - The public API key string or undefined to clear
|
|
93
|
+
* @throws {Error} If the key doesn't start with "public_"
|
|
94
|
+
*/
|
|
95
|
+
set publicApiKey(value) {
|
|
96
|
+
this.setPublicKey(value);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Gets the default context used for toggle evaluations.
|
|
100
|
+
*
|
|
101
|
+
* @returns The current default ToggleContext
|
|
102
|
+
*/
|
|
103
|
+
get defaultContext() {
|
|
104
|
+
return this._defaultContext;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Sets the default context used for toggle evaluations.
|
|
108
|
+
*
|
|
109
|
+
* @param value - The ToggleContext to use as default
|
|
110
|
+
*/
|
|
111
|
+
set defaultContext(value) {
|
|
112
|
+
this._defaultContext = value;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Gets the organization ID extracted from the public API key.
|
|
116
|
+
*
|
|
117
|
+
* @returns The organization ID string or undefined if not available
|
|
118
|
+
*/
|
|
119
|
+
get organizationId() {
|
|
120
|
+
return this._organizationId;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Gets the Horizon endpoint URLs used for load balancing.
|
|
124
|
+
*
|
|
125
|
+
* These URLs are used to distribute requests across multiple Horizon endpoints.
|
|
126
|
+
* If endpoints fail, the system will attempt to use the default horizon endpoint service.
|
|
127
|
+
*
|
|
128
|
+
* @returns Array of Horizon endpoint URLs
|
|
129
|
+
* @see {@link https://hyphen.ai/horizon} for more information
|
|
130
|
+
*/
|
|
131
|
+
get horizonUrls() {
|
|
132
|
+
return this._horizonUrls;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Sets the Horizon endpoint URLs for load balancing.
|
|
136
|
+
*
|
|
137
|
+
* Configures multiple Horizon endpoints that will be used for load balancing.
|
|
138
|
+
* When endpoints fail, the system will fall back to the default horizon endpoint service.
|
|
139
|
+
*
|
|
140
|
+
* @param value - Array of Horizon endpoint URLs or empty array to clear
|
|
141
|
+
* @see {@link https://hyphen.ai/horizon} for more information
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```typescript
|
|
145
|
+
* const toggle = new Toggle();
|
|
146
|
+
* toggle.horizonUrls = [
|
|
147
|
+
* 'https://org1.toggle.hyphen.cloud',
|
|
148
|
+
* 'https://org2.toggle.hyphen.cloud'
|
|
149
|
+
* ];
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
set horizonUrls(value) {
|
|
153
|
+
this._horizonUrls = value;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Gets the application ID used for toggle context.
|
|
157
|
+
*
|
|
158
|
+
* @returns The current application ID or undefined if not set
|
|
159
|
+
*/
|
|
160
|
+
get applicationId() {
|
|
161
|
+
return this._applicationId;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Sets the application ID used for toggle context.
|
|
165
|
+
*
|
|
166
|
+
* @param value - The application ID string or undefined to clear
|
|
167
|
+
*/
|
|
168
|
+
set applicationId(value) {
|
|
169
|
+
this._applicationId = value;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Gets the environment used for toggle context.
|
|
173
|
+
*
|
|
174
|
+
* @returns The current environment (defaults to 'development')
|
|
175
|
+
*/
|
|
176
|
+
get environment() {
|
|
177
|
+
return this._environment;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Sets the environment used for toggle context.
|
|
181
|
+
*
|
|
182
|
+
* @param value - The environment string or undefined to clear
|
|
183
|
+
*/
|
|
184
|
+
set environment(value) {
|
|
185
|
+
this._environment = value;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Gets the default targeting key used for toggle evaluations.
|
|
189
|
+
*
|
|
190
|
+
* @returns The current default targeting key or undefined if not set
|
|
191
|
+
*/
|
|
192
|
+
get defaultTargetingKey() {
|
|
193
|
+
return this._defaultTargetingKey;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Sets the default targeting key used for toggle evaluations.
|
|
197
|
+
*
|
|
198
|
+
* @param value - The targeting key string or undefined to clear
|
|
199
|
+
*/
|
|
200
|
+
set defaultTargetingKey(value) {
|
|
201
|
+
this._defaultTargetingKey = value;
|
|
202
|
+
}
|
|
203
|
+
async get(toggleKey, defaultValue, options) {
|
|
204
|
+
var _a, _b, _c, _d, _e, _f;
|
|
205
|
+
try {
|
|
206
|
+
const context = {
|
|
207
|
+
application: (_a = this._applicationId) != null ? _a : "",
|
|
208
|
+
environment: (_b = this._environment) != null ? _b : "development"
|
|
209
|
+
};
|
|
210
|
+
if (options == null ? void 0 : options.context) {
|
|
211
|
+
context.targetingKey = options == null ? void 0 : options.context.targetingKey;
|
|
212
|
+
context.ipAddress = options == null ? void 0 : options.context.ipAddress;
|
|
213
|
+
context.user = options == null ? void 0 : options.context.user;
|
|
214
|
+
context.customAttributes = options == null ? void 0 : options.context.customAttributes;
|
|
215
|
+
} else {
|
|
216
|
+
context.targetingKey = (_c = this._defaultContext) == null ? void 0 : _c.targetingKey;
|
|
217
|
+
context.ipAddress = (_d = this._defaultContext) == null ? void 0 : _d.ipAddress;
|
|
218
|
+
context.user = (_e = this._defaultContext) == null ? void 0 : _e.user;
|
|
219
|
+
context.customAttributes = (_f = this._defaultContext) == null ? void 0 : _f.customAttributes;
|
|
220
|
+
}
|
|
221
|
+
const fetchOptions = {
|
|
222
|
+
headers: {}
|
|
223
|
+
};
|
|
224
|
+
if (!context.targetingKey) {
|
|
225
|
+
context.targetingKey = this.getTargetingKey(context);
|
|
226
|
+
}
|
|
227
|
+
if (this._publicApiKey) {
|
|
228
|
+
fetchOptions.headers["x-api-key"] = this._publicApiKey;
|
|
229
|
+
} else {
|
|
230
|
+
throw new Error("You must set the publicApiKey");
|
|
231
|
+
}
|
|
232
|
+
if (context.application === "") {
|
|
233
|
+
throw new Error("You must set the applicationId");
|
|
234
|
+
}
|
|
235
|
+
const result = await this.fetch("/toggle/evaluate", context, fetchOptions);
|
|
236
|
+
if (result == null ? void 0 : result.toggles) {
|
|
237
|
+
return result.toggles[toggleKey].value;
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
this.emit(ToggleEvents.Error, error);
|
|
241
|
+
}
|
|
242
|
+
return defaultValue;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Retrieves a boolean toggle value.
|
|
246
|
+
*
|
|
247
|
+
* This is a convenience method that wraps the generic get() method with boolean type safety.
|
|
248
|
+
*
|
|
249
|
+
* @param toggleKey - The key of the toggle to retrieve
|
|
250
|
+
* @param defaultValue - The boolean value to return if the toggle is not found or an error occurs
|
|
251
|
+
* @param options - Optional configuration including context for toggle evaluation
|
|
252
|
+
* @returns Promise resolving to the boolean toggle value or defaultValue
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* ```typescript
|
|
256
|
+
* const toggle = new Toggle({ publicApiKey: 'public_key', applicationId: 'app-id' });
|
|
257
|
+
* const isFeatureEnabled = await toggle.getBoolean('feature-flag', false);
|
|
258
|
+
* console.log(isFeatureEnabled); // true or false
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
async getBoolean(toggleKey, defaultValue, options) {
|
|
262
|
+
return this.get(toggleKey, defaultValue, options);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Retrieves a string toggle value.
|
|
266
|
+
*
|
|
267
|
+
* This is a convenience method that wraps the generic get() method with string type safety.
|
|
268
|
+
*
|
|
269
|
+
* @param toggleKey - The key of the toggle to retrieve
|
|
270
|
+
* @param defaultValue - The string value to return if the toggle is not found or an error occurs
|
|
271
|
+
* @param options - Optional configuration including context for toggle evaluation
|
|
272
|
+
* @returns Promise resolving to the string toggle value or defaultValue
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* ```typescript
|
|
276
|
+
* const toggle = new Toggle({ publicApiKey: 'public_key', applicationId: 'app-id' });
|
|
277
|
+
* const message = await toggle.getString('welcome-message', 'Hello World');
|
|
278
|
+
* console.log(message); // 'Welcome to our app!' or 'Hello World'
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
async getString(toggleKey, defaultValue, options) {
|
|
282
|
+
return this.get(toggleKey, defaultValue, options);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Retrieves an object toggle value.
|
|
286
|
+
*
|
|
287
|
+
* This is a convenience method that wraps the generic get() method with object type safety.
|
|
288
|
+
* Note that the toggle service may return JSON as a string, which should be parsed if needed.
|
|
289
|
+
*
|
|
290
|
+
* @template T - The expected object type
|
|
291
|
+
* @param toggleKey - The key of the toggle to retrieve
|
|
292
|
+
* @param defaultValue - The object value to return if the toggle is not found or an error occurs
|
|
293
|
+
* @param options - Optional configuration including context for toggle evaluation
|
|
294
|
+
* @returns Promise resolving to the object toggle value or defaultValue
|
|
295
|
+
*
|
|
296
|
+
* @example
|
|
297
|
+
* ```typescript
|
|
298
|
+
* const toggle = new Toggle({ publicApiKey: 'public_key', applicationId: 'app-id' });
|
|
299
|
+
* const config = await toggle.getObject('app-config', { theme: 'light' });
|
|
300
|
+
* console.log(config); // { theme: 'dark', features: ['a', 'b'] } or { theme: 'light' }
|
|
301
|
+
* ```
|
|
302
|
+
*/
|
|
303
|
+
async getObject(toggleKey, defaultValue, options) {
|
|
304
|
+
return this.get(toggleKey, defaultValue, options);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Retrieves a number toggle value.
|
|
308
|
+
*
|
|
309
|
+
* This is a convenience method that wraps the generic get() method with number type safety.
|
|
310
|
+
*
|
|
311
|
+
* @param toggleKey - The key of the toggle to retrieve
|
|
312
|
+
* @param defaultValue - The number value to return if the toggle is not found or an error occurs
|
|
313
|
+
* @param options - Optional configuration including context for toggle evaluation
|
|
314
|
+
* @returns Promise resolving to the number toggle value or defaultValue
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* ```typescript
|
|
318
|
+
* const toggle = new Toggle({ publicApiKey: 'public_key', applicationId: 'app-id' });
|
|
319
|
+
* const maxRetries = await toggle.getNumber('max-retries', 3);
|
|
320
|
+
* console.log(maxRetries); // 5 or 3
|
|
321
|
+
* ```
|
|
322
|
+
*/
|
|
323
|
+
async getNumber(toggleKey, defaultValue, options) {
|
|
324
|
+
return this.get(toggleKey, defaultValue, options);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Makes an HTTP POST request to the specified URL with automatic authentication.
|
|
328
|
+
*
|
|
329
|
+
* This method uses browser-compatible fetch and automatically includes the
|
|
330
|
+
* public API key in the x-api-key header if available. It supports load
|
|
331
|
+
* balancing across multiple horizon URLs with fallback behavior.
|
|
332
|
+
*
|
|
333
|
+
* @template T - The expected response type
|
|
334
|
+
* @param path - The API path to request (e.g., '/api/toggles')
|
|
335
|
+
* @param payload - The JSON payload to send in the request body
|
|
336
|
+
* @param options - Optional fetch configuration
|
|
337
|
+
* @returns Promise resolving to the parsed JSON response
|
|
338
|
+
* @throws {Error} If no horizon URLs are configured or all requests fail
|
|
339
|
+
*
|
|
340
|
+
* @example
|
|
341
|
+
* ```typescript
|
|
342
|
+
* const toggle = new Toggle({
|
|
343
|
+
* publicApiKey: 'public_your-key-here',
|
|
344
|
+
* horizonUrls: ['https://api.hyphen.cloud']
|
|
345
|
+
* });
|
|
346
|
+
*
|
|
347
|
+
* interface ToggleResponse {
|
|
348
|
+
* enabled: boolean;
|
|
349
|
+
* value: string;
|
|
350
|
+
* }
|
|
351
|
+
*
|
|
352
|
+
* const result = await toggle.fetch<ToggleResponse>('/api/toggle/feature-flag', {
|
|
353
|
+
* context: { targetingKey: 'user-123' }
|
|
354
|
+
* });
|
|
355
|
+
* console.log(result.enabled); // true/false
|
|
356
|
+
* ```
|
|
357
|
+
*/
|
|
358
|
+
async fetch(path, payload, options) {
|
|
359
|
+
if (this._horizonUrls.length === 0) {
|
|
360
|
+
throw new Error("No horizon URLs configured. Set horizonUrls or provide a valid publicApiKey.");
|
|
361
|
+
}
|
|
362
|
+
const headers = {
|
|
363
|
+
"Content-Type": "application/json"
|
|
364
|
+
};
|
|
365
|
+
if (options == null ? void 0 : options.headers) {
|
|
366
|
+
if (options.headers instanceof Headers) {
|
|
367
|
+
options.headers.forEach((value, key) => {
|
|
368
|
+
headers[key] = value;
|
|
369
|
+
});
|
|
370
|
+
} else if (Array.isArray(options.headers)) {
|
|
371
|
+
options.headers.forEach(([key, value]) => {
|
|
372
|
+
headers[key] = value;
|
|
373
|
+
});
|
|
374
|
+
} else {
|
|
375
|
+
Object.assign(headers, options.headers);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (this._publicApiKey) {
|
|
379
|
+
headers["x-api-key"] = this._publicApiKey;
|
|
380
|
+
}
|
|
381
|
+
const fetchOptions = {
|
|
382
|
+
method: "POST",
|
|
383
|
+
...options,
|
|
384
|
+
headers,
|
|
385
|
+
body: payload ? JSON.stringify(payload) : options == null ? void 0 : options.body
|
|
386
|
+
};
|
|
387
|
+
const errors = [];
|
|
388
|
+
for (const baseUrl of this._horizonUrls) {
|
|
389
|
+
try {
|
|
390
|
+
const url = `${baseUrl.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
391
|
+
const response = await fetch(url, fetchOptions);
|
|
392
|
+
if (!response.ok) {
|
|
393
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
394
|
+
}
|
|
395
|
+
const data = await response.json();
|
|
396
|
+
return data;
|
|
397
|
+
} catch (error) {
|
|
398
|
+
const fetchError = error instanceof Error ? error : new Error("Unknown fetch error");
|
|
399
|
+
errors.push(fetchError);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
throw new Error(`All horizon URLs failed. Last errors: ${errors.map((e) => e.message).join(", ")}`);
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Validates and sets the public API key. This is used internally
|
|
406
|
+
*
|
|
407
|
+
* @param key - The public API key string or undefined to clear
|
|
408
|
+
* @throws {Error} If the key doesn't start with "public_"
|
|
409
|
+
*/
|
|
410
|
+
setPublicKey(key) {
|
|
411
|
+
if (key !== void 0 && !key.startsWith("public_")) {
|
|
412
|
+
throw new Error("Public API key must start with 'public_'");
|
|
413
|
+
}
|
|
414
|
+
this._publicApiKey = key;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Extracts the organization ID from a public API key.
|
|
418
|
+
*
|
|
419
|
+
* The public key format is: `public_<base64-encoded-data>`
|
|
420
|
+
* The base64 data contains: `orgId:secretData`
|
|
421
|
+
* Only alphanumeric characters, underscores, and hyphens are considered valid in org IDs.
|
|
422
|
+
*
|
|
423
|
+
* @param publicKey - The public API key to extract the organization ID from
|
|
424
|
+
* @returns The organization ID if valid and extractable, undefined otherwise
|
|
425
|
+
*
|
|
426
|
+
* @example
|
|
427
|
+
* ```typescript
|
|
428
|
+
* const toggle = new Toggle();
|
|
429
|
+
* const orgId = toggle.getOrgIdFromPublicKey('public_dGVzdC1vcmc6c2VjcmV0');
|
|
430
|
+
* console.log(orgId); // 'test-org'
|
|
431
|
+
* ```
|
|
432
|
+
*/
|
|
433
|
+
getOrgIdFromPublicKey(publicKey) {
|
|
434
|
+
try {
|
|
435
|
+
const keyWithoutPrefix = publicKey.replace(/^public_/, "");
|
|
436
|
+
const decoded = globalThis.atob ? globalThis.atob(keyWithoutPrefix) : Buffer.from(keyWithoutPrefix, "base64").toString();
|
|
437
|
+
const [orgId] = decoded.split(":");
|
|
438
|
+
const isValidOrgId = /^[a-zA-Z0-9_-]+$/.test(orgId);
|
|
439
|
+
return isValidOrgId ? orgId : void 0;
|
|
440
|
+
} catch (e) {
|
|
441
|
+
return void 0;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Builds the default Horizon API URL for the given public key.
|
|
446
|
+
*
|
|
447
|
+
* If a valid organization ID can be extracted from the public key, returns an
|
|
448
|
+
* organization-specific URL. Otherwise, returns the default fallback URL.
|
|
449
|
+
*
|
|
450
|
+
* @param publicKey - The public API key to build the URL for
|
|
451
|
+
* @returns Organization-specific URL or default fallback URL
|
|
452
|
+
*
|
|
453
|
+
* @example
|
|
454
|
+
* ```typescript
|
|
455
|
+
* const toggle = new Toggle();
|
|
456
|
+
*
|
|
457
|
+
* // With valid org ID
|
|
458
|
+
* const orgUrl = toggle.buildDefaultHorizonUrl('public_dGVzdC1vcmc6c2VjcmV0');
|
|
459
|
+
* console.log(orgUrl); // 'https://test-org.toggle.hyphen.cloud'
|
|
460
|
+
*
|
|
461
|
+
* // With invalid key
|
|
462
|
+
* const defaultUrl = toggle.buildDefaultHorizonUrl('invalid-key');
|
|
463
|
+
* console.log(defaultUrl); // 'https://toggle.hyphen.cloud'
|
|
464
|
+
* ```
|
|
465
|
+
*/
|
|
466
|
+
getDefaultHorizonUrl(publicKey) {
|
|
467
|
+
if (publicKey) {
|
|
468
|
+
const orgId = this.getOrgIdFromPublicKey(publicKey);
|
|
469
|
+
return orgId ? `https://${orgId}.toggle.hyphen.cloud` : "https://toggle.hyphen.cloud";
|
|
470
|
+
}
|
|
471
|
+
return "https://toggle.hyphen.cloud";
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Will get the urls. If you pass in the public key it will provide two urls.
|
|
475
|
+
* @param publicKey
|
|
476
|
+
* @returns
|
|
477
|
+
*/
|
|
478
|
+
getDefaultHorizonUrls(publicKey) {
|
|
479
|
+
let result = [
|
|
480
|
+
this.getDefaultHorizonUrl()
|
|
481
|
+
];
|
|
482
|
+
if (publicKey) {
|
|
483
|
+
const defaultUrl = result[0];
|
|
484
|
+
const orgUrl = this.getDefaultHorizonUrl(publicKey);
|
|
485
|
+
result = [];
|
|
486
|
+
if (orgUrl !== defaultUrl) {
|
|
487
|
+
result.push(orgUrl);
|
|
488
|
+
}
|
|
489
|
+
result.push(defaultUrl);
|
|
490
|
+
}
|
|
491
|
+
return result;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Generates a unique targeting key based on available context.
|
|
495
|
+
*
|
|
496
|
+
* @returns A targeting key in the format: `[app]-[env]-[random]` or simplified versions
|
|
497
|
+
*/
|
|
498
|
+
generateTargetKey() {
|
|
499
|
+
const randomSuffix = Math.random().toString(36).substring(7);
|
|
500
|
+
const app = this._applicationId || "";
|
|
501
|
+
const env = this._environment || "";
|
|
502
|
+
const components = [
|
|
503
|
+
app,
|
|
504
|
+
env,
|
|
505
|
+
randomSuffix
|
|
506
|
+
].filter(Boolean);
|
|
507
|
+
return components.join("-");
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Extracts targeting key from a toggle context with fallback logic.
|
|
511
|
+
*
|
|
512
|
+
* @param context - The toggle context to extract targeting key from
|
|
513
|
+
* @returns The targeting key string
|
|
514
|
+
*/
|
|
515
|
+
getTargetingKey(context) {
|
|
516
|
+
if (context.targetingKey) {
|
|
517
|
+
return context.targetingKey;
|
|
518
|
+
}
|
|
519
|
+
if (context.user) {
|
|
520
|
+
return context.user.id;
|
|
521
|
+
}
|
|
522
|
+
return this._defaultTargetingKey;
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
__name(_Toggle, "Toggle");
|
|
526
|
+
var Toggle = _Toggle;
|
|
527
|
+
|
|
528
|
+
// src/toggle-provider.tsx
|
|
529
|
+
var import_react = require("react");
|
|
530
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
531
|
+
var ToggleContext = (0, import_react.createContext)(null);
|
|
532
|
+
function ToggleProvider({
|
|
533
|
+
children,
|
|
534
|
+
publicApiKey,
|
|
535
|
+
applicationId,
|
|
536
|
+
environment,
|
|
537
|
+
defaultContext,
|
|
538
|
+
horizonUrls,
|
|
539
|
+
defaultTargetKey
|
|
540
|
+
}) {
|
|
541
|
+
const toggle = (0, import_react.useMemo)(() => {
|
|
542
|
+
return new Toggle({
|
|
543
|
+
publicApiKey,
|
|
544
|
+
applicationId,
|
|
545
|
+
environment,
|
|
546
|
+
defaultContext,
|
|
547
|
+
horizonUrls,
|
|
548
|
+
defaultTargetKey
|
|
549
|
+
});
|
|
550
|
+
}, [
|
|
551
|
+
publicApiKey,
|
|
552
|
+
applicationId,
|
|
553
|
+
environment,
|
|
554
|
+
defaultContext,
|
|
555
|
+
horizonUrls,
|
|
556
|
+
defaultTargetKey
|
|
557
|
+
]);
|
|
558
|
+
(0, import_react.useEffect)(() => {
|
|
559
|
+
return () => {
|
|
560
|
+
};
|
|
561
|
+
}, [toggle]);
|
|
562
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToggleContext.Provider, { value: toggle, children });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// src/use-toggle.tsx
|
|
566
|
+
var import_react2 = require("react");
|
|
567
|
+
function useToggle() {
|
|
568
|
+
const toggle = (0, import_react2.useContext)(ToggleContext);
|
|
569
|
+
if (!toggle) {
|
|
570
|
+
throw new Error(
|
|
571
|
+
"useToggle must be used within a ToggleProvider. Wrap your component tree with <ToggleProvider> or use withToggleProvider()."
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
return toggle;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// src/with-toggle-provider.tsx
|
|
578
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
579
|
+
function withToggleProvider(options) {
|
|
580
|
+
return function(Component) {
|
|
581
|
+
const WrappedComponent = (props) => {
|
|
582
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ToggleProvider, { ...options, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Component, { ...props }) });
|
|
583
|
+
};
|
|
584
|
+
const componentName = Component.displayName || Component.name || "Component";
|
|
585
|
+
WrappedComponent.displayName = `withToggleProvider(${componentName})`;
|
|
586
|
+
return WrappedComponent;
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
590
|
+
0 && (module.exports = {
|
|
591
|
+
Toggle,
|
|
592
|
+
ToggleContext,
|
|
593
|
+
ToggleProvider,
|
|
594
|
+
useToggle,
|
|
595
|
+
withToggleProvider
|
|
596
|
+
});
|
|
597
|
+
/* v8 ignore next -- @preserve */
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ToggleOptions, Toggle } from '@hyphen/browser-sdk';
|
|
2
|
+
export { Toggle, ToggleOptions } from '@hyphen/browser-sdk';
|
|
3
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
|
+
import { ReactNode, Context, ComponentType } from 'react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Props for the ToggleProvider component
|
|
8
|
+
* Extends ToggleOptions from @hyphen/browser-sdk
|
|
9
|
+
*/
|
|
10
|
+
interface ToggleProviderProps extends ToggleOptions {
|
|
11
|
+
/**
|
|
12
|
+
* React children to be wrapped by the provider
|
|
13
|
+
*/
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* React Context for the Toggle instance
|
|
19
|
+
*/
|
|
20
|
+
declare const ToggleContext: Context<Toggle | null>;
|
|
21
|
+
/**
|
|
22
|
+
* Provider component that creates and provides a Toggle instance to the React tree
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* <ToggleProvider publicApiKey="public_..." applicationId="my-app">
|
|
27
|
+
* <App />
|
|
28
|
+
* </ToggleProvider>
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
declare function ToggleProvider({ children, publicApiKey, applicationId, environment, defaultContext, horizonUrls, defaultTargetKey, }: ToggleProviderProps): react_jsx_runtime.JSX.Element;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Hook to access the Toggle instance from the ToggleProvider
|
|
35
|
+
*
|
|
36
|
+
* @throws {Error} If used outside of a ToggleProvider
|
|
37
|
+
* @returns {Toggle} The Toggle instance from the provider
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* function MyComponent() {
|
|
42
|
+
* const toggle = useToggle();
|
|
43
|
+
* const isEnabled = toggle.getBoolean('my-feature', false);
|
|
44
|
+
*
|
|
45
|
+
* return <div>{isEnabled ? 'Feature enabled' : 'Feature disabled'}</div>;
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
declare function useToggle(): Toggle;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Higher-order component that wraps a component with ToggleProvider
|
|
53
|
+
*
|
|
54
|
+
* @param options - Configuration options for the Toggle instance
|
|
55
|
+
* @returns A function that wraps a component with ToggleProvider
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```tsx
|
|
59
|
+
* export default withToggleProvider({
|
|
60
|
+
* publicApiKey: "public_...",
|
|
61
|
+
* applicationId: "my-app",
|
|
62
|
+
* environment: "production",
|
|
63
|
+
* defaultContext: {
|
|
64
|
+
* user: { id: "user-123" }
|
|
65
|
+
* }
|
|
66
|
+
* })(App);
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
declare function withToggleProvider(options: ToggleOptions): <P extends object>(Component: ComponentType<P>) => ComponentType<P>;
|
|
70
|
+
|
|
71
|
+
export { ToggleContext, ToggleProvider, type ToggleProviderProps, useToggle, withToggleProvider };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,499 @@
|
|
|
1
|
-
//
|
|
2
|
-
import {
|
|
1
|
+
// node_modules/.pnpm/@hyphen+browser-sdk@1.0.6/node_modules/@hyphen/browser-sdk/dist/index.js
|
|
2
|
+
import { Hookified } from "hookified";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
5
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
6
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
7
|
+
var ToggleEvents = /* @__PURE__ */ (function(ToggleEvents2) {
|
|
8
|
+
ToggleEvents2["Error"] = "error";
|
|
9
|
+
return ToggleEvents2;
|
|
10
|
+
})({});
|
|
11
|
+
var _Toggle = class _Toggle2 extends Hookified {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
super();
|
|
14
|
+
__publicField(this, "_publicApiKey");
|
|
15
|
+
__publicField(this, "_organizationId");
|
|
16
|
+
__publicField(this, "_applicationId");
|
|
17
|
+
__publicField(this, "_environment");
|
|
18
|
+
__publicField(this, "_horizonUrls", []);
|
|
19
|
+
__publicField(this, "_defaultContext");
|
|
20
|
+
__publicField(this, "_defaultTargetingKey", `${Math.random().toString(36).substring(7)}`);
|
|
21
|
+
if (options == null ? void 0 : options.applicationId) {
|
|
22
|
+
this._applicationId = options.applicationId;
|
|
23
|
+
}
|
|
24
|
+
if (options == null ? void 0 : options.environment) {
|
|
25
|
+
this._environment = options.environment;
|
|
26
|
+
} else {
|
|
27
|
+
this._environment = "development";
|
|
28
|
+
}
|
|
29
|
+
if (options == null ? void 0 : options.defaultContext) {
|
|
30
|
+
this._defaultContext = options.defaultContext;
|
|
31
|
+
}
|
|
32
|
+
if (options == null ? void 0 : options.publicApiKey) {
|
|
33
|
+
this._publicApiKey = options.publicApiKey;
|
|
34
|
+
this._organizationId = this.getOrgIdFromPublicKey(this._publicApiKey);
|
|
35
|
+
}
|
|
36
|
+
if (options == null ? void 0 : options.horizonUrls) {
|
|
37
|
+
this._horizonUrls = options.horizonUrls;
|
|
38
|
+
} else {
|
|
39
|
+
this._horizonUrls = this.getDefaultHorizonUrls(this._publicApiKey);
|
|
40
|
+
}
|
|
41
|
+
if (options == null ? void 0 : options.defaultTargetKey) {
|
|
42
|
+
this._defaultTargetingKey = options == null ? void 0 : options.defaultTargetKey;
|
|
43
|
+
} else {
|
|
44
|
+
if (this._defaultContext) {
|
|
45
|
+
this._defaultTargetingKey = this.getTargetingKey(this._defaultContext);
|
|
46
|
+
} else {
|
|
47
|
+
this._defaultTargetingKey = this.generateTargetKey();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Gets the public API key used for authentication.
|
|
53
|
+
*
|
|
54
|
+
* @returns The current public API key or undefined if not set
|
|
55
|
+
*/
|
|
56
|
+
get publicApiKey() {
|
|
57
|
+
return this._publicApiKey;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Sets the public API key used for authentication.
|
|
61
|
+
*
|
|
62
|
+
* @param value - The public API key string or undefined to clear
|
|
63
|
+
* @throws {Error} If the key doesn't start with "public_"
|
|
64
|
+
*/
|
|
65
|
+
set publicApiKey(value) {
|
|
66
|
+
this.setPublicKey(value);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Gets the default context used for toggle evaluations.
|
|
70
|
+
*
|
|
71
|
+
* @returns The current default ToggleContext
|
|
72
|
+
*/
|
|
73
|
+
get defaultContext() {
|
|
74
|
+
return this._defaultContext;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Sets the default context used for toggle evaluations.
|
|
78
|
+
*
|
|
79
|
+
* @param value - The ToggleContext to use as default
|
|
80
|
+
*/
|
|
81
|
+
set defaultContext(value) {
|
|
82
|
+
this._defaultContext = value;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Gets the organization ID extracted from the public API key.
|
|
86
|
+
*
|
|
87
|
+
* @returns The organization ID string or undefined if not available
|
|
88
|
+
*/
|
|
89
|
+
get organizationId() {
|
|
90
|
+
return this._organizationId;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Gets the Horizon endpoint URLs used for load balancing.
|
|
94
|
+
*
|
|
95
|
+
* These URLs are used to distribute requests across multiple Horizon endpoints.
|
|
96
|
+
* If endpoints fail, the system will attempt to use the default horizon endpoint service.
|
|
97
|
+
*
|
|
98
|
+
* @returns Array of Horizon endpoint URLs
|
|
99
|
+
* @see {@link https://hyphen.ai/horizon} for more information
|
|
100
|
+
*/
|
|
101
|
+
get horizonUrls() {
|
|
102
|
+
return this._horizonUrls;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Sets the Horizon endpoint URLs for load balancing.
|
|
106
|
+
*
|
|
107
|
+
* Configures multiple Horizon endpoints that will be used for load balancing.
|
|
108
|
+
* When endpoints fail, the system will fall back to the default horizon endpoint service.
|
|
109
|
+
*
|
|
110
|
+
* @param value - Array of Horizon endpoint URLs or empty array to clear
|
|
111
|
+
* @see {@link https://hyphen.ai/horizon} for more information
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```typescript
|
|
115
|
+
* const toggle = new Toggle();
|
|
116
|
+
* toggle.horizonUrls = [
|
|
117
|
+
* 'https://org1.toggle.hyphen.cloud',
|
|
118
|
+
* 'https://org2.toggle.hyphen.cloud'
|
|
119
|
+
* ];
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
set horizonUrls(value) {
|
|
123
|
+
this._horizonUrls = value;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Gets the application ID used for toggle context.
|
|
127
|
+
*
|
|
128
|
+
* @returns The current application ID or undefined if not set
|
|
129
|
+
*/
|
|
130
|
+
get applicationId() {
|
|
131
|
+
return this._applicationId;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Sets the application ID used for toggle context.
|
|
135
|
+
*
|
|
136
|
+
* @param value - The application ID string or undefined to clear
|
|
137
|
+
*/
|
|
138
|
+
set applicationId(value) {
|
|
139
|
+
this._applicationId = value;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Gets the environment used for toggle context.
|
|
143
|
+
*
|
|
144
|
+
* @returns The current environment (defaults to 'development')
|
|
145
|
+
*/
|
|
146
|
+
get environment() {
|
|
147
|
+
return this._environment;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Sets the environment used for toggle context.
|
|
151
|
+
*
|
|
152
|
+
* @param value - The environment string or undefined to clear
|
|
153
|
+
*/
|
|
154
|
+
set environment(value) {
|
|
155
|
+
this._environment = value;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Gets the default targeting key used for toggle evaluations.
|
|
159
|
+
*
|
|
160
|
+
* @returns The current default targeting key or undefined if not set
|
|
161
|
+
*/
|
|
162
|
+
get defaultTargetingKey() {
|
|
163
|
+
return this._defaultTargetingKey;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Sets the default targeting key used for toggle evaluations.
|
|
167
|
+
*
|
|
168
|
+
* @param value - The targeting key string or undefined to clear
|
|
169
|
+
*/
|
|
170
|
+
set defaultTargetingKey(value) {
|
|
171
|
+
this._defaultTargetingKey = value;
|
|
172
|
+
}
|
|
173
|
+
async get(toggleKey, defaultValue, options) {
|
|
174
|
+
var _a, _b, _c, _d, _e, _f;
|
|
175
|
+
try {
|
|
176
|
+
const context = {
|
|
177
|
+
application: (_a = this._applicationId) != null ? _a : "",
|
|
178
|
+
environment: (_b = this._environment) != null ? _b : "development"
|
|
179
|
+
};
|
|
180
|
+
if (options == null ? void 0 : options.context) {
|
|
181
|
+
context.targetingKey = options == null ? void 0 : options.context.targetingKey;
|
|
182
|
+
context.ipAddress = options == null ? void 0 : options.context.ipAddress;
|
|
183
|
+
context.user = options == null ? void 0 : options.context.user;
|
|
184
|
+
context.customAttributes = options == null ? void 0 : options.context.customAttributes;
|
|
185
|
+
} else {
|
|
186
|
+
context.targetingKey = (_c = this._defaultContext) == null ? void 0 : _c.targetingKey;
|
|
187
|
+
context.ipAddress = (_d = this._defaultContext) == null ? void 0 : _d.ipAddress;
|
|
188
|
+
context.user = (_e = this._defaultContext) == null ? void 0 : _e.user;
|
|
189
|
+
context.customAttributes = (_f = this._defaultContext) == null ? void 0 : _f.customAttributes;
|
|
190
|
+
}
|
|
191
|
+
const fetchOptions = {
|
|
192
|
+
headers: {}
|
|
193
|
+
};
|
|
194
|
+
if (!context.targetingKey) {
|
|
195
|
+
context.targetingKey = this.getTargetingKey(context);
|
|
196
|
+
}
|
|
197
|
+
if (this._publicApiKey) {
|
|
198
|
+
fetchOptions.headers["x-api-key"] = this._publicApiKey;
|
|
199
|
+
} else {
|
|
200
|
+
throw new Error("You must set the publicApiKey");
|
|
201
|
+
}
|
|
202
|
+
if (context.application === "") {
|
|
203
|
+
throw new Error("You must set the applicationId");
|
|
204
|
+
}
|
|
205
|
+
const result = await this.fetch("/toggle/evaluate", context, fetchOptions);
|
|
206
|
+
if (result == null ? void 0 : result.toggles) {
|
|
207
|
+
return result.toggles[toggleKey].value;
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
this.emit(ToggleEvents.Error, error);
|
|
211
|
+
}
|
|
212
|
+
return defaultValue;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Retrieves a boolean toggle value.
|
|
216
|
+
*
|
|
217
|
+
* This is a convenience method that wraps the generic get() method with boolean type safety.
|
|
218
|
+
*
|
|
219
|
+
* @param toggleKey - The key of the toggle to retrieve
|
|
220
|
+
* @param defaultValue - The boolean value to return if the toggle is not found or an error occurs
|
|
221
|
+
* @param options - Optional configuration including context for toggle evaluation
|
|
222
|
+
* @returns Promise resolving to the boolean toggle value or defaultValue
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* ```typescript
|
|
226
|
+
* const toggle = new Toggle({ publicApiKey: 'public_key', applicationId: 'app-id' });
|
|
227
|
+
* const isFeatureEnabled = await toggle.getBoolean('feature-flag', false);
|
|
228
|
+
* console.log(isFeatureEnabled); // true or false
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
async getBoolean(toggleKey, defaultValue, options) {
|
|
232
|
+
return this.get(toggleKey, defaultValue, options);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Retrieves a string toggle value.
|
|
236
|
+
*
|
|
237
|
+
* This is a convenience method that wraps the generic get() method with string type safety.
|
|
238
|
+
*
|
|
239
|
+
* @param toggleKey - The key of the toggle to retrieve
|
|
240
|
+
* @param defaultValue - The string value to return if the toggle is not found or an error occurs
|
|
241
|
+
* @param options - Optional configuration including context for toggle evaluation
|
|
242
|
+
* @returns Promise resolving to the string toggle value or defaultValue
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* ```typescript
|
|
246
|
+
* const toggle = new Toggle({ publicApiKey: 'public_key', applicationId: 'app-id' });
|
|
247
|
+
* const message = await toggle.getString('welcome-message', 'Hello World');
|
|
248
|
+
* console.log(message); // 'Welcome to our app!' or 'Hello World'
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
async getString(toggleKey, defaultValue, options) {
|
|
252
|
+
return this.get(toggleKey, defaultValue, options);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Retrieves an object toggle value.
|
|
256
|
+
*
|
|
257
|
+
* This is a convenience method that wraps the generic get() method with object type safety.
|
|
258
|
+
* Note that the toggle service may return JSON as a string, which should be parsed if needed.
|
|
259
|
+
*
|
|
260
|
+
* @template T - The expected object type
|
|
261
|
+
* @param toggleKey - The key of the toggle to retrieve
|
|
262
|
+
* @param defaultValue - The object value to return if the toggle is not found or an error occurs
|
|
263
|
+
* @param options - Optional configuration including context for toggle evaluation
|
|
264
|
+
* @returns Promise resolving to the object toggle value or defaultValue
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* ```typescript
|
|
268
|
+
* const toggle = new Toggle({ publicApiKey: 'public_key', applicationId: 'app-id' });
|
|
269
|
+
* const config = await toggle.getObject('app-config', { theme: 'light' });
|
|
270
|
+
* console.log(config); // { theme: 'dark', features: ['a', 'b'] } or { theme: 'light' }
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
async getObject(toggleKey, defaultValue, options) {
|
|
274
|
+
return this.get(toggleKey, defaultValue, options);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Retrieves a number toggle value.
|
|
278
|
+
*
|
|
279
|
+
* This is a convenience method that wraps the generic get() method with number type safety.
|
|
280
|
+
*
|
|
281
|
+
* @param toggleKey - The key of the toggle to retrieve
|
|
282
|
+
* @param defaultValue - The number value to return if the toggle is not found or an error occurs
|
|
283
|
+
* @param options - Optional configuration including context for toggle evaluation
|
|
284
|
+
* @returns Promise resolving to the number toggle value or defaultValue
|
|
285
|
+
*
|
|
286
|
+
* @example
|
|
287
|
+
* ```typescript
|
|
288
|
+
* const toggle = new Toggle({ publicApiKey: 'public_key', applicationId: 'app-id' });
|
|
289
|
+
* const maxRetries = await toggle.getNumber('max-retries', 3);
|
|
290
|
+
* console.log(maxRetries); // 5 or 3
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
async getNumber(toggleKey, defaultValue, options) {
|
|
294
|
+
return this.get(toggleKey, defaultValue, options);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Makes an HTTP POST request to the specified URL with automatic authentication.
|
|
298
|
+
*
|
|
299
|
+
* This method uses browser-compatible fetch and automatically includes the
|
|
300
|
+
* public API key in the x-api-key header if available. It supports load
|
|
301
|
+
* balancing across multiple horizon URLs with fallback behavior.
|
|
302
|
+
*
|
|
303
|
+
* @template T - The expected response type
|
|
304
|
+
* @param path - The API path to request (e.g., '/api/toggles')
|
|
305
|
+
* @param payload - The JSON payload to send in the request body
|
|
306
|
+
* @param options - Optional fetch configuration
|
|
307
|
+
* @returns Promise resolving to the parsed JSON response
|
|
308
|
+
* @throws {Error} If no horizon URLs are configured or all requests fail
|
|
309
|
+
*
|
|
310
|
+
* @example
|
|
311
|
+
* ```typescript
|
|
312
|
+
* const toggle = new Toggle({
|
|
313
|
+
* publicApiKey: 'public_your-key-here',
|
|
314
|
+
* horizonUrls: ['https://api.hyphen.cloud']
|
|
315
|
+
* });
|
|
316
|
+
*
|
|
317
|
+
* interface ToggleResponse {
|
|
318
|
+
* enabled: boolean;
|
|
319
|
+
* value: string;
|
|
320
|
+
* }
|
|
321
|
+
*
|
|
322
|
+
* const result = await toggle.fetch<ToggleResponse>('/api/toggle/feature-flag', {
|
|
323
|
+
* context: { targetingKey: 'user-123' }
|
|
324
|
+
* });
|
|
325
|
+
* console.log(result.enabled); // true/false
|
|
326
|
+
* ```
|
|
327
|
+
*/
|
|
328
|
+
async fetch(path, payload, options) {
|
|
329
|
+
if (this._horizonUrls.length === 0) {
|
|
330
|
+
throw new Error("No horizon URLs configured. Set horizonUrls or provide a valid publicApiKey.");
|
|
331
|
+
}
|
|
332
|
+
const headers = {
|
|
333
|
+
"Content-Type": "application/json"
|
|
334
|
+
};
|
|
335
|
+
if (options == null ? void 0 : options.headers) {
|
|
336
|
+
if (options.headers instanceof Headers) {
|
|
337
|
+
options.headers.forEach((value, key) => {
|
|
338
|
+
headers[key] = value;
|
|
339
|
+
});
|
|
340
|
+
} else if (Array.isArray(options.headers)) {
|
|
341
|
+
options.headers.forEach(([key, value]) => {
|
|
342
|
+
headers[key] = value;
|
|
343
|
+
});
|
|
344
|
+
} else {
|
|
345
|
+
Object.assign(headers, options.headers);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (this._publicApiKey) {
|
|
349
|
+
headers["x-api-key"] = this._publicApiKey;
|
|
350
|
+
}
|
|
351
|
+
const fetchOptions = {
|
|
352
|
+
method: "POST",
|
|
353
|
+
...options,
|
|
354
|
+
headers,
|
|
355
|
+
body: payload ? JSON.stringify(payload) : options == null ? void 0 : options.body
|
|
356
|
+
};
|
|
357
|
+
const errors = [];
|
|
358
|
+
for (const baseUrl of this._horizonUrls) {
|
|
359
|
+
try {
|
|
360
|
+
const url = `${baseUrl.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
361
|
+
const response = await fetch(url, fetchOptions);
|
|
362
|
+
if (!response.ok) {
|
|
363
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
364
|
+
}
|
|
365
|
+
const data = await response.json();
|
|
366
|
+
return data;
|
|
367
|
+
} catch (error) {
|
|
368
|
+
const fetchError = error instanceof Error ? error : new Error("Unknown fetch error");
|
|
369
|
+
errors.push(fetchError);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
throw new Error(`All horizon URLs failed. Last errors: ${errors.map((e) => e.message).join(", ")}`);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Validates and sets the public API key. This is used internally
|
|
376
|
+
*
|
|
377
|
+
* @param key - The public API key string or undefined to clear
|
|
378
|
+
* @throws {Error} If the key doesn't start with "public_"
|
|
379
|
+
*/
|
|
380
|
+
setPublicKey(key) {
|
|
381
|
+
if (key !== void 0 && !key.startsWith("public_")) {
|
|
382
|
+
throw new Error("Public API key must start with 'public_'");
|
|
383
|
+
}
|
|
384
|
+
this._publicApiKey = key;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Extracts the organization ID from a public API key.
|
|
388
|
+
*
|
|
389
|
+
* The public key format is: `public_<base64-encoded-data>`
|
|
390
|
+
* The base64 data contains: `orgId:secretData`
|
|
391
|
+
* Only alphanumeric characters, underscores, and hyphens are considered valid in org IDs.
|
|
392
|
+
*
|
|
393
|
+
* @param publicKey - The public API key to extract the organization ID from
|
|
394
|
+
* @returns The organization ID if valid and extractable, undefined otherwise
|
|
395
|
+
*
|
|
396
|
+
* @example
|
|
397
|
+
* ```typescript
|
|
398
|
+
* const toggle = new Toggle();
|
|
399
|
+
* const orgId = toggle.getOrgIdFromPublicKey('public_dGVzdC1vcmc6c2VjcmV0');
|
|
400
|
+
* console.log(orgId); // 'test-org'
|
|
401
|
+
* ```
|
|
402
|
+
*/
|
|
403
|
+
getOrgIdFromPublicKey(publicKey) {
|
|
404
|
+
try {
|
|
405
|
+
const keyWithoutPrefix = publicKey.replace(/^public_/, "");
|
|
406
|
+
const decoded = globalThis.atob ? globalThis.atob(keyWithoutPrefix) : Buffer.from(keyWithoutPrefix, "base64").toString();
|
|
407
|
+
const [orgId] = decoded.split(":");
|
|
408
|
+
const isValidOrgId = /^[a-zA-Z0-9_-]+$/.test(orgId);
|
|
409
|
+
return isValidOrgId ? orgId : void 0;
|
|
410
|
+
} catch (e) {
|
|
411
|
+
return void 0;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Builds the default Horizon API URL for the given public key.
|
|
416
|
+
*
|
|
417
|
+
* If a valid organization ID can be extracted from the public key, returns an
|
|
418
|
+
* organization-specific URL. Otherwise, returns the default fallback URL.
|
|
419
|
+
*
|
|
420
|
+
* @param publicKey - The public API key to build the URL for
|
|
421
|
+
* @returns Organization-specific URL or default fallback URL
|
|
422
|
+
*
|
|
423
|
+
* @example
|
|
424
|
+
* ```typescript
|
|
425
|
+
* const toggle = new Toggle();
|
|
426
|
+
*
|
|
427
|
+
* // With valid org ID
|
|
428
|
+
* const orgUrl = toggle.buildDefaultHorizonUrl('public_dGVzdC1vcmc6c2VjcmV0');
|
|
429
|
+
* console.log(orgUrl); // 'https://test-org.toggle.hyphen.cloud'
|
|
430
|
+
*
|
|
431
|
+
* // With invalid key
|
|
432
|
+
* const defaultUrl = toggle.buildDefaultHorizonUrl('invalid-key');
|
|
433
|
+
* console.log(defaultUrl); // 'https://toggle.hyphen.cloud'
|
|
434
|
+
* ```
|
|
435
|
+
*/
|
|
436
|
+
getDefaultHorizonUrl(publicKey) {
|
|
437
|
+
if (publicKey) {
|
|
438
|
+
const orgId = this.getOrgIdFromPublicKey(publicKey);
|
|
439
|
+
return orgId ? `https://${orgId}.toggle.hyphen.cloud` : "https://toggle.hyphen.cloud";
|
|
440
|
+
}
|
|
441
|
+
return "https://toggle.hyphen.cloud";
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Will get the urls. If you pass in the public key it will provide two urls.
|
|
445
|
+
* @param publicKey
|
|
446
|
+
* @returns
|
|
447
|
+
*/
|
|
448
|
+
getDefaultHorizonUrls(publicKey) {
|
|
449
|
+
let result = [
|
|
450
|
+
this.getDefaultHorizonUrl()
|
|
451
|
+
];
|
|
452
|
+
if (publicKey) {
|
|
453
|
+
const defaultUrl = result[0];
|
|
454
|
+
const orgUrl = this.getDefaultHorizonUrl(publicKey);
|
|
455
|
+
result = [];
|
|
456
|
+
if (orgUrl !== defaultUrl) {
|
|
457
|
+
result.push(orgUrl);
|
|
458
|
+
}
|
|
459
|
+
result.push(defaultUrl);
|
|
460
|
+
}
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Generates a unique targeting key based on available context.
|
|
465
|
+
*
|
|
466
|
+
* @returns A targeting key in the format: `[app]-[env]-[random]` or simplified versions
|
|
467
|
+
*/
|
|
468
|
+
generateTargetKey() {
|
|
469
|
+
const randomSuffix = Math.random().toString(36).substring(7);
|
|
470
|
+
const app = this._applicationId || "";
|
|
471
|
+
const env = this._environment || "";
|
|
472
|
+
const components = [
|
|
473
|
+
app,
|
|
474
|
+
env,
|
|
475
|
+
randomSuffix
|
|
476
|
+
].filter(Boolean);
|
|
477
|
+
return components.join("-");
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Extracts targeting key from a toggle context with fallback logic.
|
|
481
|
+
*
|
|
482
|
+
* @param context - The toggle context to extract targeting key from
|
|
483
|
+
* @returns The targeting key string
|
|
484
|
+
*/
|
|
485
|
+
getTargetingKey(context) {
|
|
486
|
+
if (context.targetingKey) {
|
|
487
|
+
return context.targetingKey;
|
|
488
|
+
}
|
|
489
|
+
if (context.user) {
|
|
490
|
+
return context.user.id;
|
|
491
|
+
}
|
|
492
|
+
return this._defaultTargetingKey;
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
__name(_Toggle, "Toggle");
|
|
496
|
+
var Toggle = _Toggle;
|
|
3
497
|
|
|
4
498
|
// src/toggle-provider.tsx
|
|
5
499
|
import {
|
|
@@ -7,7 +501,6 @@ import {
|
|
|
7
501
|
useMemo,
|
|
8
502
|
useEffect
|
|
9
503
|
} from "react";
|
|
10
|
-
import { Toggle } from "@hyphen/browser-sdk";
|
|
11
504
|
import { jsx } from "react/jsx-runtime";
|
|
12
505
|
var ToggleContext = createContext(null);
|
|
13
506
|
function ToggleProvider({
|
|
@@ -68,7 +561,7 @@ function withToggleProvider(options) {
|
|
|
68
561
|
};
|
|
69
562
|
}
|
|
70
563
|
export {
|
|
71
|
-
|
|
564
|
+
Toggle,
|
|
72
565
|
ToggleContext,
|
|
73
566
|
ToggleProvider,
|
|
74
567
|
useToggle,
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyphen/react-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Hyphen SDK for React",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
6
7
|
"module": "dist/index.js",
|
|
7
8
|
"exports": {
|
|
8
9
|
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
9
11
|
"import": "./dist/index.js",
|
|
10
|
-
"
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
11
13
|
}
|
|
12
14
|
},
|
|
13
15
|
"types": "dist/index.d.ts",
|
|
@@ -22,30 +24,30 @@
|
|
|
22
24
|
"author": "Team Hyphen <hello@hyphen.ai>",
|
|
23
25
|
"license": "MIT",
|
|
24
26
|
"devDependencies": {
|
|
25
|
-
"@biomejs/biome": "2.3.
|
|
26
|
-
"@faker-js/faker": "10.
|
|
27
|
-
"@swc/core": "1.15.
|
|
27
|
+
"@biomejs/biome": "2.3.11",
|
|
28
|
+
"@faker-js/faker": "10.2.0",
|
|
29
|
+
"@swc/core": "1.15.8",
|
|
28
30
|
"@testing-library/react": "16.3.1",
|
|
29
|
-
"@types/node": "25.0.
|
|
30
|
-
"@types/react": "19.2.
|
|
31
|
-
"@vitest/coverage-v8": "4.0.
|
|
31
|
+
"@types/node": "25.0.9",
|
|
32
|
+
"@types/react": "19.2.8",
|
|
33
|
+
"@vitest/coverage-v8": "4.0.17",
|
|
32
34
|
"dotenv": "17.2.3",
|
|
33
|
-
"happy-dom": "20.
|
|
35
|
+
"happy-dom": "20.3.1",
|
|
34
36
|
"react": "19.2.3",
|
|
35
37
|
"react-dom": "19.2.3",
|
|
36
38
|
"rimraf": "6.1.2",
|
|
37
39
|
"tsd": "0.33.0",
|
|
38
40
|
"tsup": "8.5.1",
|
|
39
41
|
"typescript": "5.9.3",
|
|
40
|
-
"vitest": "4.0.
|
|
42
|
+
"vitest": "4.0.17"
|
|
41
43
|
},
|
|
42
44
|
"files": [
|
|
43
45
|
"dist",
|
|
44
46
|
"LICENSE"
|
|
45
47
|
],
|
|
46
48
|
"dependencies": {
|
|
47
|
-
"@hyphen/browser-sdk": "1.0.
|
|
48
|
-
"hookified": "1.
|
|
49
|
+
"@hyphen/browser-sdk": "1.0.6",
|
|
50
|
+
"hookified": "1.15.0"
|
|
49
51
|
},
|
|
50
52
|
"peerDependencies": {
|
|
51
53
|
"react": "^18.0.0 || ^19.0.0"
|