From 388b825392dc7667026c1d86609a08d5a0d17951 Mon Sep 17 00:00:00 2001 From: Vladislav Gruchik <4280527+vagruchi@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:02:39 +0200 Subject: [PATCH 1/6] feat: add create srt credentials method to StreamCall --- src/ApiClient.ts | 54 ++++++++++++++++++++++++++++++++++++++++++++- src/StreamCall.ts | 44 +++++++++++++++++++++++++++++++++++- src/StreamClient.ts | 22 +++++------------- src/types.ts | 1 + 4 files changed, 102 insertions(+), 19 deletions(-) diff --git a/src/ApiClient.ts b/src/ApiClient.ts index c8e0168..e95ea70 100644 --- a/src/ApiClient.ts +++ b/src/ApiClient.ts @@ -1,5 +1,11 @@ import { v4 as uuidv4 } from 'uuid'; -import { ApiConfig, RequestMetadata, StreamError } from './types'; +import { + ApiConfig, + RequestMetadata, + StreamError, + UserTokenPayload, +} from './types'; +import { JWTUserToken } from './utils/create-token'; import { APIError } from './gen/models'; import { getRateLimitFromResponseHeader } from './utils/rate-limit'; @@ -147,4 +153,50 @@ export class ApiClient { return newParams.join('&'); }; + + /** + * + * @param payload + * - user_id - the id of the user the token is for + * - validity_in_seconds - how many seconds is the token valid for (starting from issued at), by default it's 1 hour, dicarded if exp is provided + * - exp - when the token expires, unix timestamp in seconds + * - iat - issued at date of the token, unix timestamp in seconds, by default it's now + */ + generateUserToken = ( + payload: { + user_id: string; + validity_in_seconds?: number; + exp?: number; + iat?: number; + } & Record, + ) => { + if (!this.apiConfig.secret) { + throw new Error('API secret is not set'); + } + + const defaultIat = Math.floor((Date.now() - 1000) / 1000); + payload.iat = payload.iat ?? defaultIat; + const validityInSeconds = payload.validity_in_seconds ?? 60 * 60; + payload.exp = payload.exp ?? payload.iat + validityInSeconds; + + return JWTUserToken(this.apiConfig.secret, payload as UserTokenPayload); + }; + + createToken = ( + userID: string, + exp = Math.round(Date.now() / 1000) + 60 * 60, + iat = Math.floor((Date.now() - 1000) / 1000), + ) => { + if (!this.apiConfig.secret) { + throw new Error('API secret is not set'); + } + + const payload: UserTokenPayload = { + user_id: userID, + exp, + iat, + }; + + return JWTUserToken(this.apiConfig.secret, payload); + }; } diff --git a/src/StreamCall.ts b/src/StreamCall.ts index f66c035..5cd655b 100644 --- a/src/StreamCall.ts +++ b/src/StreamCall.ts @@ -1,8 +1,14 @@ -import { GetOrCreateCallRequest, QueryCallMembersRequest } from './gen/models'; +import { + CallResponse, + GetOrCreateCallRequest, + QueryCallMembersRequest, +} from './gen/models'; import { CallApi } from './gen/video/CallApi'; import { OmitTypeId } from './types'; export class StreamCall extends CallApi { + data?: CallResponse; + get cid() { return `${this.type}:${this.id}`; } @@ -16,4 +22,40 @@ export class StreamCall extends CallApi { ...(request ?? {}), }); }; + + getOrCreate = async (request?: GetOrCreateCallRequest) => { + const response = await super.getOrCreate(request); + this.data = response.call; + return response; + }; + + get = async () => { + const response = await super.get(); + this.data = response.call; + return response; + }; + + createSRTCredetials = ( + userID: string, + ): { + address: string; + } => { + if (!this.data) { + throw new Error( + 'Object is not initialized, call get() or getOrCreate() first', + ); + } + + const token = this.videoApi.apiClient.createToken(userID, undefined); + const segments = token.split('.'); + if (segments.length !== 3) { + throw new Error('Invalid token format'); + } + + return { + address: this.data.ingress.srt.address + .replace('{passphrase}', segments[2]) + .replace('{token}', token), + }; + }; } diff --git a/src/StreamClient.ts b/src/StreamClient.ts index 373cdf0..caf5edc 100644 --- a/src/StreamClient.ts +++ b/src/StreamClient.ts @@ -54,6 +54,7 @@ export class StreamClient extends CommonApi { baseUrl: chatBaseUrl, timeout, agent: config?.agent as RequestInit['dispatcher'], + secret, }); const videoApiClient = new ApiClient({ @@ -62,6 +63,7 @@ export class StreamClient extends CommonApi { baseUrl: videoBaseUrl, timeout, agent: config?.agent as RequestInit['dispatcher'], + secret, }); const feedsApiClient = new ApiClient({ @@ -70,6 +72,7 @@ export class StreamClient extends CommonApi { baseUrl: feedsBaseUrl, timeout, agent: config?.agent as RequestInit['dispatcher'], + secret, }); super(chatApiClient); @@ -136,14 +139,7 @@ export class StreamClient extends CommonApi { exp?: number; iat?: number; } & Record, - ) => { - const defaultIat = Math.floor((Date.now() - 1000) / 1000); - payload.iat = payload.iat ?? defaultIat; - const validityInSeconds = payload.validity_in_seconds ?? 60 * 60; - payload.exp = payload.exp ?? payload.iat + validityInSeconds; - - return JWTUserToken(this.secret, payload as UserTokenPayload); - }; + ) => this.apiClient.generateUserToken(payload); /** * @@ -179,15 +175,7 @@ export class StreamClient extends CommonApi { userID: string, exp = Math.round(Date.now() / 1000) + 60 * 60, iat = Math.floor((Date.now() - 1000) / 1000), - ) => { - const payload: UserTokenPayload = { - user_id: userID, - exp, - iat, - }; - - return JWTUserToken(this.secret, payload); - }; + ) => this.apiClient.createToken(userID, exp, iat); /** * diff --git a/src/types.ts b/src/types.ts index 7e93e14..3d923d5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,6 +7,7 @@ export interface ApiConfig { /** The timeout for requests in milliseconds. The default is 3000. */ timeout: number; agent?: RequestInit['dispatcher']; + secret?: string; } export interface RequestMetadata { From 61539ebfa798a4b1e8d762fe761605ebf68cbbf5 Mon Sep 17 00:00:00 2001 From: Vladislav Gruchik <4280527+vagruchi@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:05:49 +0200 Subject: [PATCH 2/6] remove unused import --- src/StreamClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StreamClient.ts b/src/StreamClient.ts index caf5edc..a72fb81 100644 --- a/src/StreamClient.ts +++ b/src/StreamClient.ts @@ -3,7 +3,7 @@ import { CommonApi } from './gen/common/CommonApi'; import { StreamVideoClient } from './StreamVideoClient'; import crypto from 'crypto'; import { StreamChatClient } from './StreamChatClient'; -import { CallTokenPayload, UserTokenPayload } from './types'; +import { CallTokenPayload } from './types'; import { FileUploadRequest, ImageUploadRequest, From 1d36b6541cd7d1477301dad2573e5750fb25fb42 Mon Sep 17 00:00:00 2001 From: Vladislav Gruchik <4280527+vagruchi@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:14:16 +0200 Subject: [PATCH 3/6] test for create srt url --- __tests__/call.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/__tests__/call.test.ts b/__tests__/call.test.ts index 7623d29..4e17a42 100644 --- a/__tests__/call.test.ts +++ b/__tests__/call.test.ts @@ -204,6 +204,14 @@ describe('call API', () => { expect(response.call.settings.backstage.enabled).toBe(true); }); + it('generate SRT credentials', () => { + const creds = call.createSRTCredetials('john'); + + expect(creds).toBeDefined(); + expect(creds.address).toBeDefined(); + expect(creds.address).not.toBe(''); + }); + it('go live', async () => { const response = await call.goLive(); From bf839ade76d16c38654cc621634b34ce79d72c85 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Tue, 30 Sep 2025 16:35:34 +0200 Subject: [PATCH 4/6] Add client reference to call --- src/StreamCall.ts | 11 +++++++++++ src/StreamVideoClient.ts | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/StreamCall.ts b/src/StreamCall.ts index 5cd655b..b049f94 100644 --- a/src/StreamCall.ts +++ b/src/StreamCall.ts @@ -1,14 +1,25 @@ +import { VideoApi } from './gen-imports'; import { CallResponse, GetOrCreateCallRequest, QueryCallMembersRequest, } from './gen/models'; import { CallApi } from './gen/video/CallApi'; +import { StreamClient } from './StreamClient'; import { OmitTypeId } from './types'; export class StreamCall extends CallApi { data?: CallResponse; + constructor( + videoApi: VideoApi, + readonly type: string, + readonly id: string, + private readonly streamClient: StreamClient, + ) { + super(videoApi, type, id); + } + get cid() { return `${this.type}:${this.id}`; } diff --git a/src/StreamVideoClient.ts b/src/StreamVideoClient.ts index ac1e6c4..08f7243 100644 --- a/src/StreamVideoClient.ts +++ b/src/StreamVideoClient.ts @@ -25,7 +25,7 @@ export class StreamVideoClient extends VideoApi { } call = (type: string, id: string) => { - return new StreamCall(this, type, id); + return new StreamCall(this, type, id, this.streamClient); }; connectOpenAi = async (options: { From 6da49b160bdd70044b758773a7db081adae62b8f Mon Sep 17 00:00:00 2001 From: Vladislav Gruchik <4280527+vagruchi@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:42:32 +0200 Subject: [PATCH 5/6] revert token changes --- src/ApiClient.ts | 54 +-------------------------------------------- src/StreamCall.ts | 4 +++- src/StreamClient.ts | 24 +++++++++++++++----- 3 files changed, 22 insertions(+), 60 deletions(-) diff --git a/src/ApiClient.ts b/src/ApiClient.ts index e95ea70..c8e0168 100644 --- a/src/ApiClient.ts +++ b/src/ApiClient.ts @@ -1,11 +1,5 @@ import { v4 as uuidv4 } from 'uuid'; -import { - ApiConfig, - RequestMetadata, - StreamError, - UserTokenPayload, -} from './types'; -import { JWTUserToken } from './utils/create-token'; +import { ApiConfig, RequestMetadata, StreamError } from './types'; import { APIError } from './gen/models'; import { getRateLimitFromResponseHeader } from './utils/rate-limit'; @@ -153,50 +147,4 @@ export class ApiClient { return newParams.join('&'); }; - - /** - * - * @param payload - * - user_id - the id of the user the token is for - * - validity_in_seconds - how many seconds is the token valid for (starting from issued at), by default it's 1 hour, dicarded if exp is provided - * - exp - when the token expires, unix timestamp in seconds - * - iat - issued at date of the token, unix timestamp in seconds, by default it's now - */ - generateUserToken = ( - payload: { - user_id: string; - validity_in_seconds?: number; - exp?: number; - iat?: number; - } & Record, - ) => { - if (!this.apiConfig.secret) { - throw new Error('API secret is not set'); - } - - const defaultIat = Math.floor((Date.now() - 1000) / 1000); - payload.iat = payload.iat ?? defaultIat; - const validityInSeconds = payload.validity_in_seconds ?? 60 * 60; - payload.exp = payload.exp ?? payload.iat + validityInSeconds; - - return JWTUserToken(this.apiConfig.secret, payload as UserTokenPayload); - }; - - createToken = ( - userID: string, - exp = Math.round(Date.now() / 1000) + 60 * 60, - iat = Math.floor((Date.now() - 1000) / 1000), - ) => { - if (!this.apiConfig.secret) { - throw new Error('API secret is not set'); - } - - const payload: UserTokenPayload = { - user_id: userID, - exp, - iat, - }; - - return JWTUserToken(this.apiConfig.secret, payload); - }; } diff --git a/src/StreamCall.ts b/src/StreamCall.ts index b049f94..7ca3c48 100644 --- a/src/StreamCall.ts +++ b/src/StreamCall.ts @@ -57,7 +57,9 @@ export class StreamCall extends CallApi { ); } - const token = this.videoApi.apiClient.createToken(userID, undefined); + const token = this.streamClient.generateUserToken({ + user_id: userID, + }); const segments = token.split('.'); if (segments.length !== 3) { throw new Error('Invalid token format'); diff --git a/src/StreamClient.ts b/src/StreamClient.ts index a72fb81..373cdf0 100644 --- a/src/StreamClient.ts +++ b/src/StreamClient.ts @@ -3,7 +3,7 @@ import { CommonApi } from './gen/common/CommonApi'; import { StreamVideoClient } from './StreamVideoClient'; import crypto from 'crypto'; import { StreamChatClient } from './StreamChatClient'; -import { CallTokenPayload } from './types'; +import { CallTokenPayload, UserTokenPayload } from './types'; import { FileUploadRequest, ImageUploadRequest, @@ -54,7 +54,6 @@ export class StreamClient extends CommonApi { baseUrl: chatBaseUrl, timeout, agent: config?.agent as RequestInit['dispatcher'], - secret, }); const videoApiClient = new ApiClient({ @@ -63,7 +62,6 @@ export class StreamClient extends CommonApi { baseUrl: videoBaseUrl, timeout, agent: config?.agent as RequestInit['dispatcher'], - secret, }); const feedsApiClient = new ApiClient({ @@ -72,7 +70,6 @@ export class StreamClient extends CommonApi { baseUrl: feedsBaseUrl, timeout, agent: config?.agent as RequestInit['dispatcher'], - secret, }); super(chatApiClient); @@ -139,7 +136,14 @@ export class StreamClient extends CommonApi { exp?: number; iat?: number; } & Record, - ) => this.apiClient.generateUserToken(payload); + ) => { + const defaultIat = Math.floor((Date.now() - 1000) / 1000); + payload.iat = payload.iat ?? defaultIat; + const validityInSeconds = payload.validity_in_seconds ?? 60 * 60; + payload.exp = payload.exp ?? payload.iat + validityInSeconds; + + return JWTUserToken(this.secret, payload as UserTokenPayload); + }; /** * @@ -175,7 +179,15 @@ export class StreamClient extends CommonApi { userID: string, exp = Math.round(Date.now() / 1000) + 60 * 60, iat = Math.floor((Date.now() - 1000) / 1000), - ) => this.apiClient.createToken(userID, exp, iat); + ) => { + const payload: UserTokenPayload = { + user_id: userID, + exp, + iat, + }; + + return JWTUserToken(this.secret, payload); + }; /** * From c83d4271b412bce410cc67662dd39be72a634e80 Mon Sep 17 00:00:00 2001 From: Vladislav Gruchik <4280527+vagruchi@users.noreply.github.com> Date: Tue, 30 Sep 2025 17:05:03 +0200 Subject: [PATCH 6/6] generate permanent user token for streaming --- src/StreamCall.ts | 2 +- src/StreamClient.ts | 18 ++++++++++++++++++ src/types.ts | 2 +- src/utils/create-token.ts | 2 +- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/StreamCall.ts b/src/StreamCall.ts index 7ca3c48..eb80e9a 100644 --- a/src/StreamCall.ts +++ b/src/StreamCall.ts @@ -57,7 +57,7 @@ export class StreamCall extends CallApi { ); } - const token = this.streamClient.generateUserToken({ + const token = this.streamClient.generatePermanentUserToken({ user_id: userID, }); const segments = token.split('.'); diff --git a/src/StreamClient.ts b/src/StreamClient.ts index 373cdf0..a4f2cde 100644 --- a/src/StreamClient.ts +++ b/src/StreamClient.ts @@ -145,6 +145,24 @@ export class StreamClient extends CommonApi { return JWTUserToken(this.secret, payload as UserTokenPayload); }; + /** + * + * @param payload + * - user_id - the id of the user the token is for + * - iat - issued at date of the token, unix timestamp in seconds, by default it's now + */ + generatePermanentUserToken = ( + payload: { + user_id: string; + iat?: number; + } & Record, + ) => { + const defaultIat = Math.floor((Date.now() - 1000) / 1000); + payload.iat = payload.iat ?? defaultIat; + + return JWTUserToken(this.secret, payload as UserTokenPayload); + }; + /** * * @param payload diff --git a/src/types.ts b/src/types.ts index 3d923d5..e3bbeee 100644 --- a/src/types.ts +++ b/src/types.ts @@ -40,7 +40,7 @@ export interface RateLimit { interface BaseTokenPayload { user_id: string; - exp: number; + exp?: number; iat: number; call_cids?: string[]; } diff --git a/src/utils/create-token.ts b/src/utils/create-token.ts index f3adcc0..d889f9a 100644 --- a/src/utils/create-token.ts +++ b/src/utils/create-token.ts @@ -4,7 +4,7 @@ export function JWTUserToken( apiSecret: Secret, payload: { user_id: string; - exp: number; + exp?: number; iat: number; call_cids?: string[]; } & { [key: string]: any },