UNPKG

app-store-server-api

Version:
218 lines (217 loc) 9.11 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import * as jose from "jose"; import { randomUUID } from "crypto"; import { AppStoreError } from "./Errors"; import { Environment, TransactionHistoryVersion } from "./Models"; export class AppStoreServerAPI { /** * @param key the key downloaded from App Store Connect in PEM-encoded PKCS8 format. * @param keyId the id of the key, retrieved from App Store Connect * @param issuerId your issuer ID, retrieved from App Store Connect * @param bundleId bundle ID of your app */ constructor(key, keyId, issuerId, bundleId, environment = Environment.Production) { this.tokenExpiry = new Date(0); this.key = jose.importPKCS8(key, "ES256"); this.keyId = keyId; this.issuerId = issuerId; this.bundleId = bundleId; this.environment = environment; if (environment === Environment.Sandbox) { this.baseUrl = "https://api.storekit-sandbox.itunes.apple.com"; } else { this.baseUrl = "https://api.storekit.itunes.apple.com"; } } // API Endpoints /** * https://developer.apple.com/documentation/appstoreserverapi/get_transaction_history */ getTransactionHistory(transactionId, query = {}, version = TransactionHistoryVersion.v1) { return __awaiter(this, void 0, void 0, function* () { const path = this.addQuery(`/inApps/${version}/history/${transactionId}`, Object.assign({}, query)); return this.makeRequest("GET", path); }); } /** * https://developer.apple.com/documentation/appstoreserverapi/get_transaction_info */ getTransactionInfo(transactionId) { return __awaiter(this, void 0, void 0, function* () { return this.makeRequest("GET", `/inApps/v1/transactions/${transactionId}`); }); } /** * https://developer.apple.com/documentation/appstoreserverapi/get_all_subscription_statuses */ getSubscriptionStatuses(transactionId, query = {}) { return __awaiter(this, void 0, void 0, function* () { const path = this.addQuery(`/inApps/v1/subscriptions/${transactionId}`, Object.assign({}, query)); return this.makeRequest("GET", path); }); } /** * https://developer.apple.com/documentation/appstoreserverapi/look_up_order_id */ lookupOrder(orderId) { return __awaiter(this, void 0, void 0, function* () { return this.makeRequest("GET", `/inApps/v1/lookup/${orderId}`); }); } /** * https://developer.apple.com/documentation/appstoreserverapi/extend_a_subscription_renewal_date */ extendSubscriptionRenewalDate(originalTransactionId, request) { return __awaiter(this, void 0, void 0, function* () { return this.makeRequest("PUT", `/inApps/v1/subscriptions/extend/${originalTransactionId}`, request); }); } /** * https://developer.apple.com/documentation/appstoreserverapi/put-v1-transactions-consumption-_transactionid_ */ sendConsumptionInformation(transactionId, request) { return __awaiter(this, void 0, void 0, function* () { return this.makeRequest("PUT", `/inApps/v1/transactions/consumption/${transactionId}`, request); }); } /** * https://developer.apple.com/documentation/appstoreserverapi/request_a_test_notification */ requestTestNotification() { return __awaiter(this, void 0, void 0, function* () { return this.makeRequest("POST", "/inApps/v1/notifications/test"); }); } /** * https://developer.apple.com/documentation/appstoreserverapi/get_test_notification_status */ getTestNotificationStatus(id) { return __awaiter(this, void 0, void 0, function* () { return this.makeRequest("GET", `/inApps/v1/notifications/test/${id}`); }); } /** * https://developer.apple.com/documentation/appstoreserverapi/get_notification_history */ getNotificationHistory(request, query = {}) { return __awaiter(this, void 0, void 0, function* () { const path = this.addQuery("/inApps/v1/notifications/history", Object.assign({}, query)); return this.makeRequest("POST", path, request); }); } /** * Performs a network request against the API and handles the result. */ makeRequest(method, path, body) { return __awaiter(this, void 0, void 0, function* () { const token = yield this.getToken(); const url = this.baseUrl + path; const serializedBody = body ? JSON.stringify(body) : undefined; const result = yield fetch(url, { method: method, body: serializedBody, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" } }); switch (result.status) { case 200: return result.json(); case 202: return; case 400: case 403: case 404: case 429: case 500: const body = yield result.json(); let retryAfter; let retryAfterHeader = result.headers.get("retry-after"); if (result.status === 429 && retryAfterHeader !== null) { retryAfter = parseInt(retryAfterHeader); } throw new AppStoreError(body.errorCode, body.errorMessage, retryAfter); case 401: this.token = undefined; throw new Error("The request is unauthorized; the JSON Web Token (JWT) is invalid."); default: throw new Error("An unknown error occurred"); } }); } /** * Returns an existing authentication token (if its still valid) or generates a new one. */ getToken() { return __awaiter(this, void 0, void 0, function* () { // Reuse previously created token if it hasn't expired. if (this.token && !this.tokenExpired) return this.token; // Tokens must expire after at most 1 hour. const now = new Date(); const expiry = new Date(now.getTime() + AppStoreServerAPI.maxTokenAge * 1000); const expirySeconds = Math.floor(expiry.getTime() / 1000); const payload = { bid: this.bundleId, nonce: randomUUID() }; const privateKey = yield this.key; const jwt = yield new jose.SignJWT(payload) .setProtectedHeader({ alg: "ES256", kid: this.keyId, typ: "JWT" }) .setIssuer(this.issuerId) .setIssuedAt() .setExpirationTime(expirySeconds) .setAudience("appstoreconnect-v1") .sign(privateKey); this.token = jwt; this.tokenExpiry = expiry; return jwt; }); } /** * Returns whether the previously generated token can still be used. */ get tokenExpired() { // We consider the token to be expired slightly before it actually is to allow for some networking latency. const headroom = 60; // seconds const now = new Date(); const cutoff = new Date(now.getTime() - headroom * 1000); return !this.tokenExpiry || this.tokenExpiry < cutoff; } /** * Serializes a query object into a query string and appends it * the provided path. */ addQuery(path, query) { const params = new URLSearchParams(); for (const [key, value] of Object.entries(query)) { if (Array.isArray(value)) { for (const item of value) { params.append(key, item.toString()); } } else { params.set(key, value.toString()); } } const queryString = params.toString(); if (queryString === "") { return path; } else { return `${path}?${queryString}`; } } } /** * The maximum age that an authentication token is allowed to have, as decided by Apple. */ AppStoreServerAPI.maxTokenAge = 3600; // seconds, = 1 hour