Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Apple App Store Server Node.js Library
The Node.js server library for the [App Store Server API](https://developer.apple.com/documentation/appstoreserverapi) and [App Store Server Notifications](https://developer.apple.com/documentation/appstoreservernotifications). Also available in [Swift](https://github.com/apple/app-store-server-library-swift), [Python](https://github.com/apple/app-store-server-library-python), and [Java](https://github.com/apple/app-store-server-library-java).
The Node.js server library for the [App Store Server API](https://developer.apple.com/documentation/appstoreserverapi), [App Store Server Notifications](https://developer.apple.com/documentation/appstoreservernotifications), and [Retention Messaging API](https://developer.apple.com/documentation/retentionmessaging). Also available in [Swift](https://github.com/apple/app-store-server-library-swift), [Python](https://github.com/apple/app-store-server-library-python), and [Java](https://github.com/apple/app-store-server-library-java).

## Table of Contents
1. [Installation](#installation)
Expand Down
260 changes: 238 additions & 22 deletions index.ts

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions jws_verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Environment } from './models/Environment';
import { JWSTransactionDecodedPayload, JWSTransactionDecodedPayloadValidator } from './models/JWSTransactionDecodedPayload';
import { ResponseBodyV2DecodedPayload, ResponseBodyV2DecodedPayloadValidator } from './models/ResponseBodyV2DecodedPayload';
import { JWSRenewalInfoDecodedPayload, JWSRenewalInfoDecodedPayloadValidator } from './models/JWSRenewalInfoDecodedPayload';
import { DecodedRealtimeRequestBody, DecodedRealtimeRequestBodyValidator } from './models/DecodedRealtimeRequestBody';
import { Validator } from './models/Validator';
import { DecodedSignedData } from './models/DecodedSignedData';
import { AppTransaction, AppTransactionValidator } from './models/AppTransaction';
Expand Down Expand Up @@ -49,6 +50,7 @@ export class SignedDataVerifier {
private JWSTransactionDecodedPayloadValidator = new JWSTransactionDecodedPayloadValidator()
private responseBodyV2DecodedPayloadValidator = new ResponseBodyV2DecodedPayloadValidator()
private appTransactionValidator = new AppTransactionValidator()
private decodedRealtimeRequestBodyValidator = new DecodedRealtimeRequestBodyValidator()

protected rootCertificates: X509Certificate[]
protected enableOnlineChecks: boolean
Expand Down Expand Up @@ -176,6 +178,25 @@ export class SignedDataVerifier {
return decodedAppTransaction
}

/**
* Verifies and decodes a Retention Messaging API signedPayload
* See {@link https://developer.apple.com/documentation/retentionmessaging/signedpayload signedPayload}
*
* @param signedPayload The payload received by your server
* @returns The decoded payload after verification
* @throws VerificationException Thrown if the data could not be verified
*/
async verifyAndDecodeRealtimeRequest(signedPayload: string): Promise<DecodedRealtimeRequestBody> {
const decodedRequest: DecodedRealtimeRequestBody = await this.verifyJWT(signedPayload, this.decodedRealtimeRequestBodyValidator, this.extractSignedDate);
if (this.environment === Environment.PRODUCTION && this.appAppleId !== decodedRequest.appAppleId) {
throw new VerificationException(VerificationStatus.INVALID_APP_IDENTIFIER)
}
if (this.environment !== decodedRequest.environment) {
throw new VerificationException(VerificationStatus.INVALID_ENVIRONMENT)
}
return decodedRequest
}

protected async verifyJWT<T>(jwt: string, validator: Validator<T>, signedDateExtractor: (decodedJWT: T) => Date): Promise<T> {
let certificateChain;
let decodedJWT
Expand Down
23 changes: 23 additions & 0 deletions models/AlternateProduct.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.

/**
* A switch-plan message and product ID you provide in a real-time response to your Get Retention Message endpoint.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/alternateproduct alternateProduct}
*/
export interface AlternateProduct {

/**
* The message identifier of the text to display in the switch-plan retention message.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/messageidentifier messageIdentifier}
**/
messageIdentifier?: string

/**
* The product identifier of the subscription the retention message suggests for your customer to switch to.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/productid productId}
**/
productId?: string
}
91 changes: 91 additions & 0 deletions models/DecodedRealtimeRequestBody.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.

import { DecodedSignedData } from "./DecodedSignedData"
import { Environment, EnvironmentValidator } from "./Environment"
import { Validator } from "./Validator"

/**
* The decoded request body the App Store sends to your server to request a real-time retention message.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/decodedrealtimerequestbody DecodedRealtimeRequestBody}
*/
export interface DecodedRealtimeRequestBody extends DecodedSignedData {

/**
* The original transaction identifier of the customer's subscription.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/originaltransactionid originalTransactionId}
**/
originalTransactionId: string

/**
* The unique identifier of the app in the App Store.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/appappleid appAppleId}
**/
appAppleId: number

/**
* The unique identifier of the auto-renewable subscription.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/productid productId}
**/
productId: string

/**
* The device's locale.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/locale locale}
**/
userLocale: string

/**
* A UUID the App Store server creates to uniquely identify each request.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/requestidentifier requestIdentifier}
**/
requestIdentifier: string

/**
* The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature (JWS) data.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/signeddate signedDate}
**/
signedDate: number

/**
* The server environment, either sandbox or production.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/environment environment}
**/
environment: Environment | string
}


export class DecodedRealtimeRequestBodyValidator implements Validator<DecodedRealtimeRequestBody> {
static readonly environmentValidator = new EnvironmentValidator()
validate(obj: any): obj is DecodedRealtimeRequestBody {
if (!(typeof obj['originalTransactionId'] === "string" || obj['originalTransactionId'] instanceof String)) {
return false
}
if (!(typeof obj['appAppleId'] === "number")) {
return false
}
if (!(typeof obj['productId'] === "string" || obj['productId'] instanceof String)) {
return false
}
if (!(typeof obj['userLocale'] === "string" || obj['userLocale'] instanceof String)) {
return false
}
if (!(typeof obj['requestIdentifier'] === "string" || obj['requestIdentifier'] instanceof String)) {
return false
}
if (!(typeof obj['signedDate'] === "number")) {
return false
}
if (!(DecodedRealtimeRequestBodyValidator.environmentValidator.validate(obj['environment']))) {
return false
}
return true
}
}
16 changes: 16 additions & 0 deletions models/DefaultConfigurationRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.

/**
* The request body that contains the default configuration information.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/defaultconfigurationrequest DefaultConfigurationRequest}
*/
export interface DefaultConfigurationRequest {

/**
* The message identifier of the message to configure as a default message.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/messageidentifier messageIdentifier}
**/
messageIdentifier?: string
}
36 changes: 36 additions & 0 deletions models/GetImageListResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.

import { GetImageListResponseItem, GetImageListResponseItemValidator } from "./GetImageListResponseItem"
import { Validator } from "./Validator"

/**
* A response that contains status information for all images.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/getimagelistresponse GetImageListResponse}
*/
export interface GetImageListResponse {

/**
* An array of all image identifiers and their image state.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/getimagelistresponseitem GetImageListResponseItem}
**/
imageIdentifiers?: GetImageListResponseItem[]
}

export class GetImageListResponseValidator implements Validator<GetImageListResponse> {
static readonly getImageListResponseItemValidator = new GetImageListResponseItemValidator()
validate(obj: any): obj is GetImageListResponse {
if (typeof obj['imageIdentifiers'] !== 'undefined') {
if (!Array.isArray(obj['imageIdentifiers'])) {
return false
}
for (const imageIdentifier of obj['imageIdentifiers']) {
if (!(GetImageListResponseValidator.getImageListResponseItemValidator.validate(imageIdentifier))) {
return false
}
}
}
return true
}
}
39 changes: 39 additions & 0 deletions models/GetImageListResponseItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.

import { ImageState, ImageStateValidator } from "./ImageState"
import { Validator } from "./Validator"

/**
* An image identifier and state information for an image.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/getimagelistresponseitem GetImageListResponseItem}
*/
export interface GetImageListResponseItem {

/**
* The identifier of the image.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/imageidentifier imageIdentifier}
**/
imageIdentifier?: string

/**
* The current state of the image.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/imagestate imageState}
**/
imageState?: ImageState | string
}

export class GetImageListResponseItemValidator implements Validator<GetImageListResponseItem> {
static readonly imageStateValidator = new ImageStateValidator()
validate(obj: any): obj is GetImageListResponseItem {
if ((typeof obj['imageIdentifier'] !== 'undefined') && !(typeof obj['imageIdentifier'] === "string" || obj['imageIdentifier'] instanceof String)) {
return false
}
if ((typeof obj['imageState'] !== 'undefined') && !(GetImageListResponseItemValidator.imageStateValidator.validate(obj['imageState']))) {
return false
}
return true
}
}
36 changes: 36 additions & 0 deletions models/GetMessageListResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.

import { GetMessageListResponseItem, GetMessageListResponseItemValidator } from "./GetMessageListResponseItem"
import { Validator } from "./Validator"

/**
* A response that contains status information for all messages.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/getmessagelistresponse GetMessageListResponse}
*/
export interface GetMessageListResponse {

/**
* An array of all message identifiers and their message state.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/getmessagelistresponseitem messageIdentifiers}
**/
messageIdentifiers?: GetMessageListResponseItem[]
}

export class GetMessageListResponseValidator implements Validator<GetMessageListResponse> {
static readonly getMessageListResponseItemValidator = new GetMessageListResponseItemValidator()
validate(obj: any): obj is GetMessageListResponse {
if (typeof obj['messageIdentifiers'] !== 'undefined') {
if (!Array.isArray(obj['messageIdentifiers'])) {
return false
}
for (const messageIdentifier of obj['messageIdentifiers']) {
if (!(GetMessageListResponseValidator.getMessageListResponseItemValidator.validate(messageIdentifier))) {
return false
}
}
}
return true
}
}
39 changes: 39 additions & 0 deletions models/GetMessageListResponseItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.

import { MessageState, MessageStateValidator } from "./MessageState"
import { Validator } from "./Validator"

/**
* A message identifier and status information for a message.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/getmessagelistresponseitem GetMessageListResponseItem}
*/
export interface GetMessageListResponseItem {

/**
* The identifier of the message.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/messageidentifier messageIdentifier}
**/
messageIdentifier?: string

/**
* The current state of the message.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/messagestate messageState}
**/
messageState?: MessageState | string
}

export class GetMessageListResponseItemValidator implements Validator<GetMessageListResponseItem> {
static readonly messageStateValidator = new MessageStateValidator()
validate(obj: any): obj is GetMessageListResponseItem {
if ((typeof obj['messageIdentifier'] !== 'undefined') && !(typeof obj['messageIdentifier'] === "string" || obj['messageIdentifier'] instanceof String)) {
return false
}
if ((typeof obj['messageState'] !== 'undefined') && !(GetMessageListResponseItemValidator.messageStateValidator.validate(obj['messageState']))) {
return false
}
return true
}
}
16 changes: 16 additions & 0 deletions models/ImageState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.

import { StringValidator } from "./Validator";

/**
* The approval state of an image.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/imagestate imageState}
*/
export enum ImageState {
PENDING_REVIEW = "PENDING_REVIEW",
APPROVED = "APPROVED",
REJECTED = "REJECTED",
}

export class ImageStateValidator extends StringValidator {}
16 changes: 16 additions & 0 deletions models/Message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.

/**
* A message identifier you provide in a real-time response to your Get Retention Message endpoint.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/message message}
*/
export interface Message {

/**
* The identifier of the message to display to the customer.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/messageidentifier messageIdentifier}
**/
messageIdentifier?: string
}
16 changes: 16 additions & 0 deletions models/MessageState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.

import { StringValidator } from "./Validator";

/**
* The approval state of a message.
*
* {@link https://developer.apple.com/documentation/retentionmessaging/messagestate messageState}
*/
export enum MessageState {
PENDING_REVIEW = "PENDING_REVIEW",
APPROVED = "APPROVED",
REJECTED = "REJECTED",
}

export class MessageStateValidator extends StringValidator {}
Loading