arnacon-webrtc-service 0.1.71 → 0.1.73

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.1.71",
3
+ "version": "0.1.73",
4
4
  "description": "Arnacon WebRTC core runtime and service modules",
5
5
  "main": "./webRTCservice/core.js",
6
6
  "type": "commonjs",
@@ -98,14 +98,16 @@ function createBridgeApi({
98
98
  return trimmed;
99
99
  }
100
100
 
101
- function narrowAudioOfferToG711(sdp) {
101
+ function narrowAudioOfferToPayloads(sdp, allowedPayloads) {
102
102
  if (!sdp || !sdp.includes("m=audio")) return sdp;
103
+ const allowedPayloadSet = new Set((allowedPayloads || []).map(String));
104
+ if (allowedPayloadSet.size === 0) return sdp;
103
105
  const sections = sdp.split(/\r\n(?=m=)/);
104
106
  return sections.map((section) => {
105
107
  if (!section.startsWith("m=audio")) return section;
106
108
  const lines = section.split(/\r\n/);
107
109
  const mLine = lines[0].split(" ");
108
- const payloads = mLine.slice(3).filter((pt) => pt === "0" || pt === "8");
110
+ const payloads = mLine.slice(3).filter((pt) => allowedPayloadSet.has(pt));
109
111
  if (!payloads.length) return section;
110
112
 
111
113
  const allowed = new Set(payloads);
@@ -119,6 +121,13 @@ function createBridgeApi({
119
121
  }).join("\r\n");
120
122
  }
121
123
 
124
+ function narrowAudioOfferForCodecPolicy(sdp, codecPolicy) {
125
+ if (codecPolicy === "pcmu") return narrowAudioOfferToPayloads(sdp, ["0"]);
126
+ if (codecPolicy === "pcma") return narrowAudioOfferToPayloads(sdp, ["8"]);
127
+ if (codecPolicy === "g711") return narrowAudioOfferToPayloads(sdp, ["0", "8"]);
128
+ return sdp;
129
+ }
130
+
122
131
  function attachOutboundDataChannel(legSessionId, legSession) {
123
132
  const pc = createPeerConnection(legSessionId);
124
133
  if (typeof pc.createDataChannel !== "function") return pc;
@@ -186,11 +195,11 @@ function createBridgeApi({
186
195
  const candidatesToEmbed = srflxAndRelay.length > 0 ? srflxAndRelay : gatheredCandidates;
187
196
  const relayCandidates = getRelayCandidates(gatheredCandidates);
188
197
  let baseOfferSdp = offer.sdp;
189
- if (callerSession.mediaCodecPolicy === "g711") {
190
- const g711OfferSdp = narrowAudioOfferToG711(baseOfferSdp);
191
- if (g711OfferSdp !== baseOfferSdp) {
192
- logger.log(`[${legSessionId}] WebRTC leg inherited G.711 bridge codec policy`);
193
- baseOfferSdp = g711OfferSdp;
198
+ if (callerSession.mediaCodecPolicy) {
199
+ const narrowedOfferSdp = narrowAudioOfferForCodecPolicy(baseOfferSdp, callerSession.mediaCodecPolicy);
200
+ if (narrowedOfferSdp !== baseOfferSdp) {
201
+ logger.log(`[${legSessionId}] WebRTC leg inherited bridge codec policy=${callerSession.mediaCodecPolicy}`);
202
+ baseOfferSdp = narrowedOfferSdp;
194
203
  }
195
204
  }
196
205
  const offerSdp = embedCandidatesInSdp(baseOfferSdp, candidatesToEmbed);
@@ -100,11 +100,11 @@ function createCallFlowApi({
100
100
  const candidatesToEmbed = srflxAndRelay.length > 0 ? srflxAndRelay : gatheredCandidates;
101
101
  const relayCandidates = getRelayCandidates(gatheredCandidates);
102
102
  let baseOfferSdp = offer.sdp;
103
- if (session.mediaCodecPolicy === "g711") {
104
- const g711OfferSdp = narrowAudioOfferToG711(baseOfferSdp);
105
- if (g711OfferSdp !== baseOfferSdp) {
106
- logger.log(`[${sessionId}] outbound WebRTC leg: narrowed RING offer to PCMU/PCMA`);
107
- baseOfferSdp = g711OfferSdp;
103
+ if (session.mediaCodecPolicy) {
104
+ const narrowedOfferSdp = narrowAudioOfferForCodecPolicy(baseOfferSdp, session.mediaCodecPolicy);
105
+ if (narrowedOfferSdp !== baseOfferSdp) {
106
+ logger.log(`[${sessionId}] outbound WebRTC leg: narrowed RING offer codecPolicy=${session.mediaCodecPolicy}`);
107
+ baseOfferSdp = narrowedOfferSdp;
108
108
  }
109
109
  }
110
110
  const offerSdp = embedCandidatesInSdp(baseOfferSdp, candidatesToEmbed);
@@ -198,6 +198,30 @@ function createCallFlowApi({
198
198
  return narrowAudioOfferToPayloads(sdp, ["8"]);
199
199
  }
200
200
 
201
+ function narrowAudioOfferToPcmu(sdp) {
202
+ return narrowAudioOfferToPayloads(sdp, ["0"]);
203
+ }
204
+
205
+ function narrowAudioOfferForCodecPolicy(sdp, codecPolicy) {
206
+ if (codecPolicy === "pcma") return narrowAudioOfferToPcma(sdp);
207
+ if (codecPolicy === "pcmu") return narrowAudioOfferToPcmu(sdp);
208
+ if (codecPolicy === "g711") return narrowAudioOfferToG711(sdp);
209
+ return sdp;
210
+ }
211
+
212
+ function getPrimaryAudioPayload(sdp) {
213
+ const audioSection = String(sdp || "").match(/m=audio[^\r\n]*[\s\S]*?(?=\r?\nm=|$)/m)?.[0] || "";
214
+ const mLine = audioSection.match(/^m=audio[^\r\n]*/m)?.[0] || "";
215
+ return mLine.split(/\s+/).slice(3)[0] || null;
216
+ }
217
+
218
+ function exactG711PolicyFromAnswer(answerSdp) {
219
+ const primaryPt = getPrimaryAudioPayload(answerSdp);
220
+ if (primaryPt === "0") return "pcmu";
221
+ if (primaryPt === "8") return "pcma";
222
+ return null;
223
+ }
224
+
201
225
  function routeCodecPolicy(destination, isInbound) {
202
226
  if (isInbound) return false;
203
227
  if (destination?.route === "sbc") return "pcma";
@@ -294,7 +318,14 @@ function createCallFlowApi({
294
318
  ensureLocalAudioTrack(session, pc, sessionId);
295
319
  const answerLabel = isInactive ? "PHASE 1 ANSWER SDP" : "ANSWER SDP";
296
320
  const answerSdp = await createAnswerSdp(pc, sessionId, answerLabel);
297
- if (!isInbound && destination?.route === "ivr") storeIvrNegotiatedAudio(session, sessionId, answerSdp);
321
+ if (!isInbound && destination?.route === "ivr") {
322
+ const exactPolicy = exactG711PolicyFromAnswer(answerSdp);
323
+ if (exactPolicy) {
324
+ session.mediaCodecPolicy = exactPolicy;
325
+ logger.log(`[${sessionId}] IVR bridge codec policy resolved to ${exactPolicy}`);
326
+ }
327
+ storeIvrNegotiatedAudio(session, sessionId, answerSdp);
328
+ }
298
329
 
299
330
  if (!isInbound) sendAck(sessionId);
300
331
  let routeResult = null;
@@ -2,14 +2,8 @@
2
2
 
3
3
  const DOMAINS = ["secnumtest.global", "secnum.global", "cellactm.global", "cellactl.global"];
4
4
  const IVR_WAITING_AUDIO_FILE = "waiting.mp3";
5
- const HARD_CODED_MULTI_RING = {
6
- callerEns: "972557012401.secnumtest.global",
7
- callerSbcNumber: "972557258108",
8
- ringTargets: [
9
- "972557012402.secnumtest.global",
10
- "972557012403.secnumtest.global",
11
- ],
12
- };
5
+ const MULTIRING_CONFIG_BASE_URL = "https://lightpbx-save-config-343948402138.europe-west1.run.app";
6
+ const MULTIRING_CONFIG_TIMEOUT_MS = 2500;
13
7
 
14
8
  function getDomains(helpers) {
15
9
  const configured = helpers.getServiceConstants()?.domains;
@@ -45,17 +39,94 @@ async function resolveEnsWallet(helpers, ensName) {
45
39
  return null;
46
40
  }
47
41
 
48
- async function buildHardcodedMultiRing(parsedFrom, helpers) {
49
- const fromEns = String(parsedFrom?.full || "").toLowerCase();
50
- const fromNumber = helpers.normalizePhone(parsedFrom?.value || parsedFrom?.full || "");
51
- const matchedEns = fromEns === HARD_CODED_MULTI_RING.callerEns;
52
- const matchedSbcNumber = fromNumber === HARD_CODED_MULTI_RING.callerSbcNumber;
53
- if (!matchedEns && !matchedSbcNumber) {
42
+ function normalizeMultiringEndpoint(value, helpers) {
43
+ const firstLabel = String(value || "").trim().split(".")[0] || "";
44
+ return helpers.normalizePhone(firstLabel).replace(/\D/g, "");
45
+ }
46
+
47
+ async function fetchMultiringConfig(endpoint, helpers) {
48
+ if (!endpoint || typeof fetch !== "function") return null;
49
+ const controller = new AbortController();
50
+ const timeout = setTimeout(() => controller.abort(), MULTIRING_CONFIG_TIMEOUT_MS);
51
+ try {
52
+ const url = `${MULTIRING_CONFIG_BASE_URL}/get_mutiring/${encodeURIComponent(endpoint)}`;
53
+ const resp = await fetch(url, { method: "GET", signal: controller.signal });
54
+ if (!resp.ok) {
55
+ helpers.logRouteDecision?.({
56
+ serviceId: "secnum",
57
+ route: "multiring-config",
58
+ endpoint,
59
+ status: resp.status,
60
+ });
61
+ return null;
62
+ }
63
+ return await resp.json();
64
+ } catch (err) {
65
+ helpers.logRouteDecision?.({
66
+ serviceId: "secnum",
67
+ route: "multiring-config",
68
+ endpoint,
69
+ error: err.message,
70
+ });
54
71
  return null;
72
+ } finally {
73
+ clearTimeout(timeout);
74
+ }
75
+ }
76
+
77
+ function extractMultiringEmails(config) {
78
+ if (!config || config.ok === false || config.found === false) return [];
79
+ if (config?.route?.type !== "multi_ring") return [];
80
+
81
+ const directEmails = config?.route?.multiRing?.group?.selectedEmails;
82
+ if (Array.isArray(directEmails)) return directEmails;
83
+
84
+ const groupId = config?.route?.multiRing?.groupId;
85
+ if (!groupId || !Array.isArray(config?.groups)) return [];
86
+ const group = config.groups.find((item) => item?.id === groupId);
87
+ return Array.isArray(group?.selectedEmails) ? group.selectedEmails : [];
88
+ }
89
+
90
+ function multiringTargetToEnsName(target, helpers, targetDomain) {
91
+ const value = String(target || "").trim().toLowerCase();
92
+ if (!value) return null;
93
+
94
+ if (value.includes("@") && !value.endsWith(".global")) {
95
+ return helpers.emailToEnsName(helpers.normalizeEmail(value), targetDomain);
96
+ }
97
+
98
+ if (value.endsWith(".global")) {
99
+ return value;
100
+ }
101
+
102
+ const normalizedPhone = helpers.normalizePhone(value).replace(/\D/g, "");
103
+ if (normalizedPhone) {
104
+ return `${normalizedPhone}.${targetDomain}`;
55
105
  }
56
106
 
107
+ return null;
108
+ }
109
+
110
+ async function buildConfiguredMultiRing(parsedTo, helpers) {
111
+ const rawTarget = helpers.normalizePhone(parsedTo?.value || parsedTo?.full || "");
112
+ if (!/^\d+$/.test(rawTarget)) return null;
113
+
114
+ const endpoint = normalizeMultiringEndpoint(rawTarget, helpers);
115
+ if (!endpoint) return null;
116
+
117
+ const config = await fetchMultiringConfig(endpoint, helpers);
118
+ const ringTargets = extractMultiringEmails(config)
119
+ .map((target) => String(target || "").trim())
120
+ .filter(Boolean);
121
+ if (ringTargets.length === 0) return null;
122
+
123
+ const targetDomain = getDomains(helpers)[0];
57
124
  const targets = [];
58
- for (const ensName of HARD_CODED_MULTI_RING.ringTargets) {
125
+ const seenEnsNames = new Set();
126
+ for (const ringTarget of ringTargets) {
127
+ const ensName = multiringTargetToEnsName(ringTarget, helpers, targetDomain);
128
+ if (!ensName || seenEnsNames.has(ensName)) continue;
129
+ seenEnsNames.add(ensName);
59
130
  const wallet = await resolveEnsWallet(helpers, ensName);
60
131
  if (!wallet) continue;
61
132
  targets.push({ wallet, ensName });
@@ -69,7 +140,7 @@ async function buildHardcodedMultiRing(parsedFrom, helpers) {
69
140
  route: "webrtc-multiring",
70
141
  mode: "first-verified-answer-wins",
71
142
  targets,
72
- ruleId: "hardcoded-secnumtest-multiring",
143
+ ruleId: `gcp-secnum-multiring:${endpoint}`,
73
144
  };
74
145
  }
75
146
 
@@ -99,9 +170,6 @@ async function resolveDestination(ctx) {
99
170
  const { parsedTo, parsedFrom, helpers } = ctx;
100
171
  if (!parsedTo) return { route: "reject", reason: "Missing destination" };
101
172
 
102
- const multiRing = await buildHardcodedMultiRing(parsedFrom, helpers);
103
- if (multiRing) return multiRing;
104
-
105
173
  const normalizedTarget = helpers.normalizePhone(parsedTo.value || parsedTo.full || "");
106
174
  if (normalizedTarget === "2006") {
107
175
  return {
@@ -112,6 +180,9 @@ async function resolveDestination(ctx) {
112
180
  };
113
181
  }
114
182
 
183
+ const multiRing = await buildConfiguredMultiRing(parsedTo, helpers);
184
+ if (multiRing) return multiRing;
185
+
115
186
  if (parsedTo.type === "raw" || parsedTo.type === "unknown") {
116
187
  return { route: "sbc", number: helpers.normalizePhone(parsedTo.value) };
117
188
  }