diff --git a/packages/integration-sdk-entity-validator/src/__tests__/validator.test.ts b/packages/integration-sdk-entity-validator/src/__tests__/validator.test.ts index 8d049031e..bf7fbaebb 100644 --- a/packages/integration-sdk-entity-validator/src/__tests__/validator.test.ts +++ b/packages/integration-sdk-entity-validator/src/__tests__/validator.test.ts @@ -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#', diff --git a/packages/integration-sdk-runtime/src/api/__tests__/index.test.ts b/packages/integration-sdk-runtime/src/api/__tests__/index.test.ts index 91f6135c9..9413d430f 100644 --- a/packages/integration-sdk-runtime/src/api/__tests__/index.test.ts +++ b/packages/integration-sdk-runtime/src/api/__tests__/index.test.ts @@ -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(); + }); + }); +}); diff --git a/packages/integration-sdk-runtime/src/api/index.ts b/packages/integration-sdk-runtime/src/api/index.ts index f30baa058..08b90cdd1 100644 --- a/packages/integration-sdk-runtime/src/api/index.ts +++ b/packages/integration-sdk-runtime/src/api/index.ts @@ -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'; @@ -18,6 +19,7 @@ interface CreateApiClientInput { retryOptions?: RetryOptions; compressUploads?: boolean; alphaOptions?: AlphaOptions; + proxyUrl?: string; } interface RetryOptions { @@ -43,6 +45,7 @@ export function createApiClient({ retryOptions, compressUploads, alphaOptions, + proxyUrl, }: CreateApiClientInput): ApiClient { const headers: Record = { 'JupiterOne-Account': account, @@ -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, }; @@ -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; +}