Comparing version 8.5.0 to 9.0.0
@@ -7,16 +7,7 @@ { | ||
}, | ||
"extends": [ | ||
"eslint:recommended", | ||
"plugin:prettier/recommended" | ||
], | ||
"extends": ["eslint:recommended", "plugin:prettier/recommended"], | ||
"parserOptions": { | ||
"ecmaVersion": 2018, | ||
"ecmaVersion": 2019, | ||
"sourceType": "module" | ||
}, | ||
"rules": { | ||
"require-atomic-updates": [ | ||
0, | ||
"error" | ||
] | ||
} | ||
} | ||
} |
@@ -7,2 +7,9 @@ # Change Log | ||
## [9.0.0](https://github.com/AndrewBarba/apns2/releases/tag/9.0.0) | ||
1. Full code cleanup | ||
2. Removes tarn | ||
3. Requires Node v12 or newer | ||
4. Rename `destroy()` to `close()` | ||
## [8.5.0](https://github.com/AndrewBarba/apns2/releases/tag/8.5.0) | ||
@@ -9,0 +16,0 @@ |
@@ -7,3 +7,3 @@ import { EventEmitter } from "events" | ||
sendMany(notifications: Notification[]): Promise<(Notification | ResponseError)[]> | ||
destroy(): Promise<void> | ||
close(): Promise<void> | ||
} | ||
@@ -10,0 +10,0 @@ |
104
lib/apns.js
const { EventEmitter } = require('events') | ||
const { Pool } = require('tarn') | ||
const jwt = require('jsonwebtoken') | ||
@@ -18,8 +17,2 @@ const Http2Client = require('./http2-client') | ||
* @const | ||
* @desc Number of connections to open up with apns API | ||
*/ | ||
const MAX_CONNECTIONS = 10 | ||
/** | ||
* @const | ||
* @desc Default host to send request | ||
@@ -61,11 +54,3 @@ */ | ||
*/ | ||
constructor({ | ||
team, | ||
keyId, | ||
signingKey, | ||
defaultTopic = null, | ||
host = HOST, | ||
port = PORT, | ||
connections = MAX_CONNECTIONS | ||
}) { | ||
constructor({ team, keyId, signingKey, defaultTopic = null, host = HOST, port = PORT }) { | ||
if (!team) throw new Error(`team is required`) | ||
@@ -79,3 +64,3 @@ if (!keyId) throw new Error(`keyId is required`) | ||
this._defaultTopic = defaultTopic | ||
this._clients = this._createClientPool({ host, port, connections }) | ||
this._client = new Http2Client(host, port) | ||
this._interval = setInterval(() => this._resetSigningToken(), RESET_TOKEN_INTERVAL).unref() | ||
@@ -90,9 +75,4 @@ this.on(Errors.expiredProviderToken, () => this._resetSigningToken()) | ||
*/ | ||
async send(notifications) { | ||
if (Array.isArray(notifications)) { | ||
console.warn('#send(Array<Notification>) is deprecated. Please use #sendMany()') // eslint-disable-line no-console | ||
return this.sendMany(notifications) | ||
} else { | ||
return this._sendOne(notifications) | ||
} | ||
send(notification) { | ||
return this._sendOne(notification) | ||
} | ||
@@ -105,9 +85,5 @@ | ||
*/ | ||
async sendMany(notifications) { | ||
let promises = notifications.map(async (notification) => { | ||
try { | ||
return await this._sendOne(notification) | ||
} catch (error) { | ||
return { error } | ||
} | ||
sendMany(notifications) { | ||
const promises = notifications.map((notification) => { | ||
return this._sendOne(notification).catch((error) => ({ error })) | ||
}) | ||
@@ -118,7 +94,7 @@ return Promise.all(promises) | ||
/** | ||
* @method destroy | ||
* @method close | ||
* @return {Promise} | ||
*/ | ||
async destroy() { | ||
return this._clients.destroy() | ||
close() { | ||
return this._client.close() | ||
} | ||
@@ -132,4 +108,5 @@ | ||
*/ | ||
async _sendOne(notification) { | ||
let options = { | ||
_sendOne(notification) { | ||
const options = { | ||
method: 'POST', | ||
path: `/${API_VERSION}/device/${encodeURIComponent(notification.deviceToken)}`, | ||
@@ -154,8 +131,8 @@ headers: { | ||
let client = await this._acquireClient() | ||
this._releaseClient(client) | ||
let body = JSON.stringify(notification.APNSOptions()) | ||
let res = await client.post(options, body) | ||
return this._handleServerResponse(res, notification) | ||
return this._client | ||
.request({ | ||
...options, | ||
body: JSON.stringify(notification.APNSOptions()) | ||
}) | ||
.then((res) => this._handleServerResponse(res, notification)) | ||
} | ||
@@ -165,37 +142,2 @@ | ||
* @private | ||
* @method _createClientPool | ||
* @param {String} host | ||
* @param {Number} port | ||
* @return {Pool} | ||
*/ | ||
_createClientPool({ host, port, connections }) { | ||
return new Pool({ | ||
create: () => new Http2Client(host, port).connect(), | ||
validate: (client) => client.ready, | ||
destroy: (client) => client.destroy(), | ||
min: 0, | ||
max: connections | ||
}) | ||
} | ||
/** | ||
* @private | ||
* @method _acquireClient | ||
* @return {Promise} | ||
*/ | ||
async _acquireClient() { | ||
return this._clients.acquire().promise | ||
} | ||
/** | ||
* @private | ||
* @method _acquireClient | ||
* @return {Promise} | ||
*/ | ||
_releaseClient(client) { | ||
return this._clients.release(client) | ||
} | ||
/** | ||
* @private | ||
* @method _handleServerResponse | ||
@@ -205,3 +147,3 @@ * @param {ServerResponse} res | ||
*/ | ||
async _handleServerResponse(res, notification) { | ||
_handleServerResponse(res, notification) { | ||
if (res.statusCode === 200) { | ||
@@ -238,3 +180,3 @@ return notification | ||
let claims = { | ||
const claims = { | ||
iss: this._team, | ||
@@ -244,5 +186,5 @@ iat: parseInt(Date.now() / 1000) | ||
let key = this._signingKey | ||
const key = this._signingKey | ||
let options = { | ||
const options = { | ||
algorithm: SIGNING_ALGORITHM, | ||
@@ -249,0 +191,0 @@ header: { |
@@ -1,9 +0,2 @@ | ||
const { URL } = require('url') | ||
const http2 = require('http2') | ||
// Check to make sure this is the native http2 | ||
if (!http2.constants || !http2.constants.NGHTTP2_SESSION_CLIENT) { | ||
throw new Error('Invalid http2 library, must be running Node v10.16 or later') | ||
} | ||
const { | ||
@@ -25,82 +18,28 @@ HTTP2_HEADER_METHOD, | ||
if (!host) throw new Error('host is required') | ||
this._url = new URL(`https://${host}:${port}`) | ||
this._timeout = timeout | ||
this._ready = false | ||
this._session = null | ||
this._client = http2.connect(`https://${host}:${port}`) | ||
} | ||
/** | ||
* @param {Boolean} ready | ||
* @param {ClientHttp2Session} | ||
*/ | ||
get ready() { | ||
return this._ready && this._session && !this._session.destroyed | ||
get client() { | ||
return this._client | ||
} | ||
/** | ||
* @param {Http2Session} | ||
* @param {Number} | ||
*/ | ||
get session() { | ||
return this._session | ||
get timeout() { | ||
return this._timeout | ||
} | ||
/** | ||
* @method connect | ||
* @return {Promise} | ||
* @method close | ||
*/ | ||
async connect() { | ||
return new Promise((resolve, reject) => { | ||
let session = http2.connect(this._url) | ||
session.once('error', reject) | ||
session.once('socketError', reject) | ||
session.once('connect', () => { | ||
this._connected(session) | ||
resolve(this) | ||
}) | ||
}) | ||
close() { | ||
this.client.close() | ||
} | ||
/** | ||
* @method destroy | ||
*/ | ||
destroy() { | ||
if (this._session && !this._session.destroyed) { | ||
this._session.destroy() | ||
} | ||
this._ready = false | ||
this._session = null | ||
} | ||
/** | ||
* @method get | ||
*/ | ||
async get(options) { | ||
options.method = `GET` | ||
return this.request(options) | ||
} | ||
/** | ||
* @method post | ||
*/ | ||
async post(options, body) { | ||
options.method = `POST` | ||
return this.request(options, body) | ||
} | ||
/** | ||
* @method put | ||
*/ | ||
async put(options, body) { | ||
options.method = `PUT` | ||
return this.request(options, body) | ||
} | ||
/** | ||
* @method delete | ||
*/ | ||
async delete(options) { | ||
options.method = `DELETE` | ||
return this.request(options) | ||
} | ||
/** | ||
* @method request | ||
@@ -110,9 +49,9 @@ * @param {Object} options | ||
* @param {String} options.host | ||
* @param {String|Buffer} [body] | ||
* @param {Object} [options.headers] | ||
* @param {String|Buffer} [options.body] | ||
* @return {Promise<ServerResponse>} | ||
*/ | ||
async request({ method, path, headers = {} }, body = null) { | ||
request({ method, path, headers = {}, body = null }) { | ||
if (!method) throw new Error('method is required') | ||
if (!path) throw new Error('path is required') | ||
if (!this._session) throw new Error('Must call connect() before making a request') | ||
@@ -123,18 +62,19 @@ return new Promise((resolve, reject) => { | ||
let req = this._session.request(headers) | ||
const req = this.client.request(headers) | ||
// Cancel request after timeout | ||
req.setTimeout(this._timeout, () => req.close(NGHTTP2_CANCEL)) | ||
req.setTimeout(this.timeout, () => { | ||
req.close(NGHTTP2_CANCEL) | ||
reject(new Error(`http2: timeout ${method} ${path}`)) | ||
}) | ||
// Response handling | ||
req.on('response', (headers) => { | ||
let body = '' | ||
req.on('data', (chunk) => (body += chunk)) | ||
let data = '' | ||
req.on('data', (chunk) => (data += chunk)) | ||
req.on('end', () => { | ||
resolve({ | ||
statusCode: headers[HTTP2_HEADER_STATUS], | ||
headers, | ||
body | ||
headers: headers, | ||
body: data | ||
}) | ||
@@ -146,3 +86,2 @@ }) | ||
req.on('error', reject) | ||
req.on('timeout', () => reject(new Error(`http2: timeout connecting to ${this._url}`))) | ||
@@ -157,19 +96,4 @@ // Post body | ||
} | ||
/** | ||
* @private | ||
* @method _connected | ||
* @param {Http2Session} session | ||
*/ | ||
_connected(session) { | ||
session.on('close', () => this.destroy()) | ||
session.on('frameError', () => this.destroy()) | ||
session.on('goaway', () => this.destroy()) | ||
session.on('socketError', () => this.destroy()) | ||
session.on('timeout', () => this.destroy()) | ||
this._session = session | ||
this._ready = true | ||
} | ||
} | ||
module.exports = HTTP2Client |
{ | ||
"name": "apns2", | ||
"version": "8.5.0", | ||
"version": "9.0.0", | ||
"description": "Node client for connecting to Apple's Push Notification Service using the new HTTP/2 protocol with JSON web tokens.", | ||
"author": "Andrew Barba <abarba.77@gmail.com>", | ||
"author": "Andrew Barba <barba@hey.com>", | ||
"main": "lib/apns.js", | ||
@@ -10,3 +10,3 @@ "types": "index.d.ts", | ||
"engines": { | ||
"node": ">=10.16.0" | ||
"node": ">=12.14.0" | ||
}, | ||
@@ -17,12 +17,12 @@ "repository": { | ||
"dependencies": { | ||
"jsonwebtoken": "^8.5.1", | ||
"tarn": "^3.0.0" | ||
"jsonwebtoken": "^8.5.1" | ||
}, | ||
"devDependencies": { | ||
"eslint": "^7.1.0", | ||
"eslint-config-prettier": "^6.11.0", | ||
"eslint-plugin-prettier": "^3.1.3", | ||
"husky": "^4.0.4", | ||
"mocha": "^7.2.0", | ||
"prettier": "^2.0.5", | ||
"dotenv": "^8.2.0", | ||
"eslint": "^7.12.1", | ||
"eslint-config-prettier": "^6.15.0", | ||
"eslint-plugin-prettier": "^3.1.4", | ||
"husky": "^4.3.0", | ||
"mocha": "^8.2.0", | ||
"prettier": "^2.1.2", | ||
"should": "^13.2.3" | ||
@@ -29,0 +29,0 @@ }, |
# APNS2 | ||
[![wercker status](https://app.wercker.com/status/0e705662e5c35d51a971764fe3e27814/s/master 'wercker status')](https://app.wercker.com/project/byKey/0e705662e5c35d51a971764fe3e27814) | ||
[![npm version](https://badge.fury.io/js/apns2.svg)](https://badge.fury.io/js/apns2) | ||
@@ -9,4 +8,2 @@ [![Twitter](https://img.shields.io/badge/twitter-@andrew_barba-blue.svg?style=flat)](http://twitter.com/andrew_barba) | ||
> Now uses the native `http2` module in Node.js v10.16 or later | ||
--- | ||
@@ -21,3 +18,3 @@ | ||
let client = new APNS({ | ||
const client = new APNS({ | ||
team: `TFLP87PW54`, | ||
@@ -39,3 +36,3 @@ keyId: `123ABC456`, | ||
let bn = new BasicNotification(deviceToken, 'Hello, World') | ||
const bn = new BasicNotification(deviceToken, 'Hello, World') | ||
@@ -54,3 +51,3 @@ try { | ||
let bn = new BasicNotification(deviceToken, 'Hello, World', { | ||
const bn = new BasicNotification(deviceToken, 'Hello, World', { | ||
badge: 4, | ||
@@ -76,3 +73,3 @@ data: { | ||
let sn = new SilentNotification(deviceToken) | ||
const sn = new SilentNotification(deviceToken) | ||
@@ -95,3 +92,3 @@ try { | ||
let notifications = [ | ||
const notifications = [ | ||
new BasicNotification(deviceToken1, 'Hello, World'), | ||
@@ -115,3 +112,3 @@ new BasicNotification(deviceToken2, 'Hello, World') | ||
let notification = new Notification(deviceToken, { | ||
const notification = new Notification(deviceToken, { | ||
aps: { ... } | ||
@@ -151,3 +148,3 @@ }) | ||
## Destroy | ||
## Close Connections | ||
@@ -157,6 +154,6 @@ If you need to close connections to Apple's APNS servers in order to allow the Node process to exit, you can tear down the APNS client: | ||
```javascript | ||
await client.destroy() | ||
await client.close() | ||
``` | ||
Once a client is destroyed you will not be able to use it again. Instead you should instantiate a new client with `new APNS()`. | ||
Once a client is closed you will not be able to use it again. Instead you should instantiate a new client with `new APNS()`. | ||
@@ -168,3 +165,3 @@ ## Environments | ||
```javascript | ||
let client = new APNS({ | ||
const client = new APNS({ | ||
host: 'api.push.apple.com', | ||
@@ -179,3 +176,3 @@ port: 443, | ||
```javascript | ||
let client = new APNS({ | ||
const client = new APNS({ | ||
host: 'api.development.push.apple.com' | ||
@@ -188,2 +185,2 @@ ... | ||
`apns2` requires Node.js v10.16 or later | ||
`apns2` requires Node.js v12.14 or later |
@@ -1,5 +0,2 @@ | ||
const fs = require('fs') | ||
const should = require('should') | ||
// Package | ||
const HTTP2Client = require('../lib/http2-client') | ||
@@ -9,12 +6,11 @@ const { APNS, Notification, BasicNotification, SilentNotification, Errors } = require('../') | ||
describe('http2', () => { | ||
describe('success', () => { | ||
describe('client', () => { | ||
let client | ||
before(async () => { | ||
before(() => { | ||
client = new HTTP2Client('www.google.com', 443) | ||
return client.connect() | ||
}) | ||
it('should make a get request', async () => { | ||
let res = await client.get({ path: '/' }) | ||
const res = await client.request({ method: 'GET', path: '/' }) | ||
res.statusCode.should.equal(200) | ||
@@ -24,27 +20,10 @@ }) | ||
it('should make a post request', async () => { | ||
let res = await client.post({ path: '/' }) | ||
const res = await client.request({ method: 'POST', path: '/' }) | ||
res.statusCode.should.equal(405) | ||
}) | ||
}) | ||
describe('error', () => { | ||
let client | ||
before(() => { | ||
client = new HTTP2Client('bogus.google.com', 443, { timeout: 500 }) | ||
}) | ||
it('should not connect', async () => { | ||
try { | ||
await client.connect() | ||
throw new Error('Should not have worked') | ||
} catch (err) { | ||
should.exist(err) | ||
} | ||
}) | ||
}) | ||
}) | ||
describe('apns', () => { | ||
let deviceToken = `5ab4be4b2e511acfc64405be02a9544295f29b6157b75e3fbc5b2f5300eeda45` | ||
const deviceToken = process.env.APNS_PUSH_TOKEN | ||
@@ -58,5 +37,3 @@ describe('signing token', () => { | ||
keyId: `7U6GT5Q49J`, | ||
signingKey: process.env.APNS_SIGNING_KEY | ||
? process.env.APNS_SIGNING_KEY.replace(/\\n/gi, '\n') | ||
: fs.readFileSync(`${__dirname}/certs/token.p8`, 'utf8'), | ||
signingKey: process.env.APNS_SIGNING_KEY, | ||
defaultTopic: `com.tablelist.Tablelist` | ||
@@ -67,3 +44,3 @@ }) | ||
it('should send a basic notification', async () => { | ||
let basicNotification = new BasicNotification(deviceToken, `Hello, Basic`) | ||
const basicNotification = new BasicNotification(deviceToken, `Hello, Basic`) | ||
return apns.send(basicNotification) | ||
@@ -73,3 +50,3 @@ }) | ||
it('should send a basic notification with options', async () => { | ||
let basicNotification = new BasicNotification(deviceToken, `Hello, 1`, { | ||
const basicNotification = new BasicNotification(deviceToken, `Hello, 1`, { | ||
badge: 1 | ||
@@ -81,3 +58,3 @@ }) | ||
it('should send a basic notification with additional data', async () => { | ||
let basicNotification = new BasicNotification(deviceToken, `Hello, ICON`, { | ||
const basicNotification = new BasicNotification(deviceToken, `Hello, ICON`, { | ||
badge: 0, | ||
@@ -92,3 +69,3 @@ data: { | ||
it('should send a silent notification', async () => { | ||
let silentNotification = new SilentNotification(deviceToken) | ||
const silentNotification = new SilentNotification(deviceToken) | ||
return apns.send(silentNotification) | ||
@@ -98,3 +75,3 @@ }) | ||
it('should send a notification', async () => { | ||
let notification = new Notification(deviceToken, { | ||
const notification = new Notification(deviceToken, { | ||
aps: { | ||
@@ -110,3 +87,3 @@ alert: { | ||
it('should send a notification with a thread-id', async () => { | ||
let notification = new Notification(deviceToken, { | ||
const notification = new Notification(deviceToken, { | ||
aps: { | ||
@@ -123,5 +100,5 @@ alert: { | ||
it('should send both notifications', async () => { | ||
let basicNotification = new BasicNotification(deviceToken, `Hello, Multiple`) | ||
let silentNotification = new SilentNotification(deviceToken) | ||
let results = await apns.sendMany([basicNotification, silentNotification]) | ||
const basicNotification = new BasicNotification(deviceToken, `Hello, Multiple`) | ||
const silentNotification = new SilentNotification(deviceToken) | ||
const results = await apns.sendMany([basicNotification, silentNotification]) | ||
should.exist(results) | ||
@@ -132,7 +109,7 @@ results.length.should.equal(2) | ||
it('should send a lot of notifications', async () => { | ||
let notifications = [] | ||
const notifications = [] | ||
for (let i = 0; i < 500; i++) { | ||
notifications.push(new BasicNotification(deviceToken, `Hello #${i}`)) | ||
} | ||
let results = await apns.sendMany(notifications) | ||
const results = await apns.sendMany(notifications) | ||
should.exist(results) | ||
@@ -143,3 +120,3 @@ results.length.should.equal(notifications.length) | ||
it('should fail to send a notification', async () => { | ||
let noti = new BasicNotification(`fakedevicetoken`, `Hello, bad token`) | ||
const noti = new BasicNotification(`fakedevicetoken`, `Hello, bad token`) | ||
try { | ||
@@ -161,3 +138,3 @@ await apns.send(noti) | ||
let noti = new BasicNotification(`fakedevicetoken`, `Hello, bad token`) | ||
const noti = new BasicNotification(`fakedevicetoken`, `Hello, bad token`) | ||
apns.send(noti).catch(should.exist) | ||
@@ -173,3 +150,3 @@ }) | ||
let noti = new BasicNotification(`fakedevicetoken`, `Hello, bad token`) | ||
const noti = new BasicNotification(`fakedevicetoken`, `Hello, bad token`) | ||
apns.send(noti).catch(should.exist) | ||
@@ -176,0 +153,0 @@ }) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
1
18
3
30483
8
730
175
- Removedtarn@^3.0.0
- Removedtarn@3.0.2(transitive)