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