From 1f3f4ad0b58abe4d05eedb41c23b2366daf1f487 Mon Sep 17 00:00:00 2001 From: Jarrett Lusso Date: Wed, 4 Jun 2025 12:12:40 -0400 Subject: [PATCH 1/2] Add support for access token authentication - Added ability to specify api_key or access_token at request time - Fixed bug with `yarn test [file]` not running just that file because it was combined with `tsd`. Made them two separate tasks. - Improved tests to catch errors when they don't instead of relying on mocha timeout to fail the test. - Updated CI --- .github/workflows/ci.yml | 5 +- README.md | 24 ++++++--- lib/client.js | 18 ++++--- lib/emailable.d.ts | 4 +- lib/emailable.js | 6 +-- package.json | 6 ++- test/account.spec.js | 2 +- test/authentication.spec.js | 102 ++++++++++++++++++++++++++++++++++++ test/batches.spec.js | 10 ++-- test/emailable.test-d.ts | 1 + test/verify.spec.js | 5 +- 11 files changed, 155 insertions(+), 28 deletions(-) create mode 100644 test/authentication.spec.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4437331..7fefba2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.x, 20.x] + node-version: [18.x, 20.x, 22.x, 23.x, 24.x] fail-fast: false steps: - uses: actions/checkout@v4 @@ -29,6 +29,9 @@ jobs: - name: Run tests run: yarn test + - name: Run type definition tests + run: yarn test:types + linters: name: Linters runs-on: ubuntu-latest diff --git a/README.md b/README.md index e1e4e81..befcba8 100644 --- a/README.md +++ b/README.md @@ -24,19 +24,31 @@ yarn add emailable ## Usage -The library needs to be configured with your account's API key which is -available in your [Emailable Dashboard](https://app.emailable.com/api). Require -it with your API key: +### Authentication -### Setup +The Emailable API requires either an API key or an access token for +authentication. API keys can be created and managed in the +[Emailable Dashboard](https://app.emailable.com/api). + +An API key can be set globally for the Emailable client: ```javascript // require with API key -var emailable = require('emailable')('live_...') +var emailable = require('emailable')('your_api_key') // ES6 import import Emailable from 'emailable'; -const emailable = Emailable('live_...'); +const emailable = Emailable('your_api_key'); +``` + +Or, you can specify an `apiKey` or an `accessToken` with each request: + +```javascript +// set api_key at request time +emailable.verify({ apiKey: 'your_api_key' }) + +// set access_token at request time +emailable.verify({ accessToken: 'your_api_key' }) ``` ### Verification diff --git a/lib/client.js b/lib/client.js index bf79de1..68120cb 100644 --- a/lib/client.js +++ b/lib/client.js @@ -4,16 +4,18 @@ const axios = require('axios') class Client { - constructor(key) { + constructor(apiKey = null) { this.instance = axios.create({ baseURL: 'https://api.emailable.com/v1/' }) - if (key) { - this.instance.defaults.headers.common['Authorization'] = `Bearer ${key}` - } + this.apiKey = apiKey } makeGetRequest(endpoint, params = {}) { + const { apiKey, accessToken, ...filteredParams } = params + const key = apiKey || accessToken || this.apiKey + const headers = key ? { Authorization: `Bearer ${key}` } : {} + return new Promise((resolve, reject) => { - this.instance.get(endpoint, { params: params }) + this.instance.get(endpoint, { params: filteredParams, headers: headers }) .then(response => resolve(response.data)) .catch(error => { if (error.response) { @@ -29,8 +31,12 @@ class Client { } makePostRequest(endpoint, data = {}) { + const { apiKey, accessToken, ...filteredData } = data + const key = apiKey || accessToken || this.apiKey + const headers = key ? { Authorization: `Bearer ${key}` } : {} + return new Promise((resolve, reject) => { - this.instance.post(endpoint, data) + this.instance.post(endpoint, filteredData, { headers: headers}) .then(response => resolve(response.data)) .catch(error => { reject({ diff --git a/lib/emailable.d.ts b/lib/emailable.d.ts index eee4135..07baa65 100644 --- a/lib/emailable.d.ts +++ b/lib/emailable.d.ts @@ -19,11 +19,11 @@ declare class Emailable { verify(email: string, options?: {}): Promise - account(): Promise + account(options?: {}): Promise readonly client: Client readonly batches: Batches } -declare function _exports(apiKey: any): Emailable +declare function _exports(apiKey?: any): Emailable export = _exports diff --git a/lib/emailable.js b/lib/emailable.js index f250c57..b230875 100644 --- a/lib/emailable.js +++ b/lib/emailable.js @@ -5,7 +5,7 @@ const Batches = require('./batches') class Emailable { - constructor(apiKey) { + constructor(apiKey = null) { this.client = new Client(apiKey) this.batches = new Batches(this.client) } @@ -14,8 +14,8 @@ class Emailable { return this.client.makePostRequest('verify', { email: email, ...options }) } - account() { - return this.client.makeGetRequest('account') + account(options = {}) { + return this.client.makeGetRequest('account', options) } } diff --git a/package.json b/package.json index b06b310..b8c00cb 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "lib/emailable.js", "types": "lib/emailable.d.ts", "scripts": { - "test": "mocha --timeout 10000 && tsd", + "test": "mocha --timeout 10000", + "test:types": "tsd", "lint": "eslint" }, "tsd": { @@ -40,5 +41,6 @@ }, "dependencies": { "axios": "^1.6.0" - } + }, + "packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72" } diff --git a/test/account.spec.js b/test/account.spec.js index 0158ab9..46fdfbe 100644 --- a/test/account.spec.js +++ b/test/account.spec.js @@ -10,7 +10,7 @@ describe('emailable.account()', () => { expect(response.owner_email).to.be.a('string') expect(response.available_credits).to.be.a('number') done() - }) + }).catch(done) }) it('should return a 401 status code when no API key', done => { diff --git a/test/authentication.spec.js b/test/authentication.spec.js new file mode 100644 index 0000000..106f671 --- /dev/null +++ b/test/authentication.spec.js @@ -0,0 +1,102 @@ +'use strict' + +const expect = require('chai').expect +const apiKey = 'test_7aff7fc0142c65f86a00' +const email = 'jarrett@emailable.com' +const emails = ['jarrett@emailable.com', 'support@emailable.com'] + +describe('authentication', () => { + + it('should authenticate with a global api key configured', done => { + const emailable = require('../lib/emailable')(apiKey) + + Promise.all([ + emailable.verify(email).then(response => { + expect(response.domain).to.be.a('string') + }), + + emailable.account().then(response => { + expect(response.owner_email).to.be.a('string') + }), + + emailable.batches.verify(emails) + .then(response => { + expect(response.id).to.have.lengthOf(24) + return emailable.batches.status(response.id) + }) + .then(statusResponse => { + expect(statusResponse.id).to.be.a('string') + }) + ]) + .then(() => done()) + .catch(done) + }) + + it('should authenticate with an api key passed in at request time', done => { + const emailable = require('../lib/emailable')() + + Promise.all([ + emailable.verify(email, { apiKey: apiKey }).then(response => { + expect(response.domain).to.be.a('string') + }), + + emailable.account({ apiKey: apiKey }).then(response => { + expect(response.owner_email).to.be.a('string') + }), + + emailable.batches.verify(emails, { apiKey: apiKey }) + .then(response => { + expect(response.id).to.have.lengthOf(24) + return emailable.batches.status(response.id, { apiKey: apiKey }) + }) + .then(statusResponse => { + expect(statusResponse.id).to.be.a('string') + }) + ]) + .then(() => done()) + .catch(done) + }) + + it('should prioritize request time authentication over global', done => { + const emailable = require('../lib/emailable')('invalid_api_key') + + Promise.all([ + emailable.verify(email, { apiKey: apiKey }).then(response => { + expect(response.domain).to.be.a('string') + }), + + emailable.account({ apiKey: apiKey }).then(response => { + expect(response.owner_email).to.be.a('string') + }), + + emailable.batches.verify(emails, { apiKey: apiKey }) + .then(response => { + expect(response.id).to.have.lengthOf(24) + return emailable.batches.status(response.id, { apiKey: apiKey }) + }) + .then(statusResponse => { + expect(statusResponse.id).to.be.a('string') + }) + ]) + .then(() => done()) + .catch(done) + }) + + it('should not modify the original params object passed in', done => { + const emailable = require('../lib/emailable')() + const params = { apiKey: apiKey } + + Promise.all([ + emailable.verify(email, params).then(() => { + expect(params.apiKey).to.equal(apiKey) + }), + + emailable.account(params).then(() => { + expect(params.apiKey).to.equal(apiKey) + }) + ]) + .then(() => done()) + .catch(done) + }) + +}) diff --git a/test/batches.spec.js b/test/batches.spec.js index 373dbae..148863b 100644 --- a/test/batches.spec.js +++ b/test/batches.spec.js @@ -10,7 +10,7 @@ describe('emailable.batches.verify()', () => { emailable.batches.verify(emails).then(response => { expect(response.id).to.have.lengthOf(24) done() - }) + }).catch(done) }) it('should return a payment error when passed { simulate: "payment_error" }', done => { @@ -32,8 +32,8 @@ describe('emailable.batches.status()', () => { expect(response.reason_counts).to.be.a('object') expect(response.message).to.be.a('string') done() - }) - }) + }).catch(done) + }).catch(done) }) it('should return verifying response when passed { simulate: "verifying" }', done => { @@ -43,8 +43,8 @@ describe('emailable.batches.status()', () => { expect(response.total).to.be.a('number') expect(response.message).to.be.a('string') done() - }) - }) + }).catch(done) + }).catch(done) }) }) diff --git a/test/emailable.test-d.ts b/test/emailable.test-d.ts index 7778500..ce57172 100644 --- a/test/emailable.test-d.ts +++ b/test/emailable.test-d.ts @@ -6,6 +6,7 @@ const emailable = Emailable('test_xxxxxxxxxx') expectType>(emailable.verify('deliverable@example.com')) expectType>(emailable.verify('deliverable@example.com', { accept_all: true })) expectType>(emailable.account()) +expectType>(emailable.account({ apiKey: 'test_xxxxxxxxxx' })) expectType>(emailable.batches.verify(['deliverable@example.com'])) expectType>(emailable.batches.verify(['deliverable@example.com'], { simulate: 'verifying' })) expectType>(emailable.batches.status('xxxxxxxxxx')) diff --git a/test/verify.spec.js b/test/verify.spec.js index 4dc2d61..945a5ac 100644 --- a/test/verify.spec.js +++ b/test/verify.spec.js @@ -15,7 +15,7 @@ describe('emailable.verify()', () => { expect(response.user).to.be.a('string') expect(response.duration).to.be.a('number') done() - }) + }).catch(done) }) it('should return a valid state', done => { @@ -23,7 +23,7 @@ describe('emailable.verify()', () => { emailable.verify('deliverable@example.com').then(response => { expect(states.includes(response.state)).to.be.equal(true) done() - }) + }).catch(done) }) it('should verify an email with accept-all enabled', done => { @@ -32,6 +32,7 @@ describe('emailable.verify()', () => { expect(response.accept_all).to.be.equal(true) done() }) + .catch(done) }) }) From 5d2cde976d7b7ae1791b72bd29a247b84df7c0aa Mon Sep 17 00:00:00 2001 From: Jarrett Lusso Date: Wed, 4 Jun 2025 14:30:14 -0400 Subject: [PATCH 2/2] Fixed space --- lib/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/client.js b/lib/client.js index 68120cb..98badd0 100644 --- a/lib/client.js +++ b/lib/client.js @@ -36,7 +36,7 @@ class Client { const headers = key ? { Authorization: `Bearer ${key}` } : {} return new Promise((resolve, reject) => { - this.instance.post(endpoint, filteredData, { headers: headers}) + this.instance.post(endpoint, filteredData, { headers: headers }) .then(response => resolve(response.data)) .catch(error => { reject({