@guidelytech/flags 0.7.0 → 0.9.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/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  decryptPayloadString,
6
6
  isEncryptedPayload,
7
7
  isFlagsPayload
8
- } from "./chunk-ZTY7K7P2.js";
8
+ } from "./chunk-K2HOYVSV.js";
9
9
  import "./chunk-OY4QS3WL.js";
10
10
 
11
11
  // src/cli-main.ts
package/dist/index.cjs CHANGED
@@ -34,6 +34,11 @@ __export(index_exports, {
34
34
  InMemoryStickyBucketStore: () => InMemoryStickyBucketStore,
35
35
  LocalStorageStickyBucketStore: () => LocalStorageStickyBucketStore,
36
36
  NUMBER_OPERATORS: () => NUMBER_OPERATORS,
37
+ OVERRIDES_ENV: () => OVERRIDES_ENV,
38
+ OVERRIDES_FILE_ENV: () => OVERRIDES_FILE_ENV,
39
+ OVERRIDES_GLOBAL: () => OVERRIDES_GLOBAL,
40
+ OVERRIDE_BEHAVIOUR_ENV: () => OVERRIDE_BEHAVIOUR_ENV,
41
+ OVERRIDE_BEHAVIOUR_GLOBAL: () => OVERRIDE_BEHAVIOUR_GLOBAL,
37
42
  PRESENCE_OPERATORS: () => PRESENCE_OPERATORS,
38
43
  SEMVER_OPERATORS: () => SEMVER_OPERATORS,
39
44
  TEXT_OPERATORS: () => TEXT_OPERATORS,
@@ -56,8 +61,10 @@ __export(index_exports, {
56
61
  jsonEquals: () => jsonEquals,
57
62
  lookupAttribute: () => lookupAttribute,
58
63
  numberOf: () => numberOf,
64
+ parseOverrides: () => parseOverrides,
59
65
  parseSemver: () => parseSemver,
60
66
  parseUrlOverrides: () => parseUrlOverrides,
67
+ readDevOverrides: () => readDevOverrides,
61
68
  sha256Hex: () => sha256Hex,
62
69
  stableStringify: () => stableStringify,
63
70
  textOf: () => textOf
@@ -893,14 +900,25 @@ function compact(attrs) {
893
900
  return out;
894
901
  }
895
902
  function parseUrlOverrides(search, param = "guidely_flags") {
896
- const out = {};
897
903
  let raw = null;
898
904
  try {
899
905
  raw = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search).get(param);
900
906
  } catch {
901
- return out;
907
+ return {};
908
+ }
909
+ return raw && parseOverrides(raw) || {};
910
+ }
911
+ function parseOverrides(text) {
912
+ const raw = text.trim();
913
+ if (raw.startsWith("{")) {
914
+ try {
915
+ const obj = JSON.parse(raw);
916
+ return obj && typeof obj === "object" && !Array.isArray(obj) ? obj : null;
917
+ } catch {
918
+ return null;
919
+ }
902
920
  }
903
- if (!raw) return out;
921
+ const out = {};
904
922
  const items = [];
905
923
  let depth = 0;
906
924
  let quoted = false;
@@ -913,7 +931,7 @@ function parseUrlOverrides(search, param = "guidely_flags") {
913
931
  } else if (c === '"') quoted = true;
914
932
  else if (c === "[" || c === "{") depth++;
915
933
  else if (c === "]" || c === "}") depth = Math.max(0, depth - 1);
916
- else if (c === "," && depth === 0) {
934
+ else if ((c === "," || c === "\n") && depth === 0) {
917
935
  items.push(raw.slice(start, i));
918
936
  start = i + 1;
919
937
  }
@@ -923,12 +941,12 @@ function parseUrlOverrides(search, param = "guidely_flags") {
923
941
  const sep = item.indexOf(":");
924
942
  if (sep <= 0) continue;
925
943
  const key = item.slice(0, sep).trim();
926
- const text = item.slice(sep + 1).trim();
944
+ const text2 = item.slice(sep + 1).trim();
927
945
  if (!key) continue;
928
946
  try {
929
- out[key] = JSON.parse(text);
947
+ out[key] = JSON.parse(text2);
930
948
  } catch {
931
- out[key] = text;
949
+ out[key] = text2;
932
950
  }
933
951
  }
934
952
  return out;
@@ -947,6 +965,74 @@ function timer(fn, ms) {
947
965
  return t;
948
966
  }
949
967
 
968
+ // src/dev-overrides.ts
969
+ var OVERRIDES_ENV = "GUIDELY_FLAG_OVERRIDES";
970
+ var OVERRIDES_FILE_ENV = "GUIDELY_FLAG_OVERRIDES_FILE";
971
+ var OVERRIDE_BEHAVIOUR_ENV = "GUIDELY_FLAG_OVERRIDE_BEHAVIOUR";
972
+ var OVERRIDES_GLOBAL = "__GUIDELY_FLAG_OVERRIDES__";
973
+ var OVERRIDE_BEHAVIOUR_GLOBAL = "__GUIDELY_FLAG_OVERRIDE_BEHAVIOUR__";
974
+ var BEHAVIOURS = ["localOnly", "localOverRemote", "remoteOverLocal"];
975
+ var runtime = () => globalThis;
976
+ function env(name) {
977
+ const g = runtime();
978
+ try {
979
+ const v = g.process?.env?.[name];
980
+ if (v !== void 0) return v;
981
+ } catch {
982
+ }
983
+ try {
984
+ return g.Deno?.env?.get(name);
985
+ } catch {
986
+ return void 0;
987
+ }
988
+ }
989
+ function readFile(path) {
990
+ const g = runtime();
991
+ const fs = g.process?.getBuiltinModule?.("node:fs");
992
+ if (fs) return fs.readFileSync(path, "utf8");
993
+ if (g.Deno?.readTextFileSync) return g.Deno.readTextFileSync(path);
994
+ throw new Error("this runtime cannot read files");
995
+ }
996
+ function behaviourOf(raw, source, out) {
997
+ if (typeof raw !== "string" || !raw.trim()) return;
998
+ const b = raw.trim();
999
+ if (BEHAVIOURS.includes(b)) {
1000
+ out.behaviour = b;
1001
+ out.sources.push(source);
1002
+ } else out.errors.push(`${source} must be localOnly, localOverRemote or remoteOverLocal (got "${raw}"); ignoring it.`);
1003
+ }
1004
+ function merge(text, source, out) {
1005
+ if (text === void 0 || !text.trim()) return;
1006
+ const values = parseOverrides(text);
1007
+ if (!values) return void out.errors.push(`${source} is not a JSON object or a key:value list; ignoring it.`);
1008
+ if (!Object.keys(values).length) return;
1009
+ Object.assign(out.values, values);
1010
+ out.sources.push(source);
1011
+ }
1012
+ function readDevOverrides() {
1013
+ const out = { values: {}, sources: [], errors: [] };
1014
+ const file = env(OVERRIDES_FILE_ENV);
1015
+ if (file?.trim()) {
1016
+ try {
1017
+ merge(readFile(file.trim()), `${OVERRIDES_FILE_ENV} (${file.trim()})`, out);
1018
+ } catch (err) {
1019
+ out.errors.push(`Could not read ${OVERRIDES_FILE_ENV} (${file.trim()}): ${err instanceof Error ? err.message : String(err)}; ignoring it.`);
1020
+ }
1021
+ }
1022
+ merge(env(OVERRIDES_ENV), OVERRIDES_ENV, out);
1023
+ const g = runtime()[OVERRIDES_GLOBAL];
1024
+ if (g && typeof g === "object" && !Array.isArray(g)) {
1025
+ if (Object.keys(g).length) {
1026
+ Object.assign(out.values, g);
1027
+ out.sources.push(OVERRIDES_GLOBAL);
1028
+ }
1029
+ } else if (typeof g === "string") merge(g, OVERRIDES_GLOBAL, out);
1030
+ else if (g !== void 0 && g !== null) out.errors.push(`${OVERRIDES_GLOBAL} must be an object or overrides text; ignoring it.`);
1031
+ behaviourOf(env(OVERRIDE_BEHAVIOUR_ENV), OVERRIDE_BEHAVIOUR_ENV, out);
1032
+ behaviourOf(runtime()[OVERRIDE_BEHAVIOUR_GLOBAL], OVERRIDE_BEHAVIOUR_GLOBAL, out);
1033
+ return out;
1034
+ }
1035
+
950
1036
  // src/evaluator.ts
951
1037
  function typeMatches(value, fallback) {
952
1038
  if (value === null || value === void 0) return false;
@@ -1037,7 +1123,7 @@ function createSseParser(onEvent) {
1037
1123
 
1038
1124
  // src/client.ts
1039
1125
  var FLAGS_SDK_NAME = "@guidelytech/flags";
1040
- var FLAGS_SDK_VERSION = "0.7.0";
1126
+ var FLAGS_SDK_VERSION = "0.9.0";
1041
1127
  var DEFAULT_FLAGS_API_URL = "https://guidelytech.com";
1042
1128
  var SECRET_PREFIX = "gly_sk_";
1043
1129
  var MAX_RESULT_CACHE = 20;
@@ -1084,6 +1170,7 @@ var FlagsClient = class {
1084
1170
  __publicField(this, "context");
1085
1171
  __publicField(this, "attributes");
1086
1172
  __publicField(this, "overrides");
1173
+ __publicField(this, "devOverrides");
1087
1174
  __publicField(this, "urlOverrides");
1088
1175
  __publicField(this, "offline");
1089
1176
  __publicField(this, "destroyed", false);
@@ -1125,12 +1212,13 @@ var FlagsClient = class {
1125
1212
  this.log = o.logger === null ? {} : o.logger ?? { warn: (...a) => console.warn("[Guidely flags]", ...a), error: (...a) => console.error("[Guidely flags]", ...a) };
1126
1213
  if (this.secret && !o.environment) throw new Error("[Guidely flags] `environment` (development | staging | production) is required with a gly_sk_ key.");
1127
1214
  if (this.secret && isBrowser()) this.log.warn?.("A gly_sk_ secret key is being used in a browser. Secret keys belong on servers; use the environment's public pk_ key here.");
1215
+ const dev = o.devOverrides === false ? null : readDevOverrides();
1128
1216
  this.cfg = {
1129
1217
  mode: o.pollingMode ?? "auto",
1130
1218
  pollMs: Math.max(1, o.pollIntervalSeconds ?? 60) * 1e3,
1131
1219
  ttlMs: Math.max(0, o.cacheTtlSeconds ?? 60) * 1e3,
1132
1220
  debounceMs: Math.max(0, o.remoteEvalDebounceMs ?? 50),
1133
- behaviour: o.overrideBehaviour ?? "localOverRemote",
1221
+ behaviour: dev?.behaviour ?? o.overrideBehaviour ?? "localOverRemote",
1134
1222
  trackExposures: o.trackExposures ?? !o.trackingCallback,
1135
1223
  reportUsage: o.reportUsage ?? true
1136
1224
  };
@@ -1139,10 +1227,16 @@ var FlagsClient = class {
1139
1227
  this.context = compact(o.contextAttributes ?? {});
1140
1228
  this.attributes = compact(o.attributes ?? {});
1141
1229
  this.overrides = { ...o.overrides ?? {} };
1230
+ this.devOverrides = dev?.values ?? {};
1142
1231
  this.urlOverrides = o.enableUrlOverrides && isBrowser() ? parseUrlOverrides(location.search) : {};
1143
1232
  this.storage = o.storage === false || o.storage === null ? null : o.storage ?? defaultStorage();
1144
1233
  this.storageKey = `guidely:flags:${key}${o.environment && this.secret ? `:${o.environment}` : ""}`;
1145
1234
  this.readyPromise = new Promise((r) => this.resolveReady = r);
1235
+ if (dev) {
1236
+ dev.errors.forEach((e) => this.reportError(new Error(e)));
1237
+ const n = Object.keys(dev.values).length;
1238
+ if (dev.sources.length) this.log.warn?.(`Dev overrides from ${dev.sources.join(", ")}: ${n} flag${n === 1 ? "" : "s"}, behaviour ${this.cfg.behaviour}.`);
1239
+ }
1146
1240
  this.getters = makeEvaluator(
1147
1241
  (k) => this.resolve(k, this.getAttributes(), true),
1148
1242
  () => this.all(this.getAttributes()),
@@ -1224,7 +1318,7 @@ var FlagsClient = class {
1224
1318
  getAttributes() {
1225
1319
  return { ...this.context, ...this.attributes };
1226
1320
  }
1227
- /** Replaces the overrides (`null` clears them). URL overrides stay. */
1321
+ /** Replaces the overrides (`null` clears them). Dev and URL overrides stay. */
1228
1322
  setOverrides(overrides) {
1229
1323
  this.overrides = { ...overrides ?? {} };
1230
1324
  this.notify();
@@ -1301,6 +1395,7 @@ var FlagsClient = class {
1301
1395
  // ---------------------------------------------------------------------------
1302
1396
  overrideFor(key) {
1303
1397
  if (Object.prototype.hasOwnProperty.call(this.urlOverrides, key)) return this.urlOverrides[key];
1398
+ if (Object.prototype.hasOwnProperty.call(this.devOverrides, key)) return this.devOverrides[key];
1304
1399
  if (Object.prototype.hasOwnProperty.call(this.overrides, key)) return this.overrides[key];
1305
1400
  return void 0;
1306
1401
  }
@@ -1312,7 +1407,6 @@ var FlagsClient = class {
1312
1407
  const forced = Object.prototype.hasOwnProperty.call(this.urlOverrides, key);
1313
1408
  const override = ov === void 0 ? null : { key, value: ov, reason: "OVERRIDE", inExperiment: false };
1314
1409
  if (override && (forced || this.cfg.behaviour !== "remoteOverLocal")) res = override;
1315
- else if (this.cfg.behaviour === "localOnly") res = notFound(key);
1316
1410
  else {
1317
1411
  res = this.evaluateRemoteOrLocal(key, attrs);
1318
1412
  if (override && res.reason === "FLAG_NOT_FOUND") res = override;
@@ -1333,11 +1427,9 @@ var FlagsClient = class {
1333
1427
  return evaluateFlag(this.payload, key, attrs, { now: this.now(), stickyBuckets: this.o.stickyBucketService });
1334
1428
  }
1335
1429
  all(attrs) {
1336
- const keys = /* @__PURE__ */ new Set([...Object.keys(this.overrides), ...Object.keys(this.urlOverrides)]);
1337
- if (this.cfg.behaviour !== "localOnly") {
1338
- if (this.remote) for (const k of Object.keys(this.results?.flags ?? {})) keys.add(k);
1339
- else for (const k of Object.keys(this.payload?.flags ?? {})) keys.add(k);
1340
- }
1430
+ const keys = /* @__PURE__ */ new Set([...Object.keys(this.overrides), ...Object.keys(this.devOverrides), ...Object.keys(this.urlOverrides)]);
1431
+ if (this.remote) for (const k of Object.keys(this.results?.flags ?? {})) keys.add(k);
1432
+ else for (const k of Object.keys(this.payload?.flags ?? {})) keys.add(k);
1341
1433
  const out = {};
1342
1434
  for (const k of keys) {
1343
1435
  const v = this.resolve(k, attrs, false).value;
@@ -1584,7 +1676,7 @@ var FlagsClient = class {
1584
1676
  async start() {
1585
1677
  const boot = this.o.payload;
1586
1678
  if (isEncryptedPayload(boot)) await this.applyRaw(boot, null);
1587
- if (!this.payload && !this.results && !this.remote) await this.loadStored();
1679
+ if (!this.payload && !this.results && !this.remote && this.cfg.behaviour !== "localOnly") await this.loadStored();
1588
1680
  if (this.destroyed) return;
1589
1681
  const bootstrapped = !!(boot || this.o.remoteResults) && !!(this.payload || this.results);
1590
1682
  if (this.cfg.behaviour === "localOnly" || this.offline || this.cfg.mode === "manual") return this.settle();
@@ -1730,6 +1822,11 @@ var FlagsClient = class {
1730
1822
  }
1731
1823
  takeBatch() {
1732
1824
  if (!this.usage.size && !this.exposures.length) return null;
1825
+ if (this.cfg.behaviour === "localOnly") {
1826
+ this.usage.clear();
1827
+ this.exposures = [];
1828
+ return null;
1829
+ }
1733
1830
  const batch = { sdk: this.o.sdk ?? { name: FLAGS_SDK_NAME, version: FLAGS_SDK_VERSION } };
1734
1831
  if (this.usage.size) batch.usage = Array.from(this.usage.values());
1735
1832
  if (this.exposures.length) batch.exposures = this.exposures;
@@ -1853,6 +1950,11 @@ var LocalStorageStickyBucketStore = class {
1853
1950
  InMemoryStickyBucketStore,
1854
1951
  LocalStorageStickyBucketStore,
1855
1952
  NUMBER_OPERATORS,
1953
+ OVERRIDES_ENV,
1954
+ OVERRIDES_FILE_ENV,
1955
+ OVERRIDES_GLOBAL,
1956
+ OVERRIDE_BEHAVIOUR_ENV,
1957
+ OVERRIDE_BEHAVIOUR_GLOBAL,
1856
1958
  PRESENCE_OPERATORS,
1857
1959
  SEMVER_OPERATORS,
1858
1960
  TEXT_OPERATORS,
@@ -1875,8 +1977,10 @@ var LocalStorageStickyBucketStore = class {
1875
1977
  jsonEquals,
1876
1978
  lookupAttribute,
1877
1979
  numberOf,
1980
+ parseOverrides,
1878
1981
  parseSemver,
1879
1982
  parseUrlOverrides,
1983
+ readDevOverrides,
1880
1984
  sha256Hex,
1881
1985
  stableStringify,
1882
1986
  textOf