flagmint-js-sdk 2.2.2 → 2.2.3-rc.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/README.md CHANGED
@@ -133,7 +133,7 @@ After `ready()`:
133
133
  | `transportMode` | `'auto' \| 'sse' \| 'long-polling'` | `'auto'` | `auto` tries SSE, then long-polling |
134
134
  | `EventSourceImpl` | EventSource | browser global | Required in Node |
135
135
  | `shareConnection` | boolean | `true` in browsers with `BroadcastChannel`, `false` in Node | One stream per API key across same-origin documents |
136
- | `enableOfflineCache` | boolean | `true` | Load/save flags via the cache adapter |
136
+ | `enableOfflineCache` | boolean | `true` | Server-eval: load/save evaluated flags. Config-sync: load/save **rules** only (evaluated maps are not cached — they are per-user and would collide across tabs). |
137
137
  | `persistContext` | boolean | `false` | Persist context in the cache adapter |
138
138
  | `cacheAdapter` | CacheAdapter | localStorage helpers | Custom cache (Redis, files, …) |
139
139
  | `restEndpoint` | string | env default | Override the long-polling / evaluate URL |
@@ -38621,6 +38621,12 @@ function saveCachedFlags$1(apiKey, data) {
38621
38621
  } catch {
38622
38622
  }
38623
38623
  }
