app-store-server-api
Version:
A client for the App Store Server API
218 lines (217 loc) • 9.11 kB
JavaScript
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