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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AnySchema } from 'ajv';
import { EntityValidator } from '../validator';

const RESOLVED_SCHEMAS_URL =
'https://raw.githubusercontent.com/JupiterOne/data-model/main/external/resolvedSchemas.json';
'https://api.us.jupiterone.io/data-model/schemas/classes';

const ENTITY_SCHEMA = {
$schema: 'http://json-schema.org/draft-07/schema#',
Expand Down
168 changes: 168 additions & 0 deletions packages/integration-sdk-runtime/src/api/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,171 @@ describe('real Alpha request with fake API key', () => {
}
});
});

describe('createApiClient', () => {
const originalEnv = process.env;

beforeEach(() => {
jest.resetModules();
process.env = { ...originalEnv };
});

afterAll(() => {
process.env = originalEnv;
});

describe('proxy configuration', () => {
it('should not configure proxy when no proxy URL is provided', () => {
const client = createApiClient({
apiBaseUrl: 'https://api.example.com',
account: 'test-account',
accessToken: 'test-token',
});

// The client should be created without proxy configuration
expect(client).toBeDefined();
});

it('should configure proxy when proxyUrl parameter is provided', () => {
const proxyUrl = 'https://foo:bar@proxy.example.com:8888';

const client = createApiClient({
apiBaseUrl: 'https://api.example.com',
account: 'test-account',
accessToken: 'test-token',
proxyUrl,
});

expect(client).toBeDefined();
// Note: We can't easily test the internal proxy config without exposing it
// This test verifies the client is created successfully with proxy config
});

it('should configure proxy from HTTPS_PROXY environment variable', () => {
process.env.HTTPS_PROXY = 'https://foo:bar@proxy.example.com:8888';

const client = createApiClient({
apiBaseUrl: 'https://api.example.com',
account: 'test-account',
accessToken: 'test-token',
});

expect(client).toBeDefined();
});

it('should configure proxy from https_proxy environment variable', () => {
process.env.https_proxy = 'http://user:pass@proxy.local:3128';

const client = createApiClient({
apiBaseUrl: 'https://api.example.com',
account: 'test-account',
accessToken: 'test-token',
});

expect(client).toBeDefined();
});

it('should prefer HTTPS_PROXY over https_proxy', () => {
process.env.HTTPS_PROXY = 'https://primary:proxy@proxy1.com:8888';
process.env.https_proxy = 'http://secondary:proxy@proxy2.com:3128';

const client = createApiClient({
apiBaseUrl: 'https://api.example.com',
account: 'test-account',
accessToken: 'test-token',
});

expect(client).toBeDefined();
});

it('should prefer proxyUrl parameter over environment variables', () => {
process.env.HTTPS_PROXY = 'https://env:proxy@env-proxy.com:8888';
const proxyUrl = 'https://param:proxy@param-proxy.com:9999';

const client = createApiClient({
apiBaseUrl: 'https://api.example.com',
account: 'test-account',
accessToken: 'test-token',
proxyUrl,
});

expect(client).toBeDefined();
});
});

describe('parseProxyUrl functionality', () => {
// We need to import the parseProxyUrl function or test it indirectly
it('should handle proxy URL with authentication', () => {
process.env.HTTPS_PROXY =
'https://username:password@proxy.example.com:8888';

const client = createApiClient({
apiBaseUrl: 'https://api.example.com',
account: 'test-account',
accessToken: 'test-token',
});

expect(client).toBeDefined();
});

it('should handle proxy URL without authentication', () => {
process.env.HTTPS_PROXY = 'https://proxy.example.com:8888';

const client = createApiClient({
apiBaseUrl: 'https://api.example.com',
account: 'test-account',
accessToken: 'test-token',
});

expect(client).toBeDefined();
});

it('should handle HTTP proxy URLs', () => {
process.env.HTTPS_PROXY = 'http://proxy.example.com:3128';

const client = createApiClient({
apiBaseUrl: 'https://api.example.com',
account: 'test-account',
accessToken: 'test-token',
});

expect(client).toBeDefined();
});

it('should throw an error for invalid proxy URLs', () => {
process.env.HTTPS_PROXY = 'invalid-url';

expect(() => {
createApiClient({
apiBaseUrl: 'https://api.example.com',
account: 'test-account',
accessToken: 'test-token',
});
}).toThrow();
});

it('should use default ports when not specified', () => {
// Test HTTPS default port (443)
process.env.HTTPS_PROXY = 'https://proxy.example.com';

let client = createApiClient({
apiBaseUrl: 'https://api.example.com',
account: 'test-account',
accessToken: 'test-token',
});

expect(client).toBeDefined();

// Test HTTP default port (80)
process.env.HTTPS_PROXY = 'http://proxy.example.com';

client = createApiClient({
apiBaseUrl: 'https://api.example.com',
account: 'test-account',
accessToken: 'test-token',
});

expect(client).toBeDefined();
});
});
});
31 changes: 31 additions & 0 deletions packages/integration-sdk-runtime/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Alpha, AlphaInterceptor, AlphaOptions } from '@lifeomic/alpha';
import { AxiosProxyConfig } from 'axios';
import { IntegrationError } from '@jupiterone/integration-sdk-core';
import dotenv from 'dotenv';
import dotenvExpand from 'dotenv-expand';
Expand All @@ -18,6 +19,7 @@ interface CreateApiClientInput {
retryOptions?: RetryOptions;
compressUploads?: boolean;
alphaOptions?: AlphaOptions;
proxyUrl?: string;
}

interface RetryOptions {
Expand All @@ -43,6 +45,7 @@ export function createApiClient({
retryOptions,
compressUploads,
alphaOptions,
proxyUrl,
}: CreateApiClientInput): ApiClient {
const headers: Record<string, string> = {
'JupiterOne-Account': account,
Expand All @@ -52,10 +55,15 @@ export function createApiClient({
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}

const proxyUrlString = proxyUrl || getProxyFromEnvironment();
const proxy = proxyUrlString ? parseProxyUrl(proxyUrlString) : undefined;

const opts: AlphaOptions = {
baseURL: apiBaseUrl,
headers,
retry: retryOptions ?? {},
...(proxy && { proxy }),
...alphaOptions,
};

Expand Down Expand Up @@ -161,3 +169,26 @@ export const getApiKeyFromEnvironment = () =>

export const getAccountFromEnvironment = () =>
getFromEnv('JUPITERONE_ACCOUNT', IntegrationAccountRequiredError);

function parseProxyUrl(proxyUrl: string) {
const url = new URL(proxyUrl);
const proxy: AxiosProxyConfig = {
host: url.hostname,
port: parseInt(url.port) || (url.protocol === 'https:' ? 443 : 80),
protocol: url.protocol.replace(':', ''),
};

if (url.username && url.password) {
proxy.auth = {
username: decodeURIComponent(url.username),
password: decodeURIComponent(url.password),
};
}

return proxy;
}

function getProxyFromEnvironment(): string | undefined {
dotenvExpand(dotenv.config());
return process.env.HTTPS_PROXY || process.env.https_proxy;
}