flagmint-js-sdk 2.2.2-rc.0 → 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 |
@@ -162,7 +162,7 @@ transportMode: 'long-polling' // POST evaluate on an interval
162
162
 
163
163
  # Connection sharing
164
164
 
165
- Used when the same app is embedded twice on one origin (for example two WeSeeDo iframes in a host page).
165
+ Used when the same app is embedded twice on one origin (for example two iframes in a host page).
166
166
 
167
167
  - **Leader** performs handshake and holds EventSource.
168
168
  - **Followers** receive flag snapshots over `BroadcastChannel` (`flagmint-share:<apiKey>`). Leadership is a `localStorage` lock (`flagmint_share_lock:<apiKey>`).
@@ -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-rc.0";
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";
@@ -41873,7 +41880,6 @@ function isConfigPayloadExpired(expiresAt, now = Date.now()) {
41873
41880
  }
41874
41881
  const PII_VALUE_ENCODING = "hmac-v1";
41875
41882
  const textEncoder = new TextEncoder();
41876
- const HASH_HEX_PATTERN = /^[0-9a-f]{64}$/;
41877
41883
  function normalizePiiString(value) {
41878
41884
  return value.trim().toLowerCase();
41879
41885
  }
@@ -41901,34 +41907,109 @@ function isPiiAttribute(attribute, piiAttributes) {
41901
41907
  }
41902
41908
  return false;
41903
41909
  }
41910
+ function payloadHasExplicitPiiFields(payload) {
41911
+ if (!payload)
41912
+ return false;
41913
+ return payload.piiSalt !== void 0 || payload.piiAttributes !== void 0 || payload.piiValueEncoding !== void 0;
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
+ }
41904
41956
  function piiWireOptionsFromPayload(payload) {
41905
41957
  if (!payload)
41906
41958
  return null;
41959
+ if (hasExplicitInvalidPiiEncoding(payload))
41960
+ return null;
41961
+ if (hasMalformedPiiAttributes(payload))
41962
+ return null;
41907
41963
  const salt = typeof payload.piiSalt === "string" ? payload.piiSalt : "";
41908
41964
  if (!salt)
41909
41965
  return null;
41910
- const encoding = typeof payload.piiValueEncoding === "string" ? payload.piiValueEncoding : PII_VALUE_ENCODING;
41911
- if (encoding !== PII_VALUE_ENCODING)
41912
- return null;
41913
- const attrs = Array.isArray(payload.piiAttributes) ? payload.piiAttributes.filter((k) => typeof k === "string") : [];
41966
+ const attrs = piiAttributeKeysFromPayload(payload);
41914
41967
  return {
41915
41968
  piiAttributes: attrs,
41916
41969
  piiSalt: salt,
41917
41970
  piiValueEncoding: PII_VALUE_ENCODING
41918
41971
  };
41919
41972
  }
