From eb681f9ddf4be3add9ddc58eef606b0ac39727aa Mon Sep 17 00:00:00 2001 From: Sam Machin Date: Thu, 20 Nov 2025 12:18:17 +0000 Subject: [PATCH 01/10] force account sip_realm to lowercase (#519) --- lib/routes/api/accounts.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/routes/api/accounts.js b/lib/routes/api/accounts.js index 4b90588..ce5096e 100644 --- a/lib/routes/api/accounts.js +++ b/lib/routes/api/accounts.js @@ -560,6 +560,8 @@ router.post('/', async(req, res) => { } delete obj[prop]; } + //force sip realm to lowercase + if (obj.sip_realm) { obj.sip_realm = obj.sip_realm.toLowerCase(); } logger.debug(`Attempting to add account ${JSON.stringify(obj)}`); const uuid = await Account.make(obj); @@ -802,6 +804,9 @@ router.put('/:sid', async(req, res) => { encryptBucketCredential(obj, storedBucketCredentials); + //force sip realm to lowercase + if (obj.sip_realm) { obj.sip_realm = obj.sip_realm.toLowerCase();} + const rowsAffected = await Account.update(sid, obj); if (rowsAffected === 0) { return res.status(404).end(); From 40754deb3efe5d07745aa605a88c38cc596a6457 Mon Sep 17 00:00:00 2001 From: Hoan Luu Huu <110280845+xquanluu@users.noreply.github.com> Date: Sat, 29 Nov 2025 09:47:40 +0700 Subject: [PATCH 02/10] soundhound speech credential support audio endpoint (#520) * soundhound speech credential support audio endpoint * soundhound speech credential support audio endpoint * wip * wip --- lib/routes/api/speech-credentials.js | 9 ++++++--- lib/utils/speech-utils.js | 5 ++++- package-lock.json | 8 ++++---- package.json | 2 +- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/routes/api/speech-credentials.js b/lib/routes/api/speech-credentials.js index ead1e2f..f63118e 100644 --- a/lib/routes/api/speech-credentials.js +++ b/lib/routes/api/speech-credentials.js @@ -166,6 +166,7 @@ const encryptCredential = (obj) => { engine_version, service_version, api_uri, + houndify_server_uri, options } = obj; @@ -329,7 +330,7 @@ const encryptCredential = (obj) => { assert(client_id, 'invalid houndify speech credential: client_id is required'); assert(client_key, 'invalid houndify speech credential: client_key is required'); assert(user_id, 'invalid houndify speech credential: user_id is required'); - const houndifyData = JSON.stringify({client_id, client_key, user_id}); + const houndifyData = JSON.stringify({client_id, client_key, user_id, houndify_server_uri}); return encrypt(houndifyData); case 'voxist': @@ -557,7 +558,8 @@ router.put('/:sid', async(req, res) => { speechmatics_stt_uri, resemble_tts_use_tls, resemble_tts_uri, - api_uri + api_uri, + houndify_server_uri } = req.body; const newCred = { @@ -593,7 +595,8 @@ router.put('/:sid', async(req, res) => { speechmatics_stt_uri, resemble_tts_uri, resemble_tts_use_tls, - api_uri + api_uri, + houndify_server_uri }; logger.info({o, newCred}, 'updating speech credential with this new credential'); obj.credential = encryptCredential(newCred); diff --git a/lib/utils/speech-utils.js b/lib/utils/speech-utils.js index e27eaa2..40937b7 100644 --- a/lib/utils/speech-utils.js +++ b/lib/utils/speech-utils.js @@ -676,8 +676,10 @@ const testHoundifyStt = async(logger, credentials) => { requestInfo: { UserID: user_id || 'test_user', Latitude: 37.388309, - Longitude: -121.973968 + Longitude: -121.973968, }, + // custom endpint is used only for feature server. + // ...(houndify_server_uri && {endpoint: houndify_server_uri}), // Audio format configuration sampleRate: 16000, @@ -887,6 +889,7 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) { obj.client_key = isObscureKey ? obscureKey(o.client_key) : o.client_key; obj.client_id = o.client_id; obj.user_id = o.user_id; + obj.houndify_server_uri = o.houndify_server_uri; } else if ('resemble' === obj.vendor) { const o = JSON.parse(decrypt(credential)); obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key; diff --git a/package-lock.json b/package-lock.json index 449e5f6..fc22ae3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@jambonz/realtimedb-helpers": "^0.8.15", "@jambonz/speech-utils": "^0.2.26", "@jambonz/time-series": "^0.2.8", - "@jambonz/verb-specifications": "^0.0.118", + "@jambonz/verb-specifications": "^0.0.122", "@soniox/soniox-node": "^1.2.2", "ajv": "^8.17.1", "argon2": "^0.40.1", @@ -4346,9 +4346,9 @@ } }, "node_modules/@jambonz/verb-specifications": { - "version": "0.0.118", - "resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.118.tgz", - "integrity": "sha512-1dGnc6TUCehjt1yGNuqh1uzk1xw9HhUm39aVUosQMHlnT0fK0ItikeJ0uttTjFastHNmPPxqJwb20wOvVGTCFg==", + "version": "0.0.122", + "resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.122.tgz", + "integrity": "sha512-7xqaULhKFywJ2ZuyiYt77iiJwJ+8b98Zt1X4+OqZ7Cdjhfo7S6KnR66XRVJHnekXbmfVv58kB0KWUux5TG//Sw==", "license": "MIT", "dependencies": { "debug": "^4.3.4", diff --git a/package.json b/package.json index a811a74..d80ddd6 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@jambonz/realtimedb-helpers": "^0.8.15", "@jambonz/speech-utils": "^0.2.26", "@jambonz/time-series": "^0.2.8", - "@jambonz/verb-specifications": "^0.0.118", + "@jambonz/verb-specifications": "^0.0.122", "@soniox/soniox-node": "^1.2.2", "ajv": "^8.17.1", "argon2": "^0.40.1", From 6ef40a648cdde4888e3e63289fcd1b3f323d75ef Mon Sep 17 00:00:00 2001 From: Hoan Luu Huu <110280845+xquanluu@users.noreply.github.com> Date: Sun, 7 Dec 2025 20:37:38 +0700 Subject: [PATCH 03/10] allow boostAudioSignal from updateCall (#523) --- lib/routes/api/accounts.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/routes/api/accounts.js b/lib/routes/api/accounts.js index ce5096e..0683b43 100644 --- a/lib/routes/api/accounts.js +++ b/lib/routes/api/accounts.js @@ -298,7 +298,8 @@ function validateUpdateCall(opts) { 'tag', 'dtmf', 'conferenceParticipantAction', - 'dub' + 'dub', + 'boostAudioSignal' ] .reduce((acc, prop) => (opts[prop] ? ++acc : acc), 0); From 15f2d92f715ad39406c3272f53a4d1f80f3a2c57 Mon Sep 17 00:00:00 2001 From: Hoan Luu Huu <110280845+xquanluu@users.noreply.github.com> Date: Mon, 8 Dec 2025 20:12:35 +0700 Subject: [PATCH 04/10] subscription update-quantities validate min voice call sessions (#521) * subscription update-quantities validate min voice call sessions * subscription update-quantities validate min voice call sessions * fixed review comment --- lib/routes/api/subscriptions.js | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/lib/routes/api/subscriptions.js b/lib/routes/api/subscriptions.js index 2961bf0..59fc0dd 100644 --- a/lib/routes/api/subscriptions.js +++ b/lib/routes/api/subscriptions.js @@ -17,6 +17,7 @@ const { } = require('../../utils/stripe-utils'); const {setupFreeTrial} = require('./utils'); const sysError = require('../error'); +const Product = require('../../models/product'); const actions = [ 'upgrade-to-paid', 'downgrade-to-free', @@ -24,6 +25,8 @@ const actions = [ 'update-quantities' ]; +const MIN_VOICE_CALL_SESSION_QUANTITY = 5; + const handleError = async(logger, method, res, err) => { if ('StatusError' === err.name) { const text = await err.text(); @@ -146,6 +149,24 @@ const upgradeToPaidPlan = async(req, res) => { await handleSubscriptionOutcome(req, res, subscription); }; + +const validateProductQuantities = async(products) => { + // validate voice call session minimums + const productModel = new Product(); + const availableProducts = await productModel.retrieveAll(); + const voiceCallSessionsProductSid = + availableProducts.find((p) => p.category === 'voice_call_session')?.product_sid; + if (voiceCallSessionsProductSid) { + const invalid = products.find((p) => { + return (p.product_sid === voiceCallSessionsProductSid && + (typeof p.quantity !== 'number' || p.quantity < MIN_VOICE_CALL_SESSION_QUANTITY)); + }); + if (invalid) { + throw new DbErrorBadRequest('invalid voice call session value, minimum is ' + + MIN_VOICE_CALL_SESSION_QUANTITY); + } + } +}; const downgradeToFreePlan = async(req, res) => { const logger = req.app.locals.logger; const {account_sid} = req.user; @@ -291,11 +312,11 @@ router.post('/', async(req, res) => { if ('update-payment-method' === action && typeof payment_method_id !== 'string') { throw new DbErrorBadRequest('missing payment_method_id'); } - if ('upgrade-to-paid' === action && (!Array.isArray(products) || 0 === products.length)) { - throw new DbErrorBadRequest('missing products'); - } - if ('update-quantities' === action && (!Array.isArray(products) || 0 === products.length)) { - throw new DbErrorBadRequest('missing products'); + if (['update-quantities', 'upgrade-to-paid'].includes(action)) { + if ((!Array.isArray(products) || 0 === products.length)) { + throw new DbErrorBadRequest('missing products'); + } + await validateProductQuantities(products); } switch (action) { From 1aa28e8ba0d75b570da05a2dc52b75371e3d5c14 Mon Sep 17 00:00:00 2001 From: Hoan Luu Huu <110280845+xquanluu@users.noreply.github.com> Date: Fri, 12 Dec 2025 20:56:55 +0700 Subject: [PATCH 05/10] fixed how to detect obscured key (#524) * fixed how to detect obscured key * wip * wip --- lib/utils/encrypt-decrypt.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/utils/encrypt-decrypt.js b/lib/utils/encrypt-decrypt.js index 0368171..772e6c9 100644 --- a/lib/utils/encrypt-decrypt.js +++ b/lib/utils/encrypt-decrypt.js @@ -50,18 +50,15 @@ function isObscureKey(bucketCredentials) { service_key = '', connection_string = '' } = bucketCredentials || {}; - let pattern; + // Pattern matches: 4-6 any characters followed by one or more X's + const pattern = /^.{4,6}X+$/; switch (vendor) { case 'aws_s3': case 's3_compatible': - pattern = /^([A-Za-z0-9]{4,6}X+$)/; return pattern.test(secret_access_key); case 'azure': - pattern = /^([A-Za-z0-9:]{4,6}X+$)/; return pattern.test(connection_string); - case 'google': { - pattern = /^([A-Za-z0-9]{4,6}X+$)/; let {private_key} = JSON.parse(service_key); const key_header = '-----BEGIN PRIVATE KEY-----\n'; private_key = private_key.slice(key_header.length, private_key.length); From dd798132295d28f6ffb612dca3fa9f404de98176 Mon Sep 17 00:00:00 2001 From: Hoan Luu Huu <110280845+xquanluu@users.noreply.github.com> Date: Wed, 17 Dec 2025 19:27:01 +0700 Subject: [PATCH 06/10] cannot fetch voice_call_session (#525) --- lib/routes/api/subscriptions.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/routes/api/subscriptions.js b/lib/routes/api/subscriptions.js index 59fc0dd..2324ca0 100644 --- a/lib/routes/api/subscriptions.js +++ b/lib/routes/api/subscriptions.js @@ -151,9 +151,7 @@ const upgradeToPaidPlan = async(req, res) => { }; const validateProductQuantities = async(products) => { - // validate voice call session minimums - const productModel = new Product(); - const availableProducts = await productModel.retrieveAll(); + const availableProducts = await Product.retrieveAll(); const voiceCallSessionsProductSid = availableProducts.find((p) => p.category === 'voice_call_session')?.product_sid; if (voiceCallSessionsProductSid) { From e02db2e0258a6044a853915d9804b37299939d0a Mon Sep 17 00:00:00 2001 From: Hoan Luu Huu <110280845+xquanluu@users.noreply.github.com> Date: Fri, 19 Dec 2025 06:36:23 +0700 Subject: [PATCH 07/10] update speech utils version 0.2.27 (#527) --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index fc22ae3..790dd9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "@jambonz/lamejs": "^1.2.2", "@jambonz/mw-registrar": "^0.2.7", "@jambonz/realtimedb-helpers": "^0.8.15", - "@jambonz/speech-utils": "^0.2.26", + "@jambonz/speech-utils": "^0.2.27", "@jambonz/time-series": "^0.2.8", "@jambonz/verb-specifications": "^0.0.122", "@soniox/soniox-node": "^1.2.2", @@ -4178,9 +4178,9 @@ } }, "node_modules/@jambonz/speech-utils": { - "version": "0.2.26", - "resolved": "https://registry.npmjs.org/@jambonz/speech-utils/-/speech-utils-0.2.26.tgz", - "integrity": "sha512-gL5PBd2zSDCxAgz5cIdiLgkhqGEkW5UizF93BV8xcpMZuzIuLhsr2WlIMY3L3vVpFSx8AtZ9XdgPtxIOqI+FHw==", + "version": "0.2.27", + "resolved": "https://registry.npmjs.org/@jambonz/speech-utils/-/speech-utils-0.2.27.tgz", + "integrity": "sha512-wx0VKq2Gy8+pNx/RwMJjvaG7HT1/pezlgKkUfL0a4DoXxwIoglVyc/2SPUMf9yZ5xKSifYxcGoGSKfor9f9EbQ==", "license": "MIT", "dependencies": { "23": "^0.0.0", diff --git a/package.json b/package.json index d80ddd6..86f6335 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "@jambonz/lamejs": "^1.2.2", "@jambonz/mw-registrar": "^0.2.7", "@jambonz/realtimedb-helpers": "^0.8.15", - "@jambonz/speech-utils": "^0.2.26", + "@jambonz/speech-utils": "^0.2.27", "@jambonz/time-series": "^0.2.8", "@jambonz/verb-specifications": "^0.0.122", "@soniox/soniox-node": "^1.2.2", From 4201ebbe9c2f20a69a49be7b0a99fa251554d1d8 Mon Sep 17 00:00:00 2001 From: Sam Machin Date: Fri, 19 Dec 2025 12:32:20 +0000 Subject: [PATCH 08/10] Fix/526 (#528) * calidate webhook urls on update * don't remove webhooks if not updated * valid if object exists --- lib/routes/api/applications.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/routes/api/applications.js b/lib/routes/api/applications.js index 6020bec..50e0d3a 100644 --- a/lib/routes/api/applications.js +++ b/lib/routes/api/applications.js @@ -101,6 +101,20 @@ async function validateUpdate(req, sid) { if (req.body.call_status_hook && typeof req.body.call_hook !== 'object') { throw new DbErrorBadRequest('\'call_status_hook\' must be an object when updating an application'); } + + let urlError; + if (req.body.call_hook) { + urlError = await isInvalidUrl(req.body.call_hook.url); + if (urlError) { + throw new DbErrorBadRequest(`call_hook ${urlError}`); + } + } + if (req.body.call_status_hook) { + urlError = await isInvalidUrl(req.body.call_status_hook.url); + if (urlError) { + throw new DbErrorBadRequest(`call_status_hook ${urlError}`); + } + } } async function validateDelete(req, sid) { @@ -290,9 +304,6 @@ router.put('/:sid', async(req, res) => { obj[`${prop}_sid`] = sid; } } - else { - obj[`${prop}_sid`] = null; - } delete obj[prop]; } From 3f1e75646783ab4751d4c3f216479ab8f5f9d993 Mon Sep 17 00:00:00 2001 From: Dave Horton Date: Mon, 22 Dec 2025 08:28:37 -0500 Subject: [PATCH 09/10] wip (#529) --- lib/routes/stripe/webhook.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/routes/stripe/webhook.js b/lib/routes/stripe/webhook.js index fc5cc16..a792b84 100644 --- a/lib/routes/stripe/webhook.js +++ b/lib/routes/stripe/webhook.js @@ -13,6 +13,10 @@ const handleInvoicePaymentSucceeded = async(logger, obj) => { const sub = await retrieveSubscription(logger, subscription); if ('active' === sub.status) { const {account_sid} = sub.metadata; + if (!account_sid) { + logger.info({subscription}, `handleInvoicePaymentSucceeded: received subscription ${sub.id} without account_sid`); + return; + } if (await Account.activateSubscription(logger, account_sid, sub.id, 'subscription_create' === obj.billing_reason ? 'upgrade to paid plan' : 'change plan details')) { logger.info(`handleInvoicePaymentSucceeded: activated subscription for account ${account_sid}`); @@ -35,6 +39,10 @@ const handleInvoicePaymentFailed = async(logger, obj) => { const sub = await retrieveSubscription(logger, subscription); logger.debug({obj}, `payment for ${obj.billing_reason} failed, subscription status is ${sub.status}`); const {account_sid} = sub.metadata; + if (!account_sid) { + logger.info({subscription}, `handleInvoicePaymentFailed: received subscription ${sub.id} without account_sid`); + return; + } if (await Account.deactivateSubscription(logger, account_sid, 'payment failed')) { logger.info(`handleInvoicePaymentFailed: deactivated subscription for account ${account_sid}`); } From 69046ab5d27ef831c093e3d6127bddffab9135bc Mon Sep 17 00:00:00 2001 From: Sam Machin Date: Wed, 7 Jan 2026 13:01:44 +0000 Subject: [PATCH 10/10] Feat/admin numbers carriers (#532) * add JAMBONES_ADMIN_CARRIER check to limit creating carriers and numbers * fix logic --- lib/routes/api/accounts.js | 9 +++++++++ lib/routes/api/phone-numbers.js | 5 +++++ lib/routes/api/sip-gateways.js | 6 ++++++ lib/routes/api/voip-carriers.js | 11 +++++++++++ 4 files changed, 31 insertions(+) diff --git a/lib/routes/api/accounts.js b/lib/routes/api/accounts.js index 0683b43..f3d02c6 100644 --- a/lib/routes/api/accounts.js +++ b/lib/routes/api/accounts.js @@ -140,6 +140,11 @@ router.put('/:sid/VoipCarriers/:voip_carrier_sid', async(req, res) => { const logger = req.app.locals.logger; try { + if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider') + && !req.user.hasScope('admin'))) { + throw new DbErrorBadRequest('insufficient privileges'); + } + const sid = parseVoipCarrierSid(req); const account_sid = parseAccountSid(req); await validateRequest(req, account_sid); @@ -159,6 +164,10 @@ router.post('/:sid/VoipCarriers', async(req, res) => { const logger = req.app.locals.logger; const payload = req.body; try { + if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider') + || !!req.user.hasScope('admin'))) { + throw new DbErrorBadRequest('insufficient privileges'); + } const account_sid = parseAccountSid(req); await validateRequest(req, account_sid); // Set the service_provder_sid to the relevent value for the account diff --git a/lib/routes/api/phone-numbers.js b/lib/routes/api/phone-numbers.js index a87d75a..3d22869 100644 --- a/lib/routes/api/phone-numbers.js +++ b/lib/routes/api/phone-numbers.js @@ -19,6 +19,11 @@ const hasWhitespace = (str) => /\s/.test(str); /* check for required fields when adding */ async function validateAdd(req) { try { + if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider') + && !req.user.hasScope('admin'))) { + throw new DbErrorBadRequest('insufficient privileges'); + } + /* account level user can only act on carriers associated to his/her account */ if (req.user.hasAccountAuth) { req.body.account_sid = req.user.account_sid; diff --git a/lib/routes/api/sip-gateways.js b/lib/routes/api/sip-gateways.js index 1246270..199b2da 100644 --- a/lib/routes/api/sip-gateways.js +++ b/lib/routes/api/sip-gateways.js @@ -45,6 +45,12 @@ const validate = async(req, sid) => { const {netmask, ipv4, inbound, outbound} = req.body; let voip_carrier_sid; + if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider') + && !req.user.hasScope('admin'))) { + throw new DbErrorBadRequest('insufficient privileges'); + } + + if (sid) { const gateway = await lookupSipGatewayBySid(sid); if (!gateway) throw new DbErrorBadRequest('invalid sip_gateway_sid'); diff --git a/lib/routes/api/voip-carriers.js b/lib/routes/api/voip-carriers.js index f1e8753..a94b3a7 100644 --- a/lib/routes/api/voip-carriers.js +++ b/lib/routes/api/voip-carriers.js @@ -9,6 +9,11 @@ const { parseVoipCarrierSid } = require('./utils'); const validate = async(req) => { const {lookupAppBySid, lookupAccountBySid} = req.app.locals; + if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider') + && !req.user.hasScope('admin'))) { + throw new DbErrorBadRequest('insufficient privileges'); + } + /* account level user can only act on carriers associated to his/her account */ if (req.user.hasAccountAuth) { req.body.account_sid = req.user.account_sid; @@ -45,6 +50,12 @@ const validateUpdate = async(req, sid) => { const validateDelete = async(req, sid) => { const {lookupCarrierBySid} = req.app.locals; + if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider') + && !req.user.hasScope('admin'))) { + throw new DbErrorBadRequest('insufficient privileges'); + } + + if (req.user.hasAccountAuth) { /* can only update carriers for the user's account */ const carrier = await lookupCarrierBySid(sid);