diff --git a/package.json b/package.json index 353b362..c0a5fa0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eppo/js-client-sdk-common", - "version": "4.8.0-alpha.0", + "version": "4.8.0-alpha.1", "description": "Common library for Eppo JavaScript SDKs (web, react native, and node)", "main": "dist/index.js", "files": [ @@ -73,6 +73,7 @@ "buffer": "npm:@eppo/buffer@6.2.0", "js-base64": "^3.7.7", "pino": "^9.5.0", + "react-native-get-random-values": "^1.11.0", "semver": "^7.5.4", "spark-md5": "^3.0.2", "uuid": "^8.3.2" diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts index 909be30..5b7b3cf 100644 --- a/src/client/eppo-client.spec.ts +++ b/src/client/eppo-client.spec.ts @@ -214,7 +214,7 @@ describe('EppoClient E2E test', () => { }); it('skips disabled flags', () => { - const encodedPrecomputedWire = client.getPrecomputedAssignments('subject', {}); + const encodedPrecomputedWire = client.getPrecomputedAssignments('subject', {}, false); const { precomputed } = JSON.parse(encodedPrecomputedWire) as IConfigurationWire; if (!precomputed) { fail('Precomputed data not in Configuration response'); @@ -229,7 +229,7 @@ describe('EppoClient E2E test', () => { }); it('evaluates and returns assignments', () => { - const encodedPrecomputedWire = client.getPrecomputedAssignments('subject', {}); + const encodedPrecomputedWire = client.getPrecomputedAssignments('subject', {}, false); const { precomputed } = JSON.parse(encodedPrecomputedWire) as IConfigurationWire; if (!precomputed) { fail('Precomputed data not in Configuration response'); @@ -248,7 +248,7 @@ describe('EppoClient E2E test', () => { // Use a known salt to produce deterministic hashes setSaltOverrideForTests(new Uint8Array([7, 53, 17, 78])); - const encodedPrecomputedWire = client.getPrecomputedAssignments('subject', {}, true); + const encodedPrecomputedWire = client.getPrecomputedAssignments('subject', {}); const { precomputed } = JSON.parse(encodedPrecomputedWire) as IConfigurationWire; if (!precomputed) { fail('Precomputed data not in Configuration response'); diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index ac68592..6bf6304 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -871,7 +871,7 @@ export default class EppoClient { getPrecomputedAssignments( subjectKey: string, subjectAttributes: Attributes | ContextAttributes = {}, - obfuscated = false, + obfuscated = true, ): string { const configDetails = this.getConfigDetails(); diff --git a/src/index.ts b/src/index.ts index 4cb0ca8..4f76142 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,6 +34,7 @@ import { import { HybridConfigurationStore } from './configuration-store/hybrid.store'; import { MemoryStore, MemoryOnlyConfigurationStore } from './configuration-store/memory.store'; import * as constants from './constants'; +import { decodePrecomputedFlag } from './decoding'; import BatchEventProcessor from './events/batch-event-processor'; import { BoundedEventQueue } from './events/bounded-event-queue'; import DefaultEventDispatcher, { @@ -47,6 +48,7 @@ import NamedEventQueue from './events/named-event-queue'; import NetworkStatusListener from './events/network-status-listener'; import HttpClient from './http-client'; import { PrecomputedFlag, Flag, ObfuscatedFlag, VariationType, FormatEnum } from './interfaces'; +import { setSaltOverrideForTests } from './obfuscation'; import { AttributeType, Attributes, @@ -125,4 +127,8 @@ export { IConfigurationWire, IPrecomputedConfigurationResponse, PrecomputedFlag, + + // Test helpers + setSaltOverrideForTests, + decodePrecomputedFlag, }; diff --git a/src/obfuscation.ts b/src/obfuscation.ts index 496bb9a..b7a004e 100644 --- a/src/obfuscation.ts +++ b/src/obfuscation.ts @@ -1,8 +1,33 @@ import base64 = require('js-base64'); import * as SparkMD5 from 'spark-md5'; +import { logger } from './application-logger'; import { PrecomputedFlag } from './interfaces'; +// Import randomBytes according to the environment +let getRandomValues: (length: number) => Uint8Array; +if (typeof window !== 'undefined' && window.crypto) { + // Browser environment + getRandomValues = (length: number) => window.crypto.getRandomValues(new Uint8Array(length)); +} else if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') { + // React Native environment + require('react-native-get-random-values'); + getRandomValues = (length: number) => { + const array = new Uint8Array(length); + return window.crypto.getRandomValues(array); + }; +} else { + // Node.js environment + import('crypto') + .then((crypto) => { + getRandomValues = (length: number) => new Uint8Array(crypto.randomBytes(length)); + return; + }) + .catch((error) => { + logger.error('[Eppo SDK] Failed to load crypto module:', error); + }); +} + export function getMD5Hash(input: string, salt = ''): string { return new SparkMD5().append(salt).append(input).end(); } @@ -49,7 +74,5 @@ export function setSaltOverrideForTests(salt: Uint8Array | null) { } export function generateSalt(length = 16): string { - return base64.fromUint8Array( - saltOverrideBytes ? saltOverrideBytes : crypto.getRandomValues(new Uint8Array(length)), - ); + return base64.fromUint8Array(saltOverrideBytes ? saltOverrideBytes : getRandomValues(length)); } diff --git a/src/react-native-get-random-values.d.ts b/src/react-native-get-random-values.d.ts new file mode 100644 index 0000000..ef065de --- /dev/null +++ b/src/react-native-get-random-values.d.ts @@ -0,0 +1 @@ +declare module 'react-native-get-random-values'; diff --git a/tsconfig.json b/tsconfig.json index 6e8b718..921e24e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,9 +9,11 @@ "outDir": "dist", "noImplicitAny": true, "strict": true, - "strictPropertyInitialization": true + "strictPropertyInitialization": true, }, - "include": ["src/**/*.ts"], + "include": [ + "src/**/*.ts" + ], "exclude": [ "node_modules", "dist", diff --git a/webpack.config.js b/webpack.config.js index 8cec840..3ee5779 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -19,6 +19,12 @@ module.exports = { }, resolve: { extensions: ['.tsx', '.ts', '.js'], + fallback: { + crypto: false, // Exclude crypto module in the browser bundle + }, + alias: { + 'react-native-get-random-values': false, // Ignore this module in non-React Native environments + }, }, output: { filename: 'eppo-sdk.js', diff --git a/yarn.lock b/yarn.lock index cded1de..74495b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2194,6 +2194,11 @@ expect@^29.0.0, expect@^29.7.0: jest-message-util "^29.7.0" jest-util "^29.7.0" +fast-base64-decode@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz#b434a0dd7d92b12b43f26819300d2dafb83ee418" + integrity sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -3931,6 +3936,13 @@ react-is@^18.0.0: resolved "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== +react-native-get-random-values@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.11.0.tgz#1ca70d1271f4b08af92958803b89dccbda78728d" + integrity sha512-4BTbDbRmS7iPdhYLRcz3PGFIpFJBwNZg9g42iwa2P6FOv9vZj/xJc678RZXnLNZzd0qd7Q3CCF6Yd+CU2eoXKQ== + dependencies: + fast-base64-decode "^1.0.0" + real-require@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78"