├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── HOW-DO-I.yml │ ├── NEW-ISSUE.yml │ └── config.yml └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .husky └── pre-commit ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── __mocks__ ├── @vonage │ └── server-sdk.js ├── fs.js ├── readline.js ├── winston.js └── yargs.js ├── __tests__ ├── __dataSets__ │ ├── apps │ │ ├── index.js │ │ ├── messageCapabilities.js │ │ ├── networkCapabilities.js │ │ ├── rtcCapabilities.js │ │ ├── verifyCapabilities.js │ │ ├── videoCapabilities.js │ │ └── voiceCapabilities.js │ └── ux.js ├── app.js ├── commands │ ├── apps │ │ ├── apps.create.test.js │ │ ├── apps.delete.test.js │ │ ├── apps.list.test.js │ │ ├── apps.show.test.js │ │ ├── apps.update.test.js │ │ ├── apps.validate.test.js │ │ ├── capabilities │ │ │ ├── apps.capabilities.remove.test.js │ │ │ └── apps.capabilities.update.test.js │ │ └── numbers │ │ │ ├── apps.numbers.link.test.js │ │ │ ├── apps.numbers.list.test.js │ │ │ └── apps.numbers.unlink.test.js │ ├── auth │ │ ├── auth.check.test.js │ │ ├── auth.set.test.js │ │ └── auth.show.test.js │ ├── balance.test.js │ ├── conversations │ │ ├── conversations.create.test.js │ │ ├── conversations.delete.test.js │ │ ├── conversations.list.test.js │ │ ├── conversations.show.test.js │ │ └── conversations.update.test.js │ ├── jwt │ │ ├── jwt.create.test.js │ │ └── jwt.validate.test.js │ ├── members │ │ ├── members.create.test.js │ │ ├── members.list.test.js │ │ ├── members.show.test.js │ │ └── members.update.test.js │ ├── numbers │ │ ├── numbers.buy.test.js │ │ ├── numbers.cancel.test.js │ │ ├── numbers.list.test.js │ │ ├── numbers.search.test.js │ │ └── numbers.update.test.js │ └── users │ │ ├── users.create.test.js │ │ ├── users.delete.test.js │ │ ├── users.list.test.js │ │ ├── users.show.test.js │ │ └── users.update.test.js ├── common.js ├── conversations.js ├── helpers.js ├── members.js ├── middleware │ ├── config.test.js │ └── log.test.js ├── numbers.js ├── test.private.key ├── users.js ├── utils │ ├── aclDiff.test.js │ ├── coerceNumber.test.js │ ├── coercePrivateKey.test.js │ ├── coerceURL.test.js │ ├── fs.test.js │ └── validateSDKAuth.test.js └── ux │ ├── confirm.test.js │ └── dump.test.js ├── bin └── vonage.js ├── data └── countries.json ├── eslint.config.js ├── jest.config.js ├── package-lock.json ├── package.json └── src ├── aclSchema.json ├── apps ├── capabilities.js ├── display.js ├── message.js ├── network.js ├── rtc.js ├── verify.js ├── video.js └── voice.js ├── commands ├── apps.js ├── apps │ ├── capabilities.js │ ├── capabilities │ │ ├── remove.js │ │ └── update.js │ ├── create.js │ ├── delete.js │ ├── list.js │ ├── numbers.js │ ├── numbers │ │ ├── link.js │ │ ├── list.js │ │ └── unlink.js │ ├── show.js │ ├── update.js │ └── validate.js ├── auth.js ├── auth │ ├── check.js │ ├── set.js │ └── show.js ├── balance.js ├── conversations.js ├── conversations │ ├── create.js │ ├── delete.js │ ├── list.js │ ├── show.js │ └── update.js ├── jwt.js ├── jwt │ ├── create.js │ └── validate.js ├── members.js ├── members │ ├── create.js │ ├── list.js │ ├── show.js │ └── update.js ├── numbers.js ├── numbers │ ├── buy.js │ ├── cancel.js │ ├── list.js │ ├── search.js │ └── update.js ├── users.js └── users │ ├── create.js │ ├── delete.js │ ├── list.js │ ├── show.js │ └── update.js ├── commonFlags.js ├── conversations ├── conversationFlags.js └── display.js ├── credentialFlags.js ├── errors └── invalidKey.js ├── members └── display.js ├── middleware ├── config.js ├── log.js └── update.js ├── numbers ├── display.js ├── features.js ├── loadOwnedNumbersFromSDK.js └── types.js ├── users └── display.js ├── utils ├── aclDiff.js ├── coerceJSON.js ├── coerceKey.js ├── coerceNumber.js ├── coerceRemove.js ├── coerceUrl.js ├── countries.js ├── fs.js ├── makeSDKCall.js ├── sdkError.js ├── settings.js └── validateSDKAuth.js └── ux ├── __mocks__ └── confirm.js ├── confirm.js ├── currency.js ├── cursor.js ├── date.js ├── descriptionList.js ├── dump.js ├── dumpAcl.js ├── dumpAuth.js ├── dumpYesNo.js ├── indentLines.js ├── lineBreak.js ├── progress.js ├── redact.js ├── spinner.js └── table.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | max_line_length = 80 8 | indent_style = space 9 | indent_size = 2 10 | charset = utf-8 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior for line endings 2 | * text=auto 3 | * text eol=lf 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/HOW-DO-I.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: How do I? 3 | description: Ask a question about a command or how to do a task with the CLI 4 | title: "How Do I: " 5 | labels: ["question"] 6 | assignees: ["manchuck", "pardel", "dragonmantank"] 7 | body: 8 | - type: textarea 9 | id: question 10 | attributes: 11 | label: How do I 12 | description: Ask the question about a command or how to do something with the CLI 13 | placeholder: configure the CLI 14 | validations: 15 | required: true 16 | - type: input 17 | id: cli_version 18 | attributes: 19 | label: ClI Version 20 | description: Run `vonage --version` 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/NEW-ISSUE.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | description: File a bug report 4 | title: "[Bug]: " 5 | labels: ["bug", "Triage"] 6 | assignees: ["manchuck", "pardel", "dragonmantank"] 7 | body: 8 | - type: dropdown 9 | id: node_version 10 | attributes: 11 | label: Node Version 12 | description: What version of NodeJS are you using? 13 | options: 14 | - 18.x 15 | - 20.x 16 | - 22.x 17 | validations: 18 | required: true 19 | - type: dropdown 20 | id: platform 21 | attributes: 22 | label: Platform 23 | description: What is the platform you are having the issue on? 24 | multiple: true 25 | options: 26 | - "Windows" 27 | - "Linux" 28 | - "Mac (intel)" 29 | - "Mac (Apple Silcon)" 30 | - "Docker Container" 31 | validations: 32 | required: true 33 | - type: input 34 | id: cli_version 35 | attributes: 36 | label: CLI Version 37 | description: What version of the CLI are you using (run `voange --version`)? 38 | placeholder: 3.x.x 39 | validations: 40 | required: true 41 | - type: textarea 42 | id: command_example 43 | attributes: 44 | label: Command 45 | description: Please provide the command you are trying to run 46 | validations: 47 | required: true 48 | - type: textarea 49 | id: expected_behavior 50 | attributes: 51 | label: Expected Behavior 52 | description: Please provide a brief description of what you wanted to 53 | happen 54 | validations: 55 | required: true 56 | - type: textarea 57 | id: actual_behavior 58 | attributes: 59 | label: Actual Behavior 60 | description: Please provide a brief description of what happened 61 | validations: 62 | required: true 63 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | blank_issues_enabled: false 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Vonage 3 | 4 | on: 5 | push: 6 | branches: 7 | - 3.x 8 | pull_request: 9 | 10 | jobs: 11 | static: 12 | name: Static Code Analysis 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Set up Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 18.x 21 | 22 | - name: Install packages 23 | run: npm install 24 | 25 | - name: Lint 26 | run: npm run lint 27 | 28 | test: 29 | runs-on: ${{ matrix.os }} 30 | name: Test and build 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | os: [ubuntu-latest, windows-latest] 36 | node: [18.x, 20.x, 22.x] 37 | steps: 38 | - uses: actions/checkout@v3 39 | 40 | - name: Set up Node.js ${{ matrix.node }} 41 | uses: actions/setup-node@v3 42 | with: 43 | node-version: ${{ matrix.node }} 44 | 45 | - name: Install packages 46 | run: npm install 47 | 48 | - name: Test 49 | env: 50 | JEST_JUNIT_CLASSNAME: "{filepath}" 51 | run: npm run test -- --coverage --reporters=jest-junit --reporters=default 52 | 53 | - name: Run codecov 54 | uses: codecov/codecov-action@v3 55 | with: 56 | directory: ./coverage 57 | 58 | - name: Upload test results to Codecov 59 | if: ${{ !cancelled() }} 60 | uses: codecov/test-results-action@v1 61 | with: 62 | token: ${{ secrets.CODECOV_TOKEN }} 63 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | env: 9 | GH_TOKEN: ${{ secrets.GH_TOKEN_COMMIT }} 10 | 11 | jobs: 12 | publish-to-npm: 13 | runs-on: ubuntu-latest 14 | name: Update Release 15 | 16 | steps: 17 | - name: Checkout 18 | id: checkout 19 | uses: actions/checkout@v4 20 | with: 21 | ref: '3.x' 22 | fetch-depth: 0 23 | 24 | - name: Setup node 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: 18.x 28 | 29 | - name: Publish to NPM 30 | run: npm publish 31 | env: 32 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | 34 | notify-release: 35 | runs-on: ubuntu-latest 36 | name: Notify Release 37 | strategy: 38 | matrix: 39 | url: [SLACK_WEBHOOK_ASK_DEVREL_URL, SLACK_WEBHOOK_DEVREL_TOOLING_URL, SLACK_WEBHOOK_DEVREL_PRIVATE_URL] 40 | steps: 41 | - name: Send to slack channles 42 | uses: slackapi/slack-github-action@v2.0.0 43 | with: 44 | webhook: ${{ secrets[matrix.url]}} 45 | webhook-type: incoming-webhook 46 | errors: true 47 | payload: | 48 | blocks: 49 | - type: "header" 50 | text: 51 | type: "plain_text" 52 | text: ":initial_external_notification_sent: Version ${{ github.event.release.name }} of the NodeJS SDK has been released" 53 | - type: "section" 54 | text: 55 | type: "mrkdwn" 56 | text: "${{ github.event.release.body }}" 57 | emoji: true 58 | - type: "divider" 59 | - type: "section" 60 | text: 61 | type: "mrkdwn" 62 | text: "You can view the full change log <${{github.event.release.html_url }}|here>" 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | examples/.env 3 | examples/private.key 4 | npm-debug.log 5 | coverage 6 | private.key 7 | .nyc 8 | .nyc_output 9 | .env 10 | .nexmo-app 11 | .vonagerc 12 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | # Lint all the staged files 2 | echo "Linting Staged files" 3 | npx lint-staged 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Getting Involved 2 | 3 | Thanks for your interest in the project, we'd love to have you involved! Check out the sections below to find out more about what to do next... 4 | 5 | ## Opening an Issue 6 | 7 | We always welcome issues, if you've seen something that isn't quite right or you have a suggestion for a new feature, please go ahead and open an issue in this project. Include as much information as you have, it really helps. 8 | 9 | ## Making a Code Change 10 | 11 | We're always open to pull requests, but these should be small and clearly described so that we can understand what you're trying to do. Feel free to open an issue first and get some discussion going. 12 | 13 | When you're ready to start coding, fork this repository to your own GitHub account and make your changes in a new branch. Once you're happy (see below for information on how to run the tests), open a pull request and explain what the change is and why you think we should include it in our project. 14 | 15 | ## Coding standards 16 | 17 | Please make sure your changes match the existing standards. 18 | 19 | ## Running the tests 20 | 21 | Run the tests with the following command: 22 | 23 | ```sh 24 | npm run test 25 | ``` 26 | -------------------------------------------------------------------------------- /__mocks__/@vonage/server-sdk.js: -------------------------------------------------------------------------------- 1 | const mockGetApplication = jest.fn(); 2 | const mockGetApplicationPage = jest.fn(); 3 | const mockListAllApplications = jest.fn(); 4 | 5 | afterEach(() => { 6 | jest.clearAllMocks(); 7 | }); 8 | 9 | const mockedVonage = jest.fn().mockImplementation(() => { 10 | return { 11 | applications: { 12 | getApplication: mockGetApplication, 13 | getApplicationPage: mockGetApplicationPage, 14 | listAllApplications: mockListAllApplications, 15 | }, 16 | }; 17 | }); 18 | 19 | mockedVonage._mockGetApplication = mockGetApplication; 20 | mockedVonage._mockGetApplicationPage = mockGetApplicationPage; 21 | mockedVonage._mockListAllApplications = mockListAllApplications; 22 | 23 | exports.Vonage = mockedVonage; 24 | -------------------------------------------------------------------------------- /__mocks__/fs.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | let mockFiles = {}; 4 | 5 | afterEach(() => { 6 | jest.clearAllMocks(); 7 | mockFiles = {}; 8 | }); 9 | 10 | const fs = jest.createMockFromModule('fs'); 11 | 12 | const __addPath = (filePath) => { 13 | if (!mockFiles[filePath]) { 14 | mockFiles[filePath] = {}; 15 | } 16 | }; 17 | 18 | const __addFile = (filePath, data) => { 19 | const pathName = path.dirname(filePath); 20 | const fileName = path.basename(filePath); 21 | 22 | __addPath(pathName); 23 | 24 | mockFiles[pathName][fileName] = data; 25 | }; 26 | 27 | const mkdir = jest.fn((directoryPath) => { 28 | __addPath(directoryPath); 29 | }); 30 | 31 | const mkdirSync = jest.fn((directoryPath) => { 32 | __addPath(directoryPath); 33 | }); 34 | 35 | const readdirSync = jest.fn((directoryPath) => mockFiles[directoryPath] || []); 36 | 37 | const existsSync = jest.fn((filePath) => { 38 | if (mockFiles[filePath]) { 39 | return true; 40 | } 41 | 42 | const pathName = path.dirname(filePath); 43 | const fileName = path.basename(filePath); 44 | 45 | if (!mockFiles[pathName]) { 46 | return false; 47 | } 48 | 49 | return !!mockFiles[pathName][fileName]; 50 | }); 51 | 52 | const readFileSync = (filePath) => { 53 | const pathName = path.dirname(filePath); 54 | const fileName = path.basename(filePath); 55 | 56 | if (!mockFiles[pathName] || !mockFiles[pathName][fileName]) { 57 | throw new Error(`ENOENT: no such file or directory, open '${filePath}'`); 58 | } 59 | 60 | return mockFiles[pathName][fileName]; 61 | }; 62 | 63 | fs.__addFile = __addFile; 64 | fs.__addPath = __addPath; 65 | fs.readFileSync = readFileSync; 66 | fs.readdirSync = readdirSync; 67 | fs.existsSync = existsSync; 68 | fs.writeFileSync = jest.fn(); 69 | fs.mkdir = mkdir; 70 | fs.mkdirSync = mkdirSync; 71 | 72 | module.exports = fs; 73 | -------------------------------------------------------------------------------- /__mocks__/readline.js: -------------------------------------------------------------------------------- 1 | afterEach(() => { 2 | jest.clearAllMocks(); 3 | }); 4 | 5 | const readline = jest.createMockFromModule('readline'); 6 | const questionMock = jest.fn(); 7 | 8 | const closeMock = jest.fn().mockImplementation(() => undefined); 9 | 10 | const createInterface = jest.fn().mockReturnValue({ 11 | question: questionMock, 12 | close: closeMock, 13 | }); 14 | 15 | readline.createInterface = createInterface; 16 | 17 | readline.__questionMock = questionMock; 18 | readline.__closeMock = closeMock; 19 | readline.__createInterfaceMock = createInterface; 20 | 21 | module.exports = readline; 22 | -------------------------------------------------------------------------------- /__mocks__/winston.js: -------------------------------------------------------------------------------- 1 | const winston = jest.createMockFromModule('winston'); 2 | 3 | const mockLogger = { 4 | info: jest.fn(), 5 | warn: jest.fn(), 6 | error: jest.fn(), 7 | debug: jest.fn(), 8 | }; 9 | 10 | winston.createLogger = jest.fn(() => mockLogger); 11 | 12 | winston.format = { 13 | combine: jest.fn(), 14 | colorize: jest.fn(), 15 | padLevels: jest.fn(), 16 | simple: jest.fn(), 17 | timestamp: jest.fn(), 18 | }; 19 | 20 | winston.transports = { 21 | Console: jest.fn(), 22 | }; 23 | 24 | winston.__mockLogger = mockLogger; 25 | 26 | module.exports = winston; 27 | -------------------------------------------------------------------------------- /__mocks__/yargs.js: -------------------------------------------------------------------------------- 1 | 2 | const yargs = jest.createMockFromModule('yargs'); 3 | 4 | yargs.exit = jest.fn(); 5 | 6 | module.exports = yargs; 7 | -------------------------------------------------------------------------------- /__tests__/__dataSets__/apps/index.js: -------------------------------------------------------------------------------- 1 | const { messageDataSets } = require('./messageCapabilities'); 2 | const { networkDataSets } = require('./networkCapabilities'); 3 | const { rtcDataSets } = require('./rtcCapabilities'); 4 | const { verifyDataSets } = require('./verifyCapabilities'); 5 | const { videoDataSets } = require('./videoCapabilities'); 6 | const { voiceDataSets } = require('./voiceCapabilities'); 7 | 8 | exports.dataSets = [ 9 | { 10 | label: 'message', 11 | testCases: messageDataSets, 12 | }, 13 | { 14 | label: 'network', 15 | testCases: networkDataSets, 16 | }, 17 | { 18 | label: 'rtc', 19 | testCases: rtcDataSets, 20 | }, 21 | { 22 | label: 'verify', 23 | testCases: verifyDataSets, 24 | }, 25 | { 26 | label: 'video', 27 | testCases: videoDataSets, 28 | }, 29 | { 30 | label: 'voice', 31 | testCases: voiceDataSets, 32 | }, 33 | ]; 34 | -------------------------------------------------------------------------------- /__tests__/__dataSets__/apps/messageCapabilities.js: -------------------------------------------------------------------------------- 1 | process.env.FORCE_COLOR = 0; 2 | const { faker } = require('@faker-js/faker'); 3 | const { getBasicApplication, addMessagesCapabilities } = require('../../app'); 4 | const { Client } = require('@vonage/server-client'); 5 | 6 | exports.messageDataSets = [ 7 | (() => { 8 | const app = Client.transformers.camelCaseObjectKeys( 9 | getBasicApplication(), 10 | true, 11 | ); 12 | 13 | const inboundUrl = faker.internet.url(); 14 | const statusUrl = faker.internet.url(); 15 | const version = faker.helpers.shuffle(['v1', 'v0.1'])[0]; 16 | const authenticateInboundMedia = faker.datatype.boolean(); 17 | 18 | return { 19 | label: 'update Message capabilities', 20 | app: app, 21 | args: { 22 | action: 'update', 23 | which: 'messages', 24 | messagesInboundUrl: inboundUrl, 25 | messagesStatusUrl: statusUrl, 26 | messagesVersion: version, 27 | messagesAuthenticateMedia: authenticateInboundMedia, 28 | }, 29 | expected: { 30 | ...app, 31 | name: `${app.name}`, 32 | capabilities: { 33 | messages: { 34 | version: version, 35 | authenticateInboundMedia: authenticateInboundMedia, 36 | webhooks: { 37 | inboundUrl: { 38 | address: inboundUrl, 39 | httpMethod: 'POST', 40 | }, 41 | statusUrl: { 42 | address: statusUrl, 43 | httpMethod: 'POST', 44 | }, 45 | }, 46 | }, 47 | }, 48 | }, 49 | }; 50 | })(), 51 | 52 | 53 | (() => { 54 | const app = Client.transformers.camelCaseObjectKeys( 55 | addMessagesCapabilities( 56 | getBasicApplication(), 57 | ), 58 | true, 59 | ); 60 | 61 | return { 62 | label: 'remov urls and remove methods', 63 | app: app, 64 | args: { 65 | action: 'update', 66 | which: 'messages', 67 | messagesInboundUrl: '__REMOVE__', 68 | messagesStatusUrl: '__REMOVE__', 69 | messagesAuthenticateMedia: app.capabilities.messages.authenticateInboundMedia, 70 | }, 71 | expected: { 72 | ...app, 73 | name: `${app.name}`, 74 | // this will remove the capability since you cannot have version and 75 | // authenticateInboundMedia without the webhooks 76 | capabilities: { 77 | }, 78 | }, 79 | }; 80 | })(), 81 | 82 | (() => { 83 | const app = Client.transformers.camelCaseObjectKeys( 84 | addMessagesCapabilities( 85 | getBasicApplication(), 86 | ), 87 | true, 88 | ); 89 | 90 | return { 91 | label: 'remove Message', 92 | app: app, 93 | args: { 94 | action: 'rm', 95 | which: 'messages', 96 | }, 97 | expected: { 98 | ...app, 99 | name: `${app.name}`, 100 | capabilities: { 101 | messages: undefined, 102 | }, 103 | }, 104 | }; 105 | })(), 106 | ]; 107 | -------------------------------------------------------------------------------- /__tests__/__dataSets__/apps/networkCapabilities.js: -------------------------------------------------------------------------------- 1 | process.env.FORCE_COLOR = 0; 2 | const { faker } = require('@faker-js/faker'); 3 | const { getBasicApplication, addNetworkCapabilities } = require('../../app'); 4 | const { Client } = require('@vonage/server-client'); 5 | 6 | exports.networkDataSets = [ 7 | (() => { 8 | const app = Client.transformers.camelCaseObjectKeys( 9 | getBasicApplication(), 10 | true, 11 | ); 12 | 13 | const redirectUrl = faker.internet.url(); 14 | const appId = faker.string.uuid(); 15 | 16 | return { 17 | label: 'update network redirect url and network app id', 18 | app: app, 19 | args: { 20 | action: 'update', 21 | which: 'network_apis', 22 | networkRedirectUrl: redirectUrl, 23 | networkAppId: appId, 24 | }, 25 | expected: { 26 | ...app, 27 | name: `${app.name}`, 28 | capabilities: { 29 | networkApis: { 30 | networkApplicationId: appId, 31 | redirectUrl: redirectUrl, 32 | }, 33 | }, 34 | }, 35 | }; 36 | })(), 37 | 38 | (() => { 39 | const app = Client.transformers.camelCaseObjectKeys( 40 | addNetworkCapabilities(getBasicApplication()), 41 | true, 42 | ); 43 | 44 | const redirectUrl = faker.internet.url(); 45 | const appId = faker.string.uuid(); 46 | 47 | return { 48 | label: 'replace network redirect url', 49 | app: app, 50 | args: { 51 | action: 'update', 52 | which: 'network_apis', 53 | networkRedirectUrl: redirectUrl, 54 | networkAppId: appId, 55 | }, 56 | expected: { 57 | ...app, 58 | name: `${app.name}`, 59 | capabilities: { 60 | networkApis: { 61 | networkApplicationId: appId, 62 | redirectUrl: redirectUrl, 63 | }, 64 | }, 65 | }, 66 | }; 67 | })(), 68 | 69 | (() => { 70 | const app = Client.transformers.camelCaseObjectKeys( 71 | addNetworkCapabilities(getBasicApplication()), 72 | true, 73 | ); 74 | 75 | return { 76 | label: 'remove network api', 77 | app: app, 78 | args: { 79 | action: 'rm', 80 | which: 'network_apis', 81 | }, 82 | expected: { 83 | ...app, 84 | name: `${app.name}`, 85 | capabilities: {}, 86 | }, 87 | }; 88 | })(), 89 | ]; 90 | -------------------------------------------------------------------------------- /__tests__/__dataSets__/apps/verifyCapabilities.js: -------------------------------------------------------------------------------- 1 | process.env.FORCE_COLOR = 0; 2 | const { faker } = require('@faker-js/faker'); 3 | const { getBasicApplication, addVerifyCapabilities } = require('../../app'); 4 | const { Client } = require('@vonage/server-client'); 5 | 6 | exports.verifyDataSets = [ 7 | (() => { 8 | const app = Client.transformers.camelCaseObjectKeys( 9 | getBasicApplication(), 10 | true, 11 | ); 12 | 13 | const statusUrl = faker.internet.url(); 14 | 15 | return { 16 | label: 'update Verify capabilities', 17 | app: app, 18 | args: { 19 | action: 'update', 20 | which: 'verify', 21 | verifyStatusUrl: statusUrl, 22 | }, 23 | expected: { 24 | ...app, 25 | name: `${app.name}`, 26 | capabilities: { 27 | verify: { 28 | version: 'v2', 29 | webhooks: { 30 | statusUrl: { 31 | address: statusUrl, 32 | httpMethod: 'POST', 33 | }, 34 | }, 35 | }, 36 | }, 37 | }, 38 | }; 39 | })(), 40 | 41 | (() => { 42 | const app = Client.transformers.camelCaseObjectKeys( 43 | addVerifyCapabilities( 44 | getBasicApplication(), 45 | ), 46 | true, 47 | ); 48 | 49 | const statusUrl = faker.internet.url(); 50 | 51 | return { 52 | label: 'replace Verify capabilities', 53 | app: app, 54 | args: { 55 | action: 'update', 56 | which: 'verify', 57 | verifyStatusUrl: statusUrl, 58 | }, 59 | expected: { 60 | ...app, 61 | name: `${app.name}`, 62 | capabilities: { 63 | verify: { 64 | version: 'v2', 65 | webhooks: { 66 | statusUrl: { 67 | address: statusUrl, 68 | httpMethod: 'POST', 69 | }, 70 | }, 71 | }, 72 | }, 73 | }, 74 | }; 75 | })(), 76 | 77 | (() => { 78 | const app = Client.transformers.camelCaseObjectKeys( 79 | addVerifyCapabilities( 80 | getBasicApplication(), 81 | ), 82 | true, 83 | ); 84 | 85 | 86 | return { 87 | label: 'remove verify when removing status url', 88 | app: app, 89 | args: { 90 | action: 'update', 91 | which: 'verify', 92 | verifyStatusUrl: '__REMOVE__', 93 | }, 94 | expected: { 95 | ...app, 96 | name: `${app.name}`, 97 | capabilities: {}, 98 | }, 99 | }; 100 | })(), 101 | 102 | (() => { 103 | const app = Client.transformers.camelCaseObjectKeys( 104 | addVerifyCapabilities( 105 | getBasicApplication(), 106 | ), 107 | true, 108 | ); 109 | 110 | return { 111 | label: 'remove Verify', 112 | app: app, 113 | args: { 114 | action: 'rm', 115 | which: 'verify', 116 | }, 117 | expected: { 118 | ...app, 119 | name: `${app.name}`, 120 | capabilities: { 121 | verify: undefined, 122 | }, 123 | }, 124 | }; 125 | })(), 126 | ]; 127 | -------------------------------------------------------------------------------- /__tests__/__dataSets__/ux.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | 3 | module.exports = [ 4 | { 5 | label: 'dump string', 6 | value: 'Vonage CLI', 7 | expected: chalk.blue('Vonage CLI'), 8 | }, 9 | { 10 | label: 'dump number', 11 | value: 42, 12 | expected: chalk.dim('42'), 13 | }, 14 | { 15 | label: 'dump value', 16 | value: null, 17 | expected: chalk.dim.yellow('Not Set'), 18 | }, 19 | { 20 | label: 'dump array', 21 | value: ['fizz', 'buzz', 42, null, undefined], 22 | expected: [ 23 | `${chalk.yellow('[')}`, 24 | ` ${chalk.blue('fizz')}`, 25 | ` ${chalk.blue('buzz')}`, 26 | ` ${chalk.dim('42')}`, 27 | ` ${chalk.dim.yellow('Not Set')}`, 28 | ` ${chalk.dim.yellow('Not Set')}`, 29 | `${chalk.yellow(']')}`, 30 | ].join('\n'), 31 | }, 32 | { 33 | label: 'dump object', 34 | value: { 35 | fizz: 'buzz', 36 | foo: 'bar', 37 | }, 38 | expected: [ 39 | `${chalk.yellow('{')}`, 40 | ` ${chalk.bold('fizz')}: ${chalk.blue('buzz')}`, 41 | ` ${chalk.bold('foo')}: ${chalk.blue('bar')}`, 42 | `${chalk.yellow('}')}`, 43 | ].join('\n'), 44 | }, 45 | { 46 | label: 'dump complex array', 47 | value: [ 48 | 'fizz', 49 | 'buzz', 50 | 42, 51 | null, 52 | undefined, 53 | ['baz', 'bat'], 54 | { 55 | foo: 'bar', 56 | }, 57 | ], 58 | expected: [ 59 | `${chalk.yellow('[')}`, 60 | ` ${chalk.blue('fizz')}`, 61 | ` ${chalk.blue('buzz')}`, 62 | ` ${chalk.dim('42')}`, 63 | ` ${chalk.dim.yellow('Not Set')}`, 64 | ` ${chalk.dim.yellow('Not Set')}`, 65 | ` ${chalk.yellow('[')}`, 66 | ` ${chalk.blue('baz')}`, 67 | ` ${chalk.blue('bat')}`, 68 | ` ${chalk.yellow(']')}`, 69 | ` ${chalk.yellow('{')}`, 70 | ` ${chalk.bold('foo')}: ${chalk.blue('bar')}`, 71 | ` ${chalk.yellow('}')}`, 72 | `${chalk.yellow(']')}`, 73 | ].join('\n'), 74 | }, 75 | { 76 | label: 'dump complex object', 77 | value: { 78 | fizz: 'buzz', 79 | foo: ['bar', 42, null, undefined], 80 | baz: { 81 | quz: 42, 82 | foo: 'bar', 83 | fizz: { 84 | buzz: null, 85 | }, 86 | }, 87 | }, 88 | expected: [ 89 | `${chalk.yellow('{')}`, 90 | ` ${chalk.bold('fizz')}: ${chalk.blue('buzz')}`, 91 | ` ${chalk.bold('foo')}: ${chalk.yellow('[')}`, 92 | ` ${chalk.blue('bar')}`, 93 | ` ${chalk.dim('42')}`, 94 | ` ${chalk.dim.yellow('Not Set')}`, 95 | ` ${chalk.dim.yellow('Not Set')}`, 96 | ` ${chalk.yellow(']')}`, 97 | ` ${chalk.bold('baz')}: ${chalk.yellow('{')}`, 98 | ` ${chalk.bold('quz')}: ${chalk.dim('42')}`, 99 | ` ${chalk.bold('foo')}: ${chalk.blue('bar')}`, 100 | ` ${chalk.bold('fizz')}: ${chalk.yellow('{')}`, 101 | ` ${chalk.bold('buzz')}: ${chalk.dim.yellow('Not Set')}`, 102 | ` ${chalk.yellow('}')}`, 103 | ` ${chalk.yellow('}')}`, 104 | `${chalk.yellow('}')}`, 105 | ].join('\n'), 106 | }, 107 | ]; 108 | -------------------------------------------------------------------------------- /__tests__/commands/apps/apps.delete.test.js: -------------------------------------------------------------------------------- 1 | const { handler } = require('../../../src/commands/apps/delete'); 2 | const { confirm } = require('../../../src/ux/confirm'); 3 | const { faker } = require('@faker-js/faker'); 4 | const { getBasicApplication } = require('../../app'); 5 | const { mockConsole } = require('../../helpers'); 6 | const { Client } = require('@vonage/server-client'); 7 | const { sdkError } = require('../../../src/utils/sdkError'); 8 | 9 | jest.mock('../../../src/utils/sdkError'); 10 | jest.mock('../../../src/ux/confirm'); 11 | 12 | describe('Command: vonage apps delete', () => { 13 | beforeEach(() => { 14 | mockConsole(); 15 | }); 16 | 17 | test('Should delete app', async () => { 18 | const app = Client.transformers.camelCaseObjectKeys( 19 | getBasicApplication(), 20 | true, 21 | true, 22 | ); 23 | const appMock = jest.fn().mockResolvedValue(app); 24 | const deleteMock = jest.fn().mockResolvedValue(undefined); 25 | const sdkMock = { 26 | applications: { 27 | getApplication: appMock, 28 | deleteApplication: deleteMock, 29 | }, 30 | }; 31 | 32 | confirm.mockResolvedValue(true); 33 | const appId = faker.string.uuid(); 34 | await handler({ 35 | id: appId, 36 | SDK: sdkMock, 37 | }); 38 | 39 | expect(deleteMock).toHaveBeenCalledWith(appId); 40 | }); 41 | 42 | test('Should not delete app when user declines', async () => { 43 | const app = Client.transformers.camelCaseObjectKeys( 44 | getBasicApplication(), 45 | true, 46 | true, 47 | ); 48 | const appMock = jest.fn().mockResolvedValue(app); 49 | const deleteMock = jest.fn().mockResolvedValue(undefined); 50 | const sdkMock = { 51 | applications: { 52 | getApplication: appMock, 53 | deleteApplication: deleteMock, 54 | }, 55 | }; 56 | 57 | confirm.mockResolvedValue(false); 58 | const appId = faker.string.uuid(); 59 | await handler({ 60 | id: appId, 61 | SDK: sdkMock, 62 | }); 63 | 64 | expect(deleteMock).not.toHaveBeenCalled(); 65 | }); 66 | 67 | test('Should handle error from delete', async () => { 68 | const app = Client.transformers.camelCaseObjectKeys( 69 | getBasicApplication(), 70 | true, 71 | true, 72 | ); 73 | const testError = new Error('Test error'); 74 | const appMock = jest.fn().mockResolvedValue(app); 75 | const deleteMock = jest.fn().mockRejectedValue(testError); 76 | const sdkMock = { 77 | applications: { 78 | getApplication: appMock, 79 | deleteApplication: deleteMock, 80 | }, 81 | }; 82 | 83 | confirm.mockResolvedValue(true); 84 | const appId = faker.string.uuid(); 85 | await handler({ 86 | id: appId, 87 | SDK: sdkMock, 88 | }); 89 | 90 | expect(deleteMock).toHaveBeenCalled(); 91 | expect(sdkError).toHaveBeenCalledWith(testError); 92 | }); 93 | }); 94 | 95 | -------------------------------------------------------------------------------- /__tests__/commands/apps/capabilities/apps.capabilities.remove.test.js: -------------------------------------------------------------------------------- 1 | process.env.FORCE_COLOR = 0; 2 | const yargs = require('yargs'); 3 | const { handler } = require('../../../../src/commands/apps/capabilities/remove'); 4 | const { mockConsole } = require('../../../helpers'); 5 | const { dataSets } = require('../../../__dataSets__/apps/index'); 6 | const { getBasicApplication } = require('../../../app'); 7 | 8 | const { confirm } = require('../../../../src/ux/confirm'); 9 | 10 | jest.mock('../../../../src/ux/confirm'); 11 | 12 | describe.each(dataSets)('Command: vonage apps capabilities rm $label', ({label, testCases}) => { 13 | beforeEach(() => { 14 | mockConsole(); 15 | }); 16 | 17 | const removeTestCases = testCases.filter(({args}) => args.action === 'rm'); 18 | 19 | test.each(removeTestCases)('Will $label', async ({app, args, expected}) => { 20 | const getAppMock = jest.fn().mockResolvedValue({...app}); 21 | const updateAppMock = jest.fn().mockResolvedValue(); 22 | const sdkMock = { 23 | applications: { 24 | getApplication: getAppMock, 25 | updateApplication: updateAppMock, 26 | }, 27 | }; 28 | 29 | confirm.mockResolvedValue(true); 30 | 31 | await handler({ 32 | SDK: sdkMock, 33 | id: app.id, 34 | ...args, 35 | }); 36 | 37 | expect(yargs.exit).not.toHaveBeenCalled(); 38 | expect(getAppMock).toHaveBeenCalledWith(app.id); 39 | expect(updateAppMock).toHaveBeenCalledWith(expected); 40 | }); 41 | 42 | test.each(removeTestCases)('Will not $label when user declines', async ({app, args}) => { 43 | const getAppMock = jest.fn().mockResolvedValue({...app}); 44 | const updateAppMock = jest.fn().mockResolvedValue(); 45 | const sdkMock = { 46 | applications: { 47 | getApplication: getAppMock, 48 | updateApplication: updateAppMock, 49 | }, 50 | }; 51 | 52 | confirm.mockResolvedValue(false); 53 | 54 | await handler({ 55 | SDK: sdkMock, 56 | id: app.id, 57 | ...args, 58 | }); 59 | 60 | expect(yargs.exit).not.toHaveBeenCalled(); 61 | expect(getAppMock).toHaveBeenCalledWith(app.id); 62 | expect(updateAppMock).not.toHaveBeenCalled(); 63 | }); 64 | 65 | test('Will not call when there are no capabilities', async () => { 66 | const app = getBasicApplication(); 67 | const getAppMock = jest.fn().mockResolvedValue(app); 68 | const updateAppMock = jest.fn().mockResolvedValue(); 69 | const sdkMock = { 70 | applications: { 71 | getApplication: getAppMock, 72 | updateApplication: updateAppMock, 73 | }, 74 | }; 75 | 76 | confirm.mockResolvedValue(false); 77 | 78 | expect(yargs.exit).not.toHaveBeenCalled(); 79 | await handler({ 80 | SDK: sdkMock, 81 | id: app.id, 82 | which: label, 83 | }); 84 | 85 | expect(getAppMock).toHaveBeenCalledWith(app.id); 86 | expect(updateAppMock).not.toHaveBeenCalled(); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /__tests__/commands/apps/capabilities/apps.capabilities.update.test.js: -------------------------------------------------------------------------------- 1 | process.env.FORCE_COLOR = 0; 2 | const { faker } = require('@faker-js/faker'); 3 | const yargs = require('yargs'); 4 | const { handler } = require('../../../../src/commands/apps/capabilities/update'); 5 | const { mockConsole } = require('../../../helpers'); 6 | const { getBasicApplication } = require('../../../app'); 7 | const { Client } = require('@vonage/server-client'); 8 | const { dataSets } = require('../../../__dataSets__/apps/index'); 9 | 10 | describe.each(dataSets)('Command: vonage apps capabilities $label', ({testCases}) => { 11 | beforeEach(() => { 12 | mockConsole(); 13 | }); 14 | 15 | const udpateTestCases = testCases.filter(({args}) => args.action === 'update'); 16 | 17 | test.each(udpateTestCases)('Will $label', async ({app, args, expected}) => { 18 | const getAppMock = jest.fn().mockResolvedValue({...app}); 19 | const updateAppMock = jest.fn().mockResolvedValue(); 20 | const sdkMock = { 21 | applications: { 22 | getApplication: getAppMock, 23 | updateApplication: updateAppMock, 24 | }, 25 | }; 26 | 27 | await handler({ 28 | SDK: sdkMock, 29 | id: app.id, 30 | ...args, 31 | }); 32 | 33 | expect(yargs.exit).not.toHaveBeenCalled(); 34 | expect(getAppMock).toHaveBeenCalledWith(app.id); 35 | expect(updateAppMock).toHaveBeenCalledWith(expected); 36 | }); 37 | }); 38 | 39 | describe('Command: vonage apps capabilities', () => { 40 | beforeEach(() => { 41 | mockConsole(); 42 | }); 43 | 44 | test('Should exit 1 if invalid flag is passed', async () => { 45 | const app = Client.transformers.camelCaseObjectKeys( 46 | getBasicApplication(), 47 | true, 48 | true, 49 | ); 50 | 51 | const getAppMock = jest.fn().mockResolvedValue({...app}); 52 | const updateAppMock = jest.fn().mockResolvedValue(); 53 | const sdkMock = { 54 | applications: { 55 | getApplication: getAppMock, 56 | updateApplication: updateAppMock, 57 | }, 58 | }; 59 | 60 | await handler({ 61 | id: app.id, 62 | SDK: sdkMock, 63 | action: 'add', 64 | which: 'rtc', 65 | networkRedirectUrl: faker.internet.url(), 66 | messagesStatusUrl: faker.internet.url(), 67 | }); 68 | 69 | expect(updateAppMock).not.toHaveBeenCalled(); 70 | expect(yargs.exit).toHaveBeenCalledWith(1); 71 | expect(console.error).toHaveBeenCalledWith('You cannot use the flag(s) [messages-status-url, network-redirect-url] when updating the rtc capability'); 72 | }); 73 | 74 | test('Should exit 1 if one flag is missing', async () => { 75 | const app = Client.transformers.camelCaseObjectKeys( 76 | getBasicApplication(), 77 | true, 78 | true, 79 | ); 80 | 81 | const getAppMock = jest.fn().mockResolvedValue({...app}); 82 | const updateAppMock = jest.fn().mockResolvedValue(); 83 | const sdkMock = { 84 | applications: { 85 | getApplication: getAppMock, 86 | updateApplication: updateAppMock, 87 | }, 88 | }; 89 | 90 | await handler({ 91 | id: app.id, 92 | SDK: sdkMock, 93 | action: 'add', 94 | which: 'rtc', 95 | }); 96 | 97 | expect(updateAppMock).not.toHaveBeenCalled(); 98 | expect(yargs.exit).toHaveBeenCalledWith(1); 99 | expect(console.error).toHaveBeenCalledWith('You must provide at least one rtc-* flag when updating the rtc capability'); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /__tests__/commands/balance.test.js: -------------------------------------------------------------------------------- 1 | process.env.FORCE_COLOR = 0; 2 | const { mockConsole } = require('../helpers'); 3 | const YAML = require('yaml'); 4 | const { faker } = require('@faker-js/faker'); 5 | const { handler } = require('../../src/commands/balance'); 6 | const { dumpYesNo } = require('../../src/ux/dumpYesNo'); 7 | const { displayCurrency } = require('../../src/ux/currency'); 8 | const { Client } = require('@vonage/server-client'); 9 | 10 | describe('Command: vonage balance', () => { 11 | beforeEach(() => { 12 | mockConsole(); 13 | }); 14 | 15 | test('Should output balance', async () => { 16 | const balance = { 17 | value: faker.finance.amount(), 18 | autoReload: faker.datatype.boolean(), 19 | }; 20 | 21 | const balanceMock = jest.fn().mockResolvedValue(balance); 22 | 23 | const sdkMock = { 24 | accounts: { 25 | getBalance: balanceMock, 26 | }, 27 | }; 28 | 29 | await handler({SDK: sdkMock}); 30 | 31 | expect(balanceMock).toHaveBeenCalled(); 32 | expect(console.log).toHaveBeenNthCalledWith( 33 | 2, 34 | [ 35 | `Account balance: ${displayCurrency(balance.value)}`, 36 | `Auto-refill enabled: ${dumpYesNo(balance.autoReload)}`, 37 | ].join('\n'), 38 | ); 39 | }); 40 | 41 | test('Should output JSON', async () => { 42 | const balance = { 43 | value: faker.finance.amount(), 44 | autoReload: faker.datatype.boolean(), 45 | }; 46 | 47 | const balanceMock = jest.fn().mockResolvedValue(balance); 48 | 49 | const sdkMock = { 50 | accounts: { 51 | getBalance: balanceMock, 52 | }, 53 | }; 54 | 55 | await handler({SDK: sdkMock, json: true}); 56 | 57 | expect(balanceMock).toHaveBeenCalled(); 58 | expect(console.log).toHaveBeenNthCalledWith( 59 | 1, 60 | JSON.stringify( 61 | Client.transformers.snakeCaseObjectKeys(balance, true, false), 62 | null, 63 | 2, 64 | ), 65 | ); 66 | }); 67 | 68 | test('Should output YAML', async () => { 69 | const balance = { 70 | value: faker.finance.amount(), 71 | autoReload: faker.datatype.boolean(), 72 | }; 73 | 74 | const balanceMock = jest.fn().mockResolvedValue(balance); 75 | 76 | const sdkMock = { 77 | accounts: { 78 | getBalance: balanceMock, 79 | }, 80 | }; 81 | 82 | await handler({SDK: sdkMock, yaml: true}); 83 | 84 | expect(balanceMock).toHaveBeenCalled(); 85 | expect(console.log).toHaveBeenNthCalledWith( 86 | 1, 87 | YAML.stringify( 88 | Client.transformers.snakeCaseObjectKeys(balance, true, false), 89 | null, 90 | 2, 91 | ), 92 | ); 93 | }); 94 | 95 | }); 96 | -------------------------------------------------------------------------------- /__tests__/commands/conversations/conversations.delete.test.js: -------------------------------------------------------------------------------- 1 | process.env.FORCE_COLOR = 0; 2 | const { confirm } = require('../../../src/ux/confirm'); 3 | const { handler } = require('../../../src/commands/conversations/delete'); 4 | const { mockConsole } = require('../../helpers'); 5 | const { getTestConversationForAPI } = require('../../conversations'); 6 | 7 | jest.mock('../../../src/ux/confirm'); 8 | 9 | describe('Command: vonage conversations delete', () => { 10 | beforeEach(() => { 11 | mockConsole(); 12 | }); 13 | 14 | test('Will delete a conversation', async () => { 15 | confirm.mockResolvedValueOnce(true); 16 | const conversation = getTestConversationForAPI(); 17 | 18 | const conversationMock = jest.fn() 19 | .mockResolvedValueOnce(conversation); 20 | 21 | const deleteConversationMock = jest.fn(); 22 | 23 | const sdkMock = { 24 | conversations: { 25 | getConversation: conversationMock, 26 | deleteConversation: deleteConversationMock, 27 | }, 28 | }; 29 | 30 | await handler({ SDK: sdkMock, id: conversation.id }); 31 | 32 | expect(conversationMock).toHaveBeenCalledWith(conversation.id); 33 | expect(deleteConversationMock).toHaveBeenCalledWith(conversation.id); 34 | 35 | expect(console.log).toHaveBeenNthCalledWith( 36 | 2, 37 | 'Conversation deleted', 38 | ); 39 | }); 40 | 41 | test('Will not delete a conversation when user declines', async () => { 42 | confirm.mockResolvedValueOnce(false); 43 | const conversation = getTestConversationForAPI(); 44 | 45 | const conversationMock = jest.fn() 46 | .mockResolvedValueOnce(conversation); 47 | 48 | const deleteConversationMock = jest.fn(); 49 | 50 | const sdkMock = { 51 | conversations: { 52 | getConversation: conversationMock, 53 | deleteConversation: deleteConversationMock, 54 | }, 55 | }; 56 | 57 | await handler({ SDK: sdkMock, id: conversation.id }); 58 | 59 | expect(conversationMock).toHaveBeenCalledWith(conversation.id); 60 | expect(deleteConversationMock).not.toHaveBeenCalled(); 61 | 62 | expect(console.log).toHaveBeenNthCalledWith( 63 | 1, 64 | 'Conversation not deleted', 65 | ); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /__tests__/commands/conversations/conversations.show.test.js: -------------------------------------------------------------------------------- 1 | process.env.FORCE_COLOR = 0; 2 | const { displayDate } = require('../../../src/ux/date'); 3 | const yargs = require('yargs'); 4 | const { handler } = require('../../../src/commands/conversations/show'); 5 | const { mockConsole } = require('../../helpers'); 6 | const { getTestConversationForAPI } = require('../../conversations'); 7 | 8 | jest.mock('yargs'); 9 | jest.mock('../../../src/ux/confirm'); 10 | 11 | describe('Command: vonage conversations show', () => { 12 | beforeEach(() => { 13 | mockConsole(); 14 | }); 15 | 16 | test('Will show a conversation', async () => { 17 | const conversation = getTestConversationForAPI(); 18 | 19 | const conversationMock = jest.fn() 20 | .mockResolvedValueOnce(conversation); 21 | 22 | const sdkMock = { 23 | conversations: { 24 | getConversation: conversationMock, 25 | }, 26 | }; 27 | 28 | await handler({ SDK: sdkMock, conversationId: conversation.id }); 29 | 30 | expect(conversationMock).toHaveBeenCalledWith(conversation.id); 31 | 32 | expect(console.log).toHaveBeenNthCalledWith( 33 | 2, 34 | [ 35 | `Name: ${conversation.name}`, 36 | `Conversation ID: ${conversation.id}`, 37 | `Display Name: ${conversation.displayName}`, 38 | `Image URL: ${conversation.imageUrl}`, 39 | `State: ${conversation.state}`, 40 | `Time to Leave: ${conversation.properties.ttl}`, 41 | `Created at: ${displayDate(conversation.timestamp.created)}`, 42 | `Updated at: ${displayDate(conversation.timestamp.updated)}`, 43 | 'Destroyed at: Not Set', 44 | `Sequence: ${conversation.sequenceNumber}`, 45 | ].join('\n'), 46 | ); 47 | }); 48 | 49 | test('Will handle an error', async () => { 50 | const conversation = getTestConversationForAPI(); 51 | 52 | const conversationMock = jest.fn() 53 | .mockRejectedValueOnce(new Error('An error occurred')); 54 | 55 | const sdkMock = { 56 | conversations: { 57 | getConversation: conversationMock, 58 | }, 59 | }; 60 | 61 | await handler({ SDK: sdkMock, id: conversation.id }); 62 | expect(console.log).not.toHaveBeenCalled(); 63 | expect(yargs.exit).toHaveBeenCalledWith(99); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /__tests__/commands/jwt/jwt.create.test.js: -------------------------------------------------------------------------------- 1 | const { faker } = require('@faker-js/faker'); 2 | const { handler, jwtFlags } = require('../../../src/commands/jwt/create'); 3 | const { mockConsole } = require('../../helpers'); 4 | const { getTestMiddlewareArgs, testPrivateKey } = require('../../common'); 5 | const jwt = require('jsonwebtoken'); 6 | 7 | describe('Command: vonage jwt create', () => { 8 | beforeEach(() => { 9 | mockConsole(); 10 | }); 11 | 12 | test('should generate a JWT', async () => { 13 | const args = getTestMiddlewareArgs(); 14 | handler({ 15 | ...args, 16 | privateKey: testPrivateKey, 17 | }); 18 | expect(console.info).toHaveBeenCalledWith('Creating JWT token'); 19 | expect(console.log).toHaveBeenCalled(); 20 | const generatedToken = console.log.mock.calls[0][0]; 21 | 22 | const decoded = jwt.verify(generatedToken, testPrivateKey, { algorithms: ['RS256'] }); 23 | expect(decoded).toHaveProperty('iat'); 24 | expect(decoded).toHaveProperty('jti'); 25 | expect(decoded).toHaveProperty('exp'); 26 | expect(decoded).not.toHaveProperty('acl'); 27 | expect(decoded).not.toHaveProperty('sub'); 28 | expect(decoded.application_id).toBe(args.appId); 29 | }); 30 | 31 | test('should generate a JWT with a subject', async () => { 32 | const args = getTestMiddlewareArgs(); 33 | const sub = faker.string.alpha(10); 34 | handler({ 35 | ...args, 36 | privateKey: testPrivateKey, 37 | sub: sub, 38 | }); 39 | expect(console.info).toHaveBeenCalledWith('Creating JWT token'); 40 | expect(console.log).toHaveBeenCalled(); 41 | const generatedToken = console.log.mock.calls[0][0]; 42 | 43 | const decoded = jwt.verify(generatedToken, testPrivateKey, { algorithms: ['RS256'] }); 44 | expect(decoded.sub).toBe(sub); 45 | }); 46 | 47 | test('should generate a JWT with an acl', async () => { 48 | const args = getTestMiddlewareArgs(); 49 | const acl = { 50 | 'paths': { 51 | '/messages/*': { 52 | 'filters': { 53 | 'to': '447977271009', 54 | }, 55 | }, 56 | '/calls/*': { 57 | 'filters': { 58 | 'to': '447977271009', 59 | }, 60 | }, 61 | '/conferences/*': {}, 62 | }, 63 | }; 64 | 65 | handler({ 66 | ...args, 67 | privateKey: testPrivateKey, 68 | acl: JSON.stringify(acl), 69 | }); 70 | 71 | expect(jwtFlags.acl.coerce(JSON.stringify(acl))) 72 | .toEqual(acl); 73 | 74 | expect(console.info).toHaveBeenCalledWith('Creating JWT token'); 75 | expect(console.log).toHaveBeenCalled(); 76 | const generatedToken = console.log.mock.calls[0][0]; 77 | 78 | const decoded = jwt.verify(generatedToken, testPrivateKey, { algorithms: ['RS256'] }); 79 | expect(decoded.acl).toBe(JSON.stringify(acl)); 80 | }); 81 | 82 | test('should error when ACL is invalid', async () => { 83 | expect(jwtFlags.acl.coerce).toBeDefined(); 84 | expect(() => jwtFlags.acl.coerce('invalid')).toThrow('Failed to parse JSON for ACL'); 85 | expect(() => jwtFlags.acl.coerce('{"foo": "bar"}')).toThrow('ACL Failed to validate against schema'); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /__tests__/commands/numbers/numbers.cancel.test.js: -------------------------------------------------------------------------------- 1 | process.env.FORCE_COLOR = 0; 2 | const yargs = require('yargs'); 3 | const { confirm } = require('../../../src/ux/confirm'); 4 | const { faker } = require('@faker-js/faker'); 5 | const { handler } = require('../../../src/commands/numbers/cancel'); 6 | const { mockConsole } = require('../../helpers'); 7 | const { getTestPhoneNumber } = require('../../numbers'); 8 | 9 | jest.mock('yargs'); 10 | jest.mock('../../../src/ux/confirm'); 11 | 12 | describe('Command: vonage numbers cancel', () => { 13 | beforeEach(() => { 14 | mockConsole(); 15 | }); 16 | 17 | test('Will cancel a number', async () => { 18 | const testNumber = { 19 | ...getTestPhoneNumber(), 20 | appId: faker.datatype.boolean() 21 | ? faker.string.uuid() 22 | : undefined, 23 | }; 24 | 25 | const numbersMock = jest.fn().mockResolvedValueOnce({ 26 | count: 1, 27 | numbers: [testNumber], 28 | }); 29 | 30 | const cancelNumberMock = jest.fn(); 31 | 32 | confirm.mockResolvedValueOnce(true); 33 | 34 | const sdkMock = { 35 | numbers: { 36 | getOwnedNumbers: numbersMock, 37 | cancelNumber: cancelNumberMock, 38 | }, 39 | }; 40 | 41 | await handler({ 42 | SDK: sdkMock, 43 | country: testNumber.country, 44 | msisdn: testNumber.msisdn, 45 | }); 46 | 47 | expect(numbersMock).toHaveBeenCalledWith({ 48 | index: 1, 49 | size: 1, 50 | country: testNumber.country, 51 | pattern: testNumber.msisdn, 52 | searchPattern: 1, 53 | }); 54 | 55 | expect(cancelNumberMock).toHaveBeenCalledWith({ 56 | country: testNumber.country, 57 | msisdn: testNumber.msisdn, 58 | }); 59 | }); 60 | 61 | test('Will not cancel the number when user declines', async () => { 62 | const testNumber = { 63 | ...getTestPhoneNumber(), 64 | appId: faker.datatype.boolean() 65 | ? faker.string.uuid() 66 | : undefined, 67 | }; 68 | 69 | const numbersMock = jest.fn().mockResolvedValueOnce({ 70 | count: 1, 71 | numbers: [testNumber], 72 | }); 73 | 74 | const cancelNumberMock = jest.fn(); 75 | 76 | confirm.mockResolvedValueOnce(false); 77 | 78 | const sdkMock = { 79 | numbers: { 80 | getOwnedNumbers: numbersMock, 81 | cancelNumber: cancelNumberMock, 82 | }, 83 | }; 84 | 85 | await handler({ 86 | SDK: sdkMock, 87 | country: testNumber.country, 88 | msisdn: testNumber.msisdn, 89 | }); 90 | 91 | expect(numbersMock).toHaveBeenCalled(); 92 | expect(cancelNumberMock).not.toHaveBeenCalled(); 93 | }); 94 | 95 | test('Will not call cancel number when number not found', async () => { 96 | const testNumber = { 97 | ...getTestPhoneNumber(), 98 | appId: faker.datatype.boolean() 99 | ? faker.string.uuid() 100 | : undefined, 101 | }; 102 | 103 | const numbersMock = jest.fn().mockResolvedValueOnce({}); 104 | 105 | const cancelNumberMock = jest.fn(); 106 | 107 | confirm.mockResolvedValueOnce(true); 108 | 109 | const sdkMock = { 110 | numbers: { 111 | getOwnedNumbers: numbersMock, 112 | cancelNumber: cancelNumberMock, 113 | }, 114 | }; 115 | 116 | await handler({ 117 | SDK: sdkMock, 118 | country: testNumber.country, 119 | msisdn: testNumber.msisdn, 120 | }); 121 | 122 | expect(numbersMock).toHaveBeenCalled(); 123 | expect(cancelNumberMock).not.toHaveBeenCalled(); 124 | expect(yargs.exit).toHaveBeenCalledWith(44); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /__tests__/commands/users/users.delete.test.js: -------------------------------------------------------------------------------- 1 | process.env.FORCE_COLOR = 0; 2 | const { confirm } = require('../../../src/ux/confirm'); 3 | const { handler } = require('../../../src/commands/users/delete'); 4 | const { mockConsole } = require('../../helpers'); 5 | const { getTestUserForAPI } = require('../../users'); 6 | 7 | jest.mock('../../../src/ux/confirm'); 8 | 9 | describe('Command: vonage users delete', () => { 10 | beforeEach(() => { 11 | mockConsole(); 12 | }); 13 | 14 | test('Will delete a user', async () => { 15 | confirm.mockResolvedValueOnce(true); 16 | const user = getTestUserForAPI(); 17 | 18 | const userMock = jest.fn() 19 | .mockResolvedValueOnce(user); 20 | 21 | const deleteUserMock = jest.fn(); 22 | 23 | const sdkMock = { 24 | users: { 25 | getUser: userMock, 26 | deleteUser: deleteUserMock, 27 | }, 28 | }; 29 | 30 | await handler({ SDK: sdkMock, id: user.id }); 31 | 32 | expect(userMock).toHaveBeenCalledWith(user.id); 33 | expect(deleteUserMock).toHaveBeenCalledWith(user.id); 34 | 35 | expect(console.log).toHaveBeenNthCalledWith( 36 | 2, 37 | 'User deleted', 38 | ); 39 | }); 40 | 41 | test('Will not delete a user when user declines', async () => { 42 | confirm.mockResolvedValueOnce(false); 43 | const user = getTestUserForAPI(); 44 | 45 | const userMock = jest.fn() 46 | .mockResolvedValueOnce(user); 47 | 48 | const deleteUserMock = jest.fn(); 49 | 50 | const sdkMock = { 51 | users: { 52 | getUser: userMock, 53 | deleteUser: deleteUserMock, 54 | }, 55 | }; 56 | 57 | await handler({ SDK: sdkMock, id: user.id }); 58 | 59 | expect(userMock).toHaveBeenCalledWith(user.id); 60 | expect(deleteUserMock).not.toHaveBeenCalled(); 61 | 62 | expect(console.log).toHaveBeenNthCalledWith( 63 | 1, 64 | 'User not deleted', 65 | ); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /__tests__/conversations.js: -------------------------------------------------------------------------------- 1 | const { faker } = require('@faker-js/faker'); 2 | const { EventType } = require('@vonage/conversations'); 3 | const conversationEvents = Object.values(EventType); 4 | 5 | const states = ['ACTIVE', 'INACTIVE', 'DELETED']; 6 | 7 | const getTestConversationForAPI = () => Object.freeze({ 8 | id: `CON-${faker.string.uuid()}`, 9 | name: `${faker.commerce.productAdjective()}_${faker.commerce.productMaterial()}_${faker.commerce.product()}`, 10 | displayName: `${faker.commerce.productAdjective()}_${faker.commerce.productMaterial()}_${faker.commerce.product()}`, 11 | imageUrl: faker.internet.url(), 12 | state: faker.helpers.shuffle(states)[0], 13 | timestamp: { 14 | created: faker.date.recent(), 15 | updated: faker.date.recent(), 16 | }, 17 | properties: { 18 | ttl: faker.number.int(), 19 | }, 20 | sequenceNumber: faker.number.int(), 21 | mediaState: { 22 | earmuff: faker.datatype.boolean(), 23 | mute: faker.datatype.boolean(), 24 | playStream: faker.datatype.boolean(), 25 | recording: faker.datatype.boolean(), 26 | transcribing: faker.datatype.boolean(), 27 | tts: faker.datatype.boolean(), 28 | }, 29 | }); 30 | 31 | const addCLIPropertiesToConversation = () => Object.freeze({ 32 | ...getTestConversationForAPI(), 33 | numbers: [ 34 | { 35 | type: 'phone', 36 | number: faker.phone.number(), 37 | }, 38 | ], 39 | properties: { 40 | ttl: faker.number.int(), 41 | }, 42 | callback: { 43 | url: faker.internet.url(), 44 | method: faker.helpers.shuffle(['GET', 'POST'])[0], 45 | eventMask: faker.helpers.shuffle(conversationEvents).slice(0, 3), 46 | params: { 47 | applicationId: faker.string.uuid(), 48 | nccoUrl: faker.internet.url(), 49 | }, 50 | }, 51 | }); 52 | 53 | 54 | exports.addCLIPropertiesToConversation = addCLIPropertiesToConversation; 55 | exports.getTestConversationForAPI = getTestConversationForAPI; 56 | -------------------------------------------------------------------------------- /__tests__/helpers.js: -------------------------------------------------------------------------------- 1 | afterEach(jest.restoreAllMocks); 2 | process.stdout.clearLine = jest.fn(); 3 | process.stderr.clearLine = jest.fn(); 4 | 5 | exports.mockConsole = () => { 6 | console.log = jest.spyOn(console, 'log'); 7 | console.warn = jest.spyOn(console, 'warn'); 8 | console.info = jest.spyOn(console, 'info'); 9 | console.debug = jest.spyOn(console, 'debug'); 10 | console.error = jest.spyOn(console, 'error'); 11 | console.table = jest.spyOn(console, 'table'); 12 | process.stdout.clearLine = jest.fn(); 13 | process.stderr.clearLine = jest.fn(); 14 | process.stdout.write = jest.fn(); 15 | process.stderr.write = jest.fn(); 16 | return console; 17 | }; 18 | -------------------------------------------------------------------------------- /__tests__/members.js: -------------------------------------------------------------------------------- 1 | const { faker } = require('@faker-js/faker'); 2 | const { getTestConversationForAPI } = require('./conversations'); 3 | const { getTestUserForAPI } = require('./users'); 4 | const { stateLabels } = require('../src/members/display'); 5 | 6 | const fromChannels = [ 7 | 'app', 8 | 'phone', 9 | 'sms', 10 | 'mms', 11 | 'whatsapp', 12 | 'viber', 13 | 'messanger', 14 | ]; 15 | 16 | const getTestMemberForAPI = () => Object.freeze({ 17 | id: `MEM-${faker.string.uuid()}`, 18 | conversationId: getTestConversationForAPI().id, 19 | user: { 20 | id: getTestUserForAPI().id, 21 | name: getTestUserForAPI().name, 22 | displayName: getTestUserForAPI().displayName, 23 | }, 24 | state: faker.helpers.shuffle(Object.keys(stateLabels))[0], 25 | timestamp: { 26 | invited: faker.date.soon().toISOString(), 27 | joined: faker.date.soon().toISOString(), 28 | left: faker.date.soon().toISOString(), 29 | }, 30 | media: { 31 | audioSettings: { 32 | muted: faker.datatype.boolean(), 33 | earmuffed: faker.datatype.boolean(), 34 | enabled: faker.datatype.boolean(), 35 | }, 36 | audio: faker.datatype.boolean(), 37 | }, 38 | knockingId: faker.string.uuid(), 39 | memberIdInviting: faker.string.uuid(), 40 | from: faker.string.uuid(), 41 | }); 42 | 43 | const randomFrom = () => faker.helpers.shuffle(fromChannels).slice( 44 | faker.helpers.rangeToNumber({ min: 1, max: fromChannels.length }), 45 | ).sort().join(','); 46 | 47 | const addAppChannelToMember = (member) => Object.freeze({ 48 | ...member, 49 | channel: { 50 | type: 'app', 51 | from: { 52 | type: randomFrom(), 53 | }, 54 | to: { 55 | user: getTestUserForAPI().id, 56 | type: 'app', 57 | }, 58 | }, 59 | }); 60 | 61 | const addPhoneChannelToMember = (member) => Object.freeze({ 62 | ...member, 63 | channel: { 64 | type: 'phone', 65 | from: { 66 | type: randomFrom(), 67 | }, 68 | to: { 69 | number: faker.phone.number({ style: 'international' }), 70 | type: 'phone', 71 | }, 72 | }, 73 | }); 74 | 75 | const addSMSChannelToMember = (member) => Object.freeze({ 76 | ...member, 77 | channel: { 78 | type: 'sms', 79 | from: { 80 | type: randomFrom(), 81 | }, 82 | to: { 83 | number: faker.phone.number({ style: 'international' }), 84 | type: 'sms', 85 | }, 86 | }, 87 | }); 88 | 89 | const addMMSChannelToMember = (member) => Object.freeze({ 90 | ...member, 91 | channel: { 92 | type: 'mms', 93 | from: { 94 | type: randomFrom(), 95 | }, 96 | to: { 97 | number: faker.phone.number({ style: 'international' }), 98 | }, 99 | }, 100 | }); 101 | 102 | const addWhatsAppChannelToMember = (member) => Object.freeze({ 103 | ...member, 104 | channel: { 105 | type: 'whatsapp', 106 | from: { 107 | type: randomFrom(), 108 | }, 109 | to: { 110 | number: faker.phone.number({ style: 'international' }), 111 | }, 112 | }, 113 | }); 114 | 115 | const addViberChannelToMember = (member) => Object.freeze({ 116 | ...member, 117 | channel: { 118 | type: 'viber', 119 | from: { 120 | type: randomFrom(), 121 | }, 122 | to: { 123 | id: faker.string.uuid(), 124 | }, 125 | }, 126 | }); 127 | 128 | const addMessengerChannelToMember = (member) => Object.freeze({ 129 | ...member, 130 | channel: { 131 | type: 'messenger', 132 | from: { 133 | type: randomFrom(), 134 | }, 135 | to: { 136 | id: faker.string.uuid(), 137 | }, 138 | }, 139 | }); 140 | 141 | exports.getTestMemberForAPI = getTestMemberForAPI; 142 | exports.addAppChannelToMember = addAppChannelToMember; 143 | exports.addPhoneChannelToMember = addPhoneChannelToMember; 144 | exports.addSMSChannelToMember = addSMSChannelToMember; 145 | exports.addMMSChannelToMember = addMMSChannelToMember; 146 | exports.addWhatsAppChannelToMember = addWhatsAppChannelToMember; 147 | exports.addViberChannelToMember = addViberChannelToMember; 148 | exports.addMessengerChannelToMember = addMessengerChannelToMember; 149 | 150 | -------------------------------------------------------------------------------- /__tests__/middleware/log.test.js: -------------------------------------------------------------------------------- 1 | const { setupLog } = require('../../src/middleware/log'); 2 | const winston = require('winston'); 3 | 4 | describe('Middleware: Log', () => { 5 | test('Will overwrite console log', () => { 6 | expect(console.info).not.toEqual(winston.__mockLogger.info); 7 | expect(console.warn).not.toEqual(winston.__mockLogger.warn); 8 | expect(console.error).not.toEqual(winston.__mockLogger.error); 9 | expect(console.debug).not.toEqual(winston.__mockLogger.debug); 10 | 11 | setupLog({}); 12 | 13 | console.info('info'); 14 | console.warn('warn'); 15 | console.error('error'); 16 | console.debug('debug'); 17 | 18 | expect(winston.__mockLogger.info).toHaveBeenCalled(); 19 | expect(winston.__mockLogger.warn).toHaveBeenCalled(); 20 | expect(winston.__mockLogger.error).toHaveBeenCalled(); 21 | expect(winston.__mockLogger.debug).toHaveBeenCalled(); 22 | 23 | expect(winston.createLogger).toHaveBeenCalledWith({ 24 | format: undefined, 25 | level: 'emerg', 26 | transports: [{}], 27 | }); 28 | }); 29 | 30 | test('Will overwrite console log and set the level to info', () => { 31 | expect(console.info).not.toEqual(winston.__mockLogger.info); 32 | expect(console.warn).not.toEqual(winston.__mockLogger.warn); 33 | expect(console.error).not.toEqual(winston.__mockLogger.error); 34 | expect(console.debug).not.toEqual(winston.__mockLogger.debug); 35 | 36 | setupLog({verbose: true}); 37 | 38 | console.info('info'); 39 | console.warn('warn'); 40 | console.error('error'); 41 | console.debug('debug'); 42 | 43 | expect(winston.__mockLogger.info).toHaveBeenCalled(); 44 | expect(winston.__mockLogger.warn).toHaveBeenCalled(); 45 | expect(winston.__mockLogger.error).toHaveBeenCalled(); 46 | expect(winston.__mockLogger.debug).toHaveBeenCalled(); 47 | 48 | expect(winston.createLogger).toHaveBeenCalledWith({ 49 | format: undefined, 50 | level: 'info', 51 | transports: [{}], 52 | }); 53 | }); 54 | 55 | test('Will overwrite console log and set the level to debug', () => { 56 | expect(console.info).not.toEqual(winston.__mockLogger.info); 57 | expect(console.warn).not.toEqual(winston.__mockLogger.warn); 58 | expect(console.error).not.toEqual(winston.__mockLogger.error); 59 | expect(console.debug).not.toEqual(winston.__mockLogger.debug); 60 | 61 | setupLog({verbose: true, debug: true}); 62 | 63 | console.info('info'); 64 | console.warn('warn'); 65 | console.error('error'); 66 | console.debug('debug'); 67 | 68 | expect(winston.__mockLogger.info).toHaveBeenCalled(); 69 | expect(winston.__mockLogger.warn).toHaveBeenCalled(); 70 | expect(winston.__mockLogger.error).toHaveBeenCalled(); 71 | expect(winston.__mockLogger.debug).toHaveBeenCalled(); 72 | 73 | expect(winston.createLogger).toHaveBeenCalledWith({ 74 | format: undefined, 75 | level: 'debug', 76 | transports: [{}], 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /__tests__/numbers.js: -------------------------------------------------------------------------------- 1 | const { faker } = require('@faker-js/faker'); 2 | const { countryCodes } = require('../src/utils/countries'); 3 | 4 | const getTestPhoneNumber = () => Object.freeze({ 5 | 'country': faker.helpers.shuffle(countryCodes)[0], 6 | 'msisdn': faker.phone.number({ style: 'international' }), 7 | 'type': faker.helpers.shuffle(['landline', 'landline-toll-free', 'mobile-lvn'])[0], 8 | 'features': faker.helpers.arrayElements([ 9 | 'VOICE', 10 | 'MMS', 11 | 'SMS', 12 | ], {min: 1, max: 3}).sort(), 13 | }); 14 | 15 | exports.getTestPhoneNumber = getTestPhoneNumber; 16 | -------------------------------------------------------------------------------- /__tests__/test.private.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDTH8cEhJKsu2hB 3 | ucgs0Blf3tvgdXuIa5sMRI3zIZGok8jqaj6DC0WdM1eiJlQCWnVL0vR25MkopMZN 4 | MTphoaVpdK9wywMVx7PsgMCDXJjW77QFJFbbcbV9KG2xvMsjJGttmVrn+9aPpCnX 5 | yT27bWkTR0k/wCAvra/st5MDKg2kB3SMc+97zlO3GqIkJJKfKYQFAO8oRBcsZ1Bx 6 | ydPWTQvyLYW15Cjv30aIGLfceFwDeUOBgBsesGFtcXvVF6sVxd5D/USunX/9es95 7 | Rdr4+9Qq4tIKZRkBz2KWD0eo256wmmq2lQoGaR9x6XgLHhUlsi6OXILcyChi2Qcc 8 | a01Hn7YvAgMBAAECggEAJsS+lIdNsddmIS+e4Q/DoRW49aJNMXNlEN8j2+Itr7GX 9 | ougom4K94UyUyotUQOxgfrB5wL1pbQO5AGLKUDRRPii1sLYu1liKIyNPdq/RxyJU 10 | Qd927awXQiji39EF0mm1KnaPOWtG7rCcGGp1Yg4Izgf4nPLIVkkENalOHzYhNB3u 11 | 4W4OIT49iw/auBF4wnl1RmXWXjkxDuk2cYT28a8hWqyQjJqXTsO+u4BaXYxSf4nP 12 | Be2yoUEFRbcxvJrhEpfODhPP83I1EBipJkhUTc5WMb/vtH2b49+TYd2tPR0LOxom 13 | mcNUWF6++ae+vL6K8Dlfcvx+CA7g7KBHHcgFCzn7GQKBgQDzc2ow5LlQQ/VfXZTz 14 | n07V/QgVQ15sA5Cf/gsvmwnGPy06Qx/WRHsz6NG8nvW2mHZwfDIHuLjBW1gcssEx 15 | mLpqav5XLZfSyjjRO/AxLIfJDx/aARp3+7Ny5aY2e3wtNx8wz4J80i7P+eX3fETM 16 | 70cWhc2PvYMDjG+O7cDW2FWAFwKBgQDeAcc/FBHLl9/HqiBvYf/Y/k0t1TUoHujO 17 | PSbP6SaN06JnvJmBANyED7sWeIPuoRFXXEr4Auu7y0C55Wlsno/ImTbJsopZ1rgU 18 | k5q4t9vcu7cGiOr7L7UkySNYZqRjwvKEJ610COexTThSwl0v3GNLP8r4AMdBaqdK 19 | uO6fVfxxqQKBgFc5ne2Atai9gJe3ltum0382FoRPy+/VYyb/xZA780eVcSXz0N9b 20 | T+0sWKFFLvJKM/1fcil0FLYqfSpjHXcgqoHgbdpcWo5KqArHd+qWctwl0Bqy1IHy 21 | q7vZ7jCNE9O7cTBy2OTSBbW8apm+a4Qlowm9zQXYN624zmueYb5YamHnAoGAZvJA 22 | KHnv/o7AkF/NhpjVARR7SYOSkLY0kl48/zBIVoAK0TvdmrqBhyOcR8E+vIsn9XCw 23 | uuzvzzdjHlDJYDruxcB2bXVDPoGY/sGrf3iSlXreVkNrY2st/o7euwFtvW0K9ElJ 24 | 34K5nbgHJClI+QajbKN6RSJnQ2hnhvjWfkBrPXECgYEA4MCEm9EyrguEO51am8va 25 | OjIAiQMkj/iyjsMDL8eH0VM+OMdieHwbkKyajyrB9dFikvWfxiuo3dU1N5vJTzty 26 | LmzkB8M/rKlAYKD8iKA8cRun4tKzRepHT3JPMu0GYTfcP9ovs5F3aEjX+UuWOO7n 27 | doWDENAr/VU1RNCDwFdxYFg= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /__tests__/users.js: -------------------------------------------------------------------------------- 1 | const { faker } = require('@faker-js/faker'); 2 | 3 | const addSIPChannelToUser = (user) => ({ 4 | ...user, 5 | channels: { 6 | ...user.channels, 7 | sip: [ 8 | ...user.channels?.sip || [], 9 | { 10 | uri: faker.internet.url({protocol: 'sip'}), 11 | username: faker.internet.username(), 12 | password: faker.internet.password(), 13 | }, 14 | ], 15 | }, 16 | }); 17 | 18 | const addWebsocketChannelToUser = (user) => ({ 19 | ...user, 20 | channels: { 21 | ...user.channels, 22 | websocket: [ 23 | ...user.channels?.websocket || [], 24 | { 25 | uri: faker.internet.url({protocol: 'wss'}), 26 | contentType: `audio/l16;rate=${faker.helpers.shuffle([8000, 16000, 32000])[0]}`, 27 | headers: { 28 | 'X-Header': faker.internet.userAgent(), 29 | }, 30 | }, 31 | ], 32 | }, 33 | }); 34 | 35 | const addPSTNChannelToUser = (user) => ({ 36 | ...user, 37 | channels: { 38 | ...user.channels, 39 | pstn: [ 40 | ...(user.channels?.pstn || []), 41 | { 42 | number: faker.phone.number({style: 'international'}), 43 | }, 44 | ], 45 | }, 46 | }); 47 | 48 | const addSMSChannelToUser = (user) => ({ 49 | ...user, 50 | channels: { 51 | ...user.channels, 52 | sms: [ 53 | { 54 | number: faker.phone.number({style: 'international'}), 55 | }, 56 | ], 57 | }, 58 | }); 59 | 60 | const addMMSChannelToUser = (user) => ({ 61 | ...user, 62 | channels: { 63 | ...user.channels, 64 | mms: [ 65 | { 66 | number: faker.phone.number({style: 'international'}), 67 | }, 68 | ], 69 | }, 70 | }); 71 | 72 | const addWhatsAppChannelToUser = (user) => ({ 73 | ...user, 74 | channels: { 75 | ...user.channels, 76 | whatsapp: [ 77 | { 78 | number: faker.phone.number({style: 'international'}), 79 | }, 80 | ], 81 | }, 82 | }); 83 | 84 | const addMessengerChannelToUser = (user) => ({ 85 | ...user, 86 | channels: { 87 | ...user.channels, 88 | messenger: [ 89 | { 90 | id:faker.internet.username(), 91 | }, 92 | ], 93 | }, 94 | }); 95 | 96 | const addViberChannelToUser = (user) => ({ 97 | ...user, 98 | channels: { 99 | ...user.channels, 100 | viber: [ 101 | { 102 | number: faker.phone.number({style: 'international'}), 103 | }, 104 | ], 105 | }, 106 | }); 107 | 108 | 109 | const getTestUserForAPI = () => Object.freeze({ 110 | id: `USR-${faker.string.uuid()}`, 111 | name: faker.internet.username(), 112 | displayName: faker.person.fullName(), 113 | imageUrl: faker.internet.url(), 114 | properties: { 115 | ttl: faker.number.int(), 116 | }, 117 | channels: {}, 118 | }); 119 | 120 | exports.getTestUserForAPI = getTestUserForAPI; 121 | 122 | exports.addSIPChannelToUser = addSIPChannelToUser; 123 | exports.addWebsocketChannelToUser = addWebsocketChannelToUser; 124 | exports.addPSTNChannelToUser = addPSTNChannelToUser; 125 | exports.addSMSChannelToUser = addSMSChannelToUser; 126 | exports.addMMSChannelToUser = addMMSChannelToUser; 127 | exports.addWhatsAppChannelToUser = addWhatsAppChannelToUser; 128 | exports.addViberChannelToUser = addViberChannelToUser; 129 | exports.addMessengerChannelToUser = addMessengerChannelToUser; 130 | 131 | -------------------------------------------------------------------------------- /__tests__/utils/coerceNumber.test.js: -------------------------------------------------------------------------------- 1 | const { coerceNumber } = require('../../src/utils/coerceNumber'); 2 | 3 | describe('Utils: coerceNumber', () => { 4 | test('Will coerce a number', () => { 5 | const coerced = coerceNumber('test')('1'); 6 | expect(coerced).toBe(1); 7 | }); 8 | 9 | test('Will throw an error if the value is not a number', () => { 10 | expect(() => coerceNumber('test')('a')).toThrow('Invalid number for test: a'); 11 | }); 12 | 13 | test('Will throw an error if the value is less than the min', () => { 14 | expect(() => coerceNumber('test', { min: 2 })('1')).toThrow('Number for test must be at least 2: 1'); 15 | }); 16 | 17 | test('Will throw an error if the value is greater than the max', () => { 18 | expect(() => coerceNumber('test', { max: 2 })('3')).toThrow('Number for test must be at most 2: 3'); 19 | }); 20 | 21 | test('Will return undefined if the value is undefined', () => { 22 | const coerced = coerceNumber('test')(undefined); 23 | expect(coerced).toBeUndefined(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /__tests__/utils/coercePrivateKey.test.js: -------------------------------------------------------------------------------- 1 | const { coerceKey } = require('../../src/utils/coerceKey'); 2 | const { faker } = require('@faker-js/faker'); 3 | const fs = require('fs'); 4 | 5 | jest.mock('fs'); 6 | 7 | describe('Utils: coerce private key', () => { 8 | test('Will return null if no private key is provided', () => { 9 | expect(coerceKey('private')(null)).toBeNull(); 10 | }); 11 | 12 | test('Will return the private key if it is valid', () => { 13 | const privateKey = `-----BEGIN PRIVATE KEY-----\n${faker.string.alpha(128)}\n-----BEGIN PRIVATE KEY-----`; 14 | expect(coerceKey('private')(privateKey)).toBe(privateKey); 15 | }); 16 | 17 | test('Will load the private key from a file if it is valid', () => { 18 | const testPrivateKeyFile = faker.system.filePath(); 19 | const privateKey = `-----BEGIN PRIVATE KEY-----\n${faker.string.alpha(128)}\n-----BEGIN PRIVATE KEY-----`; 20 | console.log(testPrivateKeyFile); 21 | fs.__addFile(testPrivateKeyFile, privateKey); 22 | expect(coerceKey('private')(testPrivateKeyFile)).toBe(privateKey); 23 | }); 24 | 25 | test('Will return the public key if it is valid', () => { 26 | const publicKey = `-----BEGIN PUBLIC KEY-----\n${faker.string.alpha(128)}\n-----BEGIN PUBLIC KEY-----`; 27 | expect(coerceKey('public')(publicKey)).toBe(publicKey); 28 | }); 29 | 30 | test('Will load the public key from a file if it is valid', () => { 31 | const testpublicKeyFile = faker.system.filePath(); 32 | const publicKey = `-----BEGIN PUBLIC KEY-----\n${faker.string.alpha(128)}\n-----BEGIN PUBLIC KEY-----`; 33 | console.log(testpublicKeyFile); 34 | fs.__addFile(testpublicKeyFile, publicKey); 35 | expect(coerceKey('public')(testpublicKeyFile)).toBe(publicKey); 36 | }); 37 | 38 | test('Will throw an error if the private key file does not exist', () => { 39 | const testPrivateKeyFile = faker.system.filePath(); 40 | expect(() => coerceKey('private')(testPrivateKeyFile)).toThrow('Key must be a valid key string or a path to a file containing a key'); 41 | }); 42 | 43 | test('Will throw an error if the private key file is invalid', () => { 44 | const testPrivateKeyFile = faker.system.filePath(); 45 | const privateKey = faker.string.alpha(128); 46 | fs.__addFile(testPrivateKeyFile, privateKey); 47 | expect(() => coerceKey('private')(testPrivateKeyFile)).toThrow('The key file does not contain a valid key string'); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /__tests__/utils/coerceURL.test.js: -------------------------------------------------------------------------------- 1 | const { coerceUrl } = require('../../src/utils/coerceUrl'); 2 | 3 | describe('Utils: coerceURL', () => { 4 | test('Will return null if no URL is provided', () => { 5 | expect(coerceUrl('some-webhook')()).toBeUndefined(); 6 | }); 7 | 8 | test('Will return the URL if it is valid', () => { 9 | const url = 'https://www.example.com/'; 10 | expect(coerceUrl('some-webhook')(url)).toBe(url); 11 | }); 12 | 13 | test('Will throw an error if the URL is invalid', () => { 14 | const url = 'not-a-url'; 15 | expect(() => coerceUrl('some-webhook')(url)).toThrow('Invalid URL for some-webhook: not-a-url'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /__tests__/utils/fs.test.js: -------------------------------------------------------------------------------- 1 | const { faker } = require('@faker-js/faker'); 2 | const { writeFileSync } = require('fs'); 3 | const { 4 | writeFile, 5 | writeJSONFile, 6 | } = require('../../src/utils/fs'); 7 | const { confirm } = require('../../src/ux/confirm'); 8 | const fs = require('fs'); 9 | 10 | jest.mock('fs'); 11 | jest.mock('../../src/ux/confirm'); 12 | 13 | describe('Utils: File system', () => { 14 | test('Will write file', async () => { 15 | const testFile = faker.system.filePath(); 16 | await writeFile(testFile, 'new data'); 17 | expect(confirm).not.toHaveBeenCalled(); 18 | expect(writeFileSync).toHaveBeenCalledWith(testFile, 'new data'); 19 | 20 | }); 21 | 22 | test('Will confirm before writing file', async () => { 23 | const testFile = faker.system.filePath(); 24 | 25 | fs.__addFile(testFile, 'test data'); 26 | 27 | confirm.mockResolvedValueOnce(true); 28 | 29 | await writeFile(testFile, 'new data'); 30 | expect(confirm).toHaveBeenCalledWith(`Overwirte file ${testFile}?`); 31 | expect(writeFileSync).toHaveBeenCalledWith(testFile, 'new data'); 32 | }); 33 | 34 | test('Will confirm before writing file but not write when user declines', async () => { 35 | const testFile = faker.system.filePath(); 36 | 37 | fs.__addFile(testFile, 'test data'); 38 | 39 | confirm.mockResolvedValueOnce(false); 40 | 41 | await expect(() => writeFile(testFile, 'new data')).rejects.toThrow('User declined to overwrite file'); 42 | expect(confirm).toHaveBeenCalledWith(`Overwirte file ${testFile}?`); 43 | expect(writeFileSync).not.toHaveBeenCalled(); 44 | }); 45 | 46 | test('Will write JSON file', async () => { 47 | const testFile = faker.system.filePath(); 48 | await writeJSONFile(testFile, {foo: 'bar'}); 49 | expect(confirm).not.toHaveBeenCalled(); 50 | expect(writeFileSync).toHaveBeenCalledWith( 51 | testFile, 52 | JSON.stringify( 53 | {foo: 'bar'}, 54 | null, 55 | 2, 56 | ), 57 | ); 58 | }); 59 | }); 60 | 61 | -------------------------------------------------------------------------------- /__tests__/ux/confirm.test.js: -------------------------------------------------------------------------------- 1 | const { confirm } = require('../../src/ux/confirm'); 2 | const readline = require('readline'); 3 | 4 | jest.mock('readline'); 5 | 6 | describe('UX: confirm', () => { 7 | test('Will confrim the message with y', async () => { 8 | readline.__questionMock.mockImplementation((question, callback) => { 9 | callback('y'); 10 | }); 11 | 12 | const result = await confirm('Are you sure?'); 13 | expect(result).toBe(true); 14 | expect(readline.__questionMock).toHaveBeenCalledWith('Are you sure? [y/n] ', expect.any(Function)); 15 | expect(readline.__closeMock).toHaveBeenCalledTimes(1); 16 | }); 17 | 18 | test('Will confrim the message with n', async () => { 19 | readline.__questionMock.mockImplementation((question, callback) => { 20 | callback('n'); 21 | }); 22 | 23 | const result = await confirm('Are you sure?'); 24 | expect(result).toBe(false); 25 | expect(readline.__questionMock).toHaveBeenCalledWith('Are you sure? [y/n] ', expect.any(Function)); 26 | expect(readline.__closeMock).toHaveBeenCalledTimes(1); 27 | }); 28 | 29 | test('Will keep asking with invalid input message', async () => { 30 | readline.__questionMock 31 | .mockImplementationOnce((question, callback) => { 32 | callback('foo'); 33 | }) 34 | .mockImplementationOnce((question, callback) => { 35 | callback('y'); 36 | }); 37 | 38 | const result = await confirm('Are you sure?'); 39 | expect(result).toBe(true); 40 | expect(readline.__questionMock).toHaveBeenNthCalledWith(1, 'Are you sure? [y/n] ', expect.any(Function)); 41 | expect(readline.__questionMock).toHaveBeenNthCalledWith(2, 'Are you sure? [y/n] ', expect.any(Function)); 42 | expect(readline.__closeMock).toHaveBeenCalledTimes(2); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /__tests__/ux/dump.test.js: -------------------------------------------------------------------------------- 1 | const { faker } = require('@faker-js/faker'); 2 | const { dumpValue, dumpKey } = require('../../src/ux/dump'); 3 | const { descriptionList } = require('../../src/ux/descriptionList'); 4 | const { dumpYesNo, dumpOnOff, dumpEnabledDisabled } = require('../../src/ux/dumpYesNo'); 5 | const { table } = require('../../src/ux/table'); 6 | const uxTests = require('../__dataSets__/ux'); 7 | 8 | describe('UX: dump', () => { 9 | test.each(uxTests)('Will $label', ({ value, expected }) => { 10 | expect(dumpValue(value)).toEqual(expected); 11 | }); 12 | }); 13 | 14 | describe('UX: description list', () => { 15 | test('Will return a string', () => { 16 | expect(descriptionList([ 17 | ['term', 'details'], 18 | ['foo', 'bar'], 19 | ])).toBe(`${dumpKey('term')}: ${dumpValue('details')}\n${dumpKey('foo')}: ${dumpValue('bar')}`); 20 | }); 21 | }); 22 | 23 | describe('UX: boolean dump', () => { 24 | test('Will return Yes or No', () => { 25 | expect(dumpYesNo(true)).toBe('✅ Yes'); 26 | expect(dumpYesNo(false)).toBe('❌ No'); 27 | 28 | expect(dumpYesNo(true, false)).toBe('✅ '); 29 | expect(dumpYesNo(false, false)).toBe('❌ '); 30 | }); 31 | 32 | test('Will return On or Off', () => { 33 | expect(dumpOnOff(true)).toBe('On'); 34 | expect(dumpOnOff(false)).toBe('Off'); 35 | }); 36 | 37 | test('Will return Enabled or Disabled', () => { 38 | expect(dumpEnabledDisabled(true)).toBe('✅ '); 39 | expect(dumpEnabledDisabled(false)).toBe('❌ '); 40 | 41 | expect(dumpEnabledDisabled(true, true)).toBe('✅ Enabled'); 42 | expect(dumpEnabledDisabled(false, true)).toBe('❌ Disabled'); 43 | }); 44 | }); 45 | 46 | describe('UX: table', () => { 47 | test('Will return a string', () => { 48 | const data = [ 49 | { 50 | id: faker.string.alpha(10), 51 | desc: faker.string.alpha(10), 52 | }, 53 | { 54 | id: faker.string.alpha(10), 55 | desc: faker.string.alpha(10), 56 | }, 57 | ]; 58 | const results = table(data).split('\n'); 59 | expect(results).toHaveLength(5); 60 | 61 | expect(results[0]).toBe('id desc '); 62 | expect(results[1]).toBe('---------- ----------'); 63 | expect(results[2]).toBe(`${data[0].id} ${data[0].desc}`); 64 | expect(results[3]).toBe(`${data[1].id} ${data[1].desc}`); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /bin/vonage.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S node 2 | const chalk = require('chalk'); 3 | const { hideBin } = require('yargs/helpers'); 4 | const yargs = require('yargs'); 5 | const { setConfig } = require('../src/middleware/config'); 6 | const { setupLog } = require('../src/middleware/log'); 7 | const { checkForUpdate } = require('../src/middleware/update'); 8 | const { dumpCommand } = require('../src/ux/dump'); 9 | const { getSettings } = require('../src/utils/settings'); 10 | const settings = getSettings(); 11 | 12 | const { needsUpdate, forceUpdate, forceMinVersion } = settings; 13 | 14 | if (needsUpdate) { 15 | const settings = getSettings(); 16 | const { latestVersion } = settings; 17 | console.log(`An update is available for the CLI. Please update to version ${latestVersion}`); 18 | console.log(`Run ${dumpCommand(`npm install -g @vonage/cli@${latestVersion}`)} to update`); 19 | } 20 | 21 | const vonageCLI = yargs(hideBin(process.argv)) 22 | .fail((msg) => { 23 | yargs.showHelp(); 24 | console.log(''); 25 | console.error(chalk.red('Error'), msg); 26 | yargs.exit(99); 27 | }) 28 | .options({ 29 | 'verbose': { 30 | alias: 'v', 31 | describe: 'Print more information', 32 | type: 'boolean', 33 | }, 34 | 'debug': { 35 | alias: 'd', 36 | describe: 'Print debug information', 37 | type: 'boolean', 38 | }, 39 | 'no-color': { 40 | describe: 'Toggle color output off', 41 | type: 'boolean', 42 | }, 43 | }) 44 | .middleware(setupLog) 45 | .middleware(setConfig) 46 | .middleware(checkForUpdate) 47 | .scriptName('vonage') 48 | .commandDir('../src/commands') 49 | .demandCommand() 50 | .showHelpOnFail(false) 51 | .help() 52 | .alias('help', 'h') 53 | .wrap(yargs.terminalWidth()); 54 | 55 | const run = async () => { 56 | if (forceUpdate) { 57 | console.log(`A critical update is available for the CLI. Please update to version ${forceMinVersion}`); 58 | console.log(`Run ${dumpCommand(`npm install -g @vonage/cli@${forceMinVersion}`)} to update`); 59 | return; 60 | } 61 | 62 | vonageCLI.parse(); 63 | }; 64 | run(); 65 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const globals = require('globals'); 2 | const eslint = require('@eslint/js'); 3 | const stylisticJs = require('@stylistic/eslint-plugin-js'); 4 | const jest = require('eslint-plugin-jest'); 5 | const nodePlugin = require('eslint-plugin-n'); 6 | 7 | module.exports = [ 8 | eslint.configs.recommended, 9 | stylisticJs.configs['disable-legacy'], 10 | nodePlugin.configs['flat/recommended'], 11 | jest.configs['flat/recommended'], 12 | jest.configs['flat/style'], 13 | { 14 | languageOptions: { 15 | ecmaVersion: 2022, 16 | sourceType: 'module', 17 | globals: { 18 | ...globals.node, 19 | ...globals.jest, 20 | }, 21 | }, 22 | plugins: { 23 | '@stylistic/js': stylisticJs, 24 | }, 25 | rules: { 26 | '@stylistic/js/semi': ['error', 'always'], 27 | '@stylistic/js/quotes': ['error', 'single'], 28 | '@stylistic/js/indent': ['error', 2], 29 | '@stylistic/js/comma-dangle': ['error', 'always-multiline'], 30 | '@stylistic/js/function-call-argument-newline': ['error', 'consistent'], 31 | '@stylistic/js/array-bracket-newline': ['error', { 'multiline': true }], 32 | '@stylistic/js/dot-location': ['error', 'property'], 33 | }, 34 | }, 35 | { 36 | files: ['packages/*/src/**/*.{js}'], 37 | }, 38 | { 39 | settings: { 40 | node: { 41 | version: '>=18.0.0', 42 | }, 43 | }, 44 | rules: { 45 | // Leave this off. This rule cannot handle monorepos 46 | 'n/no-missing-import': ['off'], 47 | 'n/no-unsupported-features/es-builtins': [ 48 | 'error', { 49 | 'ignores': [], 50 | }, 51 | ], 52 | }, 53 | }, 54 | 55 | ]; 56 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | coverageDirectory: '/coverage/', 3 | collectCoverageFrom: ['src/**/*.js'], 4 | coveragePathIgnorePatterns: [ 5 | 'node_modules', 6 | '/testHelpers/*', 7 | '/__tests__', 8 | '/src/commands/apps/capabilities.js', 9 | '/src/commands/apps/numbers.js', 10 | ], 11 | testMatch: ['/__tests__/**/*.test.js'], 12 | }; 13 | 14 | module.exports = config; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/package.json", 3 | "name": "@vonage/cli", 4 | "version": "3.0.4", 5 | "description": "Vonage CLI", 6 | "license": "Apache 2.0", 7 | "contributors": [ 8 | { 9 | "name": "Paul Ardeleanu", 10 | "url": "https://github.com/pardel" 11 | }, 12 | { 13 | "name": "Chuck \"MANCHUCK\" Reeves", 14 | "url": "https://github.com/manchuck" 15 | }, 16 | { 17 | "name": "Chris Tankersley", 18 | "url": "https://github.com/dragonmantank" 19 | } 20 | ], 21 | "bin": { 22 | "vonage": "bin/vonage.js" 23 | }, 24 | "files": [ 25 | "src/**", 26 | "bin/**", 27 | "data/**" 28 | ], 29 | "scripts": { 30 | "eslint": "eslint", 31 | "jest": "jest", 32 | "lint": "eslint .", 33 | "lint:fix": "eslint --fix .", 34 | "prepare": "husky", 35 | "test": "jest --silent --verbose", 36 | "test:watch": "jest --verbose --silent --watchAll" 37 | }, 38 | "lint-staged": { 39 | "package.json": [ 40 | "npx sort-package-json" 41 | ], 42 | "*.js": [ 43 | "eslint --fix" 44 | ] 45 | }, 46 | "dependencies": { 47 | "@laboralphy/did-you-mean": "2.3.0", 48 | "@vonage/auth": "1.12.0", 49 | "@vonage/conversations": "1.7.2", 50 | "@vonage/jwt": "1.11.0", 51 | "@vonage/server-client": "1.16.1", 52 | "@vonage/server-sdk": "3.19.2", 53 | "ajv": "8.17.1", 54 | "camelcase": "6.3.0", 55 | "chalk": "4", 56 | "compare-versions": "6.1.1", 57 | "deepmerge": "4.3.1", 58 | "easy-table": "1.2.0", 59 | "json-diff": "1.0.6", 60 | "jsonwebtoken": "9.0.2", 61 | "node-fetch": "2.7.0", 62 | "snakecase": "1.0.0", 63 | "table": "6.9.0", 64 | "winston": "3.17.0", 65 | "yaml": "2.6.1", 66 | "yargs": "17.7.2", 67 | "yargs-parser": "21.1.1" 68 | }, 69 | "devDependencies": { 70 | "@eslint/js": "9.16.0", 71 | "@faker-js/faker": "9.3.0", 72 | "@stylistic/eslint-plugin-js": "2.11.0", 73 | "eslint-plugin-jest": "28.9.0", 74 | "eslint-plugin-n": "17.14.0", 75 | "globals": "15.13.0", 76 | "husky": "9.1.7", 77 | "jest": "29.7.0", 78 | "jest-junit": "16.0.0", 79 | "lint-staged": "15.2.10", 80 | "winston-transport": "4.9.0" 81 | }, 82 | "engines": { 83 | "node": ">=18.0.0" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/aclSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "title": "Vonage JWT ACL Schema", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "required": ["paths"], 7 | "properties": { 8 | "paths": { 9 | "type": "object", 10 | "additionalProperties": false, 11 | "patternProperties": { 12 | "^/[^\\s]*$": { 13 | "type": "object", 14 | "additionalProperties": false, 15 | "properties": { 16 | "methods": { 17 | "type": "array", 18 | "items": { 19 | "type": "string", 20 | "enum": ["GET", "POST", "PUT", "DELETE", "PATCH"] 21 | } 22 | }, 23 | "filters": { 24 | "type": "object", 25 | "additionalProperties": true 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/apps/capabilities.js: -------------------------------------------------------------------------------- 1 | const camelCase = require('camelcase'); 2 | 3 | const capabilityLabels = { 4 | 'messages': 'Messages', 5 | 'networkApis': 'Network APIs', 6 | 'rtc': 'RTC', 7 | 'vbc': 'VBC', 8 | 'verify': 'Verify', 9 | 'video': 'Video', 10 | 'voice': 'Voice', 11 | }; 12 | 13 | const capabilities = Object.keys(capabilityLabels); 14 | 15 | const getAppCapabilities = ({capabilities: appCapabilities = {}} = {}) => 16 | Object.keys(appCapabilities) 17 | .filter((capability) => capabilities.includes(camelCase(capability))) 18 | .sort() 19 | .map(capability => camelCase(capability)); 20 | 21 | exports.getAppCapabilities = getAppCapabilities; 22 | 23 | exports.capabilityLabels = capabilityLabels; 24 | 25 | exports.capabilities = capabilities; 26 | -------------------------------------------------------------------------------- /src/apps/message.js: -------------------------------------------------------------------------------- 1 | const { coerceUrl } = require('../utils/coerceUrl'); 2 | const { coerceRemoveCallback } = require('../utils/coerceRemove'); 3 | 4 | const updateMessages = (app, flags) => { 5 | const newMessages = { 6 | webhooks: { 7 | inboundUrl: { 8 | address: app.capabilities?.messages?.webhooks?.inboundUrl?.address, 9 | httpMethod: app.capabilities?.messages?.webhooks?.inboundUrl?.httpMethod, 10 | }, 11 | statusUrl: { 12 | address: app.capabilities?.messages?.webhooks?.statusUrl?.address, 13 | httpMethod: app.capabilities?.messages?.webhooks?.statusUrl?.httpMethod, 14 | }, 15 | }, 16 | version: app.capabilities?.messages?.version, 17 | authenticateInboundMedia: app.capabilities?.messages?.authenticateInboundMedia, 18 | }; 19 | 20 | addInboundUrl(newMessages, flags); 21 | addStatusUrl(newMessages, flags); 22 | 23 | if (flags.messagesVersion) { 24 | newMessages.version = flags.messagesVersion; 25 | } 26 | 27 | if (flags.authenticateInboundMedia === undefined) { 28 | newMessages.authenticateInboundMedia = flags.messagesAuthenticateMedia; 29 | } 30 | 31 | app.capabilities.messages = JSON.parse(JSON.stringify(newMessages)); 32 | 33 | if (Object.keys(app.capabilities.messages).length < 1) { 34 | app.capabilities.messages = undefined; 35 | return; 36 | } 37 | 38 | if (Object.keys(app.capabilities.messages.webhooks).length < 1) { 39 | app.capabilities.messages.webhooks = undefined; 40 | } 41 | 42 | if (app.capabilities.messages.webhooks === undefined) { 43 | app.capabilities.messages = undefined; 44 | } 45 | }; 46 | 47 | const addInboundUrl = (capability, flags) => { 48 | if (flags.messagesInboundUrl === '__REMOVE__') { 49 | capability.webhooks.inboundUrl = undefined; 50 | return; 51 | } 52 | 53 | const newInboundUrl = capability.webhooks?.inboundUrl; 54 | 55 | if (flags.messagesInboundUrl && flags.messagesInboundUrl !== '__REMOVE__') { 56 | newInboundUrl.address = flags.messagesInboundUrl; 57 | newInboundUrl.httpMethod = 'POST'; 58 | } 59 | 60 | capability.webhooks.inboundUrl = JSON.parse(JSON.stringify(newInboundUrl)); 61 | 62 | if (Object.keys(capability.webhooks.inboundUrl).length < 1) { 63 | capability.webhooks.inboundUrl = undefined; 64 | } 65 | }; 66 | 67 | const addStatusUrl = (capability, flags) => { 68 | if (flags.messagesStatusUrl === '__REMOVE__') { 69 | capability.webhooks.statusUrl = undefined; 70 | return; 71 | } 72 | 73 | const newStatusUrl = capability.webhooks?.statusUrl; 74 | 75 | if (flags.messagesStatusUrl && flags.messagesStatusUrl !== '__REMOVE__') { 76 | newStatusUrl.address = flags.messagesStatusUrl; 77 | newStatusUrl.httpMethod = 'POST'; 78 | } 79 | 80 | capability.webhooks.statusUrl = JSON.parse(JSON.stringify(newStatusUrl)); 81 | 82 | if (Object.keys(capability.webhooks.statusUrl).length < 1) { 83 | capability.webhooks.statusUrl = undefined; 84 | } 85 | }; 86 | 87 | const group = 'Message Capabilities'; 88 | 89 | const messageFlags = { 90 | 'messages-inbound-url': { 91 | description: 'URL for inbound messages', 92 | type: 'string', 93 | group: group, 94 | implies: ['messages-status-url'], 95 | coerce: coerceRemoveCallback(coerceUrl('messages-inbound-url')), 96 | }, 97 | 'messages-status-url': { 98 | description: 'URL for status messages', 99 | type: 'string', 100 | group: group, 101 | implies: ['messages-inbound-url'], 102 | coerce: coerceRemoveCallback(coerceUrl('messages-status-url')), 103 | }, 104 | 'messages-version': { 105 | description: 'Version for webhook data', 106 | type: 'string', 107 | group: group, 108 | choices: ['v0.1', 'v1'], 109 | implies: ['messages-inbound-url', 'messages-status-url'], 110 | }, 111 | 'messages-authenticate-media': { 112 | description: 'Authenticate inbound media', 113 | type: 'boolean', 114 | group: group, 115 | }, 116 | }; 117 | 118 | exports.messageGroup = group; 119 | 120 | exports.messageFlags = messageFlags; 121 | 122 | exports.updateMessages = updateMessages; 123 | -------------------------------------------------------------------------------- /src/apps/network.js: -------------------------------------------------------------------------------- 1 | const { coerceUrl } = require('../utils/coerceUrl'); 2 | const { coerceRemoveCallback, coerceRemove } = require('../utils/coerceRemove'); 3 | 4 | const updateNetwork = (app, flags) => { 5 | const newNetwork = { 6 | networkApplicationId: app.capabilities?.networkApis?.networkApplicationId, 7 | redirectUrl: app.capabilities?.networkApis?.redirectUrl, 8 | }; 9 | 10 | if (flags.networkAppId) { 11 | newNetwork.networkApplicationId = flags.networkAppId; 12 | } 13 | 14 | if (flags.networkRedirectUrl) { 15 | newNetwork.redirectUrl = flags.networkRedirectUrl; 16 | } 17 | 18 | app.capabilities.networkApis = JSON.parse(JSON.stringify(newNetwork)); 19 | 20 | console.debug('Updated Network capabilities', app.capabilities.networkApis); 21 | }; 22 | 23 | const group = 'Network Capabilities'; 24 | 25 | const networkFlags = { 26 | 'network-app-id': { 27 | description: 'Network registration application ID', 28 | type: 'string', 29 | group: group, 30 | coerce: coerceRemove, 31 | }, 32 | 'network-redirect-url':{ 33 | description: 'URL to redirect to exchange code for token', 34 | coerce: coerceRemoveCallback(coerceUrl('network-redirect-url')), 35 | type: 'boolean', 36 | group: group, 37 | implies: ['network-app-id'], 38 | }, 39 | }; 40 | 41 | exports.networkGroup = group; 42 | 43 | exports.networkFlags = networkFlags; 44 | 45 | exports.updateNetwork = updateNetwork; 46 | -------------------------------------------------------------------------------- /src/apps/rtc.js: -------------------------------------------------------------------------------- 1 | const { coerceUrl } = require('../utils/coerceUrl'); 2 | const { coerceRemoveCallback, coerceRemoveList } = require('../utils/coerceRemove'); 3 | 4 | const updateRTC = (app, flags) => { 5 | const newRTC = { 6 | webhooks: { 7 | eventUrl: { 8 | address: app.capabilities?.rtc?.webhooks?.eventUrl?.address, 9 | httpMethod: app.capabilities?.rtc?.webhooks?.eventUrl?.httpMethod, 10 | }, 11 | }, 12 | signedCallbacks: app.capabilities?.rtc?.signedCallbacks, 13 | }; 14 | 15 | console.debug(`eventurl: ${flags.rtcEventUrl}`); 16 | if (flags.rtcEventUrl) { 17 | newRTC.webhooks.eventUrl.address = flags.rtcEventUrl; 18 | } 19 | 20 | if (flags.rtcEventMethod) { 21 | newRTC.webhooks.eventUrl.httpMethod = flags.rtcEventMethod; 22 | } 23 | 24 | if (flags.rtcSignedEvent) { 25 | newRTC.signedCallbacks = flags.rtcSignedEvent; 26 | } 27 | 28 | // Make sure we have a default method for the event URL 29 | if (!newRTC.webhooks.eventUrl.httpMethod && newRTC.webhooks.eventUrl.address) { 30 | newRTC.webhooks.eventUrl.httpMethod = 'POST'; 31 | } 32 | 33 | // Remove undefined values 34 | app.capabilities.rtc = JSON.parse(JSON.stringify(newRTC)); 35 | 36 | if (Object.keys(app.capabilities.rtc).length < 1) { 37 | app.capabilities.rtc = undefined; 38 | } 39 | 40 | console.debug('Updated RTC capabilities', app.capabilities.rtc); 41 | }; 42 | 43 | const group = 'RTC Capabilities'; 44 | 45 | const rtcFlags = { 46 | 'rtc-event-url':{ 47 | description: 'URL to receive RTC events', 48 | coerce: coerceRemoveCallback(coerceUrl('rct-event-url')), 49 | group: group, 50 | }, 51 | 'rtc-event-method':{ 52 | description: 'HTTP method for RTC events', 53 | coerce: coerceRemoveList('rtc-event-method', ['GET', 'POST']), 54 | group: group, 55 | }, 56 | 'rtc-signed-event':{ 57 | description: 'Used signed webhook for RTC events', 58 | type: 'boolean', 59 | group: group, 60 | }, 61 | }; 62 | 63 | exports.rtcGroup = group; 64 | 65 | exports.rtcFlags = rtcFlags; 66 | 67 | exports.updateRTC = updateRTC; 68 | -------------------------------------------------------------------------------- /src/apps/verify.js: -------------------------------------------------------------------------------- 1 | const { coerceUrl } = require('../utils/coerceUrl'); 2 | const { coerceRemoveCallback } = require('../utils/coerceRemove'); 3 | 4 | const updateVerify = (app, flags) => { 5 | if (flags.verifyStatusUrl !== undefined) { 6 | app.capabilities.verify = { 7 | version: 'v2', 8 | webhooks: { 9 | statusUrl: { 10 | address: flags.verifyStatusUrl, 11 | httpMethod: 'POST', 12 | }, 13 | }, 14 | }; 15 | } 16 | 17 | if (flags.verifyStatusUrl === '__REMOVE__') { 18 | app.capabilities.verify = undefined; 19 | } 20 | }; 21 | 22 | const group = 'Verify Capabilities'; 23 | 24 | const verifyFlags = { 25 | 'verify-status-url':{ 26 | description: 'URL for verify status messages', 27 | coerce: coerceRemoveCallback(coerceUrl('verify-status-url')), 28 | group, 29 | }, 30 | }; 31 | 32 | exports.verifyGroup = group; 33 | 34 | exports.verifyFlags = verifyFlags; 35 | 36 | exports.updateVerify = updateVerify; 37 | -------------------------------------------------------------------------------- /src/commands/apps.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | const { handler} = require('./apps/list'); 3 | const { dumpCommand } = require('../ux/dump'); 4 | 5 | exports.command = 'apps [command]'; 6 | 7 | exports.desc = 'Manage applications'; 8 | 9 | exports.builder = (yargs) => yargs.commandDir('apps') 10 | .epilogue(`When no command is given, ${dumpCommand('vonage apps')} will act the same as ${dumpCommand('vonage apps list')}. Run ${dumpCommand('vonage apps list --help')} to see options.`); 11 | 12 | exports.handler = handler; 13 | 14 | -------------------------------------------------------------------------------- /src/commands/apps/capabilities.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | exports.command = 'capabilities '; 3 | 4 | exports.desc = 'Manage application capabilities'; 5 | 6 | exports.builder = (yargs) => yargs.commandDir('capabilities'); 7 | 8 | -------------------------------------------------------------------------------- /src/commands/apps/capabilities/remove.js: -------------------------------------------------------------------------------- 1 | const { displayApplication } = require('../../../apps/display'); 2 | const { getAppCapabilities, capabilities, capabilityLabels } = require('../../../apps/capabilities'); 3 | const { makeSDKCall } = require('../../../utils/makeSDKCall'); 4 | const { apiKey, apiSecret } = require('../../../credentialFlags'); 5 | const { dumpCommand } = require('../../../ux/dump'); 6 | const { confirm } = require('../../../ux/confirm'); 7 | const { force } = require('../../../commonFlags'); 8 | 9 | exports.command = 'rm '; 10 | 11 | exports.description = 'Remove a capability from an application'; 12 | 13 | /* istanbul ignore next */ 14 | exports.builder = (yargs) => yargs 15 | .options({ 16 | 'api-key': apiKey, 17 | 'api-secret': apiSecret, 18 | 'force': force, 19 | }) 20 | .example( 21 | dumpCommand('$0 apps capabilities rm 000[...]000 network_apis'), 22 | 'Remove the network_apis capability from application', 23 | ) 24 | .positional( 25 | 'which', 26 | { 27 | describe: 'Capability to remove', 28 | choices: capabilities, 29 | }, 30 | ) 31 | .positional( 32 | 'id', 33 | { 34 | type: 'string', 35 | describe: 'The application ID', 36 | }, 37 | ); 38 | 39 | exports.handler = async (argv) => { 40 | const { SDK, id, which } = argv; 41 | console.info(`Removing ${which} capability from application ${id}`); 42 | 43 | const app = await makeSDKCall( 44 | SDK.applications.getApplication.bind(SDK.applications), 45 | 'Fetching Application', 46 | id, 47 | ); 48 | console.log(''); 49 | console.debug(`Loaded application ${app.name} (${app.id})`); 50 | console.debug(`Current capabilities: ${getAppCapabilities(app).length}`); 51 | 52 | if (getAppCapabilities(app).length < 1) { 53 | console.log('No capabilities to remove'); 54 | return; 55 | } 56 | 57 | const okToRemove = await confirm(`Remove ${capabilityLabels[which]} capability from ${app.name} (${app.id})?`); 58 | console.log(''); 59 | 60 | if (okToRemove) { 61 | app.capabilities[which === 'network_apis' ? 'networkApis' : which] = undefined; 62 | await makeSDKCall( 63 | SDK.applications.updateApplication.bind(SDK.applications), 64 | `Removing ${which} capability from application ${id}`, 65 | app, 66 | ); 67 | } 68 | 69 | console.log( okToRemove 70 | ? `Removed ${capabilityLabels[which]} capability from ${app.name}` 71 | : `Did not remove ${capabilityLabels[which]} capability from ${app.name}`, 72 | ); 73 | 74 | console.log(''); 75 | displayApplication(app); 76 | }; 77 | -------------------------------------------------------------------------------- /src/commands/apps/create.js: -------------------------------------------------------------------------------- 1 | const { Client } = require('@vonage/server-client'); 2 | const chalk = require('chalk'); 3 | const YAML = require('yaml'); 4 | const { writeFile } = require('../../utils/fs'); 5 | const { makeSDKCall } = require('../../utils/makeSDKCall'); 6 | const { displayApplication } = require('../../apps/display'); 7 | const { dumpCommand } = require('../../ux/dump'); 8 | const { coerceKey } = require('../../utils/coerceKey'); 9 | const { apiKey, apiSecret } = require('../../credentialFlags'); 10 | const { json, yaml, force } = require('../../commonFlags'); 11 | 12 | exports.command = 'create '; 13 | 14 | exports.desc = 'Create a new application'; 15 | 16 | /* istanbul ignore next */ 17 | exports.builder = (yargs) => yargs 18 | .positional( 19 | 'name', 20 | { 21 | describe: 'The name you want to give the application', 22 | }, 23 | ).options({ 24 | 'improve-ai': { 25 | describe: 'Allow Vonage to improve AI models by using your data', 26 | type: 'boolean', 27 | default: false, 28 | group: 'Create Application', 29 | }, 30 | 'private-key-file': { 31 | describe: 'Path where you want to save the private key file', 32 | default: process.cwd() + '/private.key', 33 | type: 'string', 34 | group: 'Create Application', 35 | }, 36 | 'public-key-file': { 37 | describe: 'Path to a public key file you want to use for this application', 38 | type: 'string', 39 | group: 'Create Application', 40 | coerce: coerceKey('public'), 41 | }, 42 | 'api-key': apiKey, 43 | 'api-secret': apiSecret, 44 | 'force': force, 45 | 'json': json, 46 | 'yaml': yaml, 47 | }) 48 | .example( 49 | dumpCommand('vonage apps create "My New Application"'), 50 | 'Create a new application with the name "My New Application"', 51 | ) 52 | .example( 53 | dumpCommand('vonage apps create "My New Application" --public-key=./public.key'), 54 | 'Create a new application with the name "My New Application" and a public key from ./public.key', 55 | ) 56 | .epilogue([ 57 | `After creating the application, you can use ${dumpCommand('vonage apps capability')} to manage the capabilities.`, 58 | `${chalk.bold('Note:')} The private key is only shown once and cannot be retrieved later. You will have to use ${dumpCommand('vonage apps update')} to generate a new private key.`, 59 | ].join('\n')); 60 | 61 | exports.handler = async (argv) => { 62 | console.info('Creating new application'); 63 | let dumpPrivateKey = false; 64 | const { SDK } = argv; 65 | 66 | const appData = { 67 | name: argv.name, 68 | privacy: { 69 | improveAI: argv.improveAi, 70 | }, 71 | keys: { 72 | publicKey: argv.publicKeyFile, 73 | }, 74 | }; 75 | 76 | const newApplication = await makeSDKCall( 77 | SDK.applications.createApplication.bind(SDK.applications), 78 | 'Creating Application', 79 | appData, 80 | ); 81 | 82 | try { 83 | if (argv.privateKeyFile) { 84 | process.stderr.write('Saving private key ...'); 85 | await writeFile(argv.privateKeyFile, newApplication.keys.privateKey); 86 | process.stderr.write('\rSaving private key ... Done!\n'); 87 | } 88 | } catch (error) { 89 | dumpPrivateKey = true; 90 | console.debug(error.name); 91 | switch(error.name) { 92 | case 'UserDeclinedError': 93 | process.stderr.write('\rSaving private key ... User declined\n'); 94 | break; 95 | default: 96 | process.stderr.write('\rSaving private key ... Failed\n'); 97 | console.error('Error saving private key:', error); 98 | } 99 | } 100 | 101 | if (argv.json) { 102 | console.log(JSON.stringify( 103 | Client.transformers.snakeCaseObjectKeys(newApplication, true), 104 | null, 105 | 2, 106 | )); 107 | return; 108 | } 109 | 110 | if (argv.yaml) { 111 | console.log(YAML.stringify( 112 | Client.transformers.snakeCaseObjectKeys(newApplication, true), 113 | )); 114 | return; 115 | } 116 | 117 | console.log('Application created'); 118 | displayApplication(newApplication); 119 | if (dumpPrivateKey) { 120 | console.log(''); 121 | console.log('Private key:'); 122 | console.log(newApplication.keys.privateKey); 123 | } 124 | }; 125 | -------------------------------------------------------------------------------- /src/commands/apps/delete.js: -------------------------------------------------------------------------------- 1 | const { confirm } = require('../../ux/confirm'); 2 | const { makeSDKCall } = require('../../utils/makeSDKCall'); 3 | const { force } = require('../../commonFlags'); 4 | const { apiKey, apiSecret } = require('../../credentialFlags'); 5 | const { dumpCommand } = require('../../ux/dump'); 6 | 7 | exports.command = 'delete '; 8 | 9 | exports.desc = 'Delete application'; 10 | 11 | /* istanbul ignore next */ 12 | exports.builder = (yargs) => yargs 13 | .positional( 14 | 'id', 15 | { 16 | describe: 'The ID of the application to delete', 17 | }, 18 | ).options({ 19 | 'api-key': apiKey, 20 | 'api-secret': apiSecret, 21 | force: force, 22 | }) 23 | .example( 24 | dumpCommand('vonage apps delete 000[...]000'), 25 | 'Delete application with ID 000[...]000', 26 | ); 27 | 28 | exports.handler = async (argv) => { 29 | console.info(`Deleting application: ${argv.id}`); 30 | 31 | const { SDK, id } = argv; 32 | 33 | const app = await makeSDKCall( 34 | SDK.applications.getApplication.bind(SDK.applications), 35 | 'Fetching Application', 36 | id, 37 | ); 38 | 39 | const okToDelete = await confirm(`Delete application ${app.name} (${app.id})?`); 40 | 41 | if (!okToDelete) { 42 | return; 43 | } 44 | 45 | await makeSDKCall( 46 | SDK.applications.deleteApplication.bind(SDK.applications), 47 | 'Deleting application', 48 | id, 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /src/commands/apps/numbers.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | const { handler } = require('./numbers/list'); 3 | 4 | exports.command = 'numbers '; 5 | 6 | exports.desc = 'Manage application numbers'; 7 | 8 | exports.builder = (yargs) => yargs.commandDir('numbers'); 9 | 10 | exports.handler = handler; 11 | 12 | -------------------------------------------------------------------------------- /src/commands/apps/numbers/link.js: -------------------------------------------------------------------------------- 1 | const yargs = require('yargs'); 2 | const { loadOwnedNumbersFromSDK } = require('../../../numbers/loadOwnedNumbersFromSDK'); 3 | const { makeSDKCall } = require('../../../utils/makeSDKCall'); 4 | const { confirm } = require('../../../ux/confirm'); 5 | const { displayFullNumber } = require('../../../numbers/display'); 6 | const { descriptionList } = require('../../../ux/descriptionList'); 7 | const YAML = require('yaml'); 8 | const { apiKey, apiSecret } = require('../../../credentialFlags'); 9 | const { json, yaml } = require('../../../commonFlags'); 10 | const { dumpCommand } = require('../../../ux/dump'); 11 | 12 | exports.command = 'link '; 13 | 14 | exports.desc = 'Link a number to an application'; 15 | 16 | exports.builder = (yargs) => yargs 17 | .positional( 18 | 'id', 19 | { 20 | describe: 'Application ID', 21 | }, 22 | ) 23 | .positional( 24 | 'msisdn', 25 | { 26 | describe: 'The number to link to the application', 27 | }, 28 | ) 29 | .options({ 30 | 'api-key': apiKey, 31 | 'api-secret': apiSecret, 32 | 'yaml': yaml, 33 | 'json': json, 34 | }) 35 | .example( 36 | dumpCommand('vonage apps link 000[...]000 19162255887'), 37 | 'Link number 19162255887 to application 000[...]000', 38 | ); 39 | 40 | exports.handler = async (argv) => { 41 | const { id, SDK, msisdn } = argv; 42 | console.info(`Linking number ${msisdn} to application ${id}`); 43 | 44 | const app = await makeSDKCall( 45 | SDK.applications.getApplication.bind(SDK.applications), 46 | 'Fetching Application', 47 | id, 48 | ); 49 | 50 | const numbers = await loadOwnedNumbersFromSDK( 51 | SDK, 52 | { 53 | msisdn: msisdn, 54 | }, 55 | ); 56 | 57 | const number = numbers.numbers[0]; 58 | 59 | if (!number) { 60 | console.error('Number not found'); 61 | yargs.exit(20); 62 | return; 63 | } 64 | 65 | console.debug('Current number properties:', number); 66 | 67 | let userConfirmedUpdate = true; 68 | 69 | if (number.appId && number.appId !== app.id) { 70 | console.log(''); 71 | userConfirmedUpdate = await confirm(`Number is already linked to application [${number.appId}]. Do you want to continue?`); 72 | } 73 | 74 | console.log(''); 75 | 76 | if (!userConfirmedUpdate) { 77 | console.log('Aborted'); 78 | return; 79 | } 80 | 81 | if (number.appId === app.id) { 82 | console.log('Number is already linked to this application'); 83 | console.log(''); 84 | console.log(descriptionList(displayFullNumber(number))); 85 | return; 86 | } 87 | 88 | console.info('Linking number to application'); 89 | 90 | number.appId = id; 91 | await makeSDKCall( 92 | SDK.numbers.updateNumber.bind(SDK.numbers), 93 | 'Linking number', 94 | number, 95 | ); 96 | 97 | if (argv.json) { 98 | console.log(JSON.stringify(number, null, 2)); 99 | return; 100 | } 101 | 102 | if (argv.yaml) { 103 | console.log(YAML.stringify(number, null, 2)); 104 | return; 105 | } 106 | 107 | console.log(''); 108 | console.log('Number linked'); 109 | console.log(descriptionList(displayFullNumber(number))); 110 | return; 111 | }; 112 | -------------------------------------------------------------------------------- /src/commands/apps/numbers/list.js: -------------------------------------------------------------------------------- 1 | const yargs = require('yargs'); 2 | const YAML = require('yaml'); 3 | const { displayNumbers } = require('../../../numbers/display'); 4 | const { Client } = require('@vonage/server-client'); 5 | const { dumpCommand } = require('../../../ux/dump'); 6 | const { loadOwnedNumbersFromSDK } = require('../../../numbers/loadOwnedNumbersFromSDK'); 7 | const { getAppCapabilities } = require('../../../apps/capabilities'); 8 | const { makeSDKCall } = require('../../../utils/makeSDKCall'); 9 | const { apiKey, apiSecret } = require('../../../credentialFlags'); 10 | const { yaml, json } = require('../../../commonFlags'); 11 | 12 | exports.command = 'list '; 13 | 14 | exports.desc = 'Show all numbers linked to an application'; 15 | 16 | exports.builder = (yargs) => yargs 17 | .positional( 18 | 'id', 19 | { 20 | describe: 'Application ID', 21 | }, 22 | ) 23 | .options({ 24 | 'api-key': apiKey, 25 | 'api-secret': apiSecret, 26 | 'yaml': yaml, 27 | 'json': json, 28 | 'fail': { 29 | describe: 'Fail when there are numbers linked to the application but the application does not have messages or voice capabilities', 30 | type: 'boolean', 31 | }, 32 | }) 33 | .epilogue(['The --fail flag will cause the command to exit with 15 code if the application does not have the voice or messages capability enabled.'].join('\n')); 34 | 35 | exports.handler = async (argv) => { 36 | const { id, SDK, fail } = argv; 37 | console.info(`Listing numbers linked to application ${id}`); 38 | 39 | const application = await makeSDKCall( 40 | SDK.applications.getApplication.bind(SDK.applications), 41 | 'Fetching Application', 42 | id, 43 | ); 44 | const { totalNumbers, numbers } = await loadOwnedNumbersFromSDK( 45 | SDK, 46 | { 47 | appId: id, 48 | message: `Fetching numbers linked to application ${application?.name}`, 49 | size: 100, 50 | all: true, 51 | }, 52 | ) || {}; 53 | 54 | console.debug('Numbers:', numbers); 55 | 56 | if (argv.yaml) { 57 | console.log(YAML.stringify( 58 | (numbers || []).map( 59 | (number) => Client.transformers.snakeCaseObjectKeys(number, true, false), 60 | ), 61 | null, 62 | 2, 63 | )); 64 | return; 65 | } 66 | 67 | if (argv.json) { 68 | console.log(JSON.stringify( 69 | (numbers || []).map( 70 | (number) => Client.transformers.snakeCaseObjectKeys(number, true, false), 71 | ), 72 | null, 73 | 2, 74 | )); 75 | return; 76 | } 77 | 78 | console.log(''); 79 | 80 | if (totalNumbers === 0) { 81 | console.log('No numbers linked to this application.'); 82 | console.log(''); 83 | console.log(`Use ${dumpCommand('vonage apps link')} to link a number to this application.`); 84 | return; 85 | } 86 | 87 | const appCapabilities = getAppCapabilities(application); 88 | const hasCorrectCapabilities = appCapabilities.includes('messages') || appCapabilities.includes('voice'); 89 | 90 | if (numbers.length > 0 && !hasCorrectCapabilities && !fail) { 91 | console.warn( 92 | 'This application does not have the voice or messages capability enabled', 93 | ); 94 | } 95 | 96 | console.log(totalNumbers > 1 97 | ? `There are ${totalNumbers} numbers linked:` 98 | : 'There is 1 number linked:', 99 | ); 100 | console.log(''); 101 | 102 | displayNumbers(numbers, ['type', 'feature', 'country']); 103 | 104 | if (numbers.length > 0 && !hasCorrectCapabilities && fail) { 105 | console.error( 106 | 'This application does not have the voice or messages capability enabled', 107 | ); 108 | 109 | yargs.exit(1); 110 | return; 111 | } 112 | }; 113 | 114 | -------------------------------------------------------------------------------- /src/commands/apps/numbers/unlink.js: -------------------------------------------------------------------------------- 1 | const YAML = require('yaml'); 2 | const { loadOwnedNumbersFromSDK } = require('../../../numbers/loadOwnedNumbersFromSDK'); 3 | const { makeSDKCall } = require('../../../utils/makeSDKCall'); 4 | const { confirm } = require('../../../ux/confirm'); 5 | const { displayFullNumber } = require('../../../numbers/display'); 6 | const { descriptionList } = require('../../../ux/descriptionList'); 7 | const { apiKey, apiSecret } = require('../../../credentialFlags'); 8 | const { json, yaml } = require('../../../commonFlags'); 9 | const { dumpCommand } = require('../../../ux/dump'); 10 | 11 | exports.command = 'unlink '; 12 | 13 | exports.desc = 'Unlink a number to an application'; 14 | 15 | exports.builder = (yargs) => yargs 16 | .positional( 17 | 'id', 18 | { 19 | describe: 'Application ID', 20 | }, 21 | ) 22 | .positional( 23 | 'msisdn', 24 | { 25 | describe: 'The number to unlink to the application', 26 | }, 27 | ) 28 | .options({ 29 | 'api-key': apiKey, 30 | 'api-secret': apiSecret, 31 | 'yaml': yaml, 32 | 'json': json, 33 | }) 34 | .example( 35 | dumpCommand('vonage apps unlink 000[...]000 19162255887'), 36 | 'Unlink number 19162255887 to application 000[...]000', 37 | ); 38 | 39 | exports.handler = async (argv) => { 40 | const { id, SDK, msisdn } = argv; 41 | console.info(`Unlinking number ${msisdn} to application ${id}`); 42 | 43 | const application = await makeSDKCall( 44 | SDK.applications.getApplication.bind(SDK.applications), 45 | 'Fetching Application', 46 | id, 47 | ); 48 | 49 | const numbers = await loadOwnedNumbersFromSDK( 50 | SDK, 51 | { 52 | msisdn: msisdn, 53 | }, 54 | ); 55 | 56 | const number = numbers.numbers[0]; 57 | if (!number) { 58 | console.error('Number not found'); 59 | return; 60 | } 61 | 62 | console.debug('Current number properties:', number); 63 | 64 | if (!number.appId) { 65 | console.log('Number is not linked to an application'); 66 | return; 67 | } 68 | 69 | if (number.appId !== id) { 70 | console.error('Number is not linked to this application'); 71 | return; 72 | } 73 | 74 | const userConfirmedUnlink= await confirm(`Are you sure you want to unlink ${msisdn} from ${application.name}?`); 75 | console.log(''); 76 | 77 | if (!userConfirmedUnlink) { 78 | console.log('Aborted'); 79 | return; 80 | } 81 | 82 | console.info('Unlinking number to application'); 83 | 84 | number.appId = null; 85 | 86 | // eslint-disable-next-line no-unused-vars 87 | const { appId, ...numberWithoutAppId } = number; 88 | 89 | await makeSDKCall( 90 | SDK.numbers.updateNumber.bind(SDK.numbers), 91 | 'Unlinking number', 92 | numberWithoutAppId, 93 | ); 94 | 95 | console.log(''); 96 | 97 | if (argv.json) { 98 | console.log(JSON.stringify(numberWithoutAppId, null, 2)); 99 | return; 100 | } 101 | 102 | if (argv.yaml) { 103 | console.log(YAML.stringify(numberWithoutAppId, null, 2)); 104 | return; 105 | } 106 | 107 | console.log('Number unlinked'); 108 | console.log(descriptionList(displayFullNumber(numberWithoutAppId))); 109 | }; 110 | -------------------------------------------------------------------------------- /src/commands/apps/show.js: -------------------------------------------------------------------------------- 1 | const YAML = require('yaml'); 2 | const { makeSDKCall } = require('../../utils/makeSDKCall'); 3 | const { displayApplication } = require('../../apps/display'); 4 | const { Client } = require('@vonage/server-client'); 5 | const { apiKey, apiSecret } = require('../../credentialFlags'); 6 | const { json, yaml } = require('../../commonFlags'); 7 | const { dumpCommand } = require('../../ux/dump'); 8 | 9 | exports.command = 'show '; 10 | 11 | exports.desc = 'Get information for an application'; 12 | 13 | /* istanbul ignore next */ 14 | exports.builder = (yargs) => yargs 15 | .positional( 16 | 'id', 17 | { 18 | describe: 'The ID of the application to show', 19 | }, 20 | ) 21 | .options({ 22 | 'api-key': apiKey, 23 | 'api-secret': apiSecret, 24 | 'yaml': yaml, 25 | 'json': json, 26 | }) 27 | .example( 28 | dumpCommand('vonage apps show 000[...]000'), 29 | 'Show information for application 000[...]000', 30 | ); 31 | 32 | exports.handler = async (argv) => { 33 | console.info(`Show information for application ${argv.id}`); 34 | const { SDK, id } = argv; 35 | 36 | const app = await makeSDKCall( 37 | SDK.applications.getApplication.bind(SDK.applications), 38 | 'Fetching Application', 39 | id, 40 | ); 41 | 42 | if (argv.yaml) { 43 | console.log(YAML.stringify( 44 | Client.transformers.snakeCaseObjectKeys(app, true, false), 45 | )); 46 | return; 47 | } 48 | 49 | if (argv.json) { 50 | console.log(JSON.stringify( 51 | Client.transformers.snakeCaseObjectKeys(app, true, false), 52 | null, 53 | 2, 54 | )); 55 | return; 56 | } 57 | 58 | displayApplication(app); 59 | }; 60 | -------------------------------------------------------------------------------- /src/commands/apps/update.js: -------------------------------------------------------------------------------- 1 | const yaml = require('yaml'); 2 | const { displayApplication } = require('../../apps/display'); 3 | const { makeSDKCall } = require('../../utils/makeSDKCall'); 4 | const { coerceKey } = require('../../utils/coerceKey'); 5 | const { Client } = require('@vonage/server-client'); 6 | const { apiKey, apiSecret } = require('../../credentialFlags'); 7 | const { dumpCommand } = require('../../ux/dump'); 8 | 9 | exports.command = 'update '; 10 | 11 | exports.desc = 'Update an application'; 12 | 13 | /* istanbul ignore next */ 14 | exports.builder = (yargs) => yargs 15 | .positional( 16 | 'id', 17 | { 18 | describe: 'The ID of the application to update', 19 | }, 20 | ).options({ 21 | 'name': { 22 | describe: 'The name you want to give the application', 23 | type: 'string', 24 | group: 'Update Application', 25 | }, 26 | 'improve-ai': { 27 | describe: 'Allow Vonage to improve AI models by using your data', 28 | type: 'boolean', 29 | group: 'Update Application', 30 | }, 31 | 'public-key-file': { 32 | describe: 'Path to a public key file you want to use for this application', 33 | type: 'string', 34 | group: 'Update Application', 35 | coerce: coerceKey('public'), 36 | }, 37 | 'api-key': apiKey, 38 | 'api-secret': apiSecret, 39 | }) 40 | .example( 41 | dumpCommand('vonage apps update 000[...]000 --name "New Name"'), 42 | 'Update the name of application 000[...]000', 43 | ); 44 | 45 | exports.handler = async (argv) => { 46 | console.info(`Updating application: ${argv.id}`); 47 | const { SDK, id } = argv; 48 | 49 | const app = await makeSDKCall( 50 | SDK.applications.getApplication.bind(SDK.applications), 51 | 'Fetching Application', 52 | id, 53 | ); 54 | 55 | let changed = false; 56 | 57 | if (argv.name !== undefined 58 | && argv.name !== app.name 59 | ) { 60 | console.debug('Updating name'); 61 | app.name = argv.name; 62 | changed = true; 63 | } 64 | 65 | if (argv.improveAi !== undefined 66 | && argv.improveAi !== app.privacy.improveAi 67 | ) { 68 | console.debug(`Updating improveAI from ${app.privacy.improveAi} to ${argv.improveAi}`); 69 | app.privacy.improveAi = argv.improveAi; 70 | changed = true; 71 | } 72 | 73 | if (argv.publicKeyFile !== undefined 74 | && argv.publicKeyFile !== app.keys.publicKey 75 | ) { 76 | console.debug('Updating publicKey'); 77 | app.keys.publicKey = argv.publicKeyFile; 78 | changed = true; 79 | } 80 | 81 | if (changed) { 82 | console.debug('Changes detected applying updates'); 83 | await makeSDKCall( 84 | SDK.applications.updateApplication.bind(SDK.applications), 85 | 'Updating Application', 86 | app, 87 | ); 88 | } 89 | 90 | if (argv.json) { 91 | console.log(JSON.stringify( 92 | Client.transformers.snakeCaseObjectKeys(app, true), 93 | null, 94 | 2, 95 | )); 96 | return; 97 | } 98 | 99 | if (argv.yaml) { 100 | console.log(yaml.stringify( 101 | Client.transformers.snakeCaseObjectKeys(app, true), 102 | )); 103 | return; 104 | } 105 | 106 | if (!changed) { 107 | console.log('No changes detected'); 108 | } 109 | 110 | console.log(''); 111 | displayApplication(app); 112 | }; 113 | -------------------------------------------------------------------------------- /src/commands/auth.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | const { handler } = require('./auth/show'); 3 | const { dumpCommand } = require('../ux/dump'); 4 | 5 | exports.command = 'auth [command]'; 6 | 7 | exports.description = 'Manage authentication information', 8 | 9 | exports.builder = (yargs) => yargs 10 | .commandDir('auth') 11 | .epilogue([ 12 | `When ${dumpCommand('command')} is not passed, ${dumpCommand('vonage auth')} will function the same as ${dumpCommand('vonage auth show')}.`, 13 | '', 14 | `For more information, type ${dumpCommand('vonage auth show --help')}`, 15 | ].join('\n')); 16 | 17 | exports.handler = handler; 18 | -------------------------------------------------------------------------------- /src/commands/auth/check.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const yargs = require('yargs'); 3 | const { dumpCommand } = require('../../ux/dump'); 4 | const { validateApiKeyAndSecret, validatePrivateKeyAndAppId } = require('../../utils/validateSDKAuth'); 5 | const { dumpAuth } = require('../../ux/dumpAuth'); 6 | const { errorNoConfig } = require('../../middleware/config'); 7 | 8 | exports.command = 'check'; 9 | 10 | exports.description = 'Checks Vonage credentials'; 11 | 12 | exports.builder = (yargs) => yargs.options({ 13 | 'local': { 14 | describe: 'Use local configuration', 15 | type: 'boolean', 16 | default: false, 17 | }, 18 | }) 19 | .example( 20 | dumpCommand('$0 auth check'), 21 | 'Check the global configuration', 22 | ) 23 | .example( 24 | dumpCommand('$0 auth check --local'), 25 | 'Check the local configuration', 26 | ) 27 | .epilogue([`By default, the global configuration is checked. Use the ${dumpCommand('--local')} flag to check the local configuration.`].join('\n')); 28 | 29 | exports.handler = async (argv) => { 30 | console.info('Displaying auth information'); 31 | 32 | // start with global 33 | let configFile = `Global credentials found at: ${argv.config.globalConfigFile}`; 34 | let configExists = argv.config.globalConfigExists; 35 | let configOk = true; 36 | let config = argv.config.global; 37 | 38 | // then CLI arguments 39 | if (argv.config.cli.apiKey 40 | || argv.config.cli.apiSecret 41 | || argv.config.cli.appId 42 | || argv.config.cli.privateKey 43 | ) { 44 | console.debug('CLI Arguments found'); 45 | configFile = 'CLI arguments'; 46 | config = argv.config.cli; 47 | configExists = true; 48 | } 49 | 50 | // finally local 51 | if (argv.local) { 52 | console.debug('Using local configuration'); 53 | configExists = argv.config.localConfigExists; 54 | configFile = `Local credentials found at: ${argv.config.localConfigFile}`; 55 | config = argv.config.local; 56 | } 57 | 58 | if (!configExists) { 59 | console.debug('No configuration found'); 60 | errorNoConfig(argv.local); 61 | return; 62 | } 63 | 64 | const validPrivateKey = config.privateKey && config.privateKey.startsWith('-----BEGIN PRIVATE KEY'); 65 | 66 | if (config.privateKey && !validPrivateKey) { 67 | console.debug('Private key is not a valid private key'); 68 | config.privateKey = 'INVALID PRIVATE KEY'; 69 | configOk = false; 70 | } 71 | 72 | console.log(configFile); 73 | console.log(''); 74 | dumpAuth(config, argv.showAll); 75 | console.log(''); 76 | 77 | configOk = await validateApiKeyAndSecret( 78 | config.apiKey, 79 | config.apiSecret, 80 | ) && configOk; 81 | 82 | if (config.appId && config.privateKey && validPrivateKey) { 83 | configOk = await validatePrivateKeyAndAppId( 84 | config.apiKey, 85 | config.apiSecret, 86 | config.appId, 87 | config.privateKey, 88 | ) && configOk; 89 | } else { 90 | console.log(`Checking App ID and Private Key: ... ${chalk.dim('skipped')}`); 91 | } 92 | 93 | if (!configOk) { 94 | console.error('Configuration is not valid'); 95 | yargs.exit(validPrivateKey ? 5 : 22); 96 | } 97 | }; 98 | 99 | -------------------------------------------------------------------------------- /src/commands/auth/set.js: -------------------------------------------------------------------------------- 1 | const yargs = require('yargs'); 2 | const { dumpAuth } = require('../../ux/dumpAuth'); 3 | const { validateApiKeyAndSecret, validatePrivateKeyAndAppId } = require('../../utils/validateSDKAuth'); 4 | const { writeJSONFile, createDirectory } = require('../../utils/fs'); 5 | const { apiKey, apiSecret, appId, privateKey } = require('../../credentialFlags'); 6 | 7 | const setApiKeyAndSecret = async (apiKey, apiSecret) => { 8 | const valid = await validateApiKeyAndSecret(apiKey, apiSecret); 9 | return valid ? { 'api-key': apiKey, 'api-secret': apiSecret } : false; 10 | }; 11 | 12 | const setAppIdAndPrivateKey = async (apiKey, apiSecret, appId, privateKey) => { 13 | if (!appId || !privateKey) { 14 | console.debug('App ID and Private Key are required'); 15 | return {}; 16 | } 17 | 18 | const valid = await validatePrivateKeyAndAppId( 19 | apiKey, 20 | apiSecret, 21 | appId, 22 | privateKey, 23 | ); 24 | return valid ? { 'app-id': appId, 'private-key': privateKey} : false; 25 | }; 26 | 27 | exports.command = 'set'; 28 | 29 | exports.description = 'Set authentication information'; 30 | 31 | exports.builder = (yargs) => yargs.options({ 32 | 'local': { 33 | describe: 'Save local configuration only', 34 | type: 'boolean', 35 | }, 36 | }) 37 | .options({ 38 | 'app-id': appId, 39 | 'private-key': privateKey, 40 | 'api-key': apiKey, 41 | 'api-secret': apiSecret, 42 | }) 43 | .demandOption(['api-key', 'api-secret']); 44 | 45 | exports.handler = async (argv) => { 46 | const apiKeySecret = await setApiKeyAndSecret( 47 | argv.config.cli.apiKey, 48 | argv.config.cli.apiSecret, 49 | ); 50 | 51 | if (apiKeySecret === false) { 52 | console.error('Invalid API Key or Secret'); 53 | yargs.exit(5); 54 | return; 55 | } 56 | 57 | const appIdPrivateKey = await setAppIdAndPrivateKey( 58 | argv.config.cli.apiKey, 59 | argv.config.cli.apiSecret, 60 | argv.config.cli.appId, 61 | argv.config.cli.privateKey, 62 | ); 63 | 64 | if (appIdPrivateKey === false) { 65 | console.error('Invalid App ID or Private Key'); 66 | yargs.exit(5); 67 | return; 68 | } 69 | 70 | const newAuthInformation = { 71 | ...apiKeySecret, 72 | ...appIdPrivateKey, 73 | }; 74 | 75 | console.debug('New auth information:', newAuthInformation); 76 | const configPath = argv.local 77 | ? argv.config.localConfigPath 78 | : argv.config.globalConfigPath; 79 | 80 | console.debug(`Config path: ${configPath}`); 81 | 82 | if (!argv.local && createDirectory(configPath) === false) { 83 | return; 84 | } 85 | 86 | const configFile = argv.local 87 | ? argv.config.localConfigFile 88 | : argv.config.globalConfigFile; 89 | 90 | try { 91 | await writeJSONFile( 92 | configFile, 93 | newAuthInformation, 94 | `Configuration file ${configFile} already exists. Overwrite?`, 95 | ); 96 | 97 | console.log(''); 98 | dumpAuth(newAuthInformation); 99 | } catch (error) { 100 | console.error('Failed to save new configuration', error); 101 | } 102 | }; 103 | -------------------------------------------------------------------------------- /src/commands/balance.js: -------------------------------------------------------------------------------- 1 | const YAML = require('yaml'); 2 | const { apiKey, apiSecret } = require('../credentialFlags'); 3 | const { json, yaml } = require('../commonFlags'); 4 | const { makeSDKCall } = require('../utils/makeSDKCall'); 5 | const { dumpCommand } = require('../ux/dump'); 6 | const { dumpYesNo } = require('../ux/dumpYesNo'); 7 | const { displayCurrency } = require('../ux/currency'); 8 | const { descriptionList } = require('../ux/descriptionList'); 9 | const { Client } = require('@vonage/server-client'); 10 | 11 | exports.command = 'balance'; 12 | 13 | exports.desc = 'Check your account balance'; 14 | 15 | exports.builder = (yargs) => yargs 16 | .options({ 17 | 'api-key': apiKey, 18 | 'api-secret': apiSecret, 19 | 'yaml': yaml, 20 | 'json': json, 21 | }) 22 | .example( 23 | dumpCommand('vonage balance'), 24 | 'Show your account balance', 25 | ); 26 | 27 | exports.handler = async (argv) => { 28 | const { SDK, yaml, json } = argv; 29 | console.info('Check your account balance'); 30 | 31 | // Get the account balance 32 | const balance = await makeSDKCall( 33 | SDK.accounts.getBalance.bind(SDK.accounts), 34 | 'Checking account balance', 35 | ); 36 | 37 | if (yaml) { 38 | console.log(YAML.stringify( 39 | Client.transformers.snakeCaseObjectKeys(balance, true, false), 40 | )); 41 | return; 42 | } 43 | 44 | if (json) { 45 | console.log(JSON.stringify( 46 | Client.transformers.snakeCaseObjectKeys(balance, true, false), 47 | null, 48 | 2, 49 | )); 50 | return; 51 | } 52 | 53 | console.log(''); 54 | console.log(descriptionList({ 55 | 'Account balance': displayCurrency(balance.value, balance.currency), 56 | 'Auto-refill enabled': dumpYesNo(balance.autoReload), 57 | })); 58 | 59 | }; 60 | -------------------------------------------------------------------------------- /src/commands/conversations.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | const { handler} = require('./conversations/list'); 3 | const { dumpCommand } = require('../ux/dump'); 4 | 5 | exports.command = 'conversations [command]'; 6 | 7 | exports.desc = 'Manage conversations'; 8 | 9 | exports.builder = (yargs) => yargs.commandDir('conversations') 10 | .epilogue(`When no command is given, ${dumpCommand('vonage conversations')} will act the same as ${dumpCommand('vonage conversations list')}. Run ${dumpCommand('vonage conversations list --help')} to see options.`); 11 | 12 | exports.handler = handler; 13 | -------------------------------------------------------------------------------- /src/commands/conversations/delete.js: -------------------------------------------------------------------------------- 1 | const { confirm } = require('../../ux/confirm'); 2 | const { appId, privateKey } = require('../../credentialFlags'); 3 | const { force } = require('../../commonFlags'); 4 | const { makeSDKCall } = require('../../utils/makeSDKCall'); 5 | 6 | exports.command = 'delete '; 7 | 8 | exports.desc = 'Delete conversation'; 9 | 10 | exports.builder = (yargs) => yargs 11 | .positional( 12 | 'id', 13 | { 14 | describe: 'The conversation ID', 15 | }) 16 | .options({ 17 | 'force': force, 18 | 'app-id': appId, 19 | 'private-key': privateKey, 20 | }); 21 | 22 | exports.handler = async (argv) => { 23 | const { SDK, id } = argv; 24 | console.info('Deleting conversation'); 25 | 26 | const conversation = await makeSDKCall( 27 | SDK.conversations.getConversation.bind(SDK.conversations), 28 | 'Fetching conversation', 29 | id, 30 | ); 31 | console.debug('Conversation to delete', conversation); 32 | 33 | if (!await confirm('Are you sure you want to delete this conversation?')) { 34 | console.log('Conversation not deleted'); 35 | return; 36 | } 37 | 38 | 39 | await makeSDKCall( 40 | SDK.conversations.deleteConversation.bind(SDK.conversations), 41 | 'Deleting conversation', 42 | conversation.id, 43 | ); 44 | 45 | console.log(''); 46 | console.log('Conversation deleted'); 47 | }; 48 | -------------------------------------------------------------------------------- /src/commands/conversations/list.js: -------------------------------------------------------------------------------- 1 | const { spinner } = require('../../ux/spinner'); 2 | const { appId, privateKey } = require('../../credentialFlags'); 3 | const { confirm } = require('../../ux/confirm'); 4 | const { conversationSummary } = require('../../conversations/display'); 5 | const { sdkError } = require('../../utils/sdkError'); 6 | 7 | exports.command = 'list'; 8 | 9 | exports.desc = 'List conversations'; 10 | 11 | exports.builder = (yargs) => yargs.options({ 12 | 'page-size': { 13 | describe: 'Number of conversations to return per page', 14 | default: 1, 15 | }, 16 | 'cursor': { 17 | describe: 'Cursor for the next page', 18 | }, 19 | 'app-id': appId, 20 | 'private-key': privateKey, 21 | }); 22 | 23 | exports.handler = async (argv) => { 24 | const { SDK, pageSize, cursor } = argv; 25 | console.info('List conversations'); 26 | let pageCursor = cursor; 27 | let okToPage = false; 28 | 29 | do { 30 | console.debug(`Fetching conversations with cursor: ${pageCursor}`); 31 | const { stop, fail } = spinner({ 32 | message: !okToPage 33 | ? 'Fetching conversations' 34 | : 'Fetching more conversations', 35 | }); 36 | let response; 37 | try { 38 | response = await SDK.conversations.getConversationPage({ 39 | pageSize: pageSize, 40 | cursor: pageCursor, 41 | }); 42 | 43 | stop(); 44 | } catch (error) { 45 | fail(); 46 | sdkError(error); 47 | return; 48 | } 49 | 50 | console.log(''); 51 | console.table([...response.conversations].map(conversationSummary)); 52 | 53 | pageCursor = response.links?.next?.href 54 | ? new URL(response.links.next.href).searchParams.get('cursor') 55 | : null; 56 | 57 | console.debug(`Next cursor: ${pageCursor}`); 58 | 59 | if (pageCursor !== null) { 60 | okToPage = await confirm('There are more conversations. Do you want to continue?'); 61 | } 62 | } while (okToPage && pageCursor !== null); 63 | 64 | console.log('Done Listing conversations'); 65 | }; 66 | -------------------------------------------------------------------------------- /src/commands/conversations/show.js: -------------------------------------------------------------------------------- 1 | const { appId, privateKey } = require('../../credentialFlags'); 2 | const { makeSDKCall } = require('../../utils/makeSDKCall'); 3 | const { displayConversation } = require('../../conversations/display'); 4 | const { conversationIdFlag } = require('../../conversations/conversationFlags'); 5 | 6 | exports.command = 'show '; 7 | 8 | exports.desc = 'Show conversation'; 9 | 10 | exports.builder = (yargs) => yargs 11 | .positional( 12 | 'conversation-id', 13 | conversationIdFlag, 14 | ) 15 | .options({ 16 | 'app-id': appId, 17 | 'private-key': privateKey, 18 | }); 19 | 20 | exports.handler = async (argv) => { 21 | const { SDK, conversationId } = argv; 22 | console.info('Showing conversation details'); 23 | 24 | const conversation = await makeSDKCall( 25 | SDK.conversations.getConversation.bind(SDK.conversations), 26 | 'Fetching conversation', 27 | conversationId, 28 | ); 29 | 30 | if (!conversation) { 31 | console.error('No conversation found'); 32 | return; 33 | } 34 | 35 | console.log(''); 36 | displayConversation(conversation); 37 | }; 38 | -------------------------------------------------------------------------------- /src/commands/conversations/update.js: -------------------------------------------------------------------------------- 1 | const merge = require('deepmerge'); 2 | const { conversationFlags, validateEvents } = require('./create'); 3 | const { appId, privateKey } = require('../../credentialFlags'); 4 | const { force } = require('../../commonFlags'); 5 | const { displayConversation } = require('../../conversations/display'); 6 | const { makeSDKCall } = require('../../utils/makeSDKCall'); 7 | const yargs = require('yargs'); 8 | 9 | exports.command = 'update '; 10 | 11 | exports.desc = 'Update conversation'; 12 | 13 | /* istanbul ignore next */ 14 | exports.builder = (yargs) => yargs 15 | .positional( 16 | 'id', 17 | { 18 | describe: 'The id of the conversation to update', 19 | }, 20 | ) 21 | .options({ 22 | ...conversationFlags, 23 | 'display-name': { 24 | ...conversationFlags['display-name'], 25 | }, 26 | 'image-url': { 27 | ...conversationFlags['image-url'], 28 | coerce: conversationFlags['image-url'].coerce, 29 | }, 30 | 'ttl': { 31 | ...conversationFlags['ttl'], 32 | }, 33 | 'custom-data': { 34 | ...conversationFlags['custom-data'], 35 | coerce: conversationFlags['custom-data'].coerce, 36 | }, 37 | 'callback-url': { 38 | ...conversationFlags['callback-url'], 39 | coerce: conversationFlags['callback-url'].coerce, 40 | }, 41 | 'callback-event-mask': { 42 | ...conversationFlags['callback-event-mask'], 43 | }, 44 | 'callback-application-id': { 45 | ...conversationFlags['callback-application-id'], 46 | }, 47 | 'callback-ncco-url': { 48 | ...conversationFlags['callback-ncco-url'], 49 | coerce: conversationFlags['callback-ncco-url'].coerce, 50 | }, 51 | 'app-id': appId, 52 | 'private-key': privateKey, 53 | 'force': force, 54 | }); 55 | 56 | const updateCallback = ({ 57 | callbackEventMask, 58 | callbackUrl, 59 | callbackMethod, 60 | ...rest 61 | }) => { 62 | const callback = JSON.parse(JSON.stringify({ 63 | eventMask: callbackEventMask?.join(','), 64 | method: callbackMethod, 65 | url: callbackUrl, 66 | params: updateParams(rest), 67 | })); 68 | 69 | return Object.keys(callback).length > 0 ? callback : undefined ; 70 | }; 71 | 72 | const updateParams = ({ 73 | callbackApplicationId, 74 | callbackNccoUrl, 75 | }) => { 76 | const params = JSON.parse(JSON.stringify({ 77 | applicationId: callbackApplicationId, 78 | nccoUrl: callbackNccoUrl, 79 | })); 80 | 81 | return Object.keys(params).length > 0 ? params : undefined ; 82 | }; 83 | 84 | exports.handler = async (argv) => { 85 | console.info('Updating conversation'); 86 | const { SDK, callbackEventMask } = argv; 87 | 88 | if (!await validateEvents(callbackEventMask)) { 89 | console.log('Aborting'); 90 | yargs.exit(1); 91 | return; 92 | } 93 | const conversation = await makeSDKCall( 94 | SDK.conversations.getConversation.bind(SDK.conversations), 95 | 'Fetching conversation', 96 | argv.id, 97 | ); 98 | 99 | const conversationToUpdate = { 100 | id: conversation.id, 101 | displayName: argv.displayName || conversation.displayName, 102 | name: argv.name || conversation.name, 103 | imageUrl: argv.imageUrl || conversation.imageUrl, 104 | properties: merge(conversation.properties, { 105 | ttl: argv.ttl || conversation.properties?.ttl, 106 | customData: argv.customData || conversation.properties?.customData, 107 | }), 108 | callback: updateCallback(argv), 109 | }; 110 | 111 | console.debug('Updated conversation', conversationToUpdate); 112 | const updatedConversation = makeSDKCall( 113 | SDK.conversations.updateConversation.bind(SDK.conversations), 114 | 'Updating conversation', 115 | conversationToUpdate, 116 | ); 117 | 118 | console.log(''); 119 | displayConversation({ 120 | ...conversation, 121 | ...updatedConversation, 122 | }); 123 | }; 124 | -------------------------------------------------------------------------------- /src/commands/jwt.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | exports.command = 'jwt '; 3 | 4 | exports.desc = 'Manage JWT tokens'; 5 | 6 | exports.builder = (yargs) => yargs.commandDir('jwt'); 7 | 8 | exports.handler = () => {}; 9 | -------------------------------------------------------------------------------- /src/commands/jwt/create.js: -------------------------------------------------------------------------------- 1 | const { tokenGenerate } = require('@vonage/jwt'); 2 | const { dumpCommand } = require('../../ux/dump'); 3 | const { coerceJSON } = require('../../utils/coerceJSON'); 4 | const schema = require('../../aclSchema.json'); 5 | const { appId, privateKey } = require('../../credentialFlags'); 6 | 7 | const jwtFlags = { 8 | exp: { 9 | number: true, 10 | describe: 'The timestamp the token expires', 11 | group: 'JWT Options:', 12 | coerce: (arg) => parseInt(arg, 10), 13 | }, 14 | ttl: { 15 | number: true, 16 | describe: 'The time to live in seconds', 17 | group: 'JWT Options:', 18 | coerce: (arg) => parseInt(arg, 10), 19 | }, 20 | sub: { 21 | string: true, 22 | group: 'JWT Options:', 23 | describe: 'The subject of the token', 24 | }, 25 | acl: { 26 | string: true, 27 | group: 'JWT Options:', 28 | describe: 'The access control list for the token', 29 | coerce: coerceJSON('ACL', schema), 30 | }, 31 | 'app-id': appId, 32 | 'private-key': privateKey, 33 | }; 34 | 35 | exports.jwtFlags = jwtFlags; 36 | 37 | exports.command = 'create'; 38 | 39 | exports.description = 'Create a JWT token for authentication'; 40 | 41 | exports.builder = (yargs) => yargs.options(jwtFlags) 42 | .example( 43 | dumpCommand('$0 jwt create'), 44 | 'Create a token using the configured private key and application id', 45 | ) 46 | .example( 47 | dumpCommand('$0 jwt create --exp 3600 --ttl 600 --sub my-subject'), 48 | 'Create a token with a 1 hour expiry, 10 minute TTL and subject "my-subject"', 49 | ) 50 | .example( 51 | dumpCommand('$0 jwt create --app-id 000[...]000 --private-key ./path/to/private.key'), 52 | 'Create a token with a different application id and private key', 53 | ) 54 | .epilogue([ 55 | '', 56 | 'By default, the private key and application id from the config will be used.', 57 | `Use ${dumpCommand('vonage auth show')} check what those values are.`, 58 | '', 59 | `If you want to create a token with a different private key or application id, you can use the ${dumpCommand('--private-key')} and ${dumpCommand('--app-id')} flags to overwrite.`, 60 | ].join('\n')); 61 | 62 | exports.handler = (argv) => { 63 | console.info('Creating JWT token'); 64 | 65 | console.debug(`App ID: ${argv.appId}`); 66 | console.debug(`Expiry: ${argv.exp}`); 67 | console.debug(`TTL: ${argv.ttl}`); 68 | console.debug(`Subject: ${argv.sub}`); 69 | console.debug(`ACL: ${argv.acl}`); 70 | console.debug(`Claims: ${argv.claim}`); 71 | 72 | const token = tokenGenerate( 73 | argv.appId, 74 | argv.privateKey, 75 | { 76 | acl: argv.acl, 77 | exp: argv.exp, 78 | ttl: argv.ttl, 79 | sub: argv.sub, 80 | }, 81 | ); 82 | 83 | console.log(token); 84 | }; 85 | -------------------------------------------------------------------------------- /src/commands/members.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | const { handler} = require('./members/list'); 3 | const { dumpCommand } = require('../ux/dump'); 4 | 5 | exports.command = 'members [command]'; 6 | 7 | exports.desc = 'Manage applications'; 8 | 9 | exports.builder = (yargs) => yargs.commandDir('members') 10 | .epilogue(`When no command is given, ${dumpCommand('vonage members')} will act the same as ${dumpCommand('vonage members list')}. Run ${dumpCommand('vonage members list --help')} to see options.`); 11 | 12 | exports.handler = handler; 13 | -------------------------------------------------------------------------------- /src/commands/members/list.js: -------------------------------------------------------------------------------- 1 | const { conversationIdFlag } = require('../../conversations/conversationFlags'); 2 | const { appId, privateKey } = require('../../credentialFlags'); 3 | const { confirm } = require('../../ux/confirm'); 4 | const { memberSummary } = require('../../members/display'); 5 | const { makeSDKCall } = require('../../utils/makeSDKCall'); 6 | 7 | exports.command = 'list '; 8 | 9 | exports.desc = 'List members'; 10 | 11 | /* istanbul ignore next */ 12 | exports.builder = (yargs) => yargs 13 | .positional( 14 | 'conversation-id', 15 | conversationIdFlag, 16 | ) 17 | .options({ 18 | 'page-size': { 19 | describe: 'Number of members to return per page', 20 | default: 100, 21 | }, 22 | 'cursor': { 23 | describe: 'Cursor for the next page', 24 | }, 25 | 'order': { 26 | describe: 'Return the members in ascending or descending order.', 27 | choices: ['asc', 'desc'], 28 | }, 29 | 'app-id': appId, 30 | 'private-key': privateKey, 31 | }); 32 | 33 | exports.handler = async (argv) => { 34 | console.info('List members'); 35 | const { SDK, pageSize, cursor, conversationId } = argv; 36 | 37 | let pageCursor = cursor; 38 | let okToPage = false; 39 | 40 | do { 41 | console.debug(`Fetching members for conversation ${conversationId} with cursor: ${pageCursor}`); 42 | const response = await makeSDKCall( 43 | SDK.conversations.getMemberPage.bind(SDK.conversations), 44 | !okToPage 45 | ? 'Fetching members' 46 | : 'Fetching more members', 47 | conversationId, 48 | { 49 | pageSize: pageSize, 50 | cursor: pageCursor, 51 | }, 52 | ); 53 | 54 | console.debug('Members fetched', response); 55 | console.debug('Members', response); 56 | const membersToDisplay = response.members.map(memberSummary); 57 | 58 | if (membersToDisplay.length === 0) { 59 | console.log('No members found for this conversation.'); 60 | return; 61 | } 62 | 63 | console.table(membersToDisplay); 64 | 65 | pageCursor = response.links?.next?.href 66 | ? new URL(response.links.next.href).searchParams.get('cursor') 67 | : null; 68 | 69 | console.debug(`Next cursor: ${pageCursor}`); 70 | 71 | if (pageCursor !== null) { 72 | okToPage = await confirm('There are more members. Do you want to continue?'); 73 | } 74 | } while (okToPage && pageCursor !== null); 75 | }; 76 | -------------------------------------------------------------------------------- /src/commands/members/show.js: -------------------------------------------------------------------------------- 1 | const { appId, privateKey } = require('../../credentialFlags'); 2 | const { conversationIdFlag } = require('../../conversations/conversationFlags'); 3 | const { displayFullMember } = require('../../members/display'); 4 | const { json, yaml } = require('../../commonFlags'); 5 | const YAML = require('yaml'); 6 | const { Client } = require('@vonage/server-client'); 7 | const { makeSDKCall } = require('../../utils/makeSDKCall'); 8 | 9 | exports.command = 'show '; 10 | 11 | exports.desc = 'Show a member. "me" is not supported as the CLI will automatically generate the JWT token. '; 12 | 13 | /* istanbul ignore next */ 14 | exports.builder = (yargs) => yargs 15 | .positional( 16 | 'conversation-id', 17 | conversationIdFlag, 18 | ) 19 | .positional( 20 | 'member-id', 21 | { 22 | describe: 'Member ID', 23 | type: 'string', 24 | }, 25 | ) 26 | .options({ 27 | 'json': json, 28 | 'yaml': yaml, 29 | 'app-id': appId, 30 | 'private-key': privateKey, 31 | }); 32 | 33 | exports.handler = async (argv) => { 34 | console.info('Show member'); 35 | const { SDK, conversationId, memberId } = argv; 36 | 37 | const member = await makeSDKCall( 38 | SDK.conversations.getMember.bind(SDK.conversations), 39 | 'Fetching member', 40 | conversationId, 41 | memberId, 42 | ); 43 | 44 | if (argv.json) { 45 | console.log(JSON.stringify( 46 | Client.transformers.snakeCaseObjectKeys(member, true), 47 | null, 48 | 2, 49 | )); 50 | return; 51 | } 52 | 53 | if (argv.yaml) { 54 | console.log(YAML.stringify( 55 | Client.transformers.snakeCaseObjectKeys(member, true), 56 | null, 57 | 2, 58 | )); 59 | return; 60 | } 61 | 62 | displayFullMember(member); 63 | }; 64 | -------------------------------------------------------------------------------- /src/commands/members/update.js: -------------------------------------------------------------------------------- 1 | const YAML = require('yaml'); 2 | const { appId, privateKey } = require('../../credentialFlags'); 3 | const { conversationIdFlag } = require('../../conversations/conversationFlags'); 4 | const { yaml, json, force } = require('../../commonFlags'); 5 | const { displayFullMember } = require('../../members/display'); 6 | const { Client } = require('@vonage/server-client'); 7 | const { makeSDKCall } = require('../../utils/makeSDKCall'); 8 | 9 | exports.command = 'update '; 10 | 11 | exports.desc = 'Update a member'; 12 | 13 | exports.builder = (yargs) => yargs 14 | .positional( 15 | 'conversation-id', 16 | conversationIdFlag, 17 | ) 18 | .positional( 19 | 'member-id', 20 | { 21 | describe: 'Member ID', 22 | type: 'string', 23 | }, 24 | ) 25 | .options({ 26 | 'state:': { 27 | describe: 'Member state', 28 | type: 'string', 29 | choices: ['joined', 'invited'], 30 | group: 'Member', 31 | }, 32 | 'from': { 33 | describe: 'The user ID of the member that is causing this update.', 34 | type: 'string', 35 | group: 'Member Channel', 36 | }, 37 | 'reason-code': { 38 | describe: 'The reason code for the update', 39 | type: 'string', 40 | group: 'Member', 41 | }, 42 | 'reason-text': { 43 | describe: 'The reason text for the update', 44 | type: 'string', 45 | group: 'Member', 46 | }, 47 | 'yaml': yaml, 48 | 'json': json, 49 | 'force': force, 50 | 'app-id': appId, 51 | 'private-key': privateKey, 52 | }); 53 | 54 | exports.handler = async (argv) => { 55 | console.info('Update member'); 56 | const { SDK, conversationId, memberId } = argv; 57 | 58 | await makeSDKCall( 59 | SDK.conversations.getMember.bind(SDK.conversations), 60 | 'Fetching member', 61 | conversationId, 62 | memberId, 63 | ); 64 | 65 | const updatedMember = await makeSDKCall( 66 | SDK.conversations.updateMember.bind(SDK.conversations), 67 | 'Updating member', 68 | conversationId, 69 | memberId, 70 | JSON.parse(JSON.stringify({ 71 | state: argv.state, 72 | from: argv.from, 73 | reason: { 74 | code: argv.reasonCode, 75 | text: argv.reasonText, 76 | }, 77 | })), 78 | ); 79 | 80 | console.debug('Updated member', updatedMember); 81 | 82 | if (argv.json) { 83 | console.log(JSON.stringify( 84 | Client.transformers.snakeCaseObjectKeys(updatedMember, true), 85 | null, 86 | 2, 87 | )); 88 | return; 89 | } 90 | 91 | if (argv.yaml) { 92 | console.log(YAML.stringify( 93 | Client.transformers.snakeCaseObjectKeys(updatedMember, true), 94 | null, 95 | 2, 96 | )); 97 | return; 98 | } 99 | 100 | console.log(''); 101 | console.log(''); 102 | displayFullMember(updatedMember); 103 | }; 104 | -------------------------------------------------------------------------------- /src/commands/numbers.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | const { handler} = require('./numbers/list'); 3 | const { dumpCommand } = require('../ux/dump'); 4 | 5 | exports.command = 'numbers [command]'; 6 | 7 | exports.desc = 'Manage numbers'; 8 | 9 | exports.builder = (yargs) => yargs.commandDir('numbers') 10 | .epilogue(`When no command is given, ${dumpCommand('vonage numbers')} will act the same as ${dumpCommand('vonage numbers list')}. Run ${dumpCommand('vonage numbers list --help')} to see options.`); 11 | 12 | exports.handler = handler; 13 | -------------------------------------------------------------------------------- /src/commands/numbers/buy.js: -------------------------------------------------------------------------------- 1 | const YAML = require('yaml'); 2 | const yargs = require('yargs'); 3 | const { confirm } = require('../../ux/confirm'); 4 | const { descriptionList } = require('../../ux/descriptionList'); 5 | const { displayCurrency } = require('../../ux/currency'); 6 | const { displayFullNumber } = require('../../numbers/display'); 7 | const { Client } = require('@vonage/server-client'); 8 | const { dumpCommand } = require('../../ux/dump'); 9 | const { makeSDKCall } = require('../../utils/makeSDKCall'); 10 | const { apiKey, apiSecret } = require('../../credentialFlags'); 11 | const { yaml, json, force } = require('../../commonFlags'); 12 | const { countryFlag } = require('../../utils/countries'); 13 | 14 | const flags = { 15 | 'target-api-key': { 16 | describe: 'Purchase this number for a sub account', 17 | group: 'Numbers', 18 | }, 19 | 'api-key': apiKey, 20 | 'api-secret': apiSecret, 21 | 'force': force, 22 | 'yaml': yaml, 23 | 'json': json, 24 | }; 25 | 26 | exports.flags = flags; 27 | 28 | exports.command = 'buy '; 29 | 30 | exports.desc = 'Purchase a number'; 31 | 32 | exports.builder = (yargs) => yargs 33 | .positional( 34 | 'country', 35 | { 36 | ...countryFlag, 37 | group: 'Numbers', 38 | }, 39 | ) 40 | .positional( 41 | 'msisdn', 42 | { 43 | describe: 'The number you want to purchase', 44 | type: 'string', 45 | group: 'Numbers', 46 | }, 47 | ) 48 | .options(flags) 49 | .epilogue(`To search for a number to purchase, use ${dumpCommand('vonage apps search')}.`); 50 | 51 | 52 | exports.handler = async (argv) => { 53 | const { SDK, country, msisdn} = argv; 54 | console.info('Purchase a number'); 55 | 56 | const { numbers } = await makeSDKCall( 57 | SDK.numbers.getAvailableNumbers.bind(SDK.numbers), 58 | 'Searching for numbers', 59 | { 60 | pattern: msisdn, 61 | searchPattern: 1, 62 | country: country, 63 | size: 1, 64 | }, 65 | ); 66 | 67 | const numberToPurchase = numbers ? numbers[0] : null; 68 | console.debug('Nubmer to purchase:', numberToPurchase); 69 | 70 | if (!numberToPurchase) { 71 | console.log(`The number ${msisdn} is no longer available for purchase`); 72 | yargs.exit(44); 73 | return; 74 | } 75 | 76 | const okToBuy = await confirm(`Are you sure you want to purchase the number ${numberToPurchase.msisdn} for ${displayCurrency(numberToPurchase.cost)}?`); 77 | 78 | if (!okToBuy) { 79 | console.log('Aborting purchase'); 80 | return; 81 | } 82 | 83 | console.log(''); 84 | 85 | await makeSDKCall( 86 | SDK.numbers.buyNumber.bind(SDK.numbers), 87 | 'Purchasing number', 88 | { 89 | country: country, 90 | msisdn: msisdn, 91 | }, 92 | ); 93 | 94 | if (argv.yaml) { 95 | console.log(YAML.stringify( 96 | Client.transformers.snakeCaseObjectKeys(numberToPurchase, true, false), 97 | null, 98 | 2, 99 | )); 100 | return; 101 | } 102 | 103 | if (argv.json) { 104 | console.log(JSON.stringify( 105 | Client.transformers.snakeCaseObjectKeys(numberToPurchase, true, false), 106 | null, 107 | 2, 108 | )); 109 | return; 110 | } 111 | 112 | console.log(`Number ${numberToPurchase.msisdn} purchased`); 113 | console.log(''); 114 | console.log(descriptionList(displayFullNumber(numberToPurchase))); 115 | }; 116 | -------------------------------------------------------------------------------- /src/commands/numbers/cancel.js: -------------------------------------------------------------------------------- 1 | const { loadOwnedNumbersFromSDK } = require('../../numbers/loadOwnedNumbersFromSDK'); 2 | const yargs = require('yargs'); 3 | const { makeSDKCall } = require('../../utils/makeSDKCall'); 4 | const { confirm } = require('../../ux/confirm'); 5 | const { dumpCommand } = require('../../ux/dump'); 6 | const { apiKey, apiSecret } = require('../../credentialFlags'); 7 | const { force } = require('../../commonFlags'); 8 | const { countryFlag } = require('../../utils/countries'); 9 | 10 | const flags = { 11 | 'api-key': apiKey, 12 | 'api-secret': apiSecret, 13 | 'force': force, 14 | }; 15 | 16 | exports.flags = flags; 17 | 18 | exports.command = 'cancel '; 19 | 20 | exports.desc = 'Cancel a number'; 21 | 22 | exports.builder = (yargs) => yargs 23 | .positional( 24 | 'country', 25 | { 26 | ...countryFlag, 27 | group: 'Numbers', 28 | }, 29 | ) 30 | .positional( 31 | 'msisdn', 32 | { 33 | describe: 'The number you want to purchase', 34 | type: 'string', 35 | group: 'Numbers', 36 | }, 37 | ) 38 | .options(flags); 39 | 40 | 41 | exports.handler = async (argv) => { 42 | const { SDK, country, msisdn } = argv; 43 | console.info('Cancelling number'); 44 | 45 | const { numbers } = await loadOwnedNumbersFromSDK( 46 | SDK, 47 | { 48 | message: 'Searching for number to cancel', 49 | msisdn: msisdn, 50 | searchPattern: 'contains', 51 | country: country, 52 | limit: 1, 53 | size: 1, 54 | }, 55 | ); 56 | 57 | const numberToCancel = numbers[0]; 58 | console.debug('Number to cancel:', numberToCancel); 59 | console.log(''); 60 | 61 | if (!numberToCancel) { 62 | console.error('Number not found. Are you sure you own this number?'); 63 | console.log(`You can run ${dumpCommand('vonage numbers list')} to see your owned numbers`); 64 | yargs.exit(44); 65 | return; 66 | } 67 | 68 | const okToCancel = await confirm(`Are you sure you want to cancel ${numberToCancel.msisdn}?`); 69 | 70 | if (!okToCancel) { 71 | console.log('Number not cancelled'); 72 | return; 73 | } 74 | 75 | await makeSDKCall( 76 | SDK.numbers.cancelNumber.bind(SDK.numbers), 77 | 'Cancelling number', 78 | { 79 | country: country, 80 | msisdn: numberToCancel.msisdn, 81 | }); 82 | console.info(`Number ${numberToCancel.msisdn} has been cancelled`); 83 | 84 | console.log(''); 85 | console.log('Number cancelled'); 86 | }; 87 | -------------------------------------------------------------------------------- /src/commands/numbers/list.js: -------------------------------------------------------------------------------- 1 | const YAML = require('yaml'); 2 | const { displayNumbers } = require('../../numbers/display'); 3 | const { Client } = require('@vonage/server-client'); 4 | const { dumpCommand } = require('../../ux/dump'); 5 | const { loadOwnedNumbersFromSDK, searchPatterns } = require('../../numbers/loadOwnedNumbersFromSDK'); 6 | const { apiKey, apiSecret } = require('../../credentialFlags'); 7 | const { yaml, json } = require('../../commonFlags'); 8 | const { countryFlag, getCountryName } = require('../../utils/countries'); 9 | const { coerceNumber } = require('../../utils/coerceNumber'); 10 | 11 | const flags = { 12 | 'country': { 13 | ...countryFlag, 14 | group: 'Numbers', 15 | }, 16 | 'pattern': { 17 | describe: `The number pattern you want to search for. Use in conjunction with ${dumpCommand('--search-pattern')}`, 18 | group: 'Numbers', 19 | }, 20 | 'search-pattern': { 21 | describe: 'The strategy you want to use for matching', 22 | choices: Object.keys(searchPatterns), 23 | default: 'contains', 24 | group: 'Numbers', 25 | }, 26 | 'limit': { 27 | describe: 'The maximum number of numbers to return', 28 | coerce: coerceNumber('limit', { min: 1 }), 29 | group: 'Numbers', 30 | }, 31 | 'api-key': apiKey, 32 | 'api-secret': apiSecret, 33 | 'yaml': yaml, 34 | 'json': json, 35 | }; 36 | 37 | exports.flags = flags; 38 | 39 | exports.command = 'list'; 40 | 41 | exports.desc = 'List all numbers that you own'; 42 | 43 | exports.builder = (yargs) => yargs 44 | .options(flags) 45 | .epilogue(`To list numbers that are linked to an application, use ${dumpCommand('vonage apps numbers list ')}.`); 46 | 47 | exports.handler = async (argv) => { 48 | const { SDK, country, limit, pattern, searchPattern } = argv; 49 | console.info('Listing owned numbers'); 50 | 51 | const { totalNumbers, numbers } = await loadOwnedNumbersFromSDK( 52 | SDK, 53 | { 54 | msisdn: pattern, 55 | searchPattern: searchPattern, 56 | country: country, 57 | limit: limit, 58 | size: 100, 59 | all: true, 60 | }, 61 | ); 62 | 63 | if (argv.yaml) { 64 | console.log(YAML.stringify( 65 | (numbers).map( 66 | (number) => Client.transformers.snakeCaseObjectKeys(number, true, false), 67 | ), 68 | null, 69 | 2, 70 | )); 71 | return; 72 | } 73 | 74 | if (argv.json) { 75 | console.log(JSON.stringify( 76 | (numbers).map( 77 | (number) => Client.transformers.snakeCaseObjectKeys(number, true, false), 78 | ), 79 | null, 80 | 2, 81 | )); 82 | return; 83 | } 84 | 85 | console.log(''); 86 | 87 | const qualifiers = [ 88 | ...(country && [` in ${getCountryName(country)}`]) || [], 89 | ...((pattern && searchPattern === 'contains' ) && [` containing ${pattern}`]) || [], 90 | ...((pattern && searchPattern === 'ends' ) && [` ending with ${pattern}`]) || [], 91 | ...((pattern && searchPattern === 'starts' ) && [` starting with ${pattern}`]) || [], 92 | ]; 93 | 94 | if (totalNumbers === 0) { 95 | console.log([ 96 | 'You do not have any numbers', 97 | ...qualifiers, 98 | ].join('')); 99 | console.log(''); 100 | console.log(`Use ${dumpCommand('vonage numbers search')} and ${dumpCommand('vonage numbers buy')} to find a number to purchase.`); 101 | return; 102 | } 103 | 104 | console.log( 105 | [ 106 | totalNumbers > 1 107 | ? `There are ${totalNumbers} numbers` 108 | : 'There is 1 number', 109 | ...qualifiers, 110 | ].join(''), 111 | ); 112 | 113 | console.log(''); 114 | 115 | displayNumbers(numbers, ['country', 'type', 'feature', 'app_id']); 116 | }; 117 | 118 | -------------------------------------------------------------------------------- /src/commands/numbers/update.js: -------------------------------------------------------------------------------- 1 | const { loadOwnedNumbersFromSDK } = require('../../numbers/loadOwnedNumbersFromSDK'); 2 | const { descriptionList } = require('../../ux/descriptionList'); 3 | const { makeSDKCall } = require('../../utils/makeSDKCall'); 4 | const { displayFullNumber } = require('../../numbers/display'); 5 | const yargs = require('yargs'); 6 | const { coerceUrl } = require('../../utils/coerceUrl'); 7 | const { dumpCommand } = require('../../ux/dump'); 8 | const { apiKey, apiSecret } = require('../../credentialFlags'); 9 | const { force } = require('../../commonFlags'); 10 | const { countryFlag } = require('../../utils/countries'); 11 | 12 | const flags = { 13 | 'voice-callback-value': { 14 | describe: 'A SIP URI or telephone number', 15 | group: 'Voice callback', 16 | }, 17 | 'voice-status-callback': { 18 | describe: 'A webhook URI for Vonage to send a request to when a call ends', 19 | coerce: coerceUrl('voice-status-callback'), 20 | group: 'Voice callback', 21 | }, 22 | 'voice-callback-type': { 23 | describe: 'A value to send in the callback request', 24 | choices: ['app', 'sip', 'tel'], 25 | group: 'Voice callback', 26 | }, 27 | 'api-key': apiKey, 28 | 'api-secret': apiSecret, 29 | 'force': force, 30 | }; 31 | 32 | exports.flags = flags; 33 | 34 | exports.command = 'update '; 35 | 36 | exports.desc = 'Update a number'; 37 | 38 | exports.builder = (yargs) => yargs 39 | .positional( 40 | 'country', 41 | { 42 | ...countryFlag, 43 | group: 'Numbers', 44 | }, 45 | ) 46 | .positional( 47 | 'msisdn', 48 | { 49 | describe: 'The number you want to purchase', 50 | type: 'string', 51 | group: 'Numbers', 52 | }, 53 | ) 54 | .options(flags) 55 | .epilogue('It is better to use application webhooks as they offer more flexibility and control over the number\'s behavior.'); 56 | 57 | exports.handler = async (argv) => { 58 | const { SDK, country, msisdn } = argv; 59 | console.info('Listing owned numbers'); 60 | 61 | const { numbers } = await loadOwnedNumbersFromSDK( 62 | SDK, 63 | { 64 | message: 'Searching for number to update', 65 | msisdn: msisdn, 66 | searchPattern: 'contains', 67 | country: country, 68 | limit: 1, 69 | size: 1, 70 | }, 71 | ); 72 | 73 | const numberToUpdate = numbers[0]; 74 | console.debug('Number to update:', numberToUpdate); 75 | console.log(''); 76 | 77 | if (!numberToUpdate) { 78 | console.error('Number not found. Are you sure you own this number?'); 79 | console.log(`You can run ${dumpCommand('vonage numbers list')} to see your owned numbers`); 80 | yargs.exit(44); 81 | return; 82 | } 83 | 84 | numberToUpdate.voiceCallbackType = argv.voiceCallbackType; 85 | numberToUpdate.voiceCallbackValue = argv.voiceCallbackValue; 86 | numberToUpdate.voiceStatusCallback = argv.voiceStatusCallback; 87 | 88 | await makeSDKCall( 89 | SDK.numbers.updateNumber.bind(SDK.numbers), 90 | 'Updating number', 91 | numberToUpdate, 92 | ); 93 | 94 | console.log(''); 95 | console.log('Number updated successfully'); 96 | console.log(''); 97 | console.log(descriptionList(displayFullNumber(numberToUpdate))); 98 | }; 99 | -------------------------------------------------------------------------------- /src/commands/users.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | const { handler} = require('./users/list'); 3 | const { dumpCommand } = require('../ux/dump'); 4 | 5 | exports.command = 'users [command]'; 6 | 7 | exports.desc = 'Manage users'; 8 | 9 | exports.builder = (yargs) => yargs.commandDir('users') 10 | .epilogue(`When no command is given, ${dumpCommand('vonage users')} will act the same as ${dumpCommand('vonage users list')}. Run ${dumpCommand('vonage users list --help')} to see options.`); 11 | 12 | exports.handler = handler; 13 | 14 | -------------------------------------------------------------------------------- /src/commands/users/delete.js: -------------------------------------------------------------------------------- 1 | const { appId, privateKey } = require('../../credentialFlags'); 2 | const { makeSDKCall } = require('../../utils/makeSDKCall'); 3 | const { confirm } = require('../../ux/confirm'); 4 | 5 | exports.command = 'delete '; 6 | 7 | exports.desc = 'Delete a user'; 8 | 9 | /* istanbul ignore next */ 10 | exports.builder = (yargs) => yargs 11 | .positional( 12 | 'id', 13 | { 14 | describe: 'The user ID', 15 | }) 16 | .options({ 17 | 'app-id': appId, 18 | 'private-key': privateKey, 19 | }); 20 | 21 | exports.handler = async (argv) => { 22 | const { SDK, id } = argv; 23 | console.info('Deleting user'); 24 | 25 | const user = await makeSDKCall( 26 | SDK.users.getUser.bind(SDK.users), 27 | 'Fetching User', 28 | id, 29 | ); 30 | 31 | const okToDelete = await confirm('Are you sure you want to delete this user?'); 32 | 33 | if (!okToDelete) { 34 | console.log('User not deleted'); 35 | return; 36 | } 37 | 38 | await makeSDKCall( 39 | SDK.users.deleteUser.bind(SDK.users), 40 | 'Deleting user', 41 | user.id, 42 | ); 43 | console.log(''); 44 | console.log('User deleted'); 45 | }; 46 | -------------------------------------------------------------------------------- /src/commands/users/list.js: -------------------------------------------------------------------------------- 1 | const { appId, privateKey } = require('../../credentialFlags'); 2 | const { confirm } = require('../../ux/confirm'); 3 | const { userSummary } = require('../../users/display'); 4 | const { makeSDKCall } = require('../../utils/makeSDKCall'); 5 | const { dumpCommand } = require('../../ux/dump'); 6 | const { Client } = require('@vonage/server-client'); 7 | 8 | exports.command = 'list'; 9 | 10 | exports.desc = 'List users'; 11 | 12 | /* istanbul ignore next */ 13 | exports.builder = (yargs) => yargs.options({ 14 | 'page-size': { 15 | describe: 'Number of users to return per page', 16 | default: 100, 17 | group: 'Paging', 18 | }, 19 | 'cursor': { 20 | describe: 'Cursor for the next page', 21 | group: 'Paging', 22 | }, 23 | 'sort': { 24 | describe: 'Sort users by name in ascending or descending order', 25 | choices: ['ASC', 'DESC'], 26 | group: 'Paging', 27 | }, 28 | 'name': { 29 | describe: 'Filter by user name', 30 | group: 'User', 31 | }, 32 | 'app-id': appId, 33 | 'private-key': privateKey, 34 | }) 35 | .epilogue([ 36 | 'Since there can be a larg nubmer of users, this command will prompt you to continue paging through the users.', 37 | `You can use the ${dumpCommand('--page-size')} flag to control the number of users returned per page.`, 38 | ].join(' ')) 39 | .example( 40 | dumpCommand('vonage users list'), 41 | 'List a page of users', 42 | ); 43 | 44 | exports.handler = async (argv) => { 45 | const { SDK, pageSize, cursor } = argv; 46 | console.info('List users'); 47 | let pageCursor = cursor; 48 | let okToPage = false; 49 | 50 | do { 51 | console.debug(`Fetching ${pageSize} users with cursor: ${pageCursor}`); 52 | const response = Client.transformers.snakeCaseObjectKeys( 53 | await makeSDKCall( 54 | SDK.users.getUserPage.bind(SDK.users), 55 | !okToPage 56 | ? 'Fetching users' 57 | : 'Fetching more users', 58 | { 59 | pageSize: pageSize, 60 | cursor: pageCursor, 61 | name: argv.name, 62 | sort: argv.sort, 63 | }, 64 | ), 65 | true, 66 | true, 67 | ); 68 | 69 | console.log(''); 70 | console.table([...(response.embedded?.users || [])].map(userSummary)); 71 | 72 | pageCursor = response.links?.next?.href 73 | ? new URL(response.links.next.href).searchParams.get('cursor') 74 | : null; 75 | 76 | console.debug(`Next cursor: ${pageCursor}`); 77 | 78 | if (pageCursor !== null) { 79 | okToPage = await confirm('There are more users. Do you want to continue?'); 80 | } 81 | } while (okToPage && pageCursor !== null); 82 | 83 | console.log('Done Listing users'); 84 | }; 85 | -------------------------------------------------------------------------------- /src/commands/users/show.js: -------------------------------------------------------------------------------- 1 | const YAML = require('yaml'); 2 | const { appId, privateKey } = require('../../credentialFlags'); 3 | const { yaml, json } = require('../../commonFlags'); 4 | const { makeSDKCall } = require('../../utils/makeSDKCall'); 5 | const { displayFullUser } = require('../../users/display'); 6 | 7 | exports.command = 'show '; 8 | 9 | exports.desc = 'Show user'; 10 | 11 | /* istanbul ignore next */ 12 | exports.builder = (yargs) => yargs 13 | .positional( 14 | 'id', 15 | { 16 | describe: 'The user ID', 17 | }) 18 | .options({ 19 | 'app-id': appId, 20 | 'private-key': privateKey, 21 | 'json': json, 22 | 'yaml': yaml, 23 | }); 24 | 25 | exports.handler = async (argv) => { 26 | const { SDK, id } = argv; 27 | console.info('Showing user details'); 28 | 29 | const user = await makeSDKCall( 30 | SDK.users.getUser.bind(SDK.users), 31 | 'Fetching User', 32 | id, 33 | ); 34 | 35 | if (argv.json) { 36 | console.log(JSON.stringify(user, null, 2)); 37 | return; 38 | } 39 | 40 | if (argv.yaml) { 41 | console.log(YAML.stringify(user)); 42 | return; 43 | } 44 | 45 | console.log(''); 46 | displayFullUser(user); 47 | }; 48 | -------------------------------------------------------------------------------- /src/commands/users/update.js: -------------------------------------------------------------------------------- 1 | const yargs = require('yargs'); 2 | const { appId, privateKey } = require('../../credentialFlags'); 3 | const { makeSDKCall } = require('../../utils/makeSDKCall'); 4 | const { displayFullUser } = require('../../users/display'); 5 | const { 6 | userFlags, 7 | validateSip, 8 | validateWss, 9 | normalizeSip, 10 | normalizeWss, 11 | } = require('./create'); 12 | 13 | exports.command = 'update '; 14 | 15 | exports.desc = 'Update a user'; 16 | 17 | /* istanbul ignore next */ 18 | exports.builder = (yargs) => yargs 19 | .positional( 20 | 'id', 21 | { 22 | describe: 'User ID', 23 | type: 'string', 24 | }, 25 | ) 26 | .options({ 27 | ...userFlags, 28 | 'app-id': appId, 29 | 'private-key': privateKey, 30 | }); 31 | 32 | exports.handler = async (argv) => { 33 | console.info('Updating user'); 34 | 35 | if (!validateSip(argv)) { 36 | console.error('Invalid SIP configuration'); 37 | yargs.exit(2); 38 | return; 39 | } 40 | 41 | if (!validateWss(argv)) { 42 | console.error('Invalid Websocket configuration'); 43 | yargs.exit(2); 44 | return; 45 | } 46 | 47 | const { SDK, id } = argv; 48 | const user = await makeSDKCall( 49 | SDK.users.getUser.bind(SDK.users), 50 | 'Fetching User', 51 | id, 52 | ); 53 | 54 | const userToUpdate = JSON.parse(JSON.stringify({ 55 | id: user.id, 56 | name: argv.name ? argv.name : user.name, 57 | displayName: argv.displayName ? argv.displayName : user.displayName, 58 | imageUrl: argv.imageUrl ? argv.imageUrl : user.imageUrl, 59 | properties: { 60 | customData: argv.customData ? argv.customData : user.properties.customData, 61 | ttl: argv.ttl ? argv.ttl : user.properties.ttl, 62 | }, 63 | channels: { 64 | sip: normalizeSip(argv), 65 | websocket: normalizeWss(argv), 66 | pstn: argv.pstnNumber ? argv.pstnNumber?.map((number) => ({ number: number })) : user.channels?.pstn, 67 | sms: argv.smsNumber ? argv.smsNumber?.map((number) => ({ number: number })) : user.channels?.sms, 68 | mms: argv.mmsNumber ? argv.mmsNumber?.map((number) => ({ number: number })) : user.channels?.mms, 69 | whatsapp: argv.whatsAppNumber ? argv.whatsAppNumber?.map((number) => ({ number: number})) : user.channels?.whatsapp, 70 | viber: argv.viberNumber ? argv.viberNumber?.map((number) => ({ number: number})) : user.channels?.viber, 71 | messenger: argv.facebookMessengerId ? argv.facebookMessengerId?.map((id) => ({ id: id})) : user.channels?.messenger, 72 | }, 73 | })); 74 | 75 | console.debug('User to update:', userToUpdate); 76 | 77 | const updatedUser = await makeSDKCall( 78 | SDK.users.updateUser.bind(SDK.users), 79 | 'Updating User', 80 | userToUpdate, 81 | ); 82 | 83 | console.log(''); 84 | displayFullUser(updatedUser); 85 | }; 86 | -------------------------------------------------------------------------------- /src/commonFlags.js: -------------------------------------------------------------------------------- 1 | exports.force = { 2 | alias: 'f', 3 | describe: 'Force the command to run without confirmation', 4 | type: 'boolean', 5 | }; 6 | 7 | exports.yaml = { 8 | describe: 'Output as YAML', 9 | type: 'boolean', 10 | conflicts: 'json', 11 | group: 'Output:', 12 | }; 13 | 14 | exports.json = { 15 | describe: 'Output as JSON', 16 | conflicts: 'yaml', 17 | type: 'boolean', 18 | group: 'Output:', 19 | }; 20 | -------------------------------------------------------------------------------- /src/conversations/conversationFlags.js: -------------------------------------------------------------------------------- 1 | exports.conversationIdFlag = { 2 | describe: 'The conversation ID', 3 | }; 4 | -------------------------------------------------------------------------------- /src/conversations/display.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const { dumpYesNo } = require('../ux/dumpYesNo'); 3 | const { indentLines } = require('../ux/indentLines'); 4 | const { descriptionList } = require('../ux/descriptionList'); 5 | const { displayDate } = require('../ux/date'); 6 | 7 | const conversationSummary = ({ 8 | name, 9 | id, 10 | displayName, 11 | imageUrl, 12 | } = {}) => 13 | Object.fromEntries([ 14 | ['Name', name], 15 | ['Conversation ID', id], 16 | ['Display Name', displayName], 17 | ['Image URL', imageUrl], 18 | ]); 19 | 20 | 21 | const displayConversation = (conversation) =>{ 22 | console.log( 23 | descriptionList([ 24 | ...Object.entries(conversationSummary(conversation)), 25 | ['State', conversation.state], 26 | ['Time to Leave', conversation.properties?.ttl], 27 | ['Created at', displayDate(conversation.timestamp?.created)], 28 | ['Updated at', displayDate(conversation.timestamp?.updated)], 29 | ['Destroyed at', displayDate(conversation.timestamp?.destroyed)], 30 | ['Sequence', conversation.sequenceNumber], 31 | ]), 32 | ); 33 | 34 | console.log(''); 35 | console.log(chalk.underline('Media State:')); 36 | console.log(indentLines(descriptionList([ 37 | ['Earmuffed', dumpYesNo(conversation.mediaState?.earmuff)], 38 | ['Muted', dumpYesNo(conversation.mediaState?.mute)], 39 | ['Playing Stream', dumpYesNo(conversation.mediaState?.playStream)], 40 | ['Recording', dumpYesNo(conversation.mediaState?.recording)], 41 | ['Transcribing', dumpYesNo(conversation.mediaState?.transcribing)], 42 | ['Text To Speech', dumpYesNo(conversation.mediaState?.tts)], 43 | ]))); 44 | }; 45 | 46 | exports.displayConversation = displayConversation; 47 | exports.conversationSummary = conversationSummary; 48 | -------------------------------------------------------------------------------- /src/credentialFlags.js: -------------------------------------------------------------------------------- 1 | const { coerceKey } = require('./utils/coerceKey'); 2 | 3 | exports.apiKey = { 4 | describe: 'Your Vonage API key', 5 | type: 'string', 6 | group: 'Vonage Credentials:', 7 | implies: 'api-secret', 8 | }; 9 | 10 | exports.apiSecret = { 11 | describe: 'Your Vonage API secret', 12 | type: 'string', 13 | implies: 'api-key', 14 | group: 'Vonage Credentials:', 15 | }; 16 | 17 | exports.privateKey = { 18 | describe: 'Your Vonage private key', 19 | type: 'string', 20 | group: 'Vonage Credentials:', 21 | implies: 'app-id', 22 | coerce: coerceKey('private'), 23 | }; 24 | 25 | exports.appId = { 26 | describe: 'Your Vonage application ID', 27 | group: 'Vonage Credentials:', 28 | type: 'string', 29 | implies: 'private-key', 30 | }; 31 | 32 | -------------------------------------------------------------------------------- /src/errors/invalidKey.js: -------------------------------------------------------------------------------- 1 | class InvalidKeyError extends Error { 2 | constructor() { 3 | super('Key must be a valid key string or a path to a file containing a key'); 4 | } 5 | } 6 | 7 | class InvalidKeyFileError extends Error { 8 | constructor() { 9 | super('The key file does not contain a valid key string'); 10 | } 11 | } 12 | 13 | module.exports = { 14 | InvalidKeyError: InvalidKeyError, 15 | InvalidKeyFileError: InvalidKeyFileError, 16 | }; 17 | 18 | -------------------------------------------------------------------------------- /src/middleware/log.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const console = require('console'); 3 | const winston = require('winston'); 4 | const { table } = require('../ux/table'); 5 | const { format, transports } = winston; 6 | 7 | const warning = (message) => process.stderr.write(`${chalk.yellow('Warning')}: ${message}\n`); 8 | 9 | const error = (message) => process.stderr.write(`${chalk.red('Error')}: ${message}\n`); 10 | 11 | exports.setupLog = (argv) => { 12 | let level = 'emerg'; 13 | if (argv.verbose) { 14 | level = 'info'; 15 | } 16 | 17 | if (argv.debug) { 18 | level = 'debug'; 19 | } 20 | 21 | const logger = winston.createLogger({ 22 | level: level, 23 | format: format.combine( 24 | format.colorize(), 25 | format.padLevels(), 26 | format.simple(), 27 | ), 28 | // TODO Add debug file like fly.io 29 | transports: [new transports.Console()], 30 | }); 31 | 32 | global.console.info = (...args) => logger.info(...args); 33 | global.console.warn = (...args) =>{ 34 | warning(args[0]); 35 | logger.warn( ...args); 36 | }; 37 | 38 | global.console.error = (...args) => { 39 | error(args[0]); 40 | logger.error( ...args); 41 | }; 42 | global.console.debug = (...args) => logger.debug( ...args); 43 | global.console.table = (...args) => console.log(table(...args)); 44 | 45 | return { 46 | logger: logger, 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /src/middleware/update.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | const { compareVersions } = require('compare-versions'); 3 | const { getSettings, setSetting } = require('../utils/settings'); 4 | 5 | const now = new Date(); 6 | 7 | exports.checkForUpdate = async () => { 8 | let lastUpdateCheck; 9 | const checkDate = parseInt(`${now.getFullYear()}${('0' + (now.getMonth()+1)).slice(-2)}${('0' + now.getDate()).slice(-2)}`); 10 | const settings = getSettings(); 11 | if (settings.needsUpdate) { 12 | return; 13 | } 14 | 15 | lastUpdateCheck = settings.lastUpdateCheck 16 | ? parseInt(settings.lastUpdateCheck) 17 | // Set to current date since this could be the first time the user is 18 | // running the CLI 19 | : checkDate; 20 | 21 | console.debug(`Last update check: ${lastUpdateCheck}`); 22 | 23 | if (!settings.lastUpdateCheck) { 24 | setSetting('lastUpdateCheck', lastUpdateCheck); 25 | } 26 | 27 | if (checkDate <= lastUpdateCheck) { 28 | console.debug('Skipping update check'); 29 | return; 30 | } 31 | 32 | console.debug('Checking for updates...'); 33 | const installedVersion = require('../../package.json').version; 34 | console.debug(`Installed version: ${installedVersion}`); 35 | 36 | const res = await fetch('https://registry.npmjs.org/@vonage/cli/latest'); 37 | const registryPackageJson = await res.json(); 38 | const latestVersion = registryPackageJson.version; 39 | const forceMinVersion = registryPackageJson?.vonageCli?.forceMinVersion || installedVersion; 40 | 41 | const needsUpdate = compareVersions(latestVersion, installedVersion) < 0; 42 | const forceUpdate = compareVersions(installedVersion, forceMinVersion) < 0; 43 | 44 | console.debug('Force min version:', forceMinVersion); 45 | console.debug(`Latest version: ${latestVersion}`); 46 | console.debug(`Needs update: ${needsUpdate ? 'Yes' : 'No'}`); 47 | console.debug(`Force update: ${forceUpdate ? 'Yes' : 'No'}`); 48 | 49 | setSetting('needsUpdate', needsUpdate); 50 | setSetting('forceUpdate', forceUpdate); 51 | setSetting('latestVersion', latestVersion); 52 | setSetting('forceMinVersion', forceMinVersion); 53 | setSetting('lastUpdateCheck', checkDate); 54 | }; 55 | -------------------------------------------------------------------------------- /src/numbers/display.js: -------------------------------------------------------------------------------- 1 | const { buildCountryString } = require('../utils/countries'); 2 | const { dumpValue } = require('../ux/dump'); 3 | const { displayCurrency } = require('../ux/currency'); 4 | const { typeLabels } = require('./types'); 5 | 6 | const displayNumber = (number = {}, fields = []) => Object.assign({ 7 | 'Number': number.msisdn, 8 | ...(fields.includes('country') ? {'Country': buildCountryString(number.country)} : {}), 9 | ...(fields.includes('type') ? {'Type': typeLabels[number.type]} : {}), 10 | ...(fields.includes('feature') ? {'Features': number.features.sort().join(', ')} : {}), 11 | ...(fields.includes('monthly_cost') ? {'Monthly Cost': displayCurrency(number.cost)} : {}), 12 | ...(fields.includes('setup_cost') ? {'Setup Cost': displayCurrency(number.initialPrice) } : {}), 13 | ...(fields.includes('app_id') ? {'Linked Application ID': number.appId || dumpValue('Not linked to any application') } : {}), 14 | ...(fields.includes('voice_callback_type') ? {'Voice Callback': number.voiceCallbackType} : {}), 15 | ...(fields.includes('voice_callback_value') ? {'Voice Callback Value': number.voiceCallbackValue } : {}), 16 | ...(fields.includes('voice_status_callback') ? {'Voice Status Callback': number.voiceStatusCallback} : {}), 17 | }); 18 | 19 | const displayNumbers = (numbers = [], fields = []) => { 20 | console.table(numbers.map((number) => displayNumber( 21 | number, 22 | fields, 23 | ))); 24 | }; 25 | 26 | exports.displayFullNumber = (number) => displayNumber( 27 | number, 28 | [ 29 | 'country', 30 | 'type', 31 | 'feature', 32 | 'monthly_cost', 33 | 'setup_cost', 34 | 'app_id', 35 | 'voice_callback_type', 36 | 'voice_callback_value', 37 | 'voice_status_callback', 38 | ]); 39 | 40 | exports.displayNumber = displayNumber; 41 | 42 | exports.displayNumbers = displayNumbers; 43 | 44 | -------------------------------------------------------------------------------- /src/numbers/features.js: -------------------------------------------------------------------------------- 1 | const features = [ 2 | 'SMS', 3 | 'MMS', 4 | 'VOICE', 5 | ]; 6 | 7 | const coerceFeatures = (value) => { 8 | const argFeatures = value.split(',').map((feature) => String(feature).toUpperCase()); 9 | 10 | const valid = argFeatures.every((feature) => features.includes(feature)); 11 | 12 | if (!valid) { 13 | throw new Error(`Invalid features: ${argFeatures.join(', ')}`); 14 | } 15 | 16 | return argFeatures; 17 | }; 18 | 19 | const featureFlag = { 20 | description: 'Available features are SMS, VOICE and MMS. To look for numbers that support multiple features, use a comma-separated value: SMS,MMS,VOICE.', 21 | type: 'string', 22 | coerce: coerceFeatures, 23 | }; 24 | 25 | exports.featureFlag = featureFlag; 26 | 27 | exports.features = features; 28 | 29 | exports.coerceFeatures = coerceFeatures; 30 | -------------------------------------------------------------------------------- /src/numbers/loadOwnedNumbersFromSDK.js: -------------------------------------------------------------------------------- 1 | const { progress } = require('../ux/progress'); 2 | const { sdkError } = require('../utils/sdkError'); 3 | const { Client } = require('@vonage/server-client'); 4 | 5 | const searchPatterns = { 6 | starts: 0, 7 | contains: 1, 8 | ends: 2, 9 | }; 10 | 11 | const loadOwnedNumbersFromSDK = async ( 12 | SDK, 13 | { 14 | appId, 15 | msisdn, 16 | message, 17 | limit, 18 | country, 19 | searchPattern, 20 | size = 100, 21 | index = 1, 22 | all = false, 23 | } = {}, 24 | ) => { 25 | const spinnerMessage = message 26 | || appId && `Fetching numbers linked to application ${appId}` 27 | || 'Fetching Owned numbers'; 28 | 29 | const { increment, setTotalSteps, finished } = progress({ message: spinnerMessage }); 30 | try { 31 | let ownedNumbers = []; 32 | let totalPages = 1; 33 | let totalNumbers = 0; 34 | do { 35 | console.debug(`Fetching numbers page ${index}`); 36 | const response = Client.transformers.camelCaseObjectKeys( 37 | await SDK.numbers.getOwnedNumbers({ 38 | country: country, 39 | applicationId: appId, 40 | searchPattern: searchPatterns[searchPattern], 41 | pattern: msisdn, 42 | size: size, 43 | index: index, 44 | }), 45 | true, 46 | ); 47 | 48 | ownedNumbers = [ 49 | ...ownedNumbers, 50 | ...(response.numbers || []), 51 | ]; 52 | 53 | totalNumbers = response.count || 0; 54 | limit = limit || totalNumbers; 55 | totalPages = Math.ceil(totalNumbers / size); 56 | setTotalSteps(totalPages); 57 | increment(); 58 | index++; 59 | console.debug(`Total owned numbers: ${totalNumbers}`); 60 | console.debug(`Total pages for numbers: ${totalPages}`); 61 | } while(all && index <= totalPages && ownedNumbers.length < limit); 62 | 63 | // The SDK does not transform this response. 64 | return { 65 | totalNumbers: totalNumbers, 66 | numbers: ownedNumbers.slice(0, limit), 67 | }; 68 | } catch (error) { 69 | sdkError(error); 70 | return {}; 71 | } finally { 72 | finished(); 73 | } 74 | }; 75 | 76 | exports.loadOwnedNumbersFromSDK = loadOwnedNumbersFromSDK; 77 | 78 | exports.searchPatterns = searchPatterns; 79 | -------------------------------------------------------------------------------- /src/numbers/types.js: -------------------------------------------------------------------------------- 1 | const typeLabels = { 2 | 'landline': 'Landline', 3 | 'landline-toll-free': 'Toll-free', 4 | 'mobile-lvn': 'Mobile', 5 | }; 6 | 7 | const types = Object.keys(typeLabels); 8 | 9 | const typeFlag = { 10 | description: 'Type of phone number', 11 | type: 'string', 12 | choices: types, 13 | }; 14 | 15 | exports.typeFlag = typeFlag; 16 | 17 | exports.typeLabels = typeLabels; 18 | 19 | exports.types = types; 20 | 21 | -------------------------------------------------------------------------------- /src/utils/aclDiff.js: -------------------------------------------------------------------------------- 1 | const jsonDiff = require('json-diff'); 2 | 3 | const status = { 4 | OK: 'OK', 5 | 6 | // Invalid means that the path is present in both but the values are different 7 | INVALID: 'INVALID', 8 | 9 | // Present that there is a difference between the token and the flag but 10 | // the validation is still considered a pass 11 | PASS: 'PASS', 12 | 13 | // Presnt in token 14 | PRESENT: 'PRESENT', 15 | 16 | // Missing in flag 17 | MISSING: 'MISSING', 18 | 19 | // Present in both but different 20 | MISMATCH: 'MISMATCH', 21 | }; 22 | 23 | const getMethods = ({methods} = {}) => methods ? methods.join(', ') : 'ANY'; 24 | 25 | const determineStatus = (which, pathDiff) => { 26 | if (!pathDiff) { 27 | return status.OK; 28 | } 29 | 30 | if (pathDiff[`${which}__deleted`]) { 31 | return status.PRESENT; 32 | } 33 | 34 | if (pathDiff[`${which}__added`]) { 35 | return status.MISSING; 36 | } 37 | 38 | if (pathDiff[which]) { 39 | return status.MISMATCH; 40 | } 41 | 42 | return status.OK; 43 | }; 44 | 45 | const processPathMethodsAndFilters = (diff, acc, path) => { 46 | if (!diff) { 47 | return; 48 | } 49 | 50 | const mismatchDiff = diff.paths[path]; 51 | 52 | const filtersStatus = determineStatus('filters', mismatchDiff); 53 | const methodsStatus = determineStatus('methods', mismatchDiff); 54 | 55 | acc.paths[path].filtersStatus = filtersStatus; 56 | acc.paths[path].methodsStatus = methodsStatus; 57 | 58 | switch (true) { 59 | case methodsStatus === status.OK && filtersStatus === status.MISSING: 60 | acc.paths[path].state = status.PASS; 61 | break; 62 | 63 | case methodsStatus !== status.OK: 64 | case filtersStatus !== status.OK && filtersStatus !== status.MISSING: 65 | case filtersStatus === status.MISMATCH: 66 | acc.paths[path].state = status.INVALID; 67 | acc.ok = false; 68 | } 69 | }; 70 | 71 | const processPath = (diff, acc, path) => { 72 | // JSON Diff will have the following structure becuase of the order 73 | // we are passing the token (first and then the flag) 74 | // This means that the diff will assume we want to match the flag to 75 | // the token. 76 | 77 | // If deleted then path is missing in flag but present in token 78 | if (diff?.paths[`${path}__deleted`]) { 79 | acc.paths[path].state = status.PRESENT; 80 | return acc; 81 | } 82 | 83 | // If added then path is missing in token but present in flag 84 | if (diff?.paths[`${path}__added`]) { 85 | acc.ok = false; 86 | 87 | acc.paths[path].state = status.MISSING; 88 | return acc; 89 | } 90 | 91 | // Now we know that the path is present in both the token and the flag 92 | // so we can compare the methods and filters 93 | processPathMethodsAndFilters(diff, acc, path); 94 | return acc; 95 | }; 96 | 97 | const aclDiff = (tokenAcl, flagAcl) => { 98 | console.info('Comparing ACLs'); 99 | 100 | const diff = jsonDiff.diff(tokenAcl, flagAcl); 101 | const merged = {paths: {...tokenAcl.paths, ...flagAcl.paths}}; 102 | 103 | return Object.entries(merged.paths).reduce( 104 | (acc, [path]) => { 105 | acc.paths[path] = { 106 | // we always want to show what is in the token the user might not 107 | // know what is in the token 108 | methods: getMethods(flagAcl.paths[path]), 109 | 110 | methodsStatus: status.OK, 111 | 112 | filtersStatus: status.OK, 113 | 114 | state: status.OK, 115 | }; 116 | 117 | processPath(diff, acc, path); 118 | return acc; 119 | }, 120 | {ok: true, paths: {}}, 121 | ); 122 | }; 123 | 124 | exports.aclDiff = aclDiff; 125 | exports.status = status; 126 | -------------------------------------------------------------------------------- /src/utils/coerceJSON.js: -------------------------------------------------------------------------------- 1 | const Ajv = require('ajv/dist/2020'); 2 | const ajv = new Ajv(); 3 | 4 | const coerceJSON = (argName, schema) => (json) => { 5 | if (!json) { 6 | return json; 7 | } 8 | 9 | if (Array.isArray(json)) { 10 | return json.map((item) => coerceJSON(argName, schema)(item)); 11 | } 12 | 13 | let arg; 14 | try { 15 | arg = JSON.parse(json); 16 | } catch (error) { 17 | throw new Error(`Failed to parse JSON for ${argName}: ${error}`); 18 | } 19 | 20 | if (!schema) { 21 | return arg; 22 | } 23 | 24 | const validate = ajv.compile(schema); 25 | 26 | const data = validate(arg); 27 | if (data) { 28 | return arg; 29 | } 30 | 31 | // TODO Dump to debug log 32 | throw new Error( 33 | `${argName} Failed to validate against schema:\n${JSON.stringify(validate.errors, null, 2)}`, 34 | ); 35 | }; 36 | 37 | exports.coerceJSON = coerceJSON; 38 | -------------------------------------------------------------------------------- /src/utils/coerceKey.js: -------------------------------------------------------------------------------- 1 | const { readFileSync, existsSync } = require('fs'); 2 | const { 3 | InvalidKeyFileError, 4 | InvalidKeyError, 5 | } = require('../errors/invalidKey'); 6 | 7 | exports.coerceKey = (which) => (arg) => { 8 | if (!arg) { 9 | return arg; 10 | } 11 | 12 | if (arg.startsWith(`-----BEGIN ${which.toUpperCase()} KEY-----`)) { 13 | return arg; 14 | } 15 | 16 | if (!existsSync(arg)) { 17 | throw new InvalidKeyError(); 18 | } 19 | 20 | const fileContents = readFileSync(arg, 'utf-8').toString(); 21 | 22 | if (!fileContents.startsWith(`-----BEGIN ${which.toUpperCase()} KEY-----`)) { 23 | throw new InvalidKeyFileError(); 24 | } 25 | 26 | return fileContents; 27 | }; 28 | -------------------------------------------------------------------------------- /src/utils/coerceNumber.js: -------------------------------------------------------------------------------- 1 | // Allows for adding min/max constraints to number arguments 2 | const coerceNumber = (argName, {min, max} = {}) => (value) => { 3 | if (value === undefined) { 4 | return value; 5 | } 6 | 7 | const number = Number(value); 8 | if (isNaN(number)) { 9 | throw new Error(`Invalid number for ${argName}: ${value}`); 10 | } 11 | 12 | if (min !== undefined && number < min) { 13 | throw new Error(`Number for ${argName} must be at least ${min}: ${value}`); 14 | } 15 | 16 | if (max !== undefined && number > max) { 17 | throw new Error(`Number for ${argName} must be at most ${max}: ${value}`); 18 | } 19 | 20 | return number; 21 | }; 22 | 23 | exports.coerceNumber = coerceNumber; 24 | -------------------------------------------------------------------------------- /src/utils/coerceRemove.js: -------------------------------------------------------------------------------- 1 | const unsetRemove = (obj, setAsNull=false) => { 2 | return Object.entries(obj).reduce( 3 | (acc, [key, value]) => { 4 | if (value === '__REMOVE__' && !setAsNull) { 5 | return acc; 6 | } 7 | 8 | if (typeof value === 'object' && value !== null) { 9 | return { 10 | ...acc, 11 | [key]: unsetRemove(value), 12 | }; 13 | } 14 | 15 | return { 16 | ...acc, 17 | [key]: value === '__REMOVE__' ? null : value, 18 | }; 19 | }, 20 | {}, 21 | ); 22 | }; 23 | 24 | exports.unsetRemove = unsetRemove; 25 | 26 | exports.coerceRemove = (arg) => { 27 | if (arg === '') { 28 | return '__REMOVE__'; 29 | } 30 | 31 | return arg; 32 | }; 33 | 34 | exports.coerceRemoveCallback = (cb) => (arg) => { 35 | if (arg === '') { 36 | return '__REMOVE__'; 37 | } 38 | 39 | return cb(arg); 40 | }; 41 | 42 | exports.coerceRemoveList = (flagName, list) => (arg) => { 43 | if (arg === '') { 44 | return '__REMOVE__'; 45 | } 46 | 47 | if (list.includes(arg)) { 48 | return arg; 49 | } 50 | 51 | throw new Error(`Invalid value [${arg}] for ${flagName}, only ${list.join(', ')} are supported.`); 52 | }; 53 | -------------------------------------------------------------------------------- /src/utils/coerceUrl.js: -------------------------------------------------------------------------------- 1 | const { URL } = require('node:url'); 2 | 3 | const coerceUrl = (argName) => (url) => { 4 | if (!url) { 5 | return url; 6 | } 7 | 8 | try { 9 | const parsed = new URL(url); 10 | return parsed.toString(); 11 | } catch (error) { 12 | console.error(`Invalid URL for ${argName}: ${url}`, error); 13 | throw new Error(`Invalid URL for ${argName}: ${url}`); 14 | } 15 | }; 16 | 17 | exports.coerceUrl = coerceUrl; 18 | -------------------------------------------------------------------------------- /src/utils/countries.js: -------------------------------------------------------------------------------- 1 | const countries = require('../../data/countries.json'); 2 | 3 | const countryCodes = Object.keys(countries); 4 | 5 | const buildCountryString = (countryCode) =>`${getCountryFlag(countryCode)} ${getCountryName(countryCode)}`; 6 | 7 | const getCountryFlag = (countryCode) => countries[countryCode].emoji.trim(); 8 | 9 | const getCountryName = (countryCode) => countries[countryCode].name; 10 | 11 | const coerceCountry = (arg) => { 12 | if (!countryCodes.includes(arg.toUpperCase())) { 13 | throw new Error(`Invalid country code: ${arg}`); 14 | } 15 | 16 | return arg.toUpperCase(); 17 | }; 18 | 19 | const countryFlag = { 20 | describe: 'The country using the two character country code in ISO 3166-1 alpha-2 format', 21 | coerce: coerceCountry, 22 | }; 23 | exports.countries = countries; 24 | 25 | exports.countryCodes = countryCodes; 26 | 27 | exports.getCountryFlag = getCountryFlag; 28 | 29 | exports.getCountryName = getCountryName; 30 | 31 | exports.buildCountryString = buildCountryString; 32 | 33 | exports.coerceCountry = coerceCountry; 34 | 35 | exports.countryFlag = countryFlag; 36 | -------------------------------------------------------------------------------- /src/utils/fs.js: -------------------------------------------------------------------------------- 1 | const { existsSync, writeFileSync, mkdirSync } = require('fs'); 2 | const { confirm } = require('../ux/confirm'); 3 | 4 | class UserDeclinedError extends Error { 5 | constructor() { 6 | super('User declined to overwrite file'); 7 | this.name = 'UserDeclinedError'; 8 | } 9 | } 10 | 11 | const createDirectory = (directory) => { 12 | if (existsSync(directory)) { 13 | console.debug('Directory already exists'); 14 | return true; 15 | } 16 | 17 | console.info(`Creating directory ${directory}`); 18 | mkdirSync(directory, { recursive: true }); 19 | return true; 20 | }; 21 | 22 | const checkOkToWrite = async (filePath, message=null) => { 23 | if (!existsSync(filePath)) { 24 | console.debug('Config file does not exist ok to write'); 25 | return true; 26 | } 27 | 28 | console.debug('Config file exists, checking if ok to write'); 29 | const okToWrite = await confirm( 30 | message || 31 | `Overwirte file ${filePath}?`, 32 | ); 33 | 34 | console.debug('Ok to write:', okToWrite); 35 | return okToWrite; 36 | }; 37 | 38 | const writeFile = async (filePath, data, message) => { 39 | const okToWrite = await checkOkToWrite(filePath, message); 40 | if (!okToWrite) { 41 | console.debug('Not writing to file'); 42 | throw new UserDeclinedError(); 43 | } 44 | 45 | console.debug(`Writing to: ${filePath}`); 46 | 47 | writeFileSync(filePath, data); 48 | console.debug(`Data saved to ${filePath}`); 49 | }; 50 | 51 | const writeJSONFile = async (filePath, data, message) => writeFile( 52 | filePath, 53 | JSON.stringify(data, null, 2), 54 | message, 55 | ); 56 | 57 | exports.UserDeclinedError = UserDeclinedError; 58 | exports.createDirectory = createDirectory; 59 | exports.checkOkToWrite = checkOkToWrite; 60 | exports.writeFile = writeFile; 61 | exports.writeJSONFile = writeJSONFile; 62 | -------------------------------------------------------------------------------- /src/utils/makeSDKCall.js: -------------------------------------------------------------------------------- 1 | const { spinner } = require('../ux/spinner'); 2 | const { sdkError } = require('../utils/sdkError'); 3 | 4 | exports.makeSDKCall = async (sdkFn, message, ...params) => { 5 | console.debug(`Calling SDK function ${sdkFn.name}`); 6 | const { stop: loadStop, fail: loadFail } = spinner({ message: message }); 7 | try { 8 | const result = await sdkFn(...params); 9 | console.debug('SDK function result', result); 10 | loadStop(); 11 | return result; 12 | } catch (error) { 13 | loadFail(); 14 | sdkError(error); 15 | return; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/utils/sdkError.js: -------------------------------------------------------------------------------- 1 | const yargs = require('yargs'); 2 | const { dumpCommand } = require('../ux/dump'); 3 | 4 | exports.sdkError = async (error) => { 5 | const statusCode = error.response?.status; 6 | const errorName = error.constructor.name; 7 | let errorData = {}; 8 | try { 9 | errorData = await error.response?.json(); 10 | } catch (error) { 11 | console.debug('Failed to parse error response', error); 12 | } 13 | 14 | console.debug(`API Error data: ${JSON.stringify(errorData, null, 2)}`); 15 | console.debug(`Status Code ${statusCode}`); 16 | console.debug(`Error name: ${errorName}`); 17 | 18 | switch (statusCode || errorName) { 19 | case 401: 20 | case 403: 21 | console.error('You are not authorized to perform this action'); 22 | console.log(''); 23 | console.log(`Please check your credentials by running ${dumpCommand('vonage auth show')} and try again`); 24 | yargs.exit(5); 25 | return; 26 | case 404: 27 | console.error( 28 | `Resource not Found${errorData.detail 29 | ? `: ${errorData.detail}` 30 | : ''}`, 31 | ); 32 | yargs.exit(20); 33 | return; 34 | 35 | case 'MissingApplicationIdError': 36 | case 'MissingPrivateKeyError': 37 | console.error('This command needs to be run against an application.'); 38 | console.log(''); 39 | console.log('You can fix this problem by:'); 40 | console.log(''); 41 | console.log(`1. Running this command again with the ${dumpCommand('--app-id')} and the ${dumpCommand('--private-key')} arguments passed in`); 42 | console.log(`2. Configure the CLI globally using the ${dumpCommand('vonage auth set')} command`); 43 | console.log(`3. Configure the CLI locally using the ${dumpCommand('vonage auth set')} command with the ${dumpCommand('--local')} flag`); 44 | yargs.exit(2); 45 | return; 46 | 47 | // This condition should be very hard to reach but is possible. 48 | // 1. The configuration file has been altered to remove the properties. 49 | // 2. The user is using environment variables that have been blanked out 50 | // Just some of of the ways I think this can be reached 51 | case 'MissingApiKeyError': 52 | case 'MissingApiSecretError': 53 | console.error('This command needs your API Key and Secret'); 54 | console.log(''); 55 | console.log('You can fix this problem by:'); 56 | console.log(''); 57 | console.log(`1. Running this command again with the ${dumpCommand('--api-key')} and the ${dumpCommand('--api-secret')} arguments passed in`); 58 | console.log(`2. Configure the CLI globally using the ${dumpCommand('vonage auth set')} command`); 59 | console.log(`3. Configure the CLI locally using the ${dumpCommand('vonage auth set')} command with the ${dumpCommand('--local')} flag`); 60 | yargs.exit(2); 61 | return; 62 | 63 | 64 | default: 65 | console.error(`Error with SDK call: ${error.message}` ); 66 | yargs.exit(99); 67 | return; 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /src/utils/settings.js: -------------------------------------------------------------------------------- 1 | const { getSharedConfig } = require('../middleware/config'); 2 | const { mkdirSync, existsSync, readFileSync, writeFileSync } = require('fs'); 3 | 4 | let settings = null; 5 | let changed = false; 6 | 7 | process.on('exit', () => { 8 | if (changed) { 9 | saveSettingsFile(); 10 | } 11 | }); 12 | 13 | const loadSettingsFile = () => { 14 | const { settingsFile, settingsFileExists } = getSharedConfig(); 15 | 16 | if (!settingsFileExists) { 17 | settings = {}; 18 | return; 19 | } 20 | 21 | try { 22 | settings = JSON.parse(readFileSync(settingsFile)); 23 | } catch (error) { 24 | console.error('Error reading settings file:', error); 25 | settings = {}; 26 | } 27 | }; 28 | 29 | const saveSettingsFile = () => { 30 | const { settingsFile, globalConfigPath} = getSharedConfig(); 31 | 32 | if (!existsSync(globalConfigPath)) { 33 | console.debug(`Creating global config folder: ${globalConfigPath}`); 34 | mkdirSync(globalConfigPath, {recursive: true}); 35 | } 36 | 37 | console.debug(`Saving settings file to: ${settingsFile}`); 38 | 39 | writeFileSync(settingsFile, JSON.stringify(settings, null, 2)); 40 | }; 41 | 42 | const setSetting = (key, value) => { 43 | changed = true; 44 | settings[key] = value; 45 | }; 46 | 47 | exports.setSetting = setSetting; 48 | exports.getSettings = () => { 49 | if (settings === null) { 50 | loadSettingsFile(); 51 | } 52 | 53 | return settings; 54 | }; 55 | 56 | -------------------------------------------------------------------------------- /src/utils/validateSDKAuth.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const { Vonage } = require('@vonage/server-sdk'); 3 | const { spinner } = require('../ux/spinner'); 4 | 5 | const validateApplicationKey = (application, privateKey) => { 6 | console.debug('Validating application key'); 7 | const publicKey = application.keys.publicKey; 8 | 9 | console.debug(`Public Key: ${publicKey}`); 10 | console.debug(`Private Key: ${privateKey}`); 11 | 12 | try { 13 | const encryptedString = crypto.publicEncrypt(publicKey, application.id); 14 | const decryptedString = crypto.privateDecrypt(privateKey, encryptedString); 15 | 16 | console.debug('Confirming public key'); 17 | return decryptedString.toString() === application.id; 18 | } catch (error) { 19 | console.debug('Error validating application key:', error); 20 | return false; 21 | } 22 | }; 23 | 24 | // We do not want to pass in the SDK here as we rebuild it to ensure we 25 | // are using the correct auth values 26 | const validatePrivateKeyAndAppId = async (apiKey, apiSecret, appId, privateKey) => { 27 | console.info('Validating API Key and Secret'); 28 | 29 | if (!appId || !privateKey) { 30 | console.debug('App ID and Private Key are required'); 31 | return false; 32 | } 33 | 34 | const {fail, stop} = spinner({message: 'Checking App ID and Private Key: ...'}); 35 | 36 | try { 37 | const vonage = new Vonage({ 38 | apiKey: apiKey, 39 | apiSecret: apiSecret, 40 | applicationId: appId, 41 | privateKey: privateKey, 42 | }); 43 | 44 | console.debug('Loading application'); 45 | 46 | // TODO update to spinner 47 | const application = await vonage.applications.getApplication(appId); 48 | console.debug('Got Application'); 49 | 50 | console.debug('Confirming public key'); 51 | const correctPublicKey = validateApplicationKey(application, privateKey); 52 | console.debug(`Public key confirmed: ${correctPublicKey ? 'Yes' : 'No'}`); 53 | 54 | stop(); 55 | 56 | return correctPublicKey; 57 | } catch (error) { 58 | console.debug('Error validating API Key Secret:', error); 59 | console.info('API Key and Secret are invalid'); 60 | fail(); 61 | return false; 62 | } 63 | }; 64 | 65 | const validateApiKeyAndSecret = async (apiKey, apiSecret) => { 66 | console.info('Validating API Key and Secret'); 67 | 68 | if (!apiKey || !apiSecret) { 69 | console.debug('API Key and Secret are required'); 70 | return false; 71 | } 72 | 73 | const {stop, fail} = spinner({message: 'Checking API Key Secret: ...'}); 74 | 75 | try { 76 | const vonage = new Vonage({ 77 | apiKey: apiKey, 78 | apiSecret: apiSecret, 79 | }); 80 | 81 | console.debug('Getting an application page'); 82 | await vonage.applications.getApplicationPage({size: 1}); 83 | console.info('API Key and Secret are valid'); 84 | stop(); 85 | return true; 86 | } catch (error) { 87 | console.debug('Error validating App ID and Private Key', error); 88 | console.info('API Key and Secret are invalid'); 89 | 90 | fail(); 91 | 92 | return false; 93 | } 94 | }; 95 | 96 | exports.validateApiKeyAndSecret = validateApiKeyAndSecret; 97 | 98 | exports.validatePrivateKeyAndAppId = validatePrivateKeyAndAppId; 99 | 100 | exports.validateApplicationKey = validateApplicationKey; 101 | -------------------------------------------------------------------------------- /src/ux/__mocks__/confirm.js: -------------------------------------------------------------------------------- 1 | afterEach(() => { 2 | jest.clearAllMocks(); 3 | }); 4 | 5 | exports.confirm = jest.fn(); 6 | -------------------------------------------------------------------------------- /src/ux/confirm.js: -------------------------------------------------------------------------------- 1 | const readline = require('readline'); 2 | const parser = require('yargs-parser'); 3 | 4 | const { argv } = parser.detailed(process.argv); 5 | const { force } = argv; 6 | 7 | const ask = (message) => new Promise((resolve) => { 8 | const rl = readline.createInterface({ 9 | input: process.stdin, 10 | output: process.stdout, 11 | }); 12 | 13 | rl.question(`${message} [y/n] `, (answer) => { 14 | resolve(answer.toLowerCase()); 15 | rl.close(); 16 | }); 17 | }); 18 | 19 | exports.confirm = async ( 20 | message, 21 | noForce = false, 22 | ) =>{ 23 | if (!noForce && force) { 24 | console.debug(`Forcing: ${message}`); 25 | return true; 26 | } 27 | 28 | console.debug(`Confirming: ${message}`); 29 | 30 | let answerCorrectly = false; 31 | do { 32 | const answer = await ask(message); 33 | 34 | if ([ 'y', 'n' ].includes(answer)) { 35 | answerCorrectly = true; 36 | return answer === 'y'; 37 | } 38 | 39 | process.stderr.write('Please answer with y for yes or n for no\n'); 40 | } while (!answerCorrectly); 41 | }; 42 | -------------------------------------------------------------------------------- /src/ux/currency.js: -------------------------------------------------------------------------------- 1 | const displayCurrency = (num) => !isNaN(num) 2 | ? parseFloat(num).toLocaleString( 3 | 'en-UK', 4 | { 5 | style: 'currency', 6 | currency: 'EUR', 7 | }, 8 | ) 9 | : undefined; 10 | 11 | exports.displayCurrency = displayCurrency; 12 | -------------------------------------------------------------------------------- /src/ux/cursor.js: -------------------------------------------------------------------------------- 1 | const yargs = require('yargs'); 2 | 3 | const resetCursor = () => { 4 | process.stderr.write('\u001B[?25h'); 5 | }; 6 | 7 | const hideCursor = () => { 8 | process.stderr.write('\u001B[?25l'); 9 | }; 10 | 11 | const exitAndShowCursor = () => { 12 | resetCursor(); 13 | yargs.exit(0); 14 | }; 15 | 16 | 17 | process.on('exit', resetCursor); 18 | process.on('SIGINT', exitAndShowCursor); 19 | process.on('SIGTERM', exitAndShowCursor); 20 | process.on('SIGQUIT', exitAndShowCursor); 21 | process.on('SIGHUP', exitAndShowCursor); 22 | 23 | exports.hideCursor = hideCursor; 24 | exports.resetCursor = resetCursor; 25 | exports.exitAndShowCursor = exitAndShowCursor; 26 | 27 | -------------------------------------------------------------------------------- /src/ux/date.js: -------------------------------------------------------------------------------- 1 | const [locale] = process.env.LC_ALL?.split('.') || ['en_US']; 2 | 3 | const displayDate = (date) => date 4 | ? new Date(date).toLocaleString( 5 | locale.replace('_', '-') || 'en-US', 6 | ) 7 | : undefined; 8 | 9 | exports.displayDate = displayDate; 10 | -------------------------------------------------------------------------------- /src/ux/descriptionList.js: -------------------------------------------------------------------------------- 1 | const { dumpKey, dumpValue } = require('./dump'); 2 | 3 | const descriptionTerm = (value) => dumpKey(value); 4 | 5 | const descriptionDetails = (value) => dumpValue(value); 6 | 7 | const descriptionList = (values) => (!Array.isArray(values) 8 | ? Object.entries(values) 9 | : values).map( 10 | ([term, details]) => `${descriptionTerm(term)}: ${descriptionDetails(details)}`, 11 | ).join('\n'); 12 | 13 | module.exports = { 14 | descriptionList, 15 | descriptionDetails, 16 | descriptionTerm, 17 | }; 18 | 19 | -------------------------------------------------------------------------------- /src/ux/dump.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | 3 | const dumpKey = (key) => `${chalk.bold(key)}`; 4 | 5 | const dumpValue = (value) => { 6 | const varType = Array.isArray(value) ? 'array' : typeof value; 7 | 8 | if (value === undefined || value === null) { 9 | return `${chalk.dim.yellow('Not Set')}`; 10 | } 11 | 12 | switch (varType) { 13 | case 'number': 14 | return `${chalk.dim(value)}`; 15 | 16 | case 'array': 17 | return dumpArray(value); 18 | 19 | case 'object': 20 | return dumpObject(value); 21 | 22 | case 'string': 23 | // falls through 24 | default: 25 | return `${chalk.blue(value)}`; 26 | } 27 | }; 28 | 29 | const dumpObject = ( 30 | data, 31 | indent = 2, 32 | ) => [ 33 | `${' '.repeat(indent - 2)}${chalk.yellow('{')}`, 34 | ...Object.entries(data).map(([key, value]) => { 35 | if (value === undefined || value === null) { 36 | return `${' '.repeat(indent)}${dumpKey(key)}: ${dumpValue(value)}`; 37 | } 38 | 39 | const varType = Array.isArray(value) ? 'array' : typeof value; 40 | switch (varType) { 41 | case 'object': 42 | return `${' '.repeat(indent)}${dumpKey(key)}: ${dumpObject( 43 | value, 44 | indent + 2, 45 | ).trimStart()}`; 46 | 47 | case 'array': 48 | return `${' '.repeat(indent)}${dumpKey(key)}: ${dumpArray( 49 | value, 50 | indent + 2, 51 | ).trimStart()}`; 52 | 53 | default: 54 | return `${' '.repeat(indent)}${dumpKey(key)}: ${dumpValue(value)}`; 55 | } 56 | }), 57 | `${' '.repeat(indent - 2)}${chalk.yellow('}')}`, 58 | ].join('\n'); 59 | 60 | 61 | const dumpArray = (data, indent = 2) => [ 62 | `${' '.repeat(indent - 2)}${chalk.yellow('[')}`, 63 | ...data.map((value) => { 64 | if (value === undefined || value === null) { 65 | return `${' '.repeat(indent)}${dumpValue(value)}`; 66 | } 67 | 68 | const varType = Array.isArray(value) ? 'array' : typeof value; 69 | switch (varType) { 70 | case 'object': 71 | return `${' '.repeat(indent)}${dumpObject( 72 | value, 73 | indent + 2, 74 | ).trimStart()}`; 75 | 76 | case 'array': 77 | return `${' '.repeat(indent)}${dumpArray( 78 | value, 79 | indent + 2, 80 | ).trimStart()}`; 81 | 82 | default: 83 | return `${' '.repeat(indent)}${dumpValue(value)}`; 84 | } 85 | }), 86 | `${' '.repeat(indent - 2)}${chalk.yellow(']')}`, 87 | ].join('\n'); 88 | 89 | const dumpCommand = (command) => chalk.green(command); 90 | 91 | exports.dumpCommand = dumpCommand; 92 | 93 | exports.dumpKey = dumpKey; 94 | 95 | exports.dumpValue = dumpValue; 96 | 97 | exports.dumpObject = dumpObject; 98 | 99 | exports.dumpArray = dumpArray; 100 | -------------------------------------------------------------------------------- /src/ux/dumpAcl.js: -------------------------------------------------------------------------------- 1 | const { table, getBorderCharacters } = require('table'); 2 | const {status } = require('../utils/aclDiff'); 3 | 4 | const tableConfig = { 5 | singleLine: true, 6 | drawHorizontalLine: () => false, 7 | border: getBorderCharacters('void'), 8 | columns: [ 9 | { 10 | paddingLeft: 0, 11 | }, 12 | { 13 | paddingRight: 0, 14 | }, 15 | ], 16 | }; 17 | 18 | const stateChars = { 19 | [status.OK]: '✅', 20 | [status.INVALID]: '❌', 21 | [status.MISSING]: '❌', 22 | [status.PASS]: 'ℹ️', 23 | [status.PRESENT]: 'ℹ️', 24 | }; 25 | 26 | const dumpStatus = (pathStatus) => { 27 | switch (pathStatus) { 28 | // Methods or filters *should* never be invalid 29 | case status.INVALID: 30 | case status.MISMATCH: 31 | return 'mismatch'; 32 | case status.MISSING: 33 | return 'missing in token'; 34 | case status.PRESENT: 35 | return 'present in token'; 36 | default: 37 | return ''; 38 | } 39 | }; 40 | 41 | const dumpAclDiff = ({paths}, infoOnly = false) => { 42 | const rows = Object.entries(paths).map(([path, data]) => { 43 | const {methods, methodsStatus, filtersStatus, state} = data; 44 | const col0 = [ 45 | infoOnly ? stateChars[status.PRESENT] : stateChars[state], 46 | `[${methods}]`, 47 | ].join(' '); 48 | 49 | const col1 = [path]; 50 | 51 | const messageParts = []; 52 | 53 | if ([status.MISSING, status.PRESENT].includes(state)) { 54 | messageParts.push('present in token'); 55 | } 56 | 57 | if (methodsStatus !== status.OK) { 58 | messageParts.push(`methods ${dumpStatus(methodsStatus)}`); 59 | } 60 | 61 | if (filtersStatus === status.MISMATCH 62 | || filtersStatus === status.PRESENT && methodsStatus === status.OK 63 | ) { 64 | messageParts.push(`filters ${dumpStatus(filtersStatus)}`); 65 | } 66 | 67 | if (filtersStatus === status.MISSING) { 68 | messageParts.push('No filter is specified in the token'); 69 | } 70 | 71 | if (messageParts.length) { 72 | col1.push(`(${messageParts.join(' & ')})`); 73 | } 74 | 75 | return [col0, col1.join(' ')]; 76 | }); 77 | 78 | return rows.length > 0 79 | ? table(rows, tableConfig) 80 | .split('\n') 81 | .map((line) => `${line}`.trim()) 82 | .slice(0, -1) 83 | .join('\n') 84 | : ''; 85 | }; 86 | 87 | exports.dumpAclDiff = dumpAclDiff; 88 | -------------------------------------------------------------------------------- /src/ux/dumpAuth.js: -------------------------------------------------------------------------------- 1 | const { Client } = require('@vonage/server-client'); 2 | const { dumpValue } = require('../ux/dump'); 3 | const { descriptionList } = require('../ux/descriptionList'); 4 | const { redact } = require('../ux/redact'); 5 | 6 | exports.dumpAuth = (config, noRedact=false) => { 7 | const dumpConfig = Client.transformers.camelCaseObjectKeys(config); 8 | let privateKey = dumpConfig.privateKey ? 'Is Set' : null; 9 | 10 | console.debug('privateKey', privateKey); 11 | 12 | switch(true) { 13 | case noRedact === true: 14 | privateKey = dumpConfig.privateKey; 15 | break; 16 | 17 | case privateKey === null: 18 | privateKey = null; 19 | break; 20 | 21 | default: 22 | privateKey = dumpConfig.privateKey.startsWith('-----BEGIN PRIVATE KEY-----') 23 | ? 'Is Set' 24 | : 'INVALID KEY'; 25 | break; 26 | } 27 | 28 | const output = {}; 29 | 30 | if (dumpConfig.apiKey) { 31 | output['API Key'] = dumpValue(dumpConfig.apiKey); 32 | } 33 | 34 | if (dumpConfig.apiSecret) { 35 | output['API Secret'] = dumpValue(noRedact 36 | ? dumpConfig.apiSecret 37 | : redact(dumpConfig.apiSecret), 38 | ); 39 | } 40 | 41 | if (dumpConfig.appId) { 42 | output['App ID'] = dumpValue(dumpConfig.appId); 43 | } 44 | 45 | if (dumpConfig.privateKey) { 46 | output['Private Key'] = dumpValue(privateKey); 47 | } 48 | 49 | console.log(descriptionList(output)); 50 | }; 51 | -------------------------------------------------------------------------------- /src/ux/dumpYesNo.js: -------------------------------------------------------------------------------- 1 | const dumpBoolean = ({value, trueWord = 'Yes', falseWord = 'No', includeText=false, noEmoji=false}) => value 2 | ? `${!noEmoji ? '✅ ' : ''}${includeText ? trueWord : ''}` 3 | : `${!noEmoji ? '❌ ' : ''}${includeText ? falseWord : ''}`; 4 | 5 | exports.dumpBoolean = dumpBoolean; 6 | 7 | exports.dumpYesNo = (value, includeText=true) => dumpBoolean({ 8 | value: value, 9 | trueWord: 'Yes', 10 | falseWord: 'No', 11 | includeText: includeText, 12 | }); 13 | 14 | exports.dumpOnOff = (value) => dumpBoolean({ 15 | value: value, 16 | trueWord: 'On', 17 | falseWord: 'Off', 18 | includeText: true, 19 | noEmoji: true, 20 | }); 21 | 22 | exports.dumpEnabledDisabled = (value, includeText=false) => dumpBoolean({ 23 | value: value, 24 | trueWord: 'Enabled', 25 | falseWord: 'Disabled', 26 | includeText: includeText, 27 | }); 28 | 29 | exports.dumpValidInvalid = (value, includeText=false) => dumpBoolean({ 30 | value: value, 31 | trueWord: 'Valid', 32 | falseWord: 'Invalid', 33 | includeText: includeText, 34 | }); 35 | 36 | exports.dumpOffOrValue = (value) => dumpBoolean({ 37 | value: value, 38 | trueWord: value, 39 | falseWord: 'Off', 40 | includeText: true, 41 | noEmoji: true, 42 | }); 43 | -------------------------------------------------------------------------------- /src/ux/indentLines.js: -------------------------------------------------------------------------------- 1 | exports.indentLines = (str, length=2) => str 2 | ? str.split('\n') 3 | .map((line) => ' '.repeat(length) + line) 4 | .join('\n') 5 | : str; 6 | -------------------------------------------------------------------------------- /src/ux/lineBreak.js: -------------------------------------------------------------------------------- 1 | exports.lineBreak = () => { 2 | // TODO check for screen reader 3 | console.log(''); 4 | console.log('--------'); 5 | console.log(''); 6 | }; 7 | -------------------------------------------------------------------------------- /src/ux/progress.js: -------------------------------------------------------------------------------- 1 | exports.progress = ({ 2 | message, 3 | columns, 4 | showSteps = true, 5 | showPercentage = true, 6 | arrowChar = '>', 7 | completedChar = '=', 8 | remainingChar = '.', 9 | openChar = '[', 10 | closeChar = ']', 11 | } = {}) => { 12 | if (!message) { 13 | throw new Error('message is required'); 14 | } 15 | 16 | if (arrowChar.length !== 1) { 17 | throw new Error('arrowChar must be a single character'); 18 | } 19 | 20 | if (completedChar.length !== 1) { 21 | throw new Error('completedChar must be a single character'); 22 | } 23 | 24 | if (remainingChar.length !== 1) { 25 | throw new Error('remainingChar must be a single character'); 26 | } 27 | 28 | if (openChar.length !== 1) { 29 | throw new Error('openChar must be a single character'); 30 | } 31 | 32 | if (closeChar.length !== 1) { 33 | throw new Error('closeChar must be a single character'); 34 | } 35 | 36 | const terminalWidth = columns && columns >= 20 ? columns : process.stderr.columns || 80; 37 | if (message.length + 3 >= terminalWidth) { 38 | throw new Error('message is too long for the terminal width'); 39 | } 40 | 41 | let totalSteps; 42 | let completedSteps = 0; 43 | 44 | const setTotalSteps = (steps) => { 45 | totalSteps = steps; 46 | printProgressBar(); 47 | }; 48 | 49 | const increment = (step = 1) => { 50 | completedSteps += step; 51 | printProgressBar(); 52 | }; 53 | 54 | const finished = () => { 55 | completedSteps = totalSteps; 56 | printProgressBar(); 57 | process.stderr.write('\n'); 58 | }; 59 | 60 | const getProgress = () => [ 61 | ...(showSteps && [`${completedSteps}/${totalSteps}`] || []), 62 | ...(showPercentage && [`${Math.round((completedSteps / totalSteps) * 100)}%`] || []), 63 | ].join(' '); 64 | 65 | const getProgressBarLength = () => { 66 | let progressLength = message.length + 3; 67 | 68 | if (showSteps) { 69 | progressLength += `${totalSteps}`.repeat(2).length + 1; 70 | } 71 | 72 | if (showPercentage) { 73 | progressLength += 5; 74 | } 75 | 76 | return terminalWidth - progressLength - 4; 77 | }; 78 | 79 | const printProgressBar = () => { 80 | if (!totalSteps) { 81 | return; 82 | } 83 | 84 | const progressBarWidth = getProgressBarLength(); 85 | const completedLength = Math.round((completedSteps / totalSteps) * progressBarWidth) || 0; 86 | const remainingLength = progressBarWidth - completedLength; 87 | const arrow = (completedLength === 0 && remainingChar) 88 | || (completedLength < progressBarWidth && arrowChar ) 89 | || completedChar; 90 | 91 | process.stderr.write( 92 | [ 93 | '\r', 94 | message, 95 | ' ', 96 | openChar, 97 | completedChar.repeat(completedLength > -1 ? completedLength : 0), 98 | arrow, 99 | remainingChar.repeat(remainingLength), 100 | closeChar, 101 | ' ', 102 | getProgress(), 103 | ].join(''), 104 | ); 105 | }; 106 | 107 | process.stderr.write(message); 108 | 109 | return { 110 | setTotalSteps, 111 | increment, 112 | finished, 113 | }; 114 | }; 115 | 116 | -------------------------------------------------------------------------------- /src/ux/redact.js: -------------------------------------------------------------------------------- 1 | const redact = (text) => text 2 | ? `${text}`.substring(0, 3) + '*'.repeat(`${text}`.length - 2) 3 | : null; 4 | 5 | exports.redact = redact; 6 | -------------------------------------------------------------------------------- /src/ux/spinner.js: -------------------------------------------------------------------------------- 1 | const { hideCursor, resetCursor } = require('./cursor'); 2 | 3 | const frames = [ 4 | '⠋', 5 | '⠙', 6 | '⠹', 7 | '⠸', 8 | '⠼', 9 | '⠴', 10 | '⠦', 11 | '⠧', 12 | '⠇', 13 | '⠏', 14 | ]; 15 | 16 | exports.spinner = ({ 17 | message, 18 | endEmoji = '✅', 19 | failedEmoji = '❌', 20 | }) => { 21 | hideCursor(); 22 | let counter = 0; 23 | process.stderr.write(`${frames[counter]} ${message}`); 24 | 25 | const intervalId = setInterval(() => { 26 | process.stderr.clearLine(); 27 | process.stderr.write(`\r${frames[counter % 9]} ${message}`); 28 | counter++; 29 | }, 80); 30 | 31 | return { 32 | stop: (endMsg) => { 33 | clearInterval(intervalId); 34 | process.stderr.clearLine(); 35 | process.stderr.write(endMsg 36 | ? `\r${endMsg}\n` 37 | : `\r${endEmoji || ''} ${message}\n`); 38 | resetCursor(); 39 | }, 40 | fail: (failMsg) => { 41 | clearInterval(intervalId); 42 | process.stderr.clearLine(); 43 | process.stderr.write(failMsg 44 | ? `\r${failMsg}\n` 45 | : `\r${failedEmoji || ''} ${message}\n`); 46 | resetCursor(); 47 | }, 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /src/ux/table.js: -------------------------------------------------------------------------------- 1 | const Table = require('easy-table'); 2 | 3 | exports.table = (data) => { 4 | const tbl = new Table; 5 | data.forEach((item) => { 6 | Object.entries(item).forEach(([key, value]) => { 7 | tbl.cell(key, value); 8 | }); 9 | 10 | tbl.newRow(); 11 | }); 12 | return tbl.toString(); 13 | }; 14 | --------------------------------------------------------------------------------