38624
+ function clearCachedFlags(apiKey) {
38625
+ try {
38626
+ storage$1.removeItem?.(getFlagsKey$1(apiKey));
38627
+ } catch {
38628
+ }
38629
+ }
38624
38630
  function loadCachedContext$1(apiKey) {
38625
38631
  try {
38626
38632
  const raw = storage$1.getItem(getContextKey$1(apiKey));
@@ -38659,6 +38665,7 @@ function clearCachedRulesSnapshot$1(apiKey) {
38659
38665
  }
38660
38666
  const cacheHelper = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
38661
38667
  __proto__: null,
38668
+ clearCachedFlags,
38662
38669
  clearCachedRulesSnapshot: clearCachedRulesSnapshot$1,
38663
38670
  loadCachedContext: loadCachedContext$1,
38664
38671
  loadCachedFlags: loadCachedFlags$1,
@@ -39093,7 +39100,7 @@ class SseTransport {
39093
39100
  this.connectPromise = new Promise((resolve, reject) => {
39094
39101
  this.connectResolve = resolve;
39095
39102
  this.connectReject = reject;
39096
- const sdkVersion = "2.2.2";
39103
+ const sdkVersion = "2.2.3-rc.0";
39097
39104
  const platform2 = typeof window !== "undefined" ? "browser" : "nodejs";
39098
39105
  const wrapperName = this.configOptions?.wrapper?.name || "native-js";
39099
39106
  const wrapperVersion = this.configOptions?.wrapper?.version || "none";
@@ -41905,22 +41912,80 @@ function payloadHasExplicitPiiFields(payload) {
41905
41912
  return false;
41906
41913
  return payload.piiSalt !== void 0 || payload.piiAttributes !== void 0 || payload.piiValueEncoding !== void 0;
41907
41914
  }
41915
+ function piiAttributeKeysFromPayload(payload) {
41916
+ if (!payload || !Array.isArray(payload.piiAttributes))
41917
+ return [];
41918
+ return payload.piiAttributes.filter((k) => typeof k === "string" && k.length > 0);
41919
+ }
41920
+ function hasMalformedPiiAttributes(payload) {
41921
+ if (!payload || payload.piiAttributes === void 0)
41922
+ return false;
41923
+ if (!Array.isArray(payload.piiAttributes))
41924
+ return true;
41925
+ return payload.piiAttributes.some(
41926
+ (entry) => typeof entry !== "string" || entry.length === 0
41927
+ );
41928
+ }
41929
+ function hasExplicitInvalidPiiEncoding(payload) {
41930
+ if (!payload || payload.piiValueEncoding === void 0)
41931
+ return false;
41932
+ return payload.piiValueEncoding !== PII_VALUE_ENCODING;
41933
+ }
41934
+ function payloadRequiresUsablePiiWire(payload) {
41935
+ if (!payload)
41936
+ return false;
41937
+ if (hasExplicitInvalidPiiEncoding(payload))
41938
+ return true;
41939
+ if (hasMalformedPiiAttributes(payload))
41940
+ return true;
41941
+ if (piiAttributeKeysFromPayload(payload).length > 0)
41942
+ return true;
41943
+ return false;
41944
+ }
41945
+ function isExplicitPiiRequirementCleared(payload) {
41946
+ if (!payload || payload.piiAttributes === void 0)
41947
+ return false;
41948
+ if (!Array.isArray(payload.piiAttributes))
41949
+ return false;
41950
+ if (payload.piiAttributes.length !== 0)
41951
+ return false;
41952
+ if (hasExplicitInvalidPiiEncoding(payload))
41953
+ return false;
41954
+ return true;
41955
+ }
41908
41956
  function piiWireOptionsFromPayload(payload) {
41909
41957
  if (!payload)
41910
41958
  return null;
41959
+ if (hasExplicitInvalidPiiEncoding(payload))
41960
+ return null;
41961
+ if (hasMalformedPiiAttributes(payload))
41962
+ return null;
41911
41963
  const salt = typeof payload.piiSalt === "string" ? payload.piiSalt : "";
41912
41964
  if (!salt)
41913
41965
  return null;
41914
- const encoding = typeof payload.piiValueEncoding === "string" ? payload.piiValueEncoding : PII_VALUE_ENCODING;
41915
- if (encoding !== PII_VALUE_ENCODING)
41916
- return null;
41917
- const attrs = Array.isArray(payload.piiAttributes) ? payload.piiAttributes.filter((k) => typeof k === "string") : [];
41966
+ const attrs = piiAttributeKeysFromPayload(payload);
41918
41967
  return {
41919
41968
  piiAttributes: attrs,
41920
41969
  piiSalt: salt,
41921
41970
  piiValueEncoding: PII_VALUE_ENCODING
41922
41971
  };
41923
41972
  }
41973
+ function isPiiWireBroken(payload, resolved, previousPriorHadPiiAttrs = false) {
41974
+ if (hasExplicitInvalidPiiEncoding(payload) || hasMalformedPiiAttributes(payload)) {
41975
+ return true;
41976
+ }
41977
+ if (resolved) {
41978
+ return false;
41979
+ }
41980
+ if (isExplicitPiiRequirementCleared(payload))
41981
+ return false;
41982
+ if (payloadRequiresUsablePiiWire(payload))
41983
+ return true;
41984
+ if (previousPriorHadPiiAttrs && payloadHasExplicitPiiFields(payload)) {
41985
+ return true;
41986
+ }
41987
+ return false;
41988
+ }
41924
41989
  function resolvePiiWireForDelta(previous, action) {
41925
41990
  if (!payloadHasExplicitPiiFields(action)) {
41926
41991
  return previous;
@@ -41973,7 +42038,8 @@ function createEmptyRulesState() {
41973
42038
  segments: /* @__PURE__ */ new Map(),
41974
42039
  ready: false,
41975
42040
  needsFullConfig: true,
41976
- piiWire: null
42041
+ piiWire: null,
42042
+ piiWireBroken: false
41977
42043
  };
41978
42044
  }
41979
42045
  function cloneState(state) {
@@ -41988,7 +42054,8 @@ function cloneState(state) {
41988
42054
  piiAttributes: [...state.piiWire.piiAttributes],
41989
42055
  piiSalt: state.piiWire.piiSalt,
41990
42056
  piiValueEncoding: state.piiWire.piiValueEncoding
41991
- } : null
42057
+ } : null,
42058
+ piiWireBroken: state.piiWireBroken
41992
42059
  };
41993
42060
  }
41994
42061
  function segmentsFromRecord(record) {
@@ -42015,14 +42082,18 @@ function hydrateFromFull(action) {
42015
42082
  if (flag?.key)
42016
42083
  flags.set(flag.key, flag);
42017
42084
  }
42085
+ const piiWire = piiWireOptionsFromPayload(action);
42086
+ const piiWireBroken = isPiiWireBroken(action, piiWire, false);
42018
42087
  return {
42019
42088
  version: action.version,
42020
42089
  expiresAt: action.expiresAt,
42021
42090
  flags,
42022
42091
  segments: segmentsFromRecord(action.segments),
42023
- ready: true,
42024
- needsFullConfig: false,
42025
- piiWire: piiWireOptionsFromPayload(action)
42092
+ // Usable rules, but PII metadata must be valid when hashing is required.
42093
+ ready: !piiWireBroken,
42094
+ needsFullConfig: piiWireBroken,
42095
+ piiWire,
42096
+ piiWireBroken
42026
42097
  };
42027
42098
  }
42028
42099
  function applyPatch(state, action) {
@@ -42035,14 +42106,19 @@ function applyPatch(state, action) {
42035
42106
  flags.set(flag.key, flag);
42036
42107
  }
42037
42108
  const nextPii = resolvePiiWireForDelta(state.piiWire, action);
42109
+ const priorHadPiiAttrs = (state.piiWire?.piiAttributes.length ?? 0) > 0 || state.piiWireBroken;
42110
+ const computedBroken = isPiiWireBroken(action, nextPii, priorHadPiiAttrs);
42111
+ const recoveredWithRequiredAttrs = !!nextPii && nextPii.piiAttributes.length > 0;
42112
+ const piiWireBroken = isExplicitPiiRequirementCleared(action) ? false : recoveredWithRequiredAttrs ? false : computedBroken || state.piiWireBroken;
42038
42113
  return {
42039
42114
  version: action.toVersion,
42040
42115
  expiresAt: typeof action.expiresAt === "number" ? action.expiresAt : state.expiresAt,
42041
42116
  flags,
42042
42117
  segments: applySegmentUpserts(state.segments, action.segments),
42043
- ready: true,
42044
- needsFullConfig: false,
42045
- piiWire: nextPii
42118
+ ready: !piiWireBroken,
42119
+ needsFullConfig: piiWireBroken,
42120
+ piiWire: nextPii,
42121
+ piiWireBroken
42046
42122
  };
42047
42123
  }
42048
42124
  function reduceRules(state, action, now = Date.now()) {
@@ -42062,8 +42138,8 @@ function reduceRules(state, action, now = Date.now()) {
42062
42138
  ...cloneState(state),
42063
42139
  version: state.version,
42064
42140
  expiresAt: action.expiresAt,
42065
- ready: true,
42066
- needsFullConfig: state.flags.size === 0 || versionMismatch ? true : state.needsFullConfig
42141
+ ready: !state.piiWireBroken,
42142
+ needsFullConfig: state.piiWireBroken || state.flags.size === 0 || versionMismatch ? true : state.needsFullConfig
42067
42143
  }
42068
42144
  };
42069
42145
  }
@@ -42119,8 +42195,8 @@ function reduceRules(state, action, now = Date.now()) {
42119
42195
  ...current,
42120
42196
  version: action.toVersion,
42121
42197
  expiresAt: typeof action.expiresAt === "number" ? action.expiresAt : current.expiresAt,
42122
- ready: true,
42123
- needsFullConfig: false
42198
+ ready: !current.piiWireBroken,
42199
+ needsFullConfig: current.piiWireBroken
42124
42200
  }
42125
42201
  };
42126
42202
  }
@@ -42143,9 +42219,12 @@ class RulesStore {
42143
42219
  return this.state;
42144
42220
  }
42145
42221
  /**
42146
- * Fail-closed readiness: lease not expired and store marked ready.
42222
+ * Fail-closed readiness: lease not expired, store marked ready, and PII
42223
+ * wire metadata usable when hashing is required.
42147
42224
  */
42148
42225
  isReady(now = Date.now()) {
42226
+ if (this.state.piiWireBroken)
42227
+ return false;
42149
42228
  if (!this.state.ready)
42150
42229
  return false;
42151
42230
  if (!this.state.expiresAt)
@@ -42229,7 +42308,9 @@ class RulesStore {
42229
42308
  piiAttributes: [...pii.piiAttributes],
42230
42309
  piiSalt: pii.piiSalt,
42231
42310
  piiValueEncoding: pii.piiValueEncoding
42232
- } : {}
42311
+ } : {},
42312
+ // Persist fail-closed even when piiWire is null (unusable salt/encoding).
42313
+ ...this.state.piiWireBroken ? { piiWireBroken: true } : {}
42233
42314
  };
42234
42315
  }
42235
42316
  hydrateFromSnapshot(snapshot, now = Date.now()) {
@@ -42239,14 +42320,17 @@ class RulesStore {
42239
42320
  flags.set(flag.key, flag);
42240
42321
  }
42241
42322
  const expired = isConfigPayloadExpired(snapshot.expiresAt, now);
42323
+ const piiWire = piiWireOptionsFromPayload(snapshot);
42324
+ const piiWireBroken = snapshot.piiWireBroken === true || isPiiWireBroken(snapshot, piiWire, false);
42242
42325
  this.state = {
42243
42326
  version: snapshot.version,
42244
42327
  expiresAt: snapshot.expiresAt,
42245
42328
  flags,
42246
42329
  segments: segmentsFromRecord(snapshot.segments),
42247
- ready: !expired && flags.size > 0,
42248
- needsFullConfig: expired || flags.size === 0,
42249
- piiWire: piiWireOptionsFromPayload(snapshot)
42330
+ ready: !expired && flags.size > 0 && !piiWireBroken,
42331
+ needsFullConfig: expired || flags.size === 0 || piiWireBroken,
42332
+ piiWire,
42333
+ piiWireBroken
42250
42334
  };
42251
42335
  }
42252
42336
  }
@@ -43217,9 +43301,10 @@ class FlagClient {
43217
43301
  }
43218
43302
  let hydratedRules = false;
43219
43303
  if (this.configSync && this.rulesStore && this.enableOfflineCache) {
43304
+ clearCachedFlags(this.apiKey);
43220
43305
  hydratedRules = await this.hydrateRulesFromCache();
43221
43306
  }
43222
- if (this.enableOfflineCache && !hydratedRules) {
43307
+ if (this.enableOfflineCache && !this.configSync && !hydratedRules) {
43223
43308
  const cached = await Promise.resolve(
43224
43309
  this.cacheAdapter.loadFlags(this.apiKey, this.cacheTTL)
43225
43310
  );
@@ -43461,7 +43546,7 @@ class FlagClient {
43461
43546
  */
43462
43547
  updateFlags(newFlags, fromShare = false) {
43463
43548
  this.flags = newFlags;
43464
- if (this.enableOfflineCache) {
43549
+ if (this.enableOfflineCache && !this.configSync) {
43465
43550
  void Promise.resolve(
43466
43551
  this.cacheAdapter.saveFlags(this.apiKey, newFlags)
43467
43552
  );