@jewel998/config 0.2.6 → 0.2.8

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.
@@ -32,7 +32,7 @@ export const buildConfigClient = (internals) => {
32
32
  batchFetchTriggered = true;
33
33
  void (async () => {
34
34
  try {
35
- const result = await withRetry(() => fetcher.fetchAll(), retry);
35
+ const { data: result } = await withRetry(() => fetcher.fetchAll(), retry);
36
36
  cache.set("__all__", result, DEFAULT_CACHE_TTL);
37
37
  for (const [key, value] of Object.entries(result)) {
38
38
  cache.set(key, value, DEFAULT_CACHE_TTL);
@@ -58,7 +58,7 @@ export const buildConfigClient = (internals) => {
58
58
  return;
59
59
  void (async () => {
60
60
  try {
61
- const result = await fetcher.fetchKeys([key]);
61
+ const { data: result } = await fetcher.fetchKeys([key]);
62
62
  for (const [k, v] of Object.entries(result)) {
63
63
  cache.set(k, v, DEFAULT_CACHE_TTL);
64
64
  data[k] = v;
@@ -59,7 +59,11 @@ export class RefreshManager {
59
59
  catch {
60
60
  // If version check fails, proceed with full fetch as fallback
61
61
  }
62
- const result = await withRetry(() => fetcher.fetchAll(), retry);
62
+ const { data: result, version: responseVersion } = await withRetry(() => fetcher.fetchAll(), retry);
63
+ // Sync the version from the config response itself — avoids drifting
64
+ // from the value learned by the earlier checkVersion() probe.
65
+ if (responseVersion !== null)
66
+ this.cachedVersion = responseVersion;
63
67
  cache.set("__all__", result, DEFAULT_CACHE_TTL);
64
68
  for (const [key, value] of Object.entries(result)) {
65
69
  cache.set(key, value, DEFAULT_CACHE_TTL);
@@ -0,0 +1,12 @@
1
+ /** API paths used by the SDK transport. */
2
+ export declare const API_ENDPOINTS: {
3
+ readonly config: "v1/config";
4
+ readonly version: "v1/version";
5
+ };
6
+ /** Shared HTTP values used by browser and server transports. */
7
+ export declare const HTTP_CONSTANTS: {
8
+ readonly jsonContentType: "application/json";
9
+ readonly jsonAccept: "application/json";
10
+ readonly etagHeader: "ETag";
11
+ readonly ifNoneMatchHeader: "If-None-Match";
12
+ };
@@ -0,0 +1,13 @@
1
+ /** API paths used by the SDK transport. */
2
+ export const API_ENDPOINTS = {
3
+ config: "v1/config",
4
+ version: "v1/version",
5
+ };
6
+ /** Shared HTTP values used by browser and server transports. */
7
+ export const HTTP_CONSTANTS = {
8
+ jsonContentType: "application/json",
9
+ jsonAccept: "application/json",
10
+ etagHeader: "ETag",
11
+ ifNoneMatchHeader: "If-None-Match",
12
+ };
13
+ //# sourceMappingURL=constants.js.map
@@ -1,4 +1,4 @@
1
- import type { ConfigFetcher } from "../types.js";
1
+ import type { ConfigFetcher, FetchResult } from "../types.js";
2
2
  export type FetchTier = "prefetch" | "page" | "idle";
3
3
  /**
4
4
  * TierFetcher — wraps a ConfigFetcher with three responsibilities:
@@ -24,12 +24,12 @@ export declare class TierFetcher {
24
24
  * Fetch a specific set of keys under a given tier.
25
25
  * Keys already tracked under any tier are skipped (deduplication).
26
26
  */
27
- fetchTier(keys: string[], tier: FetchTier): Promise<Record<string, unknown>>;
27
+ fetchTier(keys: string[], tier: FetchTier): Promise<FetchResult>;
28
28
  /**
29
29
  * Fetch all remaining keys (idle tier).
30
30
  * Only keys not already tracked are added to the idle set.
31
31
  */
32
- fetchAll(): Promise<Record<string, unknown>>;
32
+ fetchAll(): Promise<FetchResult>;
33
33
  /** Check the remote version (passthrough). */
34
34
  checkVersion(): ReturnType<ConfigFetcher["checkVersion"]>;
35
35
  /**
@@ -37,7 +37,7 @@ export declare class TierFetcher {
37
37
  * Used by doRefresh() — we're intentionally re-fetching already-fetched keys
38
38
  * with a new context, so the allFetched deduplication should be bypassed.
39
39
  */
40
- refetchKeys(keys: string[]): Promise<Record<string, unknown>>;
40
+ refetchKeys(keys: string[]): Promise<FetchResult>;
41
41
  /**
42
42
  * Record keys as fetched under a tier WITHOUT performing a network fetch.
43
43
  * Used when values are hydrated from persistent storage (the boot gate),
@@ -36,9 +36,9 @@ export class TierFetcher {
36
36
  async fetchTier(keys, tier) {
37
37
  const toFetch = keys.filter((k) => !this.allFetched.has(k));
38
38
  if (toFetch.length === 0)
39
- return {};
39
+ return { data: {}, version: null };
40
40
  const result = await this.fetcher.fetchKeys(toFetch);
41
- this.recordKeys(Object.keys(result), tier);
41
+ this.recordKeys(Object.keys(result.data), tier);
42
42
  return result;
43
43
  }
44
44
  /**
@@ -47,7 +47,7 @@ export class TierFetcher {
47
47
  */
48
48
  async fetchAll() {
49
49
  const result = await this.fetcher.fetchAll();
50
- for (const key of Object.keys(result)) {
50
+ for (const key of Object.keys(result.data)) {
51
51
  if (!this.allFetched.has(key)) {
52
52
  this.fetchedKeys.get("idle").add(key);
53
53
  this.allFetched.add(key);
@@ -1,15 +1,16 @@
1
+ import { API_ENDPOINTS } from "../constants.js";
1
2
  export const createBatchFetcher = (transport) => ({
2
3
  async fetchAll() {
3
- const response = await transport.request("v1/config");
4
- return response.data;
4
+ const response = await transport.request(API_ENDPOINTS.config);
5
+ return { data: response.data, version: response.version ?? null };
5
6
  },
6
7
  async fetchKeys(_keys) {
7
8
  // Batch mode always fetches everything
8
- const response = await transport.request("v1/config");
9
- return response.data;
9
+ const response = await transport.request(API_ENDPOINTS.config);
10
+ return { data: response.data, version: response.version ?? null };
10
11
  },
11
12
  async checkVersion() {
12
- const response = await transport.request("v1/version");
13
+ const response = await transport.request(API_ENDPOINTS.version);
13
14
  return {
14
15
  version: String(response.version),
15
16
  changedKeys: response.changedKeys ?? [],
@@ -1,10 +1,11 @@
1
+ import { API_ENDPOINTS } from "../constants.js";
1
2
  export const createProjectedFetcher = (transport) => {
2
3
  let pendingKeys = new Set();
3
4
  let batchPromise = null;
4
5
  return {
5
6
  async fetchAll() {
6
- const response = await transport.request("v1/config");
7
- return response.data;
7
+ const response = await transport.request(API_ENDPOINTS.config);
8
+ return { data: response.data, version: response.version ?? null };
8
9
  },
9
10
  async fetchKeys(keys) {
10
11
  for (const key of keys) {
@@ -17,10 +18,10 @@ export const createProjectedFetcher = (transport) => {
17
18
  pendingKeys = new Set();
18
19
  batchPromise = null;
19
20
  try {
20
- const response = await transport.request("v1/config", {
21
+ const response = await transport.request(API_ENDPOINTS.config, {
21
22
  keys: batchedKeys,
22
23
  });
23
- resolve(response.data);
24
+ resolve({ data: response.data, version: response.version ?? null });
24
25
  }
25
26
  catch (err) {
26
27
  reject(err);
@@ -32,14 +33,14 @@ export const createProjectedFetcher = (transport) => {
32
33
  // Return only the keys this caller requested
33
34
  const filtered = {};
34
35
  for (const key of keys) {
35
- if (key in result) {
36
- filtered[key] = result[key];
36
+ if (key in result.data) {
37
+ filtered[key] = result.data[key];
37
38
  }
38
39
  }
39
- return filtered;
40
+ return { data: filtered, version: result.version };
40
41
  },
41
42
  async checkVersion() {
42
- const response = await transport.request("v1/version");
43
+ const response = await transport.request(API_ENDPOINTS.version);
43
44
  return {
44
45
  version: String(response.version),
45
46
  changedKeys: response.changedKeys ?? [],
@@ -27,11 +27,13 @@
27
27
  // Re-fetches only already-fetched keys in tier order.
28
28
  // ═══════════════════════════════════════════════════════════════
29
29
  import { browserStorage } from "./cache/browserStorage.js";
30
+ import { API_ENDPOINTS } from "./constants.js";
30
31
  import { SdkError, ConfigError } from "./errors/index.js";
31
32
  import { TypedEventEmitter } from "./events/EventEmitter.js";
32
33
  import { createProjectedFetcher } from "./fetch/projectedFetcher.js";
33
34
  import { TierFetcher } from "./fetch/TierFetcher.js";
34
35
  import { withRetry } from "./retry/RetryEngine.js";
36
+ import { warnForFetchFailure, warnIfCspBlocksUrl } from "./transport/browserDiagnostics.js";
35
37
  import { createHttpTransport } from "./transport/HttpTransport.js";
36
38
  import { DEFAULT_CACHE_TTL, DEFAULT_RETRY } from "./types.js";
37
39
  const DEFAULT_BASE_URL = "https://jewel998-config.web.app/api";
@@ -77,6 +79,7 @@ export function initConfig(options) {
77
79
  const defaults = options.defaults ?? {};
78
80
  const pollInterval = options.pollInterval ?? 300_000;
79
81
  const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
82
+ warnIfCspBlocksUrl(baseUrl);
80
83
  const storage = options.storage ?? browserStorage();
81
84
  const retry = { ...DEFAULT_RETRY };
82
85
  const timeoutMs = options.timeout ?? DEFAULT_TIMEOUT_MS;
@@ -130,7 +133,7 @@ export function initConfig(options) {
130
133
  // ── Remote version check (shared by boot gate + polling) ─────
131
134
  async function fetchRemoteVersion() {
132
135
  try {
133
- const res = await fetch(`${baseUrl}/v1/version?clientId=${options.clientId}`, {
136
+ const res = await fetch(`${baseUrl}/${API_ENDPOINTS.version}?clientId=${options.clientId}`, {
134
137
  headers: cachedVersion ? { "If-None-Match": `"${cachedVersion}"` } : {},
135
138
  });
136
139
  // 304 => unchanged from the version we sent in If-None-Match.
@@ -141,7 +144,8 @@ export function initConfig(options) {
141
144
  const { version } = (await res.json());
142
145
  return version ?? null;
143
146
  }
144
- catch {
147
+ catch (error) {
148
+ warnForFetchFailure(`${baseUrl}/${API_ENDPOINTS.version}`, error);
145
149
  return null;
146
150
  }
147
151
  }
@@ -180,12 +184,12 @@ export function initConfig(options) {
180
184
  cachedVersion = remote;
181
185
  }
182
186
  const result = await withRetry(() => tierFetcher.fetchTier(initPrefetch, "prefetch"), retry);
183
- const fetched = applyResult(result);
184
- // Record the version these values correspond to.
185
- const v = await fetchRemoteVersion();
186
- if (v !== null)
187
- persistVersion(v);
188
- tierFetcher.notifyWaiters(result);
187
+ const fetched = applyResult(result.data);
188
+ // Persist the version delivered alongside these values — no extra
189
+ // /version round trip needed; the /config response carries it.
190
+ if (result.version !== null)
191
+ persistVersion(result.version);
192
+ tierFetcher.notifyWaiters(result.data);
189
193
  emitUpdated(fetched, "prefetch");
190
194
  }
191
195
  catch (err) {
@@ -214,11 +218,10 @@ export function initConfig(options) {
214
218
  cachedVersion = remote;
215
219
  }
216
220
  const result = await tierFetcher.fetchAll();
217
- const fetched = applyResult(result);
218
- const v = await fetchRemoteVersion();
219
- if (v !== null)
220
- persistVersion(v);
221
- tierFetcher.notifyWaiters(result);
221
+ const fetched = applyResult(result.data);
222
+ if (result.version !== null)
223
+ persistVersion(result.version);
224
+ tierFetcher.notifyWaiters(result.data);
222
225
  emitUpdated(fetched, "idle");
223
226
  }
224
227
  catch {
@@ -261,18 +264,22 @@ export function initConfig(options) {
261
264
  if (pf.length > 0) {
262
265
  refetches.push(withRetry(() => tierFetcher.refetchKeys(pf), retry)
263
266
  .then((r) => {
264
- applyResult(r);
265
- tierFetcher.notifyWaiters(r);
266
- allFetched.push(...Object.keys(r));
267
+ applyResult(r.data);
268
+ if (r.version !== null)
269
+ persistVersion(r.version);
270
+ tierFetcher.notifyWaiters(r.data);
271
+ allFetched.push(...Object.keys(r.data));
267
272
  })
268
273
  .catch(() => { }));
269
274
  }
270
275
  if (page.length > 0) {
271
276
  refetches.push(withRetry(() => tierFetcher.refetchKeys(page), retry)
272
277
  .then((r) => {
273
- applyResult(r);
274
- tierFetcher.notifyWaiters(r);
275
- allFetched.push(...Object.keys(r));
278
+ applyResult(r.data);
279
+ if (r.version !== null)
280
+ persistVersion(r.version);
281
+ tierFetcher.notifyWaiters(r.data);
282
+ allFetched.push(...Object.keys(r.data));
276
283
  })
277
284
  .catch(() => { }));
278
285
  }
@@ -280,9 +287,11 @@ export function initConfig(options) {
280
287
  refetches.push(tierFetcher
281
288
  .fetchAll()
282
289
  .then((r) => {
283
- applyResult(r);
284
- tierFetcher.notifyWaiters(r);
285
- allFetched.push(...Object.keys(r));
290
+ applyResult(r.data);
291
+ if (r.version !== null)
292
+ persistVersion(r.version);
293
+ tierFetcher.notifyWaiters(r.data);
294
+ allFetched.push(...Object.keys(r.data));
286
295
  })
287
296
  .catch(() => { }));
288
297
  }
@@ -300,8 +309,10 @@ export function initConfig(options) {
300
309
  void (async () => {
301
310
  try {
302
311
  const result = await withRetry(() => tierFetcher.fetchTier(keys, "page"), retry);
303
- const fetched = applyResult(result);
304
- tierFetcher.notifyWaiters(result);
312
+ const fetched = applyResult(result.data);
313
+ if (result.version !== null)
314
+ persistVersion(result.version);
315
+ tierFetcher.notifyWaiters(result.data);
305
316
  emitUpdated(fetched, "page");
306
317
  }
307
318
  catch (err) {
@@ -3,7 +3,7 @@ import { DEFAULT_CACHE_TTL } from "../types.js";
3
3
  const scheduleBackgroundFetch = (ctx) => {
4
4
  const doFetch = async () => {
5
5
  try {
6
- const data = await withRetry(() => ctx.fetcher.fetchAll(), ctx.retry);
6
+ const { data } = await withRetry(() => ctx.fetcher.fetchAll(), ctx.retry);
7
7
  ctx.cache.set("__all__", data, DEFAULT_CACHE_TTL);
8
8
  for (const [key, value] of Object.entries(data)) {
9
9
  ctx.cache.set(key, value, DEFAULT_CACHE_TTL);
@@ -7,7 +7,7 @@ export const executePessimistic = async (ctx) => {
7
7
  setTimeout(() => reject(new TimeoutError(ctx.timeout)), ctx.timeout);
8
8
  });
9
9
  try {
10
- const data = await Promise.race([fetchPromise, timeoutPromise]);
10
+ const { data } = await Promise.race([fetchPromise, timeoutPromise]);
11
11
  ctx.cache.set("__all__", data, DEFAULT_CACHE_TTL);
12
12
  for (const [key, value] of Object.entries(data)) {
13
13
  ctx.cache.set(key, value, DEFAULT_CACHE_TTL);
@@ -1,15 +1,19 @@
1
1
  /// <reference lib="dom" />
2
+ import { API_ENDPOINTS, HTTP_CONSTANTS } from "../constants.js";
2
3
  import { AuthenticationError, ConfigError, RateLimitError } from "../errors/index.js";
4
+ import { warnForFetchFailure, warnIfCspBlocksUrl } from "./browserDiagnostics.js";
3
5
  import { CircuitBreaker } from "./CircuitBreaker.js";
4
6
  /** Errors that should permanently stop retrying (circuit opens) */
5
7
  const FATAL_STATUS_CODES = new Set([400, 401, 403]);
6
8
  /** Cooldown before trying again after a fatal error (5 minutes) */
7
9
  const CIRCUIT_COOLDOWN_MS = 5 * 60 * 1000;
8
10
  export const createHttpTransport = (config) => {
11
+ warnIfCspBlocksUrl(config.baseUrl);
9
12
  const circuitBreaker = new CircuitBreaker({
10
13
  fatalCodes: FATAL_STATUS_CODES,
11
14
  cooldownMs: CIRCUIT_COOLDOWN_MS,
12
15
  });
16
+ let versionCache;
13
17
  return {
14
18
  async request(endpoint, body) {
15
19
  // ── Circuit breaker check ──────────────────────────────
@@ -35,25 +39,50 @@ export const createHttpTransport = (config) => {
35
39
  // Client keys (cid_) use POST with body (server-side evaluation needs context).
36
40
  // Server keys (svr_) use GET with query params (fetches full flag data).
37
41
  const isServerKey = config.clientId.startsWith("svr_");
42
+ const isVersionRequest = endpoint === API_ENDPOINTS.version;
43
+ const cachedVersion = isVersionRequest ? versionCache : undefined;
44
+ const conditionalHeaders = cachedVersion
45
+ ? { [HTTP_CONSTANTS.ifNoneMatchHeader]: cachedVersion.etag }
46
+ : {};
38
47
  let response;
39
- if (isServerKey) {
40
- // GET request — clientId as query param
41
- const params = new URLSearchParams({ clientId: config.clientId });
42
- if (body?.keys && Array.isArray(body.keys)) {
43
- params.set("keys", body.keys.join(","));
48
+ try {
49
+ if (isServerKey) {
50
+ // GET request — clientId as query param
51
+ const params = new URLSearchParams({ clientId: config.clientId });
52
+ if (body?.keys && Array.isArray(body.keys)) {
53
+ params.set("keys", body.keys.join(","));
54
+ }
55
+ response = await fetch(`${url}?${params.toString()}`, {
56
+ method: "GET",
57
+ headers: { Accept: HTTP_CONSTANTS.jsonAccept, ...conditionalHeaders },
58
+ });
59
+ }
60
+ else {
61
+ // POST request — full body with context
62
+ response = await fetch(url, {
63
+ method: "POST",
64
+ headers: {
65
+ "Content-Type": HTTP_CONSTANTS.jsonContentType,
66
+ ...conditionalHeaders,
67
+ },
68
+ body: JSON.stringify({ data: requestData }),
69
+ });
44
70
  }
45
- response = await fetch(`${url}?${params.toString()}`, {
46
- method: "GET",
47
- headers: { Accept: "application/json" },
48
- });
49
71
  }
50
- else {
51
- // POST request — full body with context
52
- response = await fetch(url, {
53
- method: "POST",
54
- headers: { "Content-Type": "application/json" },
55
- body: JSON.stringify({ data: requestData }),
56
- });
72
+ catch (error) {
73
+ warnForFetchFailure(url, error);
74
+ throw error;
75
+ }
76
+ // A conditional version request has no response body on 304. Reuse the
77
+ // last successful body so callers never attempt to parse an empty body.
78
+ if (response.status === 304 && cachedVersion) {
79
+ circuitBreaker.recordSuccess();
80
+ return cachedVersion.body;
81
+ }
82
+ if (response.status === 304) {
83
+ const error = new ConfigError("Version endpoint returned 304 but no cached response is available", "NETWORK_ERROR");
84
+ circuitBreaker.recordFailure(response.status, error);
85
+ throw error;
57
86
  }
58
87
  if (!response.ok) {
59
88
  const errorBody = await response.json().catch(() => ({}));
@@ -103,7 +132,13 @@ export const createHttpTransport = (config) => {
103
132
  }
104
133
  // Success: record in circuit breaker (closes if HALF_OPEN)
105
134
  circuitBreaker.recordSuccess();
106
- return (await response.json());
135
+ const responseBody = (await response.json());
136
+ if (isVersionRequest) {
137
+ const etag = response.headers.get(HTTP_CONSTANTS.etagHeader);
138
+ if (etag)
139
+ versionCache = { etag, body: responseBody };
140
+ }
141
+ return responseBody;
107
142
  },
108
143
  };
109
144
  };
@@ -0,0 +1,4 @@
1
+ /** Warn when a readable meta CSP blocks the SDK's API connection. */
2
+ export declare function warnIfCspBlocksUrl(baseUrl: string): void;
3
+ /** Fetch failures are intentionally non-fatal; cached/default values can continue. */
4
+ export declare function warnForFetchFailure(url: string, error: unknown): void;
@@ -0,0 +1,69 @@
1
+ /// <reference lib="dom" />
2
+ const warnedMessages = new Set();
3
+ function warnOnce(message, cause) {
4
+ if (warnedMessages.has(message))
5
+ return;
6
+ warnedMessages.add(message);
7
+ if (cause === undefined)
8
+ console.warn(`[config] ${message}`);
9
+ else
10
+ console.warn(`[config] ${message}`, cause);
11
+ }
12
+ function sourceAllowsUrl(source, target) {
13
+ const normalized = source.replace(/^['"]|['"]$/g, "");
14
+ if (normalized === "*")
15
+ return true;
16
+ if (normalized === "none")
17
+ return false;
18
+ if (normalized === "self")
19
+ return target.origin === window.location.origin;
20
+ if (/^[a-z][a-z\d+.-]*:$/i.test(normalized))
21
+ return target.protocol === normalized;
22
+ try {
23
+ const sourceUrl = new URL(normalized);
24
+ if (sourceUrl.protocol !== target.protocol)
25
+ return false;
26
+ const sourceHost = sourceUrl.hostname.toLowerCase();
27
+ const targetHost = target.hostname.toLowerCase();
28
+ const hostMatches = sourceHost.startsWith("*.")
29
+ ? targetHost.endsWith(sourceHost.slice(1))
30
+ : sourceHost === targetHost;
31
+ const portMatches = !sourceUrl.port || sourceUrl.port === target.port;
32
+ return hostMatches && portMatches;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ /** Warn when a readable meta CSP blocks the SDK's API connection. */
39
+ export function warnIfCspBlocksUrl(baseUrl) {
40
+ if (typeof window === "undefined" || typeof document === "undefined")
41
+ return;
42
+ let target;
43
+ try {
44
+ target = new URL(baseUrl, window.location.href);
45
+ }
46
+ catch {
47
+ return;
48
+ }
49
+ const policies = Array.from(document.querySelectorAll('meta[http-equiv="Content-Security-Policy"]'))
50
+ .map((meta) => meta.content)
51
+ .filter(Boolean);
52
+ for (const policy of policies) {
53
+ const directives = policy.split(";").map((directive) => directive.trim().split(/\s+/));
54
+ const connect = directives.find(([name]) => name === "connect-src");
55
+ const fallback = directives.find(([name]) => name === "default-src");
56
+ const sources = connect?.slice(1) ?? fallback?.slice(1);
57
+ if (sources && !sources.some((source) => sourceAllowsUrl(source, target))) {
58
+ warnOnce(`CSP may block requests to ${target.origin}. Add ${target.origin} to connect-src (or default-src) in your Content-Security-Policy.`);
59
+ }
60
+ }
61
+ }
62
+ /** Fetch failures are intentionally non-fatal; cached/default values can continue. */
63
+ export function warnForFetchFailure(url, error) {
64
+ if (typeof window === "undefined")
65
+ return;
66
+ const message = error instanceof Error ? error.message : String(error);
67
+ warnOnce(`The config request to ${url} failed. This may be a CORS or CSP connect-src restriction; the SDK will continue using cached/default values.`, message);
68
+ }
69
+ //# sourceMappingURL=browserDiagnostics.js.map
package/dist/types.d.ts CHANGED
@@ -137,11 +137,22 @@ export interface LoadingResult {
137
137
  export interface LoadingStrategyHandler {
138
138
  execute(context: LoadingContext): LoadingResult | Promise<LoadingResult>;
139
139
  }
140
+ /**
141
+ * Result of a config fetch — the resolved values plus the config version
142
+ * they correspond to. The version comes from the same `/config` response
143
+ * that delivered the data, so the SDK can persist it without a second
144
+ * `/version` round trip.
145
+ */
146
+ export interface FetchResult {
147
+ data: Record<string, unknown>;
148
+ /** Config version from the response, or null if the response omitted it. */
149
+ version: string | null;
150
+ }
140
151
  export interface ConfigFetcher {
141
152
  /** Fetch all configs for the bound project+environment */
142
- fetchAll(): Promise<Record<string, unknown>>;
153
+ fetchAll(): Promise<FetchResult>;
143
154
  /** Fetch specific keys only */
144
- fetchKeys(keys: string[]): Promise<Record<string, unknown>>;
155
+ fetchKeys(keys: string[]): Promise<FetchResult>;
145
156
  /** Check current config version (lightweight — single Firestore read) */
146
157
  checkVersion(): Promise<{
147
158
  version: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jewel998/config",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "private": false,
5
5
  "description": "Free, self-hosted feature flags and remote configuration SDK",
6
6
  "keywords": [