flagmint-js-sdk 2.2.2-rc.0 → 2.2.3-rc.0
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 +2 -2
- package/dist/flagmint.cjs.js +131 -32
- package/dist/flagmint.cjs.js.map +1 -1
- package/dist/flagmint.es.js +131 -32
- package/dist/flagmint.es.js.map +1 -1
- package/dist/flagmint.umd.js +131 -32
- package/dist/flagmint.umd.js.map +1 -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/cacheHelper.d.ts +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -133,7 +133,7 @@ After `ready()`:
|
|
|
133
133
|
| `transportMode` | `'auto' \| 'sse' \| 'long-polling'` | `'auto'` | `auto` tries SSE, then long-polling |
|
|
134
134
|
| `EventSourceImpl` | EventSource | browser global | Required in Node |
|
|
135
135
|
| `shareConnection` | boolean | `true` in browsers with `BroadcastChannel`, `false` in Node | One stream per API key across same-origin documents |
|
|
136
|
-
| `enableOfflineCache` | boolean | `true` |
|
|
136
|
+
| `enableOfflineCache` | boolean | `true` | Server-eval: load/save evaluated flags. Config-sync: load/save **rules** only (evaluated maps are not cached — they are per-user and would collide across tabs). |
|
|
137
137
|
| `persistContext` | boolean | `false` | Persist context in the cache adapter |
|
|
138
138
|
| `cacheAdapter` | CacheAdapter | localStorage helpers | Custom cache (Redis, files, …) |
|
|
139
139
|
| `restEndpoint` | string | env default | Override the long-polling / evaluate URL |
|
|
@@ -162,7 +162,7 @@ transportMode: 'long-polling' // POST evaluate on an interval
|
|
|
162
162
|
|
|
163
163
|
# Connection sharing
|
|
164
164
|
|
|
165
|
-
Used when the same app is embedded twice on one origin (for example two
|
|
165
|
+
Used when the same app is embedded twice on one origin (for example two iframes in a host page).
|
|
166
166
|
|
|
167
167
|
- **Leader** performs handshake and holds EventSource.
|
|
168
168
|
- **Followers** receive flag snapshots over `BroadcastChannel` (`flagmint-share:<apiKey>`). Leadership is a `localStorage` lock (`flagmint_share_lock:<apiKey>`).
|
package/dist/flagmint.cjs.js
CHANGED
|
@@ -38621,6 +38621,12 @@ function saveCachedFlags$1(apiKey, data) {
|
|
|
38621
38621
|
} catch {
|
|
38622
38622
|
}
|
|
38623
38623
|
}
|
|
38624
|
+
function clearCachedFlags(apiKey) {
|
|
38625
|
+
try {
|
|
38626
|
+
storage$1.removeItem?.(getFlagsKey$1(apiKey));
|
|
38627
|
+
} catch {
|
|
38628
|
+
}
|
|
38629
|
+
}
|
|
38624
38630
|
function loadCachedContext$1(apiKey) {
|
|
38625
38631
|
try {
|
|
38626
38632
|
const raw = storage$1.getItem(getContextKey$1(apiKey));
|
|
@@ -38659,6 +38665,7 @@ function clearCachedRulesSnapshot$1(apiKey) {
|
|
|
38659
38665
|
}
|
|
38660
38666
|
const cacheHelper = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
38661
38667
|
__proto__: null,
|
|
38668
|
+
clearCachedFlags,
|
|
38662
38669
|
clearCachedRulesSnapshot: clearCachedRulesSnapshot$1,
|
|
38663
38670
|
loadCachedContext: loadCachedContext$1,
|
|
38664
38671
|
loadCachedFlags: loadCachedFlags$1,
|
|
@@ -39093,7 +39100,7 @@ class SseTransport {
|
|
|
39093
39100
|
this.connectPromise = new Promise((resolve, reject) => {
|
|
39094
39101
|
this.connectResolve = resolve;
|
|
39095
39102
|
this.connectReject = reject;
|
|
39096
|
-
const sdkVersion = "2.2.
|
|
39103
|
+
const sdkVersion = "2.2.3-rc.0";
|
|
39097
39104
|
const platform2 = typeof window !== "undefined" ? "browser" : "nodejs";
|
|
39098
39105
|
const wrapperName = this.configOptions?.wrapper?.name || "native-js";
|
|
39099
39106
|
const wrapperVersion = this.configOptions?.wrapper?.version || "none";
|
|
@@ -41873,7 +41880,6 @@ function isConfigPayloadExpired(expiresAt, now = Date.now()) {
|
|
|
41873
41880
|
}
|
|
41874
41881
|
const PII_VALUE_ENCODING = "hmac-v1";
|
|
41875
41882
|
const textEncoder = new TextEncoder();
|
|
41876
|
-
const HASH_HEX_PATTERN = /^[0-9a-f]{64}$/;
|
|
41877
41883
|
function normalizePiiString(value) {
|
|
41878
41884
|
return value.trim().toLowerCase();
|
|
41879
41885
|
}
|
|
@@ -41901,34 +41907,109 @@ function isPiiAttribute(attribute, piiAttributes) {
|
|
|
41901
41907
|
}
|
|
41902
41908
|
return false;
|
|
41903
41909
|
}
|
|
41910
|
+
function payloadHasExplicitPiiFields(payload) {
|
|
41911
|
+
if (!payload)
|
|
41912
|
+
return false;
|
|
41913
|
+
return payload.piiSalt !== void 0 || payload.piiAttributes !== void 0 || payload.piiValueEncoding !== void 0;
|
|
41914
|
+
}
|
|
41915
|
+
function piiAttributeKeysFromPayload(payload) {
|
|
41916
|
+
if (!payload || !Array.isArray(payload.piiAttributes))
|
|
41917
|
+
return [];
|
|
41918
|
+
return payload.piiAttributes.filter((k) => typeof k === "string" && k.length > 0);
|
|
41919
|
+
}
|
|
41920
|
+
function hasMalformedPiiAttributes(payload) {
|
|
41921
|
+
if (!payload || payload.piiAttributes === void 0)
|
|
41922
|
+
return false;
|
|
41923
|
+
if (!Array.isArray(payload.piiAttributes))
|
|
41924
|
+
return true;
|
|
41925
|
+
return payload.piiAttributes.some(
|
|
41926
|
+
(entry) => typeof entry !== "string" || entry.length === 0
|
|
41927
|
+
);
|
|
41928
|
+
}
|
|
41929
|
+
function hasExplicitInvalidPiiEncoding(payload) {
|
|
41930
|
+
if (!payload || payload.piiValueEncoding === void 0)
|
|
41931
|
+
return false;
|
|
41932
|
+
return payload.piiValueEncoding !== PII_VALUE_ENCODING;
|
|
41933
|
+
}
|
|
41934
|
+
function payloadRequiresUsablePiiWire(payload) {
|
|
41935
|
+
if (!payload)
|
|
41936
|
+
return false;
|
|
41937
|
+
if (hasExplicitInvalidPiiEncoding(payload))
|
|
41938
|
+
return true;
|
|
41939
|
+
if (hasMalformedPiiAttributes(payload))
|
|
41940
|
+
return true;
|
|
41941
|
+
if (piiAttributeKeysFromPayload(payload).length > 0)
|
|
41942
|
+
return true;
|
|
41943
|
+
return false;
|
|
41944
|
+
}
|
|
41945
|
+
function isExplicitPiiRequirementCleared(payload) {
|
|
41946
|
+
if (!payload || payload.piiAttributes === void 0)
|
|
41947
|
+
return false;
|
|
41948
|
+
if (!Array.isArray(payload.piiAttributes))
|
|
41949
|
+
return false;
|
|
41950
|
+
if (payload.piiAttributes.length !== 0)
|
|
41951
|
+
return false;
|
|
41952
|
+
if (hasExplicitInvalidPiiEncoding(payload))
|
|
41953
|
+
return false;
|
|
41954
|
+
return true;
|
|
41955
|
+
}
|
|
41904
41956
|
function piiWireOptionsFromPayload(payload) {
|
|
41905
41957
|
if (!payload)
|
|
41906
41958
|
return null;
|
|
41959
|
+
if (hasExplicitInvalidPiiEncoding(payload))
|
|
41960
|
+
return null;
|
|
41961
|
+
if (hasMalformedPiiAttributes(payload))
|
|
41962
|
+
return null;
|
|
41907
41963
|
const salt = typeof payload.piiSalt === "string" ? payload.piiSalt : "";
|
|
41908
41964
|
if (!salt)
|
|
41909
41965
|
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") : [];
|
|
41966
|
+
const attrs = piiAttributeKeysFromPayload(payload);
|
|
41914
41967
|
return {
|
|
41915
41968
|
piiAttributes: attrs,
|
|
41916
41969
|
piiSalt: salt,
|
|
41917
41970
|
piiValueEncoding: PII_VALUE_ENCODING
|
|
41918
41971
|
};
|
|
41919
41972
|
}
|
|
41920
|
-
function
|
|
41973
|
+
function isPiiWireBroken(payload, resolved, previousPriorHadPiiAttrs = false) {
|
|
41974
|
+
if (hasExplicitInvalidPiiEncoding(payload) || hasMalformedPiiAttributes(payload)) {
|
|
41975
|
+
return true;
|
|
41976
|
+
}
|
|
41977
|
+
if (resolved) {
|
|
41978
|
+
return false;
|
|
41979
|
+
}
|
|
41980
|
+
if (isExplicitPiiRequirementCleared(payload))
|
|
41981
|
+
return false;
|
|
41982
|
+
if (payloadRequiresUsablePiiWire(payload))
|
|
41983
|
+
return true;
|
|
41984
|
+
if (previousPriorHadPiiAttrs && payloadHasExplicitPiiFields(payload)) {
|
|
41985
|
+
return true;
|
|
41986
|
+
}
|
|
41987
|
+
return false;
|
|
41988
|
+
}
|
|
41989
|
+
function resolvePiiWireForDelta(previous, action) {
|
|
41990
|
+
if (!payloadHasExplicitPiiFields(action)) {
|
|
41991
|
+
return previous;
|
|
41992
|
+
}
|
|
41993
|
+
const merged = {
|
|
41994
|
+
piiSalt: action.piiSalt !== void 0 ? action.piiSalt : previous?.piiSalt,
|
|
41995
|
+
piiAttributes: action.piiAttributes !== void 0 ? action.piiAttributes : previous?.piiAttributes,
|
|
41996
|
+
piiValueEncoding: action.piiValueEncoding !== void 0 ? action.piiValueEncoding : previous?.piiValueEncoding
|
|
41997
|
+
};
|
|
41998
|
+
return piiWireOptionsFromPayload(merged);
|
|
41999
|
+
}
|
|
42000
|
+
function applyPiiHashToFlatContext(context, options, mode) {
|
|
41921
42001
|
if (!options || !options.piiSalt || !options.piiAttributes.length || options.piiValueEncoding !== PII_VALUE_ENCODING) {
|
|
41922
42002
|
return context;
|
|
41923
42003
|
}
|
|
42004
|
+
if (mode?.alreadyHashed) {
|
|
42005
|
+
return context;
|
|
42006
|
+
}
|
|
41924
42007
|
let changed = false;
|
|
41925
42008
|
const out = { ...context };
|
|
41926
42009
|
for (const [key2, value] of Object.entries(context)) {
|
|
41927
42010
|
if (!isPiiAttribute(key2, options.piiAttributes))
|
|
41928
42011
|
continue;
|
|
41929
42012
|
if (typeof value === "string") {
|
|
41930
|
-
if (HASH_HEX_PATTERN.test(value))
|
|
41931
|
-
continue;
|
|
41932
42013
|
out[key2] = hashPiiValue(value, options.piiSalt);
|
|
41933
42014
|
changed = true;
|
|
41934
42015
|
continue;
|
|
@@ -41938,8 +42019,6 @@ function applyPiiHashToFlatContext(context, options) {
|
|
|
41938
42019
|
const next = value.map((entry) => {
|
|
41939
42020
|
if (typeof entry !== "string")
|
|
41940
42021
|
return entry;
|
|
41941
|
-
if (HASH_HEX_PATTERN.test(entry))
|
|
41942
|
-
return entry;
|
|
41943
42022
|
arrayChanged = true;
|
|
41944
42023
|
return hashPiiValue(entry, options.piiSalt);
|
|
41945
42024
|
});
|
|
@@ -41959,7 +42038,8 @@ function createEmptyRulesState() {
|
|
|
41959
42038
|
segments: /* @__PURE__ */ new Map(),
|
|
41960
42039
|
ready: false,
|
|
41961
42040
|
needsFullConfig: true,
|
|
41962
|
-
piiWire: null
|
|
42041
|
+
piiWire: null,
|
|
42042
|
+
piiWireBroken: false
|
|
41963
42043
|
};
|
|
41964
42044
|
}
|
|
41965
42045
|
function cloneState(state) {
|
|
@@ -41974,7 +42054,8 @@ function cloneState(state) {
|
|
|
41974
42054
|
piiAttributes: [...state.piiWire.piiAttributes],
|
|
41975
42055
|
piiSalt: state.piiWire.piiSalt,
|
|
41976
42056
|
piiValueEncoding: state.piiWire.piiValueEncoding
|
|
41977
|
-
} : null
|
|
42057
|
+
} : null,
|
|
42058
|
+
piiWireBroken: state.piiWireBroken
|
|
41978
42059
|
};
|
|
41979
42060
|
}
|
|
41980
42061
|
function segmentsFromRecord(record) {
|
|
@@ -42001,14 +42082,18 @@ function hydrateFromFull(action) {
|
|
|
42001
42082
|
if (flag?.key)
|
|
42002
42083
|
flags.set(flag.key, flag);
|
|
42003
42084
|
}
|
|
42085
|
+
const piiWire = piiWireOptionsFromPayload(action);
|
|
42086
|
+
const piiWireBroken = isPiiWireBroken(action, piiWire, false);
|
|
42004
42087
|
return {
|
|
42005
42088
|
version: action.version,
|
|
42006
42089
|
expiresAt: action.expiresAt,
|
|
42007
42090
|
flags,
|
|
42008
42091
|
segments: segmentsFromRecord(action.segments),
|
|
42009
|
-
|
|
42010
|
-
|
|
42011
|
-
|
|
42092
|
+
// Usable rules, but PII metadata must be valid when hashing is required.
|
|
42093
|
+
ready: !piiWireBroken,
|
|
42094
|
+
needsFullConfig: piiWireBroken,
|
|
42095
|
+
piiWire,
|
|
42096
|
+
piiWireBroken
|
|
42012
42097
|
};
|
|
42013
42098
|
}
|
|
42014
42099
|
function applyPatch(state, action) {
|
|
@@ -42020,15 +42105,20 @@ function applyPatch(state, action) {
|
|
|
42020
42105
|
if (flag?.key)
|
|
42021
42106
|
flags.set(flag.key, flag);
|
|
42022
42107
|
}
|
|
42023
|
-
const nextPii =
|
|
42108
|
+
const nextPii = resolvePiiWireForDelta(state.piiWire, action);
|
|
42109
|
+
const priorHadPiiAttrs = (state.piiWire?.piiAttributes.length ?? 0) > 0 || state.piiWireBroken;
|
|
42110
|
+
const computedBroken = isPiiWireBroken(action, nextPii, priorHadPiiAttrs);
|
|
42111
|
+
const recoveredWithRequiredAttrs = !!nextPii && nextPii.piiAttributes.length > 0;
|
|
42112
|
+
const piiWireBroken = isExplicitPiiRequirementCleared(action) ? false : recoveredWithRequiredAttrs ? false : computedBroken || state.piiWireBroken;
|
|
42024
42113
|
return {
|
|
42025
42114
|
version: action.toVersion,
|
|
42026
42115
|
expiresAt: typeof action.expiresAt === "number" ? action.expiresAt : state.expiresAt,
|
|
42027
42116
|
flags,
|
|
42028
42117
|
segments: applySegmentUpserts(state.segments, action.segments),
|
|
42029
|
-
ready:
|
|
42030
|
-
needsFullConfig:
|
|
42031
|
-
piiWire: nextPii
|
|
42118
|
+
ready: !piiWireBroken,
|
|
42119
|
+
needsFullConfig: piiWireBroken,
|
|
42120
|
+
piiWire: nextPii,
|
|
42121
|
+
piiWireBroken
|
|
42032
42122
|
};
|
|
42033
42123
|
}
|
|
42034
42124
|
function reduceRules(state, action, now = Date.now()) {
|
|
@@ -42048,8 +42138,8 @@ function reduceRules(state, action, now = Date.now()) {
|
|
|
42048
42138
|
...cloneState(state),
|
|
42049
42139
|
version: state.version,
|
|
42050
42140
|
expiresAt: action.expiresAt,
|
|
42051
|
-
ready:
|
|
42052
|
-
needsFullConfig: state.flags.size === 0 || versionMismatch ? true : state.needsFullConfig
|
|
42141
|
+
ready: !state.piiWireBroken,
|
|
42142
|
+
needsFullConfig: state.piiWireBroken || state.flags.size === 0 || versionMismatch ? true : state.needsFullConfig
|
|
42053
42143
|
}
|
|
42054
42144
|
};
|
|
42055
42145
|
}
|
|
@@ -42105,8 +42195,8 @@ function reduceRules(state, action, now = Date.now()) {
|
|
|
42105
42195
|
...current,
|
|
42106
42196
|
version: action.toVersion,
|
|
42107
42197
|
expiresAt: typeof action.expiresAt === "number" ? action.expiresAt : current.expiresAt,
|
|
42108
|
-
ready:
|
|
42109
|
-
needsFullConfig:
|
|
42198
|
+
ready: !current.piiWireBroken,
|
|
42199
|
+
needsFullConfig: current.piiWireBroken
|
|
42110
42200
|
}
|
|
42111
42201
|
};
|
|
42112
42202
|
}
|
|
@@ -42129,9 +42219,12 @@ class RulesStore {
|
|
|
42129
42219
|
return this.state;
|
|
42130
42220
|
}
|
|
42131
42221
|
/**
|
|
42132
|
-
* Fail-closed readiness: lease not expired
|
|
42222
|
+
* Fail-closed readiness: lease not expired, store marked ready, and PII
|
|
42223
|
+
* wire metadata usable when hashing is required.
|
|
42133
42224
|
*/
|
|
42134
42225
|
isReady(now = Date.now()) {
|
|
42226
|
+
if (this.state.piiWireBroken)
|
|
42227
|
+
return false;
|
|
42135
42228
|
if (!this.state.ready)
|
|
42136
42229
|
return false;
|
|
42137
42230
|
if (!this.state.expiresAt)
|
|
@@ -42215,7 +42308,9 @@ class RulesStore {
|
|
|
42215
42308
|
piiAttributes: [...pii.piiAttributes],
|
|
42216
42309
|
piiSalt: pii.piiSalt,
|
|
42217
42310
|
piiValueEncoding: pii.piiValueEncoding
|
|
42218
|
-
} : {}
|
|
42311
|
+
} : {},
|
|
42312
|
+
// Persist fail-closed even when piiWire is null (unusable salt/encoding).
|
|
42313
|
+
...this.state.piiWireBroken ? { piiWireBroken: true } : {}
|
|
42219
42314
|
};
|
|
42220
42315
|
}
|
|
42221
42316
|
hydrateFromSnapshot(snapshot, now = Date.now()) {
|
|
@@ -42225,14 +42320,17 @@ class RulesStore {
|
|
|
42225
42320
|
flags.set(flag.key, flag);
|
|
42226
42321
|
}
|
|
42227
42322
|
const expired = isConfigPayloadExpired(snapshot.expiresAt, now);
|
|
42323
|
+
const piiWire = piiWireOptionsFromPayload(snapshot);
|
|
42324
|
+
const piiWireBroken = snapshot.piiWireBroken === true || isPiiWireBroken(snapshot, piiWire, false);
|
|
42228
42325
|
this.state = {
|
|
42229
42326
|
version: snapshot.version,
|
|
42230
42327
|
expiresAt: snapshot.expiresAt,
|
|
42231
42328
|
flags,
|
|
42232
42329
|
segments: segmentsFromRecord(snapshot.segments),
|
|
42233
|
-
ready: !expired && flags.size > 0,
|
|
42234
|
-
needsFullConfig: expired || flags.size === 0,
|
|
42235
|
-
piiWire
|
|
42330
|
+
ready: !expired && flags.size > 0 && !piiWireBroken,
|
|
42331
|
+
needsFullConfig: expired || flags.size === 0 || piiWireBroken,
|
|
42332
|
+
piiWire,
|
|
42333
|
+
piiWireBroken
|
|
42236
42334
|
};
|
|
42237
42335
|
}
|
|
42238
42336
|
}
|
|
@@ -42434,7 +42532,7 @@ function flattenEvaluationContext(context) {
|
|
|
42434
42532
|
function toComparableString(value) {
|
|
42435
42533
|
if (value === null || value === void 0)
|
|
42436
42534
|
return "";
|
|
42437
|
-
return String(value);
|
|
42535
|
+
return String(value).trim().toLowerCase();
|
|
42438
42536
|
}
|
|
42439
42537
|
function toComparableArray(value) {
|
|
42440
42538
|
if (Array.isArray(value)) {
|
|
@@ -43203,9 +43301,10 @@ class FlagClient {
|
|
|
43203
43301
|
}
|
|
43204
43302
|
let hydratedRules = false;
|
|
43205
43303
|
if (this.configSync && this.rulesStore && this.enableOfflineCache) {
|
|
43304
|
+
clearCachedFlags(this.apiKey);
|
|
43206
43305
|
hydratedRules = await this.hydrateRulesFromCache();
|
|
43207
43306
|
}
|
|
43208
|
-
if (this.enableOfflineCache && !hydratedRules) {
|
|
43307
|
+
if (this.enableOfflineCache && !this.configSync && !hydratedRules) {
|
|
43209
43308
|
const cached = await Promise.resolve(
|
|
43210
43309
|
this.cacheAdapter.loadFlags(this.apiKey, this.cacheTTL)
|
|
43211
43310
|
);
|
|
@@ -43447,7 +43546,7 @@ class FlagClient {
|
|
|
43447
43546
|
*/
|
|
43448
43547
|
updateFlags(newFlags, fromShare = false) {
|
|
43449
43548
|
this.flags = newFlags;
|
|
43450
|
-
if (this.enableOfflineCache) {
|
|
43549
|
+
if (this.enableOfflineCache && !this.configSync) {
|
|
43451
43550
|
void Promise.resolve(
|
|
43452
43551
|
this.cacheAdapter.saveFlags(this.apiKey, newFlags)
|
|
43453
43552
|
);
|