-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.js
179 lines (163 loc) · 5.11 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
const jwt = require('jsonwebtoken');
const http2 = require('http2');
const querystring = require('querystring');
module.exports = class ApplePush {
/**
* Constructor method for the ApplePush class.
* Will set the `url` attribute to the sandbox or production
* urls for the APNS server depending upon the value
* of the NODE_ENV environment variable.
*
* @return {ApplePush} A new instance of type ApplePush
*/
constructor() {
if (process.env.NODE_ENV === "production") {
this.url = "https://api.push.apple.com";
} else {
this.url = "https://api.sandbox.push.apple.com";
}
/**
* A map of the various network errors that can be encountered
* while sending out the push to APNS.
*
* @type {Object}
*/
this.ERRORS = {
400: "Bad request",
403: "There was an error with the certificate or with the provider’s authentication token.",
405: "The request used an invalid :method value. Only POST requests are supported.",
410: "The device token is no longer active for the topic.",
413: "The notification payload was too large.",
429: "The server received too many requests for the same device token.",
500: "Internal server error.",
503: "The server is shutting down and unavailable."
};
}
/**
* Sends a new push notification via the APNS service
* The method uses the APNS service in a stateless manner making use of a shortlived
* HTTP/2 session.
*
* @param {Any} payload - Can be a `string` or `object` to be posted
* @param {String} jwt - json web token to sent for authentication
*
* @return {Promise} A promise that resolves if the request is successful or rejects
* with an error
*/
push (payload, jwt, deviceToken, bundleId, options) {
return new Promise((resolve, reject) => {
if (!payload) {
reject(Error('Parameter `payload` is required'));
return;
}
if (!jwt) {
reject(Error('Parameter `jwt` is required'));
return;
}
if (!deviceToken) {
reject(Error('Parameter `deviceToken` is required'));
return;
}
if (!bundleId) {
reject(Error('Parameter `bundleId` is required'));
return;
}
const session = http2.connect(this.url);
const sessionErrorHandler = (error) => {
session.destroy();
reject(error);
}
session.on('error', sessionErrorHandler);
session.on('socketError', sessionErrorHandler);
session.on('goaway', sessionErrorHandler);
let headers = {
':path': `/3/device/${deviceToken}`,
':method': 'POST',
'authorization': `bearer ${jwt}`,
'apns-topic': bundleId
};
if (options) {
if (options.id) { headers['apns-id'] = options.id; }
if (options.expiration) { headers['apns-expiration'] = options.expiration; }
if (options.priority) { headers['apns-priority'] = options.priority; }
if (options.collapseId) { headers['apns-collapse-id'] = options.collapseId; }
}
const req = session.request(headers);
req.on('aborted', error => {
req.close();
sessionErrorHandler(error);
});
req.on('response', (headers, flags) => {
let data = '';
req.on('data', chunk => { data += chunk; }).on('end', () => {
if (headers[":status"] === 200) {
resolve({
"status": 200,
"apns-id": headers['apns-id']
});
} else {
data = JSON.parse(data);
let error = undefined;
let errorText = this.ERRORS[headers[":status"]];
if (errorText) {
error = new Error(errorText);
error.reason = data.reason;
error["apns-id"] = headers["apns-id"];
} else {
error = new Error(`Remote server responded with error code ${headers[':status']}`)
}
reject(error);
}
session.destroy();
});
});
const postbody = querystring.stringify(payload);
req.end(postbody);
});
}
/**
* Create a new JWT according to the APNS specifications.
*
* @param {String} teamId - The teamId of the organization
* @param {String} keyId - The key id for the application
* @param {String} key - The key to be used for signing
*
* @return {Promise} A Promise that resolves with the JWT
* or rejects if there was an error
*/
createToken(teamId, keyId, key) {
return new Promise((resolve, reject) => {
if (!teamId) {
reject( new Error('Parameter `teamId` is required') );
return;
}
if (!keyId) {
reject( new Error('Parameter `keyId` is required') );
return;
}
if (!key || key.trim() === "") {
reject( new Error('Parameter `key` is required') );
return;
}
let signingOptions = {
issuer: teamId,
algorithm: 'ES256',
header: {
kid: keyId
}
};
jwt.sign({}, key, signingOptions, (err, token) => {
if (err) {
reject(err);
} else {
try {
const result = token;
resolve(result);
} catch(error) {
reject(error);
}
}
});
});
}
}