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