Skip to content

Commit db0104d

Browse files
committed
feat: temporarily add token source
1 parent 18ffe81 commit db0104d

File tree

3 files changed

+248
-1
lines changed

3 files changed

+248
-1
lines changed

packages/react/src/TokenSource.ts

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
import { RoomConfiguration } from '@livekit/protocol';
2+
import { decodeJwt } from 'jose';
3+
4+
const ONE_SECOND_IN_MILLISECONDS = 1000;
5+
const ONE_MINUTE_IN_MILLISECONDS = 60 * ONE_SECOND_IN_MILLISECONDS;
6+
7+
/**
8+
* TokenSource handles getting credentials for connecting to a new Room, caching
9+
* the last result and using it until it expires. */
10+
export abstract class TokenSource {
11+
private request: TokenSource.Request = {};
12+
13+
protected cachedResponse: TokenSource.Response | null = null;
14+
15+
private inProgressFetch: Promise<TokenSource.Response> | null = null;
16+
17+
protected getCachedResponseJwtPayload() {
18+
const token = this.cachedResponse?.participantToken;
19+
if (!token) {
20+
return null;
21+
}
22+
23+
return decodeJwt<{ roomConfig?: ReturnType<RoomConfiguration['toJson']> }>(token);
24+
}
25+
26+
protected isCachedResponseExpired() {
27+
const jwtPayload = this.getCachedResponseJwtPayload();
28+
if (!jwtPayload?.exp) {
29+
return true;
30+
}
31+
const expInMilliseconds = jwtPayload.exp * ONE_SECOND_IN_MILLISECONDS;
32+
const expiresAt = new Date(expInMilliseconds - ONE_MINUTE_IN_MILLISECONDS);
33+
34+
const now = new Date();
35+
return expiresAt >= now;
36+
}
37+
38+
getCachedResponseRoomConfig() {
39+
const roomConfigJsonValue = this.getCachedResponseJwtPayload()?.roomConfig;
40+
if (!roomConfigJsonValue) {
41+
return null;
42+
}
43+
return RoomConfiguration.fromJson(roomConfigJsonValue);
44+
}
45+
46+
protected isSameAsCachedRequest(request: TokenSource.Request) {
47+
if (!this.request) {
48+
return false;
49+
}
50+
51+
if (this.request.roomName !== request.roomName) {
52+
return false;
53+
}
54+
if (this.request.participantName !== request.participantName) {
55+
return false;
56+
}
57+
if (
58+
(!this.request.roomConfig && request.roomConfig) ||
59+
(this.request.roomConfig && !request.roomConfig)
60+
) {
61+
return false;
62+
}
63+
if (
64+
this.request.roomConfig &&
65+
request.roomConfig &&
66+
!this.request.roomConfig.equals(request.roomConfig)
67+
) {
68+
return false;
69+
}
70+
71+
return true;
72+
}
73+
74+
/**
75+
* Store request metadata which will be provide explicitly when fetching new credentials.
76+
*
77+
* @example new TokenSource.Custom((request /* <= This value! *\/) => ({ serverUrl: "...", participantToken: "..." })) */
78+
setRequest(request: TokenSource.Request) {
79+
if (!this.isSameAsCachedRequest(request)) {
80+
this.cachedResponse = null;
81+
}
82+
this.request = request;
83+
}
84+
85+
clearRequest() {
86+
this.request = {};
87+
this.cachedResponse = null;
88+
}
89+
90+
async generate() {
91+
if (this.isCachedResponseExpired()) {
92+
await this.refresh();
93+
}
94+
95+
return this.cachedResponse!;
96+
}
97+
98+
async refresh() {
99+
if (this.inProgressFetch) {
100+
await this.inProgressFetch;
101+
return;
102+
}
103+
104+
try {
105+
this.inProgressFetch = this.fetch(this.request);
106+
this.cachedResponse = await this.inProgressFetch;
107+
} finally {
108+
this.inProgressFetch = null;
109+
}
110+
}
111+
112+
protected abstract fetch(
113+
request: TokenSource.Request,
114+
): Promise<TokenSource.Response>;
115+
}
116+
117+
export namespace TokenSource {
118+
export type Request = {
119+
/** The name of the room being requested when generating credentials */
120+
roomName?: string;
121+
122+
/** The identity of the participant being requested for this client when generating credentials */
123+
participantName?: string;
124+
125+
/**
126+
* A RoomConfiguration object can be passed to request extra parameters should be included when
127+
* generating connection credentials - dispatching agents, defining egress settings, etc
128+
* @see https://docs.livekit.io/home/get-started/authentication/#room-configuration
129+
*/
130+
roomConfig?: RoomConfiguration;
131+
};
132+
export type Response = {
133+
serverUrl: string;
134+
participantToken: string;
135+
};
136+
137+
export type LiteralOptions = { loggerName?: string };
138+
139+
/** TokenSource.Literal contains a single, literal set of credentials.
140+
* Note that refreshing credentials isn't implemented, because there is only one set provided.
141+
* */
142+
export class Literal extends TokenSource {
143+
private log = console;
144+
145+
constructor(payload: Response /* , options?: LiteralOptions */) {
146+
super();
147+
this.cachedResponse = payload;
148+
149+
// this.log = getLogger(options?.loggerName ?? LoggerNames.TokenSource);
150+
}
151+
152+
async fetch() {
153+
if (this.isCachedResponseExpired()) {
154+
// FIXME: figure out a better logging solution?
155+
this.log.warn(
156+
'The credentials within TokenSource.Literal have expired, so any upcoming uses of them will likely fail.',
157+
);
158+
}
159+
return this.cachedResponse!;
160+
}
161+
}
162+
163+
/** TokenSource.Custom allows a user to define a manual function which generates new
164+
* {@link Response} values on demand. Use this to get credentials from custom backends / etc.
165+
* */
166+
export class Custom extends TokenSource {
167+
protected fetch: (request: Request) => Promise<Response>;
168+
169+
constructor(handler: (request: Request) => Promise<Response>) {
170+
super();
171+
this.fetch = handler;
172+
}
173+
}
174+
175+
export type SandboxTokenServerOptions = Pick<
176+
Request,
177+
'roomName' | 'participantName' | 'roomConfig'
178+
> & {
179+
sandboxId: string;
180+
baseUrl?: string;
181+
loggerName?: string;
182+
183+
/** Disable sandbox security related warning log if TokenSource.Sandbox is used in
184+
* production */
185+
disableSecurityWarning?: boolean;
186+
};
187+
188+
/** TokenSource.SandboxTokenServer queries a sandbox token server for credentials,
189+
* which supports quick prototyping / getting started types of use cases.
190+
*
191+
* This token provider is INSECURE and should NOT be used in production.
192+
*
193+
* For more info:
194+
* @see https://cloud.livekit.io/projects/p_/sandbox/templates/token-server */
195+
export class SandboxTokenServer extends TokenSource {
196+
protected options: SandboxTokenServerOptions;
197+
198+
private log = console;
199+
200+
constructor(options: SandboxTokenServerOptions) {
201+
super();
202+
this.options = options;
203+
204+
// this.log = getLogger(options.loggerName ?? LoggerNames.TokenSource);
205+
206+
if (process.env.NODE_ENV === 'production' && !this.options.disableSecurityWarning) {
207+
// FIXME: figure out a better logging solution?
208+
this.log.warn(
209+
'TokenSource.SandboxTokenServer is meant for development, and is not security hardened. In production, implement your own token generation solution.',
210+
);
211+
}
212+
}
213+
214+
async fetch(request: Request) {
215+
const baseUrl = this.options.baseUrl ?? 'https://cloud-api.livekit.io';
216+
217+
const roomName = this.options.roomName ?? request.roomName;
218+
const participantName = this.options.participantName ?? request.participantName;
219+
const roomConfig = this.options.roomConfig ?? request.roomConfig;
220+
221+
const response = await fetch(`${baseUrl}/api/sandbox/connection-details`, {
222+
method: 'POST',
223+
headers: {
224+
'X-Sandbox-ID': this.options.sandboxId,
225+
'Content-Type': 'application/json',
226+
},
227+
body: JSON.stringify({
228+
roomName,
229+
participantName,
230+
roomConfig: roomConfig?.toJson(),
231+
}),
232+
});
233+
234+
if (!response.ok) {
235+
throw new Error(
236+
`Error generating token from sandbox token server: received ${response.status} / ${await response.text()}`,
237+
);
238+
}
239+
240+
const body: Exclude<Response, 'roomConfig'> = await response.json();
241+
return { ...body, roomConfig };
242+
}
243+
}
244+
}

packages/react/src/hooks/useConversationWith.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Room, RoomEvent, ConnectionState, TrackPublishOptions, Track } from 'li
33
import { EventEmitter } from 'events';
44
import { useCallback, useEffect, useMemo, useState } from 'react';
55

6-
import { ConnectionCredentials as TokenSource } from '../utils/ConnectionCredentialsProvider';
6+
import { TokenSource } from '../TokenSource';
77
import { useMaybeRoomContext } from '../context';
88
import { RoomAgentDispatch, RoomConfiguration } from '@livekit/protocol';
99
import { useAgent } from './useAgent';

packages/react/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ export * from './assets/icons';
1010

1111
export * from './assets/images';
1212

13+
// FIXME: the below is temporary, at least until a new `livekit-client` package version is published
14+
export * from './TokenSource';
15+
1316
// Re-exports from core
1417
export { setLogLevel, setLogExtension, isTrackReference } from '@livekit/components-core';
1518
export type {

0 commit comments

Comments
 (0)