UNPKG

@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
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; } }