Compare commits

...

19 Commits

Author SHA1 Message Date
Dave Horton
17afb7102a update speech-utils with fix for aws tts 2023-03-24 08:19:18 -04:00
Anton Voylenko
6e7cb9b332 update README (#129) 2023-03-23 15:44:45 -04:00
Anton Voylenko
34f83e323c update swagger yaml (#127) 2023-03-23 09:08:38 -04:00
Anton Voylenko
00af458cb3 Migrate to argon2 from argon2-ffi (#126) 2023-03-22 16:15:01 -04:00
Dave Horton
389017a5c4 update to latest speech-utils 2023-03-20 15:37:35 -04:00
Dave Horton
c4cc6c51ee eliminate parsing of jwt to support either jwt or api key (#124)
* eliminate parsing of jwt to support either jwt or api key

* fixes for preventing non-authorized changes to users

* update to AWS v3 api
2023-03-14 18:54:56 -04:00
Dave Horton
aea7388ba0 refactor of speech-utils (#123) 2023-03-14 10:01:05 -04:00
Dave Horton
3d86292a90 prevent updates to users that would move them to a different account … (#122)
* prevent updates to users that would move them to a different account or service provider, or make them admin users

* bugfix: when updating account as admin user, verify the account sid

* validate account sid when SP user tries to update
2023-03-08 11:38:49 -05:00
Dave Horton
08962fe7ba bugfix: get of speech credential was not returning soniox api_key 2023-03-03 13:49:36 -05:00
Dave Horton
e573f6ab06 change property names for custom speech 2023-03-02 15:28:53 -05:00
Dave Horton
4934e2a1ca add support for custom speech api (#121) 2023-03-02 14:13:41 -05:00
EgleH
cc384995ea add support for soniox speech (#120)
Co-authored-by: eglehelms <e.helms@cognigy.com>
2023-02-26 11:31:39 -05:00
Dave Horton
d4506fb8fa bump version 2023-02-24 10:05:14 -05:00
Dave Horton
042a2c37dc update dockerfile 2023-02-23 08:21:30 -05:00
EgleH
6da1903dee Update node to node:18.14.0-alpine3.16 (#117) 2023-02-21 07:54:26 -05:00
dependabot[bot]
10009d903e Bump undici from 5.11.0 to 5.19.1 (#115)
Bumps [undici](https://github.com/nodejs/undici) from 5.11.0 to 5.19.1.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v5.11.0...v5.19.1)

---
updated-dependencies:
- dependency-name: undici
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-16 17:33:12 -05:00
Snyk bot
69a72c5e43 fix: upgrade aws-sdk from 2.1238.0 to 2.1302.0 (#110)
Snyk has created this PR to upgrade aws-sdk from 2.1238.0 to 2.1302.0.

See this package in npm:
https://www.npmjs.com/package/aws-sdk

See this project in Snyk:
https://app.snyk.io/org/davehorton/project/b7e09765-19b2-4aa5-90ba-10432e250041?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-02-16 13:31:57 -05:00
Dave Horton
f7f3881d70 update to @jambonz/verb-specifications (#109) 2023-02-15 10:15:06 -05:00
Hoan Luu Huu
4d48c6946c feat: start using verb-specifications (#107)
* feat: start using verb-specifications

* fix: verb specification v2

* fix vulnerabilities

---------

Co-authored-by: Quan HL <quanluuhoang8@gmail.com>
Co-authored-by: Dave Horton <daveh@beachdognet.com>
2023-02-14 17:24:19 -05:00
18 changed files with 3531 additions and 1562 deletions

View File

@@ -1,4 +1,4 @@
FROM --platform=linux/amd64 node:18-alpine3.16 as base
FROM --platform=linux/amd64 node:18.14.1-alpine3.16 as base
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
@@ -20,4 +20,4 @@ ARG NODE_ENV
ENV NODE_ENV $NODE_ENV
CMD [ "node", "app.js" ]
CMD [ "node", "app.js" ]

View File

@@ -1,4 +1,4 @@
FROM --platform=linux/amd64 node:18.9.0-alpine3.16 as base
FROM --platform=linux/amd64 node:18.14.1-alpine3.16 as base
RUN apk --update --no-cache add --virtual .builds-deps build-base python3

View File

@@ -15,15 +15,17 @@ JAMBONES_MYSQL_CONNECTION_LIMIT # defaults to 10
JAMBONES_REDIS_HOST
JAMBONES_REDIS_PORT
JAMBONES_LOGLEVEL # defaults to info
JAMBONES_API_VERSION # defaults to v1
JAMBONES_API_VERSION # defaults to v1
JAMBONES_TIME_SERIES_HOST
JWT_SECRET
HTTP_PORT # defaults to 3000
```
#### Database dependency
A mysql database is used to store long-lived objects such as Accounts, Applications, etc. To create the database schema, use or review the scripts in the 'db' folder, particularly:
- [create_db.sql](db/create_db.sql), which creates the database and associated user (you may want to edit the username and password),
- [jambones-sql.sql](db/jambones-sql.sql), which creates the schema,
- [create-admin-token.sql](db/create-admin-token.sql), which creates an admin-level auth token that can be used for testing/exercising the API.
- [seed-production-database-open-source.sql](db/seed-production-database-open-source.sql), which seeds the database with initial dataset(accounts, permissions, api keys, applications etc).
- [create-admin-user.sql](db/create-admin-user.sql), which creates admin user with password set to "admin". The password will be forced to change after the first login.
> Note: due to the dependency on the npmjs [mysql](https://www.npmjs.com/package/mysql) package, the mysql database must be configured to use sql [native authentication](https://medium.com/@crmcmullen/how-to-run-mysql-8-0-with-native-password-authentication-502de5bac661).

7
app.js
View File

@@ -41,11 +41,16 @@ const {
addKey,
retrieveKey,
deleteKey,
getTtsVoices
} = require('@jambonz/realtimedb-helpers')({
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
const {
getTtsVoices
} = require('@jambonz/speech-utils')({
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
const {
lookupAppBySid,
lookupAccountBySid,

10
db/create-admin-user.sql Normal file
View File

@@ -0,0 +1,10 @@
/* hashed password is "admin" */
insert into users (user_sid, name, email, hashed_password, force_change, provider, email_validated)
values ('12c80508-edf9-4b22-8d09-55abd02648eb', 'admin', 'joe@foo.bar', '$argon2i$v=19$m=65536,t=3,p=4$c2FsdHNhbHRzYWx0c2FsdA$x5OO6gXFXS25oqUU2JvbYqrSgRxBujNUJBq6xv9EgjM', 1, 'local', 1);
insert into user_permissions (user_permissions_sid, user_sid, permission_sid)
values ('8919e0dc-4d69-4de5-be56-a121598d9093', '12c80508-edf9-4b22-8d09-55abd02648eb', 'ffbc342a-546a-11ed-bdc3-0242ac120002');
insert into user_permissions (user_permissions_sid, user_sid, permission_sid)
values ('d6fdf064-0a65-4b17-8b10-5500e956a159', '12c80508-edf9-4b22-8d09-55abd02648eb', 'ffbc3a10-546a-11ed-bdc3-0242ac120002');
insert into user_permissions (user_permissions_sid, user_sid, permission_sid)
values ('f68185dd-0486-4767-a77d-a0b84c1b236e' ,'12c80508-edf9-4b22-8d09-55abd02648eb', 'ffbc3c5e-546a-11ed-bdc3-0242ac120002');

View File

@@ -99,7 +99,7 @@ const checkApiTokens = (logger, token, done) => {
hasServiceProviderAuth: scope === 'service_provider',
hasAccountAuth: scope === 'account'
};
logger.info(user, `successfully validated with scope ${scope}`);
logger.debug({user}, `successfully validated with scope ${scope}`);
return done(null, user, {scope});
});
});

View File

@@ -344,10 +344,20 @@ async function validateUpdate(req, sid) {
if (req.user.service_provider_sid && !req.user.hasScope('admin')) {
const result = await Account.retrieve(sid);
if (!result || result.length === 0) {
throw new DbErrorBadRequest(`account not found for sid ${sid}`);
}
if (result[0].service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorUnprocessableRequest('cannot update account from different service provider');
}
}
if (req.user.hasScope('admin')) {
/* check to be sure that the account_sid exists */
const result = await Account.retrieve(sid);
if (!result || result.length === 0) {
throw new DbErrorBadRequest(`account not found for sid ${sid}`);
}
}
if (req.body.service_provider_sid) throw new DbErrorBadRequest('service_provider_sid may not be modified');
}
async function validateDelete(req, sid) {

View File

@@ -6,7 +6,7 @@ const Webhook = require('../../models/webhook');
const {promisePool} = require('../../db');
const decorate = require('./decorate');
const sysError = require('../error');
const jambonzAppJsonValidator = require('../../utils/jambonz-app-json-validation');
const { validate } = require('@jambonz/verb-specifications');
const preconditions = {
'add': validateAdd,
'update': validateUpdate
@@ -80,7 +80,11 @@ router.post('/', async(req, res) => {
// validate app json if required
if (obj['app_json']) {
const app_json = JSON.parse(obj['app_json']);
jambonzAppJsonValidator(logger, app_json);
try {
validate(logger, app_json);
} catch (err) {
throw new DbErrorBadRequest(err);
}
}
const uuid = await Application.make(obj);
@@ -189,7 +193,11 @@ router.put('/:sid', async(req, res) => {
// validate app json if required
if (obj['app_json']) {
const app_json = JSON.parse(obj['app_json']);
jambonzAppJsonValidator(logger, app_json);
try {
validate(logger, app_json);
} catch (err) {
throw new DbErrorBadRequest(err);
}
}
const rowsAffected = await Application.update(sid, obj);

View File

@@ -17,6 +17,7 @@ const {
testNuanceStt,
testNuanceTts,
testDeepgramStt,
testSonioxStt,
testIbmTts,
testIbmStt
} = require('../../utils/speech-utils');
@@ -52,7 +53,10 @@ const encryptCredential = (obj) => {
stt_api_key,
stt_region,
riva_server_uri,
instance_id
instance_id,
custom_stt_url,
custom_tts_url,
auth_token = ''
} = obj;
switch (vendor) {
@@ -112,8 +116,17 @@ const encryptCredential = (obj) => {
const nvidiaData = JSON.stringify({ riva_server_uri });
return encrypt(nvidiaData);
case 'soniox':
assert(api_key, 'invalid soniox speech credential: api_key is required');
const sonioxData = JSON.stringify({api_key});
return encrypt(sonioxData);
default:
assert(false, `invalid or missing vendor: ${vendor}`);
if (vendor.startsWith('custom:')) {
const customData = JSON.stringify({auth_token, custom_stt_url, custom_tts_url});
return encrypt(customData);
}
else assert(false, `invalid or missing vendor: ${vendor}`);
}
};
@@ -220,6 +233,16 @@ router.get('/', async(req, res) => {
const o = JSON.parse(decrypt(credential));
obj.riva_server_uri = o.riva_server_uri;
}
else if ('soniox' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
}
else if (obj.vendor.startsWith('custom:')) {
const o = JSON.parse(decrypt(credential));
obj.auth_token = obscureKey(o.auth_token);
obj.custom_stt_url = o.custom_stt_url;
obj.custom_tts_url = o.custom_tts_url;
}
return obj;
}));
} catch (err) {
@@ -285,6 +308,16 @@ router.get('/:sid', async(req, res) => {
const o = JSON.parse(decrypt(credential));
obj.riva_server_uri = o.riva_server_uri;
}
else if ('soniox' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
}
else if (obj.vendor.startsWith('custom:')) {
const o = JSON.parse(decrypt(credential));
obj.auth_token = obscureKey(o.auth_token);
obj.custom_stt_url = o.custom_stt_url;
obj.custom_tts_url = o.custom_tts_url;
}
res.status(200).json(obj);
} catch (err) {
sysError(logger, res, err);
@@ -583,8 +616,22 @@ router.get('/:sid/test', async(req, res) => {
}
}
}
else if (cred.vendor === 'soniox') {
const {api_key} = credential;
if (cred.use_for_stt) {
try {
await testSonioxStt(logger, {api_key});
results.stt.status = 'ok';
SpeechCredential.sttTestResult(sid, true);
} catch (err) {
results.stt = {status: 'fail', reason: err.message};
SpeechCredential.sttTestResult(sid, false);
}
}
}
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}

View File

@@ -1,6 +1,5 @@
const router = require('express').Router();
const User = require('../../models/user');
const jwt = require('jsonwebtoken');
const request = require('request');
const {DbErrorBadRequest} = require('../../utils/errors');
const {generateHashedPassword, verifyPassword} = require('../../utils/password-utils');
@@ -28,7 +27,8 @@ AND account_subscriptions.pending=0`;
const updateSql = 'UPDATE users set hashed_password = ?, force_change = false WHERE user_sid = ?';
const retrieveStaticIps = 'SELECT * FROM account_static_ips WHERE account_sid = ?';
const validateRequest = async(user_sid, payload) => {
const validateRequest = async(user_sid, req) => {
const payload = req.body;
const {
old_password,
new_password,
@@ -37,12 +37,43 @@ const validateRequest = async(user_sid, payload) => {
email,
email_activation_code,
force_change,
is_active} = payload;
is_active
} = payload;
const [r] = await promisePool.query(retrieveSql, user_sid);
if (r.length === 0) return null;
if (r.length === 0) {
throw new DbErrorBadRequest('Invalid request: user_sid does not exist');
}
const user = r[0];
/* it is not allowed for anyone to promote a user to a higher level of authority */
if (null === payload.account_sid || null === payload.service_provider_sid) {
throw new DbErrorBadRequest('Invalid request: user may not be promoted');
}
if (req.user.hasAccountAuth) {
/* account user may not change modify account_sid or service_provider_sid */
if ('account_sid' in payload && payload.account_sid !== user.account_sid) {
throw new DbErrorBadRequest('Invalid request: user may not be promoted or moved to another account');
}
if ('service_provider_sid' in payload && payload.service_provider_sid !== user.service_provider_sid) {
throw new DbErrorBadRequest('Invalid request: user may not be promoted or moved to another service provider');
}
}
if (req.user.hasServiceProviderAuth) {
if ('service_provider_sid' in payload && payload.service_provider_sid !== user.service_provider_sid) {
throw new DbErrorBadRequest('Invalid request: user may not be promoted or moved to another service provider');
}
}
if ('account_sid' in payload) {
const [r] = await promisePool.query('SELECT * FROM accounts WHERE account_sid = ?', payload.account_sid);
if (r.length === 0) throw new DbErrorBadRequest('Invalid request: account_sid does not exist');
const {service_provider_sid} = r[0];
if (service_provider_sid !== user.service_provider_sid) {
throw new DbErrorBadRequest('Invalid request: user may not be moved to another service provider');
}
}
if ((old_password && !new_password) || (new_password && !old_password)) {
throw new DbErrorBadRequest('new_password and old_password both required');
}
@@ -62,23 +93,18 @@ const validateRequest = async(user_sid, payload) => {
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const token = req.user.jwt;
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
let usersList;
try {
let results;
if (decodedJwt.scope === 'admin') {
if (req.user.hasAdminAuth) {
results = await User.retrieveAll();
}
else if (decodedJwt.scope === 'account') {
results = await User.retrieveAllForAccount(decodedJwt.account_sid, true);
else if (req.user.hasAccountAuth) {
results = await User.retrieveAllForAccount(req.user.account_sid, true);
}
else if (decodedJwt.scope === 'service_provider') {
results = await User.retrieveAllForServiceProvider(decodedJwt.service_provider_sid, true);
}
else {
throw new DbErrorBadRequest(`invalid scope: ${decodedJwt.scope}`);
else if (req.user.hasServiceProviderAuth) {
results = await User.retrieveAllForServiceProvider(req.user.service_provider_sid, true);
}
if (results.length === 0) throw new Error('failure retrieving users list');
@@ -222,8 +248,6 @@ router.get('/me', async(req, res) => {
router.get('/:user_sid', async(req, res) => {
const logger = req.app.locals.logger;
const token = req.user.jwt;
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
const {user_sid} = req.params;
try {
@@ -232,9 +256,9 @@ router.get('/:user_sid', async(req, res) => {
const {hashed_password, ...rest} = user;
if (!user) throw new Error('failure retrieving user');
if (decodedJwt.scope === 'admin' ||
decodedJwt.scope === 'account' && decodedJwt.account_sid === user.account_sid ||
decodedJwt.scope === 'service_provider' && decodedJwt.service_provider_sid === user.service_provider_sid) {
if (req.user.hasAdminAuth ||
req.user.hasAccountAuth && req.user.account_sid === user.account_sid ||
req.user.hasServiceProviderAuth && req.user.service_provider_sid === user.service_provider_sid) {
res.status(200).json(rest);
} else {
res.sendStatus(403);
@@ -249,8 +273,7 @@ router.put('/:user_sid', async(req, res) => {
const logger = req.app.locals.logger;
const {user_sid} = req.params;
const user = await User.retrieve(user_sid);
const token = req.user.jwt;
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
const {hasAccountAuth, hasServiceProviderAuth, hasAdminAuth} = req.user;
const {
old_password,
new_password,
@@ -266,15 +289,15 @@ router.put('/:user_sid', async(req, res) => {
//if (req.user.user_sid && req.user.user_sid !== user_sid) return res.sendStatus(403);
if (decodedJwt.scope !== 'admin' &&
!(decodedJwt.scope === 'account' && decodedJwt.account_sid === user[0].account_sid) &&
!(decodedJwt.scope === 'service_provider' && decodedJwt.service_provider_sid === user[0].service_provider_sid) &&
if (!hasAdminAuth &&
!(hasAccountAuth && req.user.account_sid === user[0].account_sid) &&
!(hasServiceProviderAuth && req.user.service_provider_sid === user[0].service_provider_sid) &&
(req.user.user_sid && req.user.user_sid !== user_sid)) {
return res.sendStatus(403);
}
try {
const user = await validateRequest(user_sid, req.body);
const user = await validateRequest(user_sid, req);
if (!user) return res.sendStatus(404);
if (new_password) {
@@ -367,8 +390,6 @@ router.post('/', async(req, res) => {
hashed_password: passwordHash,
};
const allUsers = await User.retrieveAll();
const token = req.user.jwt;
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
delete payload.initial_password;
try {
@@ -385,30 +406,27 @@ router.post('/', async(req, res) => {
return res.status(422).json({msg: 'user with this email already exists'});
}
if (decodedJwt.scope === 'admin') {
if (req.user.hasAdminAuth) {
logger.debug({payload}, 'POST /users');
const uuid = await User.make(payload);
res.status(201).json({user_sid: uuid});
}
else if (decodedJwt.scope === 'account') {
else if (req.user.hasAccountAuth) {
logger.debug({payload}, 'POST /users');
const uuid = await User.make({
...payload,
account_sid: decodedJwt.account_sid,
account_sid: req.user.account_sid,
});
res.status(201).json({user_sid: uuid});
}
else if (decodedJwt.scope === 'service_provider') {
else if (req.user.hasServiceProviderAuth) {
logger.debug({payload}, 'POST /users');
const uuid = await User.make({
...payload,
service_provider_sid: decodedJwt.service_provider_sid,
service_provider_sid: req.user.service_provider_sid,
});
res.status(201).json({user_sid: uuid});
}
else {
throw new DbErrorBadRequest(`invalid scope: ${decodedJwt.scope}`);
}
} catch (err) {
sysError(logger, res, err);
}
@@ -417,24 +435,21 @@ router.post('/', async(req, res) => {
router.delete('/:user_sid', async(req, res) => {
const logger = req.app.locals.logger;
const {user_sid} = req.params;
const token = req.user.jwt;
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
const allUsers = await User.retrieveAll();
const activeAdminUsers = allUsers.filter((e) => !e.account_sid && !e.service_provider_sid && e.is_active);
const user = await User.retrieve(user_sid);
try {
if (decodedJwt.scope === 'admin' && !user.account_sid && !user.service_provider_sid &&
activeAdminUsers.length === 1) {
if (req.user.hasAdminAuth && activeAdminUsers.length === 1) {
throw new Error('cannot delete this admin user - there are no other active admin users');
}
if (decodedJwt.scope === 'admin' ||
(decodedJwt.scope === 'account' && decodedJwt.account_sid === user[0].account_sid) ||
(decodedJwt.scope === 'service_provider' && decodedJwt.service_provider_sid === user[0].service_provider_sid)) {
if (req.user.hasAdminAuth ||
(req.user.hasAccountAuth && req.user.account_sid === user[0].account_sid) ||
(req.user.hasServiceProviderAuth && req.user.service_provider_sid === user[0].service_provider_sid)) {
await User.remove(user_sid);
//logout user after self-delete
if (decodedJwt.user_sid === user_sid) {
if (req.user.user_sid === user_sid) {
request({
url:'http://localhost:3000/v1/logout',
method: 'POST',
@@ -448,12 +463,11 @@ router.delete('/:user_sid', async(req, res) => {
}
return res.sendStatus(204);
} else {
throw new DbErrorBadRequest(`invalid scope: ${decodedJwt.scope}`);
throw new DbErrorBadRequest('invalid request');
}
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

View File

@@ -10,7 +10,34 @@ info:
version: 1.0.0
servers:
- url: /v1
description: development server
description: jambonz API server
tags:
- name: Authentication
description: Authentication operations
- name: Accounts
description: Accounts operations
- name: Users
description: Users operations
- name: Applications
description: Applications operations
- name: Phone Numbers
description: Phone Numbers operations
- name: Api Keys
description: Api Keys operations
- name: Service Providers
description: Service Providers operations
- name: SBCs
description: SBCs operations
- name: Voip Carriers
description: Voip Carriers operations
- name: Sip Gateways
description: Sip Gateways operations
- name: Smpp Gateways
description: Smpp Gateways operations
- name: Webhooks
description: Webhooks operations
- name: Microsoft Teams Tenants
description: Microsoft Teams Tenants operations
paths:
/BetaInviteCodes:
post:
@@ -79,6 +106,8 @@ paths:
type: string
format: uuid
post:
tags:
- Accounts
summary: add a VoiPCarrier to an account based on PredefinedCarrier template
operationId: createVoipCarrierFromTemplate
responses:
@@ -96,6 +125,8 @@ paths:
$ref: '#/components/schemas/GeneralError'
/Sbcs:
post:
tags:
- SBCs
summary: add an SBC address
operationId: createSbc
requestBody:
@@ -134,7 +165,9 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
summary: retrieve public IP addresses of the jambonz Sbcs
tags:
- SBCs
summary: retrieve public IP addresses of the jambonz SBCs
operationId: listSbcs
parameters:
- in: query
@@ -171,7 +204,9 @@ paths:
schema:
type: string
delete:
summary: delete sbc address
tags:
- SBCs
summary: delete SBC address
operationId: deleteSbcAddress
responses:
200:
@@ -240,6 +275,8 @@ paths:
/ApiKeys:
post:
tags:
- Api Keys
summary: create an api key
operationId: createApikey
requestBody:
@@ -277,7 +314,7 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Apikeys/{ApiKeySid}:
/ApiKeys/{ApiKeySid}:
parameters:
- name: ApiKeySid
in: path
@@ -285,6 +322,8 @@ paths:
schema:
type: string
delete:
tags:
- Api Keys
summary: delete api key
operationId: deleteApiKey
responses:
@@ -294,6 +333,8 @@ paths:
description: api key or account not found
/signin:
post:
tags:
- Authentication
summary: sign in using email and password
operationId: loginUser
requestBody:
@@ -335,6 +376,8 @@ paths:
$ref: '#/components/schemas/GeneralError'
/logout:
post:
tags:
- Authentication
summary: log out and deactivate jwt
operationId: logoutUser
responses:
@@ -348,6 +391,8 @@ paths:
$ref: '#/components/schemas/GeneralError'
/forgot-password:
post:
tags:
- Authentication
summary: send link to reset password
operationId: forgotPassword
requestBody:
@@ -377,6 +422,8 @@ paths:
$ref: '#/components/schemas/GeneralError'
/change-password:
post:
tags:
- Authentication
summary: changePassword
operationId: changePassword
requestBody:
@@ -408,6 +455,8 @@ paths:
$ref: '#/components/schemas/GeneralError'
/register:
post:
tags:
- Authentication
summary: create a new user and account
operationId: registerUser
requestBody:
@@ -516,7 +565,9 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
/Users:
get:
get:
tags:
- Users
summary: list all users
operationId: listUsers
responses:
@@ -547,6 +598,8 @@ paths:
schema:
type: string
get:
tags:
- Users
summary: retrieve user information
operationId: getUser
requestBody:
@@ -583,6 +636,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
put:
tags:
- Users
summary: update user information
operationId: updateUser
requestBody:
@@ -603,8 +658,6 @@ paths:
new_password:
type: string
description: new password
name:
type: string
is_active:
type: boolean
force_change:
@@ -629,6 +682,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
post:
tags:
- Users
summary: create a new user
operationId: createUser
requestBody:
@@ -649,8 +704,6 @@ paths:
type: string
permissions:
type: array
force_change:
type: boolean
old_password:
type: string
description: existing password, which is to be replaced
@@ -670,6 +723,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
delete:
tags:
- Users
summary: delete a user
operationId: deleteUser
responses:
@@ -687,6 +742,8 @@ paths:
$ref: '#/components/schemas/GeneralError'
/Users/me:
get:
tags:
- Users
summary: retrieve details about logged-in user and associated account
operationId: getMyDetails
responses:
@@ -852,6 +909,8 @@ paths:
/ActivationCode:
post:
tags:
- Authentication
summary: send an activation code to the user
operationId: sendActivationCode
requestBody:
@@ -897,6 +956,8 @@ paths:
schema:
type: string
put:
tags:
- Authentication
summary: validate an activation code
operationId: validateActivationCode
requestBody:
@@ -976,6 +1037,8 @@ paths:
schema:
type: string
get:
tags:
- Webhooks
summary: retrieve webhook
operationId: getWebhook
responses:
@@ -996,6 +1059,8 @@ paths:
/VoipCarriers:
post:
tags:
- Voip Carriers
summary: create voip carrier
operationId: createVoipCarrier
requestBody:
@@ -1083,6 +1148,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Voip Carriers
summary: list voip carriers
operationId: listVoipCarriers
responses:
@@ -1110,6 +1177,8 @@ paths:
schema:
type: string
delete:
tags:
- Voip Carriers
summary: delete a voip carrier
operationId: deleteVoipCarrier
responses:
@@ -1132,6 +1201,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Voip Carriers
summary: retrieve voip carrier
operationId: getVoipCarrier
responses:
@@ -1150,6 +1221,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
put:
tags:
- Voip Carriers
summary: update voip carrier
operationId: updateVoipCarrier
parameters:
@@ -1189,6 +1262,8 @@ paths:
/SipGateways:
post:
tags:
- Sip Gateways
summary: create sip gateway
operationId: createSipGateway
requestBody:
@@ -1240,6 +1315,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Sip Gateways
summary: list sip gateways
operationId: listSipGateways
parameters:
@@ -1274,6 +1351,8 @@ paths:
schema:
type: string
delete:
tags:
- Sip Gateways
summary: delete a sip gateway
operationId: deleteSipGateway
responses:
@@ -1288,6 +1367,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Sip Gateways
summary: retrieve sip gateway
operationId: getSipGateway
responses:
@@ -1306,6 +1387,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
put:
tags:
- Sip Gateways
summary: update sip gateway
operationId: updateSipGateway
requestBody:
@@ -1332,6 +1415,8 @@ paths:
$ref: '#/components/schemas/GeneralError'
/SmppGateways:
post:
tags:
- Smpp Gateways
summary: create smpp gateway
operationId: createSmppGateway
requestBody:
@@ -1387,6 +1472,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Smpp Gateways
summary: list smpp gateways
operationId: listSmppGateways
responses:
@@ -1414,6 +1501,8 @@ paths:
schema:
type: string
delete:
tags:
- Smpp Gateways
summary: delete a smpp gateway
operationId: deleteSmppGateway
responses:
@@ -1428,6 +1517,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Smpp Gateways
summary: retrieve smpp gateway
operationId: getSmppGateway
responses:
@@ -1446,7 +1537,9 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
put:
summary: update sip gateway
tags:
- Smpp Gateways
summary: update smpp gateway
operationId: updateSmppGateway
requestBody:
content:
@@ -1472,6 +1565,8 @@ paths:
$ref: '#/components/schemas/GeneralError'
/PhoneNumbers:
post:
tags:
- Phone Numbers
summary: provision a phone number into inventory from a Voip Carrier
operationId: provisionPhoneNumber
requestBody:
@@ -1527,6 +1622,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Phone Numbers
summary: list phone numbers
operationId: listProvisionedPhoneNumbers
responses:
@@ -1554,6 +1651,8 @@ paths:
schema:
type: string
delete:
tags:
- Phone Numbers
summary: delete a phone number
operationId: deletePhoneNumber
responses:
@@ -1576,6 +1675,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Phone Numbers
summary: retrieve phone number
operationId: getPhoneNumber
responses:
@@ -1594,6 +1695,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
put:
tags:
- Phone Numbers
summary: update phone number
operationId: updatePhoneNumber
requestBody:
@@ -1625,6 +1728,8 @@ paths:
/ServiceProviders:
post:
tags:
- Service Providers
summary: create service provider
operationId: createServiceProvider
requestBody:
@@ -1667,6 +1772,8 @@ paths:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Service Providers
summary: list service providers
operationId: listServiceProviders
responses:
@@ -1695,6 +1802,8 @@ paths:
schema:
type: string
delete:
tags:
- Service Providers
summary: delete a service provider
operationId: deleteServiceProvider
responses:
@@ -1716,6 +1825,8 @@ paths:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Service Providers
summary: retrieve service provider
operationId: getServiceProvider
responses:
@@ -1733,8 +1844,10 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
put:
tags:
- Service Providers
summary: update service provider
operationId: updateServiceProvider
requestBody:
@@ -1769,6 +1882,8 @@ paths:
type: string
format: uuid
get:
tags:
- Service Providers
summary: get all accounts for a service provider
operationId: getServiceProviderAccounts
responses:
@@ -1794,6 +1909,8 @@ paths:
type: string
format: uuid
get:
tags:
- Service Providers
summary: get all carriers for a service provider
operationId: getServiceProviderCarriers
responses:
@@ -1810,6 +1927,8 @@ paths:
404:
description: service provider not found
post:
tags:
- Service Providers
summary: create a carrier
operationId: createCarrierForServiceProvider
requestBody:
@@ -1841,6 +1960,8 @@ paths:
type: string
format: uuid
post:
tags:
- Service Providers
summary: add a VoiPCarrier to a service provider based on PredefinedCarrier template
operationId: createVoipCarrierFromTemplate
responses:
@@ -1866,6 +1987,8 @@ paths:
type: string
format: uuid
post:
tags:
- Service Providers
summary: create a speech credential for a service provider
operationId: addSpeechCredentialForSeerviceProvider
requestBody:
@@ -1897,6 +2020,8 @@ paths:
type: string
format: uuid
get:
tags:
- Service Providers
summary: get a specific speech credential
operationId: getSpeechCredential
responses:
@@ -1909,6 +2034,8 @@ paths:
404:
description: credential not found
put:
tags:
- Service Providers
summary: update a speech credential
operationId: updateSpeechCredential
requestBody:
@@ -1928,6 +2055,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
delete:
tags:
- Service Providers
summary: delete a speech credential
operationId: deleteSpeechCredential
responses:
@@ -1937,6 +2066,8 @@ paths:
description: credential not found
/ServiceProviders/{ServiceProviderSid}/SpeechCredentials/{SpeechCredentialSid}/test:
get:
tags:
- Service Providers
summary: test a speech credential
operationId: testSpeechCredential
parameters:
@@ -1991,6 +2122,8 @@ paths:
$ref: '#/components/schemas/GeneralError'
/ServiceProviders/{ServiceProviderSid}/Limits:
post:
tags:
- Service Providers
summary: create a limit for a service provider
operationId: addLimitForServiceProvider
parameters:
@@ -2021,6 +2154,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Service Providers
summary: retrieve call capacity and other limits from the service provider
operationId: getServiceProviderLimits
parameters:
@@ -2049,6 +2184,8 @@ paths:
$ref: '#/components/schemas/GeneralError'
/Accounts/{AccountSid}/Limits:
post:
tags:
- Accounts
summary: create a limit for an account
operationId: addLimitForAccount
parameters:
@@ -2079,6 +2216,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Accounts
summary: retrieve call capacity and other limits from the account
operationId: getAccountLimits
parameters:
@@ -2107,6 +2246,8 @@ paths:
$ref: '#/components/schemas/GeneralError'
/MicrosoftTeamsTenants:
post:
tags:
- Microsoft Teams Tenants
summary: provision a customer tenant for MS Teams
operationId: createMsTeamsTenant
requestBody:
@@ -2154,6 +2295,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Microsoft Teams Tenants
summary: list MS Teams tenants
operationId: listMsTeamsTenants
responses:
@@ -2180,6 +2323,8 @@ paths:
type: string
format: uuid
delete:
tags:
- Microsoft Teams Tenants
summary: delete an MS Teams tenant
operationId: deleteTenant
responses:
@@ -2194,6 +2339,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Microsoft Teams Tenants
summary: retrieve an MS Teams tenant
operationId: getTenant
responses:
@@ -2213,7 +2360,9 @@ paths:
$ref: '#/components/schemas/GeneralError'
put:
summary: update tenant
tags:
- Microsoft Teams Tenants
summary: update an MS Teams tenant
operationId: putTenant
requestBody:
content:
@@ -2241,6 +2390,8 @@ paths:
/Accounts:
post:
tags:
- Accounts
summary: create an account
operationId: createAccount
requestBody:
@@ -2408,6 +2559,8 @@ paths:
address should be blacklisted by the platform (0 means forever).
get:
tags:
- Accounts
summary: list accounts
operationId: listAccounts
responses:
@@ -2435,6 +2588,8 @@ paths:
type: string
format: uuid
delete:
tags:
- Accounts
summary: delete an account
operationId: deleteAccount
responses:
@@ -2455,6 +2610,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Accounts
summary: retrieve account
operationId: getAccount
responses:
@@ -2474,6 +2631,8 @@ paths:
$ref: '#/components/schemas/GeneralError'
put:
tags:
- Accounts
summary: update account
operationId: updateAccount
requestBody:
@@ -2512,6 +2671,8 @@ paths:
schema:
type: boolean
get:
tags:
- Accounts
summary: get webhook signing secret, regenerating if requested
operationId: getWebhookSecret
responses:
@@ -2542,6 +2703,8 @@ paths:
type: string
format: uuid
get:
tags:
- Accounts
summary: get all api keys for an account
operationId: getAccountApiKeys
responses:
@@ -2576,6 +2739,8 @@ paths:
schema:
type: string
post:
tags:
- Accounts
summary: add or change the sip realm
operationId: createSipRealm
responses:
@@ -2604,6 +2769,8 @@ paths:
format: uuid
post:
tags:
- Accounts
summary: add a speech credential
operationId: createSpeechCredential
requestBody:
@@ -2631,6 +2798,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Accounts
summary: retrieve all speech credentials for an account
operationId: listSpeechCredentials
responses:
@@ -2659,6 +2828,8 @@ paths:
type: string
format: uuid
get:
tags:
- Accounts
summary: get a specific speech credential
operationId: getSpeechCredential
responses:
@@ -2671,6 +2842,8 @@ paths:
404:
description: credential not found
put:
tags:
- Accounts
summary: update a speech credential
operationId: updateSpeechCredential
requestBody:
@@ -2690,6 +2863,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
delete:
tags:
- Accounts
summary: delete a speech credential
operationId: deleteSpeechCredential
responses:
@@ -2699,6 +2874,8 @@ paths:
description: credential not found
/Accounts/{AccountSid}/SpeechCredentials/{SpeechCredentialSid}/test:
get:
tags:
- Accounts
summary: test a speech credential
operationId: testSpeechCredential
parameters:
@@ -2812,6 +2989,8 @@ paths:
- inbound
- outbound
get:
tags:
- Accounts
summary: retrieve recent calls for an account
operationId: listRecentCalls
responses:
@@ -2908,6 +3087,8 @@ paths:
schema:
type: string
get:
tags:
- Accounts
summary: retrieve sip trace detail for a call
operationId: getRecentCallTrace
responses:
@@ -2933,6 +3114,8 @@ paths:
schema:
type: string
get:
tags:
- Service Providers
summary: retrieve pcap for a call
operationId: getRecentCallTrace
responses:
@@ -3005,6 +3188,8 @@ paths:
- inbound
- outbound
get:
tags:
- Service Providers
summary: retrieve recent calls for an account
operationId: listRecentCalls
responses:
@@ -3104,6 +3289,8 @@ paths:
schema:
type: string
get:
tags:
- Service Providers
summary: retrieve sip trace detail for a call
operationId: getRecentCallTrace
responses:
@@ -3129,6 +3316,8 @@ paths:
schema:
type: string
get:
tags:
- Accounts
summary: retrieve pcap for a call
operationId: getRecentCallTrace
responses:
@@ -3201,6 +3390,8 @@ paths:
- device-limit
- api-limit
get:
tags:
- Service Providers
summary: retrieve alerts for a service provider
operationId: listAlerts
responses:
@@ -3311,6 +3502,8 @@ paths:
- device-limit
- api-limit
get:
tags:
- Accounts
summary: retrieve alerts for an account
operationId: listAlerts
responses:
@@ -3359,6 +3552,8 @@ paths:
description: account not found
/Applications:
post:
tags:
- Applications
summary: create application
operationId: createApplication
requestBody:
@@ -3424,6 +3619,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Applications
summary: list applications
operationId: listApplications
responses:
@@ -3452,6 +3649,8 @@ paths:
schema:
type: string
delete:
tags:
- Applications
summary: delete an application
operationId: deleteApplication
responses:
@@ -3472,6 +3671,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Applications
summary: retrieve an application
responses:
200:
@@ -3489,6 +3690,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
put:
tags:
- Applications
summary: update application
operationId: updateApplication
requestBody:
@@ -3517,6 +3720,8 @@ paths:
/Accounts/{AccountSid}/Calls:
post:
tags:
- Accounts
summary: create a call
operationId: createCall
parameters:
@@ -3578,6 +3783,8 @@ paths:
400:
description: bad request
get:
tags:
- Accounts
summary: list calls
operationId: listCalls
parameters:
@@ -3618,6 +3825,8 @@ paths:
schema:
type: string
delete:
tags:
- Accounts
summary: delete a call
operationId: deleteCall
responses:
@@ -3638,6 +3847,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- Accounts
summary: retrieve a call
operationId: getCall
responses:
@@ -3656,6 +3867,8 @@ paths:
schema:
$ref: '#/components/schemas/GeneralError'
post:
tags:
- Accounts
summary: update a call
operationId: updateCall
requestBody:
@@ -3741,6 +3954,8 @@ paths:
$ref: '#/components/schemas/GeneralError'
/Accounts/{AccountSid}/Messages:
post:
tags:
- Accounts
summary: create an outgoing SMS message
operationId: createMessage
parameters:

View File

@@ -1,112 +0,0 @@
const {DbErrorBadRequest} = require('../utils/errors');
const assert = require('assert');
const _specData = require('./specs');
const specs = new Map();
for (const key in _specData) { specs.set(key, _specData[key]); }
function normalizeJambones(logger, obj) {
if (!Array.isArray(obj)) throw new DbErrorBadRequest('malformed jambonz payload: must be array');
const document = [];
for (const tdata of obj) {
if (typeof tdata !== 'object') throw new DbErrorBadRequest('malformed jambonz payload: must be array of objects');
if ('verb' in tdata) {
// {verb: 'say', text: 'foo..bar'..}
const name = tdata.verb;
const o = {};
Object.keys(tdata)
.filter((k) => k !== 'verb')
.forEach((k) => o[k] = tdata[k]);
const o2 = {};
o2[name] = o;
document.push(o2);
}
else if (Object.keys(tdata).length === 1) {
// {'say': {..}}
document.push(tdata);
}
else {
logger.info(tdata, 'malformed jambonz payload: missing verb property');
throw new DbErrorBadRequest('malformed jambonz payload: missing verb property');
}
}
logger.debug({ document }, `normalizeJambones: returning document with ${document.length} tasks`);
return document;
}
function validate(logger, obj) {
normalizeJambones(logger, obj).map((tdata) => {
const keys = Object.keys(tdata);
const name = keys[0];
const data = tdata[name];
validateVerb(name, data, logger);
});
}
function validateVerb(name, data, logger) {
logger.debug(`validating ${name} with data ${JSON.stringify(data)}`);
// validate the instruction is supported
if (!specs.has(name)) throw new DbErrorBadRequest(`invalid instruction: ${name}`);
// check type of each element and make sure required elements are present
const specData = specs.get(name);
let required = specData.required || [];
for (const dKey in data) {
if (dKey in specData.properties) {
const dVal = data[dKey];
const dSpec = specData.properties[dKey];
logger.debug(`Task:validate validating property ${dKey} with value ${JSON.stringify(dVal)}`);
if (typeof dSpec === 'string' && dSpec === 'array') {
if (!Array.isArray(dVal)) throw new DbErrorBadRequest(`${name}: property ${dKey} is not an array`);
}
else if (typeof dSpec === 'string' && dSpec.includes('|')) {
const types = dSpec.split('|').map((t) => t.trim());
if (!types.includes(typeof dVal) && !(types.includes('array') && Array.isArray(dVal))) {
throw new DbErrorBadRequest(`${name}: property ${dKey} has invalid data type, must be one of ${types}`);
}
}
else if (typeof dSpec === 'string' && ['number', 'string', 'object', 'boolean'].includes(dSpec)) {
// simple types
if (typeof dVal !== specData.properties[dKey]) {
throw new DbErrorBadRequest(`${name}: property ${dKey} has invalid data type`);
}
}
else if (Array.isArray(dSpec) && dSpec[0].startsWith('#')) {
const name = dSpec[0].slice(1);
for (const item of dVal) {
validate(name, item);
}
}
else if (typeof dSpec === 'object') {
// complex types
const type = dSpec.type;
assert.ok(['number', 'string', 'object', 'boolean'].includes(type),
`invalid or missing type in spec ${JSON.stringify(dSpec)}`);
if (type === 'string' && dSpec.enum) {
assert.ok(Array.isArray(dSpec.enum), `enum must be an array ${JSON.stringify(dSpec.enum)}`);
if (!dSpec.enum.includes(dVal)) {
throw new DbErrorBadRequest(`invalid value ${dVal} must be one of ${dSpec.enum}`);
}
}
}
else if (typeof dSpec === 'string' && dSpec.startsWith('#')) {
// reference to another datatype (i.e. nested type)
const name = dSpec.slice(1);
//const obj = {};
//obj[name] = dVal;
validate(name, dVal);
}
else {
assert.ok(0, `invalid spec ${JSON.stringify(dSpec)}`);
}
required = required.filter((item) => item !== dKey);
}
else if (dKey === '_') {
/* no op: allow arbitrary info to be carried here, used by conference e.g in transfer */
}
else throw new DbErrorBadRequest(`${name}: unknown property ${dKey}`);
}
if (required.length > 0) throw new DbErrorBadRequest(`${name}: missing value for ${required}`);
}
module.exports = validate;

View File

@@ -1,18 +1,19 @@
const crypto = require('crypto');
const { argon2i } = require('argon2-ffi');
const argon2 = require('argon2');
const util = require('util');
const { argon2i } = argon2;
const getRandomBytes = util.promisify(crypto.randomBytes);
const generateHashedPassword = async(password) => {
const salt = await getRandomBytes(32);
const passwordHash = await argon2i.hash(password, salt);
const passwordHash = await argon2.hash(password, { type: argon2i, salt });
return passwordHash;
};
const verifyPassword = async(passwordHash, password) => {
const isCorrect = await argon2i.verify(passwordHash, password);
return isCorrect;
const verifyPassword = (passwordHash, password) => {
return argon2.verify(passwordHash, password);
};
const hashString = (s) => crypto.createHash('md5').update(s).digest('hex');

View File

@@ -1,759 +0,0 @@
{
"sip:decline": {
"properties": {
"id": "string",
"status": "number",
"reason": "string",
"headers": "object"
},
"required": [
"status"
]
},
"sip:request": {
"properties": {
"id": "string",
"method": "string",
"body": "string",
"headers": "object",
"actionHook": "object|string"
},
"required": [
"method"
]
},
"sip:refer": {
"properties": {
"id": "string",
"referTo": "string",
"referredBy": "string",
"headers": "object",
"actionHook": "object|string",
"eventHook": "object|string"
},
"required": [
"referTo"
]
},
"config": {
"properties": {
"id": "string",
"synthesizer": "#synthesizer",
"recognizer": "#recognizer",
"bargeIn": "#bargeIn",
"record": "#recordOptions",
"amd": "#amd",
"notifyEvents": "boolean"
},
"required": []
},
"bargeIn": {
"properties": {
"enable": "boolean",
"sticky": "boolean",
"actionHook": "object|string",
"input": "array",
"finishOnKey": "string",
"numDigits": "number",
"minDigits": "number",
"maxDigits": "number",
"interDigitTimeout": "number",
"dtmfBargein": "boolean",
"minBargeinWordCount": "number"
},
"required": [
"enable"
]
},
"dequeue": {
"properties": {
"id": "string",
"name": "string",
"actionHook": "object|string",
"timeout": "number",
"beep": "boolean"
},
"required": [
"name"
]
},
"enqueue": {
"properties": {
"id": "string",
"name": "string",
"actionHook": "object|string",
"waitHook": "object|string",
"_": "object"
},
"required": [
"name"
]
},
"leave": {
"properties": {
"id": "string"
}
},
"hangup": {
"properties": {
"id": "string",
"headers": "object"
},
"required": [
]
},
"play": {
"properties": {
"id": "string",
"url": "string|array",
"loop": "number|string",
"earlyMedia": "boolean",
"seekOffset": "number|string",
"timeoutSecs": "number|string",
"actionHook": "object|string"
},
"required": [
"url"
]
},
"say": {
"properties": {
"id": "string",
"text": "string|array",
"loop": "number|string",
"synthesizer": "#synthesizer",
"earlyMedia": "boolean",
"disableTtsCache": "boolean"
},
"required": [
"text"
]
},
"gather": {
"properties": {
"id": "string",
"actionHook": "object|string",
"finishOnKey": "string",
"input": "array",
"numDigits": "number",
"minDigits": "number",
"maxDigits": "number",
"interDigitTimeout": "number",
"partialResultHook": "object|string",
"speechTimeout": "number",
"listenDuringPrompt": "boolean",
"dtmfBargein": "boolean",
"bargein": "boolean",
"minBargeinWordCount": "number",
"timeout": "number",
"recognizer": "#recognizer",
"play": "#play",
"say": "#say"
},
"required": [
]
},
"conference": {
"properties": {
"id": "string",
"name": "string",
"beep": "boolean",
"startConferenceOnEnter": "boolean",
"endConferenceOnExit": "boolean",
"maxParticipants": "number",
"joinMuted": "boolean",
"actionHook": "object|string",
"waitHook": "object|string",
"statusEvents": "array",
"statusHook": "object|string",
"enterHook": "object|string",
"record": "#record"
},
"required": [
"name"
]
},
"dial": {
"properties": {
"id": "string",
"actionHook": "object|string",
"answerOnBridge": "boolean",
"callerId": "string",
"confirmHook": "object|string",
"referHook": "object|string",
"dialMusic": "string",
"dtmfCapture": "object",
"dtmfHook": "object|string",
"headers": "object",
"listen": "#listen",
"target": ["#target"],
"timeLimit": "number",
"timeout": "number",
"proxy": "string",
"transcribe": "#transcribe",
"amd": "#amd"
},
"required": [
"target"
]
},
"dialogflow": {
"properties": {
"id": "string",
"credentials": "object|string",
"project": "string",
"environment": "string",
"region": {
"type": "string",
"enum": ["europe-west1", "europe-west2", "australia-southeast1", "asia-northeast1"]
},
"lang": "string",
"actionHook": "object|string",
"eventHook": "object|string",
"events": "[string]",
"welcomeEvent": "string",
"welcomeEventParams": "object",
"noInputTimeout": "number",
"noInputEvent": "string",
"passDtmfAsTextInput": "boolean",
"thinkingMusic": "string",
"tts": "#synthesizer",
"bargein": "boolean"
},
"required": [
"project",
"credentials",
"lang"
]
},
"dtmf": {
"properties": {
"id": "string",
"dtmf": "string",
"duration": "number"
},
"required": [
"dtmf"
]
},
"lex": {
"properties": {
"id": "string",
"botId": "string",
"botAlias": "string",
"credentials": "object",
"region": "string",
"locale": "string",
"intent": "#lexIntent",
"welcomeMessage": "string",
"metadata": "object",
"bargein": "boolean",
"passDtmf": "boolean",
"actionHook": "object|string",
"eventHook": "object|string",
"noInputTimeout": "number",
"tts": "#synthesizer"
},
"required": [
"botId",
"botAlias",
"region",
"credentials"
]
},
"listen": {
"properties": {
"id": "string",
"actionHook": "object|string",
"auth": "#auth",
"finishOnKey": "string",
"maxLength": "number",
"metadata": "object",
"mixType": {
"type": "string",
"enum": ["mono", "stereo", "mixed"]
},
"passDtmf": "boolean",
"playBeep": "boolean",
"sampleRate": "number",
"timeout": "number",
"transcribe": "#transcribe",
"url": "string",
"wsAuth": "#auth",
"earlyMedia": "boolean"
},
"required": [
"url"
]
},
"message": {
"properties": {
"id": "string",
"carrier": "string",
"account_sid": "string",
"message_sid": "string",
"to": "string",
"from": "string",
"text": "string",
"media": "string|array",
"actionHook": "object|string"
},
"required": [
"to",
"from"
]
},
"pause": {
"properties": {
"id": "string",
"length": "number"
},
"required": [
"length"
]
},
"rasa": {
"properties": {
"id": "string",
"url": "string",
"recognizer": "#recognizer",
"tts": "#synthesizer",
"prompt": "string",
"actionHook": "object|string",
"eventHook": "object|string"
},
"required": [
"url"
]
},
"record": {
"properties": {
"path": "string"
},
"required": [
"path"
]
},
"recordOptions": {
"properties": {
"action": {
"type": "string",
"enum": ["startCallRecording", "stopCallRecording", "pauseCallRecording", "resumeCallRecording"]
},
"recordingID": "string",
"siprecServerURL": "string"
},
"required": [
"action"
]
},
"redirect": {
"properties": {
"id": "string",
"actionHook": "object|string"
},
"required": [
"actionHook"
]
},
"rest:dial": {
"properties": {
"id": "string",
"account_sid": "string",
"application_sid": "string",
"call_hook": "object|string",
"call_status_hook": "object|string",
"from": "string",
"fromHost": "string",
"speech_synthesis_vendor": "string",
"speech_synthesis_voice": "string",
"speech_synthesis_language": "string",
"speech_recognizer_vendor": "string",
"speech_recognizer_language": "string",
"tag": "object",
"to": "#target",
"headers": "object",
"timeout": "number"
},
"required": [
"call_hook",
"from",
"to"
]
},
"tag": {
"properties": {
"id": "string",
"data": "object"
},
"required": [
"data"
]
},
"transcribe": {
"properties": {
"id": "string",
"transcriptionHook": "string",
"recognizer": "#recognizer",
"earlyMedia": "boolean"
},
"required": [
"recognizer"
]
},
"target": {
"properties": {
"type": {
"type": "string",
"enum": ["phone", "sip", "user", "teams"]
},
"confirmHook": "object|string",
"method": {
"type": "string",
"enum": ["GET", "POST"]
},
"headers": "object",
"from": "#dialFrom",
"name": "string",
"number": "string",
"sipUri": "string",
"auth": "#auth",
"vmail": "boolean",
"tenant": "string",
"trunk": "string",
"overrideTo": "string"
},
"required": [
"type"
]
},
"dialFrom": {
"properties": {
"user": "string",
"host": "string"
},
"required": [
]
},
"auth": {
"properties": {
"username": "string",
"password": "string"
},
"required": [
"username",
"password"
]
},
"synthesizer": {
"properties": {
"vendor": {
"type": "string",
"enum": ["google", "aws", "polly", "microsoft", "nuance", "ibm", "default"]
},
"language": "string",
"voice": "string",
"engine": {
"type": "string",
"enum": ["standard", "neural"]
},
"gender": {
"type": "string",
"enum": ["MALE", "FEMALE", "NEUTRAL"]
}
},
"required": [
"vendor"
]
},
"recognizer": {
"properties": {
"vendor": {
"type": "string",
"enum": ["google", "aws", "microsoft", "nuance", "deepgram", "ibm", "default"]
},
"language": "string",
"vad": "#vad",
"hints": "array",
"hintsBoost": "number",
"altLanguages": "array",
"profanityFilter": "boolean",
"interim": "boolean",
"singleUtterance": "boolean",
"dualChannel": "boolean",
"separateRecognitionPerChannel": "boolean",
"punctuation": "boolean",
"enhancedModel": "boolean",
"words": "boolean",
"diarization": "boolean",
"diarizationMinSpeakers": "number",
"diarizationMaxSpeakers": "number",
"interactionType": {
"type": "string",
"enum": [
"unspecified",
"discussion",
"presentation",
"phone_call",
"voicemail",
"voice_search",
"voice_command",
"dictation"
]
},
"naicsCode": "number",
"identifyChannels": "boolean",
"vocabularyName": "string",
"vocabularyFilterName": "string",
"filterMethod": {
"type": "string",
"enum": [
"remove",
"mask",
"tag"
]
},
"model": "string",
"outputFormat": {
"type": "string",
"enum": [
"simple",
"detailed"
]
},
"profanityOption": {
"type": "string",
"enum": [
"masked",
"removed",
"raw"
]
},
"requestSnr": "boolean",
"initialSpeechTimeoutMs": "number",
"azureServiceEndpoint": "string",
"azureSttEndpointId": "string",
"asrDtmfTerminationDigit": "string",
"asrTimeout": "number",
"nuanceOptions": "#nuanceOptions",
"deepgramOptions": "#deepgramOptions",
"ibmOptions": "#ibmOptions"
},
"required": [
"vendor"
]
},
"ibmOptions": {
"properties": {
"sttApiKey": "string",
"sttRegion": "string",
"ttsApiKey": "string",
"ttsRegion": "string",
"instanceId": "string",
"model": "string",
"languageCustomizationId": "string",
"acousticCustomizationId": "string",
"baseModelVersion": "string",
"watsonMetadata": "string",
"watsonLearningOptOut": "boolean"
},
"required": [
]
},
"deepgramOptions": {
"properties": {
"apiKey": "string",
"tier": {
"type": "string",
"enum": [
"enhanced",
"base"
]
},
"model": {
"type": "string",
"enum": [
"general",
"meeting",
"phonecall",
"voicemail",
"finance",
"conversationalai",
"video",
"custom"
]
},
"customModel": "string",
"version": "string",
"punctuate": "boolean",
"profanityFilter": "boolean",
"redact": {
"type": "string",
"enum": [
"pci",
"numbers",
"true",
"ssn"
]
},
"diarize": "boolean",
"diarizeVersion": "string",
"ner": "boolean",
"multichannel": "boolean",
"alternatives": "number",
"numerals": "boolean",
"search": "array",
"replace": "array",
"keywords": "array",
"endpointing": "boolean",
"vadTurnoff": "number",
"tag": "string"
}
},
"nuanceOptions": {
"properties": {
"clientId": "string",
"secret": "string",
"kryptonEndpoint": "string",
"topic": "string",
"utteranceDetectionMode": {
"type": "string",
"enum": [
"single",
"multiple",
"disabled"
]
},
"punctuation": "boolean",
"profanityFilter": "boolean",
"includeTokenization": "boolean",
"discardSpeakerAdaptation": "boolean",
"suppressCallRecording": "boolean",
"maskLoadFailures": "boolean",
"suppressInitialCapitalization": "boolean",
"allowZeroBaseLmWeight": "boolean",
"filterWakeupWord": "boolean",
"resultType": {
"type": "string",
"enum": [
"final",
"partial",
"immutable_partial"
]
},
"noInputTimeoutMs": "number",
"recognitionTimeoutMs": "number",
"utteranceEndSilenceMs": "number",
"maxHypotheses": "number",
"speechDomain": "string",
"formatting": "#formatting",
"clientData": "object",
"userId": "string",
"speechDetectionSensitivity": "number",
"resources": ["#resource"]
},
"required": [
]
},
"resource": {
"properties": {
"externalReference": "#resourceReference",
"inlineWordset": "string",
"builtin": "string",
"inlineGrammar": "string",
"wakeupWord": "[string]",
"weightName": {
"type": "string",
"enum": [
"defaultWeight",
"lowest",
"low",
"medium",
"high",
"highest"
]
},
"weightValue": "number",
"reuse": {
"type": "string",
"enum": [
"undefined_reuse",
"low_reuse",
"high_reuse"
]
}
},
"required": [
]
},
"resourceReference": {
"properties": {
"type": {
"type": "string",
"enum": [
"undefined_resource_type",
"wordset",
"compiled_wordset",
"domain_lm",
"speaker_profile",
"grammar",
"settings"
]
},
"uri": "string",
"maxLoadFailures": "boolean",
"requestTimeoutMs": "number",
"headers": "object"
},
"required": [
]
},
"formatting": {
"properties": {
"scheme": "string",
"options": "object"
},
"required": [
"scheme",
"options"
]
},
"lexIntent": {
"properties": {
"name": "string",
"slots": "object"
},
"required": [
"name"
]
},
"vad": {
"properties": {
"enable": "boolean",
"voiceMs": "number",
"mode": "number"
},
"required": [
"enable"
]
},
"amd": {
"properties": {
"actionHook": "object|string",
"thresholdWordCount": "number",
"timers": "#amdTimers",
"recognizer": "#recognizer"
},
"required": [
"actionHook"
]
},
"amdTimers": {
"properties": {
"noSpeechTimeoutMs": "number",
"decisionTimeoutMs": "number",
"toneTimeoutMs": "number",
"greetingCompletionTimeoutMs": "number"
}
}
}

View File

@@ -1,12 +1,30 @@
const ttsGoogle = require('@google-cloud/text-to-speech');
const sttGoogle = require('@google-cloud/speech').v1p1beta1;
const Polly = require('aws-sdk/clients/polly');
const AWS = require('aws-sdk');
const { PollyClient, DescribeVoicesCommand } = require('@aws-sdk/client-polly');
const { TranscribeClient, ListVocabulariesCommand } = require('@aws-sdk/client-transcribe');
const { Deepgram } = require('@deepgram/sdk');
const sdk = require('microsoft-cognitiveservices-speech-sdk');
const { SpeechClient } = require('@soniox/soniox-node');
const bent = require('bent');
const fs = require('fs');
const testSonioxStt = async(logger, credentials) => {
const api_key = credentials;
const soniox = new SpeechClient(api_key);
return new Promise(async(resolve, reject) => {
try {
const result = await soniox.transcribeFileShort('data/test_audio.wav');
if (result.words.length > 0) resolve(result);
else reject(new Error('no transcript returned'));
} catch (error) {
logger.info({error}, 'failed to get soniox transcript');
reject(error);
}
});
};
const testNuanceTts = async(logger, getTtsVoices, credentials) => {
const voices = await getTtsVoices({vendor: 'nuance', credentials});
return voices;
@@ -102,25 +120,28 @@ const testMicrosoftStt = async(logger, credentials) => {
});
};
const testAwsTts = (logger, credentials) => {
const polly = new Polly(credentials);
return new Promise((resolve, reject) => {
polly.describeVoices({LanguageCode: 'en-US'}, (err, data) => {
if (err) return reject(err);
resolve();
});
});
const testAwsTts = async(logger, credentials) => {
try {
const client = new PollyClient(credentials);
const command = new DescribeVoicesCommand({LanguageCode: 'en-US'});
const response = await client.send(command);
return response;
} catch (err) {
logger.info({err}, 'testMicrosoftTts - failed to list voices for region ${region}');
throw err;
}
};
const testAwsStt = (logger, credentials) => {
const transcribeservice = new AWS.TranscribeService(credentials);
return new Promise((resolve, reject) => {
transcribeservice.listVocabularies((err, data) => {
if (err) return reject(err);
logger.info({data}, 'retrieved language models');
resolve();
});
});
const testAwsStt = async(logger, credentials) => {
try {
const client = new TranscribeClient(credentials);
const command = new ListVocabulariesCommand({});
const response = await client.send(command);
return response;
} catch (err) {
logger.info({err}, 'testMicrosoftTts - failed to list voices for region ${region}');
throw err;
}
};
const testMicrosoftTts = async(logger, credentials) => {
@@ -180,7 +201,7 @@ const testWellSaidTts = async(logger, credentials) => {
const testIbmTts = async(logger, getTtsVoices, credentials) => {
const {tts_api_key, tts_region} = credentials;
const voices = await getTtsVoices({vendor: 'ibm', credentials: {api_key: tts_api_key, region: tts_region}});
const voices = await getTtsVoices({vendor: 'ibm', credentials: {tts_api_key, tts_region}});
return voices;
};
@@ -225,5 +246,6 @@ module.exports = {
testNuanceStt,
testDeepgramStt,
testIbmTts,
testIbmStt
testIbmStt,
testSonioxStt
};

3622
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "jambonz-api-server",
"version": "v0.8.0",
"version": "v0.8.1",
"description": "",
"main": "app.js",
"scripts": {
@@ -18,21 +18,24 @@
"url": "https://github.com/jambonz/jambonz-api-server.git"
},
"dependencies": {
"@aws-sdk/client-polly": "^3.290.0",
"@aws-sdk/client-transcribe": "^3.290.0",
"@deepgram/sdk": "^1.10.2",
"@google-cloud/speech": "^5.1.0",
"@google-cloud/text-to-speech": "^4.0.3",
"@jambonz/db-helpers": "^0.7.3",
"@jambonz/realtimedb-helpers": "^0.6.0",
"@jambonz/realtimedb-helpers": "^0.7.0",
"@jambonz/speech-utils": "^0.0.6",
"@jambonz/time-series": "^0.2.5",
"argon2-ffi": "^2.0.0",
"aws-sdk": "^2.1152.0",
"@jambonz/verb-specifications": "^0.0.3",
"@soniox/soniox-node": "^1.1.0",
"argon2": "^0.30.3",
"bent": "^7.3.12",
"cors": "^2.8.5",
"debug": "^4.3.4",
"express": "^4.18.1",
"express-rate-limit": "^6.4.0",
"form-data": "^2.5.1",
"form-urlencoded": "^6.1.0",
"helmet": "^5.1.0",
"ibm-watson": "^7.1.2",
"jsonwebtoken": "^9.0.0",

View File

@@ -171,6 +171,35 @@ test('speech credentials tests', async(t) => {
t.ok(result.statusCode === 200 && result.body.stt.status === 'ok', 'successfully tested speech credential for microsoft stt');
}
/* add / test a credential for AWS */
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY && process.env.AWS_REGION) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'aws',
use_for_tts: true,
use_for_stt: true,
access_key_id: process.env.AWS_ACCESS_KEY_ID,
secret_access_key: process.env.AWS_SECRET_ACCESS_KEY,
aws_region: process.env.AWS_REGION
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for AWS');
const ms_sid = result.body.sid;
/* test the speech credential */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}/test`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
});
//console.log(JSON.stringify(result));
t.ok(result.statusCode === 200 && result.body.tts.status === 'ok', 'successfully tested speech credential for AWS tts');
t.ok(result.statusCode === 200 && result.body.stt.status === 'ok', 'successfully tested speech credential for AWS stt');
}
/* add a credential for wellsaid */
if (process.env.WELLSAID_API_KEY) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
@@ -300,6 +329,38 @@ test('speech credentials tests', async(t) => {
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
}
/* add a credential for Siniox */
if (process.env.SONIOX_API_KEY) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'soniox',
use_for_stt: true,
api_key: process.env.SONIOX_API_KEY
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for soniox');
const ms_sid = result.body.sid;
/* test the speech credential */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}/test`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
});
console.log(JSON.stringify(result));
t.ok(result.statusCode === 200 && result.body.stt.status === 'ok', 'successfully tested speech credential for soniox');
/* delete the credential */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}`, {
auth: authUser,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
}
/* add a credential for nvidia */
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,