livekit-server-sdk 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -95
- package/dist/AccessToken.d.ts +1 -0
- package/dist/AccessToken.d.ts.map +1 -0
- package/dist/AccessToken.js +3 -0
- package/dist/AccessToken.js.map +1 -1
- package/dist/EgressClient.d.ts +1 -0
- package/dist/EgressClient.d.ts.map +1 -0
- package/dist/EgressClient.js +4 -1
- package/dist/EgressClient.js.map +1 -1
- package/dist/IngressClient.d.ts +1 -0
- package/dist/IngressClient.d.ts.map +1 -0
- package/dist/IngressClient.js +3 -0
- package/dist/IngressClient.js.map +1 -1
- package/dist/RoomServiceClient.d.ts +1 -0
- package/dist/RoomServiceClient.d.ts.map +1 -0
- package/dist/RoomServiceClient.js.map +1 -1
- package/dist/ServiceBase.d.ts +1 -0
- package/dist/ServiceBase.d.ts.map +1 -0
- package/dist/ServiceBase.js +3 -0
- package/dist/ServiceBase.js.map +1 -1
- package/dist/SipClient.d.ts +76 -0
- package/dist/SipClient.d.ts.map +1 -0
- package/dist/SipClient.js +168 -0
- package/dist/SipClient.js.map +1 -0
- package/dist/TwirpRPC.d.ts +1 -0
- package/dist/TwirpRPC.d.ts.map +1 -0
- package/dist/TwirpRPC.js.map +1 -1
- package/dist/WebhookReceiver.d.ts +2 -1
- package/dist/WebhookReceiver.d.ts.map +1 -0
- package/dist/WebhookReceiver.js.map +1 -1
- package/dist/grants.d.ts +2 -1
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +3 -0
- package/dist/grants.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/src/AccessToken.test.ts +96 -0
- package/src/AccessToken.ts +151 -0
- package/src/EgressClient.ts +617 -0
- package/src/IngressClient.ts +257 -0
- package/src/RoomServiceClient.ts +358 -0
- package/src/ServiceBase.ts +35 -0
- package/src/SipClient.ts +277 -0
- package/src/TwirpRPC.ts +54 -0
- package/src/WebhookReceiver.test.ts +44 -0
- package/src/WebhookReceiver.ts +85 -0
- package/src/grants.test.ts +30 -0
- package/src/grants.ts +96 -0
- package/src/index.ts +53 -0
- package/.changeset/README.md +0 -8
- package/.changeset/config.json +0 -11
- package/.eslintrc.cjs +0 -17
- package/.github/banner_dark.png +0 -0
- package/.github/banner_light.png +0 -0
- package/.github/workflows/release.yaml +0 -46
- package/.github/workflows/test.yaml +0 -46
- package/.gitmodules +0 -3
- package/.prettierignore +0 -8
- package/CHANGELOG.md +0 -81
- package/LICENSE +0 -201
- package/NOTICE +0 -13
- package/renovate.json +0 -18
- package/tsconfig.eslint.json +0 -5
- package/vite.config.js +0 -8
package/src/SipClient.ts
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import {
|
|
5
|
+
CreateSIPDispatchRuleRequest,
|
|
6
|
+
CreateSIPParticipantRequest,
|
|
7
|
+
CreateSIPTrunkRequest,
|
|
8
|
+
DeleteSIPDispatchRuleRequest,
|
|
9
|
+
DeleteSIPTrunkRequest,
|
|
10
|
+
ListSIPDispatchRuleRequest,
|
|
11
|
+
ListSIPDispatchRuleResponse,
|
|
12
|
+
ListSIPTrunkRequest,
|
|
13
|
+
ListSIPTrunkResponse,
|
|
14
|
+
SIPDispatchRule,
|
|
15
|
+
SIPDispatchRuleDirect,
|
|
16
|
+
SIPDispatchRuleIndividual,
|
|
17
|
+
SIPDispatchRuleInfo,
|
|
18
|
+
SIPParticipantInfo,
|
|
19
|
+
SIPTrunkInfo,
|
|
20
|
+
} from '@livekit/protocol';
|
|
21
|
+
import ServiceBase from './ServiceBase.js';
|
|
22
|
+
import { Rpc, TwirpRpc, livekitPackage } from './TwirpRPC.js';
|
|
23
|
+
|
|
24
|
+
const svc = 'SIP';
|
|
25
|
+
|
|
26
|
+
export interface CreateSipTrunkOptions {
|
|
27
|
+
name?: string;
|
|
28
|
+
metadata?: string;
|
|
29
|
+
inbound_addresses?: string[];
|
|
30
|
+
inbound_numbers?: string[];
|
|
31
|
+
inbound_username?: string;
|
|
32
|
+
inbound_password?: string;
|
|
33
|
+
outbound_address?: string;
|
|
34
|
+
outbound_username?: string;
|
|
35
|
+
outbound_password?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SipDispatchRuleDirect {
|
|
39
|
+
type: 'direct';
|
|
40
|
+
roomName: string;
|
|
41
|
+
pin?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SipDispatchRuleIndividual {
|
|
45
|
+
type: 'individual';
|
|
46
|
+
roomPrefix: string;
|
|
47
|
+
pin?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface CreateSipDispatchRuleOptions {
|
|
51
|
+
name?: string;
|
|
52
|
+
metadata?: string;
|
|
53
|
+
trunkIds?: string[];
|
|
54
|
+
hidePhoneNumber?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface CreateSipParticipantOptions {
|
|
58
|
+
participantIdentity?: string;
|
|
59
|
+
participantName?: string;
|
|
60
|
+
participantMetadata?: string;
|
|
61
|
+
dtmf?: string;
|
|
62
|
+
playRingtone?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Client to access Egress APIs
|
|
67
|
+
*/
|
|
68
|
+
export class SipClient extends ServiceBase {
|
|
69
|
+
private readonly rpc: Rpc;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param host hostname including protocol. i.e. 'https://cluster.livekit.io'
|
|
73
|
+
* @param apiKey API Key, can be set in env var LIVEKIT_API_KEY
|
|
74
|
+
* @param secret API Secret, can be set in env var LIVEKIT_API_SECRET
|
|
75
|
+
*/
|
|
76
|
+
constructor(host: string, apiKey?: string, secret?: string) {
|
|
77
|
+
super(apiKey, secret);
|
|
78
|
+
this.rpc = new TwirpRpc(host, livekitPackage);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param number phone number of the trunk
|
|
83
|
+
* @param opts CreateSipTrunkOptions
|
|
84
|
+
*/
|
|
85
|
+
async createSipTrunk(number: string, opts?: CreateSipTrunkOptions): Promise<SIPTrunkInfo> {
|
|
86
|
+
let inboundAddresses: string[] | undefined;
|
|
87
|
+
let inboundNumbers: string[] | undefined;
|
|
88
|
+
let inboundUsername: string = '';
|
|
89
|
+
let inboundPassword: string = '';
|
|
90
|
+
let outboundAddress: string = '';
|
|
91
|
+
let outboundUsername: string = '';
|
|
92
|
+
let outboundPassword: string = '';
|
|
93
|
+
let name: string = '';
|
|
94
|
+
let metadata: string = '';
|
|
95
|
+
|
|
96
|
+
if (opts !== undefined) {
|
|
97
|
+
inboundAddresses = opts.inbound_addresses;
|
|
98
|
+
inboundNumbers = opts.inbound_numbers;
|
|
99
|
+
inboundUsername = opts.inbound_username || '';
|
|
100
|
+
inboundPassword = opts.inbound_password || '';
|
|
101
|
+
outboundAddress = opts.outbound_address || '';
|
|
102
|
+
outboundUsername = opts.outbound_username || '';
|
|
103
|
+
outboundPassword = opts.outbound_password || '';
|
|
104
|
+
name = opts.name || '';
|
|
105
|
+
metadata = opts.metadata || '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const req = new CreateSIPTrunkRequest({
|
|
109
|
+
name: name,
|
|
110
|
+
metadata: metadata,
|
|
111
|
+
inboundAddresses: inboundAddresses,
|
|
112
|
+
inboundNumbers: inboundNumbers,
|
|
113
|
+
inboundUsername: inboundUsername,
|
|
114
|
+
inboundPassword: inboundPassword,
|
|
115
|
+
outboundNumber: number,
|
|
116
|
+
outboundAddress: outboundAddress,
|
|
117
|
+
outboundUsername: outboundUsername,
|
|
118
|
+
outboundPassword: outboundPassword,
|
|
119
|
+
}).toJson();
|
|
120
|
+
|
|
121
|
+
const data = await this.rpc.request(svc, 'CreateSIPTrunk', req, await this.authHeader({}));
|
|
122
|
+
return SIPTrunkInfo.fromJson(data, { ignoreUnknownFields: true });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async listSipTrunk(): Promise<Array<SIPTrunkInfo>> {
|
|
126
|
+
const req: Partial<ListSIPTrunkRequest> = {};
|
|
127
|
+
const data = await this.rpc.request(
|
|
128
|
+
svc,
|
|
129
|
+
'ListSIPTrunk',
|
|
130
|
+
new ListSIPTrunkRequest(req).toJson(),
|
|
131
|
+
await this.authHeader({}),
|
|
132
|
+
);
|
|
133
|
+
return ListSIPTrunkResponse.fromJson(data, { ignoreUnknownFields: true }).items ?? [];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param sipTrunkId sip trunk to delete
|
|
138
|
+
*/
|
|
139
|
+
async deleteSipTrunk(sipTrunkId: string): Promise<SIPTrunkInfo> {
|
|
140
|
+
const data = await this.rpc.request(
|
|
141
|
+
svc,
|
|
142
|
+
'DeleteSIPTrunk',
|
|
143
|
+
new DeleteSIPTrunkRequest({ sipTrunkId }).toJson(),
|
|
144
|
+
await this.authHeader({}),
|
|
145
|
+
);
|
|
146
|
+
return SIPTrunkInfo.fromJson(data, { ignoreUnknownFields: true });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param rule sip dispatch rule
|
|
151
|
+
* @param opts CreateSipDispatchRuleOptions
|
|
152
|
+
*/
|
|
153
|
+
async createSipDispatchRule(
|
|
154
|
+
rule: SipDispatchRuleDirect | SipDispatchRuleIndividual,
|
|
155
|
+
opts?: CreateSipDispatchRuleOptions,
|
|
156
|
+
): Promise<SIPDispatchRuleInfo> {
|
|
157
|
+
let trunkIds: string[] | undefined;
|
|
158
|
+
let hidePhoneNumber: boolean = false;
|
|
159
|
+
let name: string = '';
|
|
160
|
+
let metadata: string = '';
|
|
161
|
+
let ruleProto: SIPDispatchRule | undefined = undefined;
|
|
162
|
+
|
|
163
|
+
if (opts !== undefined) {
|
|
164
|
+
trunkIds = opts.trunkIds;
|
|
165
|
+
hidePhoneNumber = opts.hidePhoneNumber || false;
|
|
166
|
+
name = opts.name || '';
|
|
167
|
+
metadata = opts.metadata || '';
|
|
168
|
+
}
|
|
169
|
+
if (rule.type == 'direct') {
|
|
170
|
+
ruleProto = new SIPDispatchRule({
|
|
171
|
+
rule: {
|
|
172
|
+
case: 'dispatchRuleDirect',
|
|
173
|
+
value: new SIPDispatchRuleDirect({
|
|
174
|
+
roomName: rule.roomName,
|
|
175
|
+
pin: rule.pin || '',
|
|
176
|
+
}),
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
} else if (rule.type == 'individual') {
|
|
180
|
+
ruleProto = new SIPDispatchRule({
|
|
181
|
+
rule: {
|
|
182
|
+
case: 'dispatchRuleIndividual',
|
|
183
|
+
value: new SIPDispatchRuleIndividual({
|
|
184
|
+
roomPrefix: rule.roomPrefix,
|
|
185
|
+
pin: rule.pin || '',
|
|
186
|
+
}),
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const req = new CreateSIPDispatchRuleRequest({
|
|
192
|
+
rule: ruleProto,
|
|
193
|
+
trunkIds: trunkIds,
|
|
194
|
+
hidePhoneNumber: hidePhoneNumber,
|
|
195
|
+
name: name,
|
|
196
|
+
metadata: metadata,
|
|
197
|
+
}).toJson();
|
|
198
|
+
|
|
199
|
+
const data = await this.rpc.request(
|
|
200
|
+
svc,
|
|
201
|
+
'CreateSIPDispatchRule',
|
|
202
|
+
req,
|
|
203
|
+
await this.authHeader({}),
|
|
204
|
+
);
|
|
205
|
+
return SIPDispatchRuleInfo.fromJson(data, { ignoreUnknownFields: true });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async listSipDispatchRule(): Promise<Array<SIPDispatchRuleInfo>> {
|
|
209
|
+
const req: Partial<ListSIPDispatchRuleRequest> = {};
|
|
210
|
+
const data = await this.rpc.request(
|
|
211
|
+
svc,
|
|
212
|
+
'ListSIPDispatchRule',
|
|
213
|
+
new ListSIPDispatchRuleRequest(req).toJson(),
|
|
214
|
+
await this.authHeader({}),
|
|
215
|
+
);
|
|
216
|
+
return ListSIPDispatchRuleResponse.fromJson(data, { ignoreUnknownFields: true }).items ?? [];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* @param sipDispatchRuleId sip trunk to delete
|
|
221
|
+
*/
|
|
222
|
+
async deleteSipDispatchRule(sipDispatchRuleId: string): Promise<SIPDispatchRuleInfo> {
|
|
223
|
+
const data = await this.rpc.request(
|
|
224
|
+
svc,
|
|
225
|
+
'DeleteSIPDispatchRule',
|
|
226
|
+
new DeleteSIPDispatchRuleRequest({ sipDispatchRuleId }).toJson(),
|
|
227
|
+
await this.authHeader({}),
|
|
228
|
+
);
|
|
229
|
+
return SIPDispatchRuleInfo.fromJson(data, { ignoreUnknownFields: true });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* @param sipTrunkId sip trunk to use for the call
|
|
234
|
+
* @param number number to dial
|
|
235
|
+
* @param roomName room to attach the call to
|
|
236
|
+
* @param opts CreateSipParticipantOptions
|
|
237
|
+
*/
|
|
238
|
+
async createSipParticipant(
|
|
239
|
+
sipTrunkId: string,
|
|
240
|
+
number: string,
|
|
241
|
+
roomName: string,
|
|
242
|
+
opts?: CreateSipParticipantOptions,
|
|
243
|
+
): Promise<SIPParticipantInfo> {
|
|
244
|
+
let participantIdentity: string = '';
|
|
245
|
+
let participantName: string = '';
|
|
246
|
+
let participantMetadata: string = '';
|
|
247
|
+
let dtmf: string = '';
|
|
248
|
+
let playRingtone: boolean = false;
|
|
249
|
+
|
|
250
|
+
if (opts !== undefined) {
|
|
251
|
+
participantIdentity = opts.participantIdentity || '';
|
|
252
|
+
participantName = opts.participantName || '';
|
|
253
|
+
participantMetadata = opts.participantMetadata || '';
|
|
254
|
+
dtmf = opts.dtmf || '';
|
|
255
|
+
playRingtone = opts.playRingtone || false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const req = new CreateSIPParticipantRequest({
|
|
259
|
+
sipTrunkId: sipTrunkId,
|
|
260
|
+
sipCallTo: number,
|
|
261
|
+
roomName: roomName,
|
|
262
|
+
participantIdentity: participantIdentity,
|
|
263
|
+
participantName: participantName,
|
|
264
|
+
participantMetadata: participantMetadata,
|
|
265
|
+
dtmf: dtmf,
|
|
266
|
+
playRingtone: playRingtone,
|
|
267
|
+
}).toJson();
|
|
268
|
+
|
|
269
|
+
const data = await this.rpc.request(
|
|
270
|
+
svc,
|
|
271
|
+
'CreateSIPParticipant',
|
|
272
|
+
req,
|
|
273
|
+
await this.authHeader({}),
|
|
274
|
+
);
|
|
275
|
+
return SIPParticipantInfo.fromJson(data, { ignoreUnknownFields: true });
|
|
276
|
+
}
|
|
277
|
+
}
|
package/src/TwirpRPC.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import type { JsonValue } from '@bufbuild/protobuf';
|
|
5
|
+
import camelcaseKeys from 'camelcase-keys';
|
|
6
|
+
|
|
7
|
+
// twirp RPC adapter for client implementation
|
|
8
|
+
|
|
9
|
+
const defaultPrefix = '/twirp';
|
|
10
|
+
|
|
11
|
+
export const livekitPackage = 'livekit';
|
|
12
|
+
export interface Rpc {
|
|
13
|
+
request(service: string, method: string, data: JsonValue, headers?: any): Promise<string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* JSON based Twirp V7 RPC
|
|
18
|
+
*/
|
|
19
|
+
export class TwirpRpc {
|
|
20
|
+
host: string;
|
|
21
|
+
|
|
22
|
+
pkg: string;
|
|
23
|
+
|
|
24
|
+
prefix: string;
|
|
25
|
+
|
|
26
|
+
constructor(host: string, pkg: string, prefix?: string) {
|
|
27
|
+
if (host.startsWith('ws')) {
|
|
28
|
+
host = host.replace('ws', 'http');
|
|
29
|
+
}
|
|
30
|
+
this.host = host;
|
|
31
|
+
this.pkg = pkg;
|
|
32
|
+
this.prefix = prefix || defaultPrefix;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async request(service: string, method: string, data: any, headers?: any): Promise<any> {
|
|
36
|
+
const path = `${this.prefix}/${this.pkg}.${service}/${method}`;
|
|
37
|
+
const url = new URL(path, this.host);
|
|
38
|
+
|
|
39
|
+
const response = await fetch(url, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: {
|
|
42
|
+
'Content-Type': 'application/json;charset=UTF-8',
|
|
43
|
+
...headers,
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify(data),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(`Request failed with status ${response.status}: ${response.statusText}`);
|
|
50
|
+
}
|
|
51
|
+
const parsedResp = await response.json();
|
|
52
|
+
return camelcaseKeys(parsedResp, { deep: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { AccessToken } from './AccessToken.js';
|
|
6
|
+
import { WebhookEvent, WebhookReceiver } from './WebhookReceiver.js';
|
|
7
|
+
|
|
8
|
+
const testApiKey = 'abcdefg';
|
|
9
|
+
const testSecret = 'abababa';
|
|
10
|
+
|
|
11
|
+
describe('webhook receiver', () => {
|
|
12
|
+
const body =
|
|
13
|
+
'{"event":"room_started", "room":{"sid":"RM_TkVjUvAqgzKz", "name":"mytestroom", "emptyTimeout":300, "creationTime":"1628545903", "turnPassword":"ICkSr2rEeslkN6e9bXL4Ji5zzMD5Z7zzr6ulOaxMj6N", "enabledCodecs":[{"mime":"audio/opus"}, {"mime":"video/VP8"}]}}';
|
|
14
|
+
const sha = 'CoEQz1chqJ9bnZRcORddjplkvpjmPujmLTR42DbefYI=';
|
|
15
|
+
const t = new AccessToken(testApiKey, testSecret);
|
|
16
|
+
t.sha256 = sha;
|
|
17
|
+
|
|
18
|
+
const receiver = new WebhookReceiver(testApiKey, testSecret);
|
|
19
|
+
|
|
20
|
+
it('should receive and decode WebhookEvent', async () => {
|
|
21
|
+
const token = await t.toJwt();
|
|
22
|
+
const event = await receiver.receive(body, token);
|
|
23
|
+
expect(event).toBeTruthy();
|
|
24
|
+
expect(event.room?.name).toBe('mytestroom');
|
|
25
|
+
expect(event.event).toBe('room_started');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('decoding json payload', () => {
|
|
30
|
+
it('should allow server to return extra fields', () => {
|
|
31
|
+
const obj = {
|
|
32
|
+
type: 'room_started',
|
|
33
|
+
room: {
|
|
34
|
+
sid: 'RM_TkVjUvAqgzKz',
|
|
35
|
+
name: 'mytestroom',
|
|
36
|
+
},
|
|
37
|
+
extra: 'extra',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const event = new WebhookEvent(obj);
|
|
41
|
+
expect(event).toBeTruthy();
|
|
42
|
+
expect(event.room?.name).toBe('mytestroom');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import type { BinaryReadOptions, JsonReadOptions, JsonValue } from '@bufbuild/protobuf';
|
|
5
|
+
import { WebhookEvent as ProtoWebhookEvent } from '@livekit/protocol';
|
|
6
|
+
import { TokenVerifier } from './AccessToken.js';
|
|
7
|
+
|
|
8
|
+
export const authorizeHeader = 'Authorize';
|
|
9
|
+
|
|
10
|
+
export class WebhookEvent extends ProtoWebhookEvent {
|
|
11
|
+
event: WebhookEventNames = '';
|
|
12
|
+
|
|
13
|
+
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): WebhookEvent {
|
|
14
|
+
return new WebhookEvent().fromBinary(bytes, options);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): WebhookEvent {
|
|
18
|
+
return new WebhookEvent().fromJson(jsonValue, options);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): WebhookEvent {
|
|
22
|
+
return new WebhookEvent().fromJsonString(jsonString, options);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type WebhookEventNames =
|
|
27
|
+
| 'room_started'
|
|
28
|
+
| 'room_finished'
|
|
29
|
+
| 'participant_joined'
|
|
30
|
+
| 'participant_left'
|
|
31
|
+
| 'track_published'
|
|
32
|
+
| 'track_unpublished'
|
|
33
|
+
| 'egress_started'
|
|
34
|
+
| 'egress_updated'
|
|
35
|
+
| 'egress_ended'
|
|
36
|
+
| 'ingress_started'
|
|
37
|
+
| 'ingress_ended'
|
|
38
|
+
/**
|
|
39
|
+
* @internal
|
|
40
|
+
* @note only used as a default value, not a valid webhook event
|
|
41
|
+
*/
|
|
42
|
+
| '';
|
|
43
|
+
|
|
44
|
+
export class WebhookReceiver {
|
|
45
|
+
private verifier: TokenVerifier;
|
|
46
|
+
|
|
47
|
+
constructor(apiKey: string, apiSecret: string) {
|
|
48
|
+
this.verifier = new TokenVerifier(apiKey, apiSecret);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
*
|
|
53
|
+
* @param body string of the posted body
|
|
54
|
+
* @param authHeader `Authorization` header from the request
|
|
55
|
+
* @param skipAuth true to skip auth validation
|
|
56
|
+
* @returns
|
|
57
|
+
*/
|
|
58
|
+
async receive(
|
|
59
|
+
body: string,
|
|
60
|
+
authHeader?: string,
|
|
61
|
+
skipAuth: boolean = false,
|
|
62
|
+
): Promise<WebhookEvent> {
|
|
63
|
+
// verify token
|
|
64
|
+
if (!skipAuth) {
|
|
65
|
+
if (!authHeader) {
|
|
66
|
+
throw new Error('authorization header is empty');
|
|
67
|
+
}
|
|
68
|
+
const claims = await this.verifier.verify(authHeader);
|
|
69
|
+
// confirm sha
|
|
70
|
+
const encoder = new TextEncoder();
|
|
71
|
+
const hash = await crypto.subtle.digest('SHA-256', encoder.encode(body));
|
|
72
|
+
const hashDecoded = btoa(
|
|
73
|
+
Array.from(new Uint8Array(hash))
|
|
74
|
+
.map((v) => String.fromCharCode(v))
|
|
75
|
+
.join(''),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (claims.sha256 !== hashDecoded) {
|
|
79
|
+
throw new Error('sha256 checksum of body does not match');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return WebhookEvent.fromJson(JSON.parse(body), { ignoreUnknownFields: true });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import { TrackSource } from '@livekit/protocol';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import { ClaimGrants, VideoGrant, claimsToJwtPayload } from './grants';
|
|
7
|
+
|
|
8
|
+
describe('ClaimGrants are parsed correctly', () => {
|
|
9
|
+
it('parses TrackSource correctly to strings', () => {
|
|
10
|
+
const grant: VideoGrant = {
|
|
11
|
+
canPublishSources: [
|
|
12
|
+
TrackSource.CAMERA,
|
|
13
|
+
TrackSource.MICROPHONE,
|
|
14
|
+
TrackSource.SCREEN_SHARE,
|
|
15
|
+
TrackSource.SCREEN_SHARE_AUDIO,
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const claim: ClaimGrants = { video: grant };
|
|
20
|
+
|
|
21
|
+
const jwtPayload = claimsToJwtPayload(claim);
|
|
22
|
+
expect(jwtPayload.video).toBeTypeOf('object');
|
|
23
|
+
expect(jwtPayload.video?.canPublishSources).toEqual([
|
|
24
|
+
'camera',
|
|
25
|
+
'microphone',
|
|
26
|
+
'screen_share',
|
|
27
|
+
'screen_share_audio',
|
|
28
|
+
]);
|
|
29
|
+
});
|
|
30
|
+
});
|
package/src/grants.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import { TrackSource } from '@livekit/protocol';
|
|
5
|
+
import { JWTPayload } from 'jose';
|
|
6
|
+
|
|
7
|
+
export function trackSourceToString(source: TrackSource) {
|
|
8
|
+
switch (source) {
|
|
9
|
+
case TrackSource.CAMERA:
|
|
10
|
+
return 'camera';
|
|
11
|
+
case TrackSource.MICROPHONE:
|
|
12
|
+
return 'microphone';
|
|
13
|
+
case TrackSource.SCREEN_SHARE:
|
|
14
|
+
return 'screen_share';
|
|
15
|
+
case TrackSource.SCREEN_SHARE_AUDIO:
|
|
16
|
+
return 'screen_share_audio';
|
|
17
|
+
default:
|
|
18
|
+
throw new TypeError(`Cannot convert TrackSource ${source} to string`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function claimsToJwtPayload(
|
|
23
|
+
grant: ClaimGrants,
|
|
24
|
+
): JWTPayload & { video?: Record<string, unknown> } {
|
|
25
|
+
const claim: Record<string, any> = { ...grant };
|
|
26
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
27
|
+
if (Array.isArray(claim.video?.canPublishSources)) {
|
|
28
|
+
claim.video.canPublishSources = claim.video.canPublishSources.map(trackSourceToString);
|
|
29
|
+
}
|
|
30
|
+
return claim;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface VideoGrant {
|
|
34
|
+
/** permission to create a room */
|
|
35
|
+
roomCreate?: boolean;
|
|
36
|
+
|
|
37
|
+
/** permission to join a room as a participant, room must be set */
|
|
38
|
+
roomJoin?: boolean;
|
|
39
|
+
|
|
40
|
+
/** permission to list rooms */
|
|
41
|
+
roomList?: boolean;
|
|
42
|
+
|
|
43
|
+
/** permission to start a recording */
|
|
44
|
+
roomRecord?: boolean;
|
|
45
|
+
|
|
46
|
+
/** permission to control a specific room, room must be set */
|
|
47
|
+
roomAdmin?: boolean;
|
|
48
|
+
|
|
49
|
+
/** name of the room, must be set for admin or join permissions */
|
|
50
|
+
room?: string;
|
|
51
|
+
|
|
52
|
+
/** permissions to control ingress, not specific to any room or ingress */
|
|
53
|
+
ingressAdmin?: boolean;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* allow participant to publish. If neither canPublish or canSubscribe is set,
|
|
57
|
+
* both publish and subscribe are enabled
|
|
58
|
+
*/
|
|
59
|
+
canPublish?: boolean;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* TrackSource types that the participant is allowed to publish
|
|
63
|
+
* When set, it supersedes CanPublish. Only sources explicitly set here can be published
|
|
64
|
+
*/
|
|
65
|
+
canPublishSources?: TrackSource[];
|
|
66
|
+
|
|
67
|
+
/** allow participant to subscribe to other tracks */
|
|
68
|
+
canSubscribe?: boolean;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* allow participants to publish data, defaults to true if not set
|
|
72
|
+
*/
|
|
73
|
+
canPublishData?: boolean;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* by default, a participant is not allowed to update its own metadata
|
|
77
|
+
*/
|
|
78
|
+
canUpdateOwnMetadata?: boolean;
|
|
79
|
+
|
|
80
|
+
/** participant isn't visible to others */
|
|
81
|
+
hidden?: boolean;
|
|
82
|
+
|
|
83
|
+
/** participant is recording the room, when set, allows room to indicate it's being recorded */
|
|
84
|
+
recorder?: boolean;
|
|
85
|
+
|
|
86
|
+
/** participant acts as a server side participant/bot/agent */
|
|
87
|
+
agent?: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** @internal */
|
|
91
|
+
export interface ClaimGrants extends JWTPayload {
|
|
92
|
+
name?: string;
|
|
93
|
+
video?: VideoGrant;
|
|
94
|
+
metadata?: string;
|
|
95
|
+
sha256?: string;
|
|
96
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
export * from './AccessToken.js';
|
|
6
|
+
export * from './EgressClient.js';
|
|
7
|
+
export * from './IngressClient.js';
|
|
8
|
+
export * from './SipClient.js';
|
|
9
|
+
export * from './RoomServiceClient.js';
|
|
10
|
+
export * from './WebhookReceiver.js';
|
|
11
|
+
export * from './grants.js';
|
|
12
|
+
export {
|
|
13
|
+
AliOSSUpload,
|
|
14
|
+
AzureBlobUpload,
|
|
15
|
+
DirectFileOutput,
|
|
16
|
+
EgressInfo,
|
|
17
|
+
EncodedFileOutput,
|
|
18
|
+
EncodedFileType,
|
|
19
|
+
EncodingOptions,
|
|
20
|
+
EncodingOptionsPreset,
|
|
21
|
+
GCPUpload,
|
|
22
|
+
ImageOutput,
|
|
23
|
+
ParticipantEgressRequest,
|
|
24
|
+
RoomCompositeEgressRequest,
|
|
25
|
+
S3Upload,
|
|
26
|
+
SegmentedFileOutput,
|
|
27
|
+
SegmentedFileProtocol,
|
|
28
|
+
StreamOutput,
|
|
29
|
+
StreamProtocol,
|
|
30
|
+
TrackCompositeEgressRequest,
|
|
31
|
+
TrackEgressRequest,
|
|
32
|
+
WebEgressRequest,
|
|
33
|
+
IngressAudioEncodingOptions,
|
|
34
|
+
IngressAudioEncodingPreset,
|
|
35
|
+
IngressAudioOptions,
|
|
36
|
+
IngressInfo,
|
|
37
|
+
IngressInput,
|
|
38
|
+
IngressState,
|
|
39
|
+
IngressVideoEncodingOptions,
|
|
40
|
+
IngressVideoEncodingPreset,
|
|
41
|
+
IngressVideoOptions,
|
|
42
|
+
DataPacket_Kind,
|
|
43
|
+
ParticipantInfo,
|
|
44
|
+
ParticipantInfo_State,
|
|
45
|
+
ParticipantPermission,
|
|
46
|
+
Room,
|
|
47
|
+
TrackInfo,
|
|
48
|
+
TrackType,
|
|
49
|
+
TrackSource,
|
|
50
|
+
SIPTrunkInfo,
|
|
51
|
+
SIPDispatchRuleInfo,
|
|
52
|
+
SIPParticipantInfo,
|
|
53
|
+
} from '@livekit/protocol';
|
package/.changeset/README.md
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
# Changesets
|
|
2
|
-
|
|
3
|
-
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
|
4
|
-
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
|
5
|
-
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
|
6
|
-
|
|
7
|
-
We have a quick list of common questions to get you started engaging with this project in
|
|
8
|
-
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
package/.changeset/config.json
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json",
|
|
3
|
-
"changelog": ["@livekit/changesets-changelog-github", { "repo": "livekit/server-sdk-js" }],
|
|
4
|
-
"commit": false,
|
|
5
|
-
"fixed": [],
|
|
6
|
-
"linked": [],
|
|
7
|
-
"access": "public",
|
|
8
|
-
"baseBranch": "main",
|
|
9
|
-
"updateInternalDependencies": "patch",
|
|
10
|
-
"ignore": []
|
|
11
|
-
}
|
package/.eslintrc.cjs
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
extends: ['plugin:import/recommended', 'airbnb-typescript/base', 'prettier'],
|
|
3
|
-
parserOptions: {
|
|
4
|
-
project: './tsconfig.eslint.json',
|
|
5
|
-
},
|
|
6
|
-
ignorePatterns: ['src/proto', 'docs/', 'dist/', 'examples'],
|
|
7
|
-
rules: {
|
|
8
|
-
'import/export': 'off',
|
|
9
|
-
'max-classes-per-file': 'off',
|
|
10
|
-
'no-param-reassign': 'off',
|
|
11
|
-
'no-await-in-loop': 'off',
|
|
12
|
-
'consistent-return': 'off',
|
|
13
|
-
'class-methods-use-this': 'off',
|
|
14
|
-
'@typescript-eslint/no-use-before-define': 'off',
|
|
15
|
-
'no-restricted-syntax': ['error', 'WithStatement', "BinaryExpression[operator='in']"],
|
|
16
|
-
},
|
|
17
|
-
};
|
package/.github/banner_dark.png
DELETED
|
Binary file
|
package/.github/banner_light.png
DELETED
|
Binary file
|