arnacon-webrtc-service 0.2.8 → 0.2.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arnacon-webrtc-service",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Arnacon WebRTC core runtime and service modules",
5
5
  "main": "./webRTCservice/core.js",
6
6
  "type": "commonjs",
@@ -1,14 +1,7 @@
1
1
  const { buildOfferPayload, serializeNotifyPayload } = require("../../participants/signaling/SignalingEnvelope");
2
-
3
- function identityLabel(identity) {
4
- if (!identity || typeof identity !== "string") return identity;
5
- const trimmed = identity.trim();
6
- const atPos = trimmed.indexOf("@");
7
- if (atPos > 0) return trimmed.slice(0, atPos);
8
- const dotPos = trimmed.indexOf(".");
9
- if (dotPos > 0) return trimmed.slice(0, dotPos);
10
- return trimmed;
11
- }
2
+ const {
3
+ identityLabel,
4
+ } = require("../../runtime/CallPairRef");
12
5
 
13
6
  function createInboundCallFlow({
14
7
  createSession,
@@ -4,22 +4,13 @@
4
4
  // Replaces the old linkedSessionId / bridgedWith / pendingBridges pairing.
5
5
 
6
6
  const { PolySession } = require("./PolySession");
7
-
8
- // Bare label of an identity: "972...@x" / "972....global" / "972..." -> "972...".
9
- function identityLabel(identity) {
10
- if (!identity || typeof identity !== "string") return identity;
11
- const trimmed = identity.trim();
12
- const atPos = trimmed.indexOf("@");
13
- if (atPos > 0) return trimmed.slice(0, atPos);
14
- const dotPos = trimmed.indexOf(".");
15
- if (dotPos > 0) return trimmed.slice(0, dotPos);
16
- return trimmed;
17
- }
7
+ const {
8
+ identityLabel,
9
+ pairKeyFromIdentities,
10
+ } = require("../../runtime/CallPairRef");
18
11
 
19
12
  function pairKey(a, b) {
20
- return [identityLabel(String(a || "").toLowerCase()), identityLabel(String(b || "").toLowerCase())]
21
- .sort()
22
- .join("|");
13
+ return pairKeyFromIdentities(a, b);
23
14
  }
24
15
 
25
16
  class PolySessionRegistry {
@@ -47,7 +38,7 @@ class PolySessionRegistry {
47
38
  }
48
39
 
49
40
  keyForPair(a, b) {
50
- return pairKey(a, b);
41
+ return pairKeyFromIdentities(a, b);
51
42
  }
52
43
 
53
44
  // Resolve (or create) the PolySession for a pair and return which leg the
@@ -56,7 +47,7 @@ class PolySessionRegistry {
56
47
  if (!a?.endpoint || !b?.endpoint) {
57
48
  throw new Error("PolySessionRegistry.resolve requires both leg endpoints");
58
49
  }
59
- const key = pairKey(a.endpoint, b.endpoint);
50
+ const key = pairKeyFromIdentities(a.endpoint, b.endpoint);
60
51
  let poly = this.byKey.get(key);
61
52
  if (!poly) {
62
53
  poly = this._create(key, a, b);
@@ -68,6 +68,7 @@ test("different pair creates a different PolySession", () => {
68
68
 
69
69
  test("pairKey is order-independent and label-normalized", () => {
70
70
  assert.equal(pairKey("A.x.global", "b"), pairKey("b", "a"));
71
+ assert.equal(pairKey("Alice.SecNum.Global", "BOB"), pairKey("alice", "bob"));
71
72
  });
72
73
 
73
74
  test("destroy removes the PolySession and frees the key", async () => {
@@ -141,3 +142,22 @@ test("supports two simultaneous calls without cross-wiring state/media", async (
141
142
  assert.equal(second.poly.legs.b.state, S.IN_CALL);
142
143
  assert.equal(media.connects.length, 2, "media must bridge each call once");
143
144
  });
145
+
146
+ test("same callee with different callers keeps separate pair records", () => {
147
+ const reg = buildRegistry();
148
+ const first = reg.resolve({
149
+ a: { endpoint: "alice.secnum.global", kind: "webrtc" },
150
+ b: { endpoint: "*9225", kind: "sip" },
151
+ target: "a",
152
+ });
153
+ const second = reg.resolve({
154
+ a: { endpoint: "carol.secnum.global", kind: "webrtc" },
155
+ b: { endpoint: "*9225", kind: "sip" },
156
+ target: "a",
157
+ });
158
+
159
+ assert.notEqual(first.key, second.key);
160
+ assert.notEqual(first.poly, second.poly);
161
+ assert.equal(reg.get(first.key), first.poly);
162
+ assert.equal(reg.get(second.key), second.poly);
163
+ });
@@ -172,3 +172,28 @@ test("double-end race then immediate redial does not leak teardown state", async
172
172
  await bAnswers(ctx, "answer-2");
173
173
  expectInCall(ctx);
174
174
  });
175
+
176
+ test("parallel calls that share a callee label stay isolated", async () => {
177
+ const first = buildWebRtcScenario();
178
+ const second = buildWebRtcScenario();
179
+
180
+ await transportOpenA(first);
181
+ await transportOpenB(first);
182
+ await transportOpenA(second);
183
+ await transportOpenB(second);
184
+
185
+ await aOffers(first, "offer-a");
186
+ await bAnswers(first, "answer-a");
187
+ await aOffers(second, "offer-c");
188
+ await bAnswers(second, "answer-c");
189
+
190
+ expectInCall(first);
191
+ expectInCall(second);
192
+
193
+ await aEnds(second);
194
+ await bCompletesEnd(second, "end-second");
195
+
196
+ // Ending one pair must not disturb the other active pair.
197
+ expectInCall(first);
198
+ expectEnded(second);
199
+ });
@@ -1015,3 +1015,13 @@ test("DEEP BUG REPRO: parser m-line failure realigns on data-only offer", async
1015
1015
  );
1016
1016
  assert.equal(pc.transceivers.find((t) => t.kind === "audio")?.sender?.track, null);
1017
1017
  });
1018
+
1019
+ test("ring signaling keeps canonical caller label in payload", async () => {
1020
+ const { neg, signaling } = build();
1021
+ await neg.ring({ from: "alice.secnum.global" });
1022
+ const message = signaling.sent.at(-1);
1023
+ assert.equal(message?.msgType, "signaling");
1024
+ assert.equal(message?.payload?.type, "offer");
1025
+ assert.equal(message?.payload?.from, "alice");
1026
+ assert.equal(message?.payload?.to, "bob.secnum.global");
1027
+ });
@@ -253,3 +253,16 @@ test("WERIFT REPLAY: decline/end then redial with audio mid=2 stays reusable", a
253
253
  await harness.dispose();
254
254
  }
255
255
  });
256
+
257
+ test("WERIFT REPLAY: ring payload keeps caller label normalized", async () => {
258
+ const harness = buildWeriftNegotiationHarness({ withAudioTrack: true, withDataChannel: true });
259
+ try {
260
+ await assert.doesNotReject(() => harness.neg.ring({ from: "alice.secnum.global" }));
261
+ const message = harness.signaling.sent.at(-1);
262
+ assert.equal(message?.msgType, "signaling");
263
+ assert.equal(message?.payload?.from, "alice");
264
+ assert.equal(message?.payload?.to, "bob.secnum.global");
265
+ } finally {
266
+ await harness.dispose();
267
+ }
268
+ });
@@ -13,16 +13,9 @@ const {
13
13
  alignEndCallAnswerSdp,
14
14
  audioDirection,
15
15
  } = require("./sdp");
16
-
17
- function identityLabel(identity) {
18
- if (!identity || typeof identity !== "string") return identity;
19
- const trimmed = identity.trim();
20
- const atPos = trimmed.indexOf("@");
21
- if (atPos > 0) return trimmed.slice(0, atPos);
22
- const dotPos = trimmed.indexOf(".");
23
- if (dotPos > 0) return trimmed.slice(0, dotPos);
24
- return trimmed;
25
- }
16
+ const {
17
+ identityLabel,
18
+ } = require("../../../runtime/CallPairRef");
26
19
 
27
20
  class WebRtcNegotiation extends CallNegotiationPort {
28
21
  constructor({
@@ -1,3 +1,9 @@
1
+ const {
2
+ identityLabel,
3
+ normalizeSessionId,
4
+ pairKeyFromIdentities,
5
+ } = require("../../runtime/CallPairRef");
6
+
1
7
  function createOfferFlow({
2
8
  sessions,
3
9
  sessionsByUser,
@@ -23,21 +29,6 @@ function createOfferFlow({
23
29
  return addr;
24
30
  }
25
31
 
26
- function identityLabel(identity) {
27
- if (!identity || typeof identity !== "string") return identity;
28
- const trimmed = identity.trim();
29
- const atPos = trimmed.indexOf("@");
30
- if (atPos > 0) return trimmed.slice(0, atPos);
31
- const dotPos = trimmed.indexOf(".");
32
- if (dotPos > 0) return trimmed.slice(0, dotPos);
33
- return trimmed;
34
- }
35
-
36
- function normalizeSessionId(sessionId) {
37
- if (!sessionId || typeof sessionId !== "string") return sessionId;
38
- return sessionId.split("|").map(identityLabel).join("|");
39
- }
40
-
41
32
  function relationSessionCandidates(from, to, sessionId) {
42
33
  const fromLabel = identityLabel(from);
43
34
  const toLabel = identityLabel(to);
@@ -47,6 +38,7 @@ function createOfferFlow({
47
38
  candidates.add(`${fromLabel}|${toLabel}`);
48
39
  candidates.add(`${toLabel}|${fromLabel}`);
49
40
  candidates.add(stableKey(from, to));
41
+ candidates.add(pairKeyFromIdentities(from, to));
50
42
  }
51
43
  return candidates;
52
44
  }
@@ -1,13 +1,6 @@
1
- // Bare label for an identity: strips an @domain or a .sub.global suffix.
2
- function identityLabel(identity) {
3
- if (!identity || typeof identity !== "string") return identity;
4
- const trimmed = identity.trim();
5
- const atPos = trimmed.indexOf("@");
6
- if (atPos > 0) return trimmed.slice(0, atPos);
7
- const dotPos = trimmed.indexOf(".");
8
- if (dotPos > 0) return trimmed.slice(0, dotPos);
9
- return trimmed;
10
- }
1
+ const {
2
+ identityLabel,
3
+ } = require("../../runtime/CallPairRef");
11
4
 
12
5
  // The single source of truth for the wire sessionId we hand a client. Every
13
6
  // client keys a session as sort(ownFullEns, peerBareNumber): it stores ITSELF as
@@ -1,17 +1,11 @@
1
- function authIdentityLabel(identity) {
2
- if (!identity || typeof identity !== "string") return identity;
3
- const trimmed = identity.trim();
4
- const atPos = trimmed.indexOf("@");
5
- if (atPos > 0) return trimmed.slice(0, atPos);
6
- const dotPos = trimmed.indexOf(".");
7
- if (dotPos > 0) return trimmed.slice(0, dotPos);
8
- return trimmed;
9
- }
10
- function authNormalizeSessionId(sessionId) {
11
- if (!sessionId || typeof sessionId !== "string") return sessionId;
12
- if (!sessionId.includes("|")) return sessionId;
13
- return sessionId.split("|").map(authIdentityLabel).join("|");
14
- }
1
+ const {
2
+ identityLabel,
3
+ normalizeSessionId,
4
+ pairKeyFromIdentities,
5
+ } = require("../../runtime/CallPairRef");
6
+
7
+ const authIdentityLabel = identityLabel;
8
+ const authNormalizeSessionId = normalizeSessionId;
15
9
 
16
10
  class SignalingAuthVerifier {
17
11
  constructor({ blockchainGateway, sessions }) {
@@ -32,6 +26,7 @@ class SignalingAuthVerifier {
32
26
  if (fromLabel && toLabel) {
33
27
  candidates.add(`${fromLabel}|${toLabel}`);
34
28
  candidates.add(`${toLabel}|${fromLabel}`);
29
+ candidates.add(pairKeyFromIdentities(fromLabel, toLabel));
35
30
  }
36
31
  for (const candidate of candidates) {
37
32
  const session = this.sessions.get(candidate);
@@ -85,22 +80,6 @@ function createSignalingPipeline({
85
80
  return null;
86
81
  }
87
82
 
88
- function identityLabel(identity) {
89
- if (!identity || typeof identity !== "string") return identity;
90
- const trimmed = identity.trim();
91
- const atPos = trimmed.indexOf("@");
92
- if (atPos > 0) return trimmed.slice(0, atPos);
93
- const dotPos = trimmed.indexOf(".");
94
- if (dotPos > 0) return trimmed.slice(0, dotPos);
95
- return trimmed;
96
- }
97
-
98
- function normalizeSessionId(sessionId) {
99
- if (!sessionId || typeof sessionId !== "string") return sessionId;
100
- if (!sessionId.includes("|")) return sessionId;
101
- return sessionId.split("|").map(identityLabel).join("|");
102
- }
103
-
104
83
  function normalizeNotifyPayload(rawPayload) {
105
84
  const source = rawPayload || {};
106
85
  const nestedPayload = parseObjectPayload(source.payload);
@@ -0,0 +1,46 @@
1
+ function identityLabel(identity) {
2
+ if (!identity || typeof identity !== "string") return identity;
3
+ const trimmed = identity.trim();
4
+ const atPos = trimmed.indexOf("@");
5
+ if (atPos > 0) return trimmed.slice(0, atPos);
6
+ const dotPos = trimmed.indexOf(".");
7
+ if (dotPos > 0) return trimmed.slice(0, dotPos);
8
+ return trimmed;
9
+ }
10
+
11
+ function normalizeIdentityForKey(identity) {
12
+ return identityLabel(String(identity || "").toLowerCase());
13
+ }
14
+
15
+ function createCallPairRef(caller, callee) {
16
+ return {
17
+ caller: caller || null,
18
+ callee: callee || null,
19
+ };
20
+ }
21
+
22
+ function pairKeyFromIdentities(a, b) {
23
+ return [normalizeIdentityForKey(a), normalizeIdentityForKey(b)]
24
+ .sort()
25
+ .join("|");
26
+ }
27
+
28
+ function pairKeyFromRef(callPairRef) {
29
+ if (!callPairRef) return pairKeyFromIdentities("", "");
30
+ return pairKeyFromIdentities(callPairRef.caller, callPairRef.callee);
31
+ }
32
+
33
+ function normalizeSessionId(sessionId) {
34
+ if (!sessionId || typeof sessionId !== "string") return sessionId;
35
+ if (!sessionId.includes("|")) return sessionId;
36
+ return sessionId.split("|").map((part) => identityLabel(part)).join("|");
37
+ }
38
+
39
+ module.exports = {
40
+ identityLabel,
41
+ normalizeIdentityForKey,
42
+ createCallPairRef,
43
+ pairKeyFromIdentities,
44
+ pairKeyFromRef,
45
+ normalizeSessionId,
46
+ };
@@ -0,0 +1,60 @@
1
+ const {
2
+ createCallPairRef,
3
+ pairKeyFromRef,
4
+ pairKeyFromIdentities,
5
+ } = require("./CallPairRef");
6
+
7
+ class CallPairResolver {
8
+ constructor({ polyRegistry } = {}) {
9
+ if (!polyRegistry) throw new Error("CallPairResolver requires polyRegistry");
10
+ this.polyRegistry = polyRegistry;
11
+ }
12
+
13
+ fromSession(session) {
14
+ if (!session) return null;
15
+ if (session.callPairRef?.caller && session.callPairRef?.callee) return session.callPairRef;
16
+ return createCallPairRef(session.callerEns, session.toIdentity);
17
+ }
18
+
19
+ fromIdentities(caller, callee) {
20
+ return createCallPairRef(caller, callee);
21
+ }
22
+
23
+ keyFromSession(session) {
24
+ const ref = this.fromSession(session);
25
+ if (!ref) return null;
26
+ return pairKeyFromRef(ref);
27
+ }
28
+
29
+ keyFromIdentities(caller, callee) {
30
+ return pairKeyFromIdentities(caller, callee);
31
+ }
32
+
33
+ keyFromPoly(poly) {
34
+ if (!poly) return null;
35
+ return pairKeyFromIdentities(poly.legs.a.endpoint, poly.legs.b.endpoint);
36
+ }
37
+
38
+ polyForSession(session) {
39
+ const key = this.keyFromSession(session);
40
+ if (!key) return null;
41
+ return this.polyRegistry.get(key) || null;
42
+ }
43
+
44
+ polyForOffer(offer) {
45
+ if (!offer) return null;
46
+ const key = this.keyFromIdentities(offer.from, offer.to);
47
+ if (!key) return null;
48
+ return this.polyRegistry.get(key) || null;
49
+ }
50
+
51
+ bindSessionPairRef(session, caller, callee) {
52
+ if (!session) return null;
53
+ session.callPairRef = createCallPairRef(caller, callee);
54
+ return session.callPairRef;
55
+ }
56
+ }
57
+
58
+ module.exports = {
59
+ CallPairResolver,
60
+ };
@@ -1,3 +1,10 @@
1
+ const {
2
+ identityLabel,
3
+ pairKeyFromIdentities,
4
+ normalizeSessionId,
5
+ createCallPairRef,
6
+ } = require("./CallPairRef");
7
+
1
8
  class SessionStore {
2
9
  constructor() {
3
10
  this.sessions = new Map();
@@ -42,23 +49,15 @@ class SessionStore {
42
49
  }
43
50
 
44
51
  identityLabel(identity) {
45
- if (!identity || typeof identity !== "string") return identity;
46
- const trimmed = identity.trim();
47
- const atPos = trimmed.indexOf("@");
48
- if (atPos > 0) return trimmed.slice(0, atPos);
49
- const dotPos = trimmed.indexOf(".");
50
- if (dotPos > 0) return trimmed.slice(0, dotPos);
51
- return trimmed;
52
+ return identityLabel(identity);
52
53
  }
53
54
 
54
55
  normalizeSessionId(sessionId) {
55
- if (!sessionId || typeof sessionId !== "string") return sessionId;
56
- if (!sessionId.includes("|")) return sessionId;
57
- return sessionId.split("|").map((part) => this.identityLabel(part)).join("|");
56
+ return normalizeSessionId(sessionId);
58
57
  }
59
58
 
60
59
  stableKey(a, b) {
61
- return [this.identityLabel(a), this.identityLabel(b)].sort().join("|");
60
+ return pairKeyFromIdentities(a, b);
62
61
  }
63
62
 
64
63
  createSession(sessionId, callerEns, toIdentity, logger = console) {
@@ -92,6 +91,7 @@ class SessionStore {
92
91
  },
93
92
  },
94
93
  inboundRingSent: false,
94
+ callPairRef: createCallPairRef(callerEns, toIdentity),
95
95
  };
96
96
  this.sessions.set(canonicalSessionId, session);
97
97
  if (callerEns && toIdentity) {
@@ -143,6 +143,11 @@ const { LEG_STATES, canBeRung, isActiveCall } = require("./modules/calls/poly/st
143
143
  const { isInactiveOffer } = require("./modules/calls/poly/negotiation/sdp");
144
144
  const { routeToCodecPolicy } = require("./modules/media/negotiation/CodecPolicy");
145
145
  const { narrowAudioOfferForCodecPolicy } = require("./modules/media/negotiation/SdpCodecNegotiator");
146
+ const {
147
+ identityLabel,
148
+ createCallPairRef,
149
+ } = require("./modules/runtime/CallPairRef");
150
+ const { CallPairResolver } = require("./modules/runtime/CallPairResolver");
146
151
  const {
147
152
  MediaStreamTrack,
148
153
  } = werift;
@@ -720,6 +725,7 @@ const polyCore = createPolyCore({
720
725
  });
721
726
  const polyRegistry = polyCore.registry;
722
727
  const polyIngress = polyCore.ingress;
728
+ const callPairResolver = new CallPairResolver({ polyRegistry });
723
729
  console.log("[poly] PolySession core is the live coordinator");
724
730
 
725
731
 
@@ -824,6 +830,7 @@ async function handleInboundCallRequest(data, serviceContext = null) {
824
830
  // An inbound call always brings a fresh session + FCM wake, so it
825
831
  // must own a fresh poly -> tear any stale one down first.
826
832
  await polyRegistry.destroy(polyRegistry.keyForPair(callerNumber, calleeEns), "inbound-fresh-call");
833
+ callPairResolver.bindSessionPairRef(session, callerNumber, calleeEns);
827
834
  const { poly } = polyRegistry.resolve({
828
835
  a: { endpoint: callerNumber, kind: "sip", phoneNumber, session },
829
836
  b: {
@@ -963,20 +970,11 @@ function createPeerConnection(...args) {
963
970
  // ─── PolySession ingress helpers (manager seam) ─────────────────────────────
964
971
 
965
972
  function pLabel(identity) {
966
- if (!identity || typeof identity !== "string") return identity;
967
- const t = identity.trim();
968
- const at = t.indexOf("@");
969
- if (at > 0) return t.slice(0, at);
970
- const dot = t.indexOf(".");
971
- if (dot > 0) return t.slice(0, dot);
972
- return t;
973
+ return identityLabel(identity);
973
974
  }
974
975
 
975
976
  function polyForSession(session) {
976
- if (!session) return null;
977
- return polyRegistry.getByEndpoint(session.callerEns)
978
- || polyRegistry.getByEndpoint(session.toIdentity)
979
- || null;
977
+ return callPairResolver.polyForSession(session);
980
978
  }
981
979
 
982
980
  // Which leg ref a webrtc message on `session` targets. callee-webrtc role => the
@@ -1088,6 +1086,7 @@ async function onDcRing(callerSessionId, payload) {
1088
1086
  };
1089
1087
  }
1090
1088
 
1089
+ callPairResolver.bindSessionPairRef(session, a.endpoint, b.endpoint);
1091
1090
  const { poly } = polyRegistry.resolve({ a, b, target: "a" });
1092
1091
  // Caller transport is already up (HTTP handshake). The SIP leg has no transport
1093
1092
  // to negotiate (openOutbound happens on ring), so mark it usable too. A webrtc
@@ -1183,8 +1182,9 @@ function onDataChannelMessage(sessionId, rawMessage, meta = {}) {
1183
1182
  if (!existing || !polyOwnsSession(existing, session, channelRole)) {
1184
1183
  (async () => {
1185
1184
  if (existing) {
1185
+ const existingKey = callPairResolver.keyFromPoly(existing);
1186
1186
  await polyRegistry.destroy(
1187
- polyRegistry.keyForPair(existing.legs.a.endpoint, existing.legs.b.endpoint),
1187
+ existingKey,
1188
1188
  "new-ring-reset",
1189
1189
  );
1190
1190
  }
@@ -1246,7 +1246,7 @@ function onDataChannelMessage(sessionId, rawMessage, meta = {}) {
1246
1246
 
1247
1247
  // HTTP /notify "answer" (callee picked up: secnum<->secnum leg or inbound callee).
1248
1248
  async function onVerifiedNotifyAnswer(sessionId, offer, session) {
1249
- const poly = polyForSession(session) || polyRegistry.getByEndpoint(offer.from);
1249
+ const poly = polyForSession(session) || callPairResolver.polyForOffer(offer);
1250
1250
  if (!poly) return { handled: false };
1251
1251
  const ref = polyRefByEndpoint(poly, offer.from)
1252
1252
  || (poly.legs.b.kind === "webrtc" ? "b" : "a");
@@ -1343,7 +1343,10 @@ async function onTransportClosed(sessionId, event = {}) {
1343
1343
  } catch (err) {
1344
1344
  console.error(`[${sessionId}] poly transport-close failed: ${err.message}`);
1345
1345
  }
1346
- try { await polyRegistry.destroy(polyRegistry.keyForPair(poly.legs.a.endpoint, poly.legs.b.endpoint), event.reason || "transport-closed"); } catch (_) {}
1346
+ try {
1347
+ const key = callPairResolver.keyFromPoly(poly);
1348
+ await polyRegistry.destroy(key, event.reason || "transport-closed");
1349
+ } catch (_) {}
1347
1350
  }
1348
1351
  destroySession(sessionId, event.notify === true);
1349
1352
  }
@@ -1389,6 +1392,7 @@ function sendDataChannelMessage(sessionId, message) {
1389
1392
 
1390
1393
  function createSession(sessionId, callerEns, toIdentity) {
1391
1394
  const session = sessionStore.createSession(sessionId, callerEns, toIdentity, console);
1395
+ session.callPairRef = createCallPairRef(callerEns, toIdentity);
1392
1396
  const call = callFactory.fromSession(session);
1393
1397
  session.call = call;
1394
1398
  session.callId = call.id;