@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.
@@ -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;
@@ -1437,7 +1437,7 @@ function isObject(value) {
1437
1437
  function isSet$3(value) {
1438
1438
  return value !== null && value !== void 0;
1439
1439
  }
1440
- const VERSION = "0.15.2";
1440
+ const VERSION = "0.16.0";
1441
1441
  const NOOP_LOG_FN = Object.assign(() => {}, { enabled: false });
1442
1442
  const debugBackend = loadDebug();
1443
1443
  const logger$2 = new class LoggerImpl {
@@ -1589,6 +1589,12 @@ function base64FromBytes$1(arr) {
1589
1589
  return globalThis.btoa(bin.join(""));
1590
1590
  }
1591
1591
  }
1592
+ function hexToBytes(hex) {
1593
+ if (hex.length % 2 !== 0) throw new Error("Hex string must have an even number of characters");
1594
+ const bytes = new Uint8Array(hex.length / 2);
1595
+ for (let i = 0; i < hex.length; i += 2) bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
1596
+ return bytes;
1597
+ }
1592
1598
  const logger$3 = logger$2.getLogger("fetch");
1593
1599
  let Fetch;
1594
1600
  (function(_Fetch) {
@@ -2882,6 +2888,7 @@ var ConfidenceServerProviderLocal = class ConfidenceServerProviderLocal {
2882
2888
  else this.materializationStore = null;
2883
2889
  }
2884
2890
  async initialize(context) {
2891
+ 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.");
2885
2892
  const signal = this.main.signal;
2886
2893
  const initialUpdateSignal = AbortSignal.any([signal, timeoutSignal(this.options.initializeTimeout ?? DEFAULT_INITIALIZE_TIMEOUT)]);
2887
2894
  try {
@@ -2994,7 +3001,9 @@ var ConfidenceServerProviderLocal = class ConfidenceServerProviderLocal {
2994
3001
  }
2995
3002
  }
2996
3003
  async updateState(signal) {
2997
- const cdnUrl = `https://confidence-resolver-state-cdn.spotifycdn.com/${await sha256Hex(this.options.flagClientSecret)}`;
3004
+ const hashHex = await sha256Hex(this.options.flagClientSecret);
3005
+ const { encryptionKey } = this.options;
3006
+ const cdnUrl = `https://confidence-resolver-state-cdn.spotifycdn.com/${encryptionKey ? `${hashHex}.enc` : hashHex}`;
2998
3007
  const headers = new Headers();
2999
3008
  if (this.stateEtag) headers.set("If-None-Match", this.stateEtag);
3000
3009
  const resp = await this.fetch(cdnUrl, {
@@ -3005,11 +3014,16 @@ var ConfidenceServerProviderLocal = class ConfidenceServerProviderLocal {
3005
3014
  if (!resp.ok) throw new Error(`Failed to fetch state: ${resp.status} ${resp.statusText}`);
3006
3015
  this.stateEtag = resp.headers.get("etag");
3007
3016
  const bytes = new Uint8Array(await resp.arrayBuffer());
3008
- const stateRequest = SetResolverStateRequest.decode(bytes);
3009
- stateRequest.sdk = {
3017
+ const sdk = {
3010
3018
  id: SdkId.SDK_ID_JS_LOCAL_SERVER_PROVIDER,
3011
3019
  version: VERSION
3012
3020
  };
3021
+ try {
3022
+ await this.flush(signal);
3023
+ } catch {}
3024
+ const plaintext = encryptionKey ? await decryptAesGcm(bytes, hexToBytes(encryptionKey)) : bytes;
3025
+ const stateRequest = SetResolverStateRequest.decode(plaintext);
3026
+ stateRequest.sdk = sdk;
3013
3027
  this.resolver.setResolverState(stateRequest);
3014
3028
  }
3015
3029
  async flush(signal) {
@@ -3090,6 +3104,18 @@ var ConfidenceServerProviderLocal = class ConfidenceServerProviderLocal {
3090
3104
  this.resolver.applyFlags(request);
3091
3105
  }
3092
3106
  };
3107
+ async function decryptAesGcm(data, rawKey) {
3108
+ const NONCE_LEN = 12;
3109
+ if (data.length < NONCE_LEN) throw new Error("Encrypted state too short (missing nonce)");
3110
+ const iv = data.buffer.slice(data.byteOffset, data.byteOffset + NONCE_LEN);
3111
+ const ciphertext = data.buffer.slice(data.byteOffset + NONCE_LEN, data.byteOffset + data.byteLength);
3112
+ const key = await crypto.subtle.importKey("raw", rawKey.buffer, "AES-GCM", false, ["decrypt"]);
3113
+ const plaintext = await crypto.subtle.decrypt({
3114
+ name: "AES-GCM",
3115
+ iv
3116
+ }, key, ciphertext);
3117
+ return new Uint8Array(plaintext);
3118
+ }
3093
3119
  function reasonStringToEnum(reason) {
3094
3120
  switch (reason) {
3095
3121
  case "MATCH": return ResolveReason.RESOLVE_REASON_MATCH;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spotify-confidence/openfeature-server-provider-local",
3
- "version": "0.15.2",
3
+ "version": "0.16.0",
4
4
  "description": "Spotify Confidence Open Feature provider",
5
5
  "type": "module",
6
6
  "files": [