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.es.js
CHANGED
|
@@ -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.
|
|
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.
|
|
39367
|
-
this.
|
|
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.
|
|
39446
|
-
*
|
|
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
|
|
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.
|
|
@@ -41871,7 +41998,6 @@ function isConfigPayloadExpired(expiresAt, now = Date.now()) {
|
|
|
41871
41998
|
}
|
|
41872
41999
|
const PII_VALUE_ENCODING = "hmac-v1";
|
|
41873
42000
|
const textEncoder = new TextEncoder();
|
|
41874
|
-
const HASH_HEX_PATTERN = /^[0-9a-f]{64}$/;
|
|
41875
42001
|
function normalizePiiString(value) {
|
|
41876
42002
|
return value.trim().toLowerCase();
|
|
41877
42003
|
}
|
|
@@ -41899,34 +42025,109 @@ function isPiiAttribute(attribute, piiAttributes) {
|
|
|
41899
42025
|
}
|
|
41900
42026
|
return false;
|
|
41901
42027
|
}
|
|
42028
|
+
function payloadHasExplicitPiiFields(payload) {
|
|
42029
|
+
if (!payload)
|
|
42030
|
+
return false;
|
|
42031
|
+
return payload.piiSalt !== void 0 || payload.piiAttributes !== void 0 || payload.piiValueEncoding !== void 0;
|
|
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
|
+
}
|
|
41902
42074
|
function piiWireOptionsFromPayload(payload) {
|
|
41903
42075
|
if (!payload)
|
|
41904
42076
|
return null;
|
|
42077
|
+
if (hasExplicitInvalidPiiEncoding(payload))
|
|
42078
|
+
return null;
|
|
42079
|
+
if (hasMalformedPiiAttributes(payload))
|
|
42080
|
+
return null;
|
|
41905
42081
|
const salt = typeof payload.piiSalt === "string" ? payload.piiSalt : "";
|
|
41906
42082
|
if (!salt)
|
|
41907
42083
|
return null;
|
|
41908
|
-
const
|
|
41909
|
-
if (encoding !== PII_VALUE_ENCODING)
|
|
41910
|
-
return null;
|
|
41911
|
-
const attrs = Array.isArray(payload.piiAttributes) ? payload.piiAttributes.filter((k) => typeof k === "string") : [];
|
|
42084
|
+
const attrs = piiAttributeKeysFromPayload(payload);
|
|
41912
42085
|
return {
|
|
41913
42086
|
piiAttributes: attrs,
|
|
41914
42087
|
piiSalt: salt,
|
|
41915
42088
|
piiValueEncoding: PII_VALUE_ENCODING
|
|
41916
42089
|
};
|
|
41917
42090
|
}
|
|
41918
|
-
function
|
|
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
|
+
}
|
|
42107
|
+
function resolvePiiWireForDelta(previous, action) {
|
|
42108
|
+
if (!payloadHasExplicitPiiFields(action)) {
|
|
42109
|
+
return previous;
|
|
42110
|
+
}
|
|
42111
|
+
const merged = {
|
|
42112
|
+
piiSalt: action.piiSalt !== void 0 ? action.piiSalt : previous?.piiSalt,
|
|
42113
|
+
piiAttributes: action.piiAttributes !== void 0 ? action.piiAttributes : previous?.piiAttributes,
|
|
42114
|
+
piiValueEncoding: action.piiValueEncoding !== void 0 ? action.piiValueEncoding : previous?.piiValueEncoding
|
|
42115
|
+
};
|
|
42116
|
+
return piiWireOptionsFromPayload(merged);
|
|
42117
|
+
}
|
|
42118
|
+
function applyPiiHashToFlatContext(context, options, mode) {
|
|
41919
42119
|
if (!options || !options.piiSalt || !options.piiAttributes.length || options.piiValueEncoding !== PII_VALUE_ENCODING) {
|
|
41920
42120
|
return context;
|
|
41921
42121
|
}
|
|
42122
|
+
if (mode?.alreadyHashed) {
|
|
42123
|
+
return context;
|
|
42124
|
+
}
|
|
41922
42125
|
let changed = false;
|
|
41923
42126
|
const out = { ...context };
|
|
41924
42127
|
for (const [key2, value] of Object.entries(context)) {
|
|
41925
42128
|
if (!isPiiAttribute(key2, options.piiAttributes))
|
|
41926
42129
|
continue;
|
|
41927
42130
|
if (typeof value === "string") {
|
|
41928
|
-
if (HASH_HEX_PATTERN.test(value))
|
|
41929
|
-
continue;
|
|
41930
42131
|
out[key2] = hashPiiValue(value, options.piiSalt);
|
|
41931
42132
|
changed = true;
|
|
41932
42133
|
continue;
|
|
@@ -41936,8 +42137,6 @@ function applyPiiHashToFlatContext(context, options) {
|
|
|
41936
42137
|
const next = value.map((entry) => {
|
|
41937
42138
|
if (typeof entry !== "string")
|
|
41938
42139
|
return entry;
|
|
41939
|
-
if (HASH_HEX_PATTERN.test(entry))
|
|
41940
|
-
return entry;
|
|
41941
42140
|
arrayChanged = true;
|
|
41942
42141
|
return hashPiiValue(entry, options.piiSalt);
|
|
41943
42142
|
});
|
|
@@ -41957,7 +42156,8 @@ function createEmptyRulesState() {
|
|
|
41957
42156
|
segments: /* @__PURE__ */ new Map(),
|
|
41958
42157
|
ready: false,
|
|
41959
42158
|
needsFullConfig: true,
|
|
41960
|
-
piiWire: null
|
|
42159
|
+
piiWire: null,
|
|
42160
|
+
piiWireBroken: false
|
|
41961
42161
|
};
|
|
41962
42162
|
}
|
|
41963
42163
|
function cloneState(state) {
|
|
@@ -41972,7 +42172,8 @@ function cloneState(state) {
|
|
|
41972
42172
|
piiAttributes: [...state.piiWire.piiAttributes],
|
|
41973
42173
|
piiSalt: state.piiWire.piiSalt,
|
|
41974
42174
|
piiValueEncoding: state.piiWire.piiValueEncoding
|
|
41975
|
-
} : null
|
|
42175
|
+
} : null,
|
|
42176
|
+
piiWireBroken: state.piiWireBroken
|
|
41976
42177
|
};
|
|
41977
42178
|
}
|
|
41978
42179
|
function segmentsFromRecord(record) {
|
|
@@ -41999,14 +42200,18 @@ function hydrateFromFull(action) {
|
|
|
41999
42200
|
if (flag?.key)
|
|
42000
42201
|
flags.set(flag.key, flag);
|
|
42001
42202
|
}
|
|
42203
|
+
const piiWire = piiWireOptionsFromPayload(action);
|
|
42204
|
+
const piiWireBroken = isPiiWireBroken(action, piiWire, false);
|
|
42002
42205
|
return {
|
|
42003
42206
|
version: action.version,
|
|
42004
42207
|
expiresAt: action.expiresAt,
|
|
42005
42208
|
flags,
|
|
42006
42209
|
segments: segmentsFromRecord(action.segments),
|
|
42007
|
-
|
|
42008
|
-
|
|
42009
|
-
|
|
42210
|
+
// Usable rules, but PII metadata must be valid when hashing is required.
|
|
42211
|
+
ready: !piiWireBroken,
|
|
42212
|
+
needsFullConfig: piiWireBroken,
|
|
42213
|
+
piiWire,
|
|
42214
|
+
piiWireBroken
|
|
42010
42215
|
};
|
|
42011
42216
|
}
|
|
42012
42217
|
function applyPatch(state, action) {
|
|
@@ -42018,15 +42223,20 @@ function applyPatch(state, action) {
|
|
|
42018
42223
|
if (flag?.key)
|
|
42019
42224
|
flags.set(flag.key, flag);
|
|
42020
42225
|
}
|
|
42021
|
-
const nextPii =
|
|
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;
|
|
42022
42231
|
return {
|
|
42023
42232
|
version: action.toVersion,
|
|
42024
42233
|
expiresAt: typeof action.expiresAt === "number" ? action.expiresAt : state.expiresAt,
|
|
42025
42234
|
flags,
|
|
42026
42235
|
segments: applySegmentUpserts(state.segments, action.segments),
|
|
42027
|
-
ready:
|
|
42028
|
-
needsFullConfig:
|
|
42029
|
-
piiWire: nextPii
|
|
42236
|
+
ready: !piiWireBroken,
|
|
42237
|
+
needsFullConfig: piiWireBroken,
|
|
42238
|
+
piiWire: nextPii,
|
|
42239
|
+
piiWireBroken
|
|
42030
42240
|
};
|
|
42031
42241
|
}
|
|
42032
42242
|
function reduceRules(state, action, now = Date.now()) {
|
|
@@ -42046,8 +42256,8 @@ function reduceRules(state, action, now = Date.now()) {
|
|
|
42046
42256
|
...cloneState(state),
|
|
42047
42257
|
version: state.version,
|
|
42048
42258
|
expiresAt: action.expiresAt,
|
|
42049
|
-
ready:
|
|
42050
|
-
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
|
|
42051
42261
|
}
|
|
42052
42262
|
};
|
|
42053
42263
|
}
|
|
@@ -42103,8 +42313,8 @@ function reduceRules(state, action, now = Date.now()) {
|
|
|
42103
42313
|
...current,
|
|
42104
42314
|
version: action.toVersion,
|
|
42105
42315
|
expiresAt: typeof action.expiresAt === "number" ? action.expiresAt : current.expiresAt,
|
|
42106
|
-
ready:
|
|
42107
|
-
needsFullConfig:
|
|
42316
|
+
ready: !current.piiWireBroken,
|
|
42317
|
+
needsFullConfig: current.piiWireBroken
|
|
42108
42318
|
}
|
|
42109
42319
|
};
|
|
42110
42320
|
}
|
|
@@ -42127,9 +42337,12 @@ class RulesStore {
|
|
|
42127
42337
|
return this.state;
|
|
42128
42338
|
}
|
|
42129
42339
|
/**
|
|
42130
|
-
* Fail-closed readiness: lease not expired
|
|
42340
|
+
* Fail-closed readiness: lease not expired, store marked ready, and PII
|
|
42341
|
+
* wire metadata usable when hashing is required.
|
|
42131
42342
|
*/
|
|
42132
42343
|
isReady(now = Date.now()) {
|
|
42344
|
+
if (this.state.piiWireBroken)
|
|
42345
|
+
return false;
|
|
42133
42346
|
if (!this.state.ready)
|
|
42134
42347
|
return false;
|
|
42135
42348
|
if (!this.state.expiresAt)
|
|
@@ -42213,7 +42426,9 @@ class RulesStore {
|
|
|
42213
42426
|
piiAttributes: [...pii.piiAttributes],
|
|
42214
42427
|
piiSalt: pii.piiSalt,
|
|
42215
42428
|
piiValueEncoding: pii.piiValueEncoding
|
|
42216
|
-
} : {}
|
|
42429
|
+
} : {},
|
|
42430
|
+
// Persist fail-closed even when piiWire is null (unusable salt/encoding).
|
|
42431
|
+
...this.state.piiWireBroken ? { piiWireBroken: true } : {}
|
|
42217
42432
|
};
|
|
42218
42433
|
}
|
|
42219
42434
|
hydrateFromSnapshot(snapshot, now = Date.now()) {
|
|
@@ -42223,14 +42438,17 @@ class RulesStore {
|
|
|
42223
42438
|
flags.set(flag.key, flag);
|
|
42224
42439
|
}
|
|
42225
42440
|
const expired = isConfigPayloadExpired(snapshot.expiresAt, now);
|
|
42441
|
+
const piiWire = piiWireOptionsFromPayload(snapshot);
|
|
42442
|
+
const piiWireBroken = snapshot.piiWireBroken === true || isPiiWireBroken(snapshot, piiWire, false);
|
|
42226
42443
|
this.state = {
|
|
42227
42444
|
version: snapshot.version,
|
|
42228
42445
|
expiresAt: snapshot.expiresAt,
|
|
42229
42446
|
flags,
|
|
42230
42447
|
segments: segmentsFromRecord(snapshot.segments),
|
|
42231
|
-
ready: !expired && flags.size > 0,
|
|
42232
|
-
needsFullConfig: expired || flags.size === 0,
|
|
42233
|
-
piiWire
|
|
42448
|
+
ready: !expired && flags.size > 0 && !piiWireBroken,
|
|
42449
|
+
needsFullConfig: expired || flags.size === 0 || piiWireBroken,
|
|
42450
|
+
piiWire,
|
|
42451
|
+
piiWireBroken
|
|
42234
42452
|
};
|
|
42235
42453
|
}
|
|
42236
42454
|
}
|
|
@@ -42432,7 +42650,7 @@ function flattenEvaluationContext(context) {
|
|
|
42432
42650
|
function toComparableString(value) {
|
|
42433
42651
|
if (value === null || value === void 0)
|
|
42434
42652
|
return "";
|
|
42435
|
-
return String(value);
|
|
42653
|
+
return String(value).trim().toLowerCase();
|
|
42436
42654
|
}
|
|
42437
42655
|
function toComparableArray(value) {
|
|
42438
42656
|
if (Array.isArray(value)) {
|
|
@@ -43150,11 +43368,11 @@ class FlagClient {
|
|
|
43150
43368
|
this.env = options.env;
|
|
43151
43369
|
this.enableFlagmint = options.enableFlagmint ?? true;
|
|
43152
43370
|
this.configSync = options.configSync ?? false;
|
|
43153
|
-
|
|
43371
|
+
logger.setup({ debugLog: options.debugLog });
|
|
43372
|
+
this.shareConnection = options.shareConnection ?? isConnectionSharingAvailable();
|
|
43154
43373
|
if (this.configSync) {
|
|
43155
43374
|
this.rulesStore = new RulesStore();
|
|
43156
43375
|
}
|
|
43157
|
-
logger.setup({ debugLog: options.debugLog });
|
|
43158
43376
|
if (this.previewMode && this.rawFlags && Object.keys(this.rawFlags).length > 0) {
|
|
43159
43377
|
this.flags = this.evaluateLocally(this.rawFlags, this.context);
|
|
43160
43378
|
this.readyPromise = Promise.resolve();
|
|
@@ -43218,7 +43436,7 @@ class FlagClient {
|
|
|
43218
43436
|
return;
|
|
43219
43437
|
}
|
|
43220
43438
|
await this.setupTransport(options);
|
|
43221
|
-
this.
|
|
43439
|
+
this.broadcastShareSnapshot();
|
|
43222
43440
|
this.isInitialized = true;
|
|
43223
43441
|
this.resolveReady();
|
|
43224
43442
|
} catch (err) {
|
|
@@ -43258,19 +43476,26 @@ class FlagClient {
|
|
|
43258
43476
|
* @returns true when this document is a follower and should skip opening a stream.
|
|
43259
43477
|
*/
|
|
43260
43478
|
async joinConnectionShare(options) {
|
|
43261
|
-
if (!this.shareConnection
|
|
43479
|
+
if (!this.shareConnection) {
|
|
43480
|
+
return false;
|
|
43481
|
+
}
|
|
43482
|
+
if (options.transport && !options.share) {
|
|
43262
43483
|
return false;
|
|
43263
43484
|
}
|
|
43264
43485
|
if (!options.share && !isConnectionSharingAvailable()) {
|
|
43265
43486
|
return false;
|
|
43266
43487
|
}
|
|
43267
|
-
|
|
43488
|
+
const shareMode = this.configSync ? "rules" : "evaluated";
|
|
43489
|
+
this.shareHub = new ConnectionShareHub(this.apiKey, {
|
|
43490
|
+
...options.share,
|
|
43491
|
+
mode: shareMode
|
|
43492
|
+
});
|
|
43268
43493
|
this.shareHub.onPromote(async () => {
|
|
43269
43494
|
logger.log("[FlagClient] Share leader departed. Promoting this document to hold the stream.");
|
|
43270
43495
|
try {
|
|
43271
43496
|
await this.setupTransport(options);
|
|
43272
43497
|
this.attachShareLeader();
|
|
43273
|
-
this.
|
|
43498
|
+
this.broadcastShareSnapshot();
|
|
43274
43499
|
} catch (err) {
|
|
43275
43500
|
const error2 = err instanceof Error ? err : new Error(String(err));
|
|
43276
43501
|
logger.error("[FlagClient] Promotion to share leader failed:", error2);
|
|
@@ -43279,7 +43504,12 @@ class FlagClient {
|
|
|
43279
43504
|
});
|
|
43280
43505
|
const role = await this.shareHub.join();
|
|
43281
43506
|
if (role === "follower") {
|
|
43282
|
-
logger.log(
|
|
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
|
+
}
|
|
43283
43513
|
this.transport = this.shareHub.createFollowerTransport();
|
|
43284
43514
|
this.bindTransport(this.transport);
|
|
43285
43515
|
await this.transport.init();
|
|
@@ -43288,8 +43518,15 @@ class FlagClient {
|
|
|
43288
43518
|
this.attachShareLeader();
|
|
43289
43519
|
return false;
|
|
43290
43520
|
}
|
|
43521
|
+
/**
|
|
43522
|
+
* Leader: wire context RPC for evaluated share only.
|
|
43523
|
+
* Rules share never forwards follower context onto the stream.
|
|
43524
|
+
*/
|
|
43291
43525
|
attachShareLeader() {
|
|
43292
|
-
this.shareHub
|
|
43526
|
+
if (!this.shareHub || this.shareHub.shareMode === "rules") {
|
|
43527
|
+
return;
|
|
43528
|
+
}
|
|
43529
|
+
this.shareHub.setLeaderContextHandler(async (context, options) => {
|
|
43293
43530
|
const mergedContext = toJsonCloneable({ ...this.context, ...context });
|
|
43294
43531
|
if (options?.persist === true) {
|
|
43295
43532
|
this.context = mergedContext;
|
|
@@ -43298,9 +43535,6 @@ class FlagClient {
|
|
|
43298
43535
|
}
|
|
43299
43536
|
}
|
|
43300
43537
|
if (!this.transport || typeof this.transport.fetchFlags !== "function") {
|
|
43301
|
-
if (this.configSync && this.rulesStore) {
|
|
43302
|
-
return this.evaluateFromRulesStore(mergedContext);
|
|
43303
|
-
}
|
|
43304
43538
|
return this.flags;
|
|
43305
43539
|
}
|
|
43306
43540
|
return this.transport.fetchFlags(mergedContext, {
|
|
@@ -43308,6 +43542,41 @@ class FlagClient {
|
|
|
43308
43542
|
});
|
|
43309
43543
|
});
|
|
43310
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
|
+
}
|
|
43311
43580
|
/**
|
|
43312
43581
|
* Creates and initializes the transport layer used to receive feature flags
|
|
43313
43582
|
* updates from the Flagmint platform.
|
|
@@ -43452,7 +43721,7 @@ class FlagClient {
|
|
|
43452
43721
|
}
|
|
43453
43722
|
this.notifySubscribers();
|
|
43454
43723
|
if (!fromShare) {
|
|
43455
|
-
this.
|
|
43724
|
+
this.broadcastShareSnapshot();
|
|
43456
43725
|
}
|
|
43457
43726
|
}
|
|
43458
43727
|
/**
|