@sentdm/voice-client 0.0.0-stage → 0.1.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +201 -0
  3. package/README.md +283 -2
  4. package/adapter/loader.d.mts +3 -0
  5. package/adapter/loader.d.mts.map +1 -0
  6. package/adapter/loader.d.ts +3 -0
  7. package/adapter/loader.d.ts.map +1 -0
  8. package/adapter/loader.js +6 -0
  9. package/adapter/loader.js.map +1 -0
  10. package/adapter/loader.mjs +3 -0
  11. package/adapter/loader.mjs.map +1 -0
  12. package/adapter/sinch.d.mts +3 -0
  13. package/adapter/sinch.d.mts.map +1 -0
  14. package/adapter/sinch.d.ts +3 -0
  15. package/adapter/sinch.d.ts.map +1 -0
  16. package/adapter/sinch.js +280 -0
  17. package/adapter/sinch.js.map +1 -0
  18. package/adapter/sinch.mjs +276 -0
  19. package/adapter/sinch.mjs.map +1 -0
  20. package/adapter/types.d.mts +78 -0
  21. package/adapter/types.d.mts.map +1 -0
  22. package/adapter/types.d.ts +78 -0
  23. package/adapter/types.d.ts.map +1 -0
  24. package/adapter/types.js +3 -0
  25. package/adapter/types.js.map +1 -0
  26. package/adapter/types.mjs +2 -0
  27. package/adapter/types.mjs.map +1 -0
  28. package/audio.d.mts +42 -0
  29. package/audio.d.mts.map +1 -0
  30. package/audio.d.ts +42 -0
  31. package/audio.d.ts.map +1 -0
  32. package/audio.js +125 -0
  33. package/audio.js.map +1 -0
  34. package/audio.mjs +121 -0
  35. package/audio.mjs.map +1 -0
  36. package/call.d.mts +60 -0
  37. package/call.d.mts.map +1 -0
  38. package/call.d.ts +60 -0
  39. package/call.d.ts.map +1 -0
  40. package/call.js +84 -0
  41. package/call.js.map +1 -0
  42. package/call.mjs +80 -0
  43. package/call.mjs.map +1 -0
  44. package/client.d.mts +111 -0
  45. package/client.d.mts.map +1 -0
  46. package/client.d.ts +111 -0
  47. package/client.d.ts.map +1 -0
  48. package/client.js +365 -0
  49. package/client.js.map +1 -0
  50. package/client.mjs +361 -0
  51. package/client.mjs.map +1 -0
  52. package/errors.d.mts +2 -0
  53. package/errors.d.mts.map +1 -0
  54. package/errors.d.ts +2 -0
  55. package/errors.d.ts.map +1 -0
  56. package/errors.js +16 -0
  57. package/errors.js.map +1 -0
  58. package/errors.mjs +4 -0
  59. package/errors.mjs.map +1 -0
  60. package/index.d.mts +4 -0
  61. package/index.d.mts.map +1 -0
  62. package/index.d.ts +4 -0
  63. package/index.d.ts.map +1 -0
  64. package/index.js +18 -0
  65. package/index.js.map +1 -0
  66. package/index.mjs +4 -0
  67. package/index.mjs.map +1 -0
  68. package/internal/tslib.js +75 -0
  69. package/internal/tslib.mjs +17 -0
  70. package/invite.d.mts +28 -0
  71. package/invite.d.mts.map +1 -0
  72. package/invite.d.ts +28 -0
  73. package/invite.d.ts.map +1 -0
  74. package/invite.js +64 -0
  75. package/invite.js.map +1 -0
  76. package/invite.mjs +60 -0
  77. package/invite.mjs.map +1 -0
  78. package/package.json +41 -3
  79. package/src/adapter/loader.ts +4 -0
  80. package/src/adapter/sinch.ts +333 -0
  81. package/src/adapter/types.ts +63 -0
  82. package/src/audio.ts +174 -0
  83. package/src/call.ts +145 -0
  84. package/src/client.ts +582 -0
  85. package/src/errors.ts +15 -0
  86. package/src/index.ts +14 -0
  87. package/src/invite.ts +75 -0
  88. package/src/sw.js +14 -0
  89. package/src/tsconfig.json +11 -0
  90. package/src/version.ts +1 -0
  91. package/sw.js +14 -0
  92. package/version.d.mts +2 -0
  93. package/version.d.mts.map +1 -0
  94. package/version.d.ts +2 -0
  95. package/version.d.ts.map +1 -0
  96. package/version.js +5 -0
  97. package/version.js.map +1 -0
  98. package/version.mjs +2 -0
  99. package/version.mjs.map +1 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ ## [0.1.1](https://github.com/sentdm/sent-dm-voice-sdk/compare/v0.1.0...v0.1.1) (2026-09-29)
4
+
5
+
6
+ ### Chores
7
+
8
+ * publish through npm trusted publishing only ([5e1e674](https://github.com/sentdm/sent-dm-voice-sdk/commit/5e1e6747ade2fc2a5acf028a9a0dc21d17f59d61))
9
+
10
+ ## [0.1.0](https://github.com/sentdm/sent-dm-voice-sdk/compare/v0.0.1...v0.1.0) (2026-09-29)
11
+
12
+
13
+ ### Features
14
+
15
+ * add calls and incoming call invites with connect and joinConference ([d11ab9c](https://github.com/sentdm/sent-dm-voice-sdk/commit/d11ab9c601ee9a051e92fa4dbace964f624c01b4))
16
+ * add provider quality warnings and reconnects, the README and the npm release pipeline ([e304bb3](https://github.com/sentdm/sent-dm-voice-sdk/commit/e304bb357aa363878f266bd68e094853f2776fba))
17
+ * add the audio controller with device selection and deviceChanged ([d1ceda1](https://github.com/sentdm/sent-dm-voice-sdk/commit/d1ceda1dcafc777bcf30f58179b18b51a0753c08))
18
+ * add the provider adapter seam and a scriptable mock adapter ([a06388d](https://github.com/sentdm/sent-dm-voice-sdk/commit/a06388dbd17465346b834818d8f613b57985d65b))
19
+ * add the React provider and hooks with activeCallChanged and invite events ([409fc94](https://github.com/sentdm/sent-dm-voice-sdk/commit/409fc940c747b785e8e8c3436ba37945df8fb8a9))
20
+ * add the SentVoice client with registration and token refresh ([244b73c](https://github.com/sentdm/sent-dm-voice-sdk/commit/244b73cf196bc7898d067679ff7a3a025b8b4bc4))
21
+ * add the Sinch adapter with incoming-call push and call audio ([afe794d](https://github.com/sentdm/sent-dm-voice-sdk/commit/afe794d6ec5c8946b7f32b46a057a2f697d521e4))
22
+ * add the telemetry module with batched usage and call quality reports ([c8c701e](https://github.com/sentdm/sent-dm-voice-sdk/commit/c8c701e6c7fe1e9644fb5596bb5a40847bd1f0de))
23
+ * scaffold @sentdm/voice with the error family and a typed emitter ([54eae6e](https://github.com/sentdm/sent-dm-voice-sdk/commit/54eae6eb8a305688ac74573f3e6a35b00cb929f4))
24
+
25
+
26
+ ### Bug Fixes
27
+
28
+ * identity namespace, telemetry logs ([8748cc3](https://github.com/sentdm/sent-dm-voice-sdk/commit/8748cc370ef4a70d8ab6e2b59f82db79f9334aee))
29
+
30
+
31
+ ### Chores
32
+
33
+ * publish @sentdm/voice as a public package ([ac84a9b](https://github.com/sentdm/sent-dm-voice-sdk/commit/ac84a9b86b59a864da05ae6b9be3f000abbe055d))
34
+
35
+
36
+ ### Documentation
37
+
38
+ * new token route, phone leg cap, telemetry 404 tolerance ([96ff8d9](https://github.com/sentdm/sent-dm-voice-sdk/commit/96ff8d9ff2fe93359dc5d2d0920c5f9e479ab8bb))
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Sent
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md CHANGED
@@ -1,3 +1,284 @@
1
- # Temporary Holding Version
1
+ # Sent Voice TypeScript SDK
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ This library lets a web app place and receive Sent Voice calls in the browser. React bindings ship separately as `@sentdm/voice-client-react`.
4
+
5
+ Your backend mints a short-lived voice token for each user with the Sent API. The SDK registers the browser with it, then places calls, receives them and reports how they go.
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ npm install @sentdm/voice-client
11
+ ```
12
+
13
+ ## Quickstart
14
+
15
+ ### 1. Mint voice tokens on your backend
16
+
17
+ Mint tokens on your server with your Sent API key, never in the browser, for the user who is signed in:
18
+
19
+ ```ts
20
+ export async function createVoiceToken(identity: string): Promise<string> {
21
+ const response = await fetch('https://api.sent.dm/v3/channels/voice/tokens', {
22
+ method: 'POST',
23
+ headers: { 'x-api-key': process.env['SENT_DM_API_KEY'] ?? '', 'Content-Type': 'application/json' },
24
+ body: JSON.stringify({ identity }),
25
+ });
26
+ if (!response.ok) throw new Error(`Minting a voice token failed with status ${response.status}`);
27
+ const { data } = (await response.json()) as { data: { token: string } };
28
+ return data.token;
29
+ }
30
+ ```
31
+
32
+ `identity` names the user in calls: letters, digits, `-` and `_`, up to 200 characters. The user is bound to your default voice number unless you pass `number`, one of the numbers you turned voice on for with `POST /v3/channels/voice`, and the token lasts `ttl` seconds (600 by default, at most 3600). Return the token as text from an endpoint of your own, such as `/api/voice-token`.
33
+
34
+ ### 2. Serve the service worker
35
+
36
+ Incoming calls reach the browser as push messages, through a service worker your app serves. Copy the one this package ships next to your pages, for example:
37
+
38
+ ```sh
39
+ cp node_modules/@sentdm/voice-client/sw.js public/sw.js
40
+ ```
41
+
42
+ The SDK registers `sw.js` relative to the page. If you serve it elsewhere, or your app already has a service worker, give it a path and scope of its own so it does not replace yours:
43
+
44
+ ```ts
45
+ import SentVoice from '@sentdm/voice-client';
46
+
47
+ const client = new SentVoice({
48
+ tokenProvider: () => fetch('/api/voice-token').then((response) => response.text()),
49
+ serviceWorker: { url: '/voice/sw.js', scope: '/voice/' },
50
+ });
51
+ ```
52
+
53
+ ### 3. Register and call
54
+
55
+ ```ts
56
+ import SentVoice from '@sentdm/voice-client';
57
+
58
+ const client = new SentVoice({
59
+ tokenProvider: () => fetch('/api/voice-token').then((response) => response.text()),
60
+ });
61
+
62
+ document.querySelector('#call')?.addEventListener('click', async () => {
63
+ await client.register();
64
+ const call = await client.connect({ to: '+14155551234' });
65
+ call.on('connected', () => console.log('Connected'));
66
+ call.on('disconnected', ({ state }) => console.log(`The call ended as ${state}`));
67
+ });
68
+ ```
69
+
70
+ The first `register()` asks the user to allow notifications, which the browser needs to deliver incoming calls. Firefox and Safari only show that prompt after a user gesture, so call it from a click, as above. It rejects when notifications are blocked or the service worker cannot be registered.
71
+
72
+ ## Token lifecycle
73
+
74
+ `tokenProvider` is how the SDK gets every token. It must return the `token` that `POST /v3/channels/voice/tokens` responded with, fetched fresh each time, because the SDK calls it again before each token expires.
75
+
76
+ - `register()` calls it and resolves once the client is `registered`. The client moves from `unregistered` to `registering` to `registered`, emitting each state as an event.
77
+ - A token provider that throws or rejects is retried after a short backoff, 2 more times by default (`registerRetries`); `register()` then rejects with a `NetworkError`. A value that is not a voice token rejects with a `TokenInvalidError` at once.
78
+ - The token is refreshed at 80% of its lifetime, or 30 seconds before it expires if that is earlier, but never before half of its lifetime has passed. `tokenWillExpire` fires first, then `tokenProvider` is called with the same retries. Nothing else changes for your app.
79
+ - If the retries run out, the client emits `error` with the cause and `offline` with a `TokenExpiredError`, then tries again about every 30 seconds until it is `registered` again; calling `register()` tries at once. Calls in progress go on, but `connect()` and `joinConference()` throw a `NotRegisteredError` while the client is offline.
80
+ - `unregister()` stops receiving calls and leaves calls in progress alone. `destroy()` ends every call and tears the client down for good.
81
+
82
+ Tokens stay in memory and are redacted from log messages.
83
+
84
+ ```ts
85
+ import SentVoice from '@sentdm/voice-client';
86
+
87
+ const client = new SentVoice({
88
+ tokenProvider: async () => {
89
+ const response = await fetch('/api/voice-token');
90
+ if (!response.ok) throw new Error(`Fetching the voice token failed with status ${response.status}`);
91
+ return response.text();
92
+ },
93
+ });
94
+
95
+ client.on('registered', () => console.log('Ready for calls'));
96
+ client.on('tokenWillExpire', ({ expiresAt }) => console.log('Refreshing, expires', new Date(expiresAt)));
97
+ client.on('offline', (reason) => console.warn(`Offline (${reason.code}), trying again every 30 seconds`));
98
+ ```
99
+
100
+ ## Calls
101
+
102
+ `connect({ to })` calls a phone number in E.164 format, like `'+14155551234'`, or another user of your app by identity, like `'ben'`. `joinConference({ name })` joins one of your account's rooms, named with letters, digits, `-` and `_`, up to 27 characters. Either throws a `SentVoiceError` with code `INVALID_ADDRESS` when `to` or `name` does not fit, and one with code `CALL_IN_PROGRESS` while a call is in progress or an incoming call is still waiting for an answer. Both open the microphone, and reject with a `MediaPermissionError` when the user refuses. `to` says who the user wants to reach; your backend's answer decides what rings.
103
+
104
+ A leg to a phone number, whether the answer connects the call to a number or a phone participant is added through the API, runs for at most what your account's balance affords at the destination's per-minute rate, capped at four hours by the provider. Legs to app users and rooms have no such limit because they cost nothing. A call that reaches the cap ends as `completed`.
105
+
106
+ ```ts
107
+ import type SentVoice from '@sentdm/voice-client';
108
+
109
+ export async function callBen(client: SentVoice) {
110
+ const call = await client.connect({ to: 'ben' });
111
+
112
+ call.on('ringing', () => console.log('Ringing'));
113
+ call.on('connected', () => call.sendDigits('1'));
114
+ call.on('muteChanged', (isMuted) => console.log(isMuted ? 'Muted' : 'Unmuted'));
115
+ call.on('disconnected', ({ state, error }) => console.log(`The call ended as ${state}`, error?.code));
116
+ return call;
117
+ }
118
+ ```
119
+
120
+ A call moves through `initiated`, `ringing`, `answered` and `connected`, and ends as `completed`, `failed`, `busy` or `noAnswer`, which `disconnected` reports. `hangup()` ends it, `mute()` toggles the microphone, `sendDigits()` sends DTMF, and `getStats()` reads its jitter, packet loss and round-trip time. `client.activeCall` is the call placed or answered last, until that call ends, and `activeCallChanged` reports each change. `client.calls` lists every call in progress.
121
+
122
+ The SDK plays the other party through an audio element it creates. Pass your own with the `audio: { element }` option.
123
+
124
+ ### Receiving calls
125
+
126
+ ```ts
127
+ import type SentVoice from '@sentdm/voice-client';
128
+
129
+ export function receiveCalls(client: SentVoice) {
130
+ client.on('incomingCall', (invite) => {
131
+ console.log('Incoming call from', invite.from);
132
+ invite.on('cancelled', () => console.log('The caller hung up'));
133
+
134
+ document.querySelector('#answer')?.addEventListener(
135
+ 'click',
136
+ async () => {
137
+ const call = await invite.accept();
138
+ call.on('disconnected', ({ state }) => console.log(`The call ended as ${state}`));
139
+ },
140
+ { once: true },
141
+ );
142
+ document.querySelector('#decline')?.addEventListener('click', () => invite.reject(), { once: true });
143
+ });
144
+ }
145
+ ```
146
+
147
+ The microphone opens when the call arrives, and `accept()` asks for permission again if that was refused, then resolves with the call. When the user refuses, it rejects with a `MediaPermissionError` and the invite stays pending, so the user can try again. `reject()` declines the call, and `cancelled` fires when the caller hangs up first. A call that arrives while another one is in progress still raises `incomingCall`; your handler decides whether to `accept()` it or `reject()` it. The call accepted last becomes `activeCall`.
148
+
149
+ ### Call quality and reconnection
150
+
151
+ `qualityWarning` fires when a network metric of a connected call crosses its threshold, and again with `cleared: true` once it recovers. Stats are sampled every half second.
152
+
153
+ | `metric` | Raised when | Cleared when |
154
+ | ------------ | ------------------------------------------------------------------------- | ------------------------------ |
155
+ | `jitter` | incoming audio jitter is above 30 ms in 3 of the last 4 samples | fewer than 3 of the last 4 are |
156
+ | `packetLoss` | over 1% of the incoming audio packets are lost in 3 of the last 4 samples | fewer than 3 of the last 4 are |
157
+ | `rtt` | the round-trip time is above 300 ms | it is 300 ms or less again |
158
+
159
+ The `rtt` warning follows every sample, so it can go on and off while the round-trip time hovers around 300 ms. A warning still raised when the call ends is not cleared.
160
+
161
+ ```ts
162
+ import type SentVoice from '@sentdm/voice-client';
163
+
164
+ export function showCallQuality(call: SentVoice.Call) {
165
+ call.on('qualityWarning', ({ metric, cleared }) => console.log(metric, cleared ? 'recovered' : 'poor'));
166
+ call.on('reconnecting', () => console.log('Connection lost, reconnecting'));
167
+ call.on('reconnected', () => console.log('Connection back'));
168
+ }
169
+ ```
170
+
171
+ When the connection drops for 2 seconds, the call goes `reconnecting`, and `reconnected` fires when it comes back on the same network. A call cannot move to a new network: after a network switch its connection does not come back, and a call that has not reconnected within 5 minutes ends as `failed` with a `NetworkError`.
172
+
173
+ ## Handling errors
174
+
175
+ Every error the SDK throws or emits is a `SentVoiceError` with a stable `code`, a `category` (`auth`, `media`, `signaling`, `network`, `validation` or `capability`) and whether it is `retriable`. Check for the subclasses with `instanceof`:
176
+
177
+ ```ts
178
+ import type SentVoice from '@sentdm/voice-client';
179
+ import { MediaPermissionError, SentVoiceError } from '@sentdm/voice-client/errors';
180
+
181
+ export async function answer(invite: SentVoice.CallInvite) {
182
+ try {
183
+ return await invite.accept();
184
+ } catch (error) {
185
+ if (error instanceof MediaPermissionError) console.warn('Allow microphone access to answer calls');
186
+ else if (error instanceof SentVoiceError) console.error(error.code, error.message);
187
+ throw error;
188
+ }
189
+ }
190
+ ```
191
+
192
+ | Error | `code` | `category` | When |
193
+ | ---------------------------- | ------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
194
+ | `TokenInvalidError` | `TOKEN_INVALID` | `auth` | `register()`: the token provider returned something that is not a voice token |
195
+ | `TokenExpiredError` | `TOKEN_EXPIRED` | `auth` | the `offline` event: the token could not be refreshed |
196
+ | `NotRegisteredError` | `NOT_REGISTERED` | `validation` | `connect()` or `joinConference()` while not registered; registering, calling or choosing a device after `destroy()` |
197
+ | `MediaPermissionError` | `MEDIA_PERMISSION_DENIED` | `media` | `connect()`, `joinConference()` or `accept()`: the microphone could not be opened |
198
+ | `CallFailedError` | `CALL_FAILED` | `signaling` | `accept()` on a call that ended before it was answered; a call the provider could not set up, such as to an identity that is not registered, ends as `failed` with it |
199
+ | `CallRejectedError` | `CALL_REJECTED` | `signaling` | not raised yet: a call the other side declines ends as `busy` |
200
+ | `NetworkError` | `NETWORK` | `network` | `register()`, when the token provider keeps failing or the calling provider cannot be loaded; a call whose connection failed or was lost |
201
+ | `CapabilityUnsupportedError` | `CAPABILITY_UNSUPPORTED` | `capability` | `setOutputDevice()` where the browser cannot choose the speaker; listing devices over plain HTTP |
202
+ | `SentVoiceError` | `INVALID_ADDRESS` | `validation` | `connect()` or `joinConference()` with a `to` or `name` that does not fit |
203
+ | `SentVoiceError` | `CALL_IN_PROGRESS` | `validation` | `connect()` or `joinConference()` while a call is in progress or an incoming call is waiting for an answer |
204
+ | `SentVoiceError` | `UNKNOWN` | `signaling` | anything else, such as `register()` failing because notifications are blocked |
205
+
206
+ Errors that happen outside a method call arrive as events: the client's `error` when a token refresh fails, and a call's `error`, followed by `disconnected` with `{ state: 'failed', error }`. The underlying provider error is not exposed on the error; the SDK reports it to Sent as telemetry.
207
+
208
+ ## Devices
209
+
210
+ ```ts
211
+ import type SentVoice from '@sentdm/voice-client';
212
+
213
+ const isHeadset = (device: MediaDeviceInfo) => device.label.includes('Headset');
214
+
215
+ export async function pickHeadset(client: SentVoice) {
216
+ const microphone = (await client.audio.inputDevices()).find(isHeadset);
217
+ if (microphone) await client.audio.setInputDevice(microphone.deviceId);
218
+ const speaker = (await client.audio.outputDevices()).find(isHeadset);
219
+ if (speaker) await client.audio.setOutputDevice(speaker.deviceId);
220
+ }
221
+
222
+ export function followHeadset(client: SentVoice) {
223
+ client.audio.on('deviceChanged', () => pickHeadset(client).catch(console.error));
224
+ }
225
+ ```
226
+
227
+ - `inputDevices()` and `outputDevices()` list the microphones and speakers. Browsers leave out labels, and may list a single device, until the user has allowed microphone access in the page, which the first call asks for.
228
+ - `setInputDevice()` switches the microphone of the call in progress and of later calls. While the device is missing, the default one is used.
229
+ - `setOutputDevice()` plays calls through a speaker. It throws a `CapabilityUnsupportedError` in browsers that cannot choose the audio output.
230
+ - Both work before `register()`, and take effect when it runs. To start with saved devices, pass `audio: { inputDeviceId, outputDeviceId }`.
231
+ - `deviceChanged` fires when a device is plugged in or out.
232
+
233
+ ## Ringtones
234
+
235
+ The SDK plays no ringtone or ringback. Play your own on `incomingCall` and when a placed call is `ringing`, and stop it when the call is answered or ends:
236
+
237
+ ```ts
238
+ import type SentVoice from '@sentdm/voice-client';
239
+
240
+ const ringtone = new Audio('/sounds/ringtone.mp3');
241
+ ringtone.loop = true;
242
+
243
+ const play = () => ringtone.play().catch(() => {});
244
+ const stop = () => {
245
+ ringtone.pause();
246
+ ringtone.currentTime = 0;
247
+ };
248
+
249
+ export function ringForIncomingCalls(client: SentVoice) {
250
+ client.on('incomingCall', (invite) => {
251
+ play();
252
+ invite.on('accepted', stop).on('rejected', stop).on('cancelled', stop);
253
+ });
254
+ }
255
+
256
+ export async function callWithRingback(client: SentVoice, to: string) {
257
+ const call = await client.connect({ to });
258
+ call.on('ringing', play).on('connected', stop).on('disconnected', stop);
259
+ return call;
260
+ }
261
+ ```
262
+
263
+ ## React
264
+
265
+ For React 18 and 19, [`@sentdm/voice-client-react`](https://www.npmjs.com/package/@sentdm/voice-client-react) wraps one client in a provider, with hooks that re-render on its events.
266
+
267
+ ## Telemetry
268
+
269
+ The SDK reports usage and call quality data to Sent, authenticated with the voice token:
270
+
271
+ - the SDK version, and the browser, its major version, the operating system, its version and the device type, as read from the user agent (the user agent itself is not sent)
272
+ - how long `register()` took, its attempts and, when it failed, its error; how long `unregister()` took
273
+ - for each call: its id, direction and outcome, how long it took to connect and how long it lasted, and at its end the average round-trip time, jitter and packet loss of samples taken every 10 seconds
274
+ - the errors the client and its calls report: code, category, whether retriable, the message, and the underlying provider error with its name, message, stack and properties
275
+
276
+ No audio, phone numbers or identities are added by the SDK; the underlying provider error travels as the provider produced it. Batches go to `https://api.sent.dm/v3/voice/telemetry` 3 seconds after the first event queued, so events that happen together share one request, and at once when a call ends, when the page is hidden and when the client is destroyed. A batch that fails, or that Sent refuses (for example with a 429 or a 5xx), is retried once, with the next batch or within 30 seconds, then dropped; telemetry never throws to your app and never delays a call. Turn it off with `telemetry: { disabled: true }`.
277
+
278
+ ## Browser realities
279
+
280
+ - WebRTC requires a secure context: serve your app over HTTPS, or from `localhost` while developing. Over plain HTTP the browser hides the microphone and service worker APIs, so registering and calling fail. It is the first thing that breaks when you open the app from another device on your network.
281
+ - A page refresh or tab close ends any live call: WebRTC media dies with the page, and there is no resuming a call across reloads.
282
+ - Laptop sleep or a network switch does not unregister the client: incoming calls reach it through the browser's push service, which reconnects by itself, so the client stays `registered`, but calls that arrive while the device sleeps or is offline are missed. If a token refresh falls in that gap, the client goes `offline` and tries again about every 30 seconds until it is `registered` again (state passes `offline → registered`).
283
+ - One `SentVoice` instance per tab. The same identity registered in several tabs/devices creates independent per-device registrations at the provider (documented provider behavior); which devices ring on an incoming call is the provider's delivery semantics — inherited as-is, pinned down empirically during implementation testing. Every device rings, the first to answer takes the call, and the others see their invite cancelled. Tabs of one browser share a registration, so they all ring.
284
+ - Tokens are per-registration; each tab runs its own `tokenProvider` calls.
@@ -0,0 +1,3 @@
1
+ import type { AdapterFactory } from "./types.mjs";
2
+ export declare const loadAdapter: AdapterFactory;
3
+ //# sourceMappingURL=loader.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.d.mts","sourceRoot":"","sources":["../src/adapter/loader.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAgB;AAE9C,eAAO,MAAM,WAAW,EAAE,cAAiC,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { AdapterFactory } from "./types.js";
2
+ export declare const loadAdapter: AdapterFactory;
3
+ //# sourceMappingURL=loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../src/adapter/loader.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAgB;AAE9C,eAAO,MAAM,WAAW,EAAE,cAAiC,CAAC"}
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadAdapter = void 0;
4
+ const sinch_1 = require("./sinch.js");
5
+ exports.loadAdapter = sinch_1.loadSinchAdapter;
6
+ //# sourceMappingURL=loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../src/adapter/loader.ts"],"names":[],"mappings":";;;AAAA,sCAA2C;AAG9B,QAAA,WAAW,GAAmB,wBAAgB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { loadSinchAdapter } from "./sinch.mjs";
2
+ export const loadAdapter = loadSinchAdapter;
3
+ //# sourceMappingURL=loader.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.mjs","sourceRoot":"","sources":["../src/adapter/loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,oBAAgB;AAG3C,MAAM,CAAC,MAAM,WAAW,GAAmB,gBAAgB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { AdapterFactory } from "./types.mjs";
2
+ export declare const loadSinchAdapter: AdapterFactory;
3
+ //# sourceMappingURL=sinch.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sinch.d.mts","sourceRoot":"","sources":["../src/adapter/sinch.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,cAAc,EAMf,oBAAgB;AAmDjB,eAAO,MAAM,gBAAgB,EAAE,cAM5B,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { AdapterFactory } from "./types.js";
2
+ export declare const loadSinchAdapter: AdapterFactory;
3
+ //# sourceMappingURL=sinch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sinch.d.ts","sourceRoot":"","sources":["../src/adapter/sinch.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,cAAc,EAMf,mBAAgB;AAmDjB,eAAO,MAAM,gBAAgB,EAAE,cAM5B,CAAC"}