livekit-server-sdk 2.3.0 → 2.5.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.
Files changed (66) hide show
  1. package/README.md +80 -95
  2. package/dist/AccessToken.d.ts +15 -3
  3. package/dist/AccessToken.d.ts.map +1 -0
  4. package/dist/AccessToken.js +32 -3
  5. package/dist/AccessToken.js.map +1 -1
  6. package/dist/EgressClient.d.ts +1 -0
  7. package/dist/EgressClient.d.ts.map +1 -0
  8. package/dist/EgressClient.js +5 -2
  9. package/dist/EgressClient.js.map +1 -1
  10. package/dist/IngressClient.d.ts +1 -0
  11. package/dist/IngressClient.d.ts.map +1 -0
  12. package/dist/IngressClient.js +3 -0
  13. package/dist/IngressClient.js.map +1 -1
  14. package/dist/RoomServiceClient.d.ts +4 -2
  15. package/dist/RoomServiceClient.d.ts.map +1 -0
  16. package/dist/RoomServiceClient.js.map +1 -1
  17. package/dist/ServiceBase.d.ts +3 -2
  18. package/dist/ServiceBase.d.ts.map +1 -0
  19. package/dist/ServiceBase.js +7 -1
  20. package/dist/ServiceBase.js.map +1 -1
  21. package/dist/SipClient.d.ts +43 -1
  22. package/dist/SipClient.d.ts.map +1 -0
  23. package/dist/SipClient.js +111 -11
  24. package/dist/SipClient.js.map +1 -1
  25. package/dist/TwirpRPC.d.ts +1 -0
  26. package/dist/TwirpRPC.d.ts.map +1 -0
  27. package/dist/TwirpRPC.js.map +1 -1
  28. package/dist/WebhookReceiver.d.ts +2 -1
  29. package/dist/WebhookReceiver.d.ts.map +1 -0
  30. package/dist/WebhookReceiver.js.map +1 -1
  31. package/dist/grants.d.ts +12 -2
  32. package/dist/grants.d.ts.map +1 -0
  33. package/dist/grants.js +3 -0
  34. package/dist/grants.js.map +1 -1
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +3 -0
  38. package/dist/index.js.map +1 -1
  39. package/package.json +7 -8
  40. package/src/AccessToken.test.ts +100 -0
  41. package/src/AccessToken.ts +184 -0
  42. package/src/EgressClient.ts +617 -0
  43. package/src/IngressClient.ts +257 -0
  44. package/src/RoomServiceClient.ts +359 -0
  45. package/src/ServiceBase.ts +38 -0
  46. package/src/SipClient.ts +427 -0
  47. package/src/TwirpRPC.ts +54 -0
  48. package/src/WebhookReceiver.test.ts +44 -0
  49. package/src/WebhookReceiver.ts +85 -0
  50. package/src/grants.test.ts +30 -0
  51. package/src/grants.ts +107 -0
  52. package/src/index.ts +53 -0
  53. package/.changeset/README.md +0 -8
  54. package/.changeset/config.json +0 -11
  55. package/.eslintrc.cjs +0 -17
  56. package/.github/banner_dark.png +0 -0
  57. package/.github/banner_light.png +0 -0
  58. package/.github/workflows/release.yaml +0 -46
  59. package/.github/workflows/test.yaml +0 -46
  60. package/.gitmodules +0 -3
  61. package/.prettierignore +0 -8
  62. package/CHANGELOG.md +0 -87
  63. package/NOTICE +0 -13
  64. package/renovate.json +0 -18
  65. package/tsconfig.eslint.json +0 -5
  66. package/vite.config.js +0 -8
