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..98badd0 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) }) })