@quonfig/node 0.0.1 → 0.0.2

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.d.cts CHANGED
@@ -56,6 +56,23 @@ interface ConfigEnvelope {
56
56
  configs: ConfigResponse[];
57
57
  meta: Meta;
58
58
  }
59
+ interface WorkspaceEnvironment {
60
+ id: string;
61
+ rules: Rule[];
62
+ }
63
+ interface WorkspaceConfigDocument {
64
+ id?: string;
65
+ key: string;
66
+ type: ConfigTypeString;
67
+ valueType: ValueType;
68
+ sendToClientSdk?: boolean;
69
+ default?: RuleSet;
70
+ environments?: WorkspaceEnvironment[];
71
+ }
72
+ type QuonfigDatadirEnvironments = Record<string, string> | Array<string | {
73
+ id?: string;
74
+ name?: string;
75
+ }>;
59
76
  type ContextValue = string | number | boolean | string[] | null | undefined;
60
77
  type Contexts = {
61
78
  [contextName: string]: {
@@ -67,7 +84,10 @@ type OnNoDefault = "error" | "warn" | "ignore";
67
84
  type ContextUploadMode = "none" | "shapes_only" | "periodic_example";
68
85
  interface QuonfigOptions {
69
86
  sdkKey: string;
87
+ /** @deprecated Use apiUrls instead. If provided, used as a single-element URL list. */
70
88
  apiUrl?: string;
89
+ /** Ordered list of API base URLs to try. Defaults to ["https://primary.quonfig.com", "https://secondary.quonfig.com"]. */
90
+ apiUrls?: string[];
71
91
  /** Base URL for the dedicated telemetry service. Defaults to https://telemetry.quonfig.com. Overridden by QUONFIG_TELEMETRY_URL env var. */
72
92
  telemetryUrl?: string;
73
93
  enableSSE?: boolean;
@@ -80,6 +100,7 @@ interface QuonfigOptions {
80
100
  collectLoggerCounts?: boolean;
81
101
  contextUploadMode?: ContextUploadMode;
82
102
  initTimeout?: number;
103
+ datadir?: string;
83
104
  datafile?: string | object;
84
105
  }
85
106
  interface EvalMatch {
@@ -196,7 +217,7 @@ declare class BoundQuonfig {
196
217
  */
197
218
  declare class Quonfig {
198
219
  private readonly sdkKey;
199
- private readonly apiUrl;
220
+ private readonly apiUrls;
200
221
  private readonly telemetryUrl?;
201
222
  private readonly enableSSE;
202
223
  private readonly enablePolling;
@@ -205,6 +226,7 @@ declare class Quonfig {
205
226
  private readonly onNoDefault;
206
227
  private readonly globalContext?;
207
228
  private readonly initTimeout;
229
+ private readonly datadir?;
208
230
  private readonly datafile?;
209
231
  private store;
210
232
  private evaluator;
@@ -221,7 +243,7 @@ declare class Quonfig {
221
243
  private exampleContexts;
222
244
  constructor(options: QuonfigOptions);
223
245
  /**
224
- * Initialize the SDK. Downloads configs from the API (or loads from datafile)
246
+ * Initialize the SDK. Downloads configs from the API (or loads from datadir/datafile)
225
247
  * and starts background update mechanisms (SSE/polling).
226
248
  *
227
249
  * Must be called before using any get* methods.
@@ -287,7 +309,8 @@ declare class Quonfig {
287
309
  close(): void;
288
310
  private requireInitialized;
289
311
  private handleNoDefault;
290
- private loadDatafile;
312
+ private loadLocalData;
313
+ private loadLocalEnvelope;
291
314
  private fetchAndInstall;
292
315
  private startSSE;
293
316
  private startPolling;
@@ -466,11 +489,12 @@ interface FetchResult {
466
489
  notChanged: boolean;
467
490
  }
468
491
  declare class Transport {
469
- private baseUrl;
492
+ private baseUrls;
493
+ private activeBaseUrl;
470
494
  private telemetryBaseUrl;
471
495
  private sdkKey;
472
496
  private etag;
473
- constructor(baseUrl: string, sdkKey: string, telemetryBaseUrl?: string);
497
+ constructor(baseUrls: string[], sdkKey: string, telemetryBaseUrl?: string);
474
498
  /**
475
499
  * Build the Basic auth header value.
476
500
  * Uses username "1" like the Go SDK: base64("1:{sdkKey}")
@@ -483,6 +507,7 @@ declare class Transport {
483
507
  /**
484
508
  * Fetch configs from GET /api/v2/configs with ETag caching.
485
509
  *
510
+ * Tries each base URL in order. Returns the first successful result.
486
511
  * Returns `{ notChanged: true }` if the server responds with 304.
487
512
  */
488
513
  fetchConfigs(): Promise<FetchResult>;
@@ -492,6 +517,7 @@ declare class Transport {
492
517
  postTelemetry(data: any): Promise<void>;
493
518
  /**
494
519
  * Get the SSE URL for config streaming.
520
+ * Uses whichever base URL last succeeded for fetchConfigs.
495
521
  */
496
522
  getSSEUrl(): string;
497
523
  /**
@@ -752,4 +778,4 @@ declare const encryption: {
752
778
  generateNewHexKey: typeof generateNewHexKey;
753
779
  };
754
780
 
755
- export { BoundQuonfig, Client, type ClientOptions, type ConfigEnvelope, type ConfigResponse, ConfigStore, ConfigType, type ConfigTypeString, type ConfigValue, ConfigValueType, ContextShapeCollector, type ContextUploadMode, type ContextValue, type Contexts, type Criterion, type Environment, type EvalMatch, type Evaluation, EvaluationSummaryCollector, Evaluator, ExampleContextCollector, type GetValue, LOG_LEVEL_PREFIX, type LogLevelName, type LogLevelNumber, type Meta, type NodeServerConfigurationAccessor, type NodeServerConfigurationRaw, OP_ALWAYS_TRUE, OP_HIERARCHICAL_MATCH, OP_IN_INT_RANGE, OP_IN_SEG, OP_NOT_IN_SEG, OP_NOT_SET, OP_PROP_AFTER, OP_PROP_BEFORE, OP_PROP_CONTAINS_ONE_OF, OP_PROP_DOES_NOT_CONTAIN_ONE_OF, OP_PROP_DOES_NOT_END_WITH_ONE_OF, OP_PROP_DOES_NOT_MATCH, OP_PROP_DOES_NOT_START_WITH_ONE_OF, OP_PROP_ENDS_WITH_ONE_OF, OP_PROP_GREATER_THAN, OP_PROP_GREATER_THAN_OR_EQUAL, OP_PROP_IS_NOT_ONE_OF, OP_PROP_IS_ONE_OF, OP_PROP_LESS_THAN, OP_PROP_LESS_THAN_OR_EQUAL, OP_PROP_MATCHES, OP_PROP_SEMVER_EQUAL, OP_PROP_SEMVER_GREATER_THAN, OP_PROP_SEMVER_LESS_THAN, OP_PROP_STARTS_WITH_ONE_OF, type OnNoDefault, type ProjectEnvId, type ProvidedData, ProvidedSource, Quonfig, type QuonfigOptions, Resolver, type Rule, type RuleSet, type SchemaData, type SegmentResolver, type SemanticVersion, TelemetryReporter, Transport, type TypedNodeServerConfigurationAccessor, type TypedNodeServerConfigurationRaw, type Value, type ValueType, type WeightedValue, WeightedValueResolver, type WeightedValuesData, compareSemver, contextLookup, decrypt, durationToMilliseconds, encrypt, encryption, evaluateCriterion, generateNewHexKey, getContextValue, getProjectEnvFromSdkKey, hashZeroToOne, mergeContexts, parseLevel, parseSemver, shouldLog, valueTypeStringForConfig, wordLevelToNumber };
781
+ export { BoundQuonfig, Client, type ClientOptions, type ConfigEnvelope, type ConfigResponse, ConfigStore, ConfigType, type ConfigTypeString, type ConfigValue, ConfigValueType, ContextShapeCollector, type ContextUploadMode, type ContextValue, type Contexts, type Criterion, type Environment, type EvalMatch, type Evaluation, EvaluationSummaryCollector, Evaluator, ExampleContextCollector, type GetValue, LOG_LEVEL_PREFIX, type LogLevelName, type LogLevelNumber, type Meta, type NodeServerConfigurationAccessor, type NodeServerConfigurationRaw, OP_ALWAYS_TRUE, OP_HIERARCHICAL_MATCH, OP_IN_INT_RANGE, OP_IN_SEG, OP_NOT_IN_SEG, OP_NOT_SET, OP_PROP_AFTER, OP_PROP_BEFORE, OP_PROP_CONTAINS_ONE_OF, OP_PROP_DOES_NOT_CONTAIN_ONE_OF, OP_PROP_DOES_NOT_END_WITH_ONE_OF, OP_PROP_DOES_NOT_MATCH, OP_PROP_DOES_NOT_START_WITH_ONE_OF, OP_PROP_ENDS_WITH_ONE_OF, OP_PROP_GREATER_THAN, OP_PROP_GREATER_THAN_OR_EQUAL, OP_PROP_IS_NOT_ONE_OF, OP_PROP_IS_ONE_OF, OP_PROP_LESS_THAN, OP_PROP_LESS_THAN_OR_EQUAL, OP_PROP_MATCHES, OP_PROP_SEMVER_EQUAL, OP_PROP_SEMVER_GREATER_THAN, OP_PROP_SEMVER_LESS_THAN, OP_PROP_STARTS_WITH_ONE_OF, type OnNoDefault, type ProjectEnvId, type ProvidedData, ProvidedSource, Quonfig, type QuonfigDatadirEnvironments, type QuonfigOptions, Resolver, type Rule, type RuleSet, type SchemaData, type SegmentResolver, type SemanticVersion, TelemetryReporter, Transport, type TypedNodeServerConfigurationAccessor, type TypedNodeServerConfigurationRaw, type Value, type ValueType, type WeightedValue, WeightedValueResolver, type WeightedValuesData, type WorkspaceConfigDocument, type WorkspaceEnvironment, compareSemver, contextLookup, decrypt, durationToMilliseconds, encrypt, encryption, evaluateCriterion, generateNewHexKey, getContextValue, getProjectEnvFromSdkKey, hashZeroToOne, mergeContexts, parseLevel, parseSemver, shouldLog, valueTypeStringForConfig, wordLevelToNumber };
package/dist/index.d.ts CHANGED
@@ -56,6 +56,23 @@ interface ConfigEnvelope {
56
56
  configs: ConfigResponse[];
57
57
  meta: Meta;
58
58
  }
59
+ interface WorkspaceEnvironment {
60
+ id: string;
61
+ rules: Rule[];
62
+ }
63
+ interface WorkspaceConfigDocument {
64
+ id?: string;
65
+ key: string;
66
+ type: ConfigTypeString;
67
+ valueType: ValueType;
68
+ sendToClientSdk?: boolean;
69
+ default?: RuleSet;
70
+ environments?: WorkspaceEnvironment[];
71
+ }
72
+ type QuonfigDatadirEnvironments = Record<string, string> | Array<string | {
73
+ id?: string;
74
+ name?: string;
75
+ }>;
59
76
  type ContextValue = string | number | boolean | string[] | null | undefined;
60
77
  type Contexts = {
61
78
  [contextName: string]: {
@@ -67,7 +84,10 @@ type OnNoDefault = "error" | "warn" | "ignore";
67
84
  type ContextUploadMode = "none" | "shapes_only" | "periodic_example";
68
85
  interface QuonfigOptions {
69
86
  sdkKey: string;
87
+ /** @deprecated Use apiUrls instead. If provided, used as a single-element URL list. */
70
88
  apiUrl?: string;
89
+ /** Ordered list of API base URLs to try. Defaults to ["https://primary.quonfig.com", "https://secondary.quonfig.com"]. */
90
+ apiUrls?: string[];
71
91
  /** Base URL for the dedicated telemetry service. Defaults to https://telemetry.quonfig.com. Overridden by QUONFIG_TELEMETRY_URL env var. */
72
92
  telemetryUrl?: string;
73
93
  enableSSE?: boolean;
@@ -80,6 +100,7 @@ interface QuonfigOptions {
80
100
  collectLoggerCounts?: boolean;
81
101
  contextUploadMode?: ContextUploadMode;
82
102
  initTimeout?: number;
103
+ datadir?: string;
83
104
  datafile?: string | object;
84
105
  }
85
106
  interface EvalMatch {
@@ -196,7 +217,7 @@ declare class BoundQuonfig {
196
217
  */
197
218
  declare class Quonfig {
198
219
  private readonly sdkKey;
199
- private readonly apiUrl;
220
+ private readonly apiUrls;
200
221
  private readonly telemetryUrl?;
201
222
  private readonly enableSSE;
202
223
  private readonly enablePolling;
@@ -205,6 +226,7 @@ declare class Quonfig {
205
226
  private readonly onNoDefault;
206
227
  private readonly globalContext?;
207
228
  private readonly initTimeout;
229
+ private readonly datadir?;
208
230
  private readonly datafile?;
209
231
  private store;
210
232
  private evaluator;
@@ -221,7 +243,7 @@ declare class Quonfig {
221
243
  private exampleContexts;
222
244
  constructor(options: QuonfigOptions);
223
245
  /**
224
- * Initialize the SDK. Downloads configs from the API (or loads from datafile)
246
+ * Initialize the SDK. Downloads configs from the API (or loads from datadir/datafile)
225
247
  * and starts background update mechanisms (SSE/polling).
226
248
  *
227
249
  * Must be called before using any get* methods.
@@ -287,7 +309,8 @@ declare class Quonfig {
287
309
  close(): void;
288
310
  private requireInitialized;
289
311
  private handleNoDefault;
290
- private loadDatafile;
312
+ private loadLocalData;
313
+ private loadLocalEnvelope;
291
314
  private fetchAndInstall;
292
315
  private startSSE;
293
316
  private startPolling;
@@ -466,11 +489,12 @@ interface FetchResult {
466
489
  notChanged: boolean;
467
490
  }
468
491
  declare class Transport {
469
- private baseUrl;
492
+ private baseUrls;
493
+ private activeBaseUrl;
470
494
  private telemetryBaseUrl;
471
495
  private sdkKey;
472
496
  private etag;
473
- constructor(baseUrl: string, sdkKey: string, telemetryBaseUrl?: string);
497
+ constructor(baseUrls: string[], sdkKey: string, telemetryBaseUrl?: string);
474
498
  /**
475
499
  * Build the Basic auth header value.
476
500
  * Uses username "1" like the Go SDK: base64("1:{sdkKey}")
@@ -483,6 +507,7 @@ declare class Transport {
483
507
  /**
484
508
  * Fetch configs from GET /api/v2/configs with ETag caching.
485
509
  *
510
+ * Tries each base URL in order. Returns the first successful result.
486
511
  * Returns `{ notChanged: true }` if the server responds with 304.
487
512
  */
488
513
  fetchConfigs(): Promise<FetchResult>;
@@ -492,6 +517,7 @@ declare class Transport {
492
517
  postTelemetry(data: any): Promise<void>;
493
518
  /**
494
519
  * Get the SSE URL for config streaming.
520
+ * Uses whichever base URL last succeeded for fetchConfigs.
495
521
  */
496
522
  getSSEUrl(): string;
497
523
  /**
@@ -752,4 +778,4 @@ declare const encryption: {
752
778
  generateNewHexKey: typeof generateNewHexKey;
753
779
  };
754
780
 
755
- export { BoundQuonfig, Client, type ClientOptions, type ConfigEnvelope, type ConfigResponse, ConfigStore, ConfigType, type ConfigTypeString, type ConfigValue, ConfigValueType, ContextShapeCollector, type ContextUploadMode, type ContextValue, type Contexts, type Criterion, type Environment, type EvalMatch, type Evaluation, EvaluationSummaryCollector, Evaluator, ExampleContextCollector, type GetValue, LOG_LEVEL_PREFIX, type LogLevelName, type LogLevelNumber, type Meta, type NodeServerConfigurationAccessor, type NodeServerConfigurationRaw, OP_ALWAYS_TRUE, OP_HIERARCHICAL_MATCH, OP_IN_INT_RANGE, OP_IN_SEG, OP_NOT_IN_SEG, OP_NOT_SET, OP_PROP_AFTER, OP_PROP_BEFORE, OP_PROP_CONTAINS_ONE_OF, OP_PROP_DOES_NOT_CONTAIN_ONE_OF, OP_PROP_DOES_NOT_END_WITH_ONE_OF, OP_PROP_DOES_NOT_MATCH, OP_PROP_DOES_NOT_START_WITH_ONE_OF, OP_PROP_ENDS_WITH_ONE_OF, OP_PROP_GREATER_THAN, OP_PROP_GREATER_THAN_OR_EQUAL, OP_PROP_IS_NOT_ONE_OF, OP_PROP_IS_ONE_OF, OP_PROP_LESS_THAN, OP_PROP_LESS_THAN_OR_EQUAL, OP_PROP_MATCHES, OP_PROP_SEMVER_EQUAL, OP_PROP_SEMVER_GREATER_THAN, OP_PROP_SEMVER_LESS_THAN, OP_PROP_STARTS_WITH_ONE_OF, type OnNoDefault, type ProjectEnvId, type ProvidedData, ProvidedSource, Quonfig, type QuonfigOptions, Resolver, type Rule, type RuleSet, type SchemaData, type SegmentResolver, type SemanticVersion, TelemetryReporter, Transport, type TypedNodeServerConfigurationAccessor, type TypedNodeServerConfigurationRaw, type Value, type ValueType, type WeightedValue, WeightedValueResolver, type WeightedValuesData, compareSemver, contextLookup, decrypt, durationToMilliseconds, encrypt, encryption, evaluateCriterion, generateNewHexKey, getContextValue, getProjectEnvFromSdkKey, hashZeroToOne, mergeContexts, parseLevel, parseSemver, shouldLog, valueTypeStringForConfig, wordLevelToNumber };
781
+ export { BoundQuonfig, Client, type ClientOptions, type ConfigEnvelope, type ConfigResponse, ConfigStore, ConfigType, type ConfigTypeString, type ConfigValue, ConfigValueType, ContextShapeCollector, type ContextUploadMode, type ContextValue, type Contexts, type Criterion, type Environment, type EvalMatch, type Evaluation, EvaluationSummaryCollector, Evaluator, ExampleContextCollector, type GetValue, LOG_LEVEL_PREFIX, type LogLevelName, type LogLevelNumber, type Meta, type NodeServerConfigurationAccessor, type NodeServerConfigurationRaw, OP_ALWAYS_TRUE, OP_HIERARCHICAL_MATCH, OP_IN_INT_RANGE, OP_IN_SEG, OP_NOT_IN_SEG, OP_NOT_SET, OP_PROP_AFTER, OP_PROP_BEFORE, OP_PROP_CONTAINS_ONE_OF, OP_PROP_DOES_NOT_CONTAIN_ONE_OF, OP_PROP_DOES_NOT_END_WITH_ONE_OF, OP_PROP_DOES_NOT_MATCH, OP_PROP_DOES_NOT_START_WITH_ONE_OF, OP_PROP_ENDS_WITH_ONE_OF, OP_PROP_GREATER_THAN, OP_PROP_GREATER_THAN_OR_EQUAL, OP_PROP_IS_NOT_ONE_OF, OP_PROP_IS_ONE_OF, OP_PROP_LESS_THAN, OP_PROP_LESS_THAN_OR_EQUAL, OP_PROP_MATCHES, OP_PROP_SEMVER_EQUAL, OP_PROP_SEMVER_GREATER_THAN, OP_PROP_SEMVER_LESS_THAN, OP_PROP_STARTS_WITH_ONE_OF, type OnNoDefault, type ProjectEnvId, type ProvidedData, ProvidedSource, Quonfig, type QuonfigDatadirEnvironments, type QuonfigOptions, Resolver, type Rule, type RuleSet, type SchemaData, type SegmentResolver, type SemanticVersion, TelemetryReporter, Transport, type TypedNodeServerConfigurationAccessor, type TypedNodeServerConfigurationRaw, type Value, type ValueType, type WeightedValue, WeightedValueResolver, type WeightedValuesData, type WorkspaceConfigDocument, type WorkspaceEnvironment, compareSemver, contextLookup, decrypt, durationToMilliseconds, encrypt, encryption, evaluateCriterion, generateNewHexKey, getContextValue, getProjectEnvFromSdkKey, hashZeroToOne, mergeContexts, parseLevel, parseSemver, shouldLog, valueTypeStringForConfig, wordLevelToNumber };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/quonfig.ts
2
2
  import { randomUUID } from "crypto";
3
- import { readFileSync } from "fs";
3
+ import { readFileSync as readFileSync2 } from "fs";
4
4
 
5
5
  // src/store.ts
6
6
  var ConfigStore = class {
@@ -825,12 +825,14 @@ function valueTypeForCoerced(valueType) {
825
825
  var SDK_VERSION = "0.1.0";
826
826
  var DEFAULT_TELEMETRY_URL = "https://telemetry.quonfig.com";
827
827
  var Transport = class {
828
- baseUrl;
828
+ baseUrls;
829
+ activeBaseUrl;
829
830
  telemetryBaseUrl;
830
831
  sdkKey;
831
832
  etag = "";
832
- constructor(baseUrl, sdkKey, telemetryBaseUrl) {
833
- this.baseUrl = baseUrl.replace(/\/$/, "");
833
+ constructor(baseUrls, sdkKey, telemetryBaseUrl) {
834
+ this.baseUrls = baseUrls.map((u) => u.replace(/\/$/, ""));
835
+ this.activeBaseUrl = this.baseUrls[0];
834
836
  const envUrl = process.env.QUONFIG_TELEMETRY_URL;
835
837
  const url = envUrl || telemetryBaseUrl || DEFAULT_TELEMETRY_URL;
836
838
  this.telemetryBaseUrl = url.replace(/\/$/, "");
@@ -857,30 +859,41 @@ var Transport = class {
857
859
  /**
858
860
  * Fetch configs from GET /api/v2/configs with ETag caching.
859
861
  *
862
+ * Tries each base URL in order. Returns the first successful result.
860
863
  * Returns `{ notChanged: true }` if the server responds with 304.
861
864
  */
862
865
  async fetchConfigs() {
863
- const headers = this.getHeaders();
864
- if (this.etag) {
865
- headers["If-None-Match"] = this.etag;
866
- }
867
- const response = await fetch(`${this.baseUrl}/api/v2/configs`, {
868
- method: "GET",
869
- headers
870
- });
871
- if (response.status === 304) {
872
- return { notChanged: true };
873
- }
874
- if (!response.ok) {
875
- const body = await response.text().catch(() => "");
876
- throw new Error(`Unexpected status ${response.status}: ${body}`);
877
- }
878
- const etag = response.headers.get("ETag");
879
- if (etag) {
880
- this.etag = etag;
866
+ let lastError;
867
+ for (const baseUrl of this.baseUrls) {
868
+ try {
869
+ const headers = this.getHeaders();
870
+ if (this.etag) {
871
+ headers["If-None-Match"] = this.etag;
872
+ }
873
+ const response = await fetch(`${baseUrl}/api/v2/configs`, {
874
+ method: "GET",
875
+ headers
876
+ });
877
+ if (response.status === 304) {
878
+ this.activeBaseUrl = baseUrl;
879
+ return { notChanged: true };
880
+ }
881
+ if (!response.ok) {
882
+ const body = await response.text().catch(() => "");
883
+ throw new Error(`Unexpected status ${response.status} from ${baseUrl}: ${body}`);
884
+ }
885
+ const etag = response.headers.get("ETag");
886
+ if (etag) {
887
+ this.etag = etag;
888
+ }
889
+ this.activeBaseUrl = baseUrl;
890
+ const envelope = await response.json();
891
+ return { envelope, notChanged: false };
892
+ } catch (err) {
893
+ lastError = err instanceof Error ? err : new Error(String(err));
894
+ }
881
895
  }
882
- const envelope = await response.json();
883
- return { envelope, notChanged: false };
896
+ throw lastError ?? new Error("All API URLs failed");
884
897
  }
885
898
  /**
886
899
  * Post telemetry data to the telemetry endpoint.
@@ -901,9 +914,10 @@ var Transport = class {
901
914
  }
902
915
  /**
903
916
  * Get the SSE URL for config streaming.
917
+ * Uses whichever base URL last succeeded for fetchConfigs.
904
918
  */
905
919
  getSSEUrl() {
906
- return `${this.baseUrl}/api/v2/sse/config`;
920
+ return `${this.activeBaseUrl}/api/v2/sse/config`;
907
921
  }
908
922
  /**
909
923
  * Get auth headers for SSE connection.
@@ -1018,6 +1032,87 @@ function shouldLog(args) {
1018
1032
  return defaultLevel <= desiredLevel;
1019
1033
  }
1020
1034
 
1035
+ // src/datadir.ts
1036
+ import { existsSync, readdirSync, readFileSync } from "fs";
1037
+ import { join } from "path";
1038
+ var CONFIG_SUBDIRS = ["configs", "feature-flags", "segments", "schemas", "log-levels"];
1039
+ function loadEnvelopeFromDatadir(datadir) {
1040
+ const environmentId = loadEnvironmentId(join(datadir, "environments.json"));
1041
+ const configs = [];
1042
+ for (const subdir of CONFIG_SUBDIRS) {
1043
+ const dir = join(datadir, subdir);
1044
+ if (!existsSync(dir)) {
1045
+ continue;
1046
+ }
1047
+ const filenames = readdirSync(dir).filter((filename) => filename.endsWith(".json")).sort((a, b) => a.localeCompare(b));
1048
+ for (const filename of filenames) {
1049
+ const raw = JSON.parse(readFileSync(join(dir, filename), "utf-8"));
1050
+ configs.push(toConfigResponse(raw, environmentId));
1051
+ }
1052
+ }
1053
+ return {
1054
+ configs,
1055
+ meta: {
1056
+ version: `datadir:${datadir}`,
1057
+ environment: environmentId
1058
+ }
1059
+ };
1060
+ }
1061
+ function loadEnvironmentId(environmentsPath) {
1062
+ if (!existsSync(environmentsPath)) {
1063
+ throw new Error(`[quonfig] Datadir is missing environments.json: ${environmentsPath}`);
1064
+ }
1065
+ const environments = JSON.parse(readFileSync(environmentsPath, "utf-8"));
1066
+ const candidates = normalizeEnvironmentCandidates(
1067
+ isWrappedEnvironmentList(environments) ? environments.environments : environments
1068
+ );
1069
+ if (candidates.length === 0) {
1070
+ return "";
1071
+ }
1072
+ return candidates[0];
1073
+ }
1074
+ function isWrappedEnvironmentList(value) {
1075
+ return Boolean(
1076
+ value && typeof value === "object" && !Array.isArray(value) && "environments" in value
1077
+ );
1078
+ }
1079
+ function normalizeEnvironmentCandidates(environments) {
1080
+ if (!environments) {
1081
+ return [];
1082
+ }
1083
+ if (Array.isArray(environments)) {
1084
+ return environments.map((entry) => {
1085
+ if (typeof entry === "string") {
1086
+ return entry;
1087
+ }
1088
+ if (entry && typeof entry === "object") {
1089
+ return entry.id ?? entry.name;
1090
+ }
1091
+ return void 0;
1092
+ }).filter((entry) => typeof entry === "string" && entry.length > 0);
1093
+ }
1094
+ if (environments && typeof environments === "object") {
1095
+ const values = Object.values(environments).map((value) => typeof value === "string" && value.length > 0 ? value : void 0).filter((value) => typeof value === "string");
1096
+ if (values.length > 0) {
1097
+ return values;
1098
+ }
1099
+ return Object.keys(environments).filter((key) => key.length > 0);
1100
+ }
1101
+ return [];
1102
+ }
1103
+ function toConfigResponse(raw, environmentId) {
1104
+ const environment = raw.environments?.find((candidate) => candidate.id === environmentId);
1105
+ return {
1106
+ id: raw.id ?? "",
1107
+ key: raw.key,
1108
+ type: raw.type,
1109
+ valueType: raw.valueType,
1110
+ sendToClientSdk: raw.sendToClientSdk ?? false,
1111
+ default: raw.default ?? { rules: [] },
1112
+ environment
1113
+ };
1114
+ }
1115
+
1021
1116
  // src/telemetry/evaluationSummaries.ts
1022
1117
  var EvaluationSummaryCollector = class {
1023
1118
  enabled;
@@ -1287,7 +1382,10 @@ var TelemetryReporter = class {
1287
1382
  };
1288
1383
 
1289
1384
  // src/quonfig.ts
1290
- var DEFAULT_API_URL = "https://api.quonfig.com";
1385
+ var DEFAULT_API_URLS = [
1386
+ "https://primary.quonfig.com",
1387
+ "https://secondary.quonfig.com"
1388
+ ];
1291
1389
  var DEFAULT_POLL_INTERVAL = 6e4;
1292
1390
  var DEFAULT_INIT_TIMEOUT = 1e4;
1293
1391
  var DEFAULT_LOG_LEVEL = 5;
@@ -1337,7 +1435,7 @@ var BoundQuonfig = class _BoundQuonfig {
1337
1435
  };
1338
1436
  var Quonfig = class {
1339
1437
  sdkKey;
1340
- apiUrl;
1438
+ apiUrls;
1341
1439
  telemetryUrl;
1342
1440
  enableSSE;
1343
1441
  enablePolling;
@@ -1346,6 +1444,7 @@ var Quonfig = class {
1346
1444
  onNoDefault;
1347
1445
  globalContext;
1348
1446
  initTimeout;
1447
+ datadir;
1349
1448
  datafile;
1350
1449
  store;
1351
1450
  evaluator;
@@ -1363,7 +1462,10 @@ var Quonfig = class {
1363
1462
  exampleContexts;
1364
1463
  constructor(options) {
1365
1464
  this.sdkKey = options.sdkKey;
1366
- this.apiUrl = (options.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
1465
+ this.apiUrls = options.apiUrls ?? (options.apiUrl ? [options.apiUrl] : DEFAULT_API_URLS);
1466
+ if (this.apiUrls.length === 0) {
1467
+ throw new Error("[quonfig] apiUrls must not be empty");
1468
+ }
1367
1469
  this.telemetryUrl = options.telemetryUrl;
1368
1470
  this.enableSSE = options.enableSSE ?? true;
1369
1471
  this.enablePolling = options.enablePolling ?? false;
@@ -1372,12 +1474,13 @@ var Quonfig = class {
1372
1474
  this.onNoDefault = options.onNoDefault ?? "error";
1373
1475
  this.globalContext = options.globalContext;
1374
1476
  this.initTimeout = options.initTimeout ?? DEFAULT_INIT_TIMEOUT;
1477
+ this.datadir = options.datadir;
1375
1478
  this.datafile = options.datafile;
1376
1479
  this.instanceHash = randomUUID();
1377
1480
  this.store = new ConfigStore();
1378
1481
  this.evaluator = new Evaluator(this.store);
1379
1482
  this.resolver = new Resolver(this.store, this.evaluator);
1380
- this.transport = new Transport(this.apiUrl, this.sdkKey, this.telemetryUrl);
1483
+ this.transport = new Transport(this.apiUrls, this.sdkKey, this.telemetryUrl);
1381
1484
  const contextUploadMode = options.contextUploadMode ?? "periodic_example";
1382
1485
  this.evaluationSummaries = new EvaluationSummaryCollector(
1383
1486
  options.collectEvaluationSummaries ?? true
@@ -1386,14 +1489,14 @@ var Quonfig = class {
1386
1489
  this.exampleContexts = new ExampleContextCollector(contextUploadMode);
1387
1490
  }
1388
1491
  /**
1389
- * Initialize the SDK. Downloads configs from the API (or loads from datafile)
1492
+ * Initialize the SDK. Downloads configs from the API (or loads from datadir/datafile)
1390
1493
  * and starts background update mechanisms (SSE/polling).
1391
1494
  *
1392
1495
  * Must be called before using any get* methods.
1393
1496
  */
1394
1497
  async init() {
1395
- if (this.datafile) {
1396
- this.loadDatafile();
1498
+ if (this.datadir || this.datafile) {
1499
+ this.loadLocalData();
1397
1500
  this.initialized = true;
1398
1501
  return;
1399
1502
  }
@@ -1598,19 +1701,24 @@ var Quonfig = class {
1598
1701
  return void 0;
1599
1702
  }
1600
1703
  }
1601
- loadDatafile() {
1602
- let data;
1603
- if (typeof this.datafile === "string") {
1604
- const raw = readFileSync(this.datafile, "utf-8");
1605
- data = JSON.parse(raw);
1606
- } else if (typeof this.datafile === "object") {
1607
- data = this.datafile;
1608
- } else {
1609
- throw new Error("Invalid datafile option");
1610
- }
1704
+ loadLocalData() {
1705
+ const data = this.loadLocalEnvelope();
1611
1706
  this.store.update(data);
1612
1707
  this.environmentId = data.meta.environment;
1613
1708
  }
1709
+ loadLocalEnvelope() {
1710
+ if (this.datadir) {
1711
+ return loadEnvelopeFromDatadir(this.datadir);
1712
+ }
1713
+ if (typeof this.datafile === "string") {
1714
+ const raw = readFileSync2(this.datafile, "utf-8");
1715
+ return JSON.parse(raw);
1716
+ }
1717
+ if (typeof this.datafile === "object") {
1718
+ return this.datafile;
1719
+ }
1720
+ throw new Error("Invalid local configuration: expected datadir or datafile");
1721
+ }
1614
1722
  async fetchAndInstall() {
1615
1723
  const result = await this.transport.fetchConfigs();
1616
1724
  if (result.notChanged) {
@@ -1704,11 +1812,28 @@ var Client = class {
1704
1812
  }
1705
1813
  async post(path, payload) {
1706
1814
  const url = `${this.apiUrl}${path}`;
1815
+ const isORPC = path.startsWith("/api/v1/");
1707
1816
  this.log("ApiClient", `POST ${url}`);
1708
- return fetch(url, {
1817
+ const body = isORPC ? JSON.stringify({ json: payload }) : JSON.stringify(payload);
1818
+ const raw = await fetch(url, {
1709
1819
  method: "POST",
1710
1820
  headers: this.headers(),
1711
- body: JSON.stringify(payload)
1821
+ body
1822
+ });
1823
+ if (!isORPC) return raw;
1824
+ const text = await raw.text();
1825
+ let unwrapped = text;
1826
+ try {
1827
+ const parsed = JSON.parse(text);
1828
+ if (parsed && typeof parsed === "object" && "json" in parsed) {
1829
+ unwrapped = JSON.stringify(parsed.json);
1830
+ }
1831
+ } catch {
1832
+ }
1833
+ return new Response(unwrapped, {
1834
+ status: raw.status,
1835
+ statusText: raw.statusText,
1836
+ headers: raw.headers
1712
1837
  });
1713
1838
  }
1714
1839
  async put(path, payload) {