@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 +21 -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 +1 -1
package/dist/index.node.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.node.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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;
|