@@ -0,0 +1,427 @@
1
+ // SPDX-FileCopyrightText: 2024 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import {
5
+ CreateSIPDispatchRuleRequest,
6
+ CreateSIPInboundTrunkRequest,
7
+ CreateSIPOutboundTrunkRequest,
8
+ CreateSIPParticipantRequest,
9
+ CreateSIPTrunkRequest,
10
+ DeleteSIPDispatchRuleRequest,
11
+ DeleteSIPTrunkRequest,
12
+ ListSIPDispatchRuleRequest,
13
+ ListSIPDispatchRuleResponse,
14
+ ListSIPInboundTrunkRequest,
15
+ ListSIPInboundTrunkResponse,
16
+ ListSIPOutboundTrunkRequest,
17
+ ListSIPOutboundTrunkResponse,
18
+ ListSIPTrunkRequest,
19
+ ListSIPTrunkResponse,
20
+ SIPDispatchRule,
21
+ SIPDispatchRuleDirect,
22
+ SIPDispatchRuleIndividual,
23
+ SIPDispatchRuleInfo,
24
+ SIPInboundTrunkInfo,
25
+ SIPOutboundTrunkInfo,
26
+ SIPParticipantInfo,
27
+ SIPTransport,
28
+ SIPTrunkInfo,
29
+ } from '@livekit/protocol';
30
+ import ServiceBase from './ServiceBase.js';
31
+ import { Rpc, TwirpRpc, livekitPackage } from './TwirpRPC.js';
32
+
33
+ const svc = 'SIP';
34
+
35
+ /**
36
+ * @deprecated use CreateSipInboundTrunkOptions or CreateSipOutboundTrunkOptions
37
+ */
38
+ export interface CreateSipTrunkOptions {
39
+ name?: string;
40
+ metadata?: string;
41
+ inbound_addresses?: string[];
42
+ inbound_numbers?: string[];
43
+ inbound_username?: string;
44
+ inbound_password?: string;
45
+ outbound_address?: string;
46
+ outbound_username?: string;
47
+ outbound_password?: string;
48
+ }
49
+ export interface CreateSipInboundTrunkOptions {
50
+ metadata?: string;
51
+ allowed_addresses?: string[];
52
+ allowed_numbers?: string[];
53
+ auth_username?: string;
54
+ auth_password?: string;
55
+ }
56
+ export interface CreateSipOutboundTrunkOptions {
57
+ metadata?: string;
58
+ transport: SIPTransport;
59
+ auth_username?: string;
60
+ auth_password?: string;
61
+ }
62
+
63
+ export interface SipDispatchRuleDirect {
64
+ type: 'direct';
65
+ roomName: string;
66
+ pin?: string;
67
+ }
68
+
69
+ export interface SipDispatchRuleIndividual {
70
+ type: 'individual';
71
+ roomPrefix: string;
72
+ pin?: string;
73
+ }
74
+
75
+ export interface CreateSipDispatchRuleOptions {
76
+ name?: string;
77
+ metadata?: string;
78
+ trunkIds?: string[];
79
+ hidePhoneNumber?: boolean;
80
+ }
81
+
82
+ export interface CreateSipParticipantOptions {
83
+ participantIdentity?: string;
84
+ participantName?: string;
85
+ participantMetadata?: string;
86
+ dtmf?: string;
87
+ playRingtone?: boolean;
88
+ hidePhoneNumber?: boolean;
89
+ }
90
+
91
+ /**
92
+ * Client to access Egress APIs
93
+ */
94
+ export class SipClient extends ServiceBase {
95
+ private readonly rpc: Rpc;
96
+
97
+ /**
98
+ * @param host hostname including protocol. i.e. 'https://cluster.livekit.io'
99
+ * @param apiKey API Key, can be set in env var LIVEKIT_API_KEY
100
+ * @param secret API Secret, can be set in env var LIVEKIT_API_SECRET
101
+ */
102
+ constructor(host: string, apiKey?: string, secret?: string) {
103
+ super(apiKey, secret);
104
+ this.rpc = new TwirpRpc(host, livekitPackage);
105
+ }
106
+
107
+ /**
108
+ * @param number phone number of the trunk
109
+ * @param opts CreateSipTrunkOptions
110
+ * @deprecated use createSipInboundTrunk or createSipOutboundTrunk
111
+ */
112
+ async createSipTrunk(number: string, opts?: CreateSipTrunkOptions): Promise<SIPTrunkInfo> {
113
+ let inboundAddresses: string[] | undefined;
114
+ let inboundNumbers: string[] | undefined;
115
+ let inboundUsername: string = '';
116
+ let inboundPassword: string = '';
117
+ let outboundAddress: string = '';
118
+ let outboundUsername: string = '';
119
+ let outboundPassword: string = '';
120
+ let name: string = '';
121
+ let metadata: string = '';
122
+
123
+ if (opts !== undefined) {
124
+ inboundAddresses = opts.inbound_addresses;
125
+ inboundNumbers = opts.inbound_numbers;
126
+ inboundUsername = opts.inbound_username || '';
127
+ inboundPassword = opts.inbound_password || '';
128
+ outboundAddress = opts.outbound_address || '';
129
+ outboundUsername = opts.outbound_username || '';
130
+ outboundPassword = opts.outbound_password || '';
131
+ name = opts.name || '';
132
+ metadata = opts.metadata || '';
133
+ }
134
+
135
+ const req = new CreateSIPTrunkRequest({
136
+ name: name,
137
+ metadata: metadata,
138
+ inboundAddresses: inboundAddresses,
139
+ inboundNumbers: inboundNumbers,
140
+ inboundUsername: inboundUsername,
141
+ inboundPassword: inboundPassword,
142
+ outboundNumber: number,
143
+ outboundAddress: outboundAddress,
144
+ outboundUsername: outboundUsername,
145
+ outboundPassword: outboundPassword,
146
+ }).toJson();
147
+
148
+ const data = await this.rpc.request(
149
+ svc,
150
+ 'CreateSIPTrunk',
151
+ req,
152
+ await this.authHeader({}, { admin: true }),
153
+ );
154
+ return SIPTrunkInfo.fromJson(data, { ignoreUnknownFields: true });
155
+ }
156
+
157
+ /**
158
+ @param name human-readable name of the trunk
159
+ * @param numbers phone numbers of the trunk
160
+ * @param opts CreateSipTrunkOptions
161
+ */
162
+ async createSipInboundTrunk(
163
+ name: string,
164
+ numbers: string[],
165
+ opts?: CreateSipInboundTrunkOptions,
166
+ ): Promise<SIPInboundTrunkInfo> {
167
+ let allowedAddresses: string[] | undefined;
168
+ let allowedNumbers: string[] | undefined;
169
+ let authUsername: string = '';
170
+ let authPassword: string = '';
171
+ let metadata: string = '';
172
+
173
+ if (opts !== undefined) {
174
+ allowedAddresses = opts.allowed_addresses;
175
+ allowedNumbers = opts.allowed_numbers;
176
+ authUsername = opts.auth_username || '';
177
+ authPassword = opts.auth_password || '';
178
+ metadata = opts.metadata || '';
179
+ }
180
+
181
+ const req = new CreateSIPInboundTrunkRequest({
182
+ trunk: new SIPInboundTrunkInfo({
183
+ name: name,
184
+ numbers: numbers,
185
+ metadata: metadata,
186
+ allowedAddresses: allowedAddresses,
187
+ allowedNumbers: allowedNumbers,
188
+ authUsername: authUsername,
189
+ authPassword: authPassword,
190
+ }),
191
+ }).toJson();
192
+
193
+ const data = await this.rpc.request(
194
+ svc,
195
+ 'CreateSIPInboundTrunk',
196
+ req,
197
+ await this.authHeader({}, { admin: true }),
198
+ );
199
+ return SIPInboundTrunkInfo.fromJson(data, { ignoreUnknownFields: true });
200
+ }
201
+
202
+ /**
203
+ * @param name human-readable name of the trunk
204
+ * @param address hostname and port of the SIP server to dial
205
+ * @param numbers phone numbers of the trunk
206
+ * @param opts CreateSipTrunkOptions
207
+ */
208
+ async createSipOutboundTrunk(
209
+ name: string,
210
+ address: string,
211
+ numbers: string[],
212
+ opts?: CreateSipOutboundTrunkOptions,
213
+ ): Promise<SIPOutboundTrunkInfo> {
214
+ let authUsername: string = '';
215
+ let authPassword: string = '';
216
+ let transport: SIPTransport = SIPTransport.SIP_TRANSPORT_AUTO;
217
+ let metadata: string = '';
218
+
219
+ if (opts !== undefined) {
220
+ authUsername = opts.auth_username || '';
221
+ authPassword = opts.auth_password || '';
222
+ transport = opts.transport || SIPTransport.SIP_TRANSPORT_AUTO;
223
+ metadata = opts.metadata || '';
224
+ }
225
+
226
+ const req = new CreateSIPOutboundTrunkRequest({
227
+ trunk: new SIPOutboundTrunkInfo({
228
+ name: name,
229
+ address: address,
230
+ numbers: numbers,
231
+ metadata: metadata,
232
+ transport: transport,
233
+ authUsername: authUsername,
234
+ authPassword: authPassword,
235
+ }),
236
+ }).toJson();
237
+
238
+ const data = await this.rpc.request(
239
+ svc,
240
+ 'CreateSIPOutboundTrunk',
241
+ req,
242
+ await this.authHeader({}, { admin: true }),
243
+ );
244
+ return SIPOutboundTrunkInfo.fromJson(data, { ignoreUnknownFields: true });
245
+ }
246
+
247
+ /**
248
+ * @deprecated use listSipInboundTrunk or listSipOutboundTrunk
249
+ */
250
+ async listSipTrunk(): Promise<Array<SIPTrunkInfo>> {
251
+ const req: Partial<ListSIPTrunkRequest> = {};
252
+ const data = await this.rpc.request(
253
+ svc,
254
+ 'ListSIPTrunk',
255
+ new ListSIPTrunkRequest(req).toJson(),
256
+ await this.authHeader({}, { admin: true }),
257
+ );
258
+ return ListSIPTrunkResponse.fromJson(data, { ignoreUnknownFields: true }).items ?? [];
259
+ }
260
+
261
+ async listSipInboundTrunk(): Promise<Array<SIPInboundTrunkInfo>> {
262
+ const req: Partial<ListSIPInboundTrunkRequest> = {};
263
+ const data = await this.rpc.request(
264
+ svc,
265
+ 'ListSIPInboundTrunk',
266
+ new ListSIPInboundTrunkRequest(req).toJson(),
267
+ await this.authHeader({}, { admin: true }),
268
+ );
269
+ return ListSIPInboundTrunkResponse.fromJson(data, { ignoreUnknownFields: true }).items ?? [];
270
+ }
271
+
272
+ async listSipOutboundTrunk(): Promise<Array<SIPOutboundTrunkInfo>> {
273
+ const req: Partial<ListSIPOutboundTrunkRequest> = {};
274
+ const data = await this.rpc.request(
275
+ svc,
276
+ 'ListSIPOutboundTrunk',
277
+ new ListSIPOutboundTrunkRequest(req).toJson(),
278
+ await this.authHeader({}, { admin: true }),
279
+ );
280
+ return ListSIPOutboundTrunkResponse.fromJson(data, { ignoreUnknownFields: true }).items ?? [];
281
+ }
282
+
283
+ /**
284
+ * @param sipTrunkId sip trunk to delete
285
+ */
286
+ async deleteSipTrunk(sipTrunkId: string): Promise<SIPTrunkInfo> {
287
+ const data = await this.rpc.request(
288
+ svc,
289
+ 'DeleteSIPTrunk',
290
+ new DeleteSIPTrunkRequest({ sipTrunkId }).toJson(),
291
+ await this.authHeader({}, { admin: true }),
292
+ );
293
+ return SIPTrunkInfo.fromJson(data, { ignoreUnknownFields: true });
294
+ }
295
+
296
+ /**
297
+ * @param rule sip dispatch rule
298
+ * @param opts CreateSipDispatchRuleOptions
299
+ */
300
+ async createSipDispatchRule(
301
+ rule: SipDispatchRuleDirect | SipDispatchRuleIndividual,
302
+ opts?: CreateSipDispatchRuleOptions,
303
+ ): Promise<SIPDispatchRuleInfo> {
304
+ let trunkIds: string[] | undefined;
305
+ let hidePhoneNumber: boolean = false;
306
+ let name: string = '';
307
+ let metadata: string = '';
308
+ let ruleProto: SIPDispatchRule | undefined = undefined;
309
+
310
+ if (opts !== undefined) {
311
+ trunkIds = opts.trunkIds;
312
+ hidePhoneNumber = opts.hidePhoneNumber || false;
313
+ name = opts.name || '';
314
+ metadata = opts.metadata || '';
315
+ }
316
+ if (rule.type == 'direct') {
317
+ ruleProto = new SIPDispatchRule({
318
+ rule: {
319
+ case: 'dispatchRuleDirect',
320
+ value: new SIPDispatchRuleDirect({
321
+ roomName: rule.roomName,
322
+ pin: rule.pin || '',
323
+ }),
324
+ },
325
+ });
326
+ } else if (rule.type == 'individual') {
327
+ ruleProto = new SIPDispatchRule({
328
+ rule: {
329
+ case: 'dispatchRuleIndividual',
330
+ value: new SIPDispatchRuleIndividual({
331
+ roomPrefix: rule.roomPrefix,
332
+ pin: rule.pin || '',
333
+ }),
334
+ },
335
+ });
336
+ }
337
+
338
+ const req = new CreateSIPDispatchRuleRequest({
339
+ rule: ruleProto,
340
+ trunkIds: trunkIds,
341
+ hidePhoneNumber: hidePhoneNumber,
342
+ name: name,
343
+ metadata: metadata,
344
+ }).toJson();
345
+
346
+ const data = await this.rpc.request(
347
+ svc,
348
+ 'CreateSIPDispatchRule',
349
+ req,
350
+ await this.authHeader({}, { admin: true }),
351
+ );
352
+ return SIPDispatchRuleInfo.fromJson(data, { ignoreUnknownFields: true });
353
+ }
354
+
355
+ async listSipDispatchRule(): Promise<Array<SIPDispatchRuleInfo>> {
356
+ const req: Partial<ListSIPDispatchRuleRequest> = {};
357
+ const data = await this.rpc.request(
358
+ svc,
359
+ 'ListSIPDispatchRule',
360
+ new ListSIPDispatchRuleRequest(req).toJson(),
361
+ await this.authHeader({}, { admin: true }),
362
+ );
363
+ return ListSIPDispatchRuleResponse.fromJson(data, { ignoreUnknownFields: true }).items ?? [];
364
+ }
365
+
366
+ /**
367
+ * @param sipDispatchRuleId sip trunk to delete
368
+ */
369
+ async deleteSipDispatchRule(sipDispatchRuleId: string): Promise<SIPDispatchRuleInfo> {
370
+ const data = await this.rpc.request(
371
+ svc,
372
+ 'DeleteSIPDispatchRule',
373
+ new DeleteSIPDispatchRuleRequest({ sipDispatchRuleId }).toJson(),
374
+ await this.authHeader({}, { admin: true }),
375
+ );
376
+ return SIPDispatchRuleInfo.fromJson(data, { ignoreUnknownFields: true });
377
+ }
378
+
379
+ /**
380
+ * @param sipTrunkId sip trunk to use for the call
381
+ * @param number number to dial
382
+ * @param roomName room to attach the call to
383
+ * @param opts CreateSipParticipantOptions
384
+ */
385
+ async createSipParticipant(
386
+ sipTrunkId: string,
387
+ number: string,
388
+ roomName: string,
389
+ opts?: CreateSipParticipantOptions,
390
+ ): Promise<SIPParticipantInfo> {
391
+ let participantIdentity: string = '';
392
+ let participantName: string = '';
393
+ let participantMetadata: string = '';
394
+ let dtmf: string = '';
395
+ let playRingtone: boolean = false;
396
+ let hidePhoneNumber: boolean = false;
397
+
398
+ if (opts !== undefined) {
399
+ participantIdentity = opts.participantIdentity || '';
400
+ participantName = opts.participantName || '';
401
+ participantMetadata = opts.participantMetadata || '';
402
+ dtmf = opts.dtmf || '';
403
+ playRingtone = opts.playRingtone || false;
404
+ hidePhoneNumber = opts.hidePhoneNumber || false;
405
+ }
406
+
407
+ const req = new CreateSIPParticipantRequest({
408
+ sipTrunkId: sipTrunkId,
409
+ sipCallTo: number,
410
+ roomName: roomName,
411
+ participantIdentity: participantIdentity,
412
+ participantName: participantName,
413
+ participantMetadata: participantMetadata,
414
+ dtmf: dtmf,
415
+ playRingtone: playRingtone,
416
+ hidePhoneNumber: hidePhoneNumber,
417
+ }).toJson();
418
+
419
+ const data = await this.rpc.request(
420
+ svc,
421
+ 'CreateSIPParticipant',
422
+ req,
423
+ await this.authHeader({}, { call: true }),
424
+ );
425
+ return SIPParticipantInfo.fromJson(data, { ignoreUnknownFields: true });
426
+ }
427
+ }
@@ -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
+ });