@fivesheepco/cloudflare-apns2
Version:
Client for connecting to Apple's Push Notification Service using the new HTTP/2 protocol with JSON web tokens.
104 lines (103 loc) • 3.71 kB
JavaScript
import { sign } from "@tsndr/cloudflare-worker-jwt";
import { ApnsError, Errors } from "./errors.js";
import { Priority } from "./notifications/notification.js";
// APNS version
const API_VERSION = 3;
// Signing algorithm for JSON web token
const SIGNING_ALGORITHM = "ES256";
// Reset our signing token every 55 minutes as reccomended by Apple
const RESET_TOKEN_INTERVAL_MS = 55 * 60 * 1000;
export var Host;
(function (Host) {
Host["production"] = "api.push.apple.com";
Host["development"] = "api.sandbox.push.apple.com";
})(Host || (Host = {}));
export class ApnsClient {
team;
keyId;
host;
signingKey;
defaultTopic;
keepAlive;
_token;
constructor(options) {
this.team = options.team;
this.keyId = options.keyId;
this.signingKey = options.signingKey;
this.defaultTopic = options.defaultTopic;
this.host = options.host ?? Host.production;
this._token = null;
}
sendMany(notifications) {
const promises = notifications.map((notification) => this.send(notification).catch((error) => ({ error })));
return Promise.all(promises);
}
async send(notification) {
const headers = new Headers();
headers.set("authorization", `bearer ${await this._getSigningToken()}`);
headers.set("apns-push-type", notification.pushType);
const apnsTopic = notification.options.topic ?? this.defaultTopic;
if (apnsTopic) {
headers.set("apns-topic", apnsTopic);
}
if (notification.priority !== Priority.immediate) {
headers.set("apns-priority", notification.priority.toString());
}
const expiration = notification.options.expiration;
if (typeof expiration !== "undefined") {
const expirationValue = typeof expiration === "number"
? expiration.toFixed(0)
: (expiration.getTime() / 1000).toFixed(0);
headers.set("apns-expiration", expirationValue);
}
if (notification.options.collapseId) {
headers.set("apns-collapse-id", notification.options.collapseId);
}
const url = `https://${this.host}:443/${API_VERSION}/device/${encodeURIComponent(notification.deviceToken)}`;
const res = await fetch(url, {
method: "POST",
headers: headers,
body: JSON.stringify(notification.buildApnsOptions()),
});
return this._handleServerResponse(res, notification);
}
async _handleServerResponse(res, notification) {
if (res.status === 200) {
return notification;
}
const responseError = await res.json().catch(() => ({
reason: Errors.unknownError,
timestamp: Date.now(),
}));
const error = new ApnsError({
statusCode: res.status,
notification: notification,
response: responseError,
});
// Reset signing token if expired
if (error.reason === Errors.expiredProviderToken) {
this._token = null;
}
throw error;
}
async _getSigningToken() {
if (this._token && Date.now() - this._token.timestamp < RESET_TOKEN_INTERVAL_MS) {
return this._token.value;
}
const claims = {
iss: this.team,
iat: Math.floor(Date.now() / 1000),
};
const token = await sign(claims, this.signingKey, {
algorithm: SIGNING_ALGORITHM,
header: {
kid: this.keyId,
},
});
this._token = {
value: token,
timestamp: Date.now(),
};
return token;
}
}