mirror of
https://github.com/jambonz/sbc-outbound.git
synced 2026-10-04 02:04:19 +00:00
Compare commits
7
Commits
v0.9.1-rc2
...
v0.9.2-rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62fb716236 | ||
|
|
b8fdf9f429 | ||
|
|
503aa73bc6 | ||
|
|
a7406ddb8a | ||
|
|
bf1c670b40 | ||
|
|
b21eaad1ff | ||
|
|
c671fbdbd0 |
@@ -6,12 +6,17 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
node-version: 20.x
|
||||
- run: npm ci
|
||||
- run: npm run jslint
|
||||
- name: Install Docker Compose
|
||||
run: |
|
||||
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
docker-compose --version
|
||||
- run: npm test
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run jslint
|
||||
npm run jslint
|
||||
@@ -50,7 +50,8 @@ const {
|
||||
lookupSipGatewaysByCarrier,
|
||||
lookupCarrierBySid,
|
||||
queryCallLimits,
|
||||
lookupCarrierByAccountLcr
|
||||
lookupCarrierByAccountLcr,
|
||||
lookupSystemInformation
|
||||
} = require('@jambonz/db-helpers')({
|
||||
host: process.env.JAMBONES_MYSQL_HOST,
|
||||
port: process.env.JAMBONES_MYSQL_PORT || 3306,
|
||||
@@ -84,6 +85,7 @@ srf.locals = {...srf.locals,
|
||||
queryCdrs,
|
||||
activeCallIds,
|
||||
idleEmitter,
|
||||
privateNetworkCidr: process.env.PRIVATE_VOIP_NETWORK_CIDR || null,
|
||||
dbHelpers: {
|
||||
ping,
|
||||
lookupOutboundCarrierForAccount,
|
||||
@@ -94,7 +96,8 @@ srf.locals = {...srf.locals,
|
||||
lookupSipGatewaysByCarrier,
|
||||
lookupCarrierBySid,
|
||||
queryCallLimits,
|
||||
lookupCarrierByAccountLcr
|
||||
lookupCarrierByAccountLcr,
|
||||
lookupSystemInformation
|
||||
},
|
||||
realtimeDbHelpers: {
|
||||
client: redisClient,
|
||||
@@ -184,10 +187,18 @@ if (process.env.K8S || process.env.HTTP_PORT) {
|
||||
});
|
||||
}
|
||||
if ('test' !== process.env.NODE_ENV) {
|
||||
/* update call stats periodically */
|
||||
setInterval(() => {
|
||||
/* update call stats periodically as well as definition of private network cidr */
|
||||
setInterval(async() => {
|
||||
stats.gauge('sbc.sip.calls.count', activeCallIds.size, ['direction:outbound',
|
||||
`instance_id:${process.env.INSTANCE_ID || 0}`]);
|
||||
|
||||
const r = await lookupSystemInformation();
|
||||
if (r) {
|
||||
if (r.private_network_cidr !== srf.locals.privateNetworkCidr) {
|
||||
logger.info(`updating private network cidr from ${srf.locals.privateNetworkCidr} to ${r.private_network_cidr}`);
|
||||
srf.locals.privateNetworkCidr = r.private_network_cidr;
|
||||
}
|
||||
}
|
||||
}, 20000);
|
||||
}
|
||||
|
||||
@@ -262,4 +273,4 @@ function handle(signal) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {srf};
|
||||
module.exports = {srf, logger};
|
||||
|
||||
+138
-73
@@ -8,29 +8,46 @@ const debug = require('debug')('jambonz:sbc-outbound');
|
||||
|
||||
const makeInviteInProgressKey = (callid) => `sbc-out-iip${callid}`;
|
||||
const IMMUTABLE_HEADERS = ['via', 'from', 'to', 'call-id', 'cseq', 'max-forwards', 'content-length'];
|
||||
/**
|
||||
* this is to make sure the outgoing From has the number in the incoming From
|
||||
* and not the incoming PAI
|
||||
*/
|
||||
const createBLegFromHeader = (req, teams, register_from_domain = null) => {
|
||||
|
||||
const createBLegFromHeader = ({
|
||||
logger,
|
||||
req,
|
||||
host,
|
||||
fromUser,
|
||||
register_from_domain,
|
||||
transport,
|
||||
teams = false,
|
||||
scheme = 'sip'
|
||||
}) => {
|
||||
const from = req.getParsedHeader('From');
|
||||
const uri = parseUri(from.uri);
|
||||
let user = uri.user || 'anonymous';
|
||||
let host = 'localhost';
|
||||
if (teams) {
|
||||
host = req.get('X-MS-Teams-Tenant-FQDN');
|
||||
}
|
||||
else if (req.has('X-Preferred-From-User') || req.has('X-Preferred-From-Host')) {
|
||||
user = req.get('X-Preferred-From-User') || user;
|
||||
host = req.get('X-Preferred-From-Host') || host;
|
||||
} else if (register_from_domain) {
|
||||
host = register_from_domain;
|
||||
const transportParam = transport ? `;transport=${transport}` : '';
|
||||
|
||||
logger.debug({from, fromUser, uri, host, scheme, transport, teams}, 'createBLegFromHeader');
|
||||
/* user */
|
||||
const user = fromUser || req.get('X-Preferred-From-User') || uri.user || 'anonymous';
|
||||
|
||||
/* host */
|
||||
if (!host) {
|
||||
if (teams) {
|
||||
host = req.get('X-MS-Teams-Tenant-FQDN');
|
||||
}
|
||||
else if (req.has('X-Preferred-From-Host')) {
|
||||
host = req.get('X-Preferred-From-Host');
|
||||
} else if (register_from_domain) {
|
||||
host = register_from_domain;
|
||||
}
|
||||
else {
|
||||
host = 'localhost';
|
||||
}
|
||||
}
|
||||
|
||||
if (from.name) {
|
||||
return `${from.name} <sip:${user}@${host}>`;
|
||||
return `${from.name} <${scheme}:${user}@${host}${transportParam}>`;
|
||||
}
|
||||
return `sip:${user}@${host}`;
|
||||
return `<${scheme}:${user}@${host}${transportParam}>`;
|
||||
};
|
||||
|
||||
const createBLegToHeader = (req, teams) => {
|
||||
const to = req.getParsedHeader('To');
|
||||
const host = teams ? req.get('X-MS-Teams-Tenant-FQDN') : 'localhost';
|
||||
@@ -193,9 +210,7 @@ class CallSession extends Emitter {
|
||||
try {
|
||||
// determine where to send the call
|
||||
debug(`connecting call: ${JSON.stringify(this.req.locals)}`);
|
||||
let headers = {
|
||||
'From': createBLegFromHeader(this.req, teams),
|
||||
'Contact': createBLegFromHeader(this.req, teams),
|
||||
const headers = {
|
||||
'To': createBLegToHeader(this.req, teams),
|
||||
Allow: 'INVITE, ACK, OPTIONS, CANCEL, BYE, NOTIFY, UPDATE, PRACK',
|
||||
'X-Account-Sid': this.account_sid
|
||||
@@ -219,6 +234,7 @@ class CallSession extends Emitter {
|
||||
if (!contact.includes('transport=ws')) {
|
||||
proxy = this.req.locals.registration.proxy;
|
||||
}
|
||||
this.logger.info(`sending call to registered user ${destUri}`);
|
||||
}
|
||||
else if (this.req.locals.target === 'forward') {
|
||||
uris = [{
|
||||
@@ -243,10 +259,6 @@ class CallSession extends Emitter {
|
||||
private_network: false,
|
||||
uri: `sip:${this.req.calledNumber}@sip.pstnhub.microsoft.com`
|
||||
}];
|
||||
headers = {
|
||||
...headers,
|
||||
Contact: `sip:${this.req.calledNumber}@${this.req.get('X-MS-Teams-Tenant-FQDN')}:5061;transport=tls`
|
||||
};
|
||||
}
|
||||
else {
|
||||
try {
|
||||
@@ -296,20 +308,18 @@ class CallSession extends Emitter {
|
||||
this.req.calledNumber.slice(1) :
|
||||
this.req.calledNumber;
|
||||
const prefix = vc.tech_prefix || '';
|
||||
const protocol = o.protocol?.startsWith('tls') ? 'tls' : (o.protocol || 'udp');
|
||||
const transport = o.protocol?.startsWith('tls') ? 'tls' : (o.protocol || 'udp');
|
||||
const hostport = !o.port || 5060 === o.port ? o.ipv4 : `${o.ipv4}:${o.port}`;
|
||||
const prependPlus = vc.e164_leading_plus && !this.req.calledNumber.startsWith('0') ? '+' : '';
|
||||
const transport = `transport=${protocol}`;
|
||||
const useSipsScheme = protocol === 'tls' &&
|
||||
!process.env.JAMBONES_USE_BEST_EFFORT_TLS &&
|
||||
o.use_sips_scheme;
|
||||
const scheme = useSipsScheme ? 'sips' : 'sip';
|
||||
const u = `${scheme}:${prefix}${prependPlus}${calledNumber}@${hostport};${transport}`;
|
||||
const scheme = transport === 'tls' && !process.env.JAMBONES_USE_BEST_EFFORT_TLS && o.use_sips_scheme ?
|
||||
'sips' : 'sip';
|
||||
let u = `${scheme}:${prefix}${prependPlus}${calledNumber}@${hostport};transport=${transport}`;
|
||||
const obj = {
|
||||
name: vc.name,
|
||||
diversion: vc.diversion,
|
||||
hostport,
|
||||
protocol: o.protocol,
|
||||
transport,
|
||||
scheme,
|
||||
register_from_domain: vc.register_from_domain
|
||||
};
|
||||
if (vc.register_username && vc.register_password) {
|
||||
@@ -318,8 +328,14 @@ class CallSession extends Emitter {
|
||||
password: vc.register_password
|
||||
};
|
||||
}
|
||||
if (vc.requires_register && vc.register_sip_realm?.length > 0) {
|
||||
proxy = u;
|
||||
u = `${scheme}:${prefix}${prependPlus}${calledNumber}@${vc.register_sip_realm};transport=${transport}`;
|
||||
this.logger.debug({uri: u}, `using outbound proxy for this registered trunk: ${proxy}`);
|
||||
}
|
||||
mapGateways.set(u, obj);
|
||||
uris.push(u);
|
||||
this.logger.debug({gateway: o}, `pushed uri ${u}`);
|
||||
if (o.protocol === 'tls/srtp') {
|
||||
/** TODO: this is a bit of a hack in the sense that we are not
|
||||
* supporting a scenario where you have a carrier with several outbound
|
||||
@@ -332,7 +348,7 @@ class CallSession extends Emitter {
|
||||
encryptedMedia = true;
|
||||
}
|
||||
});
|
||||
// Check private network for each gw
|
||||
/* Check private network for each gw */
|
||||
uris = await Promise.all(uris.map(async(u) => {
|
||||
return {
|
||||
private_network: await isPrivateVoipNetwork(u),
|
||||
@@ -354,14 +370,12 @@ class CallSession extends Emitter {
|
||||
debug(`sending call to PSTN ${uris}`);
|
||||
}
|
||||
|
||||
// private_network should be called at last
|
||||
/* private_network should be called at last - try public first */
|
||||
uris = uris.sort((a, b) => a.private_network - b.private_network);
|
||||
const toPrivate = uris.some((u) => u.private_network === true);
|
||||
const toPublic = uris.some((u) => u.private_network === false);
|
||||
let isOfferUpdatedToPrivate = toPrivate && !toPublic;
|
||||
|
||||
|
||||
// rtpengine 'offer'
|
||||
const opts = updateRtpEngineFlags(this.req.body, {
|
||||
...this.rtpEngineOpts.common,
|
||||
...this.rtpEngineOpts.uac.mediaOpts,
|
||||
@@ -370,7 +384,6 @@ class CallSession extends Emitter {
|
||||
sdp: this.req.body
|
||||
});
|
||||
let response = await this.offer(opts);
|
||||
debug(`response from rtpengine to offer ${JSON.stringify(response)}`);
|
||||
this.logger.debug({offer: opts, response}, 'initial offer to rtpengine');
|
||||
if ('ok' !== response.result) {
|
||||
this.logger.error(`rtpengine offer failed with ${JSON.stringify(response)}`);
|
||||
@@ -383,10 +396,12 @@ class CallSession extends Emitter {
|
||||
// crank through the list of gateways until connected, exhausted or caller hangs up
|
||||
let earlyMedia = false;
|
||||
while (uris.length) {
|
||||
let hdrs = { ...headers};
|
||||
let hdrs = { ...headers };
|
||||
const {private_network, uri} = uris.shift();
|
||||
|
||||
/* if we've exhausted attempts to public endpoints and are switching to trying private, we need new rtp */
|
||||
if (private_network && !isOfferUpdatedToPrivate) {
|
||||
// Cannot make call to all public Uris, now come to talk with private network Uris
|
||||
this.logger.info('switching to attempt to deliver call via private network now..');
|
||||
this.rtpEngineResource.destroy()
|
||||
.catch((err) => this.logger.info({err}, 'Error destroying rtpe to re-connect to private network'));
|
||||
response = await this.offer({
|
||||
@@ -395,8 +410,8 @@ class CallSession extends Emitter {
|
||||
});
|
||||
isOfferUpdatedToPrivate = true;
|
||||
}
|
||||
const gw = mapGateways.get(uri);
|
||||
const passFailure = 0 === uris.length; // only a single target
|
||||
|
||||
/* on the second and subsequent attempts, use the same Call-ID and CSeq from the first attempt */
|
||||
if (0 === uris.length) {
|
||||
try {
|
||||
const key = makeInviteInProgressKey(this.req.get('Call-ID'));
|
||||
@@ -412,32 +427,76 @@ class CallSession extends Emitter {
|
||||
this.logger.info({err}, 'Error retrieving iip key');
|
||||
}
|
||||
}
|
||||
// INVITE request line and To header should be the same.
|
||||
hdrs = {...hdrs, 'To': uri};
|
||||
if (gw) {
|
||||
this.logger.info({gw}, `sending INVITE to ${uri} via carrier ${gw.name}`);
|
||||
if (gw.diversion) {
|
||||
let div = gw.diversion;
|
||||
if (div.startsWith('+')) {
|
||||
div = `<sip:${div}@${gw.hostport}>;reason=unknown;counter=1;privacy=off`;
|
||||
}
|
||||
else div = `<sip:+${div}@${gw.hostport}>;reason=unknown;counter=1;privacy=off`;
|
||||
hdrs = {
|
||||
...hdrs,
|
||||
'Diversion': div
|
||||
};
|
||||
}
|
||||
if (gw.register_from_domain) {
|
||||
hdrs = {
|
||||
...hdrs,
|
||||
'From': createBLegFromHeader(this.req, teams, gw.register_from_domain)
|
||||
};
|
||||
}
|
||||
}
|
||||
else this.logger.info(`sending INVITE to ${uri} via proxy ${proxy})`);
|
||||
try {
|
||||
const responseHeaders = this.privateSipAddress ? {Contact: `<sip:${this.privateSipAddress}>`} : {};
|
||||
|
||||
/* INVITE request line and To header should be the same. */
|
||||
hdrs = {...hdrs, 'To': uri};
|
||||
|
||||
/* only now can we set Contact & From header since they depend on transport and scheme of gw */
|
||||
const gw = mapGateways.get(uri);
|
||||
if (gw) {
|
||||
const {scheme, transport} = gw;
|
||||
this.logger.info({gw}, `sending INVITE to ${uri} via carrier ${gw.name}`);
|
||||
hdrs = {
|
||||
...hdrs,
|
||||
From: gw.register_from_domain ?
|
||||
createBLegFromHeader({
|
||||
logger: this.logger,
|
||||
req: this.req,
|
||||
register_from_domain: gw.register_from_domain,
|
||||
scheme,
|
||||
transport,
|
||||
...(private_network && {host: this.privateSipAddress})
|
||||
}) :
|
||||
createBLegFromHeader({
|
||||
logger: this.logger,
|
||||
req: this.req,
|
||||
scheme,
|
||||
transport,
|
||||
...(private_network && {host: this.privateSipAddress})
|
||||
}),
|
||||
Contact: createBLegFromHeader({
|
||||
logger: this.logger,
|
||||
req: this.req,
|
||||
...(gw.auth?.username && {fromUser: gw.auth?.username}),
|
||||
scheme,
|
||||
transport,
|
||||
...(private_network && {host: this.privateSipAddress})
|
||||
}),
|
||||
...(gw.diversion && {
|
||||
Diversion: gw.diversion.startsWith('+') ?
|
||||
`<sip:${gw.diversion}@${gw.hostport}>;reason=unknown;counter=1;privacy=off` :
|
||||
`<sip:+${gw.diversion}@${gw.hostport}>;reason=unknown;counter=1;privacy=off`
|
||||
})
|
||||
};
|
||||
}
|
||||
else if (teams) {
|
||||
hdrs = {
|
||||
...hdrs,
|
||||
'From': createBLegFromHeader({logger: this.logger, req: this.req, teams: true, transport: 'tls'}),
|
||||
'Contact': `sip:${this.req.calledNumber}@${this.req.get('X-MS-Teams-Tenant-FQDN')}:5061;transport=tls`
|
||||
};
|
||||
}
|
||||
else {
|
||||
hdrs = {
|
||||
...hdrs,
|
||||
'From': createBLegFromHeader({
|
||||
logger: this.logger,
|
||||
req: this.req,
|
||||
...(private_network && {host: this.privateSipAddress})
|
||||
}),
|
||||
'Contact': createBLegFromHeader({
|
||||
logger: this.logger,
|
||||
req: this.req,
|
||||
...(private_network && {host: this.privateSipAddress})
|
||||
})
|
||||
};
|
||||
const p = proxy ? ` via ${proxy}` : '';
|
||||
this.logger.info({uri, p}, `sending INVITE to ${uri}${p}`);
|
||||
}
|
||||
|
||||
/* now launch an outbound call attempt */
|
||||
const passFailure = 0 === uris.length; // only propagate failure on last attempt
|
||||
try {
|
||||
const {uas, uac} = await this.srf.createB2BUA(this.req, this.res, uri, {
|
||||
proxy,
|
||||
passFailure,
|
||||
@@ -463,7 +522,6 @@ class CallSession extends Emitter {
|
||||
'-Session-Expires'
|
||||
],
|
||||
headers: hdrs,
|
||||
responseHeaders,
|
||||
auth: gw ? gw.auth : undefined,
|
||||
localSdpB: response.sdp,
|
||||
localSdpA: async(sdp, res) => {
|
||||
@@ -516,6 +574,9 @@ class CallSession extends Emitter {
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Error saving Call-ID/CSeq');
|
||||
}
|
||||
|
||||
this.contactHeader = inv.get('Contact');
|
||||
this.logger.info(`outbound call attempt to ${uri} has contact header ${this.contactHeader}`);
|
||||
},
|
||||
cbProvisional: (response) => {
|
||||
if (!earlyMedia && [180, 183].includes(response.status) && response.body) earlyMedia = true;
|
||||
@@ -788,14 +849,19 @@ Duration=${payload.duration} `
|
||||
sdp
|
||||
};
|
||||
response = await this.answer(opts);
|
||||
/* now remove asymeetric as B party (looking at you Genesys ring group) may need port re-learning on invites */
|
||||
/* now remove asymmetric as B party (looking at you Genesys ring group) may need port re-learning on invites */
|
||||
answerMedia.flags = answerMedia.flags.filter((f) => f !== 'asymmetric');
|
||||
if ('ok' !== response.result) {
|
||||
res.send(488);
|
||||
throw new Error(`_onReinvite: rtpengine failed: ${JSON.stringify(response)}`);
|
||||
}
|
||||
this.logger.debug({opts, sdp: response.sdp}, 'CallSession:_onReinvite: (answer) sending back upstream');
|
||||
res.send(200, {body: response.sdp});
|
||||
res.send(200, {
|
||||
body: response.sdp,
|
||||
headers: {
|
||||
'Contact': this.contactHeader
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
res.send(err.status || 500);
|
||||
this.logger.error(err, 'Error handling reinvite');
|
||||
@@ -1037,14 +1103,13 @@ Duration=${payload.duration} `
|
||||
const referredBy = req.getParsedHeader('Referred-By');
|
||||
if (!referredBy) return res.send(400);
|
||||
const u = parseUri(referredBy.uri);
|
||||
const farEnd = parseUri(this.connectedUri);
|
||||
uri.host = farEnd.host;
|
||||
uri.port = farEnd.port;
|
||||
|
||||
/* delete contact if it was there from feature server */
|
||||
delete customHeaders['contact'];
|
||||
|
||||
const response = await this.uac.request({
|
||||
method: 'REFER',
|
||||
headers: {
|
||||
// Make sure the uri is protected by <> if uri is complex form
|
||||
'Refer-To': `<${stringifyUri(uri)}>`,
|
||||
'Referred-By': `<${stringifyUri(u)}>`,
|
||||
...customHeaders
|
||||
|
||||
+10
-6
@@ -4,9 +4,6 @@ const debug = require('debug')('jambonz:sbc-outbound');
|
||||
const CIDRMatcher = require('cidr-matcher');
|
||||
const dns = require('dns');
|
||||
|
||||
const cidrMatcher = process.env.PRIVATE_VOIP_NETWORK_CIDR ?
|
||||
new CIDRMatcher(process.env.PRIVATE_VOIP_NETWORK_CIDR.split(',')) : null;
|
||||
|
||||
function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams) {
|
||||
const from = req.getParsedHeader('from');
|
||||
const rtpCopy = JSON.parse(JSON.stringify(rtpCharacteristics));
|
||||
@@ -191,8 +188,12 @@ const nudgeCallCounts = async(logger, sids, nudgeOperator, writers) => {
|
||||
};
|
||||
|
||||
const isPrivateVoipNetwork = async(uri) => {
|
||||
if (cidrMatcher) {
|
||||
const {srf, logger} = require('..');
|
||||
const {privateNetworkCidr} = srf.locals;
|
||||
|
||||
if (privateNetworkCidr) {
|
||||
try {
|
||||
const matcher = new CIDRMatcher(privateNetworkCidr.split(','));
|
||||
const arr = /sips?:.*@(.*?)(:\d+)?(;.*)$/.exec(uri);
|
||||
if (arr) {
|
||||
const input = arr[1];
|
||||
@@ -203,12 +204,15 @@ const isPrivateVoipNetwork = async(uri) => {
|
||||
addresses = await dns.resolve4(input);
|
||||
}
|
||||
for (const ip of addresses) {
|
||||
if (cidrMatcher.contains(ip)) {
|
||||
if (matcher.contains(ip)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {}
|
||||
} catch (err) {
|
||||
logger.info({err, privateNetworkCidr},
|
||||
'Error checking private network CIDR, probably misconfigured must be a comma separated list of CIDRs');
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
Generated
+469
-4546
File diff suppressed because it is too large
Load Diff
+5
-5
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "sbc-outbound",
|
||||
"version": "0.9.0",
|
||||
"version": "0.9.2",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
"node": ">= 18.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"sip",
|
||||
@@ -27,8 +27,8 @@
|
||||
"jslint": "eslint app.js lib --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jambonz/db-helpers": "^0.9.3",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.8",
|
||||
"@jambonz/db-helpers": "^0.9.7",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.9",
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/mw-registrar": "0.2.7",
|
||||
"@jambonz/rtpengine-utils": "^0.4.4",
|
||||
@@ -38,7 +38,7 @@
|
||||
"cidr-matcher": "^2.1.1",
|
||||
"debug": "^4.3.4",
|
||||
"drachtio-fn-b2b-sugar": "^0.1.0",
|
||||
"drachtio-srf": "^4.5.35",
|
||||
"drachtio-srf": "^4.5.39",
|
||||
"express": "^4.19.2",
|
||||
"pino": "^8.20.0",
|
||||
"sdp-transform": "^2.14.2"
|
||||
|
||||
Reference in New Issue
Block a user