diff --git a/demo/hooks/useAgentManager.ts b/demo/hooks/useAgentManager.ts index 72f3e0b9..b35bb3ec 100644 --- a/demo/hooks/useAgentManager.ts +++ b/demo/hooks/useAgentManager.ts @@ -24,7 +24,7 @@ interface UseAgentManagerOptions { fluent?: boolean; }; enableAnalytics?: boolean; - distinctId?: string; + externalId?: string; mixpanelKey?: string; mixpanelAdditionalProperties?: Record; } @@ -37,7 +37,7 @@ export function useAgentManager(props: UseAgentManagerOptions) { mode, auth, enableAnalytics, - distinctId, + externalId, streamOptions, mixpanelKey, mixpanelAdditionalProperties, @@ -100,7 +100,7 @@ export function useAgentManager(props: UseAgentManagerOptions) { auth, wsURL, enableAnalitics: enableAnalytics, - distinctId, + externalId, mixpanelKey, mixpanelAdditionalProperties, streamOptions, @@ -113,7 +113,7 @@ export function useAgentManager(props: UseAgentManagerOptions) { throw e; } - }, [agentManager, agentId, baseURL, wsURL, mode, auth, enableAnalytics, distinctId, streamOptions]); + }, [agentManager, agentId, baseURL, wsURL, mode, auth, enableAnalytics, externalId, streamOptions]); const disconnect = useCallback(async () => { if (!agentManager) return; diff --git a/package.json b/package.json index 7b52a26a..bb849b78 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@d-id/client-sdk", "private": false, - "version": "1.1.14", + "version": "1.1.15", "type": "module", "description": "d-id client sdk", "repository": { diff --git a/src/api/agents.ts b/src/api/agents.ts index fc671e10..afcb5656 100644 --- a/src/api/agents.ts +++ b/src/api/agents.ts @@ -15,9 +15,10 @@ import { RequestOptions, createClient } from './apiClient'; export function createAgentsApi( auth: Auth, host: string = didApiUrl, - onError?: (error: Error, errorData: object) => void + onError?: (error: Error, errorData: object) => void, + externalId?: string ) { - const client = createClient(auth, `${host}/agents`, onError); + const client = createClient(auth, `${host}/agents`, onError, externalId); return { create(payload: AgentPayload, options?: RequestOptions) { diff --git a/src/api/apiClient.ts b/src/api/apiClient.ts index 9df60bbd..725da135 100644 --- a/src/api/apiClient.ts +++ b/src/api/apiClient.ts @@ -15,7 +15,12 @@ const retryHttpTooManyRequests = (operation: () => Promise): Promise => shouldRetryFn: error => error.status === 429, }); -export function createClient(auth: Auth, host = didApiUrl, onError?: (error: Error, errorData: object) => void) { +export function createClient( + auth: Auth, + host = didApiUrl, + onError?: (error: Error, errorData: object) => void, + externalId?: string +) { const client = async (url: string, options?: RequestOptions) => { const { skipErrorHandler, ...fetchOptions } = options || {}; @@ -24,7 +29,7 @@ export function createClient(auth: Auth, host = didApiUrl, onError?: (error: Err ...fetchOptions, headers: { ...fetchOptions.headers, - Authorization: getAuthHeader(auth), + Authorization: getAuthHeader(auth, externalId), 'Content-Type': 'application/json', }, }) diff --git a/src/auth/get-auth-header.test.ts b/src/auth/get-auth-header.test.ts new file mode 100644 index 00000000..7fbaff11 --- /dev/null +++ b/src/auth/get-auth-header.test.ts @@ -0,0 +1,174 @@ +import { Auth } from '../types/auth'; +import { getAuthHeader, getExternalId } from './get-auth-header'; + +jest.mock('../utils', () => ({ + getRandom: jest.fn(() => 'mocked-random-id'), +})); + +const { getRandom } = require('../utils'); +const mockGetRandom = getRandom as jest.Mock; + +describe('getExternalId', () => { + beforeEach(() => { + window.localStorage.clear(); + }); + + afterEach(() => { + window.localStorage.clear(); + }); + + it('should return the provided externalId and store it in localStorage', () => { + const externalId = 'user-123'; + const result = getExternalId(externalId); + + expect(result).toBe(externalId); + expect(window.localStorage.getItem('did_external_key_id')).toBe(externalId); + }); + + it('should return existing externalId from localStorage when no parameter is provided', () => { + const existingId = 'existing-user-id'; + window.localStorage.setItem('did_external_key_id', existingId); + + const result = getExternalId(); + + expect(result).toBe(existingId); + expect(window.localStorage.getItem('did_external_key_id')).toBe(existingId); + }); + + it('should generate and store a new externalId when localStorage is empty and no parameter is provided', () => { + const { getRandom } = require('../utils'); + const mockRandomId = 'generated-id-456'; + (getRandom as jest.Mock).mockReturnValueOnce(mockRandomId); + + const result = getExternalId(); + + expect(result).toBe(mockRandomId); + expect(window.localStorage.getItem('did_external_key_id')).toBe(mockRandomId); + }); + + it('should update localStorage when a new externalId is provided', () => { + const oldId = 'old-user-id'; + const newId = 'new-user-id'; + window.localStorage.setItem('did_external_key_id', oldId); + + const result = getExternalId(newId); + + expect(result).toBe(newId); + expect(window.localStorage.getItem('did_external_key_id')).toBe(newId); + }); + + it('should handle empty string as a valid externalId', () => { + const emptyId = ''; + const result = getExternalId(emptyId); + + expect(result).toBe(emptyId); + expect(window.localStorage.getItem('did_external_key_id')).toBe(emptyId); + }); +}); + +describe('getAuthHeader', () => { + beforeEach(() => { + window.localStorage.clear(); + jest.resetModules(); + }); + + afterEach(() => { + window.localStorage.clear(); + }); + + describe('Bearer token auth', () => { + it('should return Bearer token header', () => { + const auth: Auth = { type: 'bearer', token: 'test-token-123' }; + const result = getAuthHeader(auth); + + expect(result).toBe('Bearer test-token-123'); + }); + + it('should ignore externalId for bearer auth', () => { + const auth: Auth = { type: 'bearer', token: 'test-token-123' }; + const result = getAuthHeader(auth, 'user-123'); + + expect(result).toBe('Bearer test-token-123'); + }); + }); + + describe('Basic auth', () => { + it('should return Basic auth header with base64 encoded credentials', () => { + const auth: Auth = { type: 'basic', username: 'user', password: 'pass' }; + const result = getAuthHeader(auth); + + expect(result).toBe('Basic ' + btoa('user:pass')); + }); + + it('should ignore externalId for basic auth', () => { + const auth: Auth = { type: 'basic', username: 'user', password: 'pass' }; + const result = getAuthHeader(auth, 'user-123'); + + expect(result).toBe('Basic ' + btoa('user:pass')); + }); + }); + + describe('Client-Key auth', () => { + beforeEach(() => { + mockGetRandom.mockReturnValue('mocked-random-id'); + }); + + it('should return Client-Key header with clientKey and generated externalId', () => { + const mockRandomId = 'generated-external-id'; + mockGetRandom.mockReturnValueOnce(mockRandomId); + + const auth: Auth = { type: 'key', clientKey: 'test-client-key' }; + const result = getAuthHeader(auth); + + expect(result).toBe('Client-Key test-client-key.generated-external-id_mocked-random-id'); + }); + + it('should use provided externalId in Client-Key header', () => { + const auth: Auth = { type: 'key', clientKey: 'test-client-key' }; + const externalId = 'user-123'; + const result = getAuthHeader(auth, externalId); + + expect(result).toBe('Client-Key test-client-key.user-123_mocked-random-id'); + expect(result).toContain('user-123'); + }); + + it('should use externalId from localStorage when not provided', () => { + const existingId = 'stored-user-id'; + window.localStorage.setItem('did_external_key_id', existingId); + + const auth: Auth = { type: 'key', clientKey: 'test-client-key' }; + const result = getAuthHeader(auth); + + expect(result).toBe('Client-Key test-client-key.stored-user-id_mocked-random-id'); + expect(result).toContain('stored-user-id'); + }); + + it('should generate new externalId and store it when localStorage is empty', () => { + const mockRandomId = 'new-generated-id'; + mockGetRandom.mockReturnValueOnce(mockRandomId); + + const auth: Auth = { type: 'key', clientKey: 'test-client-key' }; + const result = getAuthHeader(auth); + + expect(result).toBe('Client-Key test-client-key.new-generated-id_mocked-random-id'); + expect(result).toContain('new-generated-id'); + expect(window.localStorage.getItem('did_external_key_id')).toBe(mockRandomId); + }); + + it('should update localStorage with provided externalId', () => { + const auth: Auth = { type: 'key', clientKey: 'test-client-key' }; + const externalId = 'new-user-id'; + getAuthHeader(auth, externalId); + + expect(window.localStorage.getItem('did_external_key_id')).toBe(externalId); + }); + }); + + describe('Error handling', () => { + it('should throw error for unknown auth type', () => { + const auth = { type: 'unknown' } as any; + + expect(() => getAuthHeader(auth)).toThrow('Unknown auth type: [object Object]'); + }); + }); +}); diff --git a/src/auth/get-auth-header.ts b/src/auth/get-auth-header.ts index edf02846..7b10fea7 100644 --- a/src/auth/get-auth-header.ts +++ b/src/auth/get-auth-header.ts @@ -1,7 +1,12 @@ import { Auth } from '$/types/auth'; import { getRandom } from '$/utils'; -export function getExternalId() { +export function getExternalId(externalId?: string): string { + if (externalId !== undefined) { + window.localStorage.setItem('did_external_key_id', externalId); + return externalId; + } + let key = window.localStorage.getItem('did_external_key_id'); if (!key) { @@ -14,13 +19,13 @@ export function getExternalId() { } let sessionKey = getRandom(); -export function getAuthHeader(auth: Auth) { +export function getAuthHeader(auth: Auth, externalId?: string) { if (auth.type === 'bearer') { return `Bearer ${auth.token}`; } else if (auth.type === 'basic') { return `Basic ${btoa(`${auth.username}:${auth.password}`)}`; } else if (auth.type === 'key') { - return `Client-Key ${auth.clientKey}.${getExternalId()}_${sessionKey}`; + return `Client-Key ${auth.clientKey}.${getExternalId(externalId)}_${sessionKey}`; } else { throw new Error(`Unknown auth type: ${auth}`); } diff --git a/src/services/agent-manager/connect-to-manager.test.ts b/src/services/agent-manager/connect-to-manager.test.ts index f4afdb5c..75755538 100644 --- a/src/services/agent-manager/connect-to-manager.test.ts +++ b/src/services/agent-manager/connect-to-manager.test.ts @@ -421,7 +421,7 @@ describe('connect-to-manager', () => { it('should include analytics data when provided', async () => { const optionsWithAnalytics = { ...mockOptions, - distinctId: 'analytics-user', + externalId: 'analytics-user', mixpanelAdditionalProperties: { plan: 'scale' }, }; @@ -432,7 +432,6 @@ describe('connect-to-manager', () => { expect.objectContaining({ version: StreamApiVersion.V1, end_user_data: { - distinct_id: 'analytics-user', plan: 'scale', }, }), diff --git a/src/services/agent-manager/connect-to-manager.ts b/src/services/agent-manager/connect-to-manager.ts index 161d99a6..100381da 100644 --- a/src/services/agent-manager/connect-to-manager.ts +++ b/src/services/agent-manager/connect-to-manager.ts @@ -36,12 +36,9 @@ function getAgentStreamV1Options(options?: ConnectToManagerOptions): CreateStrea const { streamOptions } = options ?? {}; const endUserData = - options?.distinctId || options?.mixpanelAdditionalProperties?.plan !== undefined + options?.mixpanelAdditionalProperties?.plan !== undefined ? { - ...(options?.distinctId ? { distinct_id: options.distinctId } : {}), - ...(options?.mixpanelAdditionalProperties?.plan !== undefined - ? { plan: options.mixpanelAdditionalProperties?.plan } - : {}), + plan: options.mixpanelAdditionalProperties?.plan, } : undefined; diff --git a/src/services/agent-manager/index.test.ts b/src/services/agent-manager/index.test.ts index 5372c060..075bdaf0 100644 --- a/src/services/agent-manager/index.test.ts +++ b/src/services/agent-manager/index.test.ts @@ -16,7 +16,7 @@ import { sendInterrupt, validateInterrupt } from '../interrupt'; import { createSocketManager } from '../socket-manager'; import { createMessageEventQueue } from '../socket-manager/message-queue'; import { initializeStreamAndChat } from './connect-to-manager'; -import { createAgentManager, getAgent } from './index'; +import { createAgentManager } from './index'; // Mock all dependencies jest.mock('../../api/agents'); @@ -93,7 +93,8 @@ describe('createAgentManager', () => { expect(createAgentsApi).toHaveBeenCalledWith( mockOptions.auth, 'https://api.d-id.com', - mockOptions.callbacks.onError + mockOptions.callbacks.onError, + undefined ); expect(mockAgentsApi.getById).toHaveBeenCalledWith('agent-123'); }); @@ -105,7 +106,7 @@ describe('createAgentManager', () => { token: 'test-mixpanel-key', agentId: 'agent-123', isEnabled: true, - distinctId: undefined, + externalId: undefined, }); expect(mockAnalytics.track).toHaveBeenCalledWith('agent-sdk', { event: 'init' }); expect(mockAnalytics.track).toHaveBeenCalledWith('agent-sdk', expect.objectContaining({ event: 'loaded' })); @@ -117,7 +118,7 @@ describe('createAgentManager', () => { mixpanelKey: 'custom-mixpanel', wsURL: 'wss://custom.com', baseURL: 'https://custom.com', - distinctId: 'custom-user', + externalId: 'custom-user', }; await createAgentManager('agent-123', customOptions); @@ -126,7 +127,7 @@ describe('createAgentManager', () => { token: 'custom-mixpanel', agentId: 'agent-123', isEnabled: true, - distinctId: 'custom-user', + externalId: 'custom-user', }); }); @@ -717,39 +718,8 @@ describe('createAgentManager', () => { token: 'test-mixpanel-key', agentId: 'agent-123', isEnabled: false, - distinctId: undefined, + externalId: undefined, }); }); }); }); - -describe('getAgent', () => { - let mockAgentsApi: any; - - beforeEach(() => { - jest.clearAllMocks(); - - mockAgentsApi = AgentsApiFactory.build({ - getById: jest.fn().mockResolvedValue({ id: 'agent-123', name: 'Test Agent' }), - }); - (createAgentsApi as jest.Mock).mockReturnValue(mockAgentsApi); - }); - - it('should get agent by ID', async () => { - const auth = { type: 'key' as const, clientKey: 'test-key', externalId: 'user-123' }; - const agent = await getAgent('agent-123', auth); - - expect(createAgentsApi).toHaveBeenCalledWith(auth, 'https://api.d-id.com'); - expect(mockAgentsApi.getById).toHaveBeenCalledWith('agent-123'); - expect(agent).toEqual({ id: 'agent-123', name: 'Test Agent' }); - }); - - it('should use custom baseURL', async () => { - const auth = { type: 'key' as const, clientKey: 'test-key', externalId: 'user-123' }; - const customBaseURL = 'https://custom-api.com'; - - await getAgent('agent-123', auth, customBaseURL); - - expect(createAgentsApi).toHaveBeenCalledWith(auth, customBaseURL); - }); -}); diff --git a/src/services/agent-manager/index.ts b/src/services/agent-manager/index.ts index e6bd23b2..47bf26e5 100644 --- a/src/services/agent-manager/index.ts +++ b/src/services/agent-manager/index.ts @@ -1,8 +1,6 @@ import { - Agent, AgentManager, AgentManagerOptions, - Auth, Chat, ChatMode, ConnectionState, @@ -67,11 +65,11 @@ export async function createAgentManager(agent: string, options: AgentManagerOpt token: mxKey, agentId: agent, isEnabled: options.enableAnalitics, - distinctId: options.distinctId, + externalId: options.externalId, mixpanelAdditionalProperties: options.mixpanelAdditionalProperties, }); analytics.track('agent-sdk', { event: 'init' }); - const agentsApi = createAgentsApi(options.auth, baseURL, options.callbacks.onError); + const agentsApi = createAgentsApi(options.auth, baseURL, options.callbacks.onError, options.externalId); const agentEntity = await agentsApi.getById(agent); analytics.enrich(getAgentInfo(agentEntity)); @@ -104,7 +102,12 @@ export async function createAgentManager(agent: string, options: AgentManagerOpt const websocketPromise = options.mode === ChatMode.DirectPlayback ? Promise.resolve(undefined) - : createSocketManager(options.auth, wsURL, { onMessage, onError: options.callbacks.onError }); + : createSocketManager( + options.auth, + wsURL, + { onMessage, onError: options.callbacks.onError }, + options.externalId + ); const initPromise = retryOperation( () => { @@ -465,9 +468,3 @@ export async function createAgentManager(agent: string, options: AgentManagerOpt }, }; } - -export function getAgent(agentId: string, auth: Auth, baseURL?: string): Promise { - const { getById } = createAgentsApi(auth, baseURL || didApiUrl); - - return getById(agentId); -} diff --git a/src/services/analytics/mixpanel.ts b/src/services/analytics/mixpanel.ts index 07619fc7..2291c868 100644 --- a/src/services/analytics/mixpanel.ts +++ b/src/services/analytics/mixpanel.ts @@ -5,7 +5,7 @@ export interface AnalyticsOptions { token: string; agentId: string; isEnabled?: boolean; - distinctId?: string; + externalId?: string; mixpanelAdditionalProperties?: Record; } @@ -42,10 +42,10 @@ export function initializeAnalytics(config: AnalyticsOptions): Analytics { return { token: config.token || 'testKey', - distinct_id: config.distinctId || getExternalId(), + distinct_id: getExternalId(config.externalId), agentId: config.agentId, additionalProperties: { - id: config.distinctId, + id: getExternalId(config.externalId), ...(config.mixpanelAdditionalProperties || {}), }, isEnabled: config.isEnabled ?? true, diff --git a/src/services/socket-manager/index.ts b/src/services/socket-manager/index.ts index 5a5801cf..bbe0e622 100644 --- a/src/services/socket-manager/index.ts +++ b/src/services/socket-manager/index.ts @@ -7,6 +7,7 @@ import { sleep } from '$/utils'; interface Options { auth: Auth; retries?: number; + externalId?: string; callbacks?: { onMessage?: (event: MessageEvent) => void; onOpen?: (event: Event) => void; @@ -24,9 +25,9 @@ export interface SocketManager { function connect(options: Options): Promise { return new Promise((resolve, reject) => { - const { callbacks, host, auth } = options; + const { callbacks, host, auth, externalId } = options; const { onMessage = null, onOpen = null, onClose = null, onError = null } = callbacks || {}; - const socket = new WebSocket(`${host}?authorization=${getAuthHeader(auth)}`); + const socket = new WebSocket(`${host}?authorization=${getAuthHeader(auth, externalId)}`); socket.onmessage = onMessage; socket.onclose = onClose; @@ -69,12 +70,14 @@ export async function createSocketManager( callbacks: { onMessage: ChatProgressCallback; onError?: (error: Error) => void; - } + }, + externalId?: string ): Promise { const messageCallbacks: ChatProgressCallback[] = callbacks?.onMessage ? [callbacks.onMessage] : []; const socket: WebSocket = await connectWithRetries({ auth, host, + externalId, callbacks: { onError: error => callbacks.onError?.(new WsError(error)), onMessage(event: MessageEvent) { diff --git a/src/types/entities/agents/manager.ts b/src/types/entities/agents/manager.ts index 139d6827..1b52db29 100644 --- a/src/types/entities/agents/manager.ts +++ b/src/types/entities/agents/manager.ts @@ -151,10 +151,7 @@ export interface AgentManagerOptions { enableAnalitics?: boolean; mixpanelKey?: string; mixpanelAdditionalProperties?: Record; - /** - * Unique ID of agent user used in analytics. Pass it to override the default way to get distinctId - */ - distinctId?: string; + externalId?: string; streamOptions?: StreamOptions; initialMessages?: Message[]; persistentChat?: boolean; diff --git a/src/types/stream/stream.ts b/src/types/stream/stream.ts index 0c7a3020..76fb22ff 100644 --- a/src/types/stream/stream.ts +++ b/src/types/stream/stream.ts @@ -67,7 +67,6 @@ export interface ManagerCallbacks { export type ManagerCallbackKeys = keyof ManagerCallbacks; export interface StreamEndUserData { - distinct_id?: string; plan?: string; } diff --git a/src/types/voice/tts.ts b/src/types/voice/tts.ts index b00f2c9a..f37300ad 100644 --- a/src/types/voice/tts.ts +++ b/src/types/voice/tts.ts @@ -1,5 +1,6 @@ export enum Providers { Amazon = 'amazon', + AzureOpenAi = 'azure-openai', Microsoft = 'microsoft', Afflorithmics = 'afflorithmics', Elevenlabs = 'elevenlabs', @@ -90,6 +91,13 @@ export interface Microsoft_tts_provider { voice_language?: string; } +/** + * AzureOpenAi provider details, contains the provider type and requested voice id and style + */ +export interface AzureOpenAi_tts_provider extends Omit { + type: Providers.AzureOpenAi; +} + /** * Amazon provider details, contains the provider type and requested voice id */ @@ -164,10 +172,15 @@ export interface VoiceConfigAfflorithmics { voiceIntelligence?: boolean; } -export type TextToSpeechProviders = Microsoft_tts_provider | Afflorithmics_tts_provider | Elevenlabs_tts_provider; +export type TextToSpeechProviders = + | Microsoft_tts_provider + | AzureOpenAi_tts_provider + | Afflorithmics_tts_provider + | Elevenlabs_tts_provider; export type ExtendedTextToSpeechProviders = TextToSpeechProviders | Amazon_tts_provider; export type StreamTextToSpeechProviders = | Microsoft_tts_provider + | AzureOpenAi_tts_provider | Afflorithmics_tts_provider | Elevenlabs_tts_provider | Amazon_tts_provider;