Skip to content

Commit 6c5b8ad

Browse files
Merge pull request #341 from alexanderjordanbaker/RetentionMessaging1.1
Add support for the Retention Messaging API 1.0-1.1 https://developer…
2 parents e5b556b + 4f395bb commit 6c5b8ad

25 files changed

+1259
-76
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Apple App Store Server Node.js Library
2-
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).
2+
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).
33

44
## Table of Contents
55
1. [Installation](#installation)

index.ts

Lines changed: 238 additions & 22 deletions
Large diffs are not rendered by default.

jws_verification.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Environment } from './models/Environment';
1010
import { JWSTransactionDecodedPayload, JWSTransactionDecodedPayloadValidator } from './models/JWSTransactionDecodedPayload';
1111
import { ResponseBodyV2DecodedPayload, ResponseBodyV2DecodedPayloadValidator } from './models/ResponseBodyV2DecodedPayload';
1212
import { JWSRenewalInfoDecodedPayload, JWSRenewalInfoDecodedPayloadValidator } from './models/JWSRenewalInfoDecodedPayload';
13+
import { DecodedRealtimeRequestBody, DecodedRealtimeRequestBodyValidator } from './models/DecodedRealtimeRequestBody';
1314
import { Validator } from './models/Validator';
1415
import { DecodedSignedData } from './models/DecodedSignedData';
1516
import { AppTransaction, AppTransactionValidator } from './models/AppTransaction';
@@ -49,6 +50,7 @@ export class SignedDataVerifier {
4950
private JWSTransactionDecodedPayloadValidator = new JWSTransactionDecodedPayloadValidator()
5051
private responseBodyV2DecodedPayloadValidator = new ResponseBodyV2DecodedPayloadValidator()
5152
private appTransactionValidator = new AppTransactionValidator()
53+
private decodedRealtimeRequestBodyValidator = new DecodedRealtimeRequestBodyValidator()
5254

5355
protected rootCertificates: X509Certificate[]
5456
protected enableOnlineChecks: boolean
@@ -176,6 +178,25 @@ export class SignedDataVerifier {
176178
return decodedAppTransaction
177179
}
178180

181+
/**
182+
* Verifies and decodes a Retention Messaging API signedPayload
183+
* See {@link https://developer.apple.com/documentation/retentionmessaging/signedpayload signedPayload}
184+
*
185+
* @param signedPayload The payload received by your server
186+
* @returns The decoded payload after verification
187+
* @throws VerificationException Thrown if the data could not be verified
188+
*/
189+
async verifyAndDecodeRealtimeRequest(signedPayload: string): Promise<DecodedRealtimeRequestBody> {
190+
const decodedRequest: DecodedRealtimeRequestBody = await this.verifyJWT(signedPayload, this.decodedRealtimeRequestBodyValidator, this.extractSignedDate);
191+
if (this.environment === Environment.PRODUCTION && this.appAppleId !== decodedRequest.appAppleId) {
192+
throw new VerificationException(VerificationStatus.INVALID_APP_IDENTIFIER)
193+
}
194+
if (this.environment !== decodedRequest.environment) {
195+
throw new VerificationException(VerificationStatus.INVALID_ENVIRONMENT)
196+
}
197+
return decodedRequest
198+
}
199+
179200
protected async verifyJWT<T>(jwt: string, validator: Validator<T>, signedDateExtractor: (decodedJWT: T) => Date): Promise<T> {
180201
let certificateChain;
181202
let decodedJWT

models/AlternateProduct.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.
2+
3+
/**
4+
* A switch-plan message and product ID you provide in a real-time response to your Get Retention Message endpoint.
5+
*
6+
* {@link https://developer.apple.com/documentation/retentionmessaging/alternateproduct alternateProduct}
7+
*/
8+
export interface AlternateProduct {
9+
10+
/**
11+
* The message identifier of the text to display in the switch-plan retention message.
12+
*
13+
* {@link https://developer.apple.com/documentation/retentionmessaging/messageidentifier messageIdentifier}
14+
**/
15+
messageIdentifier?: string
16+
17+
/**
18+
* The product identifier of the subscription the retention message suggests for your customer to switch to.
19+
*
20+
* {@link https://developer.apple.com/documentation/retentionmessaging/productid productId}
21+
**/
22+
productId?: string
23+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.
2+
3+
import { DecodedSignedData } from "./DecodedSignedData"
4+
import { Environment, EnvironmentValidator } from "./Environment"
5+
import { Validator } from "./Validator"
6+
7+
/**
8+
* The decoded request body the App Store sends to your server to request a real-time retention message.
9+
*
10+
* {@link https://developer.apple.com/documentation/retentionmessaging/decodedrealtimerequestbody DecodedRealtimeRequestBody}
11+
*/
12+
export interface DecodedRealtimeRequestBody extends DecodedSignedData {
13+
14+
/**
15+
* The original transaction identifier of the customer's subscription.
16+
*
17+
* {@link https://developer.apple.com/documentation/retentionmessaging/originaltransactionid originalTransactionId}
18+
**/
19+
originalTransactionId: string
20+
21+
/**
22+
* The unique identifier of the app in the App Store.
23+
*
24+
* {@link https://developer.apple.com/documentation/retentionmessaging/appappleid appAppleId}
25+
**/
26+
appAppleId: number
27+
28+
/**
29+
* The unique identifier of the auto-renewable subscription.
30+
*
31+
* {@link https://developer.apple.com/documentation/retentionmessaging/productid productId}
32+
**/
33+
productId: string
34+
35+
/**
36+
* The device's locale.
37+
*
38+
* {@link https://developer.apple.com/documentation/retentionmessaging/locale locale}
39+
**/
40+
userLocale: string
41+
42+
/**
43+
* A UUID the App Store server creates to uniquely identify each request.
44+
*
45+
* {@link https://developer.apple.com/documentation/retentionmessaging/requestidentifier requestIdentifier}
46+
**/
47+
requestIdentifier: string
48+
49+
/**
50+
* The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature (JWS) data.
51+
*
52+
* {@link https://developer.apple.com/documentation/retentionmessaging/signeddate signedDate}
53+
**/
54+
signedDate: number
55+
56+
/**
57+
* The server environment, either sandbox or production.
58+
*
59+
* {@link https://developer.apple.com/documentation/retentionmessaging/environment environment}
60+
**/
61+
environment: Environment | string
62+
}
63+
64+
65+
export class DecodedRealtimeRequestBodyValidator implements Validator<DecodedRealtimeRequestBody> {
66+
static readonly environmentValidator = new EnvironmentValidator()
67+
validate(obj: any): obj is DecodedRealtimeRequestBody {
68+
if (!(typeof obj['originalTransactionId'] === "string" || obj['originalTransactionId'] instanceof String)) {
69+
return false
70+
}
71+
if (!(typeof obj['appAppleId'] === "number")) {
72+
return false
73+
}
74+
if (!(typeof obj['productId'] === "string" || obj['productId'] instanceof String)) {
75+
return false
76+
}
77+
if (!(typeof obj['userLocale'] === "string" || obj['userLocale'] instanceof String)) {
78+
return false
79+
}
80+
if (!(typeof obj['requestIdentifier'] === "string" || obj['requestIdentifier'] instanceof String)) {
81+
return false
82+
}
83+
if (!(typeof obj['signedDate'] === "number")) {
84+
return false
85+
}
86+
if (!(DecodedRealtimeRequestBodyValidator.environmentValidator.validate(obj['environment']))) {
87+
return false
88+
}
89+
return true
90+
}
91+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.
2+
3+
/**
4+
* The request body that contains the default configuration information.
5+
*
6+
* {@link https://developer.apple.com/documentation/retentionmessaging/defaultconfigurationrequest DefaultConfigurationRequest}
7+
*/
8+
export interface DefaultConfigurationRequest {
9+
10+
/**
11+
* The message identifier of the message to configure as a default message.
12+
*
13+
* {@link https://developer.apple.com/documentation/retentionmessaging/messageidentifier messageIdentifier}
14+
**/
15+
messageIdentifier?: string
16+
}

models/GetImageListResponse.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.
2+
3+
import { GetImageListResponseItem, GetImageListResponseItemValidator } from "./GetImageListResponseItem"
4+
import { Validator } from "./Validator"
5+
6+
/**
7+
* A response that contains status information for all images.
8+
*
9+
* {@link https://developer.apple.com/documentation/retentionmessaging/getimagelistresponse GetImageListResponse}
10+
*/
11+
export interface GetImageListResponse {
12+
13+
/**
14+
* An array of all image identifiers and their image state.
15+
*
16+
* {@link https://developer.apple.com/documentation/retentionmessaging/getimagelistresponseitem GetImageListResponseItem}
17+
**/
18+
imageIdentifiers?: GetImageListResponseItem[]
19+
}
20+
21+
export class GetImageListResponseValidator implements Validator<GetImageListResponse> {
22+
static readonly getImageListResponseItemValidator = new GetImageListResponseItemValidator()
23+
validate(obj: any): obj is GetImageListResponse {
24+
if (typeof obj['imageIdentifiers'] !== 'undefined') {
25+
if (!Array.isArray(obj['imageIdentifiers'])) {
26+
return false
27+
}
28+
for (const imageIdentifier of obj['imageIdentifiers']) {
29+
if (!(GetImageListResponseValidator.getImageListResponseItemValidator.validate(imageIdentifier))) {
30+
return false
31+
}
32+
}
33+
}
34+
return true
35+
}
36+
}

models/GetImageListResponseItem.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.
2+
3+
import { ImageState, ImageStateValidator } from "./ImageState"
4+
import { Validator } from "./Validator"
5+
6+
/**
7+
* An image identifier and state information for an image.
8+
*
9+
* {@link https://developer.apple.com/documentation/retentionmessaging/getimagelistresponseitem GetImageListResponseItem}
10+
*/
11+
export interface GetImageListResponseItem {
12+
13+
/**
14+
* The identifier of the image.
15+
*
16+
* {@link https://developer.apple.com/documentation/retentionmessaging/imageidentifier imageIdentifier}
17+
**/
18+
imageIdentifier?: string
19+
20+
/**
21+
* The current state of the image.
22+
*
23+
* {@link https://developer.apple.com/documentation/retentionmessaging/imagestate imageState}
24+
**/
25+
imageState?: ImageState | string
26+
}
27+
28+
export class GetImageListResponseItemValidator implements Validator<GetImageListResponseItem> {
29+
static readonly imageStateValidator = new ImageStateValidator()
30+
validate(obj: any): obj is GetImageListResponseItem {
31+
if ((typeof obj['imageIdentifier'] !== 'undefined') && !(typeof obj['imageIdentifier'] === "string" || obj['imageIdentifier'] instanceof String)) {
32+
return false
33+
}
34+
if ((typeof obj['imageState'] !== 'undefined') && !(GetImageListResponseItemValidator.imageStateValidator.validate(obj['imageState']))) {
35+
return false
36+
}
37+
return true
38+
}
39+
}

models/GetMessageListResponse.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.
2+
3+
import { GetMessageListResponseItem, GetMessageListResponseItemValidator } from "./GetMessageListResponseItem"
4+
import { Validator } from "./Validator"
5+
6+
/**
7+
* A response that contains status information for all messages.
8+
*
9+
* {@link https://developer.apple.com/documentation/retentionmessaging/getmessagelistresponse GetMessageListResponse}
10+
*/
11+
export interface GetMessageListResponse {
12+
13+
/**
14+
* An array of all message identifiers and their message state.
15+
*
16+
* {@link https://developer.apple.com/documentation/retentionmessaging/getmessagelistresponseitem messageIdentifiers}
17+
**/
18+
messageIdentifiers?: GetMessageListResponseItem[]
19+
}
20+
21+
export class GetMessageListResponseValidator implements Validator<GetMessageListResponse> {
22+
static readonly getMessageListResponseItemValidator = new GetMessageListResponseItemValidator()
23+
validate(obj: any): obj is GetMessageListResponse {
24+
if (typeof obj['messageIdentifiers'] !== 'undefined') {
25+
if (!Array.isArray(obj['messageIdentifiers'])) {
26+
return false
27+
}
28+
for (const messageIdentifier of obj['messageIdentifiers']) {
29+
if (!(GetMessageListResponseValidator.getMessageListResponseItemValidator.validate(messageIdentifier))) {
30+
return false
31+
}
32+
}
33+
}
34+
return true
35+
}
36+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.
2+
3+
import { MessageState, MessageStateValidator } from "./MessageState"
4+
import { Validator } from "./Validator"
5+
6+
/**
7+
* A message identifier and status information for a message.
8+
*
9+
* {@link https://developer.apple.com/documentation/retentionmessaging/getmessagelistresponseitem GetMessageListResponseItem}
10+
*/
11+
export interface GetMessageListResponseItem {
12+
13+
/**
14+
* The identifier of the message.
15+
*
16+
* {@link https://developer.apple.com/documentation/retentionmessaging/messageidentifier messageIdentifier}
17+
**/
18+
messageIdentifier?: string
19+
20+
/**
21+
* The current state of the message.
22+
*
23+
* {@link https://developer.apple.com/documentation/retentionmessaging/messagestate messageState}
24+
**/
25+
messageState?: MessageState | string
26+
}
27+
28+
export class GetMessageListResponseItemValidator implements Validator<GetMessageListResponseItem> {
29+
static readonly messageStateValidator = new MessageStateValidator()
30+
validate(obj: any): obj is GetMessageListResponseItem {
31+
if ((typeof obj['messageIdentifier'] !== 'undefined') && !(typeof obj['messageIdentifier'] === "string" || obj['messageIdentifier'] instanceof String)) {
32+
return false
33+
}
34+
if ((typeof obj['messageState'] !== 'undefined') && !(GetMessageListResponseItemValidator.messageStateValidator.validate(obj['messageState']))) {
35+
return false
36+
}
37+
return true
38+
}
39+
}

0 commit comments

Comments
 (0)