@spotify-confidence/openfeature-server-provider-local 0.15.2 → 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 CHANGED
@@ -1,5 +1,26 @@
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
+
3
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)
4
25
 
5
26
 
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
@@ -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;
@@ -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.15.2";
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 cdnUrl = `https://confidence-resolver-state-cdn.spotifycdn.com/${await sha256Hex(this.options.flagClientSecret)}`;
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 stateRequest = SetResolverStateRequest.decode(bytes);
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;
@@ -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;