@dinoconfig/js-sdk 1.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/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.md +743 -0
- package/index.d.ts +88 -0
- package/index.js +883 -0
- package/lib/cache/cache-manager.d.ts +86 -0
- package/lib/cache/cache.types.d.ts +105 -0
- package/lib/cache/index.d.ts +8 -0
- package/lib/cache/memory-cache.d.ts +86 -0
- package/lib/cache/storage-cache.d.ts +58 -0
- package/lib/config-api.d.ts +111 -0
- package/lib/dinoconfig-js-sdk.d.ts +140 -0
- package/lib/discovery-api.d.ts +174 -0
- package/lib/http-client.d.ts +245 -0
- package/lib/types.d.ts +364 -0
- package/package.json +77 -0
package/index.js
ADDED
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
class w {
|
|
2
|
+
/**
|
|
3
|
+
* Creates a new HttpClient instance.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} baseUrl - The base URL of the DinoConfig API
|
|
6
|
+
* @param {number} [timeout=10000] - Default request timeout in milliseconds
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const client = new HttpClient('https://api.dinoconfig.com', 15000);
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
constructor(e, t = 1e4) {
|
|
14
|
+
this.baseUrl = e.replace(/\/$/, ""), this.defaultTimeout = t;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Configures authorization by exchanging the API key for an access token.
|
|
18
|
+
*
|
|
19
|
+
* This method:
|
|
20
|
+
* 1. Extracts the API key from the provided headers
|
|
21
|
+
* 2. Exchanges it for a JWT access token
|
|
22
|
+
* 3. Configures the Authorization header for subsequent requests
|
|
23
|
+
*
|
|
24
|
+
* @async
|
|
25
|
+
* @method configureAuthorizationHeader
|
|
26
|
+
* @param {Record<string, string>} headers - Headers containing the X-API-Key
|
|
27
|
+
* @returns {Promise<void>}
|
|
28
|
+
* @throws {Error} If token exchange fails
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* await client.configureAuthorizationHeader({
|
|
33
|
+
* 'X-API-Key': 'dino_your-api-key-here'
|
|
34
|
+
* });
|
|
35
|
+
* // Client is now authenticated
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
async configureAuthorizationHeader(e) {
|
|
39
|
+
const t = e["X-API-Key"], s = await this.exchangeApiKeyForToken(t);
|
|
40
|
+
this.defaultHeaders = {
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
Authorization: `Bearer ${s}`,
|
|
43
|
+
...e
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Exchanges an API key for a JWT access token.
|
|
48
|
+
*
|
|
49
|
+
* Makes a POST request to the token exchange endpoint with the API key
|
|
50
|
+
* and returns the access token from the response.
|
|
51
|
+
*
|
|
52
|
+
* @async
|
|
53
|
+
* @private
|
|
54
|
+
* @method exchangeApiKeyForToken
|
|
55
|
+
* @param {string} apiKey - The API key to exchange
|
|
56
|
+
* @returns {Promise<string>} The JWT access token
|
|
57
|
+
* @throws {Error} If the exchange fails or the API key is invalid
|
|
58
|
+
*
|
|
59
|
+
* @remarks
|
|
60
|
+
* The API key is sent via HTTPS, which encrypts it in transit.
|
|
61
|
+
* The server validates the key and returns a short-lived access token.
|
|
62
|
+
*/
|
|
63
|
+
async exchangeApiKeyForToken(e) {
|
|
64
|
+
try {
|
|
65
|
+
const t = await fetch(`${this.baseUrl}/api/auth/sdk-token/exchange`, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: {
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
"x-api-key": e
|
|
70
|
+
},
|
|
71
|
+
credentials: "include"
|
|
72
|
+
});
|
|
73
|
+
if (!t.ok) {
|
|
74
|
+
const r = await t.text();
|
|
75
|
+
throw new Error(`Failed to exchange API key for token: ${t.status} ${r}`);
|
|
76
|
+
}
|
|
77
|
+
return (await t.json()).access_token;
|
|
78
|
+
} catch (t) {
|
|
79
|
+
const s = t instanceof Error ? t.message : "Unknown error";
|
|
80
|
+
throw new Error(`Failed to authenticate with API key: ${s}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Makes an HTTP request to the API.
|
|
85
|
+
*
|
|
86
|
+
* Handles:
|
|
87
|
+
* - Request formatting and headers
|
|
88
|
+
* - Timeout via AbortController
|
|
89
|
+
* - Response parsing
|
|
90
|
+
* - Error handling
|
|
91
|
+
* - Retry logic with exponential backoff
|
|
92
|
+
*
|
|
93
|
+
* @async
|
|
94
|
+
* @private
|
|
95
|
+
* @method request
|
|
96
|
+
* @typeParam T - The expected response data type
|
|
97
|
+
* @param {string} method - HTTP method (GET, POST, PUT, PATCH, DELETE)
|
|
98
|
+
* @param {string} endpoint - API endpoint path (e.g., '/api/configs')
|
|
99
|
+
* @param {any} [data] - Request body data (for POST, PUT, PATCH)
|
|
100
|
+
* @param {RequestOptions} [options={}] - Request customization options
|
|
101
|
+
* @returns {Promise<ApiResponse<T>>} The API response
|
|
102
|
+
* @throws {ApiError} If the request fails after all retries
|
|
103
|
+
*/
|
|
104
|
+
async request(e, t, s, r = {}) {
|
|
105
|
+
const a = `${this.baseUrl}${t}`, i = r.timeout || this.defaultTimeout, c = r.retries || 0;
|
|
106
|
+
let h = null;
|
|
107
|
+
for (let l = 0; l <= c; l++)
|
|
108
|
+
try {
|
|
109
|
+
const n = new AbortController(), o = setTimeout(() => n.abort(), i), d = {
|
|
110
|
+
method: e,
|
|
111
|
+
headers: this.defaultHeaders,
|
|
112
|
+
signal: n.signal
|
|
113
|
+
};
|
|
114
|
+
s && (e === "POST" || e === "PUT" || e === "PATCH") && (d.body = JSON.stringify(s));
|
|
115
|
+
const f = await fetch(a, d);
|
|
116
|
+
clearTimeout(o);
|
|
117
|
+
const p = await f.json();
|
|
118
|
+
if (!f.ok)
|
|
119
|
+
throw {
|
|
120
|
+
message: p.message || f.statusText,
|
|
121
|
+
status: f.status,
|
|
122
|
+
code: p.code
|
|
123
|
+
};
|
|
124
|
+
return {
|
|
125
|
+
data: p,
|
|
126
|
+
success: !0
|
|
127
|
+
};
|
|
128
|
+
} catch (n) {
|
|
129
|
+
if (h = n, n instanceof Error && "status" in n) {
|
|
130
|
+
const o = n;
|
|
131
|
+
if (o.status >= 400 && o.status < 500)
|
|
132
|
+
throw n;
|
|
133
|
+
}
|
|
134
|
+
if (l === c)
|
|
135
|
+
throw n;
|
|
136
|
+
await new Promise((o) => setTimeout(o, Math.pow(2, l) * 1e3));
|
|
137
|
+
}
|
|
138
|
+
throw h || new Error("Request failed after all retries");
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Makes a GET request to the specified endpoint.
|
|
142
|
+
*
|
|
143
|
+
* @async
|
|
144
|
+
* @method get
|
|
145
|
+
* @typeParam T - The expected response data type
|
|
146
|
+
* @param {string} endpoint - The API endpoint path
|
|
147
|
+
* @param {RequestOptions} [options] - Optional request configuration
|
|
148
|
+
* @returns {Promise<ApiResponse<T>>} The API response
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```typescript
|
|
152
|
+
* const response = await client.get<Config>('/api/configs/123');
|
|
153
|
+
* console.log(response.data.name);
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
async get(e, t) {
|
|
157
|
+
return this.request("GET", e, void 0, t);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Makes a POST request to the specified endpoint.
|
|
161
|
+
*
|
|
162
|
+
* @async
|
|
163
|
+
* @method post
|
|
164
|
+
* @typeParam T - The expected response data type
|
|
165
|
+
* @param {string} endpoint - The API endpoint path
|
|
166
|
+
* @param {any} [data] - The request body data
|
|
167
|
+
* @param {RequestOptions} [options] - Optional request configuration
|
|
168
|
+
* @returns {Promise<ApiResponse<T>>} The API response
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```typescript
|
|
172
|
+
* const response = await client.post<Config>('/api/configs', {
|
|
173
|
+
* name: 'NewConfig',
|
|
174
|
+
* formData: { key: 'value' }
|
|
175
|
+
* });
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
async post(e, t, s) {
|
|
179
|
+
return this.request("POST", e, t, s);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Makes a PUT request to the specified endpoint.
|
|
183
|
+
*
|
|
184
|
+
* @async
|
|
185
|
+
* @method put
|
|
186
|
+
* @typeParam T - The expected response data type
|
|
187
|
+
* @param {string} endpoint - The API endpoint path
|
|
188
|
+
* @param {any} [data] - The request body data
|
|
189
|
+
* @param {RequestOptions} [options] - Optional request configuration
|
|
190
|
+
* @returns {Promise<ApiResponse<T>>} The API response
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```typescript
|
|
194
|
+
* const response = await client.put<Config>('/api/configs/123', {
|
|
195
|
+
* name: 'UpdatedConfig'
|
|
196
|
+
* });
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
async put(e, t, s) {
|
|
200
|
+
return this.request("PUT", e, t, s);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Makes a PATCH request to the specified endpoint.
|
|
204
|
+
*
|
|
205
|
+
* @async
|
|
206
|
+
* @method patch
|
|
207
|
+
* @typeParam T - The expected response data type
|
|
208
|
+
* @param {string} endpoint - The API endpoint path
|
|
209
|
+
* @param {any} [data] - The request body data (partial update)
|
|
210
|
+
* @param {RequestOptions} [options] - Optional request configuration
|
|
211
|
+
* @returns {Promise<ApiResponse<T>>} The API response
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```typescript
|
|
215
|
+
* const response = await client.patch<Config>('/api/configs/123', {
|
|
216
|
+
* formData: { updatedKey: 'newValue' }
|
|
217
|
+
* });
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
async patch(e, t, s) {
|
|
221
|
+
return this.request("PATCH", e, t, s);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Makes a DELETE request to the specified endpoint.
|
|
225
|
+
*
|
|
226
|
+
* @async
|
|
227
|
+
* @method delete
|
|
228
|
+
* @typeParam T - The expected response data type
|
|
229
|
+
* @param {string} endpoint - The API endpoint path
|
|
230
|
+
* @param {RequestOptions} [options] - Optional request configuration
|
|
231
|
+
* @returns {Promise<ApiResponse<T>>} The API response
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```typescript
|
|
235
|
+
* const response = await client.delete('/api/configs/123');
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
async delete(e, t) {
|
|
239
|
+
return this.request("DELETE", e, void 0, t);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Updates the authorization token.
|
|
243
|
+
*
|
|
244
|
+
* Use this method if you need to manually update the token
|
|
245
|
+
* (e.g., after refreshing it externally).
|
|
246
|
+
*
|
|
247
|
+
* @method setToken
|
|
248
|
+
* @param {string} token - The new JWT access token
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```typescript
|
|
252
|
+
* client.setToken('new-jwt-token');
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
setToken(e) {
|
|
256
|
+
this.defaultHeaders.Authorization = `Bearer ${e}`;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Sets a custom header for all subsequent requests.
|
|
260
|
+
*
|
|
261
|
+
* @method setHeader
|
|
262
|
+
* @param {string} key - The header name
|
|
263
|
+
* @param {string} value - The header value
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* ```typescript
|
|
267
|
+
* client.setHeader('X-Custom-Header', 'custom-value');
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
setHeader(e, t) {
|
|
271
|
+
this.defaultHeaders[e] = t;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Removes a custom header from subsequent requests.
|
|
275
|
+
*
|
|
276
|
+
* @method removeHeader
|
|
277
|
+
* @param {string} key - The header name to remove
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* ```typescript
|
|
281
|
+
* client.removeHeader('X-Custom-Header');
|
|
282
|
+
* ```
|
|
283
|
+
*/
|
|
284
|
+
removeHeader(e) {
|
|
285
|
+
delete this.defaultHeaders[e];
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const y = "/api/sdk/brands";
|
|
289
|
+
class C {
|
|
290
|
+
constructor(e, t) {
|
|
291
|
+
this.httpClient = e, this.cacheManager = t;
|
|
292
|
+
}
|
|
293
|
+
async get(e, t, s) {
|
|
294
|
+
const { brand: r, config: a, requestOptions: i } = this.parseConfigArgs(
|
|
295
|
+
e,
|
|
296
|
+
t,
|
|
297
|
+
s
|
|
298
|
+
), c = `config:${r}:${a}`, h = this.cacheManager && i?.cache !== !1 && !i?.forceRefresh;
|
|
299
|
+
if (h) {
|
|
300
|
+
const o = await this.cacheManager.get(c);
|
|
301
|
+
if (o !== null)
|
|
302
|
+
return o;
|
|
303
|
+
}
|
|
304
|
+
const l = await this.httpClient.get(
|
|
305
|
+
this.buildConfigUrl(r, a),
|
|
306
|
+
i
|
|
307
|
+
), n = {
|
|
308
|
+
...l,
|
|
309
|
+
data: this.transformConfigResponse(l.data)
|
|
310
|
+
};
|
|
311
|
+
return h && l.success && await this.cacheManager.set(c, n, { ttl: i?.ttl }), n;
|
|
312
|
+
}
|
|
313
|
+
async getValue(e, t, s, r) {
|
|
314
|
+
const { brand: a, config: i, key: c, requestOptions: h } = this.parseValueArgs(
|
|
315
|
+
e,
|
|
316
|
+
t,
|
|
317
|
+
s,
|
|
318
|
+
r
|
|
319
|
+
), l = `config:${a}:${i}:${c}`, n = this.cacheManager && h?.cache !== !1 && !h?.forceRefresh;
|
|
320
|
+
if (n) {
|
|
321
|
+
const d = await this.cacheManager.get(l);
|
|
322
|
+
if (d !== null)
|
|
323
|
+
return d;
|
|
324
|
+
}
|
|
325
|
+
const o = await this.httpClient.get(
|
|
326
|
+
this.buildValueUrl(a, i, c),
|
|
327
|
+
h
|
|
328
|
+
);
|
|
329
|
+
return n && o.success && await this.cacheManager.set(l, o, { ttl: h?.ttl }), o;
|
|
330
|
+
}
|
|
331
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
332
|
+
// Private helpers
|
|
333
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
334
|
+
/**
|
|
335
|
+
* Parses arguments for the get() method.
|
|
336
|
+
*/
|
|
337
|
+
parseConfigArgs(e, t, s) {
|
|
338
|
+
if (typeof t == "string")
|
|
339
|
+
return {
|
|
340
|
+
brand: e,
|
|
341
|
+
config: t,
|
|
342
|
+
requestOptions: s
|
|
343
|
+
};
|
|
344
|
+
const r = this.parsePath(e, 2);
|
|
345
|
+
return {
|
|
346
|
+
brand: r[0],
|
|
347
|
+
config: r[1],
|
|
348
|
+
requestOptions: t
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Parses arguments for the getValue() method.
|
|
353
|
+
*/
|
|
354
|
+
parseValueArgs(e, t, s, r) {
|
|
355
|
+
if (typeof t == "string")
|
|
356
|
+
return {
|
|
357
|
+
brand: e,
|
|
358
|
+
config: t,
|
|
359
|
+
key: s,
|
|
360
|
+
requestOptions: r
|
|
361
|
+
};
|
|
362
|
+
const a = this.parsePath(e, 3);
|
|
363
|
+
return {
|
|
364
|
+
brand: a[0],
|
|
365
|
+
config: a[1],
|
|
366
|
+
key: a[2],
|
|
367
|
+
requestOptions: t
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Parses a dot-notation path into components.
|
|
372
|
+
*/
|
|
373
|
+
parsePath(e, t) {
|
|
374
|
+
const s = e.split(".");
|
|
375
|
+
if (s.length !== t) {
|
|
376
|
+
const r = t === 2 ? "brandName.configName" : "brandName.configName.keyName";
|
|
377
|
+
throw new Error(`Invalid path format "${e}". Expected "${r}"`);
|
|
378
|
+
}
|
|
379
|
+
return s;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Builds the URL for fetching an entire config.
|
|
383
|
+
*/
|
|
384
|
+
buildConfigUrl(e, t) {
|
|
385
|
+
return `${y}/${this.encode(e)}/configs/${this.encode(t)}`;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Builds the URL for fetching a single value.
|
|
389
|
+
*/
|
|
390
|
+
buildValueUrl(e, t, s) {
|
|
391
|
+
return `${y}/${this.encode(e)}/configs/${this.encode(t)}/${this.encode(s)}`;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* URL-encodes a path segment.
|
|
395
|
+
*/
|
|
396
|
+
encode(e) {
|
|
397
|
+
return encodeURIComponent(e);
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Transforms the backend response to the public ConfigData shape.
|
|
401
|
+
*/
|
|
402
|
+
transformConfigResponse(e) {
|
|
403
|
+
return {
|
|
404
|
+
name: e.name,
|
|
405
|
+
description: e.description,
|
|
406
|
+
values: e.formData,
|
|
407
|
+
version: e.version,
|
|
408
|
+
keys: e.keys,
|
|
409
|
+
createdAt: e.createdAt,
|
|
410
|
+
updatedAt: e.updatedAt
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const g = "/api/sdk";
|
|
415
|
+
class x {
|
|
416
|
+
constructor(e) {
|
|
417
|
+
this.httpClient = e;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Lists all brands accessible by the current API key.
|
|
421
|
+
*
|
|
422
|
+
* @example
|
|
423
|
+
* ```typescript
|
|
424
|
+
* const response = await dinoconfig.discovery.listBrands();
|
|
425
|
+
* response.data.forEach(brand => {
|
|
426
|
+
* console.log(`${brand.name}: ${brand.configCount} configs`);
|
|
427
|
+
* });
|
|
428
|
+
* ```
|
|
429
|
+
*/
|
|
430
|
+
async listBrands(e) {
|
|
431
|
+
const t = await this.httpClient.get(
|
|
432
|
+
`${g}/brands`,
|
|
433
|
+
e
|
|
434
|
+
);
|
|
435
|
+
return this.extractData(t, t.data.brands);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Lists all configurations for a specific brand.
|
|
439
|
+
*
|
|
440
|
+
* @example
|
|
441
|
+
* ```typescript
|
|
442
|
+
* const response = await dinoconfig.discovery.listConfigs('MyBrand');
|
|
443
|
+
* response.data.forEach(config => {
|
|
444
|
+
* console.log(`${config.name}: ${config.keys.length} keys`);
|
|
445
|
+
* });
|
|
446
|
+
* ```
|
|
447
|
+
*/
|
|
448
|
+
async listConfigs(e, t) {
|
|
449
|
+
const s = await this.httpClient.get(
|
|
450
|
+
this.buildBrandUrl(e, "/configs"),
|
|
451
|
+
t
|
|
452
|
+
);
|
|
453
|
+
return this.extractData(s, s.data.configs);
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Gets the schema/structure for a specific configuration.
|
|
457
|
+
*
|
|
458
|
+
* @example
|
|
459
|
+
* ```typescript
|
|
460
|
+
* const response = await dinoconfig.discovery.getSchema('MyBrand', 'FeatureFlags');
|
|
461
|
+
* Object.entries(response.data.fields).forEach(([name, field]) => {
|
|
462
|
+
* console.log(`${name}: ${field.type}`);
|
|
463
|
+
* });
|
|
464
|
+
* ```
|
|
465
|
+
*/
|
|
466
|
+
async getSchema(e, t, s) {
|
|
467
|
+
return this.httpClient.get(
|
|
468
|
+
this.buildConfigUrl(e, t, "/schema"),
|
|
469
|
+
s
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Performs full introspection, returning all brands, configs, and keys.
|
|
474
|
+
*
|
|
475
|
+
* @example
|
|
476
|
+
* ```typescript
|
|
477
|
+
* const response = await dinoconfig.discovery.introspect();
|
|
478
|
+
* response.data.brands.forEach(brand => {
|
|
479
|
+
* console.log(`Brand: ${brand.name}`);
|
|
480
|
+
* brand.configs.forEach(config => {
|
|
481
|
+
* console.log(` Config: ${config.name} (${config.keys.length} keys)`);
|
|
482
|
+
* });
|
|
483
|
+
* });
|
|
484
|
+
* ```
|
|
485
|
+
*/
|
|
486
|
+
async introspect(e) {
|
|
487
|
+
return this.httpClient.get(`${g}/introspect`, e);
|
|
488
|
+
}
|
|
489
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
490
|
+
// Private helpers
|
|
491
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
492
|
+
/**
|
|
493
|
+
* Builds URL path for brand-level endpoints.
|
|
494
|
+
*/
|
|
495
|
+
buildBrandUrl(e, t = "") {
|
|
496
|
+
return `${g}/brands/${this.encode(e)}${t}`;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Builds URL path for config-level endpoints.
|
|
500
|
+
*/
|
|
501
|
+
buildConfigUrl(e, t, s = "") {
|
|
502
|
+
return `${g}/brands/${this.encode(e)}/configs/${this.encode(t)}${s}`;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* URL-encodes a path segment.
|
|
506
|
+
*/
|
|
507
|
+
encode(e) {
|
|
508
|
+
return encodeURIComponent(e);
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Extracts and transforms response data.
|
|
512
|
+
*/
|
|
513
|
+
extractData(e, t) {
|
|
514
|
+
return {
|
|
515
|
+
...e,
|
|
516
|
+
data: t
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
class m {
|
|
521
|
+
constructor(e, t) {
|
|
522
|
+
this.ttl = e, this.maxSize = t, this.cache = /* @__PURE__ */ new Map(), this.hits = 0, this.misses = 0;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Get a value from the cache.
|
|
526
|
+
*
|
|
527
|
+
* @param {string} key - Cache key
|
|
528
|
+
* @returns {T | null} Cached value or null if not found/expired
|
|
529
|
+
*/
|
|
530
|
+
get(e) {
|
|
531
|
+
const t = this.cache.get(e);
|
|
532
|
+
return t ? this.isExpired(t) ? (this.cache.delete(e), this.misses++, null) : (this.hits++, t.value) : (this.misses++, null);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Set a value in the cache.
|
|
536
|
+
*
|
|
537
|
+
* @param {string} key - Cache key
|
|
538
|
+
* @param {T} value - Value to cache
|
|
539
|
+
* @param {CacheOptions} options - Optional cache options
|
|
540
|
+
*/
|
|
541
|
+
set(e, t, s) {
|
|
542
|
+
const r = s?.ttl ?? this.ttl, a = {
|
|
543
|
+
value: t,
|
|
544
|
+
timestamp: Date.now(),
|
|
545
|
+
expiresAt: Date.now() + r
|
|
546
|
+
};
|
|
547
|
+
this.cache.size >= this.maxSize && !this.cache.has(e) && this.evictOldest(), this.cache.set(e, a);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Delete a value from the cache.
|
|
551
|
+
*
|
|
552
|
+
* @param {string} key - Cache key
|
|
553
|
+
*/
|
|
554
|
+
delete(e) {
|
|
555
|
+
this.cache.delete(e);
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Clear all entries from the cache.
|
|
559
|
+
*/
|
|
560
|
+
clear() {
|
|
561
|
+
this.cache.clear(), this.hits = 0, this.misses = 0;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Invalidate entries matching a pattern.
|
|
565
|
+
*
|
|
566
|
+
* @param {string} pattern - Regex pattern to match keys
|
|
567
|
+
*/
|
|
568
|
+
invalidate(e) {
|
|
569
|
+
const t = new RegExp(e);
|
|
570
|
+
for (const s of this.cache.keys())
|
|
571
|
+
t.test(s) && this.cache.delete(s);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Check if an entry exists and is not expired.
|
|
575
|
+
*
|
|
576
|
+
* @param {string} key - Cache key
|
|
577
|
+
* @returns {boolean} True if entry exists and is valid
|
|
578
|
+
*/
|
|
579
|
+
has(e) {
|
|
580
|
+
const t = this.cache.get(e);
|
|
581
|
+
return t ? this.isExpired(t) ? (this.cache.delete(e), !1) : !0 : !1;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Get cache statistics.
|
|
585
|
+
*
|
|
586
|
+
* @returns {CacheStats} Cache statistics
|
|
587
|
+
*/
|
|
588
|
+
getStats() {
|
|
589
|
+
const e = this.hits + this.misses;
|
|
590
|
+
return {
|
|
591
|
+
hits: this.hits,
|
|
592
|
+
misses: this.misses,
|
|
593
|
+
size: this.cache.size,
|
|
594
|
+
hitRate: e > 0 ? this.hits / e : 0
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Check if an entry is expired.
|
|
599
|
+
*
|
|
600
|
+
* @private
|
|
601
|
+
* @param {CacheEntry} entry - Cache entry
|
|
602
|
+
* @returns {boolean} True if expired
|
|
603
|
+
*/
|
|
604
|
+
isExpired(e) {
|
|
605
|
+
return Date.now() > e.expiresAt;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Evict the oldest entry from the cache.
|
|
609
|
+
* Uses LRU-like strategy based on expiration time.
|
|
610
|
+
*
|
|
611
|
+
* @private
|
|
612
|
+
*/
|
|
613
|
+
evictOldest() {
|
|
614
|
+
let e = null, t = 1 / 0;
|
|
615
|
+
for (const [s, r] of this.cache.entries())
|
|
616
|
+
r.expiresAt < t && (t = r.expiresAt, e = s);
|
|
617
|
+
e && this.cache.delete(e);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
class A {
|
|
621
|
+
constructor() {
|
|
622
|
+
if (typeof window > "u" || !window.localStorage)
|
|
623
|
+
throw new Error("localStorage is not available in this environment");
|
|
624
|
+
this.storage = window.localStorage;
|
|
625
|
+
}
|
|
626
|
+
getItem(e) {
|
|
627
|
+
try {
|
|
628
|
+
return this.storage.getItem(e);
|
|
629
|
+
} catch {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
setItem(e, t) {
|
|
634
|
+
try {
|
|
635
|
+
this.storage.setItem(e, t);
|
|
636
|
+
} catch {
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
removeItem(e) {
|
|
640
|
+
try {
|
|
641
|
+
this.storage.removeItem(e);
|
|
642
|
+
} catch {
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
clear() {
|
|
646
|
+
try {
|
|
647
|
+
const e = Object.keys(this.storage);
|
|
648
|
+
for (const t of e)
|
|
649
|
+
t.startsWith("dinoconfig:") && this.storage.removeItem(t);
|
|
650
|
+
} catch {
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
class b {
|
|
655
|
+
constructor(e) {
|
|
656
|
+
if (this.prefix = "dinoconfig:", e === "localStorage")
|
|
657
|
+
this.adapter = new A();
|
|
658
|
+
else
|
|
659
|
+
throw new Error("IndexedDB storage is not yet implemented");
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Get a value from storage cache.
|
|
663
|
+
*
|
|
664
|
+
* @param {string} key - Cache key
|
|
665
|
+
* @returns {Promise<T | null>} Cached value or null if not found/expired
|
|
666
|
+
*/
|
|
667
|
+
async get(e) {
|
|
668
|
+
try {
|
|
669
|
+
const t = this.adapter.getItem(this.prefix + e);
|
|
670
|
+
if (!t)
|
|
671
|
+
return null;
|
|
672
|
+
const s = JSON.parse(t);
|
|
673
|
+
return this.isExpired(s) ? (this.adapter.removeItem(this.prefix + e), null) : s.value;
|
|
674
|
+
} catch {
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Set a value in storage cache.
|
|
680
|
+
*
|
|
681
|
+
* @param {string} key - Cache key
|
|
682
|
+
* @param {T} value - Value to cache
|
|
683
|
+
* @param {CacheOptions} options - Optional cache options
|
|
684
|
+
*/
|
|
685
|
+
async set(e, t, s) {
|
|
686
|
+
try {
|
|
687
|
+
const r = s?.ttl ?? 3e5, a = {
|
|
688
|
+
value: t,
|
|
689
|
+
timestamp: Date.now(),
|
|
690
|
+
expiresAt: Date.now() + r
|
|
691
|
+
};
|
|
692
|
+
this.adapter.setItem(this.prefix + e, JSON.stringify(a));
|
|
693
|
+
} catch {
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Delete a value from storage cache.
|
|
698
|
+
*
|
|
699
|
+
* @param {string} key - Cache key
|
|
700
|
+
*/
|
|
701
|
+
async delete(e) {
|
|
702
|
+
try {
|
|
703
|
+
this.adapter.removeItem(this.prefix + e);
|
|
704
|
+
} catch {
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Clear all entries from storage cache.
|
|
709
|
+
*/
|
|
710
|
+
async clear() {
|
|
711
|
+
try {
|
|
712
|
+
this.adapter.clear();
|
|
713
|
+
} catch {
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Invalidate entries matching a pattern.
|
|
718
|
+
*
|
|
719
|
+
* @param {string} pattern - Regex pattern to match keys
|
|
720
|
+
*/
|
|
721
|
+
async invalidate(e) {
|
|
722
|
+
try {
|
|
723
|
+
const t = new RegExp(e), r = this.adapter.storage;
|
|
724
|
+
if (!r)
|
|
725
|
+
return;
|
|
726
|
+
const a = [];
|
|
727
|
+
for (let i = 0; i < r.length; i++) {
|
|
728
|
+
const c = r.key(i);
|
|
729
|
+
if (c && c.startsWith(this.prefix)) {
|
|
730
|
+
const h = c.substring(this.prefix.length);
|
|
731
|
+
t.test(h) && a.push(c);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
for (const i of a)
|
|
735
|
+
this.adapter.removeItem(i);
|
|
736
|
+
} catch {
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Check if an entry is expired.
|
|
741
|
+
*
|
|
742
|
+
* @private
|
|
743
|
+
* @param {CacheEntry} entry - Cache entry
|
|
744
|
+
* @returns {boolean} True if expired
|
|
745
|
+
*/
|
|
746
|
+
isExpired(e) {
|
|
747
|
+
return Date.now() > e.expiresAt;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
class $ {
|
|
751
|
+
constructor(e) {
|
|
752
|
+
if (this.config = e, !e.enabled) {
|
|
753
|
+
this.memoryCache = new m(0, 0);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if (this.memoryCache = new m(
|
|
757
|
+
e.ttl,
|
|
758
|
+
e.maxSize
|
|
759
|
+
), e.storage && e.storage !== "memory")
|
|
760
|
+
try {
|
|
761
|
+
this.storageCache = new b(e.storage);
|
|
762
|
+
} catch {
|
|
763
|
+
this.storageCache = void 0;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Get a value from cache.
|
|
768
|
+
* Checks L1 (memory) first, then L2 (storage) if available.
|
|
769
|
+
*
|
|
770
|
+
* @param {string} key - Cache key
|
|
771
|
+
* @returns {Promise<T | null>} Cached value or null if not found
|
|
772
|
+
*/
|
|
773
|
+
async get(e) {
|
|
774
|
+
if (!this.config.enabled)
|
|
775
|
+
return null;
|
|
776
|
+
const t = this.memoryCache.get(e);
|
|
777
|
+
if (t !== null)
|
|
778
|
+
return t;
|
|
779
|
+
if (this.storageCache) {
|
|
780
|
+
const s = await this.storageCache.get(e);
|
|
781
|
+
if (s !== null)
|
|
782
|
+
return this.memoryCache.set(e, s), s;
|
|
783
|
+
}
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Set a value in cache.
|
|
788
|
+
* Writes to both L1 (memory) and L2 (storage) if available.
|
|
789
|
+
*
|
|
790
|
+
* @param {string} key - Cache key
|
|
791
|
+
* @param {T} value - Value to cache
|
|
792
|
+
* @param {CacheOptions} options - Optional cache options
|
|
793
|
+
*/
|
|
794
|
+
async set(e, t, s) {
|
|
795
|
+
this.config.enabled && (this.memoryCache.set(e, t, s), this.storageCache && await this.storageCache.set(e, t, s));
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Delete a value from cache.
|
|
799
|
+
* Removes from both L1 and L2.
|
|
800
|
+
*
|
|
801
|
+
* @param {string} key - Cache key
|
|
802
|
+
*/
|
|
803
|
+
async delete(e) {
|
|
804
|
+
this.memoryCache.delete(e), this.storageCache && await this.storageCache.delete(e);
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Clear all cache entries.
|
|
808
|
+
*/
|
|
809
|
+
async clear() {
|
|
810
|
+
this.memoryCache.clear(), this.storageCache && await this.storageCache.clear();
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Invalidate entries matching a pattern.
|
|
814
|
+
*
|
|
815
|
+
* @param {string} pattern - Regex pattern to match keys
|
|
816
|
+
*/
|
|
817
|
+
async invalidate(e) {
|
|
818
|
+
if (!e) {
|
|
819
|
+
await this.clear();
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
this.memoryCache.invalidate(e), this.storageCache && await this.storageCache.invalidate(e);
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Check if a key exists in cache.
|
|
826
|
+
*
|
|
827
|
+
* @param {string} key - Cache key
|
|
828
|
+
* @returns {boolean} True if key exists and is not expired
|
|
829
|
+
*/
|
|
830
|
+
has(e) {
|
|
831
|
+
return this.config.enabled ? this.memoryCache.has(e) : !1;
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Get cache statistics.
|
|
835
|
+
*
|
|
836
|
+
* @returns {CacheStats} Cache statistics
|
|
837
|
+
*/
|
|
838
|
+
getStats() {
|
|
839
|
+
return this.memoryCache.getStats();
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Prefetch a value into cache.
|
|
843
|
+
* This is a convenience method for warming the cache.
|
|
844
|
+
*
|
|
845
|
+
* @param {string} key - Cache key
|
|
846
|
+
* @param {() => Promise<T>} fetcher - Function to fetch the value if not cached
|
|
847
|
+
*/
|
|
848
|
+
async prefetch(e, t) {
|
|
849
|
+
const s = await this.get(e);
|
|
850
|
+
if (s !== null)
|
|
851
|
+
return s;
|
|
852
|
+
const r = await t();
|
|
853
|
+
return await this.set(e, r), r;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
async function I(u) {
|
|
857
|
+
const {
|
|
858
|
+
apiKey: e,
|
|
859
|
+
baseUrl: t = "http://localhost:3000",
|
|
860
|
+
timeout: s = 1e4,
|
|
861
|
+
cache: r
|
|
862
|
+
} = u, a = new w(t, s);
|
|
863
|
+
await a.configureAuthorizationHeader({
|
|
864
|
+
"X-API-Key": e
|
|
865
|
+
});
|
|
866
|
+
const i = new $({
|
|
867
|
+
enabled: r?.enabled ?? !1,
|
|
868
|
+
ttl: r?.ttl ?? 6e4,
|
|
869
|
+
maxSize: r?.maxSize ?? 1e3,
|
|
870
|
+
storage: r?.storage,
|
|
871
|
+
staleWhileRevalidate: r?.staleWhileRevalidate ?? !1
|
|
872
|
+
});
|
|
873
|
+
return {
|
|
874
|
+
configs: new C(a, i),
|
|
875
|
+
discovery: new x(a),
|
|
876
|
+
cache: i
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
export {
|
|
880
|
+
C as ConfigAPI,
|
|
881
|
+
x as DiscoveryAPI,
|
|
882
|
+
I as dinoconfigApi
|
|
883
|
+
};
|