@spotify-confidence/openfeature-server-provider-local 0.15.1 → 0.16.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/CHANGELOG.md +30 -0
- package/dist/client.js +11 -1
- package/dist/confidence_resolver.wasm +0 -0
- package/dist/index.fetch.d.ts +2 -0
- package/dist/index.fetch.js +30 -4
- package/dist/index.inlined.d.ts +2 -0
- package/dist/index.inlined.js +31 -5
- package/dist/index.node.d.ts +2 -0
- package/dist/index.node.js +30 -4
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.16.0](https://github.com/spotify/confidence-resolver/compare/openfeature-provider-js-v0.15.2...openfeature-provider-js-v0.16.0) (2026-07-08)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **js:** decrypt CDN state on host instead of in WASM ([#475](https://github.com/spotify/confidence-resolver/issues/475)) ([99d0f29](https://github.com/spotify/confidence-resolver/commit/99d0f29c60161eb2840d45949b655909a1434973))
|
|
9
|
+
* **js:** publish flag evaluations to window global for session recording ([#445](https://github.com/spotify/confidence-resolver/issues/445)) ([475195b](https://github.com/spotify/confidence-resolver/commit/475195bb6ce3a4c274399193d50cc926bf12d4ba))
|
|
10
|
+
* **js:** support encrypted CDN resolver state ([#457](https://github.com/spotify/confidence-resolver/issues/457)) ([eb37c72](https://github.com/spotify/confidence-resolver/commit/eb37c72d8f22619abb50cbae8fede8ce74c87ea1))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **providers:** flush logs before state update to reduce WASM memory growth ([#469](https://github.com/spotify/confidence-resolver/issues/469)) ([633b269](https://github.com/spotify/confidence-resolver/commit/633b2695e50e7ffff5a3d3b0140fe7bb200ceead))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Dependencies
|
|
19
|
+
|
|
20
|
+
* The following workspace dependencies were updated
|
|
21
|
+
* dependencies
|
|
22
|
+
* rust-guest bumped from 0.2.5 to 0.3.0
|
|
23
|
+
|
|
24
|
+
## [0.15.2](https://github.com/spotify/confidence-resolver/compare/openfeature-provider-js-v0.15.1...openfeature-provider-js-v0.15.2) (2026-06-17)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Dependencies
|
|
28
|
+
|
|
29
|
+
* The following workspace dependencies were updated
|
|
30
|
+
* dependencies
|
|
31
|
+
* rust-guest bumped from 0.2.4 to 0.2.5
|
|
32
|
+
|
|
3
33
|
## [0.15.1](https://github.com/spotify/confidence-resolver/compare/openfeature-provider-js-v0.15.0...openfeature-provider-js-v0.15.1) (2026-06-05)
|
|
4
34
|
|
|
5
35
|
|
package/dist/client.js
CHANGED
|
@@ -146,6 +146,14 @@ function evaluateAssignment(resolvedValue, defaultValue, path) {
|
|
|
146
146
|
}
|
|
147
147
|
return resolvedValue;
|
|
148
148
|
}
|
|
149
|
+
function publishFlagEvaluation(name, variant) {
|
|
150
|
+
if (typeof window === "undefined") return;
|
|
151
|
+
window.__confidence ??= {};
|
|
152
|
+
const confidence = window.__confidence;
|
|
153
|
+
confidence.flags ??= {};
|
|
154
|
+
if (confidence.flags[name]?.variant === variant) return;
|
|
155
|
+
confidence.flags[name] = { variant };
|
|
156
|
+
}
|
|
149
157
|
const ConfidenceContext = createContext(null);
|
|
150
158
|
const warnedFlags = /* @__PURE__ */ new Set();
|
|
151
159
|
function ConfidenceClientProvider({ bundle, apply, children }) {
|
|
@@ -178,10 +186,12 @@ function useFlagDetails(flagKey, defaultValue, options) {
|
|
|
178
186
|
const autoExpose = options?.expose !== false;
|
|
179
187
|
const doExpose = useCallback(() => {
|
|
180
188
|
if (resolution.shouldApply) ctx?.apply(baseFlagName);
|
|
189
|
+
if (resolution.variant) publishFlagEvaluation(`flags/${baseFlagName}`, resolution.variant);
|
|
181
190
|
}, [
|
|
182
191
|
ctx,
|
|
183
192
|
baseFlagName,
|
|
184
|
-
resolution.shouldApply
|
|
193
|
+
resolution.shouldApply,
|
|
194
|
+
resolution.variant
|
|
185
195
|
]);
|
|
186
196
|
useEffect(() => {
|
|
187
197
|
if (autoExpose) doExpose();
|
|
Binary file
|
package/dist/index.fetch.d.ts
CHANGED
|
@@ -486,6 +486,8 @@ type FlagBundle$1 = FlagBundle;
|
|
|
486
486
|
interface SnapshotConfig {}
|
|
487
487
|
interface ProviderOptions {
|
|
488
488
|
flagClientSecret: string;
|
|
489
|
+
/** Hex-encoded AES-256 encryption key for decrypting CDN state. */
|
|
490
|
+
encryptionKey?: string;
|
|
489
491
|
initializeTimeout?: number;
|
|
490
492
|
/** Interval in milliseconds between state polling updates. Defaults to 30000ms. */
|
|
491
493
|
stateUpdateInterval?: number;
|
package/dist/index.fetch.js
CHANGED
|
@@ -1434,7 +1434,7 @@ function isObject(value) {
|
|
|
1434
1434
|
function isSet$3(value) {
|
|
1435
1435
|
return value !== null && value !== void 0;
|
|
1436
1436
|
}
|
|
1437
|
-
const VERSION = "0.
|
|
1437
|
+
const VERSION = "0.16.0";
|
|
1438
1438
|
const NOOP_LOG_FN = Object.assign(() => {}, { enabled: false });
|
|
1439
1439
|
const debugBackend = loadDebug();
|
|
1440
1440
|
const logger$2 = new class LoggerImpl {
|
|
@@ -1586,6 +1586,12 @@ function base64FromBytes$1(arr) {
|
|
|
1586
1586
|
return globalThis.btoa(bin.join(""));
|
|
1587
1587
|
}
|
|
1588
1588
|
}
|
|
1589
|
+
function hexToBytes(hex) {
|
|
1590
|
+
if (hex.length % 2 !== 0) throw new Error("Hex string must have an even number of characters");
|
|
1591
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
1592
|
+
for (let i = 0; i < hex.length; i += 2) bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
|
1593
|
+
return bytes;
|
|
1594
|
+
}
|
|
1589
1595
|
const logger$3 = logger$2.getLogger("fetch");
|
|
1590
1596
|
let Fetch;
|
|
1591
1597
|
(function(_Fetch) {
|
|
@@ -2879,6 +2885,7 @@ var ConfidenceServerProviderLocal = class ConfidenceServerProviderLocal {
|
|
|
2879
2885
|
else this.materializationStore = null;
|
|
2880
2886
|
}
|
|
2881
2887
|
async initialize(context) {
|
|
2888
|
+
if (!this.options.encryptionKey) logger$1.warn("No encryptionKey provided. Falling back to unencrypted state. An encryption key will be required in an upcoming version.");
|
|
2882
2889
|
const signal = this.main.signal;
|
|
2883
2890
|
const initialUpdateSignal = AbortSignal.any([signal, timeoutSignal(this.options.initializeTimeout ?? DEFAULT_INITIALIZE_TIMEOUT)]);
|
|
2884
2891
|
try {
|
|
@@ -2991,7 +2998,9 @@ var ConfidenceServerProviderLocal = class ConfidenceServerProviderLocal {
|
|
|
2991
2998
|
}
|
|
2992
2999
|
}
|
|
2993
3000
|
async updateState(signal) {
|
|
2994
|
-
const
|
|
3001
|
+
const hashHex = await sha256Hex(this.options.flagClientSecret);
|
|
3002
|
+
const { encryptionKey } = this.options;
|
|
3003
|
+
const cdnUrl = `https://confidence-resolver-state-cdn.spotifycdn.com/${encryptionKey ? `${hashHex}.enc` : hashHex}`;
|
|
2995
3004
|
const headers = new Headers();
|
|
2996
3005
|
if (this.stateEtag) headers.set("If-None-Match", this.stateEtag);
|
|
2997
3006
|
const resp = await this.fetch(cdnUrl, {
|
|
@@ -3002,11 +3011,16 @@ var ConfidenceServerProviderLocal = class ConfidenceServerProviderLocal {
|
|
|
3002
3011
|
if (!resp.ok) throw new Error(`Failed to fetch state: ${resp.status} ${resp.statusText}`);
|
|
3003
3012
|
this.stateEtag = resp.headers.get("etag");
|
|
3004
3013
|
const bytes = new Uint8Array(await resp.arrayBuffer());
|
|
3005
|
-
const
|
|
3006
|
-
stateRequest.sdk = {
|
|
3014
|
+
const sdk = {
|
|
3007
3015
|
id: SdkId.SDK_ID_JS_LOCAL_SERVER_PROVIDER,
|
|
3008
3016
|
version: VERSION
|
|
3009
3017
|
};
|
|
3018
|
+
try {
|
|
3019
|
+
await this.flush(signal);
|
|
3020
|
+
} catch {}
|
|
3021
|
+
const plaintext = encryptionKey ? await decryptAesGcm(bytes, hexToBytes(encryptionKey)) : bytes;
|
|
3022
|
+
const stateRequest = SetResolverStateRequest.decode(plaintext);
|
|
3023
|
+
stateRequest.sdk = sdk;
|
|
3010
3024
|
this.resolver.setResolverState(stateRequest);
|
|
3011
3025
|
}
|
|
3012
3026
|
async flush(signal) {
|
|
@@ -3087,6 +3101,18 @@ var ConfidenceServerProviderLocal = class ConfidenceServerProviderLocal {
|
|
|
3087
3101
|
this.resolver.applyFlags(request);
|
|
3088
3102
|
}
|
|
3089
3103
|
};
|
|
3104
|
+
async function decryptAesGcm(data, rawKey) {
|
|
3105
|
+
const NONCE_LEN = 12;
|
|
3106
|
+
if (data.length < NONCE_LEN) throw new Error("Encrypted state too short (missing nonce)");
|
|
3107
|
+
const iv = data.buffer.slice(data.byteOffset, data.byteOffset + NONCE_LEN);
|
|
3108
|
+
const ciphertext = data.buffer.slice(data.byteOffset + NONCE_LEN, data.byteOffset + data.byteLength);
|
|
3109
|
+
const key = await crypto.subtle.importKey("raw", rawKey.buffer, "AES-GCM", false, ["decrypt"]);
|
|
3110
|
+
const plaintext = await crypto.subtle.decrypt({
|
|
3111
|
+
name: "AES-GCM",
|
|
3112
|
+
iv
|
|
3113
|
+
}, key, ciphertext);
|
|
3114
|
+
return new Uint8Array(plaintext);
|
|
3115
|
+
}
|
|
3090
3116
|
function reasonStringToEnum(reason) {
|
|
3091
3117
|
switch (reason) {
|
|
3092
3118
|
case "MATCH": return ResolveReason.RESOLVE_REASON_MATCH;
|
package/dist/index.inlined.d.ts
CHANGED
|
@@ -486,6 +486,8 @@ type FlagBundle$1 = FlagBundle;
|
|
|
486
486
|
interface SnapshotConfig {}
|
|
487
487
|
interface ProviderOptions {
|
|
488
488
|
flagClientSecret: string;
|
|
489
|
+
/** Hex-encoded AES-256 encryption key for decrypting CDN state. */
|
|
490
|
+
encryptionKey?: string;
|
|
489
491
|
initializeTimeout?: number;
|
|
490
492
|
/** Interval in milliseconds between state polling updates. Defaults to 30000ms. */
|
|
491
493
|
stateUpdateInterval?: number;
|