From fb676f28ebfd219d5c9585ea5f943e2e3690d65d Mon Sep 17 00:00:00 2001 From: Cynthia Lang <39891200+cynsupercat@users.noreply.github.com> Date: Wed, 21 Feb 2024 14:55:55 +1100 Subject: [PATCH] chore: Add overrides for headers when baseConfig contains immutable keys (#1511) --- .../sdk/src/config/config.test.ts | 26 +++++- .../blockchain-data/sdk/src/config/index.ts | 22 +++-- packages/config/package.json | 2 + packages/config/src/index.test.ts | 87 +++++++++++++++++++ packages/config/src/index.ts | 66 ++++++++++++-- 5 files changed, 190 insertions(+), 13 deletions(-) create mode 100644 packages/config/src/index.test.ts diff --git a/packages/blockchain-data/sdk/src/config/config.test.ts b/packages/blockchain-data/sdk/src/config/config.test.ts index 1aee3e0b59..3dd0bdb1a5 100644 --- a/packages/blockchain-data/sdk/src/config/config.test.ts +++ b/packages/blockchain-data/sdk/src/config/config.test.ts @@ -1,4 +1,4 @@ -import { Environment, ImmutableConfiguration } from '@imtbl/config'; +import { Environment, ImmutableConfiguration, KeyHeaders } from '@imtbl/config'; import { BlockchainData } from '../blockchain-data'; import { BlockchainDataModuleConfiguration } from './index'; @@ -64,4 +64,28 @@ describe('BlockchainData', () => { headers ); }); + + it('should instantiate a BlockchainData with rate limit and api key in headers', async () => { + const rateLimitKey = 'rateLimit'; + const apiKey = 'api'; + + const headers = { + testHeader: 'ts-immutable-sdk-0.0.1', + [KeyHeaders.API_KEY]: apiKey, + [KeyHeaders.RATE_LIMITING_KEY]: rateLimitKey, + }; + + const config: BlockchainDataModuleConfiguration = { + baseConfig: { environment: Environment.PRODUCTION, rateLimitingKey: rateLimitKey, apiKey }, + overrides: { + basePath: 'https://api.dev.immutable.com/v1', + headers, + }, + }; + const blockchainData = new BlockchainData(config); + expect(blockchainData).toBeInstanceOf(BlockchainData); + expect(blockchainData.config.apiConfig.baseOptions?.headers).toMatchObject( + headers + ); + }); }); diff --git a/packages/blockchain-data/sdk/src/config/index.ts b/packages/blockchain-data/sdk/src/config/index.ts index 13f17eafe8..d3413cc7e1 100644 --- a/packages/blockchain-data/sdk/src/config/index.ts +++ b/packages/blockchain-data/sdk/src/config/index.ts @@ -3,6 +3,7 @@ import { Environment, ImmutableConfiguration, ModuleConfiguration, + addKeysToHeadersOverride } from '@imtbl/config'; import { mr } from '@imtbl/generated-clients'; @@ -20,16 +21,23 @@ export interface APIConfigurationParams { * createAPIConfiguration to create a custom Configuration * other than the production and sandbox defined below. */ -export const createAPIConfiguration = ({ - baseConfig, - basePath, - headers: baseHeaders, -}: APIConfigurationParams): mr.Configuration => { +export const createAPIConfiguration = (overrides: APIConfigurationParams): mr.Configuration => { + const { + baseConfig, + basePath, + headers: baseHeaders, + } = overrides; + if (!basePath.trim()) { throw Error('basePath can not be empty'); } - const headers = { ...(baseHeaders || {}), ...defaultHeaders }; + const headers = { + ...(baseHeaders || {}), + ...(addKeysToHeadersOverride(baseConfig, overrides) || {}), + ...defaultHeaders + }; + const configParams: mr.ConfigurationParameters = { ...baseConfig, basePath, @@ -40,7 +48,7 @@ export const createAPIConfiguration = ({ }; export interface BlockchainDataModuleConfiguration - extends ModuleConfiguration {} + extends ModuleConfiguration { } export class BlockchainDataConfiguration { readonly apiConfig: mr.Configuration; diff --git a/packages/config/package.json b/packages/config/package.json index c5ab612c0e..94aac708fa 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -47,6 +47,8 @@ "scripts": { "build": "NODE_ENV=production rollup --config rollup.config.js", "lint": "eslint ./src --ext .ts,.jsx,.tsx --max-warnings=0", + "test": "jest", + "test:watch": "jest --watch", "typecheck": "tsc --noEmit --jsx preserve" }, "type": "module", diff --git a/packages/config/src/index.test.ts b/packages/config/src/index.test.ts new file mode 100644 index 0000000000..98fd66475b --- /dev/null +++ b/packages/config/src/index.test.ts @@ -0,0 +1,87 @@ +import { + Environment, + ImmutableConfiguration, + KeyHeaders, + addKeysToHeadersOverride, +} from './index'; + +describe('Key header override', () => { + const apiKey = 'testKey'; + const publishableKey = 'testPublishableKey'; + const rateLimitingKey = 'testRateLimitKey'; + + it('should return passed in override if no base config is present', () => { + const overrides = { + headers: { + testHeader: 'test', + }, + }; + + const result = addKeysToHeadersOverride(undefined, overrides); + expect(result).toEqual(overrides); + }); + + it('should return passed in override if no keys are present', () => { + const baseConfig = new ImmutableConfiguration({ + environment: Environment.SANDBOX, + }); + const overrides = { + headers: { + testHeader: 'test', + }, + }; + + const result = addKeysToHeadersOverride(baseConfig, overrides); + expect(result).toEqual(overrides); + }); + + it('Should append headers to override', () => { + const baseConfig = new ImmutableConfiguration({ + environment: Environment.SANDBOX, + apiKey, + rateLimitingKey, + publishableKey, + }); + + const overrides = { + headers: { + [KeyHeaders.API_KEY]: apiKey, + [KeyHeaders.RATE_LIMITING_KEY]: rateLimitingKey, + [KeyHeaders.PUBLISHABLE_KEY]: publishableKey, + }, + }; + + const result = addKeysToHeadersOverride(baseConfig, overrides); + expect(result).toEqual(overrides); + }); + + it('Should merge headers with existing overrides, with user overrides taking precedence', () => { + const baseConfig = new ImmutableConfiguration({ + environment: Environment.SANDBOX, + apiKey, + rateLimitingKey, + publishableKey, + }); + + const overrides = { + headers: { + [KeyHeaders.API_KEY]: 'userOverriddenApiKey', + // eslint-disable-next-line @typescript-eslint/naming-convention + 'new-header': 'test', + }, + }; + const expectedOverrides = { + headers: { + [KeyHeaders.API_KEY]: 'userOverriddenApiKey', + [KeyHeaders.RATE_LIMITING_KEY]: rateLimitingKey, + [KeyHeaders.PUBLISHABLE_KEY]: publishableKey, + // eslint-disable-next-line @typescript-eslint/naming-convention + 'new-header': 'test', + }, + }; + + const result = addKeysToHeadersOverride(baseConfig, overrides); + + expect(result).toEqual(expectedOverrides); + }); +}); diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 0abedbe7fc..785b4e2809 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -6,6 +6,12 @@ export enum Environment { SANDBOX = 'sandbox', } +export enum KeyHeaders { + API_KEY = 'x-immutable-api-key', + PUBLISHABLE_KEY = 'x-immutable-publishable-key', + RATE_LIMITING_KEY = 'x-api-key', +} + export class ImmutableConfiguration { readonly environment: Environment; @@ -15,13 +21,63 @@ export class ImmutableConfiguration { readonly publishableKey?: string; - constructor(options: { environment: Environment }) { + constructor(options: { + environment: Environment, + rateLimitingKey?: string, + apiKey?: string, + publishableKey?: string + }) { this.environment = options.environment; + this.publishableKey = options.publishableKey; + this.apiKey = options.apiKey; + this.rateLimitingKey = options.rateLimitingKey; + setEnvironment(options.environment); track('config', 'created_imtbl_config'); } } +// Adds publishableKey, apiKey, and rateLimitingKey to the headers of the overrides object +// if exists in base config. Otherwise returns the overrides object as is. +// Use this for openapi generated clients with security headers. +export const addKeysToHeadersOverride = }>( + baseConfig: ImmutableConfiguration | undefined, + overrides: T | undefined): T | undefined => { + if (!baseConfig || (!baseConfig.apiKey && !baseConfig.publishableKey && !baseConfig.rateLimitingKey)) { + return overrides; + } + + const newHeaders: Record = {}; + + if (baseConfig.apiKey) { + newHeaders[KeyHeaders.API_KEY] = baseConfig.apiKey; + } + + if (baseConfig.publishableKey) { + newHeaders[KeyHeaders.PUBLISHABLE_KEY] = baseConfig.publishableKey; + } + + if (baseConfig.rateLimitingKey) { + newHeaders[KeyHeaders.RATE_LIMITING_KEY] = baseConfig.rateLimitingKey; + } + + // If overrides and overrides.headers exist, merge them with newHeaders, giving precedence to existing overrides + if (overrides && overrides.headers) { + return { + ...overrides, + headers: { + ...newHeaders, // Add newHeaders first so that the existing keys in overrides.headers can override them + ...overrides.headers, + }, + }; + } + + return { + ...overrides, + headers: newHeaders, + } as T; +}; + const API_KEY_PREFIX = 'sk_imapik-'; const PUBLISHABLE_KEY_PREFIX = 'pk_imapik-'; @@ -31,22 +87,22 @@ export const addApiKeyToAxiosHeader = (apiKey: string) => { 'Invalid API key. Create your api key in Immutable developer hub. https://hub.immutable.com', ); } - axios.defaults.headers.common['x-immutable-api-key'] = apiKey; + axios.defaults.headers.common[KeyHeaders.API_KEY] = apiKey; }; export const addPublishableKeyToAxiosHeader = (publishableKey: string) => { if (!publishableKey.startsWith(PUBLISHABLE_KEY_PREFIX)) { throw new Error( 'Invalid Publishable key. Create your Publishable key in Immutable developer hub.' - + ' https://hub.immutable.com', + + ' https://hub.immutable.com', ); } setPublishableApiKey(publishableKey); - axios.defaults.headers.common['x-immutable-publishable-key'] = publishableKey; + axios.defaults.headers.common[KeyHeaders.PUBLISHABLE_KEY] = publishableKey; }; export const addRateLimitingKeyToAxiosHeader = (rateLimitingKey: string) => { - axios.defaults.headers.common['x-api-key'] = rateLimitingKey; + axios.defaults.headers.common[KeyHeaders.RATE_LIMITING_KEY] = rateLimitingKey; }; type ImmutableConfigurationWithRequireableFields = ImmutableConfiguration &