arnacon-webrtc-service 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +67 -0
  2. package/config.json +146 -0
  3. package/package.json +43 -0
  4. package/services/basicService.json +20 -0
  5. package/services/email.json +22 -0
  6. package/services/esimera.json +20 -0
  7. package/services/phonemyemail.json +20 -0
  8. package/services/secnum.json +20 -0
  9. package/services/vodaphone.json +20 -0
  10. package/webRTCservice/build.js +3 -0
  11. package/webRTCservice/clean.js +3 -0
  12. package/webRTCservice/core.js +30 -0
  13. package/webRTCservice/modules/blockchain.js +311 -0
  14. package/webRTCservice/modules/bridge.js +189 -0
  15. package/webRTCservice/modules/callFlow.js +262 -0
  16. package/webRTCservice/modules/callRouter.js +108 -0
  17. package/webRTCservice/modules/callRuntimeCore.js +112 -0
  18. package/webRTCservice/modules/dataChannel.js +55 -0
  19. package/webRTCservice/modules/handlers.js +60 -0
  20. package/webRTCservice/modules/handshakeFlow.js +71 -0
  21. package/webRTCservice/modules/httpServer.js +155 -0
  22. package/webRTCservice/modules/inboundCallFlow.js +111 -0
  23. package/webRTCservice/modules/messagingFlow.js +92 -0
  24. package/webRTCservice/modules/notification.js +413 -0
  25. package/webRTCservice/modules/offerFlow.js +111 -0
  26. package/webRTCservice/modules/peerConnection.js +258 -0
  27. package/webRTCservice/modules/polyfills.js +138 -0
  28. package/webRTCservice/modules/providerPolicy.js +36 -0
  29. package/webRTCservice/modules/sdpUtils.js +117 -0
  30. package/webRTCservice/modules/sessionStore.js +207 -0
  31. package/webRTCservice/modules/signalingHandlers.js +144 -0
  32. package/webRTCservice/modules/signalingPipeline.js +93 -0
  33. package/webRTCservice/modules/sipClient.js +179 -0
  34. package/webRTCservice/modules/sipRuntime.js +54 -0
  35. package/webRTCservice/package.json +15 -0
  36. package/webRTCservice/services/basicService.js +76 -0
  37. package/webRTCservice/services/basicService.runner.js +7 -0
  38. package/webRTCservice/services/email.js +103 -0
  39. package/webRTCservice/services/esimera.js +114 -0
  40. package/webRTCservice/services/esimera.runner.js +7 -0
  41. package/webRTCservice/services/phonemyemail.js +90 -0
  42. package/webRTCservice/services/secnum.js +91 -0
  43. package/webRTCservice/services/vodaphone.js +83 -0
  44. package/webRTCservice/validateExports.js +19 -0
  45. package/webRTCservice/webRTCmanager.js +969 -0