41920
- function applyPiiHashToFlatContext(context, options) {
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
+ }
41989
+ function resolvePiiWireForDelta(previous, action) {
41990
+ if (!payloadHasExplicitPiiFields(action)) {
41991
+ return previous;
41992
+ }
41993
+ const merged = {
41994
+ piiSalt: action.piiSalt !== void 0 ? action.piiSalt : previous?.piiSalt,
41995
+ piiAttributes: action.piiAttributes !== void 0 ? action.piiAttributes : previous?.piiAttributes,
41996
+ piiValueEncoding: action.piiValueEncoding !== void 0 ? action.piiValueEncoding : previous?.piiValueEncoding
41997
+ };
41998
+ return piiWireOptionsFromPayload(merged);
41999
+ }
42000
+ function applyPiiHashToFlatContext(context, options, mode) {
41921
42001
  if (!options || !options.piiSalt || !options.piiAttributes.length || options.piiValueEncoding !== PII_VALUE_ENCODING) {
41922
42002
  return context;
41923
42003
  }
42004
+ if (mode?.alreadyHashed) {
42005
+ return context;
42006
+ }
41924
42007
  let changed = false;
41925
42008
  const out = { ...context };
41926
42009
  for (const [key2, value] of Object.entries(context)) {
41927
42010
  if (!isPiiAttribute(key2, options.piiAttributes))
41928
42011
  continue;
41929
42012
  if (typeof value === "string") {
41930
- if (HASH_HEX_PATTERN.test(value))
41931
- continue;
41932
42013
  out[key2] = hashPiiValue(value, options.piiSalt);
41933
42014
  changed = true;
41934
42015
  continue;
@@ -41938,8 +42019,6 @@ function applyPiiHashToFlatContext(context, options) {
41938
42019
  const next = value.map((entry) => {
41939
42020
  if (typeof entry !== "string")
41940
42021
  return entry;
41941
- if (HASH_HEX_PATTERN.test(entry))
41942
- return entry;
41943
42022
  arrayChanged = true;
41944
42023
  return hashPiiValue(entry, options.piiSalt);
41945
42024
  });
@@ -41959,7 +42038,8 @@ function createEmptyRulesState() {
41959
42038
  segments: /* @__PURE__ */ new Map(),
41960
42039
  ready: false,
41961
42040
  needsFullConfig: true,
41962
- piiWire: null
42041
+ piiWire: null,
42042
+ piiWireBroken: false
41963
42043
  };
41964
42044
  }
41965
42045
  function cloneState(state) {
@@ -41974,7 +42054,8 @@ function cloneState(state) {
41974
42054
  piiAttributes: [...state.piiWire.piiAttributes],
41975
42055
  piiSalt: state.piiWire.piiSalt,
41976
42056
  piiValueEncoding: state.piiWire.piiValueEncoding
41977
- } : null
42057
+ } : null,
42058
+ piiWireBroken: state.piiWireBroken
41978
42059
  };
41979
42060
  }
41980
42061
  function segmentsFromRecord(record) {
@@ -42001,14 +42082,18 @@ function hydrateFromFull(action) {
42001
42082
  if (flag?.key)
42002
42083
  flags.set(flag.key, flag);
42003
42084
  }
42085
+ const piiWire = piiWireOptionsFromPayload(action);
42086
+ const piiWireBroken = isPiiWireBroken(action, piiWire, false);
42004
42087
  return {
42005
42088
  version: action.version,
42006
42089
  expiresAt: action.expiresAt,
42007
42090
  flags,
42008
42091
  segments: segmentsFromRecord(action.segments),
42009
- ready: true,
42010
- needsFullConfig: false,
42011
- 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
42012
42097
  };
42013
42098
  }
42014
42099
  function applyPatch(state, action) {
@@ -42020,15 +42105,20 @@ function applyPatch(state, action) {
42020
42105
  if (flag?.key)
42021
42106
  flags.set(flag.key, flag);
42022
42107
  }
42023
- const nextPii = piiWireOptionsFromPayload(action) ?? state.piiWire;
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;
42024
42113
  return {
42025
42114
  version: action.toVersion,
42026
42115
  expiresAt: typeof action.expiresAt === "number" ? action.expiresAt : state.expiresAt,
42027
42116
  flags,
42028
42117
  segments: applySegmentUpserts(state.segments, action.segments),
42029
- ready: true,
42030
- needsFullConfig: false,
42031
- piiWire: nextPii
42118
+ ready: !piiWireBroken,
42119
+ needsFullConfig: piiWireBroken,
42120
+ piiWire: nextPii,
42121
+ piiWireBroken
42032
42122
  };
42033
42123
  }
42034
42124
  function reduceRules(state, action, now = Date.now()) {
@@ -42048,8 +42138,8 @@ function reduceRules(state, action, now = Date.now()) {
42048
42138
  ...cloneState(state),
42049
42139
  version: state.version,
42050
42140
  expiresAt: action.expiresAt,
42051
- ready: true,
42052
- 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
42053
42143
  }
42054
42144
  };
42055
42145
  }
@@ -42105,8 +42195,8 @@ function reduceRules(state, action, now = Date.now()) {
42105
42195
  ...current,
42106
42196
  version: action.toVersion,
42107
42197
  expiresAt: typeof action.expiresAt === "number" ? action.expiresAt : current.expiresAt,
42108
- ready: true,
42109
- needsFullConfig: false
42198
+ ready: !current.piiWireBroken,
42199
+ needsFullConfig: current.piiWireBroken
42110
42200
  }
42111
42201
  };
42112
42202
  }
@@ -42129,9 +42219,12 @@ class RulesStore {
42129
42219
  return this.state;
42130
42220
  }
42131
42221
  /**
42132
- * 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.
42133
42224
  */
42134
42225
  isReady(now = Date.now()) {
42226
+ if (this.state.piiWireBroken)
42227
+ return false;
42135
42228
  if (!this.state.ready)
42136
42229
  return false;
42137
42230
  if (!this.state.expiresAt)
@@ -42215,7 +42308,9 @@ class RulesStore {
42215
42308
  piiAttributes: [...pii.piiAttributes],
42216
42309
  piiSalt: pii.piiSalt,
42217
42310
  piiValueEncoding: pii.piiValueEncoding
42218
- } : {}
42311
+ } : {},
42312
+ // Persist fail-closed even when piiWire is null (unusable salt/encoding).
42313
+ ...this.state.piiWireBroken ? { piiWireBroken: true } : {}
42219
42314
  };
42220
42315
  }
42221
42316
  hydrateFromSnapshot(snapshot, now = Date.now()) {
@@ -42225,14 +42320,17 @@ class RulesStore {
42225
42320
  flags.set(flag.key, flag);
42226
42321
  }
42227
42322
  const expired = isConfigPayloadExpired(snapshot.expiresAt, now);
42323
+ const piiWire = piiWireOptionsFromPayload(snapshot);
42324
+ const piiWireBroken = snapshot.piiWireBroken === true || isPiiWireBroken(snapshot, piiWire, false);
42228
42325
  this.state = {
42229
42326
  version: snapshot.version,
42230
42327
  expiresAt: snapshot.expiresAt,
42231
42328
  flags,
42232
42329
  segments: segmentsFromRecord(snapshot.segments),
42233
- ready: !expired && flags.size > 0,
42234
- needsFullConfig: expired || flags.size === 0,
42235
- piiWire: piiWireOptionsFromPayload(snapshot)
42330
+ ready: !expired && flags.size > 0 && !piiWireBroken,
42331
+ needsFullConfig: expired || flags.size === 0 || piiWireBroken,
42332
+ piiWire,
42333
+ piiWireBroken
42236
42334
  };
42237
42335
  }
42238
42336
  }
@@ -42434,7 +42532,7 @@ function flattenEvaluationContext(context) {
42434
42532
  function toComparableString(value) {
42435
42533
  if (value === null || value === void 0)
42436
42534
  return "";
42437
- return String(value);
42535
+ return String(value).trim().toLowerCase();
42438
42536
  }
42439
42537
  function toComparableArray(value) {
42440
42538
  if (Array.isArray(value)) {
@@ -43203,9 +43301,10 @@ class FlagClient {
43203
43301
  }
43204
43302
  let hydratedRules = false;
43205
43303
  if (this.configSync && this.rulesStore && this.enableOfflineCache) {
43304
+ clearCachedFlags(this.apiKey);
43206
43305
  hydratedRules = await this.hydrateRulesFromCache();
43207
43306
  }
43208
- if (this.enableOfflineCache && !hydratedRules) {
43307
+ if (this.enableOfflineCache && !this.configSync && !hydratedRules) {
43209
43308
  const cached = await Promise.resolve(
43210
43309
  this.cacheAdapter.loadFlags(this.apiKey, this.cacheTTL)
43211
43310
  );
@@ -43447,7 +43546,7 @@ class FlagClient {
43447
43546
  */
43448
43547
  updateFlags(newFlags, fromShare = false) {
43449
43548
  this.flags = newFlags;
43450
- if (this.enableOfflineCache) {
43549
+ if (this.enableOfflineCache && !this.configSync) {
43451
43550
  void Promise.resolve(
43452
43551
  this.cacheAdapter.saveFlags(this.apiKey, newFlags)
43453
43552
  );