From c4a67ced0a4b9b8643f24a2549c46e76a4e82c4e Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 6 Sep 2024 12:48:28 -0700 Subject: [PATCH 01/18] feat: Add platform support for async hashing. --- .../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 | 15 +++++--- .../sdk-client/src/context/ensureKey.ts | 2 +- .../src/flag-manager/FlagManager.ts | 30 ++++++++++++---- .../src/flag-manager/FlagPersistence.ts | 19 +++++++---- .../sdk-client/src/flag-manager/FlagStore.ts | 5 +-- .../sdk-client/src/storage/namespaceUtils.ts | 34 ++++++++++++------- .../sdk-server/src/BigSegmentsManager.ts | 4 +++ .../shared/sdk-server/src/LDClientImpl.ts | 5 +++ .../sdk-server/src/evaluation/Bucketer.ts | 4 +++ 13 files changed, 146 insertions(+), 69 deletions(-) diff --git a/packages/shared/common/src/api/platform/Crypto.ts b/packages/shared/common/src/api/platform/Crypto.ts index 417fe03fbd..984e2f1aad 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 555f7d0b24..b5414b543f 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 c0daf17c61..6e90c10af3 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 0ee0c70cf2..c7a6421953 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 c769b1b6be..1c9d89673a 100644 --- a/packages/shared/sdk-client/src/context/addAutoEnv.ts +++ b/packages/shared/sdk-client/src/context/addAutoEnv.ts @@ -36,10 +36,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; @@ -60,7 +60,12 @@ export const addApplicationInfo = ( const hasher = crypto.createHash('sha256'); hasher.update(id); - app.key = hasher.digest('base64'); + const digest = hasher.digest || hasher.asyncDigest; + if(!digest) { + // This represents an error in platform implementation. + throw new Error("Platform must implement digest or asyncDigest"); + } + app.key = await digest('base64'); app.envAttributesVersion = app.envAttributesVersion || defaultAutoEnvSchemaVersion; return app; @@ -95,7 +100,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 +123,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 7b7f18cf3d..5ba0f2309f 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/flag-manager/FlagManager.ts b/packages/shared/sdk-client/src/flag-manager/FlagManager.ts index c90b32c51e..b87cb2667a 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 flagPersistence: 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.flagPersistence = 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.flagPersistence).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.flagPersistence).upsert(context, key, item); } /** * Asynchronously load cached values from persistence. */ async loadCached(context: Context): Promise { - return this.flagPersistence.loadCached(context); + return (await this.flagPersistence).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 e7d903e240..847bee23db 100644 --- a/packages/shared/sdk-client/src/flag-manager/FlagPersistence.ts +++ b/packages/shared/sdk-client/src/flag-manager/FlagPersistence.ts @@ -14,7 +14,7 @@ import { ItemDescriptor } from './ItemDescriptor'; */ export default class FlagPersistence { private contextIndex: ContextIndex | undefined; - private indexKey: string; + private indexKey?: string; constructor( private readonly platform: Platform, @@ -24,8 +24,13 @@ export default class FlagPersistence { private readonly flagUpdater: FlagUpdater, private readonly logger: LDLogger, private readonly timeStamper: () => number = () => Date.now(), - ) { - this.indexKey = namespaceForContextIndex(this.environmentNamespace); + ) {} + + private async getIndexKey() { + if (!this.indexKey) { + this.indexKey = await namespaceForContextIndex(this.environmentNamespace); + } + return this.indexKey; } /** @@ -55,7 +60,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 +108,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.getIndexKey()); if (!json) { this.contextIndex = new ContextIndex(); return this.contextIndex; @@ -121,7 +126,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 +137,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.getIndexKey(), index.toJson()); const allFlags = this.flagStore.getAll(); // mapping item descriptors to flags diff --git a/packages/shared/sdk-client/src/flag-manager/FlagStore.ts b/packages/shared/sdk-client/src/flag-manager/FlagStore.ts index f58959721a..d9ce91b110 100644 --- a/packages/shared/sdk-client/src/flag-manager/FlagStore.ts +++ b/packages/shared/sdk-client/src/flag-manager/FlagStore.ts @@ -31,10 +31,7 @@ export class DefaultFlagStore implements FlagStore { } get(key: string): ItemDescriptor | undefined { - if (Object.prototype.hasOwnProperty.call(this.flags, key)) { - return this.flags[key]; - } - return undefined; + return this.flags[key]; } getAll(): { [key: string]: ItemDescriptor } { diff --git a/packages/shared/sdk-client/src/storage/namespaceUtils.ts b/packages/shared/sdk-client/src/storage/namespaceUtils.ts index c977bf18ac..eb33627168 100644 --- a/packages/shared/sdk-client/src/storage/namespaceUtils.ts +++ b/packages/shared/sdk-client/src/storage/namespaceUtils.ts @@ -5,20 +5,28 @@ export type Namespace = 'LaunchDarkly' | 'AnonymousKeys' | 'ContextKeys' | 'Cont /** * 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) => { + const hasher = crypto.createHash('sha256').update(input); + const digestMethod = hasher.digest ?? hasher.asyncDigest; + if(digestMethod) { + // This represents an error in platform implementation. + throw new Error("Platform must implement digest or asyncDigest"); + } + return digestMethod!('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 +41,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 +49,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 +57,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 10cedd57bd..0724e702f4 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 688a36b40f..f9801b94e0 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 1d10fba2b7..16e4b8b622 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'); } From 5d111060bac897c058380949199dbe45afce6113 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 6 Sep 2024 13:29:58 -0700 Subject: [PATCH 02/18] Lint --- packages/sdk/browser/jest.config.js | 9 ++++----- packages/sdk/cloudflare/jsr.json | 12 ++---------- packages/shared/sdk-client/src/context/addAutoEnv.ts | 4 ++-- .../shared/sdk-client/src/storage/namespaceUtils.ts | 4 ++-- packages/shared/sdk-server/src/BigSegmentsManager.ts | 4 ++-- packages/shared/sdk-server/src/LDClientImpl.ts | 4 ++-- .../shared/sdk-server/src/evaluation/Bucketer.ts | 4 ++-- 7 files changed, 16 insertions(+), 25 deletions(-) diff --git a/packages/sdk/browser/jest.config.js b/packages/sdk/browser/jest.config.js index 364918be3a..1f0bda3723 100644 --- a/packages/sdk/browser/jest.config.js +++ b/packages/sdk/browser/jest.config.js @@ -1,12 +1,11 @@ - export default { preset: 'ts-jest', testEnvironment: 'jest-environment-jsdom', transform: { - "^.+\\.tsx?$": "ts-jest" - // process `*.tsx` files with `ts-jest` + '^.+\\.tsx?$': 'ts-jest', + // process `*.tsx` files with `ts-jest` }, moduleNameMapper: { - '\\.(gif|ttf|eot|svg|png)$': '/test/__ mocks __/fileMock.js', + '\\.(gif|ttf|eot|svg|png)$': '/test/__ mocks __/fileMock.js', }, -} +}; diff --git a/packages/sdk/cloudflare/jsr.json b/packages/sdk/cloudflare/jsr.json index 8f0b8980b1..cde7947a83 100644 --- a/packages/sdk/cloudflare/jsr.json +++ b/packages/sdk/cloudflare/jsr.json @@ -3,15 +3,7 @@ "version": "2.5.13", "exports": "./src/index.ts", "publish": { - "include": [ - "LICENSE", - "README.md", - "package.json", - "jsr.json", - "src/**/*.ts" - ], - "exclude": [ - "src/**/*.test.ts" - ] + "include": ["LICENSE", "README.md", "package.json", "jsr.json", "src/**/*.ts"], + "exclude": ["src/**/*.test.ts"] } } diff --git a/packages/shared/sdk-client/src/context/addAutoEnv.ts b/packages/shared/sdk-client/src/context/addAutoEnv.ts index 1c9d89673a..2fdea1f5c4 100644 --- a/packages/shared/sdk-client/src/context/addAutoEnv.ts +++ b/packages/shared/sdk-client/src/context/addAutoEnv.ts @@ -61,9 +61,9 @@ export const addApplicationInfo = async ( const hasher = crypto.createHash('sha256'); hasher.update(id); const digest = hasher.digest || hasher.asyncDigest; - if(!digest) { + if (!digest) { // This represents an error in platform implementation. - throw new Error("Platform must implement digest or asyncDigest"); + throw new Error('Platform must implement digest or asyncDigest'); } app.key = await digest('base64'); app.envAttributesVersion = app.envAttributesVersion || defaultAutoEnvSchemaVersion; diff --git a/packages/shared/sdk-client/src/storage/namespaceUtils.ts b/packages/shared/sdk-client/src/storage/namespaceUtils.ts index eb33627168..c6ee0c60a9 100644 --- a/packages/shared/sdk-client/src/storage/namespaceUtils.ts +++ b/packages/shared/sdk-client/src/storage/namespaceUtils.ts @@ -9,9 +9,9 @@ function hashAndBase64Encode(crypto: Crypto): (input: string) => Promise return async (input) => { const hasher = crypto.createHash('sha256').update(input); const digestMethod = hasher.digest ?? hasher.asyncDigest; - if(digestMethod) { + if (digestMethod) { // This represents an error in platform implementation. - throw new Error("Platform must implement digest or asyncDigest"); + throw new Error('Platform must implement digest or asyncDigest'); } return digestMethod!('base64'); }; diff --git a/packages/shared/sdk-server/src/BigSegmentsManager.ts b/packages/shared/sdk-server/src/BigSegmentsManager.ts index 0724e702f4..d3311cc8e6 100644 --- a/packages/shared/sdk-server/src/BigSegmentsManager.ts +++ b/packages/shared/sdk-server/src/BigSegmentsManager.ts @@ -144,9 +144,9 @@ export default class BigSegmentsManager { private hashForUserKey(userKey: string): string { const hasher = this.crypto.createHash('sha256'); hasher.update(userKey); - if(!hasher.digest) { + if (!hasher.digest) { // This represents an error in platform implementation. - throw new Error("Platform must implement digest or asyncDigest"); + 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 f9801b94e0..ef6c85840f 100644 --- a/packages/shared/sdk-server/src/LDClientImpl.ts +++ b/packages/shared/sdk-server/src/LDClientImpl.ts @@ -689,9 +689,9 @@ 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) { + if (!this.platform.crypto.createHmac) { // This represents an error in platform implementation. - throw new Error("Platform must implement createHmac"); + throw new Error('Platform must implement createHmac'); } const hmac = this.platform.crypto.createHmac('sha256', this.sdkKey); diff --git a/packages/shared/sdk-server/src/evaluation/Bucketer.ts b/packages/shared/sdk-server/src/evaluation/Bucketer.ts index 16e4b8b622..c9febf4f07 100644 --- a/packages/shared/sdk-server/src/evaluation/Bucketer.ts +++ b/packages/shared/sdk-server/src/evaluation/Bucketer.ts @@ -26,9 +26,9 @@ export default class Bucketer { private sha1Hex(value: string) { const hash = this.crypto.createHash('sha1'); hash.update(value); - if(!hash.digest) { + if (!hash.digest) { // This represents an error in platform implementation. - throw new Error("Platform must implement digest or asyncDigest"); + throw new Error('Platform must implement digest or asyncDigest'); } return hash.digest('hex'); } From 2c0a4b168332658305f6bea8cbe7d791770ec30b Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 6 Sep 2024 14:36:46 -0700 Subject: [PATCH 03/18] Fix hasher check for client. --- packages/shared/sdk-client/src/storage/namespaceUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/sdk-client/src/storage/namespaceUtils.ts b/packages/shared/sdk-client/src/storage/namespaceUtils.ts index c6ee0c60a9..c683ffc60c 100644 --- a/packages/shared/sdk-client/src/storage/namespaceUtils.ts +++ b/packages/shared/sdk-client/src/storage/namespaceUtils.ts @@ -9,7 +9,7 @@ function hashAndBase64Encode(crypto: Crypto): (input: string) => Promise return async (input) => { const hasher = crypto.createHash('sha256').update(input); const digestMethod = hasher.digest ?? hasher.asyncDigest; - if (digestMethod) { + if (!digestMethod) { // This represents an error in platform implementation. throw new Error('Platform must implement digest or asyncDigest'); } From 3215c2456d6da93442313e74be9e311c9ecaf32c Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 6 Sep 2024 14:50:43 -0700 Subject: [PATCH 04/18] Fix hasher selection. --- packages/shared/sdk-client/src/context/addAutoEnv.ts | 11 ++++++++--- .../shared/sdk-client/src/storage/namespaceUtils.ts | 12 ++++++++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/packages/shared/sdk-client/src/context/addAutoEnv.ts b/packages/shared/sdk-client/src/context/addAutoEnv.ts index 2fdea1f5c4..a6e28c1e08 100644 --- a/packages/shared/sdk-client/src/context/addAutoEnv.ts +++ b/packages/shared/sdk-client/src/context/addAutoEnv.ts @@ -60,11 +60,16 @@ export const addApplicationInfo = async ( const hasher = crypto.createHash('sha256'); hasher.update(id); - const digest = hasher.digest || hasher.asyncDigest; - if (!digest) { + const digest = async (encoding: string) => { + if (hasher.digest) { + return hasher.digest(encoding); + } else if (hasher.asyncDigest) { + return hasher.asyncDigest(encoding); + } // This represents an error in platform implementation. throw new Error('Platform must implement digest or asyncDigest'); - } + }; + app.key = await digest('base64'); app.envAttributesVersion = app.envAttributesVersion || defaultAutoEnvSchemaVersion; diff --git a/packages/shared/sdk-client/src/storage/namespaceUtils.ts b/packages/shared/sdk-client/src/storage/namespaceUtils.ts index c683ffc60c..b78e2b4d12 100644 --- a/packages/shared/sdk-client/src/storage/namespaceUtils.ts +++ b/packages/shared/sdk-client/src/storage/namespaceUtils.ts @@ -8,12 +8,16 @@ export type Namespace = 'LaunchDarkly' | 'AnonymousKeys' | 'ContextKeys' | 'Cont function hashAndBase64Encode(crypto: Crypto): (input: string) => Promise { return async (input) => { const hasher = crypto.createHash('sha256').update(input); - const digestMethod = hasher.digest ?? hasher.asyncDigest; - if (!digestMethod) { + const digest = async (encoding: string) => { + if (hasher.digest) { + return hasher.digest(encoding); + } else if (hasher.asyncDigest) { + return hasher.asyncDigest(encoding); + } // This represents an error in platform implementation. throw new Error('Platform must implement digest or asyncDigest'); - } - return digestMethod!('base64'); + }; + return digest('base64'); }; } From 5bdb00cbef25c88b8c2e2e0e7260ec48baa8d5e0 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 6 Sep 2024 15:11:39 -0700 Subject: [PATCH 05/18] Lint --- packages/shared/sdk-client/src/context/addAutoEnv.ts | 3 ++- packages/shared/sdk-client/src/storage/namespaceUtils.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/shared/sdk-client/src/context/addAutoEnv.ts b/packages/shared/sdk-client/src/context/addAutoEnv.ts index a6e28c1e08..70a92ad0be 100644 --- a/packages/shared/sdk-client/src/context/addAutoEnv.ts +++ b/packages/shared/sdk-client/src/context/addAutoEnv.ts @@ -63,7 +63,8 @@ export const addApplicationInfo = async ( const digest = async (encoding: string) => { if (hasher.digest) { return hasher.digest(encoding); - } else if (hasher.asyncDigest) { + } + if (hasher.asyncDigest) { return hasher.asyncDigest(encoding); } // This represents an error in platform implementation. diff --git a/packages/shared/sdk-client/src/storage/namespaceUtils.ts b/packages/shared/sdk-client/src/storage/namespaceUtils.ts index b78e2b4d12..c97ac884e0 100644 --- a/packages/shared/sdk-client/src/storage/namespaceUtils.ts +++ b/packages/shared/sdk-client/src/storage/namespaceUtils.ts @@ -11,7 +11,8 @@ function hashAndBase64Encode(crypto: Crypto): (input: string) => Promise const digest = async (encoding: string) => { if (hasher.digest) { return hasher.digest(encoding); - } else if (hasher.asyncDigest) { + } + if (hasher.asyncDigest) { return hasher.asyncDigest(encoding); } // This represents an error in platform implementation. From 5fe646c7c86fbb71dbe1cb2d585abe5dd07ba4e3 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 6 Sep 2024 15:15:27 -0700 Subject: [PATCH 06/18] Fix merge --- packages/shared/sdk-client/src/flag-manager/FlagStore.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/shared/sdk-client/src/flag-manager/FlagStore.ts b/packages/shared/sdk-client/src/flag-manager/FlagStore.ts index d9ce91b110..f58959721a 100644 --- a/packages/shared/sdk-client/src/flag-manager/FlagStore.ts +++ b/packages/shared/sdk-client/src/flag-manager/FlagStore.ts @@ -31,7 +31,10 @@ export class DefaultFlagStore implements FlagStore { } get(key: string): ItemDescriptor | undefined { - return this.flags[key]; + if (Object.prototype.hasOwnProperty.call(this.flags, key)) { + return this.flags[key]; + } + return undefined; } getAll(): { [key: string]: ItemDescriptor } { From e7f757a67b376af0307f935404b1649cc509cc33 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 6 Sep 2024 15:16:27 -0700 Subject: [PATCH 07/18] More corrections. --- packages/sdk/browser/jest.config.js | 9 +++++---- packages/sdk/cloudflare/jsr.json | 12 ++++++++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/sdk/browser/jest.config.js b/packages/sdk/browser/jest.config.js index 1f0bda3723..364918be3a 100644 --- a/packages/sdk/browser/jest.config.js +++ b/packages/sdk/browser/jest.config.js @@ -1,11 +1,12 @@ + export default { preset: 'ts-jest', testEnvironment: 'jest-environment-jsdom', transform: { - '^.+\\.tsx?$': 'ts-jest', - // process `*.tsx` files with `ts-jest` + "^.+\\.tsx?$": "ts-jest" + // process `*.tsx` files with `ts-jest` }, moduleNameMapper: { - '\\.(gif|ttf|eot|svg|png)$': '/test/__ mocks __/fileMock.js', + '\\.(gif|ttf|eot|svg|png)$': '/test/__ mocks __/fileMock.js', }, -}; +} diff --git a/packages/sdk/cloudflare/jsr.json b/packages/sdk/cloudflare/jsr.json index cde7947a83..8f0b8980b1 100644 --- a/packages/sdk/cloudflare/jsr.json +++ b/packages/sdk/cloudflare/jsr.json @@ -3,7 +3,15 @@ "version": "2.5.13", "exports": "./src/index.ts", "publish": { - "include": ["LICENSE", "README.md", "package.json", "jsr.json", "src/**/*.ts"], - "exclude": ["src/**/*.test.ts"] + "include": [ + "LICENSE", + "README.md", + "package.json", + "jsr.json", + "src/**/*.ts" + ], + "exclude": [ + "src/**/*.test.ts" + ] } } From 7bd42589b515372deb2eb7002175e2b3bf2d3117 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 6 Sep 2024 15:20:12 -0700 Subject: [PATCH 08/18] Consistent promise approach. --- .../sdk-client/src/flag-manager/FlagManager.ts | 10 +++++----- .../sdk-client/src/flag-manager/FlagPersistence.ts | 14 +++++--------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/shared/sdk-client/src/flag-manager/FlagManager.ts b/packages/shared/sdk-client/src/flag-manager/FlagManager.ts index b87cb2667a..267895b656 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: Promise; + private flagPersistencePromise: Promise; /** * @param platform implementation of various platform provided functionality @@ -32,7 +32,7 @@ export default class FlagManager { private readonly timeStamper: () => number = () => Date.now(), ) { this.flagUpdater = new FlagUpdater(this.flagStore, logger); - this.flagPersistence = this.initPersistence( + this.flagPersistencePromise = this.initPersistence( platform, sdkKey, maxCachedContexts, @@ -80,7 +80,7 @@ export default class FlagManager { * Persistence initialization is handled by {@link FlagPersistence} */ async init(context: Context, newFlags: { [key: string]: ItemDescriptor }): Promise { - return (await this.flagPersistence).init(context, newFlags); + return (await this.flagPersistencePromise).init(context, newFlags); } /** @@ -88,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 (await 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 (await 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 847bee23db..c761847a4c 100644 --- a/packages/shared/sdk-client/src/flag-manager/FlagPersistence.ts +++ b/packages/shared/sdk-client/src/flag-manager/FlagPersistence.ts @@ -15,6 +15,7 @@ import { ItemDescriptor } from './ItemDescriptor'; export default class FlagPersistence { private contextIndex: ContextIndex | undefined; private indexKey?: string; + private indexKeyPromise: Promise; constructor( private readonly platform: Platform, @@ -24,13 +25,8 @@ export default class FlagPersistence { private readonly flagUpdater: FlagUpdater, private readonly logger: LDLogger, private readonly timeStamper: () => number = () => Date.now(), - ) {} - - private async getIndexKey() { - if (!this.indexKey) { - this.indexKey = await namespaceForContextIndex(this.environmentNamespace); - } - return this.indexKey; + ) { + this.indexKeyPromise = namespaceForContextIndex(this.environmentNamespace); } /** @@ -108,7 +104,7 @@ export default class FlagPersistence { return this.contextIndex; } - const json = await this.platform.storage?.get(await this.getIndexKey()); + const json = await this.platform.storage?.get(await this.indexKeyPromise); if (!json) { this.contextIndex = new ContextIndex(); return this.contextIndex; @@ -137,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(await this.getIndexKey(), index.toJson()); + await this.platform.storage?.set(await this.indexKeyPromise, index.toJson()); const allFlags = this.flagStore.getAll(); // mapping item descriptors to flags From dba54b2a465340b9912c2d8abaca493c6bb75831 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 6 Sep 2024 15:25:05 -0700 Subject: [PATCH 09/18] Refactoring. --- .../shared/sdk-client/src/context/addAutoEnv.ts | 16 ++-------------- packages/shared/sdk-client/src/crypto/digest.ts | 12 ++++++++++++ .../sdk-client/src/storage/namespaceUtils.ts | 14 ++------------ 3 files changed, 16 insertions(+), 26 deletions(-) create mode 100644 packages/shared/sdk-client/src/crypto/digest.ts diff --git a/packages/shared/sdk-client/src/context/addAutoEnv.ts b/packages/shared/sdk-client/src/context/addAutoEnv.ts index 70a92ad0be..f1bfea55d2 100644 --- a/packages/shared/sdk-client/src/context/addAutoEnv.ts +++ b/packages/shared/sdk-client/src/context/addAutoEnv.ts @@ -14,6 +14,7 @@ import { import Configuration from '../configuration'; import { getOrGenerateKey } from '../storage/getOrGenerateKey'; import { namespaceForGeneratedContextKey } from '../storage/namespaceUtils'; +import digest from '../crypto/digest'; const { isLegacyUser, isSingleKind, isMultiKind } = internal; const defaultAutoEnvSchemaVersion = '1.0'; @@ -58,20 +59,7 @@ export const addApplicationInfo = async ( ...(versionName ? { versionName } : {}), }; - const hasher = crypto.createHash('sha256'); - hasher.update(id); - const digest = async (encoding: string) => { - 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'); - }; - - app.key = await digest('base64'); + app.key = await digest(crypto.createHash('sha256').update(id), 'base64'); app.envAttributesVersion = app.envAttributesVersion || defaultAutoEnvSchemaVersion; return app; 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 0000000000..24625ec049 --- /dev/null +++ b/packages/shared/sdk-client/src/crypto/digest.ts @@ -0,0 +1,12 @@ +import { Hasher } from '@launchdarkly/js-sdk-common'; + +export default function digest(hasher: Hasher, encoding: string) { + 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/storage/namespaceUtils.ts b/packages/shared/sdk-client/src/storage/namespaceUtils.ts index c97ac884e0..cf02e5c11a 100644 --- a/packages/shared/sdk-client/src/storage/namespaceUtils.ts +++ b/packages/shared/sdk-client/src/storage/namespaceUtils.ts @@ -1,4 +1,5 @@ import { Context, Crypto } from '@launchdarkly/js-sdk-common'; +import digest from '../crypto/digest'; export type Namespace = 'LaunchDarkly' | 'AnonymousKeys' | 'ContextKeys' | 'ContextIndex'; @@ -7,18 +8,7 @@ export type Namespace = 'LaunchDarkly' | 'AnonymousKeys' | 'ContextKeys' | 'Cont */ function hashAndBase64Encode(crypto: Crypto): (input: string) => Promise { return async (input) => { - const hasher = crypto.createHash('sha256').update(input); - const digest = async (encoding: string) => { - 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'); - }; - return digest('base64'); + return digest(crypto.createHash('sha256').update(input), 'base64'); }; } From c12b4385624086386abbbddf809683dfdaf6a679 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 6 Sep 2024 15:31:31 -0700 Subject: [PATCH 10/18] Linting --- packages/sdk/browser/jest.config.js | 9 ++++----- packages/sdk/cloudflare/jsr.json | 12 ++---------- packages/shared/sdk-client/src/context/addAutoEnv.ts | 2 +- .../shared/sdk-client/src/storage/namespaceUtils.ts | 1 + 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/packages/sdk/browser/jest.config.js b/packages/sdk/browser/jest.config.js index 364918be3a..1f0bda3723 100644 --- a/packages/sdk/browser/jest.config.js +++ b/packages/sdk/browser/jest.config.js @@ -1,12 +1,11 @@ - export default { preset: 'ts-jest', testEnvironment: 'jest-environment-jsdom', transform: { - "^.+\\.tsx?$": "ts-jest" - // process `*.tsx` files with `ts-jest` + '^.+\\.tsx?$': 'ts-jest', + // process `*.tsx` files with `ts-jest` }, moduleNameMapper: { - '\\.(gif|ttf|eot|svg|png)$': '/test/__ mocks __/fileMock.js', + '\\.(gif|ttf|eot|svg|png)$': '/test/__ mocks __/fileMock.js', }, -} +}; diff --git a/packages/sdk/cloudflare/jsr.json b/packages/sdk/cloudflare/jsr.json index 8f0b8980b1..cde7947a83 100644 --- a/packages/sdk/cloudflare/jsr.json +++ b/packages/sdk/cloudflare/jsr.json @@ -3,15 +3,7 @@ "version": "2.5.13", "exports": "./src/index.ts", "publish": { - "include": [ - "LICENSE", - "README.md", - "package.json", - "jsr.json", - "src/**/*.ts" - ], - "exclude": [ - "src/**/*.test.ts" - ] + "include": ["LICENSE", "README.md", "package.json", "jsr.json", "src/**/*.ts"], + "exclude": ["src/**/*.test.ts"] } } diff --git a/packages/shared/sdk-client/src/context/addAutoEnv.ts b/packages/shared/sdk-client/src/context/addAutoEnv.ts index f1bfea55d2..f66f61b01e 100644 --- a/packages/shared/sdk-client/src/context/addAutoEnv.ts +++ b/packages/shared/sdk-client/src/context/addAutoEnv.ts @@ -12,9 +12,9 @@ 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'; -import digest from '../crypto/digest'; const { isLegacyUser, isSingleKind, isMultiKind } = internal; const defaultAutoEnvSchemaVersion = '1.0'; diff --git a/packages/shared/sdk-client/src/storage/namespaceUtils.ts b/packages/shared/sdk-client/src/storage/namespaceUtils.ts index cf02e5c11a..51b1e57494 100644 --- a/packages/shared/sdk-client/src/storage/namespaceUtils.ts +++ b/packages/shared/sdk-client/src/storage/namespaceUtils.ts @@ -1,4 +1,5 @@ import { Context, Crypto } from '@launchdarkly/js-sdk-common'; + import digest from '../crypto/digest'; export type Namespace = 'LaunchDarkly' | 'AnonymousKeys' | 'ContextKeys' | 'ContextIndex'; From 5c36b6e2e286b3a7814d5d3c469ff7900bc5abce Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 6 Sep 2024 15:32:06 -0700 Subject: [PATCH 11/18] Remove ancillary linting. --- packages/sdk/browser/jest.config.js | 9 +++++---- packages/sdk/cloudflare/jsr.json | 12 ++++++++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/sdk/browser/jest.config.js b/packages/sdk/browser/jest.config.js index 1f0bda3723..364918be3a 100644 --- a/packages/sdk/browser/jest.config.js +++ b/packages/sdk/browser/jest.config.js @@ -1,11 +1,12 @@ + export default { preset: 'ts-jest', testEnvironment: 'jest-environment-jsdom', transform: { - '^.+\\.tsx?$': 'ts-jest', - // process `*.tsx` files with `ts-jest` + "^.+\\.tsx?$": "ts-jest" + // process `*.tsx` files with `ts-jest` }, moduleNameMapper: { - '\\.(gif|ttf|eot|svg|png)$': '/test/__ mocks __/fileMock.js', + '\\.(gif|ttf|eot|svg|png)$': '/test/__ mocks __/fileMock.js', }, -}; +} diff --git a/packages/sdk/cloudflare/jsr.json b/packages/sdk/cloudflare/jsr.json index cde7947a83..8f0b8980b1 100644 --- a/packages/sdk/cloudflare/jsr.json +++ b/packages/sdk/cloudflare/jsr.json @@ -3,7 +3,15 @@ "version": "2.5.13", "exports": "./src/index.ts", "publish": { - "include": ["LICENSE", "README.md", "package.json", "jsr.json", "src/**/*.ts"], - "exclude": ["src/**/*.test.ts"] + "include": [ + "LICENSE", + "README.md", + "package.json", + "jsr.json", + "src/**/*.ts" + ], + "exclude": [ + "src/**/*.test.ts" + ] } } From e1585a93ec156b6042ff4668c49700e32ec05598 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 6 Sep 2024 15:36:28 -0700 Subject: [PATCH 12/18] More lint --- packages/shared/sdk-client/src/storage/namespaceUtils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/shared/sdk-client/src/storage/namespaceUtils.ts b/packages/shared/sdk-client/src/storage/namespaceUtils.ts index 51b1e57494..e5a28123c4 100644 --- a/packages/shared/sdk-client/src/storage/namespaceUtils.ts +++ b/packages/shared/sdk-client/src/storage/namespaceUtils.ts @@ -8,9 +8,7 @@ export type Namespace = 'LaunchDarkly' | 'AnonymousKeys' | 'ContextKeys' | 'Cont * Hashes the input and encodes it as base64 */ function hashAndBase64Encode(crypto: Crypto): (input: string) => Promise { - return async (input) => { - return digest(crypto.createHash('sha256').update(input), 'base64'); - }; + return async (input) => digest(crypto.createHash('sha256').update(input), 'base64'); } const noop = async (input: string) => input; // no-op transform From 80537103c315cef9a7e76acc03510f0e99f7f13e Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 6 Sep 2024 15:43:45 -0700 Subject: [PATCH 13/18] feat: Implement browser crypto --- .../__tests__/platform/BrowserHasher.test.ts | 53 ++++++++++ .../__tests__/platform/randomUuidV4.test.ts | 29 ++++++ packages/sdk/browser/package.json | 4 +- .../sdk/browser/src/platform/BrowserCrypto.ts | 14 +++ .../sdk/browser/src/platform/BrowserHasher.ts | 44 +++++++++ .../browser/src/platform/BrowserPlatform.ts | 6 +- .../sdk/browser/src/platform/randomUuidV4.ts | 99 +++++++++++++++++++ 7 files changed, 245 insertions(+), 4 deletions(-) create mode 100644 packages/sdk/browser/__tests__/platform/BrowserHasher.test.ts create mode 100644 packages/sdk/browser/__tests__/platform/randomUuidV4.test.ts create mode 100644 packages/sdk/browser/src/platform/BrowserCrypto.ts create mode 100644 packages/sdk/browser/src/platform/BrowserHasher.ts create mode 100644 packages/sdk/browser/src/platform/randomUuidV4.ts diff --git a/packages/sdk/browser/__tests__/platform/BrowserHasher.test.ts b/packages/sdk/browser/__tests__/platform/BrowserHasher.test.ts new file mode 100644 index 0000000000..091e9f0864 --- /dev/null +++ b/packages/sdk/browser/__tests__/platform/BrowserHasher.test.ts @@ -0,0 +1,53 @@ +// TextEncoder should be part of jsdom, but it is not. So we can import it from node in the tests. +import { webcrypto } from 'node:crypto'; +import { TextEncoder } from 'node:util'; + +import BrowserHasher from '../../src/platform/BrowserHasher'; + +global.TextEncoder = TextEncoder; + +// Crypto is injectable as it is also not correctly available with the combination of node and jsdom. + +/** + * The links below are different from js-sha256 and are useful to verify the + * correctness of hash and encoding output: + * https://www.liavaag.org/English/SHA-Generator/ + */ +describe('PlatformHasher', () => { + test('sha256 produces correct base64 output', async () => { + // @ts-ignore + const h = new BrowserHasher(webcrypto, 'sha256'); + + h.update('test-app-id'); + const output = await h.asyncDigest('base64'); + + expect(output).toEqual('XVm6ZNk6ejx6+IVtL7zfwYwRQ2/ck9+y7FaN32EcudQ='); + }); + + test('sha256 produces correct hex output', async () => { + // @ts-ignore + const h = new BrowserHasher(webcrypto, 'sha256'); + + h.update('test-app-id'); + const output = await h.asyncDigest('hex'); + + expect(output).toEqual('5d59ba64d93a7a3c7af8856d2fbcdfc18c11436fdc93dfb2ec568ddf611cb9d4'); + }); + + test('unsupported hash algorithm', async () => { + expect(() => { + // @ts-ignore + // eslint-disable-next-line no-new + new BrowserHasher(webcrypto, 'sha1'); + }).toThrow(/unsupported/i); + }); + + test('unsupported output algorithm', async () => { + expect(async () => { + // @ts-ignore + const h = new BrowserHasher(webcrypto, 'sha256'); + h.update('test-app-id'); + await h.asyncDigest('base122'); + }).toThrow(/unsupported/i); + }); +}); diff --git a/packages/sdk/browser/__tests__/platform/randomUuidV4.test.ts b/packages/sdk/browser/__tests__/platform/randomUuidV4.test.ts new file mode 100644 index 0000000000..03ec22d120 --- /dev/null +++ b/packages/sdk/browser/__tests__/platform/randomUuidV4.test.ts @@ -0,0 +1,29 @@ +/* eslint-disable no-bitwise */ +import { fallbackUuidV4, formatDataAsUuidV4 } from '../../src/platform/randomUuidV4'; + +it('formats conformant UUID', () => { + // For this test we remove the random component and just inspect the variant and version. + const idA = formatDataAsUuidV4(Array(16).fill(0x00)); + const idB = formatDataAsUuidV4(Array(16).fill(0xff)); + const idC = fallbackUuidV4(); + + // 32 characters and 4 dashes + expect(idC).toHaveLength(36); + const versionA = idA[14]; + const versionB = idB[14]; + const versionC = idB[14]; + + expect(versionA).toEqual('4'); + expect(versionB).toEqual('4'); + expect(versionC).toEqual('4'); + + // Keep only the top 2 bits. + const specifierA = parseInt(idA[19], 16) & 0xc; + const specifierB = parseInt(idB[19], 16) & 0xc; + const specifierC = parseInt(idC[19], 16) & 0xc; + + // bit 6 should be 0 and bit 8 should be one, which is 0x8 + expect(specifierA).toEqual(0x8); + expect(specifierB).toEqual(0x8); + expect(specifierC).toEqual(0x8); +}); diff --git a/packages/sdk/browser/package.json b/packages/sdk/browser/package.json index 8723526a35..1d1c44388e 100644 --- a/packages/sdk/browser/package.json +++ b/packages/sdk/browser/package.json @@ -27,7 +27,7 @@ ], "scripts": { "clean": "rimraf dist", - "build": "vite build", + "build": "tsc --noEmit && vite build", "lint": "eslint . --ext .ts,.tsx", "prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../.prettierignore", "test": "jest", @@ -35,7 +35,7 @@ "check": "yarn prettier && yarn lint && yarn build && yarn test" }, "dependencies": { - "@launchdarkly/js-client-sdk-common": "1.5.0" + "@launchdarkly/js-client-sdk-common": "1.7.0" }, "devDependencies": { "@launchdarkly/private-js-mocks": "0.0.1", diff --git a/packages/sdk/browser/src/platform/BrowserCrypto.ts b/packages/sdk/browser/src/platform/BrowserCrypto.ts new file mode 100644 index 0000000000..e241bc50a2 --- /dev/null +++ b/packages/sdk/browser/src/platform/BrowserCrypto.ts @@ -0,0 +1,14 @@ +import { Crypto } from '@launchdarkly/js-client-sdk-common'; + +import BrowserHasher from './BrowserHasher'; +import randomUuidV4 from './randomUuidV4'; + +export default class BrowserCrypto implements Crypto { + createHash(algorithm: string): BrowserHasher { + return new BrowserHasher(window.crypto, algorithm); + } + + randomUUID(): string { + return randomUuidV4(); + } +} diff --git a/packages/sdk/browser/src/platform/BrowserHasher.ts b/packages/sdk/browser/src/platform/BrowserHasher.ts new file mode 100644 index 0000000000..fc46d8d878 --- /dev/null +++ b/packages/sdk/browser/src/platform/BrowserHasher.ts @@ -0,0 +1,44 @@ +import { Hasher } from '@launchdarkly/js-client-sdk-common'; + +export default class BrowserHasher implements Hasher { + private data: string[] = []; + private algorithm: string; + constructor( + private readonly webcrypto: Crypto, + algorithm: string, + ) { + switch (algorithm) { + case 'sha1': + this.algorithm = 'SHA-1'; + break; + case 'sha256': + this.algorithm = 'SHA-256'; + break; + default: + throw new Error(`Algorithm is not supported ${algorithm}`); + } + } + + async asyncDigest(encoding: string): Promise { + const combinedData = this.data.join(''); + const encoded = new TextEncoder().encode(combinedData); + const digestedBuffer = await this.webcrypto.subtle.digest(this.algorithm, encoded); + switch (encoding) { + case 'base64': + return btoa(String.fromCharCode(...new Uint8Array(digestedBuffer))); + case 'hex': + // Convert the buffer to an array of uint8 values, then convert each of those to hex. + // The map function on a Uint8Array directly only maps to other Uint8Arrays. + return [...new Uint8Array(digestedBuffer)] + .map((val) => val.toString(16).padStart(2, '0')) + .join(''); + default: + throw new Error(`Encoding is not supported ${encoding}`); + } + } + + update(data: string): Hasher { + this.data.push(data); + return this as Hasher; + } +} diff --git a/packages/sdk/browser/src/platform/BrowserPlatform.ts b/packages/sdk/browser/src/platform/BrowserPlatform.ts index 419840d5d3..8865ebc3c8 100644 --- a/packages/sdk/browser/src/platform/BrowserPlatform.ts +++ b/packages/sdk/browser/src/platform/BrowserPlatform.ts @@ -1,16 +1,18 @@ import { + Crypto, + /* platform */ LDOptions, Storage, - /* platform */ } from '@launchdarkly/js-client-sdk-common'; +import BrowserCrypto from './BrowserCrypto'; import LocalStorage, { isLocalStorageSupported } from './LocalStorage'; export default class BrowserPlatform /* implements platform.Platform */ { // encoding?: Encoding; // info: Info; // fileSystem?: Filesystem; - // crypto: Crypto; + crypto: Crypto = new BrowserCrypto(); // requests: Requests; storage?: Storage; diff --git a/packages/sdk/browser/src/platform/randomUuidV4.ts b/packages/sdk/browser/src/platform/randomUuidV4.ts new file mode 100644 index 0000000000..0659d58f72 --- /dev/null +++ b/packages/sdk/browser/src/platform/randomUuidV4.ts @@ -0,0 +1,99 @@ +// The implementation in this file generates UUIDs in v4 format and is suitable +// for use as a UUID in LaunchDarkly events. It is not a rigorous implementation. + +// It uses crypto.randomUUID when available. +// If crypto.randomUUID is not available, then it uses random values and forms +// the UUID itself. +// When possible it uses crypto.getRandomValues, but it can use Math.random +// if crypto.getRandomValues is not available. + +// UUIDv4 Struct definition. +// https://www.rfc-archive.org/getrfc.php?rfc=4122 +// Appendix A. Appendix A - Sample Implementation +const timeLow = { + start: 0, + end: 3, +}; +const timeMid = { + start: 4, + end: 5, +}; +const timeHiAndVersion = { + start: 6, + end: 7, +}; +const clockSeqHiAndReserved = { + start: 8, + end: 8, +}; +const clockSeqLow = { + start: 9, + end: 9, +}; +const nodes = { + start: 10, + end: 15, +}; + +function getRandom128bit(): number[] { + if (crypto && crypto.getRandomValues) { + const typedArray = new Uint8Array(16); + crypto.getRandomValues(typedArray); + return [...typedArray.values()]; + } + const values = []; + for (let index = 0; index < 16; index += 1) { + // Math.random is 0-1 with inclusive min and exclusive max. + values.push(Math.floor(Math.random() * 256)); + } + return values; +} + +function hex(bytes: number[], range: { start: number; end: number }): string { + let strVal = ''; + for (let index = range.start; index <= range.end; index += 1) { + strVal += bytes[index].toString(16).padStart(2, '0'); + } + return strVal; +} + +/** + * Given a list of 16 random bytes generate a UUID in v4 format. + * + * Note: The input bytes are modified to conform to the requirements of UUID v4. + * + * @param bytes A list of 16 bytes. + * @returns A UUID v4 string. + */ +export function formatDataAsUuidV4(bytes: number[]): string { + // https://www.rfc-archive.org/getrfc.php?rfc=4122 + // 4.4. Algorithms for Creating a UUID from Truly Random or + // Pseudo-Random Numbers + + // Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and + // one, respectively. + // eslint-disable-next-line no-bitwise, no-param-reassign + bytes[clockSeqHiAndReserved.start] = (bytes[clockSeqHiAndReserved.start] | 0x80) & 0xbf; + // Set the four most significant bits (bits 12 through 15) of the time_hi_and_version field to + // the 4-bit version number from Section 4.1.3. + // eslint-disable-next-line no-bitwise, no-param-reassign + bytes[timeHiAndVersion.start] = (bytes[timeHiAndVersion.start] & 0x0f) | 0x40; + + return ( + `${hex(bytes, timeLow)}-${hex(bytes, timeMid)}-${hex(bytes, timeHiAndVersion)}-` + + `${hex(bytes, clockSeqHiAndReserved)}${hex(bytes, clockSeqLow)}-${hex(bytes, nodes)}` + ); +} + +export function fallbackUuidV4(): string { + const bytes = getRandom128bit(); + return formatDataAsUuidV4(bytes); +} + +export default function randomUuidV4(): string { + if (typeof crypto !== undefined && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + + return fallbackUuidV4(); +} From 232b7eee29be7056f74d9aa5989d893b729cc179 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 6 Sep 2024 15:58:19 -0700 Subject: [PATCH 14/18] fix tests --- .../__tests__/platform/BrowserHasher.test.ts | 28 ++++++++++++++++--- packages/sdk/browser/tsconfig.json | 2 +- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/sdk/browser/__tests__/platform/BrowserHasher.test.ts b/packages/sdk/browser/__tests__/platform/BrowserHasher.test.ts index 091e9f0864..986a84d32d 100644 --- a/packages/sdk/browser/__tests__/platform/BrowserHasher.test.ts +++ b/packages/sdk/browser/__tests__/platform/BrowserHasher.test.ts @@ -34,20 +34,40 @@ describe('PlatformHasher', () => { expect(output).toEqual('5d59ba64d93a7a3c7af8856d2fbcdfc18c11436fdc93dfb2ec568ddf611cb9d4'); }); + test('sha1 produces correct base64 output', async () => { + // @ts-ignore + const h = new BrowserHasher(webcrypto, 'sha1'); + + h.update('test-app-id'); + const output = await h.asyncDigest('base64'); + + expect(output).toEqual('kydC7cRd9+LWbu4Ss/t1FiFmDcs='); + }); + + test('sha1 produces correct hex output', async () => { + // @ts-ignore + const h = new BrowserHasher(webcrypto, 'sha1'); + + h.update('test-app-id'); + const output = await h.asyncDigest('hex'); + + expect(output).toEqual('932742edc45df7e2d66eee12b3fb751621660dcb'); + }); + test('unsupported hash algorithm', async () => { expect(() => { // @ts-ignore // eslint-disable-next-line no-new - new BrowserHasher(webcrypto, 'sha1'); - }).toThrow(/unsupported/i); + new BrowserHasher(webcrypto, 'sha512'); + }).toThrow(/Algorithm is not supported/i); }); test('unsupported output algorithm', async () => { - expect(async () => { + await expect(async () => { // @ts-ignore const h = new BrowserHasher(webcrypto, 'sha256'); h.update('test-app-id'); await h.asyncDigest('base122'); - }).toThrow(/unsupported/i); + }).rejects.toThrow(/Encoding is not supported/i); }); }); diff --git a/packages/sdk/browser/tsconfig.json b/packages/sdk/browser/tsconfig.json index b1c92fdd99..79420d3d43 100644 --- a/packages/sdk/browser/tsconfig.json +++ b/packages/sdk/browser/tsconfig.json @@ -4,7 +4,7 @@ "declaration": true, "declarationMap": true, "jsx": "react-jsx", - "lib": ["es6", "dom"], + "lib": ["ES2017", "dom"], "module": "ES6", "moduleResolution": "node", "noImplicitOverride": true, From 6c153b829c357276a88af28701cc883206ffc836 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 9 Sep 2024 08:48:39 -0700 Subject: [PATCH 15/18] PR Feedback. --- packages/shared/sdk-client/src/crypto/digest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/sdk-client/src/crypto/digest.ts b/packages/shared/sdk-client/src/crypto/digest.ts index 24625ec049..c6b38292a5 100644 --- a/packages/shared/sdk-client/src/crypto/digest.ts +++ b/packages/shared/sdk-client/src/crypto/digest.ts @@ -1,6 +1,6 @@ import { Hasher } from '@launchdarkly/js-sdk-common'; -export default function digest(hasher: Hasher, encoding: string) { +export default async function digest(hasher: Hasher, encoding: string): Promise { if (hasher.digest) { return hasher.digest(encoding); } From 21d3db78db9c6e3f584cfc91347cc5abe7fc997e Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:22:45 -0700 Subject: [PATCH 16/18] Add browser encoding. --- .../browser/src/platform/BrowserEncoding.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 packages/sdk/browser/src/platform/BrowserEncoding.ts diff --git a/packages/sdk/browser/src/platform/BrowserEncoding.ts b/packages/sdk/browser/src/platform/BrowserEncoding.ts new file mode 100644 index 0000000000..f673c13273 --- /dev/null +++ b/packages/sdk/browser/src/platform/BrowserEncoding.ts @@ -0,0 +1,18 @@ +import { Encoding } from '@launchdarkly/js-client-sdk-common'; + +function bytesToBase64(bytes: Uint8Array) { + const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join(''); + return btoa(binString); +} + +/** + * Implementation Note: This btoa handles unicode characters, which the base btoa in the browser + * does not. + * Background: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem + */ + +export default class BrowserEncoding implements Encoding { + btoa(data: string): string { + return bytesToBase64(new TextEncoder().encode(data)); + } +} From 5767037033e88e34dc114615b7e31dd307ca8742 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:33:10 -0700 Subject: [PATCH 17/18] Add tests for browser encoding. --- .../__tests__/platform/BrowserEncoding.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 packages/sdk/browser/__tests__/platform/BrowserEncoding.test.ts diff --git a/packages/sdk/browser/__tests__/platform/BrowserEncoding.test.ts b/packages/sdk/browser/__tests__/platform/BrowserEncoding.test.ts new file mode 100644 index 0000000000..a248656098 --- /dev/null +++ b/packages/sdk/browser/__tests__/platform/BrowserEncoding.test.ts @@ -0,0 +1,18 @@ +// TextEncoder should be part of jsdom, but it is not. So we can import it from node in the tests. +import { TextEncoder } from 'node:util'; + +import BrowserEncoding from '../../src/platform/BrowserEncoding'; + +global.TextEncoder = TextEncoder; + +it('can base64 a basic ASCII string', () => { + const encoding = new BrowserEncoding(); + expect(encoding.btoa('toaster')).toEqual('dG9hc3Rlcg=='); +}); + +it('can base64 a unicode string containing multi-byte character', () => { + const encoding = new BrowserEncoding(); + expect(encoding.btoa('✇⽊❽⾵⊚▴ⶊ↺➹≈⋟⚥⤅⊈ⲏⷨ⾭Ⲗ⑲▯ⶋₐℛ⬎⿌🦄')).toEqual( + '4pyH4r2K4p294r614oqa4pa04raK4oa64p654omI4ouf4pql4qSF4oqI4rKP4reo4r6t4rKW4pGy4pav4raL4oKQ4oSb4qyO4r+M8J+mhA==', + ); +}); From 5506bbe415a3f09dd2ee31a64fb830ad582c56ff Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:46:27 -0700 Subject: [PATCH 18/18] Test vector generation comment. --- packages/sdk/browser/__tests__/platform/BrowserHasher.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/sdk/browser/__tests__/platform/BrowserHasher.test.ts b/packages/sdk/browser/__tests__/platform/BrowserHasher.test.ts index 986a84d32d..cb143f119f 100644 --- a/packages/sdk/browser/__tests__/platform/BrowserHasher.test.ts +++ b/packages/sdk/browser/__tests__/platform/BrowserHasher.test.ts @@ -9,8 +9,7 @@ global.TextEncoder = TextEncoder; // Crypto is injectable as it is also not correctly available with the combination of node and jsdom. /** - * The links below are different from js-sha256 and are useful to verify the - * correctness of hash and encoding output: + * Test vectors generated using. * https://www.liavaag.org/English/SHA-Generator/ */ describe('PlatformHasher', () => {