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