Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/strong-singers-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@evervault/sdk': minor
---

Introduce agent parameters to SDK constructor to allow greater networking control from the Node SDK
12 changes: 9 additions & 3 deletions lib/core/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ const axios = require('axios');
* @param {string} appUuid
* @param {string} apiKey
* @param {import('../types').HttpConfig} config
* @param {{ httpAgent?: import('http').Agent, httpsAgent?: import('https').Agent }} [agents]
*/
module.exports = (appUuid, apiKey, config) => {
module.exports = (appUuid, apiKey, config, { httpAgent, httpsAgent } = {}) => {
const request = (
method,
path,
Expand All @@ -28,7 +29,7 @@ module.exports = (appUuid, apiKey, config) => {
headers['api-key'] = apiKey;
}

return axios({
const requestConfig = {
url:
path.startsWith('https://') || path.startsWith('http://')
? path
Expand All @@ -38,7 +39,12 @@ module.exports = (appUuid, apiKey, config) => {
data,
validateStatus: (_) => true,
responseType,
});
};

if (httpAgent) requestConfig.httpAgent = httpAgent;
if (httpsAgent) requestConfig.httpsAgent = httpsAgent;

return axios(requestConfig);
};

const get = (path, headers) => request('GET', path, headers);
Expand Down
24 changes: 23 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const crypto = require('crypto');
const http = require('http');
const https = require('https');
const retry = require('async-retry');
const { Buffer } = require('buffer');
Expand Down Expand Up @@ -75,9 +76,30 @@ class EvervaultClient {
curve = options.curve;
}

if (
options.httpAgent != null &&
!(options.httpAgent instanceof http.Agent)
) {
throw new errors.EvervaultError(
'options.httpAgent must be an instance of http.Agent'
);
}

if (
options.httpsAgent != null &&
!(options.httpsAgent instanceof https.Agent)
) {
throw new errors.EvervaultError(
'options.httpsAgent must be an instance of https.Agent'
);
}

this.curve = curve;
this.retry = options.retry;
this.http = Http(appId, apiKey, this.config.http);
this.http = Http(appId, apiKey, this.config.http, {
httpAgent: options.httpAgent,
httpsAgent: options.httpsAgent,
});
this.crypto = Crypto(this.config.encryption[curve]);
this.httpsHelper = httpsHelper;
this.apiKey = apiKey;
Expand Down
2 changes: 2 additions & 0 deletions lib/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export interface SdkOptions {
curve?: SupportedCurve;
retry?: boolean;
enableOutboundRelay?: boolean;
httpAgent?: import('http').Agent;
httpsAgent?: import('https').Agent;
}

export interface PCRs {
Expand Down
84 changes: 84 additions & 0 deletions tests/core/http.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const http = require('http');
const https = require('https');
const { expect } = require('chai');
const { errors } = require('../../lib/utils');
const nock = require('nock');
Expand Down Expand Up @@ -593,6 +595,88 @@ describe('Http Module', () => {
});
});

describe('agent forwarding', () => {
const rewire = require('rewire');

/**
* Creates a rewired http module with a stub axios and optional agents,
* returning both the client and a getter for the last captured axios config.
*/
const buildClientWithAxiosStub = (agents = {}) => {
const httpModule = rewire('../../lib/core/http');
let capturedConfig;
const axiosStub = (cfg) => {
capturedConfig = cfg;
return Promise.resolve({
status: 200,
data: {},
headers: { 'x-poll-interval': '5' },
});
};
httpModule.__set__('axios', axiosStub);
const client = httpModule(testAppId, testApiKey, testValidConfig, agents);
return {
client,
getCapturedConfig: () => capturedConfig,
};
};

context('when httpAgent is provided', () => {
it('sets httpAgent on the axios request config and does not set httpsAgent', async () => {
const agent = new http.Agent();
const { client, getCapturedConfig } = buildClientWithAxiosStub({
httpAgent: agent,
});

await client.getRelayOutboundConfig();

expect(getCapturedConfig().httpAgent).to.equal(agent);
expect(getCapturedConfig().httpsAgent).to.be.undefined;
});
});

context('when httpsAgent is provided', () => {
it('sets httpsAgent on the axios request config and does not set httpAgent', async () => {
const agent = new https.Agent();
const { client, getCapturedConfig } = buildClientWithAxiosStub({
httpsAgent: agent,
});

await client.getRelayOutboundConfig();

expect(getCapturedConfig().httpsAgent).to.equal(agent);
expect(getCapturedConfig().httpAgent).to.be.undefined;
});
});

context('when both agents are provided', () => {
it('sets both httpAgent and httpsAgent on the axios request config', async () => {
const httpAgentInstance = new http.Agent();
const httpsAgentInstance = new https.Agent();
const { client, getCapturedConfig } = buildClientWithAxiosStub({
httpAgent: httpAgentInstance,
httpsAgent: httpsAgentInstance,
});

await client.getRelayOutboundConfig();

expect(getCapturedConfig().httpAgent).to.equal(httpAgentInstance);
expect(getCapturedConfig().httpsAgent).to.equal(httpsAgentInstance);
});
});

context('when no agents are provided', () => {
it('does not set httpAgent or httpsAgent on the axios request config', async () => {
const { client, getCapturedConfig } = buildClientWithAxiosStub();

await client.getRelayOutboundConfig();

expect(getCapturedConfig().httpAgent).to.be.undefined;
expect(getCapturedConfig().httpsAgent).to.be.undefined;
});
});
});

describe('getRelayOutboundConfig', () => {
context('Given an api key', () => {
context('Request is successful', () => {
Expand Down
54 changes: 54 additions & 0 deletions tests/sdk.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const http = require('http');
const https = require('https');
const chai = require('chai');
chai.use(require('sinon-chai'));
const { expect } = chai;
Expand Down Expand Up @@ -25,6 +27,58 @@ describe('evervault client', () => {
const test = () => new Evervault(testAppId, '');
expect(test).to.throw(errors.EvervaultError);
});

context('agent validation', () => {
it('should throw if httpAgent is not an instance of http.Agent', () => {
const Evervault = require('../lib');
const test = () =>
new Evervault(testAppId, testApiKey, { httpAgent: { fake: true } });
expect(test).to.throw(
errors.EvervaultError,
'options.httpAgent must be an instance of http.Agent'
);
});

it('should throw if httpsAgent is not an instance of https.Agent', () => {
const Evervault = require('../lib');
const test = () =>
new Evervault(testAppId, testApiKey, {
httpsAgent: { fake: true },
});
expect(test).to.throw(
errors.EvervaultError,
'options.httpsAgent must be an instance of https.Agent'
);
});

it('should accept a valid http.Agent without throwing', () => {
const Evervault = require('../lib');
const test = () =>
new Evervault(testAppId, testApiKey, {
httpAgent: new http.Agent(),
});
expect(test).to.not.throw();
});

it('should accept a valid https.Agent without throwing', () => {
const Evervault = require('../lib');
const test = () =>
new Evervault(testAppId, testApiKey, {
httpsAgent: new https.Agent(),
});
expect(test).to.not.throw();
});

it('should accept both a valid http.Agent and https.Agent without throwing', () => {
const Evervault = require('../lib');
const test = () =>
new Evervault(testAppId, testApiKey, {
httpAgent: new http.Agent(),
httpsAgent: new https.Agent(),
});
expect(test).to.not.throw();
});
});
});

context('calling methods that require api keys', () => {
Expand Down