@16x/webphone-sdk 3.1.10 → 3.2.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 CHANGED
@@ -1,214 +1,211 @@
1
- # CCBar WebPhone SDK Core
2
-
3
- `@16x/webphone-sdk` 是 CCBar WebPhone 的 TypeScript SDK。Core 提供无 UI 的连接、通话、媒体设备、重连和脱敏诊断能力,可用于 Vue、React、原生 Web 与受支持的移动浏览器。
4
-
5
- ## 安装
6
-
7
- ```bash
8
- npm install @16x/webphone-sdk
9
- ```
10
-
11
- 公开包地址:[https://www.npmjs.com/package/@16x/webphone-sdk](https://www.npmjs.com/package/@16x/webphone-sdk)。任何人都可以直接安装,不需要 token。
12
-
13
- 浏览器只应接收客户服务端下发的会话(已含短期 WSS ticket 与 SIP 凭证)。不要把 `appSecret`、AES 密钥、固定 SIP 密码或 TURN 密码写入前端代码、示例、日志或构建产物。客户从安装到首通的完整路径见 `docs/quick-start.md`,PC/H5 选择见 `docs/guides/mobile-h5.md`。
14
-
15
- ## Core 快速开始
16
-
17
- ```ts
18
- import { CCBarClient } from '@16x/webphone-sdk';
19
-
20
- const client = new CCBarClient({
21
- platform: 'web',
22
- sessionProvider: {
23
- // 你们自己的会话接口:返回一份 WebPhoneSession,签名与解密都留在服务端
24
- createSession: async () => {
25
- const response = await fetch('/get-session', { method: 'POST', credentials: 'include' });
26
- if (!response.ok) throw new Error('获取会话失败');
27
- return response.json();
28
- },
29
- },
30
- });
31
-
32
- await client.initialize();
33
- await client.connect();
34
-
35
- const call = await client.dial({ destination: '13800138000' });
36
- await call.hangup();
37
-
38
- await client.disconnect();
39
- await client.dispose();
40
- ```
41
-
42
- 在 SPA 中应由应用级容器持有一个 `CCBarClient`。`connect()`、`disconnect()` 和 `dispose()` 支持幂等调用;组件卸载或应用退出时必须调用 `dispose()` 释放 Timer、Listener、WebSocket、PeerConnection、Track 和 Audio 资源。
43
-
44
- 根入口与 `/diagnostics` 可在 SSR/Node 导入阶段安全加载,不会访问 `window`、`document` 或 `navigator`。浏览器媒体能力只在初始化或后续操作时检测。
45
-
46
- ## 主要 API
47
-
48
- - `CCBarClient.initialize/connect/disconnect/dispose`
49
- - `CCBarClient.dial/getCalls/getActiveCall/setActiveCall`
50
- - `CCBarClient.setAgentStatus`
51
- - `CCBarClient.media` 设备与音频控制
52
- - `CCBarClient.on/off` 类型化事件
53
- - `CCBarClient.getDiagnostics` 脱敏诊断报告
54
- - `CCBarCall.mute/unmute/hold/resume/sendDtmf/transfer/hangup`
55
-
56
- 构造参数里几个按需用的:`sessionProvider`(**必填**,4.0.0 起唯一会话来源,见下一节)、
57
- `sipKeepaliveSeconds`(3.1.1+,默认 25 秒重发 REGISTER 撑住 NAT/代理上的长连接,`0` 关闭)、
58
- `sharedWorker`(多标签页入口)、`baseUrl`、`maxConcurrentCalls`。
59
-
60
- 拨号时 SDK 会在 `pcConfig` 里带上 `iceCandidatePoolSize: 2`(3.1.2+):JsSIP 要等 ICE 收集完成才发
61
- INVITE,预收集候选才能做到「点完就出局」。
62
-
63
- PC Web 支持完整 Core 通话能力。移动 H5 首期仅承诺受支持系统浏览器中的前台单路外呼;来电、保持、转接、后台和锁屏通话不在承诺范围内。
64
-
65
- ## 会话来源(sessionProvider)
66
-
67
- 会话由接入方在自己的服务端拼好后交给 SDK:**4.0.0 起这是唯一的会话来源**(3.x 的 `tokenProvider`
68
- 以及 SDK 自己请求平台会话接口的能力已移除,迁移方式见 `CHANGELOG.md` 的 4.0.0 条目)。
69
-
70
- ```ts
71
- const client = new CCBarClient({
72
- platform: 'web',
73
- sessionProvider: {
74
- // 必填:返回一份 WebPhoneSession(sip.uri / sip.registerTicket / transport.wssUrl / iceServers / capabilities…)
75
- createSession: async () => {
76
- const response = await fetch('/get-session', { method: 'POST', credentials: 'include' });
77
- if (!response.ok) throw new Error('获取坐席账号失败');
78
- return response.json();
79
- },
80
- // 可选:缺省时 SDK 用 createSession 重建会话(会话到期前会调用一次)
81
- // refreshSession: async (sessionId) => { ... },
82
- // 可选:缺省时 setAgentStatus 抛 CAPABILITY_NOT_SUPPORTED,不静默失败
83
- // setAgentStatus: async ({ status, reason }) => { ... },
84
- },
85
- });
86
-
87
- await client.connect({ extension: '8001' });
88
- ```
89
-
90
- SDK 从会话里读的字段:
91
-
92
- | 字段 | 用途 |
93
- |---|---|
94
- | `sip.uri` / `sip.registrar` | REGISTER 的账号与 registrar |
95
- | `sip.registerTicket` | SIP 密码(旧平台就是服务端解出来的那个) |
96
- | `sip.registerExpires` | REGISTER 有效期(秒),没给或非正数按 300 |
97
- | `transport.wssUrl` | 软电话地址;只剥离 `ticket` 参数,`?token=` 原样保留 |
98
- | `transport.ticket` | 走 `xcall-ticket.<ticket>` 子协议;为空则只声明 `sip` |
99
- | `iceServers` / `capabilities` | ICE 配置 / 能力开关(outbound、inbound、mute、dtmf、hold、blind_transfer) |
100
-
101
- **密钥与密码都留在服务端**:签名的 appSecret、坐席密码的解密密钥都不要下发浏览器;
102
- 会话(含 SIP 密码)在你们自己的服务端拼好,页面只拿结果。
103
-
104
- ## Legacy Adapter
105
-
106
- 只有显式导入 Legacy 子路径才会在浏览器附加 `window.CCBarSDK`;根入口没有该副作用。Legacy 子路径同样可以安全地在 SSR 中导入。
107
-
108
- ```ts
109
- import { CCBarSDK } from '@16x/webphone-sdk/legacy';
110
-
111
- const legacy = new CCBarSDK({
112
- platform: 'web',
113
- sessionProvider: {
114
- createSession: async () => fetch('/get-session', {
115
- method: 'POST',
116
- credentials: 'include',
117
- }).then(response => response.json()),
118
- },
119
- });
120
-
121
- await legacy.login();
122
- const call = await legacy.call('13800138000');
123
- await legacy.hangup();
124
- await legacy.signOut();
125
- ```
126
-
127
- ### `createLegacySessionProvider`:老平台的会话来源
128
-
129
- 老平台(`token/fs` + `seat/account/get` 这一套)可以把「取 token → 取坐席账号 → 解 SIP 密码 → 拼会话」
130
- 整段交给 SDK,页面只提供换票口子:
131
-
132
- ```ts
133
- import { createLegacySessionProvider } from '@16x/webphone-sdk/legacy';
134
-
135
- const client = new CCBarClient({
136
- sessionProvider: createLegacySessionProvider({
137
- host: 'https://vxapi.yundianlab.com',
138
- // 取 token 二选一:给函数,或给地址让 SDK 去 POST(都是你们自己的接口)
139
- getToken: () => fetch('/get-token', { method: 'POST' }).then(response => response.json()),
140
- // tokenUrl: '/get-token',
141
- }),
142
- });
143
- ```
144
-
145
- - **SDK 不碰 API SECRET**:`/openapi/v1/token/fs` 的加签始终在你们服务端,SDK 只拿结果;
146
- - **AES key 会随包下发**:老平台那把 16 字节 key/iv 是固定值(旧版 `ccbar.js` 里同样是硬编码),
147
- 可用 `aesKey` / `aesIv` 覆盖;介意的话就走服务端拼会话(见下一节);
148
- - 坐席状态:`client.setAgentStatus(...)` 打到 `{host}/openapi/token/v1/seats/set-status`,
149
- 其中 `extension` 传的是**坐席账号**(`username`,可能带企业前缀);
150
- - **浏览器直连网关**:需要网关放行 CORS 与调用方 IP(`seat/account/get` 有 IP 白名单),部署前先确认;
151
- - 取票与 SIP 密码都按有效期提前换(`refreshBufferSeconds`,默认 180 秒)。
152
-
153
- ### `decryptSipPassword`:解 `seat/account/get` 返回的 `password`
154
-
155
- `password`(或 `registerPassword`)是 **AES-128-CBC/Pkcs7 的密文、Base64 编码**,
156
- SDK 把解密单独导出,服务端(Node 18+)与页面都能用:
157
-
158
- ```ts
159
- import { decryptSipPassword } from '@16x/webphone-sdk/legacy';
160
-
161
- const password = await decryptSipPassword(data.password); // 明文 SIP 密码
162
- // 环境用的是别的 key/iv 时:await decryptSipPassword(data.password, { key, iv })
163
- ```
164
-
165
- - key/iv 各 16 字节,**默认是平台内置固定值**(与旧版 `ccbar.js` 一致),可用 `key`/`iv` 覆盖;
166
- - 失败抛 `CCBarError`,可读原因在 `error.cause`(密文为空 / 不是合法 Base64 / 解密失败);
167
- - 依赖 WebCrypto:浏览器需安全上下文(https / localhost),Node 需 18+;
168
- - `createLegacySessionProvider` 内部用的就是它。
169
-
170
- 旧方法映射如下:
171
-
172
- | Legacy | Core |
173
- |---|---|
174
- | `getToken()` / `getAccount()` / `login()` | `client.connect()` |
175
- | `call(number)` | `client.dial({ destination: number })` |
176
- | `insideCall(number)` | `client.dial({ destination: number, type: 'extension' })` |
177
- | `answer()` | 当前 `call.answer()` |
178
- | `hangup()` | 当前 `call.hangup()` |
179
- | `holdCall()` / `unholdCall()` | 当前 `call.hold()` / `call.resume()` |
180
- | `transSo(number)` | 当前 `call.transfer({ target: number, type: 'blind' })` |
181
- | `setId()` / `setRe()` | `client.setAgentStatus('available' / 'break')` |
182
- | `signOut()` | `client.disconnect()` 后 `client.dispose()` |
183
-
184
- `insideCall(number)` 会使用 `type: 'extension'` 标记 SIP 呼叫(INVITE 上带 `X-XCall-Destination-Type: extension`),
185
- 浏览器不拼接客户前缀;后端按当前会话租户解析分机并路由到同租户坐席。普通 `dial()` 不传 type,继续按外呼处理。
186
-
187
- 注意:如果你们的网关只认「企业前缀 + 分机号」,`type: 'extension'` 这个头它不解析,
188
- 就需要自己拼好前缀再拨(`dial({ destination: \`${prefix}${number}\` })`),
189
- 具体见客户示例仓库的 `prefixExtension()`。
190
-
191
- `setBu()` 不会伪造 `busy`。该方法抛出 `DeprecatedError`,因为忙碌状态应由通话状态或服务端状态策略驱动。所有 Legacy 方法仅在开发构建中各警告一次,生产构建不输出弃用日志。`signOut()` 是终态操作;如需保留实例以便重新连接,请直接使用 Core 的 `disconnect()`。
192
-
193
- ## 版本与更新记录
194
-
195
- 当前版本见 `package.json`,每个版本改了什么见 [CHANGELOG.md](./CHANGELOG.md)。
196
- 最近几个版本:4.0.0 移除 `tokenProvider`(只保留 `sessionProvider`,破坏性变更);3.1.0 新增 `sessionProvider`;3.1.1 新增 SIP 保活;3.1.2 拨号带 ICE 候选池。
197
-
198
- ## 开发与验收
199
-
200
- ```bash
201
- npm run lint
202
- npm run typecheck
203
- npm test
204
- npm run build
205
- npm run pack:check
206
- ```
207
-
208
- 正式包通过 `exports` 提供根入口、`/ui`、`/legacy`、`/diagnostics` 和 `/styles.css`,不包含测试、示例、Source Map 或凭证文件。
209
-
210
- ## License
211
-
212
- Copyright 16X. All rights reserved.
213
-
214
- Private customer distribution only. No redistribution or sublicensing without written permission.
1
+ # CCBar WebPhone SDK
2
+
3
+ `@16x/webphone-sdk` 是浏览器端 WebPhone SDK。客户只需提供服务端取票接口,并在前端配置 `createTokenSessionProvider`。SDK 会在内部获取坐席账号、创建会话、登录 SIP、刷新凭证并请求坐席状态。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install @16x/webphone-sdk
9
+ ```
10
+
11
+ 公开包地址:[npmjs](https://www.npmjs.com/package/@16x/webphone-sdk)。
12
+
13
+ ## 最简接入
14
+
15
+ ### 1. 服务端实现取票接口
16
+
17
+ 浏览器通过客户自己的 `POST /get-token` 接口取短期 fs token。服务端从已登录用户的会话中取得分机号,使用服务端环境变量中的 APP Key 和 security(APP Secret)签名调用 `POST /openapi/v1/token/fs`,然后返回上游的 token 和 expires。不要接受浏览器提交的 APP Key、security 或任意分机号。
18
+
19
+ 取票请求与签名示例见[会话服务指南](docs/guides/session-service.md)。
20
+
21
+ ### 2. 创建客户端并连接
22
+
23
+ ```ts
24
+ import { CCBarClient, createTokenSessionProvider } from '@16x/webphone-sdk';
25
+
26
+ const client = new CCBarClient({
27
+ platform: 'web',
28
+ sessionProvider: createTokenSessionProvider({
29
+ // 请向商务经理或对接人获取 HTTP API 网关地址
30
+ host: 'https://api.example.com',
31
+ // 请向商务经理或对接人获取完整的 SIP WebSocket 地址
32
+ sipWsUrl: 'wss://sip.example.com/api/fs/sip-ws',
33
+ // 这是客户需要实现的唯一服务端方法
34
+ getToken: async () => {
35
+ const response = await fetch('/get-token', {
36
+ method: 'POST',
37
+ credentials: 'include',
38
+ });
39
+ if (!response.ok) throw new Error('获取 token 失败');
40
+ return response.json();
41
+ },
42
+ }),
43
+ });
44
+
45
+ await client.initialize();
46
+ await client.connect();
47
+ ```
48
+
49
+ `host` 用于 SDK 内部请求坐席账号和状态接口;`sipWsUrl` 是完整的 SIP WebSocket 地址。get-token 应返回 `{ token, expires }`(或上游 token 响应)。
50
+
51
+ 请向商务经理或对接人获取 HTTP API 网关地址 `host` 和完整的 SIP WebSocket 地址 `sipWsUrl`。`host` 用于坐席账号/状态接口;`sipWsUrl` 应包含 `wss://` 协议和服务端提供的路径。另请获取 APP Key、security(仅供客户服务端实现 `getToken`,不要放进浏览器代码)以及话单查询/推送接口资料;话单由客户业务系统另行集成,SDK 不提供查询或推送能力。
52
+
53
+ ### 3. 拨打、控制和挂断
54
+
55
+ ```ts
56
+ const call = await client.dial({ destination: '9196' });
57
+ await call.mute();
58
+ await call.unmute();
59
+ await call.hold();
60
+ await call.resume();
61
+ await call.hangup();
62
+ ```
63
+
64
+ ### 4. 传递 userData 并关联话单
65
+
66
+ 拨打时可以通过 `userdata` 传递业务关联信息。SDK 会把它放进 SIP `INVITE` 请求的 `X-User-Data` header,供平台侧读取:
67
+
68
+ ```ts
69
+ const userdata = encodeURIComponent(JSON.stringify({
70
+ orderId: 'ORDER-20261003-001',
71
+ source: 'crm',
72
+ }));
73
+
74
+ const call = await client.dial({
75
+ destination: '9196',
76
+ userdata,
77
+ });
78
+ ```
79
+
80
+ `userdata` 必须是字符串;未传、空字符串或纯空白不会发送该 header。值只支持可见 ASCII,不能包含换行、制表符或未编码的中文;传入不合规内容会以 `CALL_INVALID_USERDATA` 拒绝拨打。像上例一样先对 JSON 做 `encodeURIComponent()`,平台侧收到 `X-User-Data` 后再 `decodeURIComponent()` 和解析即可。不要放入密码、token 等敏感信息,因为 SIP header 可能经过或记录在平台网络设备中。
81
+
82
+ SDK 负责把 `userdata` 透传到 SIP 请求。若要让它出现在话单推送或话单查询结果中,平台服务端还需要读取 `X-User-Data`,将其解析并保存到话单字段;请与平台侧确认字段映射和推送、查询接口是否都返回该字段。SDK 不会直接写入或查询话单。
83
+
84
+ ### 5. 查看连接状态、坐席状态和拨打结果
85
+
86
+ 通过 `client.on()` 监听连接状态。连接注册成功和连接状态是不同事件:
87
+
88
+ ```ts
89
+ const offConnection = client.on('connection.stateChanged', ({ state }) => {
90
+ // offline | connecting | connected | reconnecting | failed
91
+ console.log('SIP connection:', state);
92
+ });
93
+
94
+ const offRegistered = client.on('connection.registered', () => {
95
+ console.log('SIP registered');
96
+ });
97
+ ```
98
+
99
+ 调用 `setAgentStatus()` 修改坐席状态;`agent.statusChanged` 在状态接口成功后发出:
100
+
101
+ ```ts
102
+ const offAgentStatus = client.on('agent.statusChanged', ({ status }) => {
103
+ console.log('Agent status:', status); // available | break | offline
104
+ });
105
+
106
+ await client.setAgentStatus('available');
107
+ ```
108
+
109
+ 坐席状态表示坐席在业务平台上的工作状态,不表示 SIP WebSocket 是否已连接,也不表示当前通话状态:
110
+
111
+ | 坐席状态 | 含义 |
112
+ |---|---|
113
+ | `available` | 坐席空闲/就绪;是否能接听新呼叫仍由平台的队列和路由策略决定。 |
114
+ | `break` | 坐席暂时休息或暂停接听,平台侧对应 `On Break`。 |
115
+ | `offline` | 坐席在业务平台登出,平台侧对应 `Logged Out`。它本身不关闭 SIP 连接;如需断开连接,还要调用 `client.disconnect()`。 |
116
+
117
+ 因此,`client.setAgentStatus('offline')` 和 `client.disconnect()` 是两个独立操作:前者更新平台坐席状态,后者断开 SDK 的 SIP 连接。反过来,连接掉线也不一定会自动把坐席状态改为 `offline`。
118
+
119
+ `client.dial()` 成功返回 Call 对象不代表通话已接通。监听 `call.stateChanged` 并检查 `to` 是否为 `active`;结束或失败分别通过 `call.ended`、`call.failed` 通知:
120
+
121
+ ```ts
122
+ const offCallState = client.on('call.stateChanged', ({ callId, from, to }) => {
123
+ console.log(`Call ${callId}: ${from} -> ${to}`);
124
+ // new | dialing | ringing | connecting | active | held | ended | failed
125
+ });
126
+
127
+ const offCallEnded = client.on('call.ended', ({ callId, reason }) => {
128
+ console.log('Call ended:', callId, reason);
129
+ });
130
+
131
+ const offCallFailed = client.on('call.failed', ({ callId, error }) => {
132
+ showCallError(error.code);
133
+ });
134
+ ```
135
+
136
+ 拨号发起阶段的错误会让 `dial()` 拒绝;接通后的通话错误通过 `call.failed` 事件报告。SDK 错误是 `CCBarError`,可根据稳定的 `code` 或 `category` 映射为客户界面的提示,不要直接展示底层网络错误、服务端原文或凭据:
137
+
138
+ ```ts
139
+ import { CCBarError } from '@16x/webphone-sdk';
140
+
141
+ try {
142
+ await client.dial({ destination: '9196' });
143
+ } catch (error) {
144
+ if (error instanceof CCBarError) {
145
+ showCallError(error.code);
146
+ } else {
147
+ showCallError('UNKNOWN_ERROR');
148
+ }
149
+ }
150
+ ```
151
+
152
+ 通话状态含义如下。`ended` 和 `failed` 都是终态;一个 Call 到达终态后不会再恢复到其他状态。
153
+
154
+ | 状态 | 含义 |
155
+ |---|---|
156
+ | `new` | SDK 已创建 Call 对象,呼叫尚未开始处理;通常是很短暂的初始状态。 |
157
+ | `dialing` | SDK 已发起呼叫,正在等待对端响应。 |
158
+ | `ringing` | 对端正在振铃,或呼叫流程已进入振铃/早期进展阶段。 |
159
+ | `connecting` | 呼叫已应答,正在完成媒体协商或建立音频连接;此时还不能当作通话已接通。 |
160
+ | `active` | 通话已接通,可以进行通话控制。 |
161
+ | `held` | 通话已保持;调用 `call.resume()` 可恢复到 `active`。 |
162
+ | `ended` | 通话已正常结束、被挂断或被拒接。 |
163
+ | `failed` | 呼叫建立或通话过程中发生失败;查看 `call.failed` 事件中的错误码。 |
164
+
165
+ `client.on()` 返回取消监听函数。页面或组件卸载时调用对应的 `off...()`,或在客户端销毁时统一清理监听器。
166
+
167
+ ## 网络和安全要求
168
+
169
+ - 页面须运行在 HTTPS 或 localhost,并取得浏览器麦克风权限。
170
+ - 网关须允许页面的 Origin 和客户出口 IP 调用坐席账号与状态接口;这两项与客户自己的 get-token 接口是分开的,请在上线前确认 CORS 和 IP 白名单。
171
+ - APP Key 和 security 只保存在服务端。浏览器运行时会持有短期 fs token 和 SIP 凭证;不要记录含 token 的 WSS URL、Authorization 请求头或 SIP 密码。
172
+ - 按客户系统的登录态调用 get-token,避免浏览器自行指定分机号。
173
+
174
+ ## 连接生命周期
175
+
176
+ 建议在系统账号登录成功或进入拨打列表页时调用 `client.connect()`。退出账号或离开拨打列表页时,按需先调用 `client.setAgentStatus('offline')`,再调用 `client.disconnect()`。客户端实例不再使用时调用 `client.dispose()` 释放监听器、定时器、连接和媒体资源。重复调用正在连接或已连接的 `connect()` 会复用当前连接。
177
+
178
+ ## 常用 API
179
+
180
+ - `client.initialize()`、`client.connect()`、`client.disconnect()`、`client.dispose()`
181
+ - `client.dial({ destination, userdata? })`
182
+ - `client.setAgentStatus('available' | 'break' | 'offline')`
183
+ - `call.mute()`、`call.unmute()`、`call.hold()`、`call.resume()`、`call.hangup()`
184
+ - `client.on(event, handler)`、`client.getDiagnostics()`
185
+
186
+ 详细 API 和完整事件说明见[客户端 API 文档](docs/api/client.md);服务端取票签名示例见[会话服务指南](docs/guides/session-service.md)。
187
+
188
+ ## 开发与验证
189
+
190
+ ```bash
191
+ npm run lint
192
+ npm run typecheck
193
+ npm test
194
+ npm run pack:check
195
+ ```
196
+
197
+ ## License
198
+
199
+ Copyright 16X. All rights reserved.
200
+
201
+ Private customer distribution only. No redistribution or sublicensing without written permission.
202
+
203
+ ## Live acceptance (maintainers)
204
+
205
+ Real-call acceptance is opt-in and runs outside `npm test`. Copy `scripts/live-call-acceptance.env.example` to a local `.env` file, fill it with a dedicated test account, and start a Chromium-based browser with remote debugging enabled at `CCBAR_LIVE_CDP_URL`. The runner requests microphone permission through CDP and uses only routes 9196 and 9199.
206
+
207
+ ```sh
208
+ npm run test:live
209
+ ```
210
+
211
+ The runner verifies token signing, account lookup, SIP registration, seat status, repeated `connect()`, disconnect/reconnect, call controls, credential failures, delayed/lost HTTP requests, browser network loss, and malformed/unreachable STUN behavior. Repeated-connect assertions compare SDK token, account, session, connect, and registration-event counts; they do not claim server-side REGISTER packet counts. It measures dial-to-active time against `CC_DIAL_ACTIVE_SLO_MS`; each sample must meet the threshold. JSON output contains scenario, stage, status codes, elapsed times, and SDK resource counts; it omits credentials, tokens, raw errors, and WebSocket URLs. Live tests require the gateway Origin/IP allowlists, browser microphone permission, reachable WSS, and both test routes.
@@ -1,6 +1,6 @@
1
1
  import { n as e, t } from "./redact-DB3CMbsK.js";
2
2
  //#region src/version.ts
3
- var n = "3.1.10", r = class {
3
+ var n = "3.2.0", r = class {
4
4
  capacity;
5
5
  entries = [];
6
6
  constructor(e) {
@@ -0,0 +1 @@
1
+ const e=require("./redact-Bq26pObO.cjs");var t=`3.2.0`,n=class{capacity;entries=[];constructor(e){if(this.capacity=e,!Number.isInteger(e)||e<=0)throw RangeError(`RingBuffer capacity must be a positive integer`)}push(e){this.entries.length===this.capacity&&this.entries.shift(),this.entries.push(e)}toArray(){return[...this.entries]}},r=`@16x/webphone-sdk`,i=200,a=class{dependencies;logs=new n(i);now;constructor(e){this.dependencies=e,this.now=e.now??(()=>new Date)}log(t,n,r){this.logs.push({timestamp:this.now().toISOString(),level:t,message:e.t(n),...r===void 0?{}:{context:e.n(r)}})}async getReport(n={}){let i=n.includeRecentLogs!==!1,a=n.includeDeviceNames===!0,u=n.maskPhoneNumbers!==!1,d=this.dependencies.getSnapshot(),f=this.dependencies.getPermission===void 0?d.permission:await o(this.dependencies.getPermission,d.permission),p=a?await s(this.dependencies.getDeviceNames):void 0,m=l(c(this.dependencies.getUserAgent)),h={generatedAt:this.now().toISOString(),sdk:{name:r,version:t},browser:m.browser,os:m.os,...d,permission:f,recentLogs:i?this.logs.toArray():[],...p===void 0?{}:{deviceNames:p}};return e.n(h,{maskPhoneNumbers:u})}};async function o(e,t){try{return await e()}catch{return t}}async function s(e){if(e===void 0)return[];try{return[...await e()]}catch{return[]}}function c(e){if(e!==void 0)try{return e()}catch{return``}return typeof navigator>`u`?``:navigator.userAgent}function l(e){return{browser:u(e),os:d(e)}}function u(e){for(let[t,n]of[[`Edge`,/Edg\/([\d.]+)/],[`Chrome`,/(?:Chrome|CriOS)\/([\d.]+)/],[`Firefox`,/(?:Firefox|FxiOS)\/([\d.]+)/],[`Safari`,/Version\/([\d.]+).*Safari/]]){let r=e.match(n);if(r?.[1]!==void 0)return{name:t,version:r[1]}}return{name:`unknown`,version:``}}function d(e){let t=e.match(/(?:iPhone|iPad).*OS ([\d_]+)/);if(t?.[1]!==void 0)return{name:`iOS`,version:t[1].replaceAll(`_`,`.`)};let n=e.match(/Android ([\d.]+)/);if(n?.[1]!==void 0)return{name:`Android`,version:n[1]};let r=e.match(/Windows NT ([\d.]+)/);if(r?.[1]!==void 0)return{name:`Windows`,version:r[1]};let i=e.match(/Mac OS X ([\d_]+)/);return i?.[1]===void 0?/Linux/.test(e)?{name:`Linux`,version:``}:{name:`unknown`,version:``}:{name:`macOS`,version:i[1].replaceAll(`_`,`.`)}}Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return n}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return t}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return a}});
@@ -1 +1 @@
1
- const e=require("./SharedWorkerProtocol-39dch6xL.cjs");require("./createJsSipFactory-Dr8HbLl-.cjs");function t(e){let t=new Map;return{peerConnection:{getSenders:()=>e.getSenders().map(n=>{let r=t.get(n);if(r!==void 0)return r;let i={get track(){return n.track},get senderKind(){let t=n.track;if(t!==null)return t.kind===`audio`||t.kind===`video`?t.kind:void 0;let r=e.getTransceivers().find(e=>e.sender===n)?.receiver.track;if(r?.kind===`audio`||r?.kind===`video`)return r.kind;let i=n.getParameters().codecs?.find(e=>typeof e.mimeType==`string`);if(i?.mimeType?.toLowerCase().startsWith(`video/`))return`video`;if(i?.mimeType?.toLowerCase().startsWith(`audio/`))return`audio`},replaceTrack:e=>n.replaceTrack(e)};return t.set(n,i),i}),close:()=>{e.connectionState!==`closed`&&e.close()}},trackSource:{onTrack(t){let n=e=>{let n=e.track;t({streams:[...e.streams],track:n})};return e.addEventListener(`track`,n),()=>e.removeEventListener(`track`,n)}}}}var n=300,r=25,i=2,a=``,o=120;function s(e){let t=Math.floor(Number(e.sip.registerExpires));return Number.isFinite(t)&&t>0?t:n}var c=class{factory;transportBaseUrl;keepaliveSeconds;agentTerminal;sessionTimers;ua;uaListeners;incomingHandlers;iceServers;pendingConnection;disposed=!1;warmPc;keepaliveTimer;keepaliveBusy=!1;registerExpiresSeconds=0;connectionHandlers=new Set;constructor(e,t,n=r,i=a,o=!1){this.factory=e,this.transportBaseUrl=t,this.keepaliveSeconds=n,this.agentTerminal=i,this.sessionTimers=o}terminalHeaders(){return this.agentTerminal===``?[]:[`X-Agent-Terminal: ${this.agentTerminal}`]}warmIceCandidatePool(){if(this.closeWarmPc(),typeof RTCPeerConnection<`u`&&this.iceServers!==void 0)try{this.warmPc=new RTCPeerConnection({iceServers:[...this.iceServers],iceCandidatePoolSize:i})}catch{this.warmPc=void 0}}closeWarmPc(){try{this.warmPc?.close()}catch{}this.warmPc=void 0}keepaliveTick(e){if(this.isCurrentUa(e)&&!this.keepaliveBusy)try{if(typeof e.isConnected==`function`&&e.isConnected()===!1||typeof e.register!=`function`)return;this.keepaliveBusy=!0,e.register({extraHeaders:this.terminalHeaders()})}catch{}finally{this.keepaliveBusy=!1}}startKeepalive(e,t){if(this.stopKeepalive(),!Number.isFinite(this.keepaliveSeconds)||this.keepaliveSeconds<=0||t>0&&t<=this.keepaliveSeconds)return;let n=Math.max(10,Math.floor(this.keepaliveSeconds))*1e3;this.keepaliveTimer=setInterval(()=>this.keepaliveTick(e),n)}stopKeepalive(){this.keepaliveTimer!==void 0&&clearInterval(this.keepaliveTimer),this.keepaliveTimer=void 0}connect(t){this.assertNotDisposed(),this.disconnect(this.registrationError(`SIP connection replaced`));try{let e=this.factory.createUa({sockets:f(this.factory.createSocket(this.withoutTicket(this.resolveWssUrl(t.transport.wssUrl)),t.transport.ticket)),uri:t.sip.uri,password:t.sip.registerTicket,registrar_server:t.sip.registrar,register:!0,register_expires:s(t),session_timers:this.sessionTimers,...this.agentTerminal===``?{}:{user_agent:this.agentTerminal}});return this.ua=e,this.registerExpiresSeconds=s(t),this.iceServers=t.iceServers,this.uaListeners=[],this.startRegistration(e)}catch(t){let n=t instanceof e.i&&t.code===`CONFIG_INVALID`?t:this.transportError(t);return this.disconnect(n),Promise.reject(n)}}disconnect(e=this.registrationError(`SIP connection disconnected`)){this.stopKeepalive(),this.closeWarmPc();let t=this.pendingConnection;this.pendingConnection=void 0,this.ua!==void 0&&(this.removeUaListeners(this.ua),this.ua.stop()),this.ua=void 0,this.uaListeners=void 0,this.iceServers=void 0,t?.reject(e)}dial(t,n){let r=this.requireUa();if(t.trim()===``)throw new e.i(`CALL_INVALID_DESTINATION`,`call`,!1);try{let e=n===void 0?{mediaConstraints:{audio:!0,video:!1},sessionTimersExpires:o,pcConfig:{iceServers:this.iceServers,iceCandidatePoolSize:i}}:{mediaConstraints:{audio:!0,video:!1},sessionTimersExpires:o,pcConfig:{iceServers:[...n.iceServers],iceCandidatePoolSize:i},extraHeaders:[...this.terminalHeaders(),...n.extraHeaders,...n.type===`extension`?[`X-XCall-Destination-Type: extension`]:[]]};return new l(r.call(t,e))}catch(t){throw new e.i(`CALL_OPERATION_NOT_ALLOWED`,`call`,!1,{cause:t})}}onIncoming(e){this.assertNotDisposed();let t=this.incomingHandlers??new Set;return this.incomingHandlers=t,t.add(e),()=>t.delete(e)}onConnectionStateChanged(e){return this.disposed?()=>void 0:(this.connectionHandlers.add(e),()=>this.connectionHandlers.delete(e))}dispose(){this.disposed||(this.disposed=!0,this.disconnect(),this.incomingHandlers=void 0,this.connectionHandlers.clear())}getResourceCounts(){return{listeners:(this.uaListeners?.length??0)+(this.incomingHandlers?.size??0)+this.connectionHandlers.size,webSockets:this.ua===void 0?0:1}}handleIncoming(e){if(!u(e))return;let t=e.session;if(t===void 0)return;let n=new l(t);this.incomingHandlers?.forEach(e=>e(n))}startRegistration(e){let t=!0,n=!1,r=new Promise((t,n)=>{this.pendingConnection={ua:e,resolve:t,reject:n}});this.listen(e,`registered`,()=>{if(this.isCurrentUa(e)){if(t){n=!0;return}this.resolveConnection(e)}}),this.listen(e,`registrationFailed`,t=>{if(!this.isCurrentUa(e))return;let n=this.registrationError(this.registrationFailureCause(t));this.connectionHandlers.forEach(e=>e(`failed`,n)),this.disconnect(n)}),this.listen(e,`disconnected`,t=>{if(!this.isCurrentUa(e))return;let n=this.transportError(t);this.connectionHandlers.forEach(e=>e(`disconnected`,n)),this.disconnect(n)}),this.listen(e,`newRTCSession`,t=>{this.isCurrentUa(e)&&this.handleIncoming(t)});try{e.start()}catch(e){let t=this.transportError(e);this.connectionHandlers.forEach(e=>e(`failed`,t)),this.disconnect(t)}finally{t=!1}return n&&this.resolveConnection(e),r}listen(e,t,n){e.on(t,n),this.uaListeners?.push({event:t,handler:n})}removeUaListeners(e){this.uaListeners?.forEach(({event:t,handler:n})=>e.off(t,n))}isCurrentUa(e){return this.ua===e}resolveConnection(e){let t=this.pendingConnection;t?.ua===e&&(this.pendingConnection=void 0,t.resolve(),this.warmIceCandidatePool(),this.startKeepalive(e,this.registerExpiresSeconds),this.connectionHandlers.forEach(e=>e(`registered`)))}resolveWssUrl(e){let t=this.transportBaseUrl?.trim()===``||this.transportBaseUrl===void 0?typeof location>`u`?`http://localhost`:location.origin:this.transportBaseUrl,n=e.startsWith(`/`)?new URL(e,t):new URL(e);return n.protocol===`http:`&&(n.protocol=`ws:`),n.protocol===`https:`&&(n.protocol=`wss:`),this.assertAllowedWssUrl(n),n.toString()}assertAllowedWssUrl(t){if(t.protocol!==`wss:`&&!(t.protocol===`ws:`&&p(t.hostname)))throw new e.i(`CONFIG_INVALID`,`configuration`,!1,{cause:t.origin})}withoutTicket(e){let t=new URL(e);return t.searchParams.delete(`ticket`),t.toString()}requireUa(){if(this.assertNotDisposed(),this.ua===void 0)throw new e.i(`CALL_NOT_CONNECTED`,`call`,!1);return this.ua}assertNotDisposed(){if(this.disposed)throw new e.i(`SDK_ALREADY_DISPOSED`,`internal`,!1)}registrationError(t){return new e.i(`REGISTRATION_FAILED`,`registration`,!0,{cause:t})}transportError(t){return new e.i(`SIP_WS_UNAVAILABLE`,`registration`,!0,{cause:t})}registrationFailureCause(e){return typeof e==`object`&&e&&`cause`in e?e.cause:e}},l=class{session;pendingHoldReject;mediaRegistration;mediaConnection;mediaHandlers=new Set;mediaWatchStarted=!1;sessionUnsubscribeFromPeerConnection;constructor(e){this.session=e}get id(){return this.session.id}get from(){return this.session.from??m(this.session.remote_identity)}getMediaRegistration(){let e=this.session.connection;if(e!=null)return this.mediaRegistration!==void 0&&this.mediaConnection===e?this.mediaRegistration:(this.mediaConnection=e,this.mediaRegistration=t(e),this.mediaRegistration)}answer(e){this.session.answer({mediaConstraints:e.mediaConstraints,pcConfig:{iceServers:[...e.pcConfig.iceServers]}})}terminate(){this.pendingHoldReject?.(Error(`SIP session terminated`)),this.session.terminate()}hold(){return this.changeHoldState(`hold`)}unhold(){return this.changeHoldState(`unhold`)}mute(){this.session.mute()}unmute(){this.session.unmute()}sendDtmf(e){this.session.sendDTMF(e)}refer(e){this.session.refer(e)}on(e,t){return e===`mediaConnected`?(this.mediaHandlers.add(t),this.watchMediaConnection(),()=>{this.mediaHandlers.delete(t),this.mediaHandlers.size===0&&(this.sessionUnsubscribeFromPeerConnection?.(),this.sessionUnsubscribeFromPeerConnection=void 0)}):(this.session.on(e,t),()=>this.session.off(e,t))}watchMediaConnection(){if(this.mediaWatchStarted)return;this.mediaWatchStarted=!0;let e=!1,t=t=>{if(t==null)return;let n=()=>{if(e)return;let n=t.connectionState||t.iceConnectionState;if(n===`connected`){e=!0;for(let e of this.mediaHandlers)try{e({state:n})}catch{}}};t.addEventListener(`connectionstatechange`,n),n()},n=this.session.connection;if(n!=null){t(n);return}let r=e=>{t(e?.peerconnection??void 0)};this.session.on(`peerconnection`,r),this.sessionUnsubscribeFromPeerConnection=()=>this.session.off(`peerconnection`,r)}changeHoldState(t){return new Promise((n,r)=>{let i=!1,a=e=>{c(e)},o=()=>{try{this.session.off(`ended`,a)}catch{}try{this.session.off(`failed`,a)}catch{}this.pendingHoldReject===c&&(this.pendingHoldReject=void 0)},s=()=>{i||(i=!0,o(),n())},c=t=>{i||(i=!0,o(),r(new e.i(`CALL_OPERATION_NOT_ALLOWED`,`call`,!1,{cause:t})))};try{this.session.on(`ended`,a),this.session.on(`failed`,a)}catch(e){c(e);return}this.pendingHoldReject=c;try{this.session[t](void 0,s)||c()}catch(e){c(e)}})}};function u(e){if(typeof e!=`object`||!e)return!1;let t=e;return t.originator===`remote`&&d(t.session)}function d(e){return typeof e==`object`&&!!e&&`id`in e}function f(e){return typeof e==`object`&&e&&(e.via_transport=`WSS`),e}function p(e){let t=e.replace(/^\[|\]$/g,``).toLowerCase();return t===`localhost`||t===`127.0.0.1`||t===`::1`||t===`0:0:0:0:0:0:0:1`}function m(e){let t=e?.uri;if(t==null)return;if(typeof t.user==`string`&&t.user!==``)return t.user;let n=t.toString();return n.match(/^sips?:([^@;>]+)/i)?.[1]??(n===``?void 0:n)}exports.JsSipAdapter=c;
1
+ const e=require("./SharedWorkerProtocol-39dch6xL.cjs");require("./createJsSipFactory-B1ORbJiP.cjs");function t(e){let t=new Map;return{peerConnection:{getSenders:()=>e.getSenders().map(n=>{let r=t.get(n);if(r!==void 0)return r;let i={get track(){return n.track},get senderKind(){let t=n.track;if(t!==null)return t.kind===`audio`||t.kind===`video`?t.kind:void 0;let r=e.getTransceivers().find(e=>e.sender===n)?.receiver.track;if(r?.kind===`audio`||r?.kind===`video`)return r.kind;let i=n.getParameters().codecs?.find(e=>typeof e.mimeType==`string`);if(i?.mimeType?.toLowerCase().startsWith(`video/`))return`video`;if(i?.mimeType?.toLowerCase().startsWith(`audio/`))return`audio`},replaceTrack:e=>n.replaceTrack(e)};return t.set(n,i),i}),close:()=>{e.connectionState!==`closed`&&e.close()}},trackSource:{onTrack(t){let n=e=>{let n=e.track;t({streams:[...e.streams],track:n})};return e.addEventListener(`track`,n),()=>e.removeEventListener(`track`,n)}}}}var n=300,r=25,i=2,a=``,o=120;function s(e){let t=Math.floor(Number(e.sip.registerExpires));return Number.isFinite(t)&&t>0?t:n}var c=class{factory;transportBaseUrl;keepaliveSeconds;agentTerminal;sessionTimers;ua;uaListeners;incomingHandlers;iceServers;pendingConnection;disposed=!1;warmPc;keepaliveTimer;keepaliveBusy=!1;registerExpiresSeconds=0;connectionHandlers=new Set;constructor(e,t,n=r,i=a,o=!1){this.factory=e,this.transportBaseUrl=t,this.keepaliveSeconds=n,this.agentTerminal=i,this.sessionTimers=o}terminalHeaders(){return this.agentTerminal===``?[]:[`X-Agent-Terminal: ${this.agentTerminal}`]}warmIceCandidatePool(){if(this.closeWarmPc(),typeof RTCPeerConnection<`u`&&this.iceServers!==void 0)try{this.warmPc=new RTCPeerConnection({iceServers:[...this.iceServers],iceCandidatePoolSize:i})}catch{this.warmPc=void 0}}closeWarmPc(){try{this.warmPc?.close()}catch{}this.warmPc=void 0}keepaliveTick(e){if(this.isCurrentUa(e)&&!this.keepaliveBusy)try{if(typeof e.isConnected==`function`&&e.isConnected()===!1||typeof e.register!=`function`)return;this.keepaliveBusy=!0,e.register({extraHeaders:this.terminalHeaders()})}catch{}finally{this.keepaliveBusy=!1}}startKeepalive(e,t){if(this.stopKeepalive(),!Number.isFinite(this.keepaliveSeconds)||this.keepaliveSeconds<=0||t>0&&t<=this.keepaliveSeconds)return;let n=Math.max(10,Math.floor(this.keepaliveSeconds))*1e3;this.keepaliveTimer=setInterval(()=>this.keepaliveTick(e),n)}stopKeepalive(){this.keepaliveTimer!==void 0&&clearInterval(this.keepaliveTimer),this.keepaliveTimer=void 0}connect(t){this.assertNotDisposed(),this.disconnect(this.registrationError(`SIP connection replaced`));try{let e=this.factory.createUa({sockets:f(this.factory.createSocket(this.withoutTicket(this.resolveWssUrl(t.transport.wssUrl)),t.transport.ticket)),uri:t.sip.uri,password:t.sip.registerTicket,registrar_server:t.sip.registrar,register:!0,register_expires:s(t),session_timers:this.sessionTimers,...this.agentTerminal===``?{}:{user_agent:this.agentTerminal}});return this.ua=e,this.registerExpiresSeconds=s(t),this.iceServers=t.iceServers,this.uaListeners=[],this.startRegistration(e)}catch(t){let n=t instanceof e.i&&t.code===`CONFIG_INVALID`?t:this.transportError(t);return this.disconnect(n),Promise.reject(n)}}disconnect(e=this.registrationError(`SIP connection disconnected`)){this.stopKeepalive(),this.closeWarmPc();let t=this.pendingConnection;this.pendingConnection=void 0,this.ua!==void 0&&(this.removeUaListeners(this.ua),this.ua.stop()),this.ua=void 0,this.uaListeners=void 0,this.iceServers=void 0,t?.reject(e)}dial(t,n){let r=this.requireUa();if(t.trim()===``)throw new e.i(`CALL_INVALID_DESTINATION`,`call`,!1);try{let e=n===void 0?{mediaConstraints:{audio:!0,video:!1},sessionTimersExpires:o,pcConfig:{iceServers:this.iceServers,iceCandidatePoolSize:i}}:{mediaConstraints:{audio:!0,video:!1},sessionTimersExpires:o,pcConfig:{iceServers:[...n.iceServers],iceCandidatePoolSize:i},extraHeaders:[...this.terminalHeaders(),...n.extraHeaders,...n.type===`extension`?[`X-XCall-Destination-Type: extension`]:[]]};return new l(r.call(t,e))}catch(t){throw new e.i(`CALL_OPERATION_NOT_ALLOWED`,`call`,!1,{cause:t})}}onIncoming(e){this.assertNotDisposed();let t=this.incomingHandlers??new Set;return this.incomingHandlers=t,t.add(e),()=>t.delete(e)}onConnectionStateChanged(e){return this.disposed?()=>void 0:(this.connectionHandlers.add(e),()=>this.connectionHandlers.delete(e))}dispose(){this.disposed||(this.disposed=!0,this.disconnect(),this.incomingHandlers=void 0,this.connectionHandlers.clear())}getResourceCounts(){return{listeners:(this.uaListeners?.length??0)+(this.incomingHandlers?.size??0)+this.connectionHandlers.size,webSockets:this.ua===void 0?0:1}}handleIncoming(e){if(!u(e))return;let t=e.session;if(t===void 0)return;let n=new l(t);this.incomingHandlers?.forEach(e=>e(n))}startRegistration(e){let t=!0,n=!1,r=new Promise((t,n)=>{this.pendingConnection={ua:e,resolve:t,reject:n}});this.listen(e,`registered`,()=>{if(this.isCurrentUa(e)){if(t){n=!0;return}this.resolveConnection(e)}}),this.listen(e,`registrationFailed`,t=>{if(!this.isCurrentUa(e))return;let n=this.registrationError(this.registrationFailureCause(t));this.connectionHandlers.forEach(e=>e(`failed`,n)),this.disconnect(n)}),this.listen(e,`disconnected`,t=>{if(!this.isCurrentUa(e))return;let n=this.transportError(t);this.connectionHandlers.forEach(e=>e(`disconnected`,n)),this.disconnect(n)}),this.listen(e,`newRTCSession`,t=>{this.isCurrentUa(e)&&this.handleIncoming(t)});try{e.start()}catch(e){let t=this.transportError(e);this.connectionHandlers.forEach(e=>e(`failed`,t)),this.disconnect(t)}finally{t=!1}return n&&this.resolveConnection(e),r}listen(e,t,n){e.on(t,n),this.uaListeners?.push({event:t,handler:n})}removeUaListeners(e){this.uaListeners?.forEach(({event:t,handler:n})=>e.off(t,n))}isCurrentUa(e){return this.ua===e}resolveConnection(e){let t=this.pendingConnection;t?.ua===e&&(this.pendingConnection=void 0,t.resolve(),this.warmIceCandidatePool(),this.startKeepalive(e,this.registerExpiresSeconds),this.connectionHandlers.forEach(e=>e(`registered`)))}resolveWssUrl(e){let t=this.transportBaseUrl?.trim()===``||this.transportBaseUrl===void 0?typeof location>`u`?`http://localhost`:location.origin:this.transportBaseUrl,n=e.startsWith(`/`)?new URL(e,t):new URL(e);return n.protocol===`http:`&&(n.protocol=`ws:`),n.protocol===`https:`&&(n.protocol=`wss:`),this.assertAllowedWssUrl(n),n.toString()}assertAllowedWssUrl(t){if(t.protocol!==`wss:`&&!(t.protocol===`ws:`&&p(t.hostname)))throw new e.i(`CONFIG_INVALID`,`configuration`,!1,{cause:t.origin})}withoutTicket(e){let t=new URL(e);return t.searchParams.delete(`ticket`),t.toString()}requireUa(){if(this.assertNotDisposed(),this.ua===void 0)throw new e.i(`CALL_NOT_CONNECTED`,`call`,!1);return this.ua}assertNotDisposed(){if(this.disposed)throw new e.i(`SDK_ALREADY_DISPOSED`,`internal`,!1)}registrationError(t){return new e.i(`REGISTRATION_FAILED`,`registration`,!0,{cause:t})}transportError(t){return new e.i(`SIP_WS_UNAVAILABLE`,`registration`,!0,{cause:t})}registrationFailureCause(e){return typeof e==`object`&&e&&`cause`in e?e.cause:e}},l=class{session;pendingHoldReject;mediaRegistration;mediaConnection;mediaHandlers=new Set;mediaWatchStarted=!1;sessionUnsubscribeFromPeerConnection;constructor(e){this.session=e}get id(){return this.session.id}get from(){return this.session.from??m(this.session.remote_identity)}getMediaRegistration(){let e=this.session.connection;if(e!=null)return this.mediaRegistration!==void 0&&this.mediaConnection===e?this.mediaRegistration:(this.mediaConnection=e,this.mediaRegistration=t(e),this.mediaRegistration)}answer(e){this.session.answer({mediaConstraints:e.mediaConstraints,pcConfig:{iceServers:[...e.pcConfig.iceServers]}})}terminate(){this.pendingHoldReject?.(Error(`SIP session terminated`)),this.session.terminate()}hold(){return this.changeHoldState(`hold`)}unhold(){return this.changeHoldState(`unhold`)}mute(){this.session.mute()}unmute(){this.session.unmute()}sendDtmf(e){this.session.sendDTMF(e)}refer(e){this.session.refer(e)}on(e,t){return e===`mediaConnected`?(this.mediaHandlers.add(t),this.watchMediaConnection(),()=>{this.mediaHandlers.delete(t),this.mediaHandlers.size===0&&(this.sessionUnsubscribeFromPeerConnection?.(),this.sessionUnsubscribeFromPeerConnection=void 0)}):(this.session.on(e,t),()=>this.session.off(e,t))}watchMediaConnection(){if(this.mediaWatchStarted)return;this.mediaWatchStarted=!0;let e=!1,t=t=>{if(t==null)return;let n=()=>{if(e)return;let n=t.connectionState||t.iceConnectionState;if(n===`connected`){e=!0;for(let e of this.mediaHandlers)try{e({state:n})}catch{}}};t.addEventListener(`connectionstatechange`,n),n()},n=this.session.connection;if(n!=null){t(n);return}let r=e=>{t(e?.peerconnection??void 0)};this.session.on(`peerconnection`,r),this.sessionUnsubscribeFromPeerConnection=()=>this.session.off(`peerconnection`,r)}changeHoldState(t){return new Promise((n,r)=>{let i=!1,a=e=>{c(e)},o=()=>{try{this.session.off(`ended`,a)}catch{}try{this.session.off(`failed`,a)}catch{}this.pendingHoldReject===c&&(this.pendingHoldReject=void 0)},s=()=>{i||(i=!0,o(),n())},c=t=>{i||(i=!0,o(),r(new e.i(`CALL_OPERATION_NOT_ALLOWED`,`call`,!1,{cause:t})))};try{this.session.on(`ended`,a),this.session.on(`failed`,a)}catch(e){c(e);return}this.pendingHoldReject=c;try{this.session[t](void 0,s)||c()}catch(e){c(e)}})}};function u(e){if(typeof e!=`object`||!e)return!1;let t=e;return t.originator===`remote`&&d(t.session)}function d(e){return typeof e==`object`&&!!e&&`id`in e}function f(e){return typeof e==`object`&&e&&(e.via_transport=`WSS`),e}function p(e){let t=e.replace(/^\[|\]$/g,``).toLowerCase();return t===`localhost`||t===`127.0.0.1`||t===`::1`||t===`0:0:0:0:0:0:0:1`}function m(e){let t=e?.uri;if(t==null)return;if(typeof t.user==`string`&&t.user!==``)return t.user;let n=t.toString();return n.match(/^sips?:([^@;>]+)/i)?.[1]??(n===``?void 0:n)}exports.JsSipAdapter=c;