From 21718ac7442055cc978502d88342f2e6daf46f81 Mon Sep 17 00:00:00 2001 From: Alex Osland Date: Thu, 16 Oct 2025 13:04:27 +0100 Subject: [PATCH 1/8] feat: allow proxy config for http and websocket requests --- src/AnamClient.ts | 8 +++ src/lib/validateProxyConfig.ts | 54 ++++++++++++++ src/modules/CoreApiRestClient.ts | 72 ++++++++++++++----- src/modules/EngineApiRestClient.ts | 49 +++++++++---- src/modules/SignallingClient.ts | 43 ++++++++--- src/modules/StreamingClient.ts | 5 ++ src/types/AnamPublicClientOptions.ts | 5 +- src/types/ProxyConfig.ts | 58 +++++++++++++++ src/types/coreApi/ApiOptions.ts | 7 ++ src/types/coreApi/CoreApiRestClientOptions.ts | 4 -- src/types/coreApi/index.ts | 2 +- src/types/index.ts | 1 + src/types/streaming/StreamingClientOptions.ts | 2 + 13 files changed, 266 insertions(+), 44 deletions(-) create mode 100644 src/lib/validateProxyConfig.ts create mode 100644 src/types/ProxyConfig.ts create mode 100644 src/types/coreApi/ApiOptions.ts delete mode 100644 src/types/coreApi/CoreApiRestClientOptions.ts diff --git a/src/AnamClient.ts b/src/AnamClient.ts index bf87271..fc66e33 100644 --- a/src/AnamClient.ts +++ b/src/AnamClient.ts @@ -9,6 +9,7 @@ import { setMetricsContext, } from './lib/ClientMetrics'; import { generateCorrelationId } from './lib/correlationId'; +import { validateProxyConfig } from './lib/validateProxyConfig'; import { CoreApiRestClient, InternalEventEmitter, @@ -117,6 +118,12 @@ export default class AnamClient { return 'Only one of sessionToken or apiKey should be used'; } + // Validate proxy configuration + const proxyError = validateProxyConfig(options?.api?.proxy); + if (proxyError) { + return proxyError; + } + // Validate persona configuration based on session token if (sessionToken) { const decodedToken = this.decodeJwt(sessionToken); @@ -226,6 +233,7 @@ export default class AnamClient { audioDeviceId: this.clientOptions?.audioDeviceId, disableInputAudio: this.clientOptions?.disableInputAudio, }, + proxy: this.clientOptions?.api?.proxy, metrics: { showPeerConnectionStatsReport: this.clientOptions?.metrics?.showPeerConnectionStatsReport ?? diff --git a/src/lib/validateProxyConfig.ts b/src/lib/validateProxyConfig.ts new file mode 100644 index 0000000..f183f47 --- /dev/null +++ b/src/lib/validateProxyConfig.ts @@ -0,0 +1,54 @@ +import { ProxyConfig } from '../types/ProxyConfig'; + +/** + * Validates proxy configuration + * @param proxyConfig - The proxy configuration to validate + * @returns Error message if invalid, undefined if valid + */ +export function validateProxyConfig( + proxyConfig: ProxyConfig | undefined, +): string | undefined { + if (!proxyConfig || !proxyConfig.enabled) { + return undefined; + } + + // Validate WebSocket proxy URL protocol + if (proxyConfig.websocket) { + try { + const wsUrl = new URL(proxyConfig.websocket); + if (wsUrl.protocol !== 'ws:' && wsUrl.protocol !== 'wss:') { + return `Invalid WebSocket proxy URL: "${proxyConfig.websocket}". WebSocket proxy must use ws:// or wss:// protocol.`; + } + } catch (error) { + return `Invalid WebSocket proxy URL: "${proxyConfig.websocket}". Must be a valid URL.`; + } + } + + // Validate API proxy URL (can be relative or absolute) + if (proxyConfig.api) { + try { + // Try to parse as absolute URL + new URL(proxyConfig.api); + } catch { + // Not an absolute URL, must be a relative path starting with / + if (!proxyConfig.api.startsWith('/')) { + return `Invalid API proxy URL: "${proxyConfig.api}". Must be an absolute URL (https://...) or a path starting with /.`; + } + } + } + + // Validate Engine proxy URL (can be relative or absolute) + if (proxyConfig.engine) { + try { + // Try to parse as absolute URL + new URL(proxyConfig.engine); + } catch { + // Not an absolute URL, must be a relative path starting with / + if (!proxyConfig.engine.startsWith('/')) { + return `Invalid Engine proxy URL: "${proxyConfig.engine}". Must be an absolute URL (https://...) or a path starting with /.`; + } + } + } + + return undefined; +} diff --git a/src/modules/CoreApiRestClient.ts b/src/modules/CoreApiRestClient.ts index bcc9a4c..0b18d0f 100644 --- a/src/modules/CoreApiRestClient.ts +++ b/src/modules/CoreApiRestClient.ts @@ -5,9 +5,10 @@ import { DEFAULT_API_VERSION, } from '../lib/constants'; import { - CoreApiRestClientOptions, + ApiOptions, PersonaConfig, StartSessionResponse, + ProxyConfig, } from '../types'; import { StartSessionOptions } from '../types/coreApi/StartSessionOptions'; import { isCustomPersonaConfig } from '../types/PersonaConfig'; @@ -17,12 +18,9 @@ export class CoreApiRestClient { private apiVersion: string; private apiKey: string | null; private sessionToken: string | null; + private proxyConfig: ProxyConfig | undefined; - constructor( - sessionToken?: string, - apiKey?: string, - options?: CoreApiRestClientOptions, - ) { + constructor(sessionToken?: string, apiKey?: string, options?: ApiOptions) { if (!sessionToken && !apiKey) { throw new Error('Either sessionToken or apiKey must be provided'); } @@ -30,6 +28,7 @@ export class CoreApiRestClient { this.apiKey = apiKey || null; this.baseUrl = options?.baseUrl || DEFAULT_API_BASE_URL; this.apiVersion = options?.apiVersion || DEFAULT_API_VERSION; + this.proxyConfig = options?.proxy || undefined; } public async startSession( @@ -55,12 +54,33 @@ export class CoreApiRestClient { } try { - const response = await fetch(`${this.getApiUrl()}/engine/session`, { + // Determine the URL and headers based on proxy configuration + let url: string; + let headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.sessionToken}`, + }; + + const targetPath = `${this.apiVersion}/engine/session`; + + if (this.proxyConfig?.enabled && this.proxyConfig?.api) { + // Use proxy base URL with same endpoint path + url = `${this.proxyConfig.api}${targetPath}`; + // standard forwarding headers + const targetUrl = new URL(`${this.baseUrl}${targetPath}`); + headers['X-Forwarded-Host'] = targetUrl.host; + headers['X-Forwarded-Proto'] = targetUrl.protocol.slice(0, -1); + headers['X-Original-URI'] = targetPath; + // full original target URL for convenience + headers['X-Anam-Target-Url'] = targetUrl.href; + } else { + // Direct call to Anam API + url = `${this.baseUrl}${targetPath}`; + } + + const response = await fetch(url, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.sessionToken}`, - }, + headers, body: JSON.stringify({ personaConfig, sessionOptions, @@ -179,12 +199,32 @@ export class CoreApiRestClient { body = { ...body, personaConfig }; } try { - const response = await fetch(`${this.getApiUrl()}/auth/session-token`, { + // Determine the URL and headers based on proxy configuration + let url: string; + let headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.apiKey}`, + }; + + const targetPath = `${this.apiVersion}/auth/session-token`; + + if (this.proxyConfig?.enabled && this.proxyConfig?.api) { + // Use proxy base URL with same endpoint path + url = `${this.proxyConfig.api}${targetPath}`; + // Add industry-standard forwarding headers + const targetUrl = new URL(`${this.baseUrl}${targetPath}`); + headers['X-Forwarded-Host'] = targetUrl.host; + headers['X-Forwarded-Proto'] = targetUrl.protocol.slice(0, -1); + headers['X-Original-URI'] = targetPath; + headers['X-Anam-Target-Url'] = targetUrl.href; + } else { + // Direct call to Anam API + url = `${this.baseUrl}${targetPath}`; + } + + const response = await fetch(url, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.apiKey}`, - }, + headers, body: JSON.stringify(body), }); const data = await response.json(); diff --git a/src/modules/EngineApiRestClient.ts b/src/modules/EngineApiRestClient.ts index 7a755b9..e332547 100644 --- a/src/modules/EngineApiRestClient.ts +++ b/src/modules/EngineApiRestClient.ts @@ -1,26 +1,49 @@ +import { ProxyConfig } from '../types/ProxyConfig'; + export class EngineApiRestClient { private baseUrl: string; private sessionId: string; + private proxyConfig: ProxyConfig | undefined; - constructor(baseUrl: string, sessionId: string) { + constructor(baseUrl: string, sessionId: string, proxyConfig?: ProxyConfig) { this.baseUrl = baseUrl; this.sessionId = sessionId; + this.proxyConfig = proxyConfig; } public async sendTalkCommand(content: string): Promise { try { - const response = await fetch( - `${this.baseUrl}/talk?session_id=${this.sessionId}`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - content, - }), - }, - ); + // Determine the URL and headers based on proxy configuration + let url: string; + let headers: Record = { + 'Content-Type': 'application/json', + }; + + const targetPath = `/talk`; + const queryString = `?session_id=${this.sessionId}`; + + if (this.proxyConfig?.enabled && this.proxyConfig?.engine) { + // Use proxy base URL with same endpoint path + url = `${this.proxyConfig.engine}${targetPath}${queryString}`; + // Add standard forwarding headers + const targetUrl = new URL(`${this.baseUrl}${targetPath}${queryString}`); + headers['X-Forwarded-Host'] = targetUrl.host; + headers['X-Forwarded-Proto'] = targetUrl.protocol.slice(0, -1); + headers['X-Original-URI'] = `${targetPath}${queryString}`; + // full original target URL for convenience + headers['X-Anam-Target-Url'] = targetUrl.href; + } else { + // Direct call to Anam engine + url = `${this.baseUrl}${targetPath}${queryString}`; + } + + const response = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify({ + content, + }), + }); if (!response.ok) { throw new Error( `Failed to send talk command: ${response.status} ${response.statusText}`, diff --git a/src/modules/SignallingClient.ts b/src/modules/SignallingClient.ts index a663308..9eaed08 100644 --- a/src/modules/SignallingClient.ts +++ b/src/modules/SignallingClient.ts @@ -6,6 +6,7 @@ import { SignalMessageAction, SignallingClientOptions, ConnectionClosedCode, + ProxyConfig, } from '../types'; import { TalkMessageStreamPayload } from '../types/signalling/TalkMessageStreamPayload'; @@ -24,15 +25,18 @@ export class SignallingClient { private wsConnectionAttempts = 0; private socket: WebSocket | null = null; private heartBeatIntervalRef: ReturnType | null = null; + private proxyConfig: ProxyConfig | undefined; constructor( sessionId: string, options: SignallingClientOptions, publicEventEmitter: PublicEventEmitter, internalEventEmitter: InternalEventEmitter, + proxyConfig?: ProxyConfig, ) { this.publicEventEmitter = publicEventEmitter; this.internalEventEmitter = internalEventEmitter; + this.proxyConfig = proxyConfig; if (!sessionId) { throw new Error('Signalling Client: sessionId is required'); @@ -51,15 +55,38 @@ export class SignallingClient { if (!url.baseUrl) { throw new Error('Signalling Client: baseUrl is required'); } - const httpProtocol = url.protocol || 'https'; - const initUrl = `${httpProtocol}://${url.baseUrl}`; - this.url = new URL(initUrl); - this.url.protocol = url.protocol === 'http' ? 'ws:' : 'wss:'; - if (url.port) { - this.url.port = url.port; + + // Construct WebSocket URL (with or without proxy) + if (this.proxyConfig?.enabled && this.proxyConfig?.websocket) { + // Use proxy WebSocket URL + this.url = new URL(this.proxyConfig.websocket); + this.url.searchParams.append('session_id', sessionId); + + // Add forwarding information as query parameters for proxy routing + const httpProtocol = url.protocol || 'https'; + const targetProtocol = httpProtocol === 'http' ? 'ws' : 'wss'; + const targetHost = url.baseUrl; + const targetPort = url.port; + const wsPath = url.signallingPath ?? '/ws'; + + this.url.searchParams.append('x_forwarded_proto', targetProtocol); + this.url.searchParams.append('x_forwarded_host', targetHost); + if (targetPort) { + this.url.searchParams.append('x_forwarded_port', targetPort); + } + this.url.searchParams.append('x_original_uri', wsPath); + } else { + // Direct connection to Anam (original behavior) + const httpProtocol = url.protocol || 'https'; + const initUrl = `${httpProtocol}://${url.baseUrl}`; + this.url = new URL(initUrl); + this.url.protocol = url.protocol === 'http' ? 'ws:' : 'wss:'; + if (url.port) { + this.url.port = url.port; + } + this.url.pathname = url.signallingPath ?? '/ws'; + this.url.searchParams.append('session_id', sessionId); } - this.url.pathname = url.signallingPath ?? '/ws'; - this.url.searchParams.append('session_id', sessionId); } public stop() { diff --git a/src/modules/StreamingClient.ts b/src/modules/StreamingClient.ts index f153119..393df6f 100644 --- a/src/modules/StreamingClient.ts +++ b/src/modules/StreamingClient.ts @@ -14,6 +14,7 @@ import { StreamingClientOptions, WebRtcTextMessageEvent, ConnectionClosedCode, + ProxyConfig, } from '../types'; import { TalkMessageStream } from '../types/TalkMessageStream'; import { TalkStreamInterruptedSignalMessage } from '../types/signalling/TalkStreamInterruptedSignalMessage'; @@ -33,6 +34,7 @@ export class StreamingClient { private signallingClient: SignallingClient; private engineApiRestClient: EngineApiRestClient; private iceServers: RTCIceServer[]; + private proxyConfig: ProxyConfig | undefined; private peerConnection: RTCPeerConnection | null = null; private connectionReceivedAnswer = false; private remoteIceCandidateBuffer: RTCIceCandidate[] = []; @@ -61,6 +63,7 @@ export class StreamingClient { ) { this.publicEventEmitter = publicEventEmitter; this.internalEventEmitter = internalEventEmitter; + this.proxyConfig = options.proxy; // initialize input audio state const { inputAudio } = options; this.inputAudioState = inputAudio.inputAudioState; @@ -85,11 +88,13 @@ export class StreamingClient { options.signalling, this.publicEventEmitter, this.internalEventEmitter, + this.proxyConfig, ); // initialize engine API client this.engineApiRestClient = new EngineApiRestClient( options.engine.baseUrl, sessionId, + this.proxyConfig, ); this.audioDeviceId = options.inputAudio.audioDeviceId; this.showPeerConnectionStatsReport = diff --git a/src/types/AnamPublicClientOptions.ts b/src/types/AnamPublicClientOptions.ts index 1368ef2..980962c 100644 --- a/src/types/AnamPublicClientOptions.ts +++ b/src/types/AnamPublicClientOptions.ts @@ -1,7 +1,8 @@ -import { CoreApiRestClientOptions } from '../types'; +import { ApiOptions, ProxyConfig } from '../types'; import { VoiceDetectionOptions } from './VoiceDetectionOptions'; + export interface AnamPublicClientOptions { - api?: CoreApiRestClientOptions; + api?: ApiOptions; voiceDetection?: VoiceDetectionOptions; audioDeviceId?: string; disableInputAudio?: boolean; diff --git a/src/types/ProxyConfig.ts b/src/types/ProxyConfig.ts new file mode 100644 index 0000000..b86a824 --- /dev/null +++ b/src/types/ProxyConfig.ts @@ -0,0 +1,58 @@ +/** + * Configuration for proxying SDK requests through a custom proxy server. + * + * When enabled, the SDK will route HTTP REST calls and WebSocket signalling + * through the specified proxy base URLs while maintaining the same endpoint paths. + * WebRTC peer connections remain direct. + * + * @remarks + * - Proxy is opt-in and disabled by default + * - Proxy base URLs replace Anam's base URLs, but paths remain the same + * - Original target info passed via standard forwarding headers (X-Forwarded-Host, X-Forwarded-Proto, X-Original-URI) + * - Also includes X-Anam-Target-Url for convenience + * + * @example + * ```typescript + * const client = createClient(sessionToken, { + * proxy: { + * enabled: true, + * api: 'https://my-proxy.com', // Proxies to api.anam.ai + * engine: 'https://my-proxy.com', // Proxies to engine servers + * websocket: 'wss://my-proxy.com' // Proxies WebSocket connections + * } + * }); + * ``` + */ +export interface ProxyConfig { + /** + * Enable or disable proxy routing + */ + enabled: boolean; + + /** + * Proxy base URL for Core API requests (session creation, auth) + * Replaces https://api.anam.ai base URL + * Endpoints like /v1/engine/session remain the same + * + * @example 'https://my-proxy.com' or '/api/proxy' for same-origin + */ + api?: string; + + /** + * Proxy base URL for Engine API requests (talk commands, etc.) + * Replaces the engine server base URL + * Endpoints like /talk remain the same + * + * @example 'https://my-proxy.com' or '/api/proxy' for same-origin + */ + engine?: string; + + /** + * Proxy base URL for WebSocket signalling connections + * Replaces the engine WebSocket base URL + * Path like /ws remains the same + * + * @example 'wss://my-proxy.com' or 'ws://localhost:3000/api/proxy' + */ + websocket?: string; +} diff --git a/src/types/coreApi/ApiOptions.ts b/src/types/coreApi/ApiOptions.ts new file mode 100644 index 0000000..c723d70 --- /dev/null +++ b/src/types/coreApi/ApiOptions.ts @@ -0,0 +1,7 @@ +import { ProxyConfig } from '../ProxyConfig'; + +export interface ApiOptions { + baseUrl?: string; + apiVersion?: string; + proxy?: ProxyConfig; +} diff --git a/src/types/coreApi/CoreApiRestClientOptions.ts b/src/types/coreApi/CoreApiRestClientOptions.ts deleted file mode 100644 index 1436955..0000000 --- a/src/types/coreApi/CoreApiRestClientOptions.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface CoreApiRestClientOptions { - baseUrl?: string; - apiVersion?: string; -} diff --git a/src/types/coreApi/index.ts b/src/types/coreApi/index.ts index 5e374db..6bf61c4 100644 --- a/src/types/coreApi/index.ts +++ b/src/types/coreApi/index.ts @@ -1,4 +1,4 @@ -export type { CoreApiRestClientOptions } from './CoreApiRestClientOptions'; +export type { ApiOptions } from './ApiOptions'; export type { StartSessionResponse, ClientConfigResponse, diff --git a/src/types/index.ts b/src/types/index.ts index f511bc7..8d5dea2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,6 +4,7 @@ export { SignalMessageAction } from './signalling'; // need to export this expli export type * from './streaming'; export type * from './coreApi'; export type { PersonaConfig } from './PersonaConfig'; +export type { ProxyConfig } from './ProxyConfig'; export type { InputAudioState } from './InputAudioState'; export { AudioPermissionState } from './InputAudioState'; export type * from './messageHistory'; diff --git a/src/types/streaming/StreamingClientOptions.ts b/src/types/streaming/StreamingClientOptions.ts index e2a564d..0f194be 100644 --- a/src/types/streaming/StreamingClientOptions.ts +++ b/src/types/streaming/StreamingClientOptions.ts @@ -1,12 +1,14 @@ import { SignallingClientOptions } from '../../types'; import { EngineApiRestClientOptions } from '../engineApi/EngineApiRestClientOptions'; import { InputAudioOptions } from './InputAudioOptions'; +import { ProxyConfig } from '../ProxyConfig'; export interface StreamingClientOptions { engine: EngineApiRestClientOptions; signalling: SignallingClientOptions; iceServers: RTCIceServer[]; inputAudio: InputAudioOptions; + proxy?: ProxyConfig; metrics?: { showPeerConnectionStatsReport?: boolean; peerConnectionStatsReportOutputFormat?: 'console' | 'json'; From 7cff06c970c0191450959eaa4369d08dabd2a6c8 Mon Sep 17 00:00:00 2001 From: Alex Osland Date: Thu, 16 Oct 2025 15:49:14 +0100 Subject: [PATCH 2/8] chore: update type comments --- src/types/ProxyConfig.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/types/ProxyConfig.ts b/src/types/ProxyConfig.ts index b86a824..ff970a3 100644 --- a/src/types/ProxyConfig.ts +++ b/src/types/ProxyConfig.ts @@ -30,9 +30,8 @@ export interface ProxyConfig { enabled: boolean; /** - * Proxy base URL for Core API requests (session creation, auth) + * Proxy base URL for Anam API requests (session creation, auth) * Replaces https://api.anam.ai base URL - * Endpoints like /v1/engine/session remain the same * * @example 'https://my-proxy.com' or '/api/proxy' for same-origin */ @@ -40,8 +39,9 @@ export interface ProxyConfig { /** * Proxy base URL for Engine API requests (talk commands, etc.) - * Replaces the engine server base URL - * Endpoints like /talk remain the same + * Replaces the engine server base URL. + * The original engine base url is dynamic for each session. + * The forwarding headers can be used to construct the full original target URL. * * @example 'https://my-proxy.com' or '/api/proxy' for same-origin */ @@ -49,8 +49,9 @@ export interface ProxyConfig { /** * Proxy base URL for WebSocket signalling connections - * Replaces the engine WebSocket base URL - * Path like /ws remains the same + * Replaces the engine WebSocket base URL. + * The original WebSocket base url is dynamic for each session. + * The websocket query parameters can be used to construct the full original target URL. * * @example 'wss://my-proxy.com' or 'ws://localhost:3000/api/proxy' */ From 9f710acb613c1c6822d0b31408ede97408198bf7 Mon Sep 17 00:00:00 2001 From: Alex Osland Date: Thu, 16 Oct 2025 16:15:53 +0100 Subject: [PATCH 3/8] fix: export deprecated type name for backwards compatibility --- src/types/coreApi/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/types/coreApi/index.ts b/src/types/coreApi/index.ts index 6bf61c4..62c07ab 100644 --- a/src/types/coreApi/index.ts +++ b/src/types/coreApi/index.ts @@ -1,4 +1,6 @@ export type { ApiOptions } from './ApiOptions'; +// for backwards compatibility with deprecated name +export type { ApiOptions as CoreApiOptions } from './ApiOptions'; export type { StartSessionResponse, ClientConfigResponse, From d589e25f28ddbe686d3d0924391b20ae73c5ef92 Mon Sep 17 00:00:00 2001 From: Alex Osland Date: Thu, 16 Oct 2025 16:16:43 +0100 Subject: [PATCH 4/8] chore: remove unused import --- src/types/AnamPublicClientOptions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/AnamPublicClientOptions.ts b/src/types/AnamPublicClientOptions.ts index 980962c..05a21d3 100644 --- a/src/types/AnamPublicClientOptions.ts +++ b/src/types/AnamPublicClientOptions.ts @@ -1,4 +1,4 @@ -import { ApiOptions, ProxyConfig } from '../types'; +import { ApiOptions } from '../types'; import { VoiceDetectionOptions } from './VoiceDetectionOptions'; export interface AnamPublicClientOptions { From e477ed59581b02883f39df7a6b6325a0aa0343cd Mon Sep 17 00:00:00 2001 From: Alex Osland Date: Thu, 16 Oct 2025 16:21:20 +0100 Subject: [PATCH 5/8] refactor: helper function for setting api proxy headers --- src/modules/CoreApiRestClient.ts | 76 +++++++++++++++----------------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/src/modules/CoreApiRestClient.ts b/src/modules/CoreApiRestClient.ts index 0b18d0f..f20f09f 100644 --- a/src/modules/CoreApiRestClient.ts +++ b/src/modules/CoreApiRestClient.ts @@ -31,6 +31,35 @@ export class CoreApiRestClient { this.proxyConfig = options?.proxy || undefined; } + /** + * Builds URL and headers for a request, applying proxy configuration if enabled + */ + private buildProxiedUrlAndHeaders( + targetPath: string, + baseHeaders: Record, + ): { url: string; headers: Record } { + if (this.proxyConfig?.enabled && this.proxyConfig?.api) { + // Use proxy base URL with same endpoint path + const url = `${this.proxyConfig.api}${targetPath}`; + // Add standard forwarding headers + const targetUrl = new URL(`${this.baseUrl}${targetPath}`); + const headers = { + ...baseHeaders, + 'X-Forwarded-Host': targetUrl.host, + 'X-Forwarded-Proto': targetUrl.protocol.slice(0, -1), + 'X-Original-URI': targetPath, + 'X-Anam-Target-Url': targetUrl.href, + }; + return { url, headers }; + } else { + // Direct call to Anam API + return { + url: `${this.baseUrl}${targetPath}`, + headers: baseHeaders, + }; + } + } + public async startSession( personaConfig?: PersonaConfig, sessionOptions?: StartSessionOptions, @@ -54,29 +83,11 @@ export class CoreApiRestClient { } try { - // Determine the URL and headers based on proxy configuration - let url: string; - let headers: Record = { + const targetPath = `${this.apiVersion}/engine/session`; + const { url, headers } = this.buildProxiedUrlAndHeaders(targetPath, { 'Content-Type': 'application/json', Authorization: `Bearer ${this.sessionToken}`, - }; - - const targetPath = `${this.apiVersion}/engine/session`; - - if (this.proxyConfig?.enabled && this.proxyConfig?.api) { - // Use proxy base URL with same endpoint path - url = `${this.proxyConfig.api}${targetPath}`; - // standard forwarding headers - const targetUrl = new URL(`${this.baseUrl}${targetPath}`); - headers['X-Forwarded-Host'] = targetUrl.host; - headers['X-Forwarded-Proto'] = targetUrl.protocol.slice(0, -1); - headers['X-Original-URI'] = targetPath; - // full original target URL for convenience - headers['X-Anam-Target-Url'] = targetUrl.href; - } else { - // Direct call to Anam API - url = `${this.baseUrl}${targetPath}`; - } + }); const response = await fetch(url, { method: 'POST', @@ -199,28 +210,11 @@ export class CoreApiRestClient { body = { ...body, personaConfig }; } try { - // Determine the URL and headers based on proxy configuration - let url: string; - let headers: Record = { + const targetPath = `${this.apiVersion}/auth/session-token`; + const { url, headers } = this.buildProxiedUrlAndHeaders(targetPath, { 'Content-Type': 'application/json', Authorization: `Bearer ${this.apiKey}`, - }; - - const targetPath = `${this.apiVersion}/auth/session-token`; - - if (this.proxyConfig?.enabled && this.proxyConfig?.api) { - // Use proxy base URL with same endpoint path - url = `${this.proxyConfig.api}${targetPath}`; - // Add industry-standard forwarding headers - const targetUrl = new URL(`${this.baseUrl}${targetPath}`); - headers['X-Forwarded-Host'] = targetUrl.host; - headers['X-Forwarded-Proto'] = targetUrl.protocol.slice(0, -1); - headers['X-Original-URI'] = targetPath; - headers['X-Anam-Target-Url'] = targetUrl.href; - } else { - // Direct call to Anam API - url = `${this.baseUrl}${targetPath}`; - } + }); const response = await fetch(url, { method: 'POST', From ba04537e31621dfa0b626f809a71d3c8b3da4718 Mon Sep 17 00:00:00 2001 From: Alex Osland Date: Tue, 21 Oct 2025 17:51:10 +0100 Subject: [PATCH 6/8] fix: extract base url from engine url for websocket proxy --- src/modules/SignallingClient.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/modules/SignallingClient.ts b/src/modules/SignallingClient.ts index 9eaed08..5dddbdb 100644 --- a/src/modules/SignallingClient.ts +++ b/src/modules/SignallingClient.ts @@ -65,13 +65,17 @@ export class SignallingClient { // Add forwarding information as query parameters for proxy routing const httpProtocol = url.protocol || 'https'; const targetProtocol = httpProtocol === 'http' ? 'ws' : 'wss'; - const targetHost = url.baseUrl; - const targetPort = url.port; - const wsPath = url.signallingPath ?? '/ws'; + + // Parse baseUrl to extract just the hostname (it may contain path segments) + const tempUrl = new URL(`${httpProtocol}://${url.baseUrl}`); + const targetHost = tempUrl.hostname; + const targetPort = url.port || tempUrl.port; + const basePath = tempUrl.pathname !== '/' ? tempUrl.pathname : ''; + const wsPath = basePath + (url.signallingPath ?? '/ws'); this.url.searchParams.append('x_forwarded_proto', targetProtocol); this.url.searchParams.append('x_forwarded_host', targetHost); - if (targetPort) { + if (targetPort && targetPort !== '80' && targetPort !== '443') { this.url.searchParams.append('x_forwarded_port', targetPort); } this.url.searchParams.append('x_original_uri', wsPath); From 0a314215663b21b253f2e47cef28a30cbe792c9f Mon Sep 17 00:00:00 2001 From: Alex Osland Date: Mon, 27 Oct 2025 16:29:48 +0000 Subject: [PATCH 7/8] refactor: proxy config renamed to api gateway config --- package.json | 2 +- src/AnamClient.ts | 10 ++-- src/lib/validateApiGatewayConfig.ts | 37 ++++++++++++ src/lib/validateProxyConfig.ts | 54 ----------------- src/modules/CoreApiRestClient.ts | 25 ++++---- src/modules/EngineApiRestClient.ts | 26 ++++---- src/modules/SignallingClient.ts | 49 ++++++++------- src/modules/StreamingClient.ts | 10 ++-- src/types/ApiGatewayConfig.ts | 46 +++++++++++++++ src/types/ProxyConfig.ts | 59 ------------------- src/types/coreApi/ApiOptions.ts | 4 +- src/types/index.ts | 2 +- src/types/streaming/StreamingClientOptions.ts | 4 +- 13 files changed, 150 insertions(+), 178 deletions(-) create mode 100644 src/lib/validateApiGatewayConfig.ts delete mode 100644 src/lib/validateProxyConfig.ts create mode 100644 src/types/ApiGatewayConfig.ts delete mode 100644 src/types/ProxyConfig.ts diff --git a/package.json b/package.json index 0966592..da4f6a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@anam-ai/js-sdk", - "version": "0.0.0-automated", + "version": "4.2.0-oz-alpha.6", "description": "Client side JavaScript SDK for Anam AI", "author": "Anam AI", "main": "dist/main/index.js", diff --git a/src/AnamClient.ts b/src/AnamClient.ts index fc66e33..1b8cf3b 100644 --- a/src/AnamClient.ts +++ b/src/AnamClient.ts @@ -9,7 +9,7 @@ import { setMetricsContext, } from './lib/ClientMetrics'; import { generateCorrelationId } from './lib/correlationId'; -import { validateProxyConfig } from './lib/validateProxyConfig'; +import { validateApiGatewayConfig } from './lib/validateApiGatewayConfig'; import { CoreApiRestClient, InternalEventEmitter, @@ -119,9 +119,9 @@ export default class AnamClient { } // Validate proxy configuration - const proxyError = validateProxyConfig(options?.api?.proxy); - if (proxyError) { - return proxyError; + const apiGatewayError = validateApiGatewayConfig(options?.api?.apiGateway); + if (apiGatewayError) { + return apiGatewayError; } // Validate persona configuration based on session token @@ -233,7 +233,7 @@ export default class AnamClient { audioDeviceId: this.clientOptions?.audioDeviceId, disableInputAudio: this.clientOptions?.disableInputAudio, }, - proxy: this.clientOptions?.api?.proxy, + apiGateway: this.clientOptions?.api?.apiGateway, metrics: { showPeerConnectionStatsReport: this.clientOptions?.metrics?.showPeerConnectionStatsReport ?? diff --git a/src/lib/validateApiGatewayConfig.ts b/src/lib/validateApiGatewayConfig.ts new file mode 100644 index 0000000..878d948 --- /dev/null +++ b/src/lib/validateApiGatewayConfig.ts @@ -0,0 +1,37 @@ +import { ApiGatewayConfig } from '../types/ApiGatewayConfig'; + +/** + * Validates API Gateway configuration + * @param apiGatewayConfig - The API Gateway configuration to validate + * @returns Error message if invalid, undefined if valid + */ +export function validateApiGatewayConfig( + apiGatewayConfig: ApiGatewayConfig | undefined, +): string | undefined { + if (!apiGatewayConfig || !apiGatewayConfig.enabled) { + return undefined; + } + + if (!apiGatewayConfig.baseUrl) { + return 'API Gateway baseUrl is required when enabled'; + } + + // Validate baseUrl format + try { + const url = new URL(apiGatewayConfig.baseUrl); + if (!['http:', 'https:', 'ws:', 'wss:'].includes(url.protocol)) { + return `Invalid API Gateway baseUrl protocol: ${url.protocol}. Must be http:, https:, ws:, or wss:`; + } + } catch (error) { + return `Invalid API Gateway baseUrl: ${apiGatewayConfig.baseUrl}`; + } + + // Validate wsPath if provided + if (apiGatewayConfig.wsPath) { + if (!apiGatewayConfig.wsPath.startsWith('/')) { + return 'API Gateway wsPath must start with /'; + } + } + + return undefined; +} diff --git a/src/lib/validateProxyConfig.ts b/src/lib/validateProxyConfig.ts deleted file mode 100644 index f183f47..0000000 --- a/src/lib/validateProxyConfig.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { ProxyConfig } from '../types/ProxyConfig'; - -/** - * Validates proxy configuration - * @param proxyConfig - The proxy configuration to validate - * @returns Error message if invalid, undefined if valid - */ -export function validateProxyConfig( - proxyConfig: ProxyConfig | undefined, -): string | undefined { - if (!proxyConfig || !proxyConfig.enabled) { - return undefined; - } - - // Validate WebSocket proxy URL protocol - if (proxyConfig.websocket) { - try { - const wsUrl = new URL(proxyConfig.websocket); - if (wsUrl.protocol !== 'ws:' && wsUrl.protocol !== 'wss:') { - return `Invalid WebSocket proxy URL: "${proxyConfig.websocket}". WebSocket proxy must use ws:// or wss:// protocol.`; - } - } catch (error) { - return `Invalid WebSocket proxy URL: "${proxyConfig.websocket}". Must be a valid URL.`; - } - } - - // Validate API proxy URL (can be relative or absolute) - if (proxyConfig.api) { - try { - // Try to parse as absolute URL - new URL(proxyConfig.api); - } catch { - // Not an absolute URL, must be a relative path starting with / - if (!proxyConfig.api.startsWith('/')) { - return `Invalid API proxy URL: "${proxyConfig.api}". Must be an absolute URL (https://...) or a path starting with /.`; - } - } - } - - // Validate Engine proxy URL (can be relative or absolute) - if (proxyConfig.engine) { - try { - // Try to parse as absolute URL - new URL(proxyConfig.engine); - } catch { - // Not an absolute URL, must be a relative path starting with / - if (!proxyConfig.engine.startsWith('/')) { - return `Invalid Engine proxy URL: "${proxyConfig.engine}". Must be an absolute URL (https://...) or a path starting with /.`; - } - } - } - - return undefined; -} diff --git a/src/modules/CoreApiRestClient.ts b/src/modules/CoreApiRestClient.ts index f20f09f..fa8e303 100644 --- a/src/modules/CoreApiRestClient.ts +++ b/src/modules/CoreApiRestClient.ts @@ -8,7 +8,7 @@ import { ApiOptions, PersonaConfig, StartSessionResponse, - ProxyConfig, + ApiGatewayConfig, } from '../types'; import { StartSessionOptions } from '../types/coreApi/StartSessionOptions'; import { isCustomPersonaConfig } from '../types/PersonaConfig'; @@ -18,7 +18,7 @@ export class CoreApiRestClient { private apiVersion: string; private apiKey: string | null; private sessionToken: string | null; - private proxyConfig: ProxyConfig | undefined; + private apiGatewayConfig: ApiGatewayConfig | undefined; constructor(sessionToken?: string, apiKey?: string, options?: ApiOptions) { if (!sessionToken && !apiKey) { @@ -28,26 +28,23 @@ export class CoreApiRestClient { this.apiKey = apiKey || null; this.baseUrl = options?.baseUrl || DEFAULT_API_BASE_URL; this.apiVersion = options?.apiVersion || DEFAULT_API_VERSION; - this.proxyConfig = options?.proxy || undefined; + this.apiGatewayConfig = options?.apiGateway || undefined; } /** - * Builds URL and headers for a request, applying proxy configuration if enabled + * Builds URL and headers for a request, applying API Gateway configuration if enabled */ - private buildProxiedUrlAndHeaders( + private buildGatewayUrlAndHeaders( targetPath: string, baseHeaders: Record, ): { url: string; headers: Record } { - if (this.proxyConfig?.enabled && this.proxyConfig?.api) { - // Use proxy base URL with same endpoint path - const url = `${this.proxyConfig.api}${targetPath}`; - // Add standard forwarding headers + if (this.apiGatewayConfig?.enabled && this.apiGatewayConfig?.baseUrl) { + // Use gateway base URL with same endpoint path + const url = `${this.apiGatewayConfig.baseUrl}${targetPath}`; + // Add complete target URL header for gateway routing const targetUrl = new URL(`${this.baseUrl}${targetPath}`); const headers = { ...baseHeaders, - 'X-Forwarded-Host': targetUrl.host, - 'X-Forwarded-Proto': targetUrl.protocol.slice(0, -1), - 'X-Original-URI': targetPath, 'X-Anam-Target-Url': targetUrl.href, }; return { url, headers }; @@ -84,7 +81,7 @@ export class CoreApiRestClient { try { const targetPath = `${this.apiVersion}/engine/session`; - const { url, headers } = this.buildProxiedUrlAndHeaders(targetPath, { + const { url, headers } = this.buildGatewayUrlAndHeaders(targetPath, { 'Content-Type': 'application/json', Authorization: `Bearer ${this.sessionToken}`, }); @@ -211,7 +208,7 @@ export class CoreApiRestClient { } try { const targetPath = `${this.apiVersion}/auth/session-token`; - const { url, headers } = this.buildProxiedUrlAndHeaders(targetPath, { + const { url, headers } = this.buildGatewayUrlAndHeaders(targetPath, { 'Content-Type': 'application/json', Authorization: `Bearer ${this.apiKey}`, }); diff --git a/src/modules/EngineApiRestClient.ts b/src/modules/EngineApiRestClient.ts index e332547..4f124bd 100644 --- a/src/modules/EngineApiRestClient.ts +++ b/src/modules/EngineApiRestClient.ts @@ -1,19 +1,23 @@ -import { ProxyConfig } from '../types/ProxyConfig'; +import { ApiGatewayConfig } from '../types/ApiGatewayConfig'; export class EngineApiRestClient { private baseUrl: string; private sessionId: string; - private proxyConfig: ProxyConfig | undefined; + private apiGatewayConfig: ApiGatewayConfig | undefined; - constructor(baseUrl: string, sessionId: string, proxyConfig?: ProxyConfig) { + constructor( + baseUrl: string, + sessionId: string, + apiGatewayConfig?: ApiGatewayConfig, + ) { this.baseUrl = baseUrl; this.sessionId = sessionId; - this.proxyConfig = proxyConfig; + this.apiGatewayConfig = apiGatewayConfig; } public async sendTalkCommand(content: string): Promise { try { - // Determine the URL and headers based on proxy configuration + // Determine the URL and headers based on API Gateway configuration let url: string; let headers: Record = { 'Content-Type': 'application/json', @@ -22,15 +26,11 @@ export class EngineApiRestClient { const targetPath = `/talk`; const queryString = `?session_id=${this.sessionId}`; - if (this.proxyConfig?.enabled && this.proxyConfig?.engine) { - // Use proxy base URL with same endpoint path - url = `${this.proxyConfig.engine}${targetPath}${queryString}`; - // Add standard forwarding headers + if (this.apiGatewayConfig?.enabled && this.apiGatewayConfig?.baseUrl) { + // Use gateway base URL with same endpoint path + url = `${this.apiGatewayConfig.baseUrl}${targetPath}${queryString}`; + // Add complete target URL header for gateway routing const targetUrl = new URL(`${this.baseUrl}${targetPath}${queryString}`); - headers['X-Forwarded-Host'] = targetUrl.host; - headers['X-Forwarded-Proto'] = targetUrl.protocol.slice(0, -1); - headers['X-Original-URI'] = `${targetPath}${queryString}`; - // full original target URL for convenience headers['X-Anam-Target-Url'] = targetUrl.href; } else { // Direct call to Anam engine diff --git a/src/modules/SignallingClient.ts b/src/modules/SignallingClient.ts index 5dddbdb..b0b5b76 100644 --- a/src/modules/SignallingClient.ts +++ b/src/modules/SignallingClient.ts @@ -6,7 +6,7 @@ import { SignalMessageAction, SignallingClientOptions, ConnectionClosedCode, - ProxyConfig, + ApiGatewayConfig, } from '../types'; import { TalkMessageStreamPayload } from '../types/signalling/TalkMessageStreamPayload'; @@ -25,18 +25,18 @@ export class SignallingClient { private wsConnectionAttempts = 0; private socket: WebSocket | null = null; private heartBeatIntervalRef: ReturnType | null = null; - private proxyConfig: ProxyConfig | undefined; + private apiGatewayConfig: ApiGatewayConfig | undefined; constructor( sessionId: string, options: SignallingClientOptions, publicEventEmitter: PublicEventEmitter, internalEventEmitter: InternalEventEmitter, - proxyConfig?: ProxyConfig, + apiGatewayConfig?: ApiGatewayConfig, ) { this.publicEventEmitter = publicEventEmitter; this.internalEventEmitter = internalEventEmitter; - this.proxyConfig = proxyConfig; + this.apiGatewayConfig = apiGatewayConfig; if (!sessionId) { throw new Error('Signalling Client: sessionId is required'); @@ -56,29 +56,34 @@ export class SignallingClient { throw new Error('Signalling Client: baseUrl is required'); } - // Construct WebSocket URL (with or without proxy) - if (this.proxyConfig?.enabled && this.proxyConfig?.websocket) { - // Use proxy WebSocket URL - this.url = new URL(this.proxyConfig.websocket); - this.url.searchParams.append('session_id', sessionId); + // Construct WebSocket URL (with or without API Gateway) + if (this.apiGatewayConfig?.enabled && this.apiGatewayConfig?.baseUrl) { + // Use API Gateway WebSocket URL + const gatewayUrl = new URL(this.apiGatewayConfig.baseUrl); + const wsPath = this.apiGatewayConfig.wsPath ?? '/ws'; + + // Construct gateway WebSocket URL + gatewayUrl.protocol = gatewayUrl.protocol.replace('http', 'ws'); + gatewayUrl.pathname = wsPath; + this.url = gatewayUrl; - // Add forwarding information as query parameters for proxy routing + // Construct the complete target WebSocket URL and pass it as a query parameter const httpProtocol = url.protocol || 'https'; const targetProtocol = httpProtocol === 'http' ? 'ws' : 'wss'; + const httpUrl = `${httpProtocol}://${url.baseUrl}`; + const targetWsPath = url.signallingPath ?? '/ws'; - // Parse baseUrl to extract just the hostname (it may contain path segments) - const tempUrl = new URL(`${httpProtocol}://${url.baseUrl}`); - const targetHost = tempUrl.hostname; - const targetPort = url.port || tempUrl.port; - const basePath = tempUrl.pathname !== '/' ? tempUrl.pathname : ''; - const wsPath = basePath + (url.signallingPath ?? '/ws'); - - this.url.searchParams.append('x_forwarded_proto', targetProtocol); - this.url.searchParams.append('x_forwarded_host', targetHost); - if (targetPort && targetPort !== '80' && targetPort !== '443') { - this.url.searchParams.append('x_forwarded_port', targetPort); + // Build complete target URL + const targetUrl = new URL(httpUrl); + targetUrl.protocol = targetProtocol === 'ws' ? 'ws:' : 'wss:'; + if (url.port) { + targetUrl.port = url.port; } - this.url.searchParams.append('x_original_uri', wsPath); + targetUrl.pathname = targetWsPath; + targetUrl.searchParams.append('session_id', sessionId); + + // Pass complete target URL as query parameter + this.url.searchParams.append('target_url', targetUrl.href); } else { // Direct connection to Anam (original behavior) const httpProtocol = url.protocol || 'https'; diff --git a/src/modules/StreamingClient.ts b/src/modules/StreamingClient.ts index 393df6f..2dbf064 100644 --- a/src/modules/StreamingClient.ts +++ b/src/modules/StreamingClient.ts @@ -14,7 +14,7 @@ import { StreamingClientOptions, WebRtcTextMessageEvent, ConnectionClosedCode, - ProxyConfig, + ApiGatewayConfig, } from '../types'; import { TalkMessageStream } from '../types/TalkMessageStream'; import { TalkStreamInterruptedSignalMessage } from '../types/signalling/TalkStreamInterruptedSignalMessage'; @@ -34,7 +34,7 @@ export class StreamingClient { private signallingClient: SignallingClient; private engineApiRestClient: EngineApiRestClient; private iceServers: RTCIceServer[]; - private proxyConfig: ProxyConfig | undefined; + private apiGatewayConfig: ApiGatewayConfig | undefined; private peerConnection: RTCPeerConnection | null = null; private connectionReceivedAnswer = false; private remoteIceCandidateBuffer: RTCIceCandidate[] = []; @@ -63,7 +63,7 @@ export class StreamingClient { ) { this.publicEventEmitter = publicEventEmitter; this.internalEventEmitter = internalEventEmitter; - this.proxyConfig = options.proxy; + this.apiGatewayConfig = options.apiGateway; // initialize input audio state const { inputAudio } = options; this.inputAudioState = inputAudio.inputAudioState; @@ -88,13 +88,13 @@ export class StreamingClient { options.signalling, this.publicEventEmitter, this.internalEventEmitter, - this.proxyConfig, + this.apiGatewayConfig, ); // initialize engine API client this.engineApiRestClient = new EngineApiRestClient( options.engine.baseUrl, sessionId, - this.proxyConfig, + this.apiGatewayConfig, ); this.audioDeviceId = options.inputAudio.audioDeviceId; this.showPeerConnectionStatsReport = diff --git a/src/types/ApiGatewayConfig.ts b/src/types/ApiGatewayConfig.ts new file mode 100644 index 0000000..c578c73 --- /dev/null +++ b/src/types/ApiGatewayConfig.ts @@ -0,0 +1,46 @@ +/** + * Configuration for routing SDK requests through an API Gateway. + * + * When enabled, the SDK will route HTTP REST calls and WebSocket signalling + * through the specified API Gateway while maintaining direct WebRTC peer connections. + * + * @remarks + * - API Gateway is opt-in and disabled by default + * - The SDK passes complete target URLs to the gateway via headers/query params + * - The gateway handles routing and forwarding to Anam's infrastructure + * - WebRTC peer connections remain direct (not routed through gateway) + * + * @example + * ```typescript + * const client = createClient(sessionToken, { + * apiGateway: { + * enabled: true, + * baseUrl: 'https://my-gateway.com', // Base URL for all gateway requests + * wsPath: '/ws' // WebSocket endpoint path (default: '/ws') + * } + * }); + * ``` + */ +export interface ApiGatewayConfig { + /** + * Enable or disable API Gateway routing + */ + enabled: boolean; + + /** + * Base URL of the API Gateway server + * Used for both HTTP and WebSocket connections + * + * @example 'https://my-gateway.com' or 'http://localhost:3001' + */ + baseUrl: string; + + /** + * WebSocket endpoint path on the gateway + * Defaults to '/ws' if not specified + * + * @example '/ws' or '/api/websocket' + * @default '/ws' + */ + wsPath?: string; +} diff --git a/src/types/ProxyConfig.ts b/src/types/ProxyConfig.ts deleted file mode 100644 index ff970a3..0000000 --- a/src/types/ProxyConfig.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Configuration for proxying SDK requests through a custom proxy server. - * - * When enabled, the SDK will route HTTP REST calls and WebSocket signalling - * through the specified proxy base URLs while maintaining the same endpoint paths. - * WebRTC peer connections remain direct. - * - * @remarks - * - Proxy is opt-in and disabled by default - * - Proxy base URLs replace Anam's base URLs, but paths remain the same - * - Original target info passed via standard forwarding headers (X-Forwarded-Host, X-Forwarded-Proto, X-Original-URI) - * - Also includes X-Anam-Target-Url for convenience - * - * @example - * ```typescript - * const client = createClient(sessionToken, { - * proxy: { - * enabled: true, - * api: 'https://my-proxy.com', // Proxies to api.anam.ai - * engine: 'https://my-proxy.com', // Proxies to engine servers - * websocket: 'wss://my-proxy.com' // Proxies WebSocket connections - * } - * }); - * ``` - */ -export interface ProxyConfig { - /** - * Enable or disable proxy routing - */ - enabled: boolean; - - /** - * Proxy base URL for Anam API requests (session creation, auth) - * Replaces https://api.anam.ai base URL - * - * @example 'https://my-proxy.com' or '/api/proxy' for same-origin - */ - api?: string; - - /** - * Proxy base URL for Engine API requests (talk commands, etc.) - * Replaces the engine server base URL. - * The original engine base url is dynamic for each session. - * The forwarding headers can be used to construct the full original target URL. - * - * @example 'https://my-proxy.com' or '/api/proxy' for same-origin - */ - engine?: string; - - /** - * Proxy base URL for WebSocket signalling connections - * Replaces the engine WebSocket base URL. - * The original WebSocket base url is dynamic for each session. - * The websocket query parameters can be used to construct the full original target URL. - * - * @example 'wss://my-proxy.com' or 'ws://localhost:3000/api/proxy' - */ - websocket?: string; -} diff --git a/src/types/coreApi/ApiOptions.ts b/src/types/coreApi/ApiOptions.ts index c723d70..4177944 100644 --- a/src/types/coreApi/ApiOptions.ts +++ b/src/types/coreApi/ApiOptions.ts @@ -1,7 +1,7 @@ -import { ProxyConfig } from '../ProxyConfig'; +import { ApiGatewayConfig } from '../ApiGatewayConfig'; export interface ApiOptions { baseUrl?: string; apiVersion?: string; - proxy?: ProxyConfig; + apiGateway?: ApiGatewayConfig; } diff --git a/src/types/index.ts b/src/types/index.ts index 8d5dea2..e263d5d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,7 +4,7 @@ export { SignalMessageAction } from './signalling'; // need to export this expli export type * from './streaming'; export type * from './coreApi'; export type { PersonaConfig } from './PersonaConfig'; -export type { ProxyConfig } from './ProxyConfig'; +export type { ApiGatewayConfig } from './ApiGatewayConfig'; export type { InputAudioState } from './InputAudioState'; export { AudioPermissionState } from './InputAudioState'; export type * from './messageHistory'; diff --git a/src/types/streaming/StreamingClientOptions.ts b/src/types/streaming/StreamingClientOptions.ts index 0f194be..0539413 100644 --- a/src/types/streaming/StreamingClientOptions.ts +++ b/src/types/streaming/StreamingClientOptions.ts @@ -1,14 +1,14 @@ import { SignallingClientOptions } from '../../types'; import { EngineApiRestClientOptions } from '../engineApi/EngineApiRestClientOptions'; import { InputAudioOptions } from './InputAudioOptions'; -import { ProxyConfig } from '../ProxyConfig'; +import { ApiGatewayConfig } from '../ApiGatewayConfig'; export interface StreamingClientOptions { engine: EngineApiRestClientOptions; signalling: SignallingClientOptions; iceServers: RTCIceServer[]; inputAudio: InputAudioOptions; - proxy?: ProxyConfig; + apiGateway?: ApiGatewayConfig; metrics?: { showPeerConnectionStatsReport?: boolean; peerConnectionStatsReportOutputFormat?: 'console' | 'json'; From 032a9dd15cbac87151dc5a041106389a122621d1 Mon Sep 17 00:00:00 2001 From: Alex Osland Date: Mon, 27 Oct 2025 16:36:14 +0000 Subject: [PATCH 8/8] revert package version --- package.json | 2 +- src/AnamClient.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index da4f6a8..0966592 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@anam-ai/js-sdk", - "version": "4.2.0-oz-alpha.6", + "version": "0.0.0-automated", "description": "Client side JavaScript SDK for Anam AI", "author": "Anam AI", "main": "dist/main/index.js", diff --git a/src/AnamClient.ts b/src/AnamClient.ts index 1b8cf3b..77d6e76 100644 --- a/src/AnamClient.ts +++ b/src/AnamClient.ts @@ -118,7 +118,7 @@ export default class AnamClient { return 'Only one of sessionToken or apiKey should be used'; } - // Validate proxy configuration + // Validate gateway configuration const apiGatewayError = validateApiGatewayConfig(options?.api?.apiGateway); if (apiGatewayError) { return apiGatewayError;