Merge branch 'main' into feat/speech_utils_03

This commit is contained in:
Hoan Luu Huu
2026-01-09 13:42:09 +07:00
committed by GitHub
12 changed files with 101 additions and 23 deletions
+16 -1
View File
@@ -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
@@ -298,7 +307,8 @@ function validateUpdateCall(opts) {
'tag',
'dtmf',
'conferenceParticipantAction',
'dub'
'dub',
'boostAudioSignal'
]
.reduce((acc, prop) => (opts[prop] ? ++acc : acc), 0);
@@ -560,6 +570,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 +814,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();
+14 -3
View File
@@ -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];
}
+5
View File
@@ -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;
+6
View File
@@ -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');
+6 -3
View File
@@ -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);
+24 -5
View File
@@ -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,22 @@ const upgradeToPaidPlan = async(req, res) => {
await handleSubscriptionOutcome(req, res, subscription);
};
const validateProductQuantities = async(products) => {
const availableProducts = await Product.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 +310,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) {
+11
View File
@@ -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);
+8
View File
@@ -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}`);
}
+2 -5
View File
@@ -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);
+4 -1
View File
@@ -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;
+4 -4
View File
@@ -23,7 +23,7 @@
"@jambonz/realtimedb-helpers": "^0.8.15",
"@jambonz/speech-utils": "^0.2.27",
"@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",
+1 -1
View File
@@ -34,7 +34,7 @@
"@jambonz/realtimedb-helpers": "^0.8.15",
"@jambonz/speech-utils": "^0.2.27",
"@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",