Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions demo/hooks/useAgentManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ interface UseAgentManagerOptions {
fluent?: boolean;
};
enableAnalytics?: boolean;
distinctId?: string;
externalId?: string;
mixpanelKey?: string;
mixpanelAdditionalProperties?: Record<string, any>;
}
Expand All @@ -37,7 +37,7 @@ export function useAgentManager(props: UseAgentManagerOptions) {
mode,
auth,
enableAnalytics,
distinctId,
externalId,
streamOptions,
mixpanelKey,
mixpanelAdditionalProperties,
Expand Down Expand Up @@ -100,7 +100,7 @@ export function useAgentManager(props: UseAgentManagerOptions) {
auth,
wsURL,
enableAnalitics: enableAnalytics,
distinctId,
externalId,
mixpanelKey,
mixpanelAdditionalProperties,
streamOptions,
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
5 changes: 3 additions & 2 deletions src/api/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
9 changes: 7 additions & 2 deletions src/api/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ const retryHttpTooManyRequests = <T>(operation: () => Promise<T>): Promise<T> =>
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 <T>(url: string, options?: RequestOptions) => {
const { skipErrorHandler, ...fetchOptions } = options || {};

Expand All @@ -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',
},
})
Expand Down
174 changes: 174 additions & 0 deletions src/auth/get-auth-header.test.ts
Original file line number Diff line number Diff line change
@@ -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]');
});
});
});
11 changes: 8 additions & 3 deletions src/auth/get-auth-header.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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}`);
}
Expand Down
3 changes: 1 addition & 2 deletions src/services/agent-manager/connect-to-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
};

Expand All @@ -432,7 +432,6 @@ describe('connect-to-manager', () => {
expect.objectContaining({
version: StreamApiVersion.V1,
end_user_data: {
distinct_id: 'analytics-user',
plan: 'scale',
},
}),
Expand Down
7 changes: 2 additions & 5 deletions src/services/agent-manager/connect-to-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading