From fc166035e31e57d46ee98fea6034ba8692dacc9e Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 21 Jan 2024 18:16:31 +0200 Subject: [PATCH] Created the httpSMS API Client --- .github/workflows/main.yml | 38 +++++++-------- .gitignore | 3 +- index.js | 7 --- index.ts | 24 +++++++++ license | 20 ++++++-- package.json | 56 ++++++++++++++++----- readme.md | 94 ++++++++++++++++++++++-------------- src/cipher-service.test.ts | 5 ++ src/cipher-service.ts | 35 ++++++++++++++ src/message-service.ts | 21 ++++++++ src/models.ts | 99 ++++++++++++++++++++++++++++++++++++++ test.js | 13 ----- tsconfig.json | 13 +++++ tsup.config.ts | 10 ++++ 14 files changed, 345 insertions(+), 93 deletions(-) delete mode 100644 index.js create mode 100644 index.ts create mode 100644 src/cipher-service.test.ts create mode 100644 src/cipher-service.ts create mode 100644 src/message-service.ts create mode 100644 src/models.ts delete mode 100644 test.js create mode 100644 tsconfig.json create mode 100644 tsup.config.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1ed55d5..463beb8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,22 +1,22 @@ name: CI on: - - push - - pull_request + - push + - pull_request jobs: - test: - name: Node.js ${{ matrix.node-version }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - node-version: - - 20 - - 18 - - 16 - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - - run: npm install - - run: npm test + test: + name: Node.js ${{ matrix.node-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: + - 20 + - 18 + - 16 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test diff --git a/.gitignore b/.gitignore index 239ecff..8bea256 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules -yarn.lock +.idea +dist diff --git a/index.js b/index.js deleted file mode 100644 index ab912a6..0000000 --- a/index.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function unicornFun(input, {postfix = 'rainbows'} = {}) { - if (typeof input !== 'string') { - throw new TypeError(`Expected a string, got ${typeof input}`); - } - - return `${input} & ${postfix}`; -} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..0f979e8 --- /dev/null +++ b/index.ts @@ -0,0 +1,24 @@ +import axios, {type AxiosInstance} from 'axios'; +import MessageService from './src/message-service.js'; +import CipherService from './src/cipher-service.js'; + +class HttpSms { + public messages: MessageService; + public cipher: CipherService; + + private readonly client: AxiosInstance; + + constructor(apiKey: string, baseUrl = 'https://api.httpsms.com') { + this.client = axios.create({ + // eslint-disable-next-line @typescript-eslint/naming-convention + baseURL: baseUrl, + headers: { + 'x-api-key': apiKey, + }, + }); + this.messages = new MessageService(this.client); + this.cipher = new CipherService(); + } +} + +export default HttpSms; diff --git a/license b/license index 81c4848..d35eecd 100644 --- a/license +++ b/license @@ -1,9 +1,21 @@ MIT License -Copyright (c) Your Name (https://yourwebsite.com) +Copyright (c) 2024 Ndole Studio -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/package.json b/package.json index 67b5148..a5e3bb6 100644 --- a/package.json +++ b/package.json @@ -1,32 +1,62 @@ { - "name": "unicorn-fun", - "version": "0.0.0", - "description": "My awesome module", + "name": "httpsms", + "version": "0.0.1", + "description": "Client for the httpSMS API", "license": "MIT", - "repository": "YOUR-GITHUB-USERNAME/unicorn-fun", + "funding": "https://github.com/sponsors/NdoleStudio", + "repository": "https://github.com/NdoleStudio/httpsms-node", "author": { - "name": "YOUR NAME", - "email": "YOUR EMAIL", - "url": "YOUR WEBSITE" + "name": "Arnold Ewin Acho", + "email": "arnold@ndolestudio.com", + "url": "https://acho.arnold.cm" }, "type": "module", - "exports": "./index.js", + "exports": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, "engines": { "node": ">=16" }, "scripts": { - "test": "xo && ava" + "test": "xo --env=node && ava && pnpm run build && tsd --typings dist/index.d.ts", + "build": "del-cli dist && tsc", + "publish": "tsup", + "prepack": "pnpm run build" }, "files": [ - "index.js" + "dist" ], "keywords": [ - "unicorn", - "fun" + "httpsms" ], - "dependencies": {}, "devDependencies": { + "@sindresorhus/tsconfig": "^5.0.0", "ava": "^5.3.0", + "del-cli": "^5.1.0", + "ts-node": "^10.9.2", + "tsd": "^0.30.4", + "tsup": "^8.0.1", "xo": "^0.54.2" + }, + "dependencies": { + "@types/node": "^20.11.5", + "axios": "^1.6.5" + }, + "ava": { + "extensions": { + "ts": "module" + }, + "nodeArguments": [ + "--loader=tsx" + ] + }, + "prettier": { + "trailingComma": "all", + "tabWidth": 4, + "semi": true, + "bracketSpacing": false, + "arrowParens": "avoid", + "singleQuote": true } } diff --git a/readme.md b/readme.md index 126a2f1..f752074 100644 --- a/readme.md +++ b/readme.md @@ -1,65 +1,87 @@ -# node-module-boilerplate +# httpsms-node -> Boilerplate to kickstart creating a Node.js module +[![Version](https://img.shields.io/npm/v/httpsms.svg)](https://www.npmjs.org/package/httpsms) +[![Build](https://github.com/NdoleStudio/httpsms-node/actions/workflows/main.yml/badge.svg)](https://github.com/NdoleStudio/httpsms-node/actions/workflows/main.yml) +[![codecov](https://codecov.io/gh/NdoleStudio/httpsms-node/branch/main/graph/badge.svg)](https://codecov.io/gh/NdoleStudio/httpsms-node) +[![GitHub contributors](https://img.shields.io/github/contributors/NdoleStudio/httpsms-node)](https://github.com/NdoleStudio/httpsms-node/graphs/contributors) +[![GitHub license](https://img.shields.io/github/license/NdoleStudio/httpsms-node?color=brightgreen)](https://github.com/NdoleStudio/httpsms-node/blob/master/LICENSE) +[![Downloads](https://img.shields.io/npm/dm/httpsms.svg)](https://www.npmjs.com/package/httpsms) -This is what I use for [my own modules](https://www.npmjs.com/~sindresorhus). +This httpSMS library provides a server side javascript and typescript client for the [httpSMS](https://httpsms.com/) API. -Also check out [`node-cli-boilerplate`](https://github.com/sindresorhus/node-cli-boilerplate). - -## Getting started - -**Click the "Use this template" button.** - -Alternatively, create a new directory and then run: +## Install ```sh -curl -fsSL https://github.com/sindresorhus/node-module-boilerplate/archive/main.tar.gz | tar -xz --strip-components=1 +pnpm install httpsms-node +# or +npm install httpsms-node +# or +yarn install httpsms-node ``` -There's also a [Yeoman generator](https://github.com/sindresorhus/generator-nm). +## Implemented ---- +- [x] **[MessageService](#messages)** + - [x] `POST /v1/messages/send`: Send a new SMS +- [x] **Cipher** + - [x] `Encrypt`: Encrypt the content of a message to cipher text + - [x] `Decrypt`: Decrypt an encrypted message content to plain text -**Remove everything from here and above** +## Usage ---- +### Initializing the Client -# unicorn-fun +An instance of the client can be created using `httpsms.New()`. -> My awesome module +```go +package main -## Install +import ( + "github.com/NdoleStudio/httpsms-go" +) -```sh -npm install unicorn-fun +func main() { + client := htpsms.New(htpsms.WithDelay(200)) +} ``` -## Usage +### Error handling -```js -import unicornFun from 'unicorn-fun'; +All API calls return an `error` as the last return object. All successful calls will return a `nil` error. -unicornFun('unicorns'); -//=> 'unicorns & rainbows' +```go +_, response, err := client.MessageService.Send(context.Background()) +if err != nil { + //handle error +} ``` -## API +### MessageService -### unicornFun(input, options?) +#### `POST /v1/messages/send`: Send a new SMS Message -#### input +```go +message, response, err := client.MessageService.Send(context.Background(), &MessageSendParams{ + Content: "This is a sample text message", + From: "+18005550199", + To: "+18005550100", +}) -Type: `string` +if err != nil { + log.Fatal(err) +} -Lorem ipsum. +log.Println(message.Code) // 202 +``` -#### options +## Testing -Type: `object` +You can run the unit tests for this client from the root directory using the command below: -##### postfix +```bash +go test -v +``` -Type: `string`\ -Default: `'rainbows'` +## License -Lorem ipsum. +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details diff --git a/src/cipher-service.test.ts b/src/cipher-service.test.ts new file mode 100644 index 0000000..9a78899 --- /dev/null +++ b/src/cipher-service.test.ts @@ -0,0 +1,5 @@ +import test from 'ava'; + +test('cipherService', t => { + t.pass(); +}); diff --git a/src/cipher-service.ts b/src/cipher-service.ts new file mode 100644 index 0000000..20f944a --- /dev/null +++ b/src/cipher-service.ts @@ -0,0 +1,35 @@ +import { + randomBytes, + createCipheriv, + createHash, + createDecipheriv, +} from 'node:crypto'; +import {Buffer} from 'node:buffer'; + +class CipherService { + public encrypt(key: string, message: string): string { + const iv = randomBytes(16); + const cipher = createCipheriv('aes-256-cfb', this.hash(key), iv); + return Buffer.from( + Buffer.concat([iv, cipher.update(message, 'utf8'), cipher.final()]), + ).toString('base64'); + } + + public decrypt(key: string, message: string): string { + const iv = randomBytes(16); + const decipher = createDecipheriv('aes-256-cfb', this.hash(key), iv); + return Buffer.from( + Buffer.concat([ + iv, + decipher.update(message, 'utf8'), + decipher.final(), + ]), + ).toString('utf8'); + } + + private hash(value: string): Buffer { + return createHash('sha256').update(value).digest(); + } +} + +export default CipherService; diff --git a/src/message-service.ts b/src/message-service.ts new file mode 100644 index 0000000..0de9997 --- /dev/null +++ b/src/message-service.ts @@ -0,0 +1,21 @@ +import type {AxiosError, AxiosInstance, AxiosResponse} from 'axios'; +import type {Message, MessageResponse, MessageSendRequest} from './models.js'; + +class MessageService { + constructor(readonly client: AxiosInstance) {} + + public async postSend(request: MessageSendRequest): Promise { + return new Promise((resolve, reject) => { + this.client + .post('/v1/messages/send', request) + .then((response: AxiosResponse) => { + resolve(response.data.data); + }) + .catch(async (error: AxiosError) => { + reject(error); + }); + }); + } +} + +export default MessageService; diff --git a/src/models.ts b/src/models.ts new file mode 100644 index 0000000..6e86747 --- /dev/null +++ b/src/models.ts @@ -0,0 +1,99 @@ +export type MessageSendRequest = { + /** @example "This is a sample text message" */ + content: string; + /** @example "+18005550199" */ + from: string; + /** + * RequestID is an optional parameter used to track a request from the client's perspective + * @example "153554b5-ae44-44a0-8f4f-7bbac5657ad4" + */ + request_id?: string; + /** + * SendAt is an optional parameter used to schedule a message to be sent at a later time + * @example "2022-06-05T14:26:09.527976+03:00" + */ + send_at?: string; + /** @example "+18005550100" */ + to: string; + /** @example false */ + encrypted: boolean; +}; + +export type MessageResponse = { + data: Message; + /** @example "message added to queue" */ + message: string; + /** @example "success" */ + status: string; +}; + +export enum Sim { + SIM1 = 'SIM1', + SIM2 = 'SIM2', +} + +export type Message = { + /** @example false */ + can_be_polled: boolean; + /** @example "+18005550100" */ + contact: string; + /** @example "This is a sample text message" */ + content: string; + /** @example false */ + encrypted: boolean; + /** @example "2022-06-05T14:26:02.302718+03:00" */ + created_at: string; + /** @example "2022-06-05T14:26:09.527976+03:00" */ + delivered_at: string; + /** @example "2022-06-05T14:26:09.527976+03:00" */ + expired_at: string; + /** @example "2022-06-05T14:26:09.527976+03:00" */ + failed_at: string; + /** @example "UNKNOWN" */ + failure_reason: string; + /** @example "32343a19-da5e-4b1b-a767-3298a73703cb" */ + id: string; + /** @example "2022-06-05T14:26:09.527976+03:00" */ + last_attempted_at: string; + /** @example 1 */ + max_send_attempts: number; + /** @example "2022-06-05T14:26:09.527976+03:00" */ + order_timestamp: string; + /** @example "+18005550199" */ + owner: string; + /** @example "2022-06-05T14:26:09.527976+03:00" */ + received_at: string; + /** @example "153554b5-ae44-44a0-8f4f-7bbac5657ad4" */ + request_id: string; + /** @example "2022-06-05T14:26:01.520828+03:00" */ + request_received_at: string; + /** @example "2022-06-05T14:26:09.527976+03:00" */ + scheduled_at: string; + /** @example "2022-06-05T14:26:09.527976+03:00" */ + scheduled_send_time: string; + /** @example 0 */ + send_attempt_count: number; + /** + * SendDuration is the number of nanoseconds from when the request was received until when the mobile phone send the message + * @example 133414 + */ + send_time: number; + /** @example "2022-06-05T14:26:09.527976+03:00" */ + sent_at: string; + /** + * SIM is the SIM card to use to send the message + * * SMS1: use the SIM card in slot 1 + * * SMS2: use the SIM card in slot 2 + * * DEFAULT: used the default communication SIM card + * @example "DEFAULT" + */ + sim: Sim; + /** @example "pending" */ + status: string; + /** @example "mobile-terminated" */ + type: string; + /** @example "2022-06-05T14:26:10.303278+03:00" */ + updated_at: string; + /** @example "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" */ + user_id: string; +}; diff --git a/test.js b/test.js deleted file mode 100644 index 4a65edc..0000000 --- a/test.js +++ /dev/null @@ -1,13 +0,0 @@ -import test from 'ava'; -import unicornFun from './index.js'; - -test('main', t => { - t.throws(() => { - unicornFun(123); - }, { - instanceOf: TypeError, - message: 'Expected a string, got number', - }); - - t.is(unicornFun('unicorns'), 'unicorns & rainbows'); -}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b8dfe5b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@sindresorhus/tsconfig", + "compilerOptions": { + "outDir": "dist", + "experimentalDecorators": true + }, + "files": [ + "index.ts" + ], + "ts-node": { + "transpileOnly": true + } +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..48788a4 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["cjs", "esm"], // Build for commonJS and ESmodules + dts: true, // Generate declaration file (.d.ts) + splitting: false, + sourcemap: true, + clean: true, +});