From 9248035a88fba1c7375c5df22ef6b4a80a867983 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 9 Sep 2024 13:06:48 -0700 Subject: [PATCH] feat: Add platform support for async hashing. (#573) This adds platform support for async hashing for use in client-side SDKs. It does not implement async hashing for any existing platform, but provides it as an option to allow for use of standard browser APIs. Allowing the usage of standard browser crypto APIs means that browser SDKs will not need to include an additional dependency to replicate built-in functionality. --- .../shared/common/src/api/platform/Crypto.ts | 21 ++++++++++-- .../__tests__/context/addAutoEnv.test.ts | 28 +++++++-------- .../flag-manager/FlagPersistence.test.ts | 34 ++++++++++++++----- .../__tests__/storage/namespaceUtils.test.ts | 14 ++++---- .../sdk-client/src/context/addAutoEnv.ts | 13 ++++--- .../sdk-client/src/context/ensureKey.ts | 2 +- .../shared/sdk-client/src/crypto/digest.ts | 12 +++++++ .../src/flag-manager/FlagManager.ts | 30 ++++++++++++---- .../src/flag-manager/FlagPersistence.ts | 13 +++---- .../sdk-client/src/storage/namespaceUtils.ts | 28 ++++++++------- .../sdk-server/src/BigSegmentsManager.ts | 4 +++ .../shared/sdk-server/src/LDClientImpl.ts | 5 +++ .../sdk-server/src/evaluation/Bucketer.ts | 4 +++ 13 files changed, 142 insertions(+), 66 deletions(-) create mode 100644 packages/shared/sdk-client/src/crypto/digest.ts diff --git a/packages/shared/common/src/api/platform/Crypto.ts b/packages/shared/common/src/api/platform/Crypto.ts index 417fe03fb..984e2f1aa 100644 --- a/packages/shared/common/src/api/platform/Crypto.ts +++ b/packages/shared/common/src/api/platform/Crypto.ts @@ -7,7 +7,19 @@ */ export interface Hasher { update(data: string): Hasher; - digest(encoding: string): string; + /** + * Note: All server SDKs MUST implement synchronous digest. + * + * Server SDKs have high performance requirements for bucketing users. + */ + digest?(encoding: string): string; + + /** + * Note: Client-side SDKs MUST implement either synchronous or asynchronous digest. + * + * Client SDKs do not have high throughput hashing operations. + */ + asyncDigest?(encoding: string): Promise; } /** @@ -17,7 +29,7 @@ export interface Hasher { * * The has implementation must support digesting to 'hex'. */ -export interface Hmac extends Hasher { +export interface Hmac { update(data: string): Hasher; digest(encoding: string): string; } @@ -27,6 +39,9 @@ export interface Hmac extends Hasher { */ export interface Crypto { createHash(algorithm: string): Hasher; - createHmac(algorithm: string, key: string): Hmac; + /** + * Note: Server SDKs MUST implement createHmac. + */ + createHmac?(algorithm: string, key: string): Hmac; randomUUID(): string; } diff --git a/packages/shared/sdk-client/__tests__/context/addAutoEnv.test.ts b/packages/shared/sdk-client/__tests__/context/addAutoEnv.test.ts index 555f7d0b2..b5414b543 100644 --- a/packages/shared/sdk-client/__tests__/context/addAutoEnv.test.ts +++ b/packages/shared/sdk-client/__tests__/context/addAutoEnv.test.ts @@ -337,7 +337,7 @@ describe('automatic environment attributes', () => { }); describe('addApplicationInfo', () => { - test('add id, version, name, versionName', () => { + test('add id, version, name, versionName', async () => { config = new Configuration({ applicationInfo: { id: 'com.from-config.ld', @@ -346,7 +346,7 @@ describe('automatic environment attributes', () => { versionName: 'test-ld-version-name', }, }); - const ldApplication = addApplicationInfo(mockPlatform, config); + const ldApplication = await addApplicationInfo(mockPlatform, config); expect(ldApplication).toEqual({ envAttributesVersion: '1.0', @@ -358,8 +358,8 @@ describe('automatic environment attributes', () => { }); }); - test('add auto env application id, name, version', () => { - const ldApplication = addApplicationInfo(mockPlatform, config); + test('add auto env application id, name, version', async () => { + const ldApplication = await addApplicationInfo(mockPlatform, config); expect(ldApplication).toEqual({ envAttributesVersion: '1.0', @@ -370,7 +370,7 @@ describe('automatic environment attributes', () => { }); }); - test('final return value should not contain falsy values', () => { + test('final return value should not contain falsy values', async () => { const mockData = info.platformData(); info.platformData = jest.fn().mockReturnValueOnce({ ...mockData, @@ -384,7 +384,7 @@ describe('automatic environment attributes', () => { }, }); - const ldApplication = addApplicationInfo(mockPlatform, config); + const ldApplication = await addApplicationInfo(mockPlatform, config); expect(ldApplication).toEqual({ envAttributesVersion: '1.0', @@ -393,15 +393,15 @@ describe('automatic environment attributes', () => { }); }); - test('omit if customer and auto env data are unavailable', () => { + test('omit if customer and auto env data are unavailable', async () => { info.platformData = jest.fn().mockReturnValueOnce({}); - const ldApplication = addApplicationInfo(mockPlatform, config); + const ldApplication = await addApplicationInfo(mockPlatform, config); expect(ldApplication).toBeUndefined(); }); - test('omit if customer unavailable and auto env data are falsy', () => { + test('omit if customer unavailable and auto env data are falsy', async () => { const mockData = info.platformData(); info.platformData = jest.fn().mockReturnValueOnce({ ld_application: { @@ -412,27 +412,27 @@ describe('automatic environment attributes', () => { }, }); - const ldApplication = addApplicationInfo(mockPlatform, config); + const ldApplication = await addApplicationInfo(mockPlatform, config); expect(ldApplication).toBeUndefined(); }); - test('omit if customer data is unavailable and auto env data only contains key and attributesVersion', () => { + test('omit if customer data is unavailable and auto env data only contains key and attributesVersion', async () => { info.platformData = jest.fn().mockReturnValueOnce({ ld_application: { key: 'key-from-sdk', envAttributesVersion: '0.0.1' }, }); - const ldApplication = addApplicationInfo(mockPlatform, config); + const ldApplication = await addApplicationInfo(mockPlatform, config); expect(ldApplication).toBeUndefined(); }); - test('omit if no id specified', () => { + test('omit if no id specified', async () => { info.platformData = jest .fn() .mockReturnValueOnce({ ld_application: { version: null, locale: '' } }); config = new Configuration({ applicationInfo: { version: '1.2.3' } }); - const ldApplication = addApplicationInfo(mockPlatform, config); + const ldApplication = await addApplicationInfo(mockPlatform, config); expect(ldApplication).toBeUndefined(); }); diff --git a/packages/shared/sdk-client/__tests__/flag-manager/FlagPersistence.test.ts b/packages/shared/sdk-client/__tests__/flag-manager/FlagPersistence.test.ts index c0daf17c6..6e90c10af 100644 --- a/packages/shared/sdk-client/__tests__/flag-manager/FlagPersistence.test.ts +++ b/packages/shared/sdk-client/__tests__/flag-manager/FlagPersistence.test.ts @@ -141,8 +141,12 @@ describe('FlagPersistence tests', () => { await fpUnderTest.init(context, flags); - const contextDataKey = namespaceForContextData(mockPlatform.crypto, TEST_NAMESPACE, context); - const contextIndexKey = namespaceForContextIndex(TEST_NAMESPACE); + const contextDataKey = await namespaceForContextData( + mockPlatform.crypto, + TEST_NAMESPACE, + context, + ); + const contextIndexKey = await namespaceForContextIndex(TEST_NAMESPACE); expect(await memoryStorage.get(contextIndexKey)).toContain(contextDataKey); expect(await memoryStorage.get(contextDataKey)).toContain('flagA'); }); @@ -175,9 +179,17 @@ describe('FlagPersistence tests', () => { await fpUnderTest.init(context1, flags); await fpUnderTest.init(context2, flags); - const context1DataKey = namespaceForContextData(mockPlatform.crypto, TEST_NAMESPACE, context1); - const context2DataKey = namespaceForContextData(mockPlatform.crypto, TEST_NAMESPACE, context2); - const contextIndexKey = namespaceForContextIndex(TEST_NAMESPACE); + const context1DataKey = await namespaceForContextData( + mockPlatform.crypto, + TEST_NAMESPACE, + context1, + ); + const context2DataKey = await namespaceForContextData( + mockPlatform.crypto, + TEST_NAMESPACE, + context2, + ); + const contextIndexKey = await namespaceForContextIndex(TEST_NAMESPACE); const indexData = await memoryStorage.get(contextIndexKey); expect(indexData).not.toContain(context1DataKey); @@ -213,7 +225,7 @@ describe('FlagPersistence tests', () => { await fpUnderTest.init(context, flags); await fpUnderTest.init(context, flags); - const contextIndexKey = namespaceForContextIndex(TEST_NAMESPACE); + const contextIndexKey = await namespaceForContextIndex(TEST_NAMESPACE); const indexData = await memoryStorage.get(contextIndexKey); expect(indexData).toContain(`"timestamp":2`); @@ -248,7 +260,11 @@ describe('FlagPersistence tests', () => { await fpUnderTest.init(context, flags); await fpUnderTest.upsert(context, 'flagA', { version: 2, flag: flagAv2 }); - const contextDataKey = namespaceForContextData(mockPlatform.crypto, TEST_NAMESPACE, context); + const contextDataKey = await namespaceForContextData( + mockPlatform.crypto, + TEST_NAMESPACE, + context, + ); // check memory flag store and persistence expect(flagStore.get('flagA')?.version).toEqual(2); @@ -286,12 +302,12 @@ describe('FlagPersistence tests', () => { flag: makeMockFlag(), }); - const activeContextDataKey = namespaceForContextData( + const activeContextDataKey = await namespaceForContextData( mockPlatform.crypto, TEST_NAMESPACE, activeContext, ); - const inactiveContextDataKey = namespaceForContextData( + const inactiveContextDataKey = await namespaceForContextData( mockPlatform.crypto, TEST_NAMESPACE, inactiveContext, diff --git a/packages/shared/sdk-client/__tests__/storage/namespaceUtils.test.ts b/packages/shared/sdk-client/__tests__/storage/namespaceUtils.test.ts index 0ee0c70cf..c7a642195 100644 --- a/packages/shared/sdk-client/__tests__/storage/namespaceUtils.test.ts +++ b/packages/shared/sdk-client/__tests__/storage/namespaceUtils.test.ts @@ -1,23 +1,25 @@ import { concatNamespacesAndValues } from '../../src/storage/namespaceUtils'; -const mockHash = (input: string) => `${input}Hashed`; -const noop = (input: string) => input; +const mockHash = async (input: string) => `${input}Hashed`; +const noop = async (input: string) => input; describe('concatNamespacesAndValues tests', () => { test('it handles one part', async () => { - const result = concatNamespacesAndValues([{ value: 'LaunchDarkly', transform: mockHash }]); + const result = await concatNamespacesAndValues([ + { value: 'LaunchDarkly', transform: mockHash }, + ]); expect(result).toEqual('LaunchDarklyHashed'); }); test('it handles empty parts', async () => { - const result = concatNamespacesAndValues([]); + const result = await concatNamespacesAndValues([]); expect(result).toEqual(''); }); test('it handles many parts', async () => { - const result = concatNamespacesAndValues([ + const result = await concatNamespacesAndValues([ { value: 'LaunchDarkly', transform: mockHash }, { value: 'ContextKeys', transform: mockHash }, { value: 'aKind', transform: mockHash }, @@ -27,7 +29,7 @@ describe('concatNamespacesAndValues tests', () => { }); test('it handles mixture of hashing and no hashing', async () => { - const result = concatNamespacesAndValues([ + const result = await concatNamespacesAndValues([ { value: 'LaunchDarkly', transform: mockHash }, { value: 'ContextKeys', transform: noop }, { value: 'aKind', transform: mockHash }, diff --git a/packages/shared/sdk-client/src/context/addAutoEnv.ts b/packages/shared/sdk-client/src/context/addAutoEnv.ts index c769b1b6b..f66f61b01 100644 --- a/packages/shared/sdk-client/src/context/addAutoEnv.ts +++ b/packages/shared/sdk-client/src/context/addAutoEnv.ts @@ -12,6 +12,7 @@ import { } from '@launchdarkly/js-sdk-common'; import Configuration from '../configuration'; +import digest from '../crypto/digest'; import { getOrGenerateKey } from '../storage/getOrGenerateKey'; import { namespaceForGeneratedContextKey } from '../storage/namespaceUtils'; @@ -36,10 +37,10 @@ export const toMulti = (c: LDSingleKindContext) => { * @param config * @return An LDApplication object with populated key, envAttributesVersion, id and version. */ -export const addApplicationInfo = ( +export const addApplicationInfo = async ( { crypto, info }: Platform, { applicationInfo }: Configuration, -): LDApplication | undefined => { +): Promise => { const { ld_application } = info.platformData(); let app = deepCompact(ld_application) ?? ({} as LDApplication); const id = applicationInfo?.id || app?.id; @@ -58,9 +59,7 @@ export const addApplicationInfo = ( ...(versionName ? { versionName } : {}), }; - const hasher = crypto.createHash('sha256'); - hasher.update(id); - app.key = hasher.digest('base64'); + app.key = await digest(crypto.createHash('sha256').update(id), 'base64'); app.envAttributesVersion = app.envAttributesVersion || defaultAutoEnvSchemaVersion; return app; @@ -95,7 +94,7 @@ export const addDeviceInfo = async (platform: Platform) => { // Check if device has any meaningful data before we return it. if (Object.keys(device).filter((k) => k !== 'key' && k !== 'envAttributesVersion').length) { - const ldDeviceNamespace = namespaceForGeneratedContextKey('ld_device'); + const ldDeviceNamespace = await namespaceForGeneratedContextKey('ld_device'); device.key = await getOrGenerateKey(ldDeviceNamespace, platform); device.envAttributesVersion = device.envAttributesVersion || defaultAutoEnvSchemaVersion; return device; @@ -118,7 +117,7 @@ export const addAutoEnv = async (context: LDContext, platform: Platform, config: (isSingleKind(context) && context.kind !== 'ld_application') || (isMultiKind(context) && !context.ld_application) ) { - ld_application = addApplicationInfo(platform, config); + ld_application = await addApplicationInfo(platform, config); } else { config.logger.warn( 'Not adding ld_application environment attributes because it already exists.', diff --git a/packages/shared/sdk-client/src/context/ensureKey.ts b/packages/shared/sdk-client/src/context/ensureKey.ts index 7b7f18cf3..5ba0f2309 100644 --- a/packages/shared/sdk-client/src/context/ensureKey.ts +++ b/packages/shared/sdk-client/src/context/ensureKey.ts @@ -31,7 +31,7 @@ const ensureKeyCommon = async (kind: string, c: LDContextCommon, platform: Platf const { anonymous, key } = c; if (anonymous && !key) { - const storageKey = namespaceForAnonymousGeneratedContextKey(kind); + const storageKey = await namespaceForAnonymousGeneratedContextKey(kind); // This mutates a cloned copy of the original context from ensureyKey so this is safe. // eslint-disable-next-line no-param-reassign c.key = await getOrGenerateKey(storageKey, platform); diff --git a/packages/shared/sdk-client/src/crypto/digest.ts b/packages/shared/sdk-client/src/crypto/digest.ts new file mode 100644 index 000000000..c6b38292a --- /dev/null +++ b/packages/shared/sdk-client/src/crypto/digest.ts @@ -0,0 +1,12 @@ +import { Hasher } from '@launchdarkly/js-sdk-common'; + +export default async function digest(hasher: Hasher, encoding: string): Promise { + if (hasher.digest) { + return hasher.digest(encoding); + } + if (hasher.asyncDigest) { + return hasher.asyncDigest(encoding); + } + // This represents an error in platform implementation. + throw new Error('Platform must implement digest or asyncDigest'); +} diff --git a/packages/shared/sdk-client/src/flag-manager/FlagManager.ts b/packages/shared/sdk-client/src/flag-manager/FlagManager.ts index c90b32c51..267895b65 100644 --- a/packages/shared/sdk-client/src/flag-manager/FlagManager.ts +++ b/packages/shared/sdk-client/src/flag-manager/FlagManager.ts @@ -15,7 +15,7 @@ import { ItemDescriptor } from './ItemDescriptor'; export default class FlagManager { private flagStore = new DefaultFlagStore(); private flagUpdater: FlagUpdater; - private flagPersistence: FlagPersistence; + private flagPersistencePromise: Promise; /** * @param platform implementation of various platform provided functionality @@ -31,10 +31,26 @@ export default class FlagManager { logger: LDLogger, private readonly timeStamper: () => number = () => Date.now(), ) { - const environmentNamespace = namespaceForEnvironment(platform.crypto, sdkKey); - this.flagUpdater = new FlagUpdater(this.flagStore, logger); - this.flagPersistence = new FlagPersistence( + this.flagPersistencePromise = this.initPersistence( + platform, + sdkKey, + maxCachedContexts, + logger, + timeStamper, + ); + } + + private async initPersistence( + platform: Platform, + sdkKey: string, + maxCachedContexts: number, + logger: LDLogger, + timeStamper: () => number = () => Date.now(), + ): Promise { + const environmentNamespace = await namespaceForEnvironment(platform.crypto, sdkKey); + + return new FlagPersistence( platform, environmentNamespace, maxCachedContexts, @@ -64,7 +80,7 @@ export default class FlagManager { * Persistence initialization is handled by {@link FlagPersistence} */ async init(context: Context, newFlags: { [key: string]: ItemDescriptor }): Promise { - return this.flagPersistence.init(context, newFlags); + return (await this.flagPersistencePromise).init(context, newFlags); } /** @@ -72,14 +88,14 @@ export default class FlagManager { * it is of an older version, then an update will not be performed. */ async upsert(context: Context, key: string, item: ItemDescriptor): Promise { - return this.flagPersistence.upsert(context, key, item); + return (await this.flagPersistencePromise).upsert(context, key, item); } /** * Asynchronously load cached values from persistence. */ async loadCached(context: Context): Promise { - return this.flagPersistence.loadCached(context); + return (await this.flagPersistencePromise).loadCached(context); } /** diff --git a/packages/shared/sdk-client/src/flag-manager/FlagPersistence.ts b/packages/shared/sdk-client/src/flag-manager/FlagPersistence.ts index e7d903e24..c761847a4 100644 --- a/packages/shared/sdk-client/src/flag-manager/FlagPersistence.ts +++ b/packages/shared/sdk-client/src/flag-manager/FlagPersistence.ts @@ -14,7 +14,8 @@ import { ItemDescriptor } from './ItemDescriptor'; */ export default class FlagPersistence { private contextIndex: ContextIndex | undefined; - private indexKey: string; + private indexKey?: string; + private indexKeyPromise: Promise; constructor( private readonly platform: Platform, @@ -25,7 +26,7 @@ export default class FlagPersistence { private readonly logger: LDLogger, private readonly timeStamper: () => number = () => Date.now(), ) { - this.indexKey = namespaceForContextIndex(this.environmentNamespace); + this.indexKeyPromise = namespaceForContextIndex(this.environmentNamespace); } /** @@ -55,7 +56,7 @@ export default class FlagPersistence { * {@link FlagUpdater} this {@link FlagPersistence} was constructed with. */ async loadCached(context: Context): Promise { - const storageKey = namespaceForContextData( + const storageKey = await namespaceForContextData( this.platform.crypto, this.environmentNamespace, context, @@ -103,7 +104,7 @@ export default class FlagPersistence { return this.contextIndex; } - const json = await this.platform.storage?.get(this.indexKey); + const json = await this.platform.storage?.get(await this.indexKeyPromise); if (!json) { this.contextIndex = new ContextIndex(); return this.contextIndex; @@ -121,7 +122,7 @@ export default class FlagPersistence { private async storeCache(context: Context): Promise { const index = await this.loadIndex(); - const storageKey = namespaceForContextData( + const storageKey = await namespaceForContextData( this.platform.crypto, this.environmentNamespace, context, @@ -132,7 +133,7 @@ export default class FlagPersistence { await Promise.all(pruned.map(async (it) => this.platform.storage?.clear(it.id))); // store index - await this.platform.storage?.set(this.indexKey, index.toJson()); + await this.platform.storage?.set(await this.indexKeyPromise, index.toJson()); const allFlags = this.flagStore.getAll(); // mapping item descriptors to flags diff --git a/packages/shared/sdk-client/src/storage/namespaceUtils.ts b/packages/shared/sdk-client/src/storage/namespaceUtils.ts index c977bf18a..e5a28123c 100644 --- a/packages/shared/sdk-client/src/storage/namespaceUtils.ts +++ b/packages/shared/sdk-client/src/storage/namespaceUtils.ts @@ -1,24 +1,26 @@ import { Context, Crypto } from '@launchdarkly/js-sdk-common'; +import digest from '../crypto/digest'; + export type Namespace = 'LaunchDarkly' | 'AnonymousKeys' | 'ContextKeys' | 'ContextIndex'; /** * Hashes the input and encodes it as base64 */ -function hashAndBase64Encode(crypto: Crypto): (input: string) => string { - return (input) => crypto.createHash('sha256').update(input).digest('base64'); +function hashAndBase64Encode(crypto: Crypto): (input: string) => Promise { + return async (input) => digest(crypto.createHash('sha256').update(input), 'base64'); } -const noop = (input: string) => input; // no-op transform +const noop = async (input: string) => input; // no-op transform -export function concatNamespacesAndValues( - parts: { value: Namespace | string; transform: (value: string) => string }[], -): string { - const processedParts = parts.map((part) => part.transform(part.value)); // use the transform from each part to transform the value +export async function concatNamespacesAndValues( + parts: { value: Namespace | string; transform: (value: string) => Promise }[], +): Promise { + const processedParts = await Promise.all(parts.map((part) => part.transform(part.value))); // use the transform from each part to transform the value return processedParts.join('_'); } -export function namespaceForEnvironment(crypto: Crypto, sdkKey: string): string { +export async function namespaceForEnvironment(crypto: Crypto, sdkKey: string): Promise { return concatNamespacesAndValues([ { value: 'LaunchDarkly', transform: noop }, { value: sdkKey, transform: hashAndBase64Encode(crypto) }, // hash sdk key and encode it @@ -33,7 +35,7 @@ export function namespaceForEnvironment(crypto: Crypto, sdkKey: string): string * when the data under the LaunchDarkly_AnonymousKeys namespace is merged with data under the * LaunchDarkly_ContextKeys namespace. */ -export function namespaceForAnonymousGeneratedContextKey(kind: string): string { +export async function namespaceForAnonymousGeneratedContextKey(kind: string): Promise { return concatNamespacesAndValues([ { value: 'LaunchDarkly', transform: noop }, { value: 'AnonymousKeys', transform: noop }, @@ -41,7 +43,7 @@ export function namespaceForAnonymousGeneratedContextKey(kind: string): string { ]); } -export function namespaceForGeneratedContextKey(kind: string): string { +export async function namespaceForGeneratedContextKey(kind: string): Promise { return concatNamespacesAndValues([ { value: 'LaunchDarkly', transform: noop }, { value: 'ContextKeys', transform: noop }, @@ -49,18 +51,18 @@ export function namespaceForGeneratedContextKey(kind: string): string { ]); } -export function namespaceForContextIndex(environmentNamespace: string): string { +export async function namespaceForContextIndex(environmentNamespace: string): Promise { return concatNamespacesAndValues([ { value: environmentNamespace, transform: noop }, { value: 'ContextIndex', transform: noop }, ]); } -export function namespaceForContextData( +export async function namespaceForContextData( crypto: Crypto, environmentNamespace: string, context: Context, -): string { +): Promise { return concatNamespacesAndValues([ { value: environmentNamespace, transform: noop }, // use existing namespace as is, don't transform { value: context.canonicalKey, transform: hashAndBase64Encode(crypto) }, // hash and encode canonical key diff --git a/packages/shared/sdk-server/src/BigSegmentsManager.ts b/packages/shared/sdk-server/src/BigSegmentsManager.ts index 10cedd57b..d3311cc8e 100644 --- a/packages/shared/sdk-server/src/BigSegmentsManager.ts +++ b/packages/shared/sdk-server/src/BigSegmentsManager.ts @@ -144,6 +144,10 @@ export default class BigSegmentsManager { private hashForUserKey(userKey: string): string { const hasher = this.crypto.createHash('sha256'); hasher.update(userKey); + if (!hasher.digest) { + // This represents an error in platform implementation. + throw new Error('Platform must implement digest or asyncDigest'); + } return hasher.digest('base64'); } diff --git a/packages/shared/sdk-server/src/LDClientImpl.ts b/packages/shared/sdk-server/src/LDClientImpl.ts index 688a36b40..ef6c85840 100644 --- a/packages/shared/sdk-server/src/LDClientImpl.ts +++ b/packages/shared/sdk-server/src/LDClientImpl.ts @@ -689,7 +689,12 @@ export default class LDClientImpl implements LDClient { secureModeHash(context: LDContext): string { const checkedContext = Context.fromLDContext(context); const key = checkedContext.valid ? checkedContext.canonicalKey : undefined; + if (!this.platform.crypto.createHmac) { + // This represents an error in platform implementation. + throw new Error('Platform must implement createHmac'); + } const hmac = this.platform.crypto.createHmac('sha256', this.sdkKey); + if (key === undefined) { throw new LDClientError('Could not generate secure mode hash for invalid context'); } diff --git a/packages/shared/sdk-server/src/evaluation/Bucketer.ts b/packages/shared/sdk-server/src/evaluation/Bucketer.ts index 1d10fba2b..c9febf4f0 100644 --- a/packages/shared/sdk-server/src/evaluation/Bucketer.ts +++ b/packages/shared/sdk-server/src/evaluation/Bucketer.ts @@ -26,6 +26,10 @@ export default class Bucketer { private sha1Hex(value: string) { const hash = this.crypto.createHash('sha1'); hash.update(value); + if (!hash.digest) { + // This represents an error in platform implementation. + throw new Error('Platform must implement digest or asyncDigest'); + } return hash.digest('hex'); }