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.
- package/README.md +21 -7
- package/dist/flagmint.cjs.js +319 -50
- package/dist/flagmint.cjs.js.map +1 -1
- package/dist/flagmint.es.js +319 -50
- package/dist/flagmint.es.js.map +1 -1
- package/dist/flagmint.umd.js +319 -50
- package/dist/flagmint.umd.js.map +1 -1
- package/dist/sdk/core/client.d.ts +17 -1
- package/dist/sdk/core/config-sync/index.d.ts +2 -2
- package/dist/sdk/core/config-sync/piiHash.d.ts +86 -6
- package/dist/sdk/core/config-sync/rulesStore.d.ts +7 -1
- package/dist/sdk/core/config-sync/types.d.ts +5 -0
- package/dist/sdk/core/helpers/connectionShare.d.ts +73 -22
- package/package.json +1 -1
package/dist/flagmint.umd.js
CHANGED
|
@@ -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.
|
|
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.
|
|
39371
|
-
this.
|
|
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.
|
|
39450
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
42012
|
-
|
|
42013
|
-
|
|
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 =
|
|
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:
|
|
42032
|
-
needsFullConfig:
|
|
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:
|
|
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:
|
|
42111
|
-
needsFullConfig:
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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
|
|
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.
|
|
43728
|
+
this.broadcastShareSnapshot();
|
|
43460
43729
|
}
|
|
43461
43730
|
}
|
|
43462
43731
|
/**
|