livekit-server-sdk 2.12.0 → 2.13.1

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/src/SipClient.ts CHANGED
@@ -2,7 +2,12 @@
2
2
  //
3
3
  // SPDX-License-Identifier: Apache-2.0
4
4
  import { Duration } from '@bufbuild/protobuf';
5
- import type { RoomConfiguration, SIPHeaderOptions } from '@livekit/protocol';
5
+ import type {
6
+ ListUpdate,
7
+ Pagination,
8
+ RoomConfiguration,
9
+ SIPHeaderOptions,
10
+ } from '@livekit/protocol';
6
11
  import {
7
12
  CreateSIPDispatchRuleRequest,
8
13
  CreateSIPInboundTrunkRequest,
@@ -29,6 +34,9 @@ import {
29
34
  SIPTransport,
30
35
  SIPTrunkInfo,
31
36
  TransferSIPParticipantRequest,
37
+ UpdateSIPDispatchRuleRequest,
38
+ UpdateSIPInboundTrunkRequest,
39
+ UpdateSIPOutboundTrunkRequest,
32
40
  } from '@livekit/protocol';
33
41
  import { ServiceBase } from './ServiceBase.js';
34
42
  import type { Rpc } from './TwirpRPC.js';
@@ -73,6 +81,7 @@ export interface CreateSipInboundTrunkOptions {
73
81
  export interface CreateSipOutboundTrunkOptions {
74
82
  metadata?: string;
75
83
  transport: SIPTransport;
84
+ destinationCountry?: string;
76
85
  /** @deprecated - use `authUsername` instead */
77
86
  auth_username?: string;
78
87
  authUsername?: string;
@@ -108,30 +117,85 @@ export interface CreateSipDispatchRuleOptions {
108
117
  }
109
118
 
110
119
  export interface CreateSipParticipantOptions {
111
- // Optional SIP From number to use. If empty, trunk number is used.
120
+ /** Optional SIP From number to use. If empty, trunk number is used. */
112
121
  fromNumber?: string;
113
- // Optional identity of the SIP participant
122
+ /** Optional identity of the SIP participant */
114
123
  participantIdentity?: string;
115
- // Optional name of the participant
124
+ /** Optional name of the participant */
116
125
  participantName?: string;
117
- // Optional metadata to attach to the participant
126
+ /** Optional metadata to attach to the participant */
118
127
  participantMetadata?: string;
119
- // Optional attributes to attach to the participant
128
+ /** Optional attributes to attach to the participant */
120
129
  participantAttributes?: { [key: string]: string };
121
- // Optionally send following DTMF digits (extension codes) when making a call.
122
- // Character 'w' can be used to add a 0.5 sec delay.
130
+ /** Optionally send following DTMF digits (extension codes) when making a call.
131
+ * Character 'w' can be used to add a 0.5 sec delay. */
123
132
  dtmf?: string;
124
- /** @deprecated - use `playDialtone` instead */
125
- playRingtone?: boolean; // Deprecated, use playDialtone instead
133
+ /** @deprecated use `playDialtone` instead */
134
+ playRingtone?: boolean;
135
+ /** If `true`, the SIP Participant plays a dial tone to the room until the phone is picked up. */
126
136
  playDialtone?: boolean;
127
- // These headers are sent as-is and may help identify this call as coming from LiveKit for the other SIP endpoint.
137
+ /** These headers are sent as-is and may help identify this call as coming from LiveKit for the other SIP endpoint. */
128
138
  headers?: { [key: string]: string };
129
- // Map SIP response headers from INVITE to sip.h.* participant attributes automatically.
139
+ /** Map SIP response headers from INVITE to sip.h.* participant attributes automatically. */
130
140
  includeHeaders?: SIPHeaderOptions;
131
141
  hidePhoneNumber?: boolean;
132
- ringingTimeout?: number; // Duration in seconds
133
- maxCallDuration?: number; // Duration in seconds
142
+ /** Maximum time for the call to ring in seconds. */
143
+ ringingTimeout?: number;
144
+ /** Maximum call duration in seconds. */
145
+ maxCallDuration?: number;
146
+ /** If `true`, Krisp noise cancellation will be enabled for the caller. */
134
147
  krispEnabled?: boolean;
148
+ /** If `true`, this will wait until the call is answered before returning. */
149
+ waitUntilAnswered?: boolean;
150
+ /** Optional request timeout in seconds. default 60 seconds if waitUntilAnswered is true, otherwise 10 seconds */
151
+ timeout?: number;
152
+ }
153
+
154
+ export interface ListSipDispatchRuleOptions {
155
+ /** Pagination options. */
156
+ page?: Pagination;
157
+ /** Rule IDs to list. If this option is set, the response will contains rules in the same order. If any of the rules is missing, a nil item in that position will be sent in the response. */
158
+ dispatchRuleIds?: string[];
159
+ /** Only list rules that contain one of the Trunk IDs, including wildcard rules. */
160
+ trunkIds?: string[];
161
+ }
162
+
163
+ export interface ListSipTrunkOptions {
164
+ /** Pagination options. */
165
+ page?: Pagination;
166
+ /** Trunk IDs to list. If this option is set, the response will contains trunks in the same order. If any of the trunks is missing, a nil item in that position will be sent in the response. */
167
+ trunkIds?: string[];
168
+ /** Only list trunks that contain one of the numbers, including wildcard trunks. */
169
+ numbers?: string[];
170
+ }
171
+
172
+ export interface SipDispatchRuleUpdateOptions {
173
+ trunkIds?: ListUpdate;
174
+ rule?: SIPDispatchRule;
175
+ name?: string;
176
+ metadata?: string;
177
+ attributes?: { [key: string]: string };
178
+ }
179
+
180
+ export interface SipInboundTrunkUpdateOptions {
181
+ numbers?: ListUpdate;
182
+ allowedAddresses?: ListUpdate;
183
+ allowedNumbers?: ListUpdate;
184
+ authUsername?: string;
185
+ authPassword?: string;
186
+ name?: string;
187
+ metadata?: string;
188
+ }
189
+
190
+ export interface SipOutboundTrunkUpdateOptions {
191
+ numbers?: ListUpdate;
192
+ allowedAddresses?: ListUpdate;
193
+ allowedNumbers?: ListUpdate;
194
+ authUsername?: string;
195
+ authPassword?: string;
196
+ destinationCountry?: string;
197
+ name?: string;
198
+ metadata?: string;
135
199
  }
136
200
 
137
201
  export interface TransferSipParticipantOptions {
@@ -206,9 +270,12 @@ export class SipClient extends ServiceBase {
206
270
  }
207
271
 
208
272
  /**
273
+ * Create a new SIP inbound trunk.
274
+ *
209
275
  * @param name - human-readable name of the trunk
210
276
  * @param numbers - phone numbers of the trunk
211
277
  * @param opts - CreateSipTrunkOptions
278
+ * @returns Created SIP inbound trunk
212
279
  */
213
280
  async createSipInboundTrunk(
214
281
  name: string,
@@ -244,10 +311,13 @@ export class SipClient extends ServiceBase {
244
311
  }
245
312
 
246
313
  /**
314
+ * Create a new SIP outbound trunk.
315
+ *
247
316
  * @param name - human-readable name of the trunk
248
317
  * @param address - hostname and port of the SIP server to dial
249
318
  * @param numbers - phone numbers of the trunk
250
319
  * @param opts - CreateSipTrunkOptions
320
+ * @returns Created SIP outbound trunk
251
321
  */
252
322
  async createSipOutboundTrunk(
253
323
  name: string,
@@ -273,6 +343,7 @@ export class SipClient extends ServiceBase {
273
343
  headers: opts.headers,
274
344
  headersToAttributes: opts.headersToAttributes,
275
345
  includeHeaders: opts.includeHeaders,
346
+ destinationCountry: opts.destinationCountry,
276
347
  }),
277
348
  }).toJson();
278
349
 
@@ -299,30 +370,45 @@ export class SipClient extends ServiceBase {
299
370
  return ListSIPTrunkResponse.fromJson(data, { ignoreUnknownFields: true }).items ?? [];
300
371
  }
301
372
 
302
- async listSipInboundTrunk(): Promise<Array<SIPInboundTrunkInfo>> {
303
- const req: Partial<ListSIPInboundTrunkRequest> = {};
373
+ /**
374
+ * List SIP inbound trunks with optional filtering.
375
+ *
376
+ * @param list - Request with optional filtering parameters
377
+ * @returns Response containing list of SIP inbound trunks
378
+ */
379
+ async listSipInboundTrunk(list: ListSipTrunkOptions = {}): Promise<Array<SIPInboundTrunkInfo>> {
380
+ const req = new ListSIPInboundTrunkRequest(list).toJson();
304
381
  const data = await this.rpc.request(
305
382
  svc,
306
383
  'ListSIPInboundTrunk',
307
- new ListSIPInboundTrunkRequest(req).toJson(),
384
+ req,
308
385
  await this.authHeader({}, { admin: true }),
309
386
  );
310
387
  return ListSIPInboundTrunkResponse.fromJson(data, { ignoreUnknownFields: true }).items ?? [];
311
388
  }
312
389
 
313
- async listSipOutboundTrunk(): Promise<Array<SIPOutboundTrunkInfo>> {
314
- const req: Partial<ListSIPOutboundTrunkRequest> = {};
390
+ /**
391
+ * List SIP outbound trunks with optional filtering.
392
+ *
393
+ * @param list - Request with optional filtering parameters
394
+ * @returns Response containing list of SIP outbound trunks
395
+ */
396
+ async listSipOutboundTrunk(list: ListSipTrunkOptions = {}): Promise<Array<SIPOutboundTrunkInfo>> {
397
+ const req = new ListSIPOutboundTrunkRequest(list).toJson();
315
398
  const data = await this.rpc.request(
316
399
  svc,
317
400
  'ListSIPOutboundTrunk',
318
- new ListSIPOutboundTrunkRequest(req).toJson(),
401
+ req,
319
402
  await this.authHeader({}, { admin: true }),
320
403
  );
321
404
  return ListSIPOutboundTrunkResponse.fromJson(data, { ignoreUnknownFields: true }).items ?? [];
322
405
  }
323
406
 
324
407
  /**
325
- * @param sipTrunkId - sip trunk to delete
408
+ * Delete a SIP trunk.
409
+ *
410
+ * @param sipTrunkId - ID of the SIP trunk to delete
411
+ * @returns Deleted trunk information
326
412
  */
327
413
  async deleteSipTrunk(sipTrunkId: string): Promise<SIPTrunkInfo> {
328
414
  const data = await this.rpc.request(
@@ -335,8 +421,11 @@ export class SipClient extends ServiceBase {
335
421
  }
336
422
 
337
423
  /**
338
- * @param rule - sip dispatch rule
424
+ * Create a new SIP dispatch rule.
425
+ *
426
+ * @param rule - SIP dispatch rule to create
339
427
  * @param opts - CreateSipDispatchRuleOptions
428
+ * @returns Created SIP dispatch rule
340
429
  */
341
430
  async createSipDispatchRule(
342
431
  rule: SipDispatchRuleDirect | SipDispatchRuleIndividual,
@@ -388,19 +477,207 @@ export class SipClient extends ServiceBase {
388
477
  return SIPDispatchRuleInfo.fromJson(data, { ignoreUnknownFields: true });
389
478
  }
390
479
 
391
- async listSipDispatchRule(): Promise<Array<SIPDispatchRuleInfo>> {
392
- const req: Partial<ListSIPDispatchRuleRequest> = {};
480
+ /**
481
+ * Updates an existing SIP dispatch rule by replacing it entirely.
482
+ *
483
+ * @param sipDispatchRuleId - ID of the SIP dispatch rule to update
484
+ * @param rule - new SIP dispatch rule
485
+ * @returns Updated SIP dispatch rule
486
+ */
487
+ async updateSipDispatchRule(
488
+ sipDispatchRuleId: string,
489
+ rule: SIPDispatchRuleInfo,
490
+ ): Promise<SIPDispatchRuleInfo> {
491
+ const req = new UpdateSIPDispatchRuleRequest({
492
+ sipDispatchRuleId: sipDispatchRuleId,
493
+ action: {
494
+ case: 'replace',
495
+ value: rule,
496
+ },
497
+ }).toJson();
498
+
499
+ const data = await this.rpc.request(
500
+ svc,
501
+ 'UpdateSIPDispatchRule',
502
+ req,
503
+ await this.authHeader({}, { admin: true }),
504
+ );
505
+
506
+ return SIPDispatchRuleInfo.fromJson(data, { ignoreUnknownFields: true });
507
+ }
508
+
509
+ /**
510
+ * Updates specific fields of an existing SIP dispatch rule.
511
+ * Only provided fields will be updated.
512
+ *
513
+ * @param sipDispatchRuleId - ID of the SIP dispatch rule to update
514
+ * @param fields - Fields of the dispatch rule to update
515
+ * @returns Updated SIP dispatch rule
516
+ */
517
+ async updateSipDispatchRuleFields(
518
+ sipDispatchRuleId: string,
519
+ fields: SipDispatchRuleUpdateOptions = {},
520
+ ): Promise<SIPDispatchRuleInfo> {
521
+ const req = new UpdateSIPDispatchRuleRequest({
522
+ sipDispatchRuleId: sipDispatchRuleId,
523
+ action: {
524
+ case: 'update',
525
+ value: fields,
526
+ },
527
+ }).toJson();
528
+
529
+ const data = await this.rpc.request(
530
+ svc,
531
+ 'UpdateSIPDispatchRule',
532
+ req,
533
+ await this.authHeader({}, { admin: true }),
534
+ );
535
+
536
+ return SIPDispatchRuleInfo.fromJson(data, { ignoreUnknownFields: true });
537
+ }
538
+
539
+ /**
540
+ * Updates an existing SIP inbound trunk by replacing it entirely.
541
+ *
542
+ * @param sipTrunkId - ID of the SIP inbound trunk to update
543
+ * @param trunk - SIP inbound trunk to update with
544
+ * @returns Updated SIP inbound trunk
545
+ */
546
+ async updateSipInboundTrunk(
547
+ sipTrunkId: string,
548
+ trunk: SIPInboundTrunkInfo,
549
+ ): Promise<SIPInboundTrunkInfo> {
550
+ const req = new UpdateSIPInboundTrunkRequest({
551
+ sipTrunkId,
552
+ action: {
553
+ case: 'replace',
554
+ value: trunk,
555
+ },
556
+ }).toJson();
557
+
558
+ const data = await this.rpc.request(
559
+ svc,
560
+ 'UpdateSIPInboundTrunk',
561
+ req,
562
+ await this.authHeader({}, { admin: true }),
563
+ );
564
+
565
+ return SIPInboundTrunkInfo.fromJson(data, { ignoreUnknownFields: true });
566
+ }
567
+
568
+ /**
569
+ * Updates specific fields of an existing SIP inbound trunk.
570
+ * Only provided fields will be updated.
571
+ *
572
+ * @param sipTrunkId - ID of the SIP inbound trunk to update
573
+ * @param fields - Fields of the inbound trunk to update
574
+ * @returns Updated SIP inbound trunk
575
+ */
576
+ async updateSipInboundTrunkFields(
577
+ sipTrunkId: string,
578
+ fields: SipInboundTrunkUpdateOptions,
579
+ ): Promise<SIPInboundTrunkInfo> {
580
+ const req = new UpdateSIPInboundTrunkRequest({
581
+ sipTrunkId,
582
+ action: {
583
+ case: 'update',
584
+ value: fields,
585
+ },
586
+ }).toJson();
587
+
588
+ const data = await this.rpc.request(
589
+ svc,
590
+ 'UpdateSIPInboundTrunk',
591
+ req,
592
+ await this.authHeader({}, { admin: true }),
593
+ );
594
+
595
+ return SIPInboundTrunkInfo.fromJson(data, { ignoreUnknownFields: true });
596
+ }
597
+
598
+ /**
599
+ * Updates an existing SIP outbound trunk by replacing it entirely.
600
+ *
601
+ * @param sipTrunkId - ID of the SIP outbound trunk to update
602
+ * @param trunk - SIP outbound trunk to update with
603
+ * @returns Updated SIP outbound trunk
604
+ */
605
+ async updateSipOutboundTrunk(
606
+ sipTrunkId: string,
607
+ trunk: SIPOutboundTrunkInfo,
608
+ ): Promise<SIPOutboundTrunkInfo> {
609
+ const req = new UpdateSIPOutboundTrunkRequest({
610
+ sipTrunkId,
611
+ action: {
612
+ case: 'replace',
613
+ value: trunk,
614
+ },
615
+ }).toJson();
616
+
617
+ const data = await this.rpc.request(
618
+ svc,
619
+ 'UpdateSIPOutboundTrunk',
620
+ req,
621
+ await this.authHeader({}, { admin: true }),
622
+ );
623
+
624
+ return SIPOutboundTrunkInfo.fromJson(data, { ignoreUnknownFields: true });
625
+ }
626
+
627
+ /**
628
+ * Updates specific fields of an existing SIP outbound trunk.
629
+ * Only provided fields will be updated.
630
+ *
631
+ * @param sipTrunkId - ID of the SIP outbound trunk to update
632
+ * @param fields - Fields of the outbound trunk to update
633
+ * @returns Updated SIP outbound trunk
634
+ */
635
+ async updateSipOutboundTrunkFields(
636
+ sipTrunkId: string,
637
+ fields: SipOutboundTrunkUpdateOptions,
638
+ ): Promise<SIPOutboundTrunkInfo> {
639
+ const req = new UpdateSIPOutboundTrunkRequest({
640
+ sipTrunkId,
641
+ action: {
642
+ case: 'update',
643
+ value: fields,
644
+ },
645
+ }).toJson();
646
+
647
+ const data = await this.rpc.request(
648
+ svc,
649
+ 'UpdateSIPOutboundTrunk',
650
+ req,
651
+ await this.authHeader({}, { admin: true }),
652
+ );
653
+
654
+ return SIPOutboundTrunkInfo.fromJson(data, { ignoreUnknownFields: true });
655
+ }
656
+
657
+ /**
658
+ * List SIP dispatch rules with optional filtering.
659
+ *
660
+ * @param list - Request with optional filtering parameters
661
+ * @returns Response containing list of SIP dispatch rules
662
+ */
663
+ async listSipDispatchRule(
664
+ list: ListSipDispatchRuleOptions = {},
665
+ ): Promise<Array<SIPDispatchRuleInfo>> {
666
+ const req = new ListSIPDispatchRuleRequest(list).toJson();
393
667
  const data = await this.rpc.request(
394
668
  svc,
395
669
  'ListSIPDispatchRule',
396
- new ListSIPDispatchRuleRequest(req).toJson(),
670
+ req,
397
671
  await this.authHeader({}, { admin: true }),
398
672
  );
399
673
  return ListSIPDispatchRuleResponse.fromJson(data, { ignoreUnknownFields: true }).items ?? [];
400
674
  }
401
675
 
402
676
  /**
403
- * @param sipDispatchRuleId - sip trunk to delete
677
+ * Delete a SIP dispatch rule.
678
+ *
679
+ * @param sipDispatchRuleId - ID of the SIP dispatch rule to delete
680
+ * @returns Deleted rule information
404
681
  */
405
682
  async deleteSipDispatchRule(sipDispatchRuleId: string): Promise<SIPDispatchRuleInfo> {
406
683
  const data = await this.rpc.request(
@@ -413,10 +690,13 @@ export class SipClient extends ServiceBase {
413
690
  }
414
691
 
415
692
  /**
693
+ * Create a new SIP participant.
694
+ *
416
695
  * @param sipTrunkId - sip trunk to use for the call
417
696
  * @param number - number to dial
418
697
  * @param roomName - room to attach the call to
419
698
  * @param opts - CreateSipParticipantOptions
699
+ * @returns Created SIP participant
420
700
  */
421
701
  async createSipParticipant(
422
702
  sipTrunkId: string,
@@ -428,6 +708,10 @@ export class SipClient extends ServiceBase {
428
708
  opts = {};
429
709
  }
430
710
 
711
+ if (opts.timeout === undefined) {
712
+ opts.timeout = opts.waitUntilAnswered ? 60 : 10;
713
+ }
714
+
431
715
  const req = new CreateSIPParticipantRequest({
432
716
  sipTrunkId: sipTrunkId,
433
717
  sipCallTo: number,
@@ -449,6 +733,7 @@ export class SipClient extends ServiceBase {
449
733
  ? new Duration({ seconds: BigInt(opts.maxCallDuration) })
450
734
  : undefined,
451
735
  krispEnabled: opts.krispEnabled,
736
+ waitUntilAnswered: opts.waitUntilAnswered,
452
737
  }).toJson();
453
738
 
454
739
  const data = await this.rpc.request(
@@ -456,14 +741,18 @@ export class SipClient extends ServiceBase {
456
741
  'CreateSIPParticipant',
457
742
  req,
458
743
  await this.authHeader({}, { call: true }),
744
+ opts.timeout,
459
745
  );
460
746
  return SIPParticipantInfo.fromJson(data, { ignoreUnknownFields: true });
461
747
  }
462
748
 
463
749
  /**
750
+ * Transfer a SIP participant to a different room.
751
+ *
464
752
  * @param roomName - room the SIP participant to transfer is connectd to
465
753
  * @param participantIdentity - identity of the SIP participant to transfer
466
754
  * @param transferTo - SIP URL to transfer the participant to
755
+ * @param opts - TransferSipParticipantOptions
467
756
  */
468
757
  async transferSipParticipant(
469
758
  roomName: string,
package/src/TwirpRPC.ts CHANGED
@@ -9,19 +9,32 @@ const defaultPrefix = '/twirp';
9
9
 
10
10
  export const livekitPackage = 'livekit';
11
11
  export interface Rpc {
12
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
- request(service: string, method: string, data: JsonValue, headers?: any): Promise<string>;
12
+ request(
13
+ service: string,
14
+ method: string,
15
+ data: JsonValue,
16
+ headers: any, // eslint-disable-line @typescript-eslint/no-explicit-any
17
+ timeout?: number,
18
+ ): Promise<string>;
14
19
  }
15
20
 
16
21
  export class TwirpError extends Error {
17
22
  status: number;
18
23
  code?: string;
24
+ metadata?: Record<string, string>;
19
25
 
20
- constructor(name: string, message: string, status: number, code?: string) {
26
+ constructor(
27
+ name: string,
28
+ message: string,
29
+ status: number,
30
+ code?: string,
31
+ metadata?: Record<string, string>,
32
+ ) {
21
33
  super(message);
22
34
  this.name = name;
23
35
  this.status = status;
24
36
  this.code = code;
37
+ this.metadata = metadata;
25
38
  }
26
39
  }
27
40
 
@@ -44,24 +57,36 @@ export class TwirpRpc {
44
57
  this.prefix = prefix || defaultPrefix;
45
58
  }
46
59
 
47
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
- async request(service: string, method: string, data: any, headers?: any): Promise<any> {
60
+ async request(
61
+ service: string,
62
+ method: string,
63
+ data: any, // eslint-disable-line @typescript-eslint/no-explicit-any
64
+ headers: any, // eslint-disable-line @typescript-eslint/no-explicit-any
65
+ timeout = 60,
66
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
+ ): Promise<any> {
49
68
  const path = `${this.prefix}/${this.pkg}.${service}/${method}`;
50
69
  const url = new URL(path, this.host);
51
-
52
- const response = await fetch(url, {
70
+ const init: RequestInit = {
53
71
  method: 'POST',
54
72
  headers: {
55
73
  'Content-Type': 'application/json;charset=UTF-8',
56
74
  ...headers,
57
75
  },
58
76
  body: JSON.stringify(data),
59
- });
77
+ };
78
+
79
+ if (timeout) {
80
+ init.signal = AbortSignal.timeout(timeout * 1000);
81
+ }
82
+
83
+ const response = await fetch(url, init);
60
84
 
61
85
  if (!response.ok) {
62
86
  const isJson = response.headers.get('content-type') === 'application/json';
63
87
  let errorMessage = 'Unknown internal error';
64
88
  let errorCode: string | undefined = undefined;
89
+ let metadata: Record<string, string> | undefined = undefined;
65
90
  try {
66
91
  if (isJson) {
67
92
  const parsedError = (await response.json()) as Record<string, unknown>;
@@ -71,6 +96,9 @@ export class TwirpRpc {
71
96
  if ('code' in parsedError) {
72
97
  errorCode = <string>parsedError.code;
73
98
  }
99
+ if ('meta' in parsedError) {
100
+ metadata = <Record<string, string>>parsedError.meta;
101
+ }
74
102
  } else {
75
103
  errorMessage = await response.text();
76
104
  }
@@ -79,7 +107,7 @@ export class TwirpRpc {
79
107
  console.debug(`Error when trying to parse error message, using defaults`, e);
80
108
  }
81
109
 
82
- throw new TwirpError(response.statusText, errorMessage, response.status, errorCode);
110
+ throw new TwirpError(response.statusText, errorMessage, response.status, errorCode, metadata);
83
111
  }
84
112
  const parsedResp = (await response.json()) as Record<string, unknown>;
85
113