flagmint-js-sdk 2.2.2 → 2.2.3-rc.1

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 |
@@ -38592,12 +38592,27 @@ function parseRulesSnapshot$1(raw) {
38592
38592
  if (!parsed || typeof parsed.version !== "number" || typeof parsed.expiresAt !== "number" || !Array.isArray(parsed.flags) || typeof parsed.segments !== "object" || parsed.segments === null) {
38593
38593
  return null;
38594
38594
  }
38595
- return {
38595
+ const snapshot = {
38596
38596
  version: parsed.version,
38597
38597
  expiresAt: parsed.expiresAt,
38598
38598
  flags: parsed.flags,
38599
38599
  segments: parsed.segments || {}
38600
38600
  };
38601
+ if (Array.isArray(parsed.piiAttributes)) {
38602
+ snapshot.piiAttributes = parsed.piiAttributes.filter(
38603
+ (attr) => typeof attr === "string"
38604
+ );
38605
+ }
38606
+ if (typeof parsed.piiSalt === "string") {
38607
+ snapshot.piiSalt = parsed.piiSalt;
38608
+ }
38609
+ if (typeof parsed.piiValueEncoding === "string") {
38610
+ snapshot.piiValueEncoding = parsed.piiValueEncoding;
38611
+ }
38612
+ if (parsed.piiWireBroken === true) {
38613
+ snapshot.piiWireBroken = true;
38614
+ }
38615
+ return snapshot;
38601
38616
  } catch {
38602
38617
  return null;
38603
38618
  }
@@ -38621,6 +38636,12 @@ function saveCachedFlags$1(apiKey, data) {
38621
38636
  } catch {
38622
38637
  }
38623
38638
  }
38639
+ function clearCachedFlags(apiKey) {
38640
+ try {
38641
+ storage$1.removeItem?.(getFlagsKey$1(apiKey));
38642
+ } catch {
38643
+ }
38644
+ }
38624
38645
  function loadCachedContext$1(apiKey) {
38625
38646
  try {
38626
38647
  const raw = storage$1.getItem(getContextKey$1(apiKey));
@@ -38659,6 +38680,7 @@ function clearCachedRulesSnapshot$1(apiKey) {
38659
38680
  }
38660
38681
  const cacheHelper = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
38661
38682
  __proto__: null,
38683
+ clearCachedFlags,
38662
38684
  clearCachedRulesSnapshot: clearCachedRulesSnapshot$1,
38663
38685
  loadCachedContext: loadCachedContext$1,
38664
38686
  loadCachedFlags: loadCachedFlags$1,
@@ -39093,7 +39115,7 @@ class SseTransport {
39093
39115
  this.connectPromise = new Promise((resolve, reject) => {
39094
39116
  this.connectResolve = resolve;
39095
39117
  this.connectReject = reject;
39096
- const sdkVersion = "2.2.2";
39118
+ const sdkVersion = "2.2.3-rc.1";
39097
39119
  const platform2 = typeof window !== "undefined" ? "browser" : "nodejs";
39098
39120
  const wrapperName = this.configOptions?.wrapper?.name || "native-js";
39099
39121
  const wrapperVersion = this.configOptions?.wrapper?.version || "none";
@@ -41905,22 +41927,80 @@ function payloadHasExplicitPiiFields(payload) {
41905
41927
  return false;
41906
41928
  return payload.piiSalt !== void 0 || payload.piiAttributes !== void 0 || payload.piiValueEncoding !== void 0;
41907
41929
  }
41930
+ function piiAttributeKeysFromPayload(payload) {
41931
+ if (!payload || !Array.isArray(payload.piiAttributes))
41932
+ return [];
41933
+ return payload.piiAttributes.filter((k) => typeof k === "string" && k.length > 0);
41934
+ }
41935
+ function hasMalformedPiiAttributes(payload) {
41936
+ if (!payload || payload.piiAttributes === void 0)
41937
+ return false;
41938
+ if (!Array.isArray(payload.piiAttributes))
41939
+ return true;
41940
+ return payload.piiAttributes.some(
41941
+ (entry) => typeof entry !== "string" || entry.length === 0
41942
+ );
41943
+ }
41944
+ function hasExplicitInvalidPiiEncoding(payload) {
41945
+ if (!payload || payload.piiValueEncoding === void 0)
41946
+ return false;
41947
+ return payload.piiValueEncoding !== PII_VALUE_ENCODING;
41948
+ }
41949
+ function payloadRequiresUsablePiiWire(payload) {
41950
+ if (!payload)
41951
+ return false;
41952
+ if (hasExplicitInvalidPiiEncoding(payload))
41953
+ return true;
41954
+ if (hasMalformedPiiAttributes(payload))
41955
+ return true;
41956
+ if (piiAttributeKeysFromPayload(payload).length > 0)
41957
+ return true;
41958
+ return false;
41959
+ }
41960
+ function isExplicitPiiRequirementCleared(payload) {
41961
+ if (!payload || payload.piiAttributes === void 0)
41962
+ return false;
41963
+ if (!Array.isArray(payload.piiAttributes))
41964
+ return false;
41965
+ if (payload.piiAttributes.length !== 0)
41966
+ return false;
41967
+ if (hasExplicitInvalidPiiEncoding(payload))
41968
+ return false;
41969
+ return true;
41970
+ }
41908
41971
  function piiWireOptionsFromPayload(payload) {
41909
41972
  if (!payload)
41910
41973
  return null;
41974
+ if (hasExplicitInvalidPiiEncoding(payload))
41975
+ return null;
41976
+ if (hasMalformedPiiAttributes(payload))
41977
+ return null;
41911
41978
  const salt = typeof payload.piiSalt === "string" ? payload.piiSalt : "";
41912
41979
  if (!salt)
41913
41980
  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") : [];
41981
+ const attrs = piiAttributeKeysFromPayload(payload);
41918
41982
  return {
41919
41983
  piiAttributes: attrs,
41920
41984
  piiSalt: salt,
41921
41985
  piiValueEncoding: PII_VALUE_ENCODING
41922
41986
  };
41923
41987
  }
41988
+ function isPiiWireBroken(payload, resolved, previousPriorHadPiiAttrs = false) {
41989
+ if (hasExplicitInvalidPiiEncoding(payload) || hasMalformedPiiAttributes(payload)) {
41990
+ return true;
41991
+ }
41992
+ if (resolved) {
41993
+ return false;
41994
+ }
41995
+ if (isExplicitPiiRequirementCleared(payload))
41996
+ return false;
41997
+ if (payloadRequiresUsablePiiWire(payload))
41998
+ return true;
41999
+ if (previousPriorHadPiiAttrs && payloadHasExplicitPiiFields(payload)) {
42000
+ return true;
42001
+ }
42002
+ return false;
42003
+ }
41924
42004
  function resolvePiiWireForDelta(previous, action) {
41925
42005
  if (!payloadHasExplicitPiiFields(action)) {
41926
42006
  return previous;
@@ -41973,7 +42053,8 @@ function createEmptyRulesState() {
41973
42053
  segments: /* @__PURE__ */ new Map(),
41974
42054
  ready: false,
41975
42055
  needsFullConfig: true,
41976
- piiWire: null
42056
+ piiWire: null,
42057
+ piiWireBroken: false
41977
42058
  };
41978
42059
  }
41979
42060
  function cloneState(state) {
@@ -41988,7 +42069,8 @@ function cloneState(state) {
41988
42069
  piiAttributes: [...state.piiWire.piiAttributes],
41989
42070
  piiSalt: state.piiWire.piiSalt,
41990
42071
  piiValueEncoding: state.piiWire.piiValueEncoding
41991
- } : null
42072
+ } : null,
42073
+ piiWireBroken: state.piiWireBroken
41992
42074
  };
41993
42075
  }
41994
42076
  function segmentsFromRecord(record) {
@@ -42015,14 +42097,18 @@ function hydrateFromFull(action) {
42015
42097
  if (flag?.key)
42016
42098
  flags.set(flag.key, flag);
42017
42099
  }
42100
+ const piiWire = piiWireOptionsFromPayload(action);
42101
+ const piiWireBroken = isPiiWireBroken(action, piiWire, false);
42018
42102
  return {
42019
42103
  version: action.version,
42020
42104
  expiresAt: action.expiresAt,
42021
42105
  flags,
42022
42106
  segments: segmentsFromRecord(action.segments),
42023
- ready: true,
42024
- needsFullConfig: false,
42025
- piiWire: piiWireOptionsFromPayload(action)
42107
+ // Usable rules, but PII metadata must be valid when hashing is required.
42108
+ ready: !piiWireBroken,
42109
+ needsFullConfig: piiWireBroken,
42110
+ piiWire,
42111
+ piiWireBroken
42026
42112
  };
42027
42113
  }
42028
42114
  function applyPatch(state, action) {
@@ -42035,14 +42121,19 @@ function applyPatch(state, action) {
42035
42121
  flags.set(flag.key, flag);
42036
42122
  }
42037
42123
  const nextPii = resolvePiiWireForDelta(state.piiWire, action);
42124
+ const priorHadPiiAttrs = (state.piiWire?.piiAttributes.length ?? 0) > 0 || state.piiWireBroken;
42125
+ const computedBroken = isPiiWireBroken(action, nextPii, priorHadPiiAttrs);
42126
+ const recoveredWithRequiredAttrs = !!nextPii && nextPii.piiAttributes.length > 0;
42127
+ const piiWireBroken = isExplicitPiiRequirementCleared(action) ? false : recoveredWithRequiredAttrs ? false : computedBroken || state.piiWireBroken;
42038
42128
  return {
42039
42129
  version: action.toVersion,
42040
42130
  expiresAt: typeof action.expiresAt === "number" ? action.expiresAt : state.expiresAt,
42041
42131
  flags,
42042
42132
  segments: applySegmentUpserts(state.segments, action.segments),
42043
- ready: true,
42044
- needsFullConfig: false,
42045
- piiWire: nextPii
42133
+ ready: !piiWireBroken,
42134
+ needsFullConfig: piiWireBroken,
42135
+ piiWire: nextPii,
42136
+ piiWireBroken
42046
42137
  };
42047
42138
  }
42048
42139
  function reduceRules(state, action, now = Date.now()) {
@@ -42062,8 +42153,8 @@ function reduceRules(state, action, now = Date.now()) {
42062
42153
  ...cloneState(state),
42063
42154
  version: state.version,
42064
42155
  expiresAt: action.expiresAt,
42065
- ready: true,
42066
- needsFullConfig: state.flags.size === 0 || versionMismatch ? true : state.needsFullConfig
42156
+ ready: !state.piiWireBroken,
42157
+ needsFullConfig: state.piiWireBroken || state.flags.size === 0 || versionMismatch ? true : state.needsFullConfig
42067
42158
  }
42068
42159
  };
42069
42160
  }
@@ -42119,8 +42210,8 @@ function reduceRules(state, action, now = Date.now()) {
42119
42210
  ...current,
42120
42211
  version: action.toVersion,
42121
42212
  expiresAt: typeof action.expiresAt === "number" ? action.expiresAt : current.expiresAt,
42122
- ready: true,
42123
- needsFullConfig: false
42213
+ ready: !current.piiWireBroken,
42214
+ needsFullConfig: current.piiWireBroken
42124
42215
  }
42125
42216
  };
42126
42217
  }
@@ -42143,9 +42234,12 @@ class RulesStore {
42143
42234
  return this.state;
42144
42235
  }
42145
42236
  /**
42146
- * Fail-closed readiness: lease not expired and store marked ready.
42237
+ * Fail-closed readiness: lease not expired, store marked ready, and PII
42238
+ * wire metadata usable when hashing is required.
42147
42239
  */
42148
42240
  isReady(now = Date.now()) {
42241
+ if (this.state.piiWireBroken)
42242
+ return false;
42149
42243
  if (!this.state.ready)
42150
42244
  return false;
42151
42245
  if (!this.state.expiresAt)
@@ -42229,7 +42323,9 @@ class RulesStore {
42229
42323
  piiAttributes: [...pii.piiAttributes],
42230
42324
  piiSalt: pii.piiSalt,
42231
42325
  piiValueEncoding: pii.piiValueEncoding
42232
- } : {}
42326
+ } : {},
42327
+ // Persist fail-closed even when piiWire is null (unusable salt/encoding).
42328
+ ...this.state.piiWireBroken ? { piiWireBroken: true } : {}
42233
42329
  };
42234
42330
  }
42235
42331
  hydrateFromSnapshot(snapshot, now = Date.now()) {
@@ -42239,14 +42335,17 @@ class RulesStore {
42239
42335
  flags.set(flag.key, flag);
42240
42336
  }
42241
42337
  const expired = isConfigPayloadExpired(snapshot.expiresAt, now);
42338
+ const piiWire = piiWireOptionsFromPayload(snapshot);
42339
+ const piiWireBroken = snapshot.piiWireBroken === true || isPiiWireBroken(snapshot, piiWire, false);
42242
42340
  this.state = {
42243
42341
  version: snapshot.version,
42244
42342
  expiresAt: snapshot.expiresAt,
42245
42343
  flags,
42246
42344
  segments: segmentsFromRecord(snapshot.segments),
42247
- ready: !expired && flags.size > 0,
42248
- needsFullConfig: expired || flags.size === 0,
42249
- piiWire: piiWireOptionsFromPayload(snapshot)
42345
+ ready: !expired && flags.size > 0 && !piiWireBroken,
42346
+ needsFullConfig: expired || flags.size === 0 || piiWireBroken,
42347
+ piiWire,
42348
+ piiWireBroken
42250
42349
  };
42251
42350
  }
42252
42351
  }
@@ -43217,9 +43316,10 @@ class FlagClient {
43217
43316
  }
43218
43317
  let hydratedRules = false;
43219
43318
  if (this.configSync && this.rulesStore && this.enableOfflineCache) {
43319
+ clearCachedFlags(this.apiKey);
43220
43320
  hydratedRules = await this.hydrateRulesFromCache();
43221
43321
  }
43222
- if (this.enableOfflineCache && !hydratedRules) {
43322
+ if (this.enableOfflineCache && !this.configSync && !hydratedRules) {
43223
43323
  const cached = await Promise.resolve(
43224
43324
  this.cacheAdapter.loadFlags(this.apiKey, this.cacheTTL)
43225
43325
  );
@@ -43461,7 +43561,7 @@ class FlagClient {
43461
43561
  */
43462
43562
  updateFlags(newFlags, fromShare = false) {
43463
43563
  this.flags = newFlags;
43464
- if (this.enableOfflineCache) {
43564
+ if (this.enableOfflineCache && !this.configSync) {
43465
43565
  void Promise.resolve(
43466
43566
  this.cacheAdapter.saveFlags(this.apiKey, newFlags)
43467
43567
  );