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.
@@ -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";
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.
@@ -41907,22 +42034,80 @@ var __publicField = (obj, key, value) => {
41907
42034
  return false;
41908
42035
  return payload.piiSalt !== void 0 || payload.piiAttributes !== void 0 || payload.piiValueEncoding !== void 0;
41909
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
+ }
41910
42078
  function piiWireOptionsFromPayload(payload) {
41911
42079
  if (!payload)
41912
42080
  return null;
42081
+ if (hasExplicitInvalidPiiEncoding(payload))
42082
+ return null;
42083
+ if (hasMalformedPiiAttributes(payload))
42084
+ return null;
41913
42085
  const salt = typeof payload.piiSalt === "string" ? payload.piiSalt : "";
41914
42086
  if (!salt)
41915
42087
  return null;
41916
- const encoding = typeof payload.piiValueEncoding === "string" ? payload.piiValueEncoding : PII_VALUE_ENCODING;
41917
- if (encoding !== PII_VALUE_ENCODING)
41918
- return null;
41919
- const attrs = Array.isArray(payload.piiAttributes) ? payload.piiAttributes.filter((k) => typeof k === "string") : [];
42088
+ const attrs = piiAttributeKeysFromPayload(payload);
41920
42089
  return {
41921
42090
  piiAttributes: attrs,
41922
42091
  piiSalt: salt,
41923
42092
  piiValueEncoding: PII_VALUE_ENCODING
41924
42093
  };
41925
42094
  }
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
+ }
41926
42111
  function resolvePiiWireForDelta(previous, action) {
41927
42112
  if (!payloadHasExplicitPiiFields(action)) {
41928
42113
  return previous;
@@ -41975,7 +42160,8 @@ var __publicField = (obj, key, value) => {
41975
42160
  segments: /* @__PURE__ */ new Map(),
41976
42161
  ready: false,
41977
42162
  needsFullConfig: true,
41978
- piiWire: null
42163
+ piiWire: null,
42164
+ piiWireBroken: false
41979
42165
  };
41980
42166
  }
41981
42167
  function cloneState(state) {
@@ -41990,7 +42176,8 @@ var __publicField = (obj, key, value) => {
41990
42176
  piiAttributes: [...state.piiWire.piiAttributes],
41991
42177
  piiSalt: state.piiWire.piiSalt,
41992
42178
  piiValueEncoding: state.piiWire.piiValueEncoding
41993
- } : null
42179
+ } : null,
42180
+ piiWireBroken: state.piiWireBroken
41994
42181
  };
41995
42182
  }
41996
42183
  function segmentsFromRecord(record) {
@@ -42017,14 +42204,18 @@ var __publicField = (obj, key, value) => {
42017
42204
  if (flag?.key)
42018
42205
  flags.set(flag.key, flag);
42019
42206
  }
42207
+ const piiWire = piiWireOptionsFromPayload(action);
42208
+ const piiWireBroken = isPiiWireBroken(action, piiWire, false);
42020
42209
  return {
42021
42210
  version: action.version,
42022
42211
  expiresAt: action.expiresAt,
42023
42212
  flags,
42024
42213
  segments: segmentsFromRecord(action.segments),
42025
- ready: true,
42026
- needsFullConfig: false,
42027
- 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
42028
42219
  };
42029
42220
  }
42030
42221
  function applyPatch(state, action) {
@@ -42037,14 +42228,19 @@ var __publicField = (obj, key, value) => {
42037
42228
  flags.set(flag.key, flag);
42038
42229
  }
42039
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;
42040
42235
  return {
42041
42236
  version: action.toVersion,
42042
42237
  expiresAt: typeof action.expiresAt === "number" ? action.expiresAt : state.expiresAt,
42043
42238
  flags,
42044
42239
  segments: applySegmentUpserts(state.segments, action.segments),
42045
- ready: true,
42046
- needsFullConfig: false,
42047
- piiWire: nextPii
42240
+ ready: !piiWireBroken,
42241
+ needsFullConfig: piiWireBroken,
42242
+ piiWire: nextPii,
42243
+ piiWireBroken
42048
42244
  };
42049
42245
  }
42050
42246
  function reduceRules(state, action, now = Date.now()) {
@@ -42064,8 +42260,8 @@ var __publicField = (obj, key, value) => {
42064
42260
  ...cloneState(state),
42065
42261
  version: state.version,
42066
42262
  expiresAt: action.expiresAt,
42067
- ready: true,
42068
- 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
42069
42265
  }
42070
42266
  };
42071
42267
  }
@@ -42121,8 +42317,8 @@ var __publicField = (obj, key, value) => {
42121
42317
  ...current,
42122
42318
  version: action.toVersion,
42123
42319
  expiresAt: typeof action.expiresAt === "number" ? action.expiresAt : current.expiresAt,
42124
- ready: true,
42125
- needsFullConfig: false
42320
+ ready: !current.piiWireBroken,
42321
+ needsFullConfig: current.piiWireBroken
42126
42322
  }
42127
42323
  };
42128
42324
  }
@@ -42145,9 +42341,12 @@ var __publicField = (obj, key, value) => {
42145
42341
  return this.state;
42146
42342
  }
42147
42343
  /**
42148
- * 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.
42149
42346
  */
42150
42347
  isReady(now = Date.now()) {
42348
+ if (this.state.piiWireBroken)
42349
+ return false;
42151
42350
  if (!this.state.ready)
42152
42351
  return false;
42153
42352
  if (!this.state.expiresAt)
@@ -42231,7 +42430,9 @@ var __publicField = (obj, key, value) => {
42231
42430
  piiAttributes: [...pii.piiAttributes],
42232
42431
  piiSalt: pii.piiSalt,
42233
42432
  piiValueEncoding: pii.piiValueEncoding
42234
- } : {}
42433
+ } : {},
42434
+ // Persist fail-closed even when piiWire is null (unusable salt/encoding).
42435
+ ...this.state.piiWireBroken ? { piiWireBroken: true } : {}
42235
42436
  };
42236
42437
  }
42237
42438
  hydrateFromSnapshot(snapshot, now = Date.now()) {
@@ -42241,14 +42442,17 @@ var __publicField = (obj, key, value) => {
42241
42442
  flags.set(flag.key, flag);
42242
42443
  }
42243
42444
  const expired = isConfigPayloadExpired(snapshot.expiresAt, now);
42445
+ const piiWire = piiWireOptionsFromPayload(snapshot);
42446
+ const piiWireBroken = snapshot.piiWireBroken === true || isPiiWireBroken(snapshot, piiWire, false);
42244
42447
  this.state = {
42245
42448
  version: snapshot.version,
42246
42449
  expiresAt: snapshot.expiresAt,
42247
42450
  flags,
42248
42451
  segments: segmentsFromRecord(snapshot.segments),
42249
- ready: !expired && flags.size > 0,
42250
- needsFullConfig: expired || flags.size === 0,
42251
- piiWire: piiWireOptionsFromPayload(snapshot)
42452
+ ready: !expired && flags.size > 0 && !piiWireBroken,
42453
+ needsFullConfig: expired || flags.size === 0 || piiWireBroken,
42454
+ piiWire,
42455
+ piiWireBroken
42252
42456
  };
42253
42457
  }
42254
42458
  }
@@ -43168,11 +43372,11 @@ var __publicField = (obj, key, value) => {
43168
43372
  this.env = options.env;
43169
43373
  this.enableFlagmint = options.enableFlagmint ?? true;
43170
43374
  this.configSync = options.configSync ?? false;
43171
- this.shareConnection = options.shareConnection ?? (this.configSync ? false : isConnectionSharingAvailable());
43375
+ logger.setup({ debugLog: options.debugLog });
43376
+ this.shareConnection = options.shareConnection ?? isConnectionSharingAvailable();
43172
43377
  if (this.configSync) {
43173
43378
  this.rulesStore = new RulesStore();
43174
43379
  }
43175
- logger.setup({ debugLog: options.debugLog });
43176
43380
  if (this.previewMode && this.rawFlags && Object.keys(this.rawFlags).length > 0) {
43177
43381
  this.flags = this.evaluateLocally(this.rawFlags, this.context);
43178
43382
  this.readyPromise = Promise.resolve();
@@ -43236,7 +43440,7 @@ var __publicField = (obj, key, value) => {
43236
43440
  return;
43237
43441
  }
43238
43442
  await this.setupTransport(options);
43239
- this.shareHub?.broadcastFlags(this.flags);
43443
+ this.broadcastShareSnapshot();
43240
43444
  this.isInitialized = true;
43241
43445
  this.resolveReady();
43242
43446
  } catch (err) {
@@ -43276,19 +43480,26 @@ var __publicField = (obj, key, value) => {
43276
43480
  * @returns true when this document is a follower and should skip opening a stream.
43277
43481
  */
43278
43482
  async joinConnectionShare(options) {
43279
- if (!this.shareConnection || options.transport) {
43483
+ if (!this.shareConnection) {
43484
+ return false;
43485
+ }
43486
+ if (options.transport && !options.share) {
43280
43487
  return false;
43281
43488
  }
43282
43489
  if (!options.share && !isConnectionSharingAvailable()) {
43283
43490
  return false;
43284
43491
  }
43285
- 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
+ });
43286
43497
  this.shareHub.onPromote(async () => {
43287
43498
  logger.log("[FlagClient] Share leader departed. Promoting this document to hold the stream.");
43288
43499
  try {
43289
43500
  await this.setupTransport(options);
43290
43501
  this.attachShareLeader();
43291
- this.shareHub?.broadcastFlags(this.flags);
43502
+ this.broadcastShareSnapshot();
43292
43503
  } catch (err) {
43293
43504
  const error = err instanceof Error ? err : new Error(String(err));
43294
43505
  logger.error("[FlagClient] Promotion to share leader failed:", error);
@@ -43297,7 +43508,12 @@ var __publicField = (obj, key, value) => {
43297
43508
  });
43298
43509
  const role = await this.shareHub.join();
43299
43510
  if (role === "follower") {
43300
- 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
+ }
43301
43517
  this.transport = this.shareHub.createFollowerTransport();
43302
43518
  this.bindTransport(this.transport);
43303
43519
  await this.transport.init();
@@ -43306,8 +43522,15 @@ var __publicField = (obj, key, value) => {
43306
43522
  this.attachShareLeader();
43307
43523
  return false;
43308
43524
  }
43525
+ /**
43526
+ * Leader: wire context RPC for evaluated share only.
43527
+ * Rules share never forwards follower context onto the stream.
43528
+ */
43309
43529
  attachShareLeader() {
43310
- this.shareHub?.setLeaderContextHandler(async (context, options) => {
43530
+ if (!this.shareHub || this.shareHub.shareMode === "rules") {
43531
+ return;
43532
+ }
43533
+ this.shareHub.setLeaderContextHandler(async (context, options) => {
43311
43534
  const mergedContext = toJsonCloneable({ ...this.context, ...context });
43312
43535
  if (options?.persist === true) {
43313
43536
  this.context = mergedContext;
@@ -43316,9 +43539,6 @@ var __publicField = (obj, key, value) => {
43316
43539
  }
43317
43540
  }
43318
43541
  if (!this.transport || typeof this.transport.fetchFlags !== "function") {
43319
- if (this.configSync && this.rulesStore) {
43320
- return this.evaluateFromRulesStore(mergedContext);
43321
- }
43322
43542
  return this.flags;
43323
43543
  }
43324
43544
  return this.transport.fetchFlags(mergedContext, {
@@ -43326,6 +43546,41 @@ var __publicField = (obj, key, value) => {
43326
43546
  });
43327
43547
  });
43328
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
+ }
43329
43584
  /**
43330
43585
  * Creates and initializes the transport layer used to receive feature flags
43331
43586
  * updates from the Flagmint platform.
@@ -43470,7 +43725,7 @@ var __publicField = (obj, key, value) => {
43470
43725
  }
43471
43726
  this.notifySubscribers();
43472
43727
  if (!fromShare) {
43473
- this.shareHub?.broadcastFlags(newFlags);
43728
+ this.broadcastShareSnapshot();
43474
43729
  }
43475
43730
  }
43476
43731
  /**