From 1eef4325728905fce5df0e329e3156d3c35dbc0a Mon Sep 17 00:00:00 2001 From: Nick Phillips Date: Wed, 10 Sep 2025 10:21:37 -0500 Subject: [PATCH 1/7] Adds proxy support using the Alpha client --- .../src/api/__tests__/index.test.ts | 176 ++++++++++++++++++ .../integration-sdk-runtime/src/api/index.ts | 35 ++++ 2 files changed, 211 insertions(+) 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 91f6135c..87391275 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,179 @@ 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 handle invalid proxy URLs gracefully', () => { + process.env.HTTPS_PROXY = 'invalid-url'; + + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + const client = createApiClient({ + apiBaseUrl: 'https://api.example.com', + account: 'test-account', + accessToken: 'test-token', + }); + + expect(client).toBeDefined(); + expect(consoleSpy).toHaveBeenCalledWith( + 'Failed to parse proxy URL:', + 'invalid-url', + expect.any(Error) + ); + + consoleSpy.mockRestore(); + }); + + 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(); + }); + }); +}); \ No newline at end of file diff --git a/packages/integration-sdk-runtime/src/api/index.ts b/packages/integration-sdk-runtime/src/api/index.ts index f30baa05..e8711025 100644 --- a/packages/integration-sdk-runtime/src/api/index.ts +++ b/packages/integration-sdk-runtime/src/api/index.ts @@ -18,6 +18,7 @@ interface CreateApiClientInput { retryOptions?: RetryOptions; compressUploads?: boolean; alphaOptions?: AlphaOptions; + proxyUrl?: string; } interface RetryOptions { @@ -43,6 +44,7 @@ export function createApiClient({ retryOptions, compressUploads, alphaOptions, + proxyUrl, }: CreateApiClientInput): ApiClient { const headers: Record = { 'JupiterOne-Account': account, @@ -52,10 +54,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 +168,31 @@ export const getApiKeyFromEnvironment = () => export const getAccountFromEnvironment = () => getFromEnv('JUPITERONE_ACCOUNT', IntegrationAccountRequiredError); + +function parseProxyUrl(proxyUrl: string) { + try { + const url = new URL(proxyUrl); + const proxy: any = { + 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; + } catch (error) { + console.warn('Failed to parse proxy URL:', proxyUrl, error instanceof TypeError ? error : new TypeError(String(error))); + return undefined; + } +} + +function getProxyFromEnvironment(): string | undefined { + dotenvExpand(dotenv.config()); + return process.env.HTTPS_PROXY || process.env.https_proxy; +} From 3320721ab0a24c4095f13d1c454eb5efbdcbc56e Mon Sep 17 00:00:00 2001 From: Nick Phillips Date: Wed, 10 Sep 2025 10:35:04 -0500 Subject: [PATCH 2/7] Changes any object to the type required by axios --- packages/integration-sdk-runtime/src/api/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/integration-sdk-runtime/src/api/index.ts b/packages/integration-sdk-runtime/src/api/index.ts index e8711025..c3e93de3 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'; @@ -172,7 +173,7 @@ export const getAccountFromEnvironment = () => function parseProxyUrl(proxyUrl: string) { try { const url = new URL(proxyUrl); - const proxy: any = { + const proxy: AxiosProxyConfig = { host: url.hostname, port: parseInt(url.port) || (url.protocol === 'https:' ? 443 : 80), protocol: url.protocol.replace(':', ''), From 6de826a2deb1a1391245daea8315b13de23d71e9 Mon Sep 17 00:00:00 2001 From: nickjupiter1 Date: Wed, 10 Sep 2025 10:36:11 -0500 Subject: [PATCH 3/7] Update packages/integration-sdk-runtime/src/api/index.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/integration-sdk-runtime/src/api/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/integration-sdk-runtime/src/api/index.ts b/packages/integration-sdk-runtime/src/api/index.ts index c3e93de3..165f0abc 100644 --- a/packages/integration-sdk-runtime/src/api/index.ts +++ b/packages/integration-sdk-runtime/src/api/index.ts @@ -188,7 +188,8 @@ function parseProxyUrl(proxyUrl: string) { return proxy; } catch (error) { - console.warn('Failed to parse proxy URL:', proxyUrl, error instanceof TypeError ? error : new TypeError(String(error))); + const parsedError = error instanceof TypeError ? error : new TypeError(String(error)); + console.warn('Failed to parse proxy URL:', proxyUrl, parsedError); return undefined; } } From 62d51816e65ff78ebdb9d4df8dfd1598e195c926 Mon Sep 17 00:00:00 2001 From: Nick Phillips Date: Wed, 10 Sep 2025 10:55:30 -0500 Subject: [PATCH 4/7] Let invalid proxy url be thrown --- packages/integration-sdk-runtime/src/api/index.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/integration-sdk-runtime/src/api/index.ts b/packages/integration-sdk-runtime/src/api/index.ts index 165f0abc..b70eeb23 100644 --- a/packages/integration-sdk-runtime/src/api/index.ts +++ b/packages/integration-sdk-runtime/src/api/index.ts @@ -171,7 +171,6 @@ export const getAccountFromEnvironment = () => getFromEnv('JUPITERONE_ACCOUNT', IntegrationAccountRequiredError); function parseProxyUrl(proxyUrl: string) { - try { const url = new URL(proxyUrl); const proxy: AxiosProxyConfig = { host: url.hostname, @@ -187,11 +186,6 @@ function parseProxyUrl(proxyUrl: string) { } return proxy; - } catch (error) { - const parsedError = error instanceof TypeError ? error : new TypeError(String(error)); - console.warn('Failed to parse proxy URL:', proxyUrl, parsedError); - return undefined; - } } function getProxyFromEnvironment(): string | undefined { From fd411019f636674c69f1f9ec628c1490382ca2ee Mon Sep 17 00:00:00 2001 From: Nick Phillips Date: Wed, 10 Sep 2025 11:02:30 -0500 Subject: [PATCH 5/7] fix tests --- .../src/api/__tests__/index.test.ts | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) 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 87391275..15fc60e5 100644 --- a/packages/integration-sdk-runtime/src/api/__tests__/index.test.ts +++ b/packages/integration-sdk-runtime/src/api/__tests__/index.test.ts @@ -301,25 +301,16 @@ describe('createApiClient', () => { expect(client).toBeDefined(); }); - it('should handle invalid proxy URLs gracefully', () => { + it('should throw an error for invalid proxy URLs', () => { process.env.HTTPS_PROXY = 'invalid-url'; - const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); - - const client = createApiClient({ - apiBaseUrl: 'https://api.example.com', - account: 'test-account', - accessToken: 'test-token', - }); - - expect(client).toBeDefined(); - expect(consoleSpy).toHaveBeenCalledWith( - 'Failed to parse proxy URL:', - 'invalid-url', - expect.any(Error) - ); - - consoleSpy.mockRestore(); + expect(() => { + createApiClient({ + apiBaseUrl: 'https://api.example.com', + account: 'test-account', + accessToken: 'test-token', + }); + }).toThrow(); }); it('should use default ports when not specified', () => { From 633759efd1d7ac4195121d5ceda7f85d2666f2b5 Mon Sep 17 00:00:00 2001 From: Nick Phillips Date: Wed, 10 Sep 2025 11:16:09 -0500 Subject: [PATCH 6/7] Fixes data-model schema reference --- .../src/__tests__/validator.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8d049031..bf7fbaeb 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#', From 087412f7d62ca13d1392cafae47b55438f8be31a Mon Sep 17 00:00:00 2001 From: Nick Phillips Date: Wed, 10 Sep 2025 11:29:08 -0500 Subject: [PATCH 7/7] runs format --- .../src/api/__tests__/index.test.ts | 5 ++-- .../integration-sdk-runtime/src/api/index.ts | 26 +++++++++---------- 2 files changed, 16 insertions(+), 15 deletions(-) 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 15fc60e5..9413d430 100644 --- a/packages/integration-sdk-runtime/src/api/__tests__/index.test.ts +++ b/packages/integration-sdk-runtime/src/api/__tests__/index.test.ts @@ -266,7 +266,8 @@ describe('createApiClient', () => { 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'; + process.env.HTTPS_PROXY = + 'https://username:password@proxy.example.com:8888'; const client = createApiClient({ apiBaseUrl: 'https://api.example.com', @@ -337,4 +338,4 @@ describe('createApiClient', () => { expect(client).toBeDefined(); }); }); -}); \ No newline at end of file +}); diff --git a/packages/integration-sdk-runtime/src/api/index.ts b/packages/integration-sdk-runtime/src/api/index.ts index b70eeb23..08b90cdd 100644 --- a/packages/integration-sdk-runtime/src/api/index.ts +++ b/packages/integration-sdk-runtime/src/api/index.ts @@ -171,21 +171,21 @@ 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(':', ''), - }; + 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), - }; - } + if (url.username && url.password) { + proxy.auth = { + username: decodeURIComponent(url.username), + password: decodeURIComponent(url.password), + }; + } - return proxy; + return proxy; } function getProxyFromEnvironment(): string | undefined {