@@ -0,0 +1,311 @@
1
+ "use strict";
2
+
3
+ const { ethers } = require("ethers");
4
+
5
+ function createBlockchainApi({
6
+ config,
7
+ providerPolicy = null,
8
+ createHttpError,
9
+ logger = console,
10
+ }) {
11
+ const POLYGON_RPC = config.polygon.rpc;
12
+ const ENS_REGISTRY_ADDRESS = config.polygon.ENSRegistry;
13
+ const NAME_WRAPPER_ADDRESS = config.polygon.NameWrapper;
14
+ const SERVICE_PROVIDER_REGISTRY_ADDRESS = config.polygon.ServiceProviderRegistry;
15
+ const SAPPHIRE_RPC = config.sapphire.rpc;
16
+ const SAPPHIRE_TESTNET_RPC = config.sapphireTestnet.rpc;
17
+ const NFT_CALLER_ID_POOL_ADDRESS = config.sapphireTestnet.NFTCallerIdPool;
18
+
19
+ const ENS_REGISTRY_ABI = [
20
+ "function owner(bytes32 node) view returns (address)",
21
+ "function resolver(bytes32 node) view returns (address)",
22
+ ];
23
+ const NAME_WRAPPER_ABI = ["function ownerOf(uint256 tokenId) view returns (address)"];
24
+ const ENS_PUBLIC_RESOLVER_ABI = [
25
+ "function addr(bytes32 node) view returns (address)",
26
+ "function text(bytes32 node, string key) view returns (string)",
27
+ ];
28
+ const SERVICE_PROVIDER_REGISTRY_ABI = [
29
+ "function serviceRegistry() view returns (address)",
30
+ ];
31
+ const SERVICE_REGISTRY_ABI = [
32
+ "function getServiceContract(bytes32 node) view returns (address)",
33
+ ];
34
+ const NFT_CALLER_ID_POOL_ABI = [
35
+ "function balanceOf(address owner) view returns (uint256)",
36
+ "function tokenOfOwnerByIndex(address owner, uint256 index) view returns (uint256)",
37
+ "function getCallerIdByTokenId(uint256 tokenId) view returns (string phoneNumber, string metadata, address owner)",
38
+ ];
39
+
40
+ let polygonProvider = null;
41
+ let sapphireProvider = null;
42
+ let sapphireTestnetProvider = null;
43
+
44
+ function getPolygonProvider() {
45
+ if (!polygonProvider) polygonProvider = new ethers.providers.JsonRpcProvider(POLYGON_RPC);
46
+ return polygonProvider;
47
+ }
48
+
49
+ function getSapphireProvider() {
50
+ if (!sapphireProvider) sapphireProvider = new ethers.providers.JsonRpcProvider(SAPPHIRE_RPC);
51
+ return sapphireProvider;
52
+ }
53
+
54
+ function getSapphireTestnetProvider() {
55
+ if (!sapphireTestnetProvider) sapphireTestnetProvider = new ethers.providers.JsonRpcProvider(SAPPHIRE_TESTNET_RPC);
56
+ return sapphireTestnetProvider;
57
+ }
58
+
59
+ function isEthAddress(str) {
60
+ return /^0x[0-9a-fA-F]{40}$/.test(str);
61
+ }
62
+
63
+ function normalizeEnsDomain(ens) {
64
+ if (!providerPolicy || typeof providerPolicy.normalizeEnsDomain !== "function") {
65
+ return String(ens || "");
66
+ }
67
+ return providerPolicy.normalizeEnsDomain(ens);
68
+ }
69
+
70
+ function namehash(name) {
71
+ if (!name) return "0x0000000000000000000000000000000000000000000000000000000000000000";
72
+ const labels = name.split(".");
73
+ let node = "0x0000000000000000000000000000000000000000000000000000000000000000";
74
+ for (let i = labels.length - 1; i >= 0; i--) {
75
+ const labelHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(labels[i]));
76
+ node = ethers.utils.keccak256(ethers.utils.solidityPack(["bytes32", "bytes32"], [node, labelHash]));
77
+ }
78
+ return node;
79
+ }
80
+
81
+ async function resolveEnsToOwner(ensName) {
82
+ const fullName = ensName.endsWith(".global") ? ensName : `${ensName}.global`;
83
+ const provider = getPolygonProvider();
84
+ const node = namehash(fullName);
85
+ const ensRegistry = new ethers.Contract(ENS_REGISTRY_ADDRESS, ENS_REGISTRY_ABI, provider);
86
+ const nameWrapper = new ethers.Contract(NAME_WRAPPER_ADDRESS, NAME_WRAPPER_ABI, provider);
87
+ const ensOwner = await ensRegistry.owner(node);
88
+ if (ensOwner.toLowerCase() === NAME_WRAPPER_ADDRESS.toLowerCase()) {
89
+ return nameWrapper.ownerOf(node);
90
+ }
91
+ return ensOwner;
92
+ }
93
+
94
+ async function resolveEnsToAddress(ensName) {
95
+ const fullName = ensName.endsWith(".global") ? ensName : `${ensName}.global`;
96
+ const provider = getPolygonProvider();
97
+ const node = namehash(fullName);
98
+ const ensRegistry = new ethers.Contract(ENS_REGISTRY_ADDRESS, ENS_REGISTRY_ABI, provider);
99
+ try {
100
+ const resolverAddr = await ensRegistry.resolver(node);
101
+ if (resolverAddr && resolverAddr !== ethers.constants.AddressZero) {
102
+ const resolver = new ethers.Contract(resolverAddr, ENS_PUBLIC_RESOLVER_ABI, provider);
103
+ const addr = await resolver.addr(node);
104
+ if (addr && addr !== ethers.constants.AddressZero) return addr;
105
+ }
106
+ } catch (err) {
107
+ logger.log(`[ENS] resolver.addr() failed for ${fullName}: ${err.message}, falling back to owner`);
108
+ }
109
+ return resolveEnsToOwner(ensName);
110
+ }
111
+
112
+ async function resolveEnsTextRecord(ensName, key) {
113
+ const fullName = ensName.endsWith(".global") ? ensName : `${ensName}.global`;
114
+ const provider = getPolygonProvider();
115
+ const node = namehash(fullName);
116
+ const ensRegistry = new ethers.Contract(ENS_REGISTRY_ADDRESS, ENS_REGISTRY_ABI, provider);
117
+ const resolverAddr = await ensRegistry.resolver(node);
118
+ if (!resolverAddr || resolverAddr === ethers.constants.AddressZero) {
119
+ return null;
120
+ }
121
+ const resolver = new ethers.Contract(resolverAddr, ENS_PUBLIC_RESOLVER_ABI, provider);
122
+ try {
123
+ const value = await resolver.text(node, key);
124
+ return value || null;
125
+ } catch (_) {
126
+ return null;
127
+ }
128
+ }
129
+
130
+ async function resolveWrappedOwner(ensName) {
131
+ const fullName = ensName.endsWith(".global") ? ensName : `${ensName}.global`;
132
+ const provider = getPolygonProvider();
133
+ const node = namehash(fullName);
134
+ const nameWrapper = new ethers.Contract(NAME_WRAPPER_ADDRESS, NAME_WRAPPER_ABI, provider);
135
+ return nameWrapper.ownerOf(node);
136
+ }
137
+
138
+ async function verifyInitialOfferSignature(offer) {
139
+ const from = normalizeEnsDomain(offer.from || "");
140
+ const { xdata, xsign, sessionId } = offer;
141
+ if (!from) throw createHttpError(400, "Missing required field: from");
142
+ if (!xdata) throw createHttpError(401, "Missing required field: xdata");
143
+ if (!xsign) throw createHttpError(401, "Missing required field: xsign");
144
+
145
+ logger.log(`[${sessionId || "no-session"}] From: ${from}`);
146
+ logger.log(`[${sessionId || "no-session"}] X sign: ${xsign}, X data: ${xdata}`);
147
+
148
+ const expectedSigner = await resolveExpectedSigner(from);
149
+
150
+ let recoveredSigner;
151
+ try {
152
+ recoveredSigner = ethers.utils.getAddress(
153
+ ethers.utils.verifyMessage(String(xdata), String(xsign)),
154
+ );
155
+ } catch (err) {
156
+ throw createHttpError(401, `Invalid xsign for xdata: ${err.message}`);
157
+ }
158
+ if (recoveredSigner !== expectedSigner) {
159
+ throw createHttpError(403, `xsign signer mismatch for ${from}: expected ${expectedSigner}, got ${recoveredSigner}`);
160
+ }
161
+ logger.log(`[${sessionId || "no-session"}] Initial offer signature verified for ${from} (${recoveredSigner})`);
162
+ }
163
+
164
+ async function resolveExpectedSigner(identity) {
165
+ if (isEthAddress(identity)) {
166
+ return ethers.utils.getAddress(identity);
167
+ }
168
+ let wrappedOwner;
169
+ try {
170
+ wrappedOwner = await resolveWrappedOwner(identity);
171
+ } catch (err) {
172
+ throw createHttpError(401, `Failed resolving wrapped owner for ${identity}: ${err.message}`);
173
+ }
174
+ if (!wrappedOwner || wrappedOwner === ethers.constants.AddressZero) {
175
+ throw createHttpError(401, `Wrapped owner not found for ${identity}`);
176
+ }
177
+ return ethers.utils.getAddress(wrappedOwner);
178
+ }
179
+
180
+ async function verifyAnswerSignature(offer, session) {
181
+ const { sessionId, xdata, xsign } = offer;
182
+ if (!session) throw createHttpError(404, "Session not found for answer verification");
183
+ if (!xdata) throw createHttpError(401, "Missing required field: xdata");
184
+ if (!xsign) throw createHttpError(401, "Missing required field: xsign");
185
+
186
+ const expectedIdentity = normalizeEnsDomain(session.toIdentity || "");
187
+ if (!expectedIdentity) {
188
+ throw createHttpError(401, "Unable to verify answer signer: missing session toIdentity");
189
+ }
190
+
191
+ const from = normalizeEnsDomain(offer.from || "");
192
+ if (from && from !== expectedIdentity) {
193
+ throw createHttpError(403, `Answer 'from' mismatch: expected ${expectedIdentity}, got ${from}`);
194
+ }
195
+
196
+ const expectedSigner = await resolveExpectedSigner(expectedIdentity);
197
+ let recoveredSigner;
198
+ try {
199
+ recoveredSigner = ethers.utils.getAddress(
200
+ ethers.utils.verifyMessage(String(xdata), String(xsign)),
201
+ );
202
+ } catch (err) {
203
+ throw createHttpError(401, `Invalid xsign for xdata: ${err.message}`);
204
+ }
205
+ if (recoveredSigner !== expectedSigner) {
206
+ throw createHttpError(
207
+ 403,
208
+ `Answer xsign signer mismatch for ${expectedIdentity}: expected ${expectedSigner}, got ${recoveredSigner}`,
209
+ );
210
+ }
211
+ logger.log(`[${sessionId || "no-session"}] Answer signature verified for ${expectedIdentity} (${recoveredSigner})`);
212
+ }
213
+
214
+ async function verifyHttpSignalingSignature(payload) {
215
+ const notifyType = payload?.type || "offer";
216
+ await verifyInitialOfferSignature(payload || {});
217
+ logger.log(
218
+ `[${payload?.sessionId || "no-session"}] HTTP signaling signature verified (type=${notifyType})`,
219
+ );
220
+ }
221
+
222
+ function getRpcForNetwork(networkName) {
223
+ switch (String(networkName || "polygon").toLowerCase()) {
224
+ case "sapphire":
225
+ case "oasis_sapphire":
226
+ return SAPPHIRE_RPC;
227
+ case "polygon":
228
+ default:
229
+ return POLYGON_RPC;
230
+ }
231
+ }
232
+
233
+ async function resolveCallerServiceProviderContract(callerEns) {
234
+ if (isEthAddress(callerEns)) return null;
235
+ callerEns = normalizeEnsDomain(callerEns);
236
+ const provider = getPolygonProvider();
237
+ const spr = new ethers.Contract(SERVICE_PROVIDER_REGISTRY_ADDRESS, SERVICE_PROVIDER_REGISTRY_ABI, provider);
238
+ let serviceRegistryAddress;
239
+ try {
240
+ serviceRegistryAddress = await spr.serviceRegistry();
241
+ } catch (err) {
242
+ logger.error(`[SPResolver] serviceRegistry() failed: ${err.message}`);
243
+ return null;
244
+ }
245
+ if (!serviceRegistryAddress || serviceRegistryAddress === ethers.constants.AddressZero) return null;
246
+ const serviceRegistry = new ethers.Contract(serviceRegistryAddress, SERVICE_REGISTRY_ABI, provider);
247
+ const fullCaller = callerEns.endsWith(".global") ? callerEns : `${callerEns}.global`;
248
+ let currentDomain = fullCaller;
249
+ while (currentDomain && currentDomain.includes(".")) {
250
+ const node = namehash(currentDomain);
251
+ try {
252
+ const contractAddr = await serviceRegistry.getServiceContract(node);
253
+ if (contractAddr && contractAddr !== ethers.constants.AddressZero) {
254
+ return {
255
+ notificationRegistryAddress: contractAddr,
256
+ networkName: "polygon",
257
+ rpcUrl: POLYGON_RPC,
258
+ isDefault: false,
259
+ };
260
+ }
261
+ } catch (_) {}
262
+ const dotIndex = currentDomain.indexOf(".");
263
+ if (dotIndex >= 0) currentDomain = currentDomain.substring(dotIndex + 1);
264
+ else break;
265
+ }
266
+ return null;
267
+ }
268
+
269
+ function getNftCallerIdPool() {
270
+ const provider = getSapphireTestnetProvider();
271
+ return new ethers.Contract(NFT_CALLER_ID_POOL_ADDRESS, NFT_CALLER_ID_POOL_ABI, provider);
272
+ }
273
+
274
+ async function nftGetOwnedNumber(walletAddress) {
275
+ try {
276
+ const pool = getNftCallerIdPool();
277
+ const balance = await pool.balanceOf(walletAddress);
278
+ if (balance.lte(0)) return null;
279
+ const tokenId = await pool.tokenOfOwnerByIndex(walletAddress, 0);
280
+ const [phoneNumber] = await pool.getCallerIdByTokenId(tokenId);
281
+ return phoneNumber || null;
282
+ } catch (err) {
283
+ logger.error(`[NFT] getOwnedNumber failed for ${walletAddress}: ${err.message}`);
284
+ return null;
285
+ }
286
+ }
287
+
288
+ return {
289
+ ethers,
290
+ getPolygonProvider,
291
+ getSapphireProvider,
292
+ getSapphireTestnetProvider,
293
+ isEthAddress,
294
+ normalizeEnsDomain,
295
+ namehash,
296
+ resolveEnsToOwner,
297
+ resolveEnsToAddress,
298
+ resolveEnsTextRecord,
299
+ resolveWrappedOwner,
300
+ verifyInitialOfferSignature,
301
+ verifyAnswerSignature,
302
+ verifyHttpSignalingSignature,
303
+ resolveCallerServiceProviderContract,
304
+ nftGetOwnedNumber,
305
+ getRpcForNetwork,
306
+ };
307
+ }
308
+
309
+ module.exports = {
310
+ createBlockchainApi,
311
+ };
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+
3
+ function createBridgeApi({
4
+ sessions,
5
+ pendingBridges,
6
+ pendingInboundCalls,
7
+ sendNotification,
8
+ sendDataChannelMessage,
9
+ startWebRtcBridge,
10
+ notiTypeCall,
11
+ RTCSessionDescription,
12
+ logger = console,
13
+ }) {
14
+ async function notifyAndBridge(callerSessionId, destination) {
15
+ const callerSession = sessions.get(callerSessionId);
16
+ if (!callerSession) throw new Error("Caller session not found");
17
+
18
+ const calleeWallet = destination.wallet;
19
+ const calleeEns = destination.ensName || calleeWallet;
20
+ const callerEns = callerSession.callerEns;
21
+
22
+ const BRIDGE_TIMEOUT = 60000;
23
+ const bridgePromise = new Promise((resolve, reject) => {
24
+ const timer = setTimeout(() => {
25
+ pendingBridges.delete(calleeWallet.toLowerCase());
26
+ reject(new Error("Callee did not connect within timeout"));
27
+ }, BRIDGE_TIMEOUT);
28
+
29
+ pendingBridges.set(calleeWallet.toLowerCase(), {
30
+ callerSessionId,
31
+ resolve,
32
+ reject,
33
+ timer,
34
+ });
35
+ });
36
+
37
+ const callPayload = JSON.stringify({
38
+ type: "call-invite",
39
+ from: callerEns,
40
+ to: calleeEns,
41
+ sessionId: callerSessionId,
42
+ });
43
+ await sendNotification(callerEns, calleeEns, callPayload, notiTypeCall);
44
+ const calleeSessionId = await bridgePromise;
45
+ startWebRtcBridge(callerSessionId, calleeSessionId);
46
+ }
47
+
48
+ function startBridgeRtp(callerSessionId, calleeSessionId) {
49
+ const callerSession = sessions.get(callerSessionId);
50
+ const calleeSession = sessions.get(calleeSessionId);
51
+ if (!callerSession || !calleeSession) return;
52
+
53
+ callerSession.bridgedWith = calleeSessionId;
54
+ calleeSession.bridgedWith = callerSessionId;
55
+ callerSession.mediaRelayActive = true;
56
+ calleeSession.mediaRelayActive = true;
57
+
58
+ let callerPipeActive = false;
59
+ let calleePipeActive = false;
60
+ let callerSourceNotified = false;
61
+ let calleeSourceNotified = false;
62
+
63
+ function wireCallerToCallee() {
64
+ if (callerPipeActive) return;
65
+ const track = callerSession.remoteTracks.find(t => t.kind === "audio");
66
+ if (!track || !calleeSession.localAudioTrack) return;
67
+ callerPipeActive = true;
68
+ track.onReceiveRtp.subscribe((rtp) => {
69
+ if (callerSession.mediaRelayActive) {
70
+ if (!calleeSourceNotified) {
71
+ calleeSourceNotified = true;
72
+ calleeSession.localAudioTrack.onSourceChanged.execute({
73
+ sequenceNumber: rtp.header.sequenceNumber,
74
+ timestamp: rtp.header.timestamp,
75
+ });
76
+ }
77
+ calleeSession.localAudioTrack.writeRtp(rtp);
78
+ }
79
+ });
80
+ }
81
+
82
+ function wireCalleeToCaller() {
83
+ if (calleePipeActive) return;
84
+ const track = calleeSession.remoteTracks.find(t => t.kind === "audio");
85
+ if (!track || !callerSession.localAudioTrack) return;
86
+ calleePipeActive = true;
87
+ track.onReceiveRtp.subscribe((rtp) => {
88
+ if (callerSession.mediaRelayActive) {
89
+ if (!callerSourceNotified) {
90
+ callerSourceNotified = true;
91
+ callerSession.localAudioTrack.onSourceChanged.execute({
92
+ sequenceNumber: rtp.header.sequenceNumber,
93
+ timestamp: rtp.header.timestamp,
94
+ });
95
+ }
96
+ callerSession.localAudioTrack.writeRtp(rtp);
97
+ }
98
+ });
99
+ }
100
+
101
+ wireCallerToCallee();
102
+ wireCalleeToCaller();
103
+
104
+ if (callerSession.peerConnection) {
105
+ callerSession.peerConnection.onTrack.subscribe((track) => {
106
+ if (track.kind === "audio") {
107
+ callerSession.remoteTracks.push(track);
108
+ wireCallerToCallee();
109
+ }
110
+ });
111
+ }
112
+ if (calleeSession.peerConnection) {
113
+ calleeSession.peerConnection.onTrack.subscribe((track) => {
114
+ if (track.kind === "audio") {
115
+ calleeSession.remoteTracks.push(track);
116
+ wireCalleeToCaller();
117
+ }
118
+ });
119
+ }
120
+ logger.log(`[Bridge] WebRTC bridge initiated between ${callerSessionId} and ${calleeSessionId}`);
121
+ }
122
+
123
+ function checkPendingBridge(sessionId, walletAddress) {
124
+ if (!walletAddress) return false;
125
+ const key = walletAddress.toLowerCase();
126
+ const pending = pendingBridges.get(key);
127
+ if (!pending) return false;
128
+ clearTimeout(pending.timer);
129
+ pendingBridges.delete(key);
130
+ pending.resolve(sessionId);
131
+ return true;
132
+ }
133
+
134
+ function checkPendingInboundCall(sessionId, walletAddress) {
135
+ if (!walletAddress) return false;
136
+ const key = walletAddress.toLowerCase();
137
+ const pending = pendingInboundCalls.get(key);
138
+ if (!pending) return false;
139
+ clearTimeout(pending.timer);
140
+ pendingInboundCalls.delete(key);
141
+ const session = sessions.get(sessionId);
142
+ if (!session) return false;
143
+ session.inboundCall = {
144
+ fromNumber: pending.fromNumber,
145
+ toNumber: pending.toNumber,
146
+ callId: pending.callId,
147
+ };
148
+ sendDataChannelMessage(sessionId, {
149
+ msgType: "call",
150
+ action: "incoming",
151
+ from: pending.fromNumber,
152
+ to: pending.toNumber,
153
+ });
154
+ return true;
155
+ }
156
+
157
+ async function handleIceRestart(sessionId, payload) {
158
+ const session = sessions.get(sessionId);
159
+ if (!session || !session.peerConnection) {
160
+ throw new Error("Session or PeerConnection not found for ICE restart");
161
+ }
162
+ const pc = session.peerConnection;
163
+ await pc.setRemoteDescription(new RTCSessionDescription(payload.sdp, "offer"));
164
+ const answer = await pc.createAnswer();
165
+ await pc.setLocalDescription(answer);
166
+ sendDataChannelMessage(sessionId, {
167
+ msgType: "signaling",
168
+ payload: {
169
+ type: "answer",
170
+ from: session.toIdentity,
171
+ to: session.callerEns,
172
+ sessionId,
173
+ sdp: answer.sdp,
174
+ },
175
+ });
176
+ }
177
+
178
+ return {
179
+ notifyAndBridge,
180
+ startBridgeRtp,
181
+ checkPendingBridge,
182
+ checkPendingInboundCall,
183
+ handleIceRestart,
184
+ };
185
+ }
186
+
187
+ module.exports = {
188
+ createBridgeApi,
189
+ };