flagmint-js-sdk 2.2.2 → 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";
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.
@@ -41903,22 +42030,80 @@ function payloadHasExplicitPiiFields(payload) {
41903
42030
  return false;
41904
42031
  return payload.piiSalt !== void 0 || payload.piiAttributes !== void 0 || payload.piiValueEncoding !== void 0;
41905
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
+ }
41906
42074
  function piiWireOptionsFromPayload(payload) {
41907
42075
  if (!payload)
41908
42076
  return null;
42077
+ if (hasExplicitInvalidPiiEncoding(payload))
42078
+ return null;
42079
+ if (hasMalformedPiiAttributes(payload))
42080
+ return null;
41909
42081
  const salt = typeof payload.piiSalt === "string" ? payload.piiSalt : "";
41910
42082
  if (!salt)
41911
42083
  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") : [];
42084
+ const attrs = piiAttributeKeysFromPayload(payload);
41916
42085
  return {
41917
42086
  piiAttributes: attrs,
41918
42087
  piiSalt: salt,
41919
42088
  piiValueEncoding: PII_VALUE_ENCODING
41920
42089
  };
41921
42090
  }
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
+ }
41922
42107
  function resolvePiiWireForDelta(previous, action) {
41923
42108
  if (!payloadHasExplicitPiiFields(action)) {
41924
42109
  return previous;
@@ -41971,7 +42156,8 @@ function createEmptyRulesState() {
41971
42156
  segments: /* @__PURE__ */ new Map(),
41972
42157
  ready: false,
41973
42158
  needsFullConfig: true,
41974
- piiWire: null
42159
+ piiWire: null,
42160
+ piiWireBroken: false
41975
42161
  };
41976
42162
  }
41977
42163
  function cloneState(state) {
@@ -41986,7 +42172,8 @@ function cloneState(state) {
41986
42172
  piiAttributes: [...state.piiWire.piiAttributes],
41987
42173
  piiSalt: state.piiWire.piiSalt,
41988
42174
  piiValueEncoding: state.piiWire.piiValueEncoding
41989
- } : null
42175
+ } : null,
42176
+ piiWireBroken: state.piiWireBroken
41990
42177
  };
41991
42178
  }
41992
42179
  function segmentsFromRecord(record) {
@@ -42013,14 +42200,18 @@ function hydrateFromFull(action) {
42013
42200
  if (flag?.key)
42014
42201
  flags.set(flag.key, flag);
42015
42202
  }
42203
+ const piiWire = piiWireOptionsFromPayload(action);
42204
+ const piiWireBroken = isPiiWireBroken(action, piiWire, false);
42016
42205
  return {
42017
42206
  version: action.version,
42018
42207
  expiresAt: action.expiresAt,
42019
42208
  flags,
42020
42209
  segments: segmentsFromRecord(action.segments),
42021
- ready: true,
42022
- needsFullConfig: false,
42023
- 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
42024
42215
  };
42025
42216
  }
42026
42217
  function applyPatch(state, action) {
@@ -42033,14 +42224,19 @@ function applyPatch(state, action) {
42033
42224
  flags.set(flag.key, flag);
42034
42225
  }
42035
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;
42036
42231
  return {
42037
42232
  version: action.toVersion,
42038
42233
  expiresAt: typeof action.expiresAt === "number" ? action.expiresAt : state.expiresAt,
42039
42234
  flags,
42040
42235
  segments: applySegmentUpserts(state.segments, action.segments),
42041
- ready: true,
42042
- needsFullConfig: false,
42043
- piiWire: nextPii
42236
+ ready: !piiWireBroken,
42237
+ needsFullConfig: piiWireBroken,
42238
+ piiWire: nextPii,
42239
+ piiWireBroken
42044
42240
  };
42045
42241
  }
42046
42242
  function reduceRules(state, action, now = Date.now()) {
@@ -42060,8 +42256,8 @@ function reduceRules(state, action, now = Date.now()) {
42060
42256
  ...cloneState(state),
42061
42257
  version: state.version,
42062
42258
  expiresAt: action.expiresAt,
42063
- ready: true,
42064
- 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
42065
42261
  }
42066
42262
  };
42067
42263
  }
@@ -42117,8 +42313,8 @@ function reduceRules(state, action, now = Date.now()) {
42117
42313
  ...current,
42118
42314
  version: action.toVersion,
42119
42315
  expiresAt: typeof action.expiresAt === "number" ? action.expiresAt : current.expiresAt,
42120
- ready: true,
42121
- needsFullConfig: false
42316
+ ready: !current.piiWireBroken,
42317
+ needsFullConfig: current.piiWireBroken
42122
42318
  }
42123
42319
  };
42124
42320
  }
@@ -42141,9 +42337,12 @@ class RulesStore {
42141
42337
  return this.state;
42142
42338
  }
42143
42339
  /**
42144
- * 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.
42145
42342
  */
42146
42343
  isReady(now = Date.now()) {
42344
+ if (this.state.piiWireBroken)
42345
+ return false;
42147
42346
  if (!this.state.ready)
42148
42347
  return false;
42149
42348
  if (!this.state.expiresAt)
@@ -42227,7 +42426,9 @@ class RulesStore {
42227
42426
  piiAttributes: [...pii.piiAttributes],
42228
42427
  piiSalt: pii.piiSalt,
42229
42428
  piiValueEncoding: pii.piiValueEncoding
42230
- } : {}
42429
+ } : {},
42430
+ // Persist fail-closed even when piiWire is null (unusable salt/encoding).
42431
+ ...this.state.piiWireBroken ? { piiWireBroken: true } : {}
42231
42432
  };
42232
42433
  }
42233
42434
  hydrateFromSnapshot(snapshot, now = Date.now()) {
@@ -42237,14 +42438,17 @@ class RulesStore {
42237
42438
  flags.set(flag.key, flag);
42238
42439
  }
42239
42440
  const expired = isConfigPayloadExpired(snapshot.expiresAt, now);
42441
+ const piiWire = piiWireOptionsFromPayload(snapshot);
42442
+ const piiWireBroken = snapshot.piiWireBroken === true || isPiiWireBroken(snapshot, piiWire, false);
42240
42443
  this.state = {
42241
42444
  version: snapshot.version,
42242
42445
  expiresAt: snapshot.expiresAt,
42243
42446
  flags,
42244
42447
  segments: segmentsFromRecord(snapshot.segments),
42245
- ready: !expired && flags.size > 0,
42246
- needsFullConfig: expired || flags.size === 0,
42247
- piiWire: piiWireOptionsFromPayload(snapshot)
42448
+ ready: !expired && flags.size > 0 && !piiWireBroken,
42449
+ needsFullConfig: expired || flags.size === 0 || piiWireBroken,
42450
+ piiWire,
42451
+ piiWireBroken
42248
42452
  };
42249
42453
  }
42250
42454
  }
@@ -43164,11 +43368,11 @@ class FlagClient {
43164
43368
  this.env = options.env;
43165
43369
  this.enableFlagmint = options.enableFlagmint ?? true;
43166
43370
  this.configSync = options.configSync ?? false;
43167
- this.shareConnection = options.shareConnection ?? (this.configSync ? false : isConnectionSharingAvailable());
43371
+ logger.setup({ debugLog: options.debugLog });
43372
+ this.shareConnection = options.shareConnection ?? isConnectionSharingAvailable();
43168
43373
  if (this.configSync) {
43169
43374
  this.rulesStore = new RulesStore();
43170
43375
  }
43171
- logger.setup({ debugLog: options.debugLog });
43172
43376
  if (this.previewMode && this.rawFlags && Object.keys(this.rawFlags).length > 0) {
43173
43377
  this.flags = this.evaluateLocally(this.rawFlags, this.context);
43174
43378
  this.readyPromise = Promise.resolve();
@@ -43232,7 +43436,7 @@ class FlagClient {
43232
43436
  return;
43233
43437
  }
43234
43438
  await this.setupTransport(options);
43235
- this.shareHub?.broadcastFlags(this.flags);
43439
+ this.broadcastShareSnapshot();
43236
43440
  this.isInitialized = true;
43237
43441
  this.resolveReady();
43238
43442
  } catch (err) {
@@ -43272,19 +43476,26 @@ class FlagClient {
43272
43476
  * @returns true when this document is a follower and should skip opening a stream.
43273
43477
  */
43274
43478
  async joinConnectionShare(options) {
43275
- if (!this.shareConnection || options.transport) {
43479
+ if (!this.shareConnection) {
43480
+ return false;
43481
+ }
43482
+ if (options.transport && !options.share) {
43276
43483
  return false;
43277
43484
  }
43278
43485
  if (!options.share && !isConnectionSharingAvailable()) {
43279
43486
  return false;
43280
43487
  }
43281
- 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
+ });
43282
43493
  this.shareHub.onPromote(async () => {
43283
43494
  logger.log("[FlagClient] Share leader departed. Promoting this document to hold the stream.");
43284
43495
  try {
43285
43496
  await this.setupTransport(options);
43286
43497
  this.attachShareLeader();
43287
- this.shareHub?.broadcastFlags(this.flags);
43498
+ this.broadcastShareSnapshot();
43288
43499
  } catch (err) {
43289
43500
  const error2 = err instanceof Error ? err : new Error(String(err));
43290
43501
  logger.error("[FlagClient] Promotion to share leader failed:", error2);
@@ -43293,7 +43504,12 @@ class FlagClient {
43293
43504
  });
43294
43505
  const role = await this.shareHub.join();
43295
43506
  if (role === "follower") {
43296
- 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
+ }
43297
43513
  this.transport = this.shareHub.createFollowerTransport();
43298
43514
  this.bindTransport(this.transport);
43299
43515
  await this.transport.init();
@@ -43302,8 +43518,15 @@ class FlagClient {
43302
43518
  this.attachShareLeader();
43303
43519
  return false;
43304
43520
  }
43521
+ /**
43522
+ * Leader: wire context RPC for evaluated share only.
43523
+ * Rules share never forwards follower context onto the stream.
43524
+ */
43305
43525
  attachShareLeader() {
43306
- this.shareHub?.setLeaderContextHandler(async (context, options) => {
43526
+ if (!this.shareHub || this.shareHub.shareMode === "rules") {
43527
+ return;
43528
+ }
43529
+ this.shareHub.setLeaderContextHandler(async (context, options) => {
43307
43530
  const mergedContext = toJsonCloneable({ ...this.context, ...context });
43308
43531
  if (options?.persist === true) {
43309
43532
  this.context = mergedContext;
@@ -43312,9 +43535,6 @@ class FlagClient {
43312
43535
  }
43313
43536
  }
43314
43537
  if (!this.transport || typeof this.transport.fetchFlags !== "function") {
43315
- if (this.configSync && this.rulesStore) {
43316
- return this.evaluateFromRulesStore(mergedContext);
43317
- }
43318
43538
  return this.flags;
43319
43539
  }
43320
43540
  return this.transport.fetchFlags(mergedContext, {
@@ -43322,6 +43542,41 @@ class FlagClient {
43322
43542
  });
43323
43543
  });
43324
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
+ }
43325
43580
  /**
43326
43581
  * Creates and initializes the transport layer used to receive feature flags
43327
43582
  * updates from the Flagmint platform.
@@ -43466,7 +43721,7 @@ class FlagClient {
43466
43721
  }
43467
43722
  this.notifySubscribers();
43468
43723
  if (!fromShare) {
43469
- this.shareHub?.broadcastFlags(newFlags);
43724
+ this.broadcastShareSnapshot();
43470
43725
  }
43471
43726
  }
43472
43727
  /**