Skip to content

Commit

Permalink
chore: Add overrides for headers when baseConfig contains immutable k…
Browse files Browse the repository at this point in the history
…eys (#1511)
  • Loading branch information
cynsupercat authored Feb 21, 2024
1 parent d8fe557 commit fb676f2
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 13 deletions.
26 changes: 25 additions & 1 deletion packages/blockchain-data/sdk/src/config/config.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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
);
});
});
22 changes: 15 additions & 7 deletions packages/blockchain-data/sdk/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Environment,
ImmutableConfiguration,
ModuleConfiguration,
addKeysToHeadersOverride
} from '@imtbl/config';
import { mr } from '@imtbl/generated-clients';

Expand All @@ -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,
Expand All @@ -40,7 +48,7 @@ export const createAPIConfiguration = ({
};

export interface BlockchainDataModuleConfiguration
extends ModuleConfiguration<APIConfigurationParams> {}
extends ModuleConfiguration<APIConfigurationParams> { }

export class BlockchainDataConfiguration {
readonly apiConfig: mr.Configuration;
Expand Down
2 changes: 2 additions & 0 deletions packages/config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
87 changes: 87 additions & 0 deletions packages/config/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
66 changes: 61 additions & 5 deletions packages/config/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 = <T extends { headers?: Record<string, string> }>(
baseConfig: ImmutableConfiguration | undefined,
overrides: T | undefined): T | undefined => {
if (!baseConfig || (!baseConfig.apiKey && !baseConfig.publishableKey && !baseConfig.rateLimitingKey)) {
return overrides;
}

const newHeaders: Record<string, string> = {};

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-';

Expand All @@ -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<T> = ImmutableConfiguration &
Expand Down

0 comments on commit fb676f2

Please sign in to comment.