flagmint-js-sdk 2.2.2-rc.0 → 2.2.3-rc-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.
@@ -39093,7 +39093,7 @@ class SseTransport {
39093
39093
  this.connectPromise = new Promise((resolve, reject) => {
39094
39094
  this.connectResolve = resolve;
39095
39095
  this.connectReject = reject;
39096
- const sdkVersion = "2.2.2-rc.0";
39096
+ const sdkVersion = "2.2.3-rc-2";
39097
39097
  const platform2 = typeof window !== "undefined" ? "browser" : "nodejs";
39098
39098
  const wrapperName = this.configOptions?.wrapper?.name || "native-js";
39099
39099
  const wrapperVersion = this.configOptions?.wrapper?.version || "none";
@@ -39362,11 +39362,15 @@ class ConnectionShareHub {
39362
39362
  this.watchId = null;
39363
39363
  this.closed = false;
39364
39364
  this.latestFlags = null;
39365
+ this.latestRules = null;
39365
39366
  this.flagsWaiters = [];
39367
+ this.rulesWaiters = [];
39366
39368
  this.flagsTimeouts = [];
39369
+ this.rulesTimeouts = [];
39367
39370
  this.contextWaiters = /* @__PURE__ */ new Map();
39368
- this.lockKey = `flagmint_share_lock:${apiKey}`;
39369
- this.channelName = `flagmint-share:${apiKey}`;
39371
+ this.mode = options.mode ?? "evaluated";
39372
+ this.lockKey = this.mode === "rules" ? `flagmint_share_rules_lock:${apiKey}` : `flagmint_share_lock:${apiKey}`;
39373
+ this.channelName = this.mode === "rules" ? `flagmint-share-rules:${apiKey}` : `flagmint-share:${apiKey}`;
39370
39374
  this.now = options.now ?? (() => Date.now());
39371
39375
  this.storage = options.storage ?? (typeof localStorage !== "undefined" ? localStorage : null);
39372
39376
  this.channelFactory = options.channelFactory;
@@ -39378,6 +39382,10 @@ class ConnectionShareHub {
39378
39382
  get currentRole() {
39379
39383
  return this.role;
39380
39384
  }
39385
+ /** `evaluated` (server eval) or `rules` (config sync). */
39386
+ get shareMode() {
39387
+ return this.mode;
39388
+ }
39381
39389
  /**
39382
39390
  * Join the share group: compete for the lock, subscribe to the channel.
39383
39391
  *
@@ -39421,16 +39429,57 @@ class ConnectionShareHub {
39421
39429
  setLeaderContextHandler(handler) {
39422
39430
  this.leaderContextHandler = handler;
39423
39431
  }
39432
+ /**
39433
+ * Rules-mode follower: hydrate local rules from the leader snapshot and
39434
+ * return flags evaluated for **this** document's context.
39435
+ *
39436
+ * @param handler Apply snapshot + local eval → flag map
39437
+ */
39438
+ setFollowerRulesApply(handler) {
39439
+ this.followerRulesApply = handler;
39440
+ }
39441
+ /**
39442
+ * Rules-mode follower: re-evaluate after local `updateContext` (no RPC).
39443
+ *
39444
+ * @param handler Context → flag map using this tab's rules store
39445
+ */
39446
+ setFollowerLocalEval(handler) {
39447
+ this.followerLocalEval = handler;
39448
+ }
39424
39449
  /**
39425
39450
  * Leader-only: publish the current evaluated flags to all followers.
39426
- * No-op on followers (avoids echo loops).
39451
+ * No-op on followers (avoids echo loops). Evaluated mode only.
39452
+ *
39453
+ * @param flags Evaluated flag map from the leader stream
39427
39454
  */
39428
39455
  broadcastFlags(flags) {
39456
+ if (this.mode !== "evaluated")
39457
+ return;
39429
39458
  if (this.role !== "leader" || !this.channel)
39430
39459
  return;
39431
39460
  this.latestFlags = flags;
39432
39461
  this.post({ type: "flags", memberId: this.memberId, flags });
39433
39462
  }
39463
+ /**
39464
+ * Leader-only (rules mode): publish a verified rules snapshot.
39465
+ * Followers hydrate and evaluate with their own context.
39466
+ *
39467
+ * @param snapshot Rules store snapshot (MAC already verified on the leader)
39468
+ * @param analytics Optional per-flag analytics map from rules
39469
+ */
39470
+ broadcastRules(snapshot, analytics) {
39471
+ if (this.mode !== "rules")
39472
+ return;
39473
+ if (this.role !== "leader" || !this.channel)
39474
+ return;
39475
+ this.latestRules = { snapshot, analytics };
39476
+ this.post({
39477
+ type: "rules",
39478
+ memberId: this.memberId,
39479
+ snapshot,
39480
+ analytics
39481
+ });
39482
+ }
39434
39483
  /** Leader-only: mirror a transport `onError` (auth, quota) to followers. */
39435
39484
  broadcastError(error2) {
39436
39485
  if (this.role !== "leader" || !this.channel)
@@ -39444,11 +39493,34 @@ class ConnectionShareHub {
39444
39493
  });
39445
39494
  }
39446
39495
  /**
39447
- * Transport used by follower FlagClients. `init` waits for the leader
39448
- * snapshot; `fetchFlags` RPCs `updateContext` through the leader.
39496
+ * Transport used by follower FlagClients.
39497
+ * - Evaluated: `init` waits for flags; `fetchFlags` RPCs through the leader.
39498
+ * - Rules: `init` waits for a rules snapshot; `fetchFlags` evaluates locally.
39449
39499
  * `destroy` is a no-op — {@link ConnectionShareHub.destroy} owns teardown.
39450
39500
  */
39451
39501
  createFollowerTransport() {
39502
+ if (this.mode === "rules") {
39503
+ return {
39504
+ init: async () => {
39505
+ const payload = await this.waitForRules();
39506
+ const flags = this.followerRulesApply?.(payload) ?? {};
39507
+ this.followerFlagsCallback?.(flags);
39508
+ },
39509
+ fetchFlags: async (context) => {
39510
+ if (!this.followerLocalEval) {
39511
+ throw new Error("Rules-share follower has no local eval handler.");
39512
+ }
39513
+ return this.followerLocalEval(context);
39514
+ },
39515
+ destroy: () => void 0,
39516
+ onFlagsUpdated: (callback) => {
39517
+ this.followerFlagsCallback = callback;
39518
+ },
39519
+ onError: (callback) => {
39520
+ this.followerErrorCallback = callback;
39521
+ }
39522
+ };
39523
+ }
39452
39524
  return {
39453
39525
  init: async () => {
39454
39526
  const flags = await this.waitForFlags();
@@ -39491,10 +39563,15 @@ class ConnectionShareHub {
39491
39563
  setTimeout(() => channel.close(), 0);
39492
39564
  }
39493
39565
  this.flagsWaiters = [];
39566
+ this.rulesWaiters = [];
39494
39567
  this.flagsTimeouts.splice(0).forEach(({ timeoutId, reject }) => {
39495
39568
  clearTimeout(timeoutId);
39496
39569
  reject(new Error("Connection share hub closed."));
39497
39570
  });
39571
+ this.rulesTimeouts.splice(0).forEach(({ timeoutId, reject }) => {
39572
+ clearTimeout(timeoutId);
39573
+ reject(new Error("Connection share hub closed."));
39574
+ });
39498
39575
  this.contextWaiters.forEach(({ reject }) => reject(new Error("Connection share hub closed.")));
39499
39576
  this.contextWaiters.clear();
39500
39577
  }
@@ -39591,7 +39668,16 @@ class ConnectionShareHub {
39591
39668
  return;
39592
39669
  switch (message.type) {
39593
39670
  case "hello":
39594
- if (this.role === "leader" && this.latestFlags) {
39671
+ if (this.role !== "leader")
39672
+ break;
39673
+ if (this.mode === "rules" && this.latestRules) {
39674
+ this.post({
39675
+ type: "rules",
39676
+ memberId: this.memberId,
39677
+ snapshot: this.latestRules.snapshot,
39678
+ analytics: this.latestRules.analytics
39679
+ });
39680
+ } else if (this.mode === "evaluated" && this.latestFlags) {
39595
39681
  this.post({
39596
39682
  type: "flags",
39597
39683
  memberId: this.memberId,
@@ -39600,7 +39686,7 @@ class ConnectionShareHub {
39600
39686
  }
39601
39687
  break;
39602
39688
  case "flags":
39603
- if (this.role === "leader")
39689
+ if (this.mode !== "evaluated" || this.role === "leader")
39604
39690
  return;
39605
39691
  if (!message.flags || typeof message.flags !== "object" || Array.isArray(message.flags)) {
39606
39692
  return;
@@ -39609,6 +39695,22 @@ class ConnectionShareHub {
39609
39695
  this.flagsWaiters.splice(0).forEach((resolve) => resolve(message.flags));
39610
39696
  this.followerFlagsCallback?.(message.flags);
39611
39697
  break;
39698
+ case "rules":
39699
+ if (this.mode !== "rules" || this.role === "leader")
39700
+ return;
39701
+ if (!message.snapshot || typeof message.snapshot !== "object")
39702
+ return;
39703
+ {
39704
+ const payload = {
39705
+ snapshot: message.snapshot,
39706
+ analytics: message.analytics
39707
+ };
39708
+ this.latestRules = payload;
39709
+ this.rulesWaiters.splice(0).forEach((resolve) => resolve(payload));
39710
+ const flags = this.followerRulesApply?.(payload) ?? {};
39711
+ this.followerFlagsCallback?.(flags);
39712
+ }
39713
+ break;
39612
39714
  case "error":
39613
39715
  if (this.role === "leader")
39614
39716
  return;
@@ -39621,7 +39723,7 @@ class ConnectionShareHub {
39621
39723
  }
39622
39724
  break;
39623
39725
  case "context-request":
39624
- if (this.role !== "leader")
39726
+ if (this.mode !== "evaluated" || this.role !== "leader")
39625
39727
  return;
39626
39728
  void this.handleContextRequest(message);
39627
39729
  break;
@@ -39706,6 +39808,31 @@ class ConnectionShareHub {
39706
39808
  this.flagsTimeouts.push({ timeoutId, reject });
39707
39809
  });
39708
39810
  }
39811
+ /**
39812
+ * Block until the leader has sent a `rules` snapshot, or {@link FOLLOWER_WAIT_MS}.
39813
+ *
39814
+ * @returns Latest rules payload from the leader
39815
+ */
39816
+ waitForRules() {
39817
+ if (this.latestRules)
39818
+ return Promise.resolve(this.latestRules);
39819
+ if (this.closed)
39820
+ return Promise.reject(new Error("Connection share hub closed."));
39821
+ return new Promise((resolve, reject) => {
39822
+ const timeoutId = setTimeout(() => {
39823
+ this.rulesWaiters = this.rulesWaiters.filter((waiter) => waiter !== onRules);
39824
+ this.rulesTimeouts = this.rulesTimeouts.filter((entry) => entry.timeoutId !== timeoutId);
39825
+ reject(new Error("Timed out waiting for shared rules snapshot from the leader."));
39826
+ }, FOLLOWER_WAIT_MS);
39827
+ const onRules = (payload) => {
39828
+ clearTimeout(timeoutId);
39829
+ this.rulesTimeouts = this.rulesTimeouts.filter((entry) => entry.timeoutId !== timeoutId);
39830
+ resolve(payload);
39831
+ };
39832
+ this.rulesWaiters.push(onRules);
39833
+ this.rulesTimeouts.push({ timeoutId, reject });
39834
+ });
39835
+ }
39709
39836
  /**
39710
39837
  * Follower `fetchFlags`: ask the leader to POST `/context` and wait for
39711
39838
  * `context-result`. Leaders run the handler locally.
@@ -41873,7 +42000,6 @@ function isConfigPayloadExpired(expiresAt, now = Date.now()) {
41873
42000
  }
41874
42001
  const PII_VALUE_ENCODING = "hmac-v1";
41875
42002
  const textEncoder = new TextEncoder();
41876
- const HASH_HEX_PATTERN = /^[0-9a-f]{64}$/;
41877
42003
  function normalizePiiString(value) {
41878
42004
  return value.trim().toLowerCase();
41879
42005
  }
@@ -41901,34 +42027,109 @@ function isPiiAttribute(attribute, piiAttributes) {
41901
42027
  }
41902
42028
  return false;
41903
42029
  }
42030
+ function payloadHasExplicitPiiFields(payload) {
42031
+ if (!payload)
42032
+ return false;
42033
+ return payload.piiSalt !== void 0 || payload.piiAttributes !== void 0 || payload.piiValueEncoding !== void 0;
42034
+ }
42035
+ function piiAttributeKeysFromPayload(payload) {
42036
+ if (!payload || !Array.isArray(payload.piiAttributes))
42037
+ return [];
42038
+ return payload.piiAttributes.filter((k) => typeof k === "string" && k.length > 0);
42039
+ }
42040
+ function hasMalformedPiiAttributes(payload) {
42041
+ if (!payload || payload.piiAttributes === void 0)
42042
+ return false;
42043
+ if (!Array.isArray(payload.piiAttributes))
42044
+ return true;
42045
+ return payload.piiAttributes.some(
42046
+ (entry) => typeof entry !== "string" || entry.length === 0
42047
+ );
42048
+ }
42049
+ function hasExplicitInvalidPiiEncoding(payload) {
42050
+ if (!payload || payload.piiValueEncoding === void 0)
42051
+ return false;
42052
+ return payload.piiValueEncoding !== PII_VALUE_ENCODING;
42053
+ }
42054
+ function payloadRequiresUsablePiiWire(payload) {
42055
+ if (!payload)
42056
+ return false;
42057
+ if (hasExplicitInvalidPiiEncoding(payload))
42058
+ return true;
42059
+ if (hasMalformedPiiAttributes(payload))
42060
+ return true;
42061
+ if (piiAttributeKeysFromPayload(payload).length > 0)
42062
+ return true;
42063
+ return false;
42064
+ }
42065
+ function isExplicitPiiRequirementCleared(payload) {
42066
+ if (!payload || payload.piiAttributes === void 0)
42067
+ return false;
42068
+ if (!Array.isArray(payload.piiAttributes))
42069
+ return false;
42070
+ if (payload.piiAttributes.length !== 0)
42071
+ return false;
42072
+ if (hasExplicitInvalidPiiEncoding(payload))
42073
+ return false;
42074
+ return true;
42075
+ }
41904
42076
  function piiWireOptionsFromPayload(payload) {
41905
42077
  if (!payload)
41906
42078
  return null;
42079
+ if (hasExplicitInvalidPiiEncoding(payload))
42080
+ return null;
42081
+ if (hasMalformedPiiAttributes(payload))
42082
+ return null;
41907
42083
  const salt = typeof payload.piiSalt === "string" ? payload.piiSalt : "";
41908
42084
  if (!salt)
41909
42085
  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") : [];
42086
+ const attrs = piiAttributeKeysFromPayload(payload);
41914
42087
  return {
41915
42088
  piiAttributes: attrs,
41916
42089
  piiSalt: salt,
41917
42090
  piiValueEncoding: PII_VALUE_ENCODING
41918
42091
  };
41919
42092
  }
41920
- function applyPiiHashToFlatContext(context, options) {
42093
+ function isPiiWireBroken(payload, resolved, previousPriorHadPiiAttrs = false) {
42094
+ if (hasExplicitInvalidPiiEncoding(payload) || hasMalformedPiiAttributes(payload)) {
42095
+ return true;
42096
+ }
42097
+ if (resolved) {
42098
+ return false;
42099
+ }
42100
+ if (isExplicitPiiRequirementCleared(payload))
42101
+ return false;
42102
+ if (payloadRequiresUsablePiiWire(payload))
42103
+ return true;
42104
+ if (previousPriorHadPiiAttrs && payloadHasExplicitPiiFields(payload)) {
42105
+ return true;
42106
+ }
42107
+ return false;
42108
+ }
42109
+ function resolvePiiWireForDelta(previous, action) {
42110
+ if (!payloadHasExplicitPiiFields(action)) {
42111
+ return previous;
42112
+ }
42113
+ const merged = {
42114
+ piiSalt: action.piiSalt !== void 0 ? action.piiSalt : previous?.piiSalt,
42115
+ piiAttributes: action.piiAttributes !== void 0 ? action.piiAttributes : previous?.piiAttributes,
42116
+ piiValueEncoding: action.piiValueEncoding !== void 0 ? action.piiValueEncoding : previous?.piiValueEncoding
42117
+ };
42118
+ return piiWireOptionsFromPayload(merged);
42119
+ }
42120
+ function applyPiiHashToFlatContext(context, options, mode) {
41921
42121
  if (!options || !options.piiSalt || !options.piiAttributes.length || options.piiValueEncoding !== PII_VALUE_ENCODING) {
41922
42122
  return context;
41923
42123
  }
42124
+ if (mode?.alreadyHashed) {
42125
+ return context;
42126
+ }
41924
42127
  let changed = false;
41925
42128
  const out = { ...context };
41926
42129
  for (const [key2, value] of Object.entries(context)) {
41927
42130
  if (!isPiiAttribute(key2, options.piiAttributes))
41928
42131
  continue;
41929
42132
  if (typeof value === "string") {
41930
- if (HASH_HEX_PATTERN.test(value))
41931
- continue;
41932
42133
  out[key2] = hashPiiValue(value, options.piiSalt);
41933
42134
  changed = true;
41934
42135
  continue;
@@ -41938,8 +42139,6 @@ function applyPiiHashToFlatContext(context, options) {
41938
42139
  const next = value.map((entry) => {
41939
42140
  if (typeof entry !== "string")
41940
42141
  return entry;
41941
- if (HASH_HEX_PATTERN.test(entry))
41942
- return entry;
41943
42142
  arrayChanged = true;
41944
42143
  return hashPiiValue(entry, options.piiSalt);
41945
42144
  });
@@ -41959,7 +42158,8 @@ function createEmptyRulesState() {
41959
42158
  segments: /* @__PURE__ */ new Map(),
41960
42159
  ready: false,
41961
42160
  needsFullConfig: true,
41962
- piiWire: null
42161
+ piiWire: null,
42162
+ piiWireBroken: false
41963
42163
  };
41964
42164
  }
41965
42165
  function cloneState(state) {
@@ -41974,7 +42174,8 @@ function cloneState(state) {
41974
42174
  piiAttributes: [...state.piiWire.piiAttributes],
41975
42175
  piiSalt: state.piiWire.piiSalt,
41976
42176
  piiValueEncoding: state.piiWire.piiValueEncoding
41977
- } : null
42177
+ } : null,
42178
+ piiWireBroken: state.piiWireBroken
41978
42179
  };
41979
42180
  }
41980
42181
  function segmentsFromRecord(record) {
@@ -42001,14 +42202,18 @@ function hydrateFromFull(action) {
42001
42202
  if (flag?.key)
42002
42203
  flags.set(flag.key, flag);
42003
42204
  }
42205
+ const piiWire = piiWireOptionsFromPayload(action);
42206
+ const piiWireBroken = isPiiWireBroken(action, piiWire, false);
42004
42207
  return {
42005
42208
  version: action.version,
42006
42209
  expiresAt: action.expiresAt,
42007
42210
  flags,
42008
42211
  segments: segmentsFromRecord(action.segments),
42009
- ready: true,
42010
- needsFullConfig: false,
42011
- piiWire: piiWireOptionsFromPayload(action)
42212
+ // Usable rules, but PII metadata must be valid when hashing is required.
42213
+ ready: !piiWireBroken,
42214
+ needsFullConfig: piiWireBroken,
42215
+ piiWire,
42216
+ piiWireBroken
42012
42217
  };
42013
42218
  }
42014
42219
  function applyPatch(state, action) {
@@ -42020,15 +42225,20 @@ function applyPatch(state, action) {
42020
42225
  if (flag?.key)
42021
42226
  flags.set(flag.key, flag);
42022
42227
  }
42023
- const nextPii = piiWireOptionsFromPayload(action) ?? state.piiWire;
42228
+ const nextPii = resolvePiiWireForDelta(state.piiWire, action);
42229
+ const priorHadPiiAttrs = (state.piiWire?.piiAttributes.length ?? 0) > 0 || state.piiWireBroken;
42230
+ const computedBroken = isPiiWireBroken(action, nextPii, priorHadPiiAttrs);
42231
+ const recoveredWithRequiredAttrs = !!nextPii && nextPii.piiAttributes.length > 0;
42232
+ const piiWireBroken = isExplicitPiiRequirementCleared(action) ? false : recoveredWithRequiredAttrs ? false : computedBroken || state.piiWireBroken;
42024
42233
  return {
42025
42234
  version: action.toVersion,
42026
42235
  expiresAt: typeof action.expiresAt === "number" ? action.expiresAt : state.expiresAt,
42027
42236
  flags,
42028
42237
  segments: applySegmentUpserts(state.segments, action.segments),
42029
- ready: true,
42030
- needsFullConfig: false,
42031
- piiWire: nextPii
42238
+ ready: !piiWireBroken,
42239
+ needsFullConfig: piiWireBroken,
42240
+ piiWire: nextPii,
42241
+ piiWireBroken
42032
42242
  };
42033
42243
  }
42034
42244
  function reduceRules(state, action, now = Date.now()) {
@@ -42048,8 +42258,8 @@ function reduceRules(state, action, now = Date.now()) {
42048
42258
  ...cloneState(state),
42049
42259
  version: state.version,
42050
42260
  expiresAt: action.expiresAt,
42051
- ready: true,
42052
- needsFullConfig: state.flags.size === 0 || versionMismatch ? true : state.needsFullConfig
42261
+ ready: !state.piiWireBroken,
42262
+ needsFullConfig: state.piiWireBroken || state.flags.size === 0 || versionMismatch ? true : state.needsFullConfig
42053
42263
  }
42054
42264
  };
42055
42265
  }
@@ -42105,8 +42315,8 @@ function reduceRules(state, action, now = Date.now()) {
42105
42315
  ...current,
42106
42316
  version: action.toVersion,
42107
42317
  expiresAt: typeof action.expiresAt === "number" ? action.expiresAt : current.expiresAt,
42108
- ready: true,
42109
- needsFullConfig: false
42318
+ ready: !current.piiWireBroken,
42319
+ needsFullConfig: current.piiWireBroken
42110
42320
  }
42111
42321
  };
42112
42322
  }
@@ -42129,9 +42339,12 @@ class RulesStore {
42129
42339
  return this.state;
42130
42340
  }
42131
42341
  /**
42132
- * Fail-closed readiness: lease not expired and store marked ready.
42342
+ * Fail-closed readiness: lease not expired, store marked ready, and PII
42343
+ * wire metadata usable when hashing is required.
42133
42344
  */
42134
42345
  isReady(now = Date.now()) {
42346
+ if (this.state.piiWireBroken)
42347
+ return false;
42135
42348
  if (!this.state.ready)
42136
42349
  return false;
42137
42350
  if (!this.state.expiresAt)
@@ -42215,7 +42428,9 @@ class RulesStore {
42215
42428
  piiAttributes: [...pii.piiAttributes],
42216
42429
  piiSalt: pii.piiSalt,
42217
42430
  piiValueEncoding: pii.piiValueEncoding
42218
- } : {}
42431
+ } : {},
42432
+ // Persist fail-closed even when piiWire is null (unusable salt/encoding).
42433
+ ...this.state.piiWireBroken ? { piiWireBroken: true } : {}
42219
42434
  };
42220
42435
  }
42221
42436
  hydrateFromSnapshot(snapshot, now = Date.now()) {
@@ -42225,14 +42440,17 @@ class RulesStore {
42225
42440
  flags.set(flag.key, flag);
42226
42441
  }
42227
42442
  const expired = isConfigPayloadExpired(snapshot.expiresAt, now);
42443
+ const piiWire = piiWireOptionsFromPayload(snapshot);
42444
+ const piiWireBroken = snapshot.piiWireBroken === true || isPiiWireBroken(snapshot, piiWire, false);
42228
42445
  this.state = {
42229
42446
  version: snapshot.version,
42230
42447
  expiresAt: snapshot.expiresAt,
42231
42448
  flags,
42232
42449
  segments: segmentsFromRecord(snapshot.segments),
42233
- ready: !expired && flags.size > 0,
42234
- needsFullConfig: expired || flags.size === 0,
42235
- piiWire: piiWireOptionsFromPayload(snapshot)
42450
+ ready: !expired && flags.size > 0 && !piiWireBroken,
42451
+ needsFullConfig: expired || flags.size === 0 || piiWireBroken,
42452
+ piiWire,
42453
+ piiWireBroken
42236
42454
  };
42237
42455
  }
42238
42456
  }
@@ -42434,7 +42652,7 @@ function flattenEvaluationContext(context) {
42434
42652
  function toComparableString(value) {
42435
42653
  if (value === null || value === void 0)
42436
42654
  return "";
42437
- return String(value);
42655
+ return String(value).trim().toLowerCase();
42438
42656
  }
42439
42657
  function toComparableArray(value) {
42440
42658
  if (Array.isArray(value)) {
@@ -43152,11 +43370,11 @@ class FlagClient {
43152
43370
  this.env = options.env;
43153
43371
  this.enableFlagmint = options.enableFlagmint ?? true;
43154
43372
  this.configSync = options.configSync ?? false;
43155
- this.shareConnection = options.shareConnection ?? (this.configSync ? false : isConnectionSharingAvailable());
43373
+ logger.setup({ debugLog: options.debugLog });
43374
+ this.shareConnection = options.shareConnection ?? isConnectionSharingAvailable();
43156
43375
  if (this.configSync) {
43157
43376
  this.rulesStore = new RulesStore();
43158
43377
  }
43159
- logger.setup({ debugLog: options.debugLog });
43160
43378
  if (this.previewMode && this.rawFlags && Object.keys(this.rawFlags).length > 0) {
43161
43379
  this.flags = this.evaluateLocally(this.rawFlags, this.context);
43162
43380
  this.readyPromise = Promise.resolve();
@@ -43220,7 +43438,7 @@ class FlagClient {
43220
43438
  return;
43221
43439
  }
43222
43440
  await this.setupTransport(options);
43223
- this.shareHub?.broadcastFlags(this.flags);
43441
+ this.broadcastShareSnapshot();
43224
43442
  this.isInitialized = true;
43225
43443
  this.resolveReady();
43226
43444
  } catch (err) {
@@ -43260,19 +43478,26 @@ class FlagClient {
43260
43478
  * @returns true when this document is a follower and should skip opening a stream.
43261
43479
  */
43262
43480
  async joinConnectionShare(options) {
43263
- if (!this.shareConnection || options.transport) {
43481
+ if (!this.shareConnection) {
43482
+ return false;
43483
+ }
43484
+ if (options.transport && !options.share) {
43264
43485
  return false;
43265
43486
  }
43266
43487
  if (!options.share && !isConnectionSharingAvailable()) {
43267
43488
  return false;
43268
43489
  }
43269
- this.shareHub = new ConnectionShareHub(this.apiKey, options.share);
43490
+ const shareMode = this.configSync ? "rules" : "evaluated";
43491
+ this.shareHub = new ConnectionShareHub(this.apiKey, {
43492
+ ...options.share,
43493
+ mode: shareMode
43494
+ });
43270
43495
  this.shareHub.onPromote(async () => {
43271
43496
  logger.log("[FlagClient] Share leader departed. Promoting this document to hold the stream.");
43272
43497
  try {
43273
43498
  await this.setupTransport(options);
43274
43499
  this.attachShareLeader();
43275
- this.shareHub?.broadcastFlags(this.flags);
43500
+ this.broadcastShareSnapshot();
43276
43501
  } catch (err) {
43277
43502
  const error2 = err instanceof Error ? err : new Error(String(err));
43278
43503
  logger.error("[FlagClient] Promotion to share leader failed:", error2);
@@ -43281,7 +43506,12 @@ class FlagClient {
43281
43506
  });
43282
43507
  const role = await this.shareHub.join();
43283
43508
  if (role === "follower") {
43284
- logger.log("[FlagClient] Following an existing Flagmint stream in another same-origin document.");
43509
+ logger.log(
43510
+ shareMode === "rules" ? "[FlagClient] Following shared config-sync rules from another same-origin document." : "[FlagClient] Following an existing Flagmint stream in another same-origin document."
43511
+ );
43512
+ if (shareMode === "rules") {
43513
+ this.attachRulesShareFollower();
43514
+ }
43285
43515
  this.transport = this.shareHub.createFollowerTransport();
43286
43516
  this.bindTransport(this.transport);
43287
43517
  await this.transport.init();
@@ -43290,8 +43520,15 @@ class FlagClient {
43290
43520
  this.attachShareLeader();
43291
43521
  return false;
43292
43522
  }
43523
+ /**
43524
+ * Leader: wire context RPC for evaluated share only.
43525
+ * Rules share never forwards follower context onto the stream.
43526
+ */
43293
43527
  attachShareLeader() {
43294
- this.shareHub?.setLeaderContextHandler(async (context, options) => {
43528
+ if (!this.shareHub || this.shareHub.shareMode === "rules") {
43529
+ return;
43530
+ }
43531
+ this.shareHub.setLeaderContextHandler(async (context, options) => {
43295
43532
  const mergedContext = toJsonCloneable({ ...this.context, ...context });
43296
43533
  if (options?.persist === true) {
43297
43534
  this.context = mergedContext;
@@ -43300,9 +43537,6 @@ class FlagClient {
43300
43537
  }
43301
43538
  }
43302
43539
  if (!this.transport || typeof this.transport.fetchFlags !== "function") {
43303
- if (this.configSync && this.rulesStore) {
43304
- return this.evaluateFromRulesStore(mergedContext);
43305
- }
43306
43540
  return this.flags;
43307
43541
  }
43308
43542
  return this.transport.fetchFlags(mergedContext, {
@@ -43310,6 +43544,41 @@ class FlagClient {
43310
43544
  });
43311
43545
  });
43312
43546
  }
43547
+ /**
43548
+ * Follower hooks for rules-only share: hydrate rules locally and evaluate
43549
+ * with this document's context (never the leader's).
43550
+ */
43551
+ attachRulesShareFollower() {
43552
+ if (!this.shareHub || !this.rulesStore)
43553
+ return;
43554
+ this.shareHub.setFollowerRulesApply((payload) => {
43555
+ this.rulesStore.hydrateFromSnapshot(payload.snapshot);
43556
+ if (payload.analytics) {
43557
+ this.updateAnalytics(payload.analytics);
43558
+ }
43559
+ const flags = this.evaluateFromRulesStore();
43560
+ this.updateFlags(flags, true);
43561
+ return flags;
43562
+ });
43563
+ this.shareHub.setFollowerLocalEval((context) => {
43564
+ return this.evaluateFromRulesStore(context);
43565
+ });
43566
+ }
43567
+ /**
43568
+ * Leader: push the right snapshot type to followers after flags/rules change.
43569
+ */
43570
+ broadcastShareSnapshot() {
43571
+ if (!this.shareHub || this.shareHub.currentRole !== "leader")
43572
+ return;
43573
+ if (this.shareHub.shareMode === "rules" && this.rulesStore) {
43574
+ const snapshot = this.rulesStore.toSnapshot();
43575
+ if (snapshot.flags.length === 0)
43576
+ return;
43577
+ this.shareHub.broadcastRules(snapshot, this.analyticsMapFromRulesStore());
43578
+ return;
43579
+ }
43580
+ this.shareHub.broadcastFlags(this.flags);
43581
+ }
43313
43582
  /**
43314
43583
  * Creates and initializes the transport layer used to receive feature flags
43315
43584
  * updates from the Flagmint platform.
@@ -43454,7 +43723,7 @@ class FlagClient {
43454
43723
  }
43455
43724
  this.notifySubscribers();
43456
43725
  if (!fromShare) {
43457
- this.shareHub?.broadcastFlags(newFlags);
43726
+ this.broadcastShareSnapshot();
43458
43727
  }
43459
43728
  }
43460
43729
  /**