├── jest.config.cjs ├── tests ├── resources │ ├── models │ │ ├── apiUnknownError.json │ │ ├── transactionInfoResponse.json │ │ ├── apiException.json │ │ ├── appTransactionInfoResponse.json │ │ ├── apiTooManyRequestsException.json │ │ ├── invalidTransactionIdError.json │ │ ├── requestTestNotificationResponse.json │ │ ├── transactionIdNotFoundError.json │ │ ├── extendRenewalDateForAllActiveSubscribersResponse.json │ │ ├── appTransactionDoesNotExistError.json │ │ ├── lookupOrderIdResponse.json │ │ ├── invalidAppAccountTokenUUIDError.json │ │ ├── familyTransactionNotSupportedError.json │ │ ├── transactionIdNotOriginalTransactionId.json │ │ ├── extendSubscriptionRenewalDateResponse.json │ │ ├── getImageListResponse.json │ │ ├── getMessageListResponse.json │ │ ├── getRefundHistoryResponse.json │ │ ├── getStatusOfSubscriptionRenewalDateExtensionsResponse.json │ │ ├── decodedRealtimeRequest.json │ │ ├── transactionHistoryResponse.json │ │ ├── getTestNotificationStatusResponse.json │ │ ├── transactionHistoryResponseWithMalformedAppAppleId.json │ │ ├── transactionHistoryResponseWithMalformedEnvironment.json │ │ ├── signedExternalPurchaseTokenNotification.json │ │ ├── signedExternalPurchaseTokenSandboxNotification.json │ │ ├── signedNotification.json │ │ ├── appTransaction.json │ │ ├── signedConsumptionRequestNotification.json │ │ ├── signedSummaryNotification.json │ │ ├── getNotificationHistoryResponse.json │ │ ├── signedRenewalInfo.json │ │ ├── signedTransaction.json │ │ └── getAllSubscriptionStatusesResponse.json │ ├── certs │ │ ├── testCA.der │ │ └── testSigningKey.p8 │ ├── mock_signed_data │ │ ├── legacyTransaction │ │ ├── transactionInfo │ │ ├── renewalInfo │ │ ├── wrongBundleId │ │ ├── missingX5CHeaderClaim │ │ └── testNotification │ └── xcode │ │ ├── xcode-signed-renewal-info │ │ ├── xcode-signed-app-transaction │ │ ├── xcode-signed-transaction │ │ ├── xcode-app-receipt-empty │ │ └── xcode-app-receipt-with-transaction ├── unit-tests │ ├── promotional_offer_signature_creator.test.ts │ ├── DecodedRealtimeRequestBody.test.ts │ ├── receipt_utility.test.ts │ ├── jws_signature_creator.test.ts │ └── RealtimeResponseBody.test.ts └── util.ts ├── models ├── DecodedSignedData.ts ├── RefundPreference.ts ├── AutoRenewStatus.ts ├── Platform.ts ├── OfferType.ts ├── ImageState.ts ├── RevocationReason.ts ├── MessageState.ts ├── Status.ts ├── OrderLookupStatus.ts ├── PurchasePlatform.ts ├── Validator.ts ├── UserStatus.ts ├── InAppOwnershipType.ts ├── Environment.ts ├── ExtendReasonCode.ts ├── ConsumptionStatus.ts ├── Message.ts ├── OfferDiscountType.ts ├── Type.ts ├── ExpirationIntent.ts ├── PriceIncreaseStatus.ts ├── TransactionReason.ts ├── DefaultConfigurationRequest.ts ├── UpdateAppAccountTokenRequest.ts ├── ConsumptionRequestReason.ts ├── PlayTime.ts ├── AccountTenure.ts ├── DeliveryStatus.ts ├── UploadMessageImage.ts ├── AlternateProduct.ts ├── SendAttemptResult.ts ├── FirstSendAttemptResult.ts ├── LifetimeDollarsPurchased.ts ├── RealtimeRequestBody.ts ├── LifetimeDollarsRefunded.ts ├── Subtype.ts ├── ResponseBodyV2.ts ├── TransactionInfoResponse.ts ├── AppTransactionInfoResponse.ts ├── SendTestNotificationResponse.ts ├── MassExtendRenewalDateResponse.ts ├── RealtimeResponseBody.ts ├── PromotionalOffer.ts ├── ExtendRenewalDateRequest.ts ├── NotificationTypeV2.ts ├── UploadMessageRequestBody.ts ├── GetImageListResponse.ts ├── GetMessageListResponse.ts ├── GetImageListResponseItem.ts ├── PromotionalOfferSignatureV1.ts ├── GetMessageListResponseItem.ts ├── SubscriptionGroupIdentifierItem.ts ├── SendAttemptItem.ts ├── MassExtendRenewalDateRequest.ts ├── OrderLookupResponse.ts ├── CheckTestNotificationResponse.ts ├── NotificationHistoryResponseItem.ts ├── StatusResponse.ts ├── RefundHistoryResponse.ts ├── ExternalPurchaseToken.ts ├── ExtendRenewalDateResponse.ts ├── NotificationHistoryResponse.ts ├── LastTransactionsItem.ts ├── NotificationHistoryRequest.ts ├── MassExtendRenewalDateStatusResponse.ts ├── TransactionHistoryRequest.ts ├── DecodedRealtimeRequestBody.ts ├── HistoryResponse.ts ├── Summary.ts ├── ConsumptionRequest.ts ├── Data.ts ├── ResponseBodyV2DecodedPayload.ts └── AppTransaction.ts ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── ci-release.yml │ ├── ci-prb.yml │ └── ci-release-docs.yml ├── LICENSE.txt ├── tsconfig.json ├── package.json ├── CONTRIBUTING.md ├── promotional_offer.ts ├── CHANGELOG.md ├── receipt_utility.ts ├── CODE_OF_CONDUCT.md ├── README.md └── jws_signature_creator.ts /jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; -------------------------------------------------------------------------------- /tests/resources/models/apiUnknownError.json: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode": 9990000, 3 | "errorMessage": "Testing error." 4 | } -------------------------------------------------------------------------------- /tests/resources/models/transactionInfoResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "signedTransactionInfo": "signed_transaction_info_value" 3 | } -------------------------------------------------------------------------------- /tests/resources/models/apiException.json: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode": 5000000, 3 | "errorMessage": "An unknown error occurred." 4 | } -------------------------------------------------------------------------------- /tests/resources/models/appTransactionInfoResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "signedAppTransactionInfo": "signed_app_transaction_info_value" 3 | } -------------------------------------------------------------------------------- /tests/resources/certs/testCA.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/app-store-server-library-node/HEAD/tests/resources/certs/testCA.der -------------------------------------------------------------------------------- /tests/resources/models/apiTooManyRequestsException.json: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode": 4290000, 3 | "errorMessage": "Rate limit exceeded." 4 | } -------------------------------------------------------------------------------- /tests/resources/models/invalidTransactionIdError.json: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode": 4000006, 3 | "errorMessage": "Invalid transaction id." 4 | } -------------------------------------------------------------------------------- /tests/resources/models/requestTestNotificationResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "testNotificationToken": "ce3af791-365e-4c60-841b-1674b43c1609" 3 | } -------------------------------------------------------------------------------- /tests/resources/models/transactionIdNotFoundError.json: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode": 4040010, 3 | "errorMessage": "Transaction id not found." 4 | } -------------------------------------------------------------------------------- /tests/resources/mock_signed_data/legacyTransaction: -------------------------------------------------------------------------------- 1 | ewoicHVyY2hhc2UtaW5mbyIgPSAiZXdvaWRISmhibk5oWTNScGIyNHRhV1FpSUQwZ0lqTXpPVGt6TXprNUlqc0tmUW89IjsKfQo= -------------------------------------------------------------------------------- /tests/resources/models/extendRenewalDateForAllActiveSubscribersResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "requestIdentifier": "758883e8-151b-47b7-abd0-60c4d804c2f5" 3 | } -------------------------------------------------------------------------------- /tests/resources/models/appTransactionDoesNotExistError.json: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode": 4040019, 3 | "errorMessage": "No AppTransaction exists for the customer." 4 | } -------------------------------------------------------------------------------- /models/DecodedSignedData.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | export interface DecodedSignedData { 4 | signedDate?: number 5 | } -------------------------------------------------------------------------------- /tests/resources/models/lookupOrderIdResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": 1, 3 | "signedTransactions": [ 4 | "signed_transaction_one", 5 | "signed_transaction_two" 6 | ] 7 | } -------------------------------------------------------------------------------- /tests/resources/models/invalidAppAccountTokenUUIDError.json: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode": 4000183, 3 | "errorMessage": "Invalid request. The app account token field must be a valid UUID." 4 | } -------------------------------------------------------------------------------- /tests/resources/models/familyTransactionNotSupportedError.json: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode": 4000185, 3 | "errorMessage": "Invalid request. Family Sharing transactions aren't supported by this endpoint." 4 | } -------------------------------------------------------------------------------- /tests/resources/models/transactionIdNotOriginalTransactionId.json: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode": 4000187, 3 | "errorMessage": "Invalid request. The transaction ID provided is not an original transaction ID." 4 | } -------------------------------------------------------------------------------- /tests/resources/models/extendSubscriptionRenewalDateResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "originalTransactionId": "2312412", 3 | "webOrderLineItemId": "9993", 4 | "success": true, 5 | "effectiveDate": 1698148900000 6 | } -------------------------------------------------------------------------------- /tests/resources/models/getImageListResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "imageIdentifiers": [ 3 | { 4 | "imageIdentifier": "a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890", 5 | "imageState": "APPROVED" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tests/resources/models/getMessageListResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "messageIdentifiers": [ 3 | { 4 | "messageIdentifier": "a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890", 5 | "messageState": "APPROVED" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tests/resources/models/getRefundHistoryResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "signedTransactions": [ 3 | "signed_transaction_one", 4 | "signed_transaction_two" 5 | ], 6 | "revision": "revision_output", 7 | "hasMore": true 8 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | docs 4 | .vscode 5 | tsconfig.tsbuildinfo 6 | .idea 7 | *.iml 8 | .npmrc 9 | 10 | .DS_Store 11 | Thumbs.db 12 | 13 | dist/**/* 14 | 15 | api-client/**/* 16 | docs/**/* 17 | -------------------------------------------------------------------------------- /tests/resources/models/getStatusOfSubscriptionRenewalDateExtensionsResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "requestIdentifier": "20fba8a0-2b80-4a7d-a17f-85c1854727f8", 3 | "complete": true, 4 | "completeDate": 1698148900000, 5 | "succeededCount": 30, 6 | "failedCount": 2 7 | } -------------------------------------------------------------------------------- /tests/resources/certs/testSigningKey.p8: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgSpP55ELdXswj9JRZ 3 | APRwtTfS4CNRqpKIs+28rNHiPAqhRANCAASs8nLES7b+goKslppNVOurf0MonZdw 4 | 3pb6TxS8Z/5j+UNY1sWK1ChxpuwNS9I3R50cfdQo/lA9PPhw6XIg8ytd 5 | -----END PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | time: "02:00" 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | time: "02:00" 13 | -------------------------------------------------------------------------------- /tests/resources/models/decodedRealtimeRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "originalTransactionId": "99371282", 3 | "appAppleId": 531412, 4 | "productId": "com.example.product", 5 | "userLocale": "en-US", 6 | "requestIdentifier": "3db5c98d-8acf-4e29-831e-8e1f82f9f6e9", 7 | "environment": "LocalTesting", 8 | "signedDate": 1698148900000 9 | } -------------------------------------------------------------------------------- /tests/resources/models/transactionHistoryResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "revision": "revision_output", 3 | "hasMore": true, 4 | "bundleId": "com.example", 5 | "appAppleId": 323232, 6 | "environment": "LocalTesting", 7 | "signedTransactions": [ 8 | "signed_transaction_value", 9 | "signed_transaction_value2" 10 | ] 11 | } -------------------------------------------------------------------------------- /tests/resources/models/getTestNotificationStatusResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "signedPayload": "signed_payload", 3 | "sendAttempts": [ 4 | { 5 | "attemptDate": 1698148900000, 6 | "sendAttemptResult": "NO_RESPONSE" 7 | }, { 8 | "attemptDate": 1698148950000, 9 | "sendAttemptResult": "SUCCESS" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /tests/resources/models/transactionHistoryResponseWithMalformedAppAppleId.json: -------------------------------------------------------------------------------- 1 | { 2 | "revision": "revision_output", 3 | "hasMore": 1, 4 | "bundleId": "com.example", 5 | "appAppleId": "hi", 6 | "environment": "LocalTesting", 7 | "signedTransactions": [ 8 | "signed_transaction_value", 9 | "signed_transaction_value2" 10 | ] 11 | } -------------------------------------------------------------------------------- /tests/resources/models/transactionHistoryResponseWithMalformedEnvironment.json: -------------------------------------------------------------------------------- 1 | { 2 | "revision": "revision_output", 3 | "hasMore": true, 4 | "bundleId": "com.example", 5 | "appAppleId": 323232, 6 | "environment": "LocalTestingxxx", 7 | "signedTransactions": [ 8 | "signed_transaction_value", 9 | "signed_transaction_value2" 10 | ] 11 | } -------------------------------------------------------------------------------- /models/RefundPreference.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Apple Inc. Licensed under MIT License. 2 | 3 | /** 4 | * A value that indicates your preferred outcome for the refund request. 5 | * 6 | * {@link https://developer.apple.com/documentation/appstoreserverapi/refundpreference refundPreference} 7 | */ 8 | export enum RefundPreference { 9 | UNDECLARED = 0, 10 | PREFER_GRANT = 1, 11 | PREFER_DECLINE = 2, 12 | NO_PREFERENCE = 3, 13 | } 14 | -------------------------------------------------------------------------------- /models/AutoRenewStatus.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { NumberValidator } from "./Validator"; 4 | 5 | /** 6 | * The renewal status for an auto-renewable subscription. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/autorenewstatus autoRenewStatus} 9 | */ 10 | export enum AutoRenewStatus { 11 | OFF = 0, 12 | ON = 1, 13 | } 14 | 15 | export class AutoRenewStatusValidator extends NumberValidator {} -------------------------------------------------------------------------------- /models/Platform.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { NumberValidator } from "./Validator"; 4 | 5 | /** 6 | * The platform on which the customer consumed the in-app purchase. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/platform platform} 9 | */ 10 | export enum Platform { 11 | UNDECLARED = 0, 12 | APPLE = 1, 13 | NON_APPLE = 2, 14 | } 15 | 16 | export class PlatformValidator extends NumberValidator {} -------------------------------------------------------------------------------- /models/OfferType.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { NumberValidator } from "./Validator"; 4 | 5 | /** 6 | * The type of offer. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/offertype offerType} 9 | */ 10 | export enum OfferType { 11 | INTRODUCTORY_OFFER = 1, 12 | PROMOTIONAL_OFFER = 2, 13 | OFFER_CODE = 3, 14 | WIN_BACK_OFFER = 4, 15 | } 16 | 17 | export class OfferTypeValidator extends NumberValidator {} -------------------------------------------------------------------------------- /models/ImageState.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | import { StringValidator } from "./Validator"; 4 | 5 | /** 6 | * The approval state of an image. 7 | * 8 | * {@link https://developer.apple.com/documentation/retentionmessaging/imagestate imageState} 9 | */ 10 | export enum ImageState { 11 | PENDING_REVIEW = "PENDING_REVIEW", 12 | APPROVED = "APPROVED", 13 | REJECTED = "REJECTED", 14 | } 15 | 16 | export class ImageStateValidator extends StringValidator {} 17 | -------------------------------------------------------------------------------- /models/RevocationReason.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { NumberValidator } from "./Validator"; 4 | 5 | /** 6 | * The reason for a refunded transaction. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/revocationreason revocationReason} 9 | */ 10 | export enum RevocationReason { 11 | REFUNDED_DUE_TO_ISSUE = 1, 12 | REFUNDED_FOR_OTHER_REASON = 0, 13 | } 14 | 15 | export class RevocationReasonValidator extends NumberValidator {} -------------------------------------------------------------------------------- /tests/resources/models/signedExternalPurchaseTokenNotification.json: -------------------------------------------------------------------------------- 1 | { 2 | "notificationType": "EXTERNAL_PURCHASE_TOKEN", 3 | "subtype": "UNREPORTED", 4 | "notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20", 5 | "version": "2.0", 6 | "signedDate": 1698148900000, 7 | "externalPurchaseToken": { 8 | "externalPurchaseId": "b2158121-7af9-49d4-9561-1f588205523e", 9 | "tokenCreationDate": 1698148950000, 10 | "appAppleId": 55555, 11 | "bundleId": "com.example" 12 | } 13 | } -------------------------------------------------------------------------------- /models/MessageState.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | import { StringValidator } from "./Validator"; 4 | 5 | /** 6 | * The approval state of a message. 7 | * 8 | * {@link https://developer.apple.com/documentation/retentionmessaging/messagestate messageState} 9 | */ 10 | export enum MessageState { 11 | PENDING_REVIEW = "PENDING_REVIEW", 12 | APPROVED = "APPROVED", 13 | REJECTED = "REJECTED", 14 | } 15 | 16 | export class MessageStateValidator extends StringValidator {} 17 | -------------------------------------------------------------------------------- /models/Status.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { NumberValidator } from "./Validator"; 4 | 5 | /** 6 | * The status of an auto-renewable subscription. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/status status} 9 | */ 10 | export enum Status { 11 | ACTIVE = 1, 12 | EXPIRED = 2, 13 | BILLING_RETRY = 3, 14 | BILLING_GRACE_PERIOD = 4, 15 | REVOKED = 5, 16 | } 17 | 18 | export class StatusValidator extends NumberValidator {} -------------------------------------------------------------------------------- /models/OrderLookupStatus.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { NumberValidator } from "./Validator"; 4 | 5 | /** 6 | * A value that indicates whether the order ID in the request is valid for your app. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/orderlookupstatus OrderLookupStatus} 9 | */ 10 | export enum OrderLookupStatus { 11 | VALID = 0, 12 | INVALID = 1, 13 | } 14 | 15 | export class OrderLookupStatusValidator extends NumberValidator {} -------------------------------------------------------------------------------- /models/PurchasePlatform.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | import { StringValidator } from "./Validator"; 4 | 5 | /** 6 | * Values that represent Apple platforms. 7 | * 8 | * {@link https://developer.apple.com/documentation/storekit/appstore/platform AppStore.Platform} 9 | */ 10 | export enum PurchasePlatform { 11 | IOS = "iOS", 12 | MAC_OS = "macOS", 13 | TV_OS = "tvOS", 14 | VISION_OS = "visionOS" 15 | } 16 | 17 | export class PurchasePlatformValidator extends StringValidator {} -------------------------------------------------------------------------------- /tests/resources/models/signedExternalPurchaseTokenSandboxNotification.json: -------------------------------------------------------------------------------- 1 | { 2 | "notificationType": "EXTERNAL_PURCHASE_TOKEN", 3 | "subtype": "UNREPORTED", 4 | "notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20", 5 | "version": "2.0", 6 | "signedDate": 1698148900000, 7 | "externalPurchaseToken": { 8 | "externalPurchaseId": "SANDBOX_b2158121-7af9-49d4-9561-1f588205523e", 9 | "tokenCreationDate": 1698148950000, 10 | "appAppleId": 55555, 11 | "bundleId": "com.example" 12 | } 13 | } -------------------------------------------------------------------------------- /models/Validator.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | export interface Validator { 4 | validate(obj: any): obj is T 5 | } 6 | 7 | export class NumberValidator implements Validator { 8 | validate(obj: any): obj is number { 9 | return typeof obj === 'number' 10 | } 11 | } 12 | 13 | export class StringValidator implements Validator { 14 | validate(obj: any): obj is string { 15 | return typeof obj === "string" || obj instanceof String 16 | } 17 | } -------------------------------------------------------------------------------- /models/UserStatus.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { NumberValidator } from "./Validator"; 4 | 5 | /** 6 | * The status of a customer’s account within your app. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/userstatus userStatus} 9 | */ 10 | export enum UserStatus { 11 | UNDECLARED = 0, 12 | ACTIVE = 1, 13 | SUSPENDED = 2, 14 | TERMINATED = 3, 15 | LIMITED_ACCESS = 4, 16 | } 17 | 18 | export class UserStatusValidator extends NumberValidator {} 19 | -------------------------------------------------------------------------------- /tests/resources/models/signedNotification.json: -------------------------------------------------------------------------------- 1 | { 2 | "notificationType": "SUBSCRIBED", 3 | "subtype": "INITIAL_BUY", 4 | "notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20", 5 | "data": { 6 | "environment": "LocalTesting", 7 | "appAppleId": 41234, 8 | "bundleId": "com.example", 9 | "bundleVersion": "1.2.3", 10 | "signedTransactionInfo": "signed_transaction_info_value", 11 | "signedRenewalInfo": "signed_renewal_info_value", 12 | "status": 1 13 | }, 14 | "version": "2.0", 15 | "signedDate": 1698148900000 16 | } -------------------------------------------------------------------------------- /models/InAppOwnershipType.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { StringValidator } from "./Validator"; 4 | 5 | /** 6 | * The relationship of the user with the family-shared purchase to which they have access. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/inappownershiptype inAppOwnershipType} 9 | */ 10 | export enum InAppOwnershipType { 11 | FAMILY_SHARED = "FAMILY_SHARED", 12 | PURCHASED = "PURCHASED", 13 | } 14 | 15 | export class InAppOwnershipTypeValidator extends StringValidator {} -------------------------------------------------------------------------------- /models/Environment.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { StringValidator } from "./Validator"; 4 | 5 | /** 6 | * The server environment, either sandbox or production. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/environment environment} 9 | */ 10 | export enum Environment { 11 | SANDBOX = "Sandbox", 12 | PRODUCTION = "Production", 13 | XCODE = "Xcode", 14 | LOCAL_TESTING = "LocalTesting", // Used for unit testing 15 | } 16 | 17 | export class EnvironmentValidator extends StringValidator {} -------------------------------------------------------------------------------- /tests/resources/models/appTransaction.json: -------------------------------------------------------------------------------- 1 | { 2 | "receiptType": "LocalTesting", 3 | "appAppleId": 531412, 4 | "bundleId": "com.example", 5 | "applicationVersion": "1.2.3", 6 | "versionExternalIdentifier": 512, 7 | "receiptCreationDate": 1698148900000, 8 | "originalPurchaseDate": 1698148800000, 9 | "originalApplicationVersion": "1.1.2", 10 | "deviceVerification": "device_verification_value", 11 | "deviceVerificationNonce": "48ccfa42-7431-4f22-9908-7e88983e105a", 12 | "preorderDate": 1698148700000, 13 | "appTransactionId": "71134", 14 | "originalPlatform": "iOS" 15 | } -------------------------------------------------------------------------------- /models/ExtendReasonCode.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { NumberValidator } from "./Validator"; 4 | 5 | /** 6 | * The code that represents the reason for the subscription-renewal-date extension. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/extendreasoncode extendReasonCode} 9 | */ 10 | export enum ExtendReasonCode { 11 | UNDECLARED = 0, 12 | CUSTOMER_SATISFACTION = 1, 13 | OTHER = 2, 14 | SERVICE_ISSUE_OR_OUTAGE = 3, 15 | } 16 | 17 | export class ExtendReasonCodeValidator extends NumberValidator {} -------------------------------------------------------------------------------- /models/ConsumptionStatus.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { NumberValidator } from "./Validator"; 4 | 5 | /** 6 | * A value that indicates the extent to which the customer consumed the in-app purchase. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/consumptionstatus consumptionStatus} 9 | */ 10 | export enum ConsumptionStatus { 11 | UNDECLARED = 0, 12 | NOT_CONSUMED = 1, 13 | PARTIALLY_CONSUMED = 2, 14 | FULLY_CONSUMED = 3, 15 | } 16 | 17 | export class ConsumptionStatusValidator extends NumberValidator {} -------------------------------------------------------------------------------- /models/Message.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | /** 4 | * A message identifier you provide in a real-time response to your Get Retention Message endpoint. 5 | * 6 | * {@link https://developer.apple.com/documentation/retentionmessaging/message message} 7 | */ 8 | export interface Message { 9 | 10 | /** 11 | * The identifier of the message to display to the customer. 12 | * 13 | * {@link https://developer.apple.com/documentation/retentionmessaging/messageidentifier messageIdentifier} 14 | **/ 15 | messageIdentifier?: string 16 | } 17 | -------------------------------------------------------------------------------- /models/OfferDiscountType.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { StringValidator } from "./Validator"; 4 | 5 | /** 6 | * The payment mode for a discount offer on an In-App Purchase. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/offerdiscounttype offerDiscountType} 9 | */ 10 | export enum OfferDiscountType { 11 | FREE_TRIAL = "FREE_TRIAL", 12 | PAY_AS_YOU_GO = "PAY_AS_YOU_GO", 13 | PAY_UP_FRONT = "PAY_UP_FRONT", 14 | ONE_TIME = "ONE_TIME" 15 | } 16 | 17 | export class OfferDiscountTypeValidator extends StringValidator {} -------------------------------------------------------------------------------- /models/Type.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { StringValidator } from "./Validator"; 4 | 5 | /** 6 | * The type of in-app purchase products you can offer in your app. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/type type} 9 | */ 10 | export enum Type { 11 | AUTO_RENEWABLE_SUBSCRIPTION = "Auto-Renewable Subscription", 12 | NON_CONSUMABLE = "Non-Consumable", 13 | CONSUMABLE = "Consumable", 14 | NON_RENEWING_SUBSCRIPTION ="Non-Renewing Subscription", 15 | } 16 | 17 | export class TypeValidator extends StringValidator {} -------------------------------------------------------------------------------- /models/ExpirationIntent.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { NumberValidator } from "./Validator"; 4 | 5 | /** 6 | * The reason an auto-renewable subscription expired. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/expirationintent expirationIntent} 9 | */ 10 | export enum ExpirationIntent { 11 | CUSTOMER_CANCELLED = 1, 12 | BILLING_ERROR = 2, 13 | CUSTOMER_DID_NOT_CONSENT_TO_PRICE_INCREASE = 3, 14 | PRODUCT_NOT_AVAILABLE = 4, 15 | OTHER = 5, 16 | } 17 | 18 | export class ExpirationIntentValidator extends NumberValidator {} -------------------------------------------------------------------------------- /models/PriceIncreaseStatus.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { NumberValidator } from "./Validator"; 4 | 5 | /** 6 | * The status that indicates whether an auto-renewable subscription is subject to a price increase. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/priceincreasestatus priceIncreaseStatus} 9 | */ 10 | export enum PriceIncreaseStatus { 11 | CUSTOMER_HAS_NOT_RESPONDED = 0, 12 | CUSTOMER_CONSENTED_OR_WAS_NOTIFIED_WITHOUT_NEEDING_CONSENT = 1, 13 | } 14 | 15 | export class PriceIncreaseStatusValidator extends NumberValidator {} -------------------------------------------------------------------------------- /models/TransactionReason.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { StringValidator } from "./Validator"; 4 | 5 | /** 6 | * The cause of a purchase transaction, which indicates whether it’s a customer’s purchase or a renewal for an auto-renewable subscription that the system initiates. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/transactionreason transactionReason} 9 | */ 10 | export enum TransactionReason { 11 | PURCHASE = "PURCHASE", 12 | RENEWAL = "RENEWAL", 13 | } 14 | 15 | export class TransactionReasonValidator extends StringValidator {} 16 | -------------------------------------------------------------------------------- /tests/resources/models/signedConsumptionRequestNotification.json: -------------------------------------------------------------------------------- 1 | { 2 | "notificationType": "CONSUMPTION_REQUEST", 3 | "notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20", 4 | "data": { 5 | "environment": "LocalTesting", 6 | "appAppleId": 41234, 7 | "bundleId": "com.example", 8 | "bundleVersion": "1.2.3", 9 | "signedTransactionInfo": "signed_transaction_info_value", 10 | "signedRenewalInfo": "signed_renewal_info_value", 11 | "status": 1, 12 | "consumptionRequestReason": "UNINTENDED_PURCHASE" 13 | }, 14 | "version": "2.0", 15 | "signedDate": 1698148900000 16 | } -------------------------------------------------------------------------------- /tests/resources/models/signedSummaryNotification.json: -------------------------------------------------------------------------------- 1 | { 2 | "notificationType": "RENEWAL_EXTENSION", 3 | "subtype": "SUMMARY", 4 | "notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20", 5 | "version": "2.0", 6 | "signedDate": 1698148900000, 7 | "summary": { 8 | "environment": "LocalTesting", 9 | "appAppleId": 41234, 10 | "bundleId": "com.example", 11 | "productId": "com.example.product", 12 | "requestIdentifier": "efb27071-45a4-4aca-9854-2a1e9146f265", 13 | "storefrontCountryCodes": [ 14 | "CAN", 15 | "USA", 16 | "MEX" 17 | ], 18 | "succeededCount": 5, 19 | "failedCount": 2 20 | } 21 | } -------------------------------------------------------------------------------- /.github/workflows/ci-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Library 2 | permissions: 3 | contents: read 4 | on: 5 | release: 6 | types: [published] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v5 12 | - uses: actions/setup-node@v6 13 | with: 14 | node-version: '18.x' 15 | scope: '@apple' 16 | always-auth: true 17 | registry-url: 'https://registry.npmjs.org' 18 | - run: yarn install --frozen-lockfile 19 | - run: yarn build 20 | - run: yarn publish --access public 21 | env: 22 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 23 | -------------------------------------------------------------------------------- /models/DefaultConfigurationRequest.ts: -------------------------------------------------------------------------------- 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 | } 17 | -------------------------------------------------------------------------------- /models/UpdateAppAccountTokenRequest.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | /** 4 | * The request body that contains an app account token value. 5 | * 6 | * {@link https://developer.apple.com/documentation/appstoreserverapi/updateappaccounttokenrequest UpdateAppAccountTokenRequest} 7 | */ 8 | export interface UpdateAppAccountTokenRequest { 9 | 10 | /** 11 | * The UUID that an app optionally generates to map a customer's in-app purchase with its resulting App Store transaction. 12 | * 13 | * {@link https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken appAccountToken} 14 | **/ 15 | appAccountToken: string 16 | } -------------------------------------------------------------------------------- /models/ConsumptionRequestReason.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Apple Inc. Licensed under MIT License. 2 | 3 | import { StringValidator } from "./Validator"; 4 | 5 | /** 6 | * The customer-provided reason for a refund request. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/consumptionrequestreason consumptionRequestReason} 9 | */ 10 | export enum ConsumptionRequestReason { 11 | UNINTENDED_PURCHASE = "UNINTENDED_PURCHASE", 12 | FULFILLMENT_ISSUE = "FULFILLMENT_ISSUE", 13 | UNSATISFIED_WITH_PURCHASE = "UNSATISFIED_WITH_PURCHASE", 14 | LEGAL = "LEGAL", 15 | OTHER = "OTHER", 16 | } 17 | 18 | export class ConsumptionRequestReasonValidator extends StringValidator {} -------------------------------------------------------------------------------- /models/PlayTime.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { NumberValidator } from "./Validator"; 4 | 5 | /** 6 | * A value that indicates the amount of time that the customer used the app. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/playtime playTime} 9 | */ 10 | export enum PlayTime { 11 | UNDECLARED = 0, 12 | ZERO_TO_FIVE_MINUTES = 1, 13 | FIVE_TO_SIXTY_MINUTES = 2, 14 | ONE_TO_SIX_HOURS = 3, 15 | SIX_HOURS_TO_TWENTY_FOUR_HOURS = 4, 16 | ONE_DAY_TO_FOUR_DAYS = 5, 17 | FOUR_DAYS_TO_SIXTEEN_DAYS = 6, 18 | OVER_SIXTEEN_DAYS = 7, 19 | } 20 | 21 | export class PlayTimeValidator extends NumberValidator {} 22 | -------------------------------------------------------------------------------- /tests/unit-tests/promotional_offer_signature_creator.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { PromotionalOfferSignatureCreator } from "../../promotional_offer"; 4 | import { readFile } from "../util" 5 | 6 | 7 | describe('Promotional Offer Signature Creation Test', () => { 8 | it('should create a non-null signature', async () => { 9 | const signatureCreator = new PromotionalOfferSignatureCreator(readFile('tests/resources/certs/testSigningKey.p8'), "keyId", "bundleId"); 10 | const signature = signatureCreator.createSignature('productId', 'offerId', 'appAccountToken', "20fba8a0-2b80-4a7d-a17f-85c1854727f8", 1698148900000) 11 | expect(signature).toBeTruthy() 12 | }) 13 | }) -------------------------------------------------------------------------------- /tests/resources/models/getNotificationHistoryResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "paginationToken": "57715481-805a-4283-8499-1c19b5d6b20a", 3 | "hasMore": true, 4 | "notificationHistory": [ 5 | { 6 | "sendAttempts": [ 7 | { 8 | "attemptDate": 1698148900000, 9 | "sendAttemptResult": "NO_RESPONSE" 10 | }, { 11 | "attemptDate": 1698148950000, 12 | "sendAttemptResult": "SUCCESS" 13 | } 14 | ], 15 | "signedPayload": "signed_payload_one" 16 | }, 17 | { 18 | "sendAttempts": [ 19 | { 20 | "attemptDate": 1698148800000, 21 | "sendAttemptResult": "CIRCULAR_REDIRECT" 22 | } 23 | ], 24 | "signedPayload": "signed_payload_two" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /models/AccountTenure.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { NumberValidator } from "./Validator"; 4 | 5 | /** 6 | * The age of the customer’s account. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/accounttenure accountTenure} 9 | */ 10 | export enum AccountTenure { 11 | UNDECLARED = 0, 12 | ZERO_TO_THREE_DAYS = 1, 13 | THREE_DAYS_TO_TEN_DAYS = 2, 14 | TEN_DAYS_TO_THIRTY_DAYS = 3, 15 | THIRTY_DAYS_TO_NINETY_DAYS = 4, 16 | NINETY_DAYS_TO_ONE_HUNDRED_EIGHTY_DAYS = 5, 17 | ONE_HUNDRED_EIGHTY_DAYS_TO_THREE_HUNDRED_SIXTY_FIVE_DAYS = 6, 18 | GREATER_THAN_THREE_HUNDRED_SIXTY_FIVE_DAYS = 7, 19 | } 20 | 21 | export class AccountTenureValidator extends NumberValidator {} 22 | -------------------------------------------------------------------------------- /models/DeliveryStatus.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { NumberValidator } from "./Validator"; 4 | 5 | /** 6 | * A value that indicates whether the app successfully delivered an in-app purchase that works properly. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/deliverystatus deliveryStatus} 9 | */ 10 | export enum DeliveryStatus { 11 | DELIVERED_AND_WORKING_PROPERLY = 0, 12 | DID_NOT_DELIVER_DUE_TO_QUALITY_ISSUE = 1, 13 | DELIVERED_WRONG_ITEM = 2, 14 | DID_NOT_DELIVER_DUE_TO_SERVER_OUTAGE = 3, 15 | DID_NOT_DELIVER_DUE_TO_IN_GAME_CURRENCY_CHANGE = 4, 16 | DID_NOT_DELIVER_FOR_OTHER_REASON = 5, 17 | } 18 | 19 | export class DeliveryStatusValidator extends NumberValidator {} -------------------------------------------------------------------------------- /.github/workflows/ci-prb.yml: -------------------------------------------------------------------------------- 1 | name: PR Builder 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | pull_request: 8 | branches: [ main ] 9 | push: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | name: Node.js Build ${{ matrix.node }} ${{ matrix.os }} 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | node: [ 18.x, 20.x, 22.x, 24.x ] 19 | os: [ ubuntu-latest ] 20 | steps: 21 | - uses: actions/checkout@v5 22 | - name: Use Node ${{ matrix.node }} 23 | uses: actions/setup-node@v6 24 | with: 25 | node-version: ${{ matrix.node }} 26 | - name: Install Node dependencies 27 | run: yarn --frozen-lockfile 28 | - name: Build 29 | run: yarn build 30 | - name: Test 31 | run: yarn test 32 | -------------------------------------------------------------------------------- /models/UploadMessageImage.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | /** 4 | * The definition of an image with its alternative text. 5 | * 6 | * {@link https://developer.apple.com/documentation/retentionmessaging/uploadmessageimage UploadMessageImage} 7 | */ 8 | export interface UploadMessageImage { 9 | 10 | /** 11 | * The unique identifier of an image. 12 | * 13 | * {@link https://developer.apple.com/documentation/retentionmessaging/imageidentifier imageIdentifier} 14 | **/ 15 | imageIdentifier: string 16 | 17 | /** 18 | * The alternative text you provide for the corresponding image. 19 | * 20 | * **Maximum length: 150 characters** 21 | * 22 | * {@link https://developer.apple.com/documentation/retentionmessaging/alttext altText} 23 | **/ 24 | altText: string 25 | } 26 | -------------------------------------------------------------------------------- /tests/resources/models/signedRenewalInfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "expirationIntent": 1, 3 | "originalTransactionId": "12345", 4 | "autoRenewProductId": "com.example.product.2", 5 | "productId": "com.example.product", 6 | "autoRenewStatus": 1, 7 | "isInBillingRetryPeriod": true, 8 | "priceIncreaseStatus": 0, 9 | "gracePeriodExpiresDate": 1698148900000, 10 | "offerType": 2, 11 | "offerIdentifier": "abc.123", 12 | "signedDate": 1698148800000, 13 | "environment": "LocalTesting", 14 | "recentSubscriptionStartDate": 1698148800000, 15 | "renewalDate": 1698148850000, 16 | "renewalPrice": 9990, 17 | "currency": "USD", 18 | "offerDiscountType": "PAY_AS_YOU_GO", 19 | "eligibleWinBackOfferIds": [ 20 | "eligible1", 21 | "eligible2" 22 | ], 23 | "appTransactionId": "71134", 24 | "offerPeriod": "P1Y", 25 | "appAccountToken": "7e3fb20b-4cdb-47cc-936d-99d65f608138" 26 | } 27 | -------------------------------------------------------------------------------- /models/AlternateProduct.ts: -------------------------------------------------------------------------------- 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 | } 24 | -------------------------------------------------------------------------------- /models/SendAttemptResult.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { StringValidator } from "./Validator"; 4 | 5 | /** 6 | * The success or error information the App Store server records when it attempts to send an App Store server notification to your server. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/sendattemptresult sendAttemptResult} 9 | */ 10 | export enum SendAttemptResult { 11 | SUCCESS = "SUCCESS", 12 | TIMED_OUT = "TIMED_OUT", 13 | TLS_ISSUE = "TLS_ISSUE", 14 | CIRCULAR_REDIRECT = "CIRCULAR_REDIRECT", 15 | NO_RESPONSE = "NO_RESPONSE", 16 | SOCKET_ISSUE = "SOCKET_ISSUE", 17 | UNSUPPORTED_CHARSET = "UNSUPPORTED_CHARSET", 18 | INVALID_RESPONSE = "INVALID_RESPONSE", 19 | PREMATURE_CLOSE = "PREMATURE_CLOSE", 20 | UNSUCCESSFUL_HTTP_RESPONSE_CODE = "UNSUCCESSFUL_HTTP_RESPONSE_CODE", 21 | OTHER = "OTHER", 22 | } 23 | 24 | export class SendAttemptResultValidator extends StringValidator {} 25 | -------------------------------------------------------------------------------- /models/FirstSendAttemptResult.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { StringValidator, Validator } from "./Validator"; 4 | 5 | /** 6 | * An error or result that the App Store server receives when attempting to send an App Store server notification to your server. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/firstsendattemptresult firstSendAttemptResult} 9 | */ 10 | export enum FirstSendAttemptResult { 11 | SUCCESS = "SUCCESS", 12 | TIMED_OUT = "TIMED_OUT", 13 | TLS_ISSUE = "TLS_ISSUE", 14 | CIRCULAR_REDIRECT = "CIRCULAR_REDIRECT", 15 | NO_RESPONSE = "NO_RESPONSE", 16 | SOCKET_ISSUE = "SOCKET_ISSUE", 17 | UNSUPPORTED_CHARSET = "UNSUPPORTED_CHARSET", 18 | INVALID_RESPONSE = "INVALID_RESPONSE", 19 | PREMATURE_CLOSE = "PREMATURE_CLOSE", 20 | UNSUCCESSFUL_HTTP_RESPONSE_CODE = "UNSUCCESSFUL_HTTP_RESPONSE_CODE", 21 | OTHER = "OTHER", 22 | } 23 | 24 | export class FirstSendAttemptResultValidator extends StringValidator {} -------------------------------------------------------------------------------- /tests/resources/models/signedTransaction.json: -------------------------------------------------------------------------------- 1 | { 2 | "transactionId":"23456", 3 | "originalTransactionId":"12345", 4 | "webOrderLineItemId":"34343", 5 | "bundleId":"com.example", 6 | "productId":"com.example.product", 7 | "subscriptionGroupIdentifier":"55555", 8 | "purchaseDate":1698148900000, 9 | "originalPurchaseDate":1698148800000, 10 | "expiresDate":1698149000000, 11 | "quantity":1, 12 | "type":"Auto-Renewable Subscription", 13 | "appAccountToken": "7e3fb20b-4cdb-47cc-936d-99d65f608138", 14 | "inAppOwnershipType":"PURCHASED", 15 | "signedDate":1698148900000, 16 | "revocationReason": 1, 17 | "revocationDate": 1698148950000, 18 | "isUpgraded": true, 19 | "offerType":1, 20 | "offerIdentifier": "abc.123", 21 | "environment":"LocalTesting", 22 | "transactionReason":"PURCHASE", 23 | "storefront":"USA", 24 | "storefrontId":"143441", 25 | "price": 10990, 26 | "currency": "USD", 27 | "offerDiscountType": "PAY_AS_YOU_GO", 28 | "appTransactionId": "71134", 29 | "offerPeriod": "P1Y" 30 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2023 Apple Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /models/LifetimeDollarsPurchased.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { NumberValidator } from "./Validator"; 4 | 5 | /** 6 | * A value that indicates the total amount, in USD, of in-app purchases the customer has made in your app, across all platforms. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarspurchased lifetimeDollarsPurchased} 9 | */ 10 | export enum LifetimeDollarsPurchased { 11 | UNDECLARED = 0, 12 | ZERO_DOLLARS = 1, 13 | ONE_CENT_TO_FORTY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 2, 14 | FIFTY_DOLLARS_TO_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 3, 15 | ONE_HUNDRED_DOLLARS_TO_FOUR_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 4, 16 | FIVE_HUNDRED_DOLLARS_TO_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 5, 17 | ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 6, 18 | TWO_THOUSAND_DOLLARS_OR_GREATER = 7, 19 | } 20 | 21 | export class LifetimeDollarsPurchasedValidator extends NumberValidator {} -------------------------------------------------------------------------------- /models/RealtimeRequestBody.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | import { Validator } from "./Validator" 4 | 5 | /** 6 | * The request body the App Store server sends to your Get Retention Message endpoint. 7 | * 8 | * {@link https://developer.apple.com/documentation/retentionmessaging/realtimerequestbody RealtimeRequestBody} 9 | */ 10 | export interface RealtimeRequestBody { 11 | 12 | /** 13 | * The payload in JSON Web Signature (JWS) format, signed by the App Store. 14 | * 15 | * {@link https://developer.apple.com/documentation/retentionmessaging/signedpayload signedPayload} 16 | **/ 17 | signedPayload?: string 18 | } 19 | 20 | 21 | export class RealtimeRequestBodyValidator implements Validator { 22 | validate(obj: any): obj is RealtimeRequestBody { 23 | if ((typeof obj['signedPayload'] !== 'undefined') && !(typeof obj['signedPayload'] === "string" || obj['signedPayload'] instanceof String)) { 24 | return false 25 | } 26 | return true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declarationMap": false, 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "alwaysStrict": true, 7 | "declaration": true, 8 | "experimentalDecorators": true, 9 | "incremental": true, 10 | "lib": [ 11 | "es2020" 12 | ], 13 | "module": "CommonJS", 14 | "newLine": "lf", 15 | "noEmitOnError": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "noImplicitAny": true, 18 | "noImplicitReturns": true, 19 | "noImplicitThis": true, 20 | "noUnusedLocals": false, 21 | "noUnusedParameters": false, 22 | "resolveJsonModule": true, 23 | "skipLibCheck": true, 24 | "strict": true, 25 | "strictNullChecks": true, 26 | "strictPropertyInitialization": true, 27 | "stripInternal": false, 28 | "target": "ES2020", 29 | "composite": false, 30 | "tsBuildInfoFile": "tsconfig.tsbuildinfo", 31 | "outDir": "dist" 32 | }, 33 | "include": [ 34 | "**/*.ts" 35 | ], 36 | "exclude": [ 37 | "node_modules", 38 | "dist/**/*" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /models/LifetimeDollarsRefunded.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { NumberValidator } from "./Validator"; 4 | 5 | /** 6 | * A value that indicates the dollar amount of refunds the customer has received in your app, since purchasing the app, across all platforms. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarsrefunded lifetimeDollarsRefunded} 9 | */ 10 | export enum LifetimeDollarsRefunded { 11 | UNDECLARED = 0, 12 | ZERO_DOLLARS = 1, 13 | ONE_CENT_TO_FORTY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 2, 14 | FIFTY_DOLLARS_TO_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 3, 15 | ONE_HUNDRED_DOLLARS_TO_FOUR_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 4, 16 | FIVE_HUNDRED_DOLLARS_TO_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 5, 17 | ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 6, 18 | TWO_THOUSAND_DOLLARS_OR_GREATER = 7, 19 | } 20 | 21 | export class LifetimeDollarsRefundedValidator extends NumberValidator {} 22 | -------------------------------------------------------------------------------- /models/Subtype.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { StringValidator } from "./Validator"; 4 | 5 | /** 6 | * A string that provides details about select notification types in version 2. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/subtype subtype} 9 | */ 10 | export enum Subtype { 11 | INITIAL_BUY = "INITIAL_BUY", 12 | RESUBSCRIBE = "RESUBSCRIBE", 13 | DOWNGRADE = "DOWNGRADE", 14 | UPGRADE = "UPGRADE", 15 | AUTO_RENEW_ENABLED = "AUTO_RENEW_ENABLED", 16 | AUTO_RENEW_DISABLED = "AUTO_RENEW_DISABLED", 17 | VOLUNTARY = "VOLUNTARY", 18 | BILLING_RETRY = "BILLING_RETRY", 19 | PRICE_INCREASE = "PRICE_INCREASE", 20 | GRACE_PERIOD = "GRACE_PERIOD", 21 | PENDING = "PENDING", 22 | ACCEPTED = "ACCEPTED", 23 | BILLING_RECOVERY = "BILLING_RECOVERY", 24 | PRODUCT_NOT_FOR_SALE = "PRODUCT_NOT_FOR_SALE", 25 | SUMMARY = "SUMMARY", 26 | FAILURE = "FAILURE", 27 | UNREPORTED = "UNREPORTED", 28 | } 29 | 30 | export class SubtypeValidator extends StringValidator {} 31 | -------------------------------------------------------------------------------- /tests/unit-tests/DecodedRealtimeRequestBody.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | import { Environment } from "../../models/Environment"; 4 | import { createSignedDataFromJson, getDefaultSignedPayloadVerifier } from "../util"; 5 | 6 | describe('DecodedRealtimeRequestBody', () => { 7 | it('should decode a realtime request', async () => { 8 | const signedRealtimeRequest = createSignedDataFromJson("tests/resources/models/decodedRealtimeRequest.json") 9 | 10 | const request = await getDefaultSignedPayloadVerifier().verifyAndDecodeRealtimeRequest(signedRealtimeRequest) 11 | 12 | expect("99371282").toBe(request.originalTransactionId) 13 | expect(531412).toBe(request.appAppleId) 14 | expect("com.example.product").toBe(request.productId) 15 | expect("en-US").toBe(request.userLocale) 16 | expect("3db5c98d-8acf-4e29-831e-8e1f82f9f6e9").toBe(request.requestIdentifier) 17 | expect(Environment.LOCAL_TESTING).toBe(request.environment) 18 | expect(1698148900000).toBe(request.signedDate) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /.github/workflows/ci-release-docs.yml: -------------------------------------------------------------------------------- 1 | name: Doc Builder 2 | permissions: 3 | contents: read 4 | on: 5 | release: 6 | types: [published] 7 | jobs: 8 | build: 9 | name: Doc Builder 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v5 13 | - name: Use Node 20.x 14 | uses: actions/setup-node@v6 15 | with: 16 | node-version: '20.x' 17 | - name: Install Node dependencies 18 | run: yarn --frozen-lockfile 19 | - name: Build 20 | run: yarn build 21 | - name: Build docs 22 | run: yarn build-docs 23 | - name: Upload docs 24 | uses: actions/upload-pages-artifact@v4 25 | with: 26 | path: docs 27 | deploy: 28 | permissions: 29 | pages: write 30 | id-token: write 31 | environment: 32 | name: github-pages 33 | url: ${{ steps.deployment.outputs.page_url }} 34 | needs: build 35 | runs-on: ubuntu-latest 36 | name: Deploy docs 37 | steps: 38 | - name: Deploy 39 | id: deployment 40 | uses: actions/deploy-pages@v4 41 | -------------------------------------------------------------------------------- /models/ResponseBodyV2.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { Validator } from "./Validator" 4 | 5 | /** 6 | * The response body the App Store sends in a version 2 server notification. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/responsebodyv2 ResponseBodyV2} 9 | */ 10 | export interface ResponseBodyV2 { 11 | 12 | /** 13 | * A cryptographically signed payload, in JSON Web Signature (JWS) format, containing the response body for a version 2 notification. 14 | * 15 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/signedpayload signedPayload} 16 | **/ 17 | signedPayload?: string 18 | } 19 | 20 | 21 | export class ResponseBodyV2Validator implements Validator { 22 | validate(obj: any): obj is ResponseBodyV2 { 23 | if ((typeof obj['signedPayload'] !== 'undefined') && !(typeof obj['signedPayload'] === "string" || obj['signedPayload'] instanceof String)) { 24 | return false 25 | } 26 | return true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/resources/models/getAllSubscriptionStatusesResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "environment": "LocalTesting", 3 | "bundleId": "com.example", 4 | "appAppleId": 5454545, 5 | "data": [ 6 | { 7 | "subscriptionGroupIdentifier": "sub_group_one", 8 | "lastTransactions": [ 9 | { 10 | "status": 1, 11 | "originalTransactionId": "3749183", 12 | "signedTransactionInfo": "signed_transaction_one", 13 | "signedRenewalInfo": "signed_renewal_one" 14 | }, 15 | { 16 | "status": 5, 17 | "originalTransactionId": "5314314134", 18 | "signedTransactionInfo": "signed_transaction_two", 19 | "signedRenewalInfo": "signed_renewal_two" 20 | } 21 | ] 22 | }, 23 | { 24 | "subscriptionGroupIdentifier": "sub_group_two", 25 | "lastTransactions": [ 26 | { 27 | "status": 2, 28 | "originalTransactionId": "3413453", 29 | "signedTransactionInfo": "signed_transaction_three", 30 | "signedRenewalInfo": "signed_renewal_three" 31 | } 32 | ] 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@apple/app-store-server-library", 3 | "version": "2.0.0", 4 | "description": "The App Store Server Library", 5 | "main": "dist/index.js", 6 | "keywords": [], 7 | "author": "Apple Inc.", 8 | "license": "MIT", 9 | "repository": { 10 | "url": "https://github.com/apple/app-store-server-library-node" 11 | }, 12 | "types": "dist/index.d.ts", 13 | "dependencies": { 14 | "@types/jsonwebtoken": "^9.0.5", 15 | "@types/jsrsasign": "^10.5.12", 16 | "@types/node": "^24.0.10", 17 | "@types/node-fetch": "^2.6.13", 18 | "base64url": "^3.0.1", 19 | "jsonwebtoken": "^9.0.2", 20 | "jsrsasign": "^11.0.0", 21 | "node-fetch": "^2.7.0" 22 | }, 23 | "devDependencies": { 24 | "@types/jest": "^30.0.0", 25 | "jest": "^30.0.5", 26 | "ts-jest": "^29.1.1", 27 | "typedoc": "^0.28.7", 28 | "typescript": "^5.3.3" 29 | }, 30 | "resolutions": { 31 | "@babel/traverse": "^7.23.6", 32 | "semver": "^6.3.1" 33 | }, 34 | "scripts": { 35 | "build": "tsc -p .", 36 | "test": "jest", 37 | "build-docs": "typedoc index.ts" 38 | }, 39 | "files": [ 40 | "dist" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /models/TransactionInfoResponse.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { Validator } from "./Validator" 4 | 5 | /** 6 | * A response that contains signed transaction information for a single transaction. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/transactioninforesponse TransactionInfoResponse} 9 | */ 10 | export interface TransactionInfoResponse { 11 | 12 | /** 13 | * A customer’s in-app purchase transaction, signed by Apple, in JSON Web Signature (JWS) format. 14 | * 15 | * {@link https://developer.apple.com/documentation/appstoreserverapi/jwstransaction JWSTransaction} 16 | **/ 17 | signedTransactionInfo?: string 18 | } 19 | 20 | 21 | export class TransactionInfoResponseValidator implements Validator { 22 | validate(obj: any): obj is TransactionInfoResponse { 23 | if ((typeof obj['signedTransactionInfo'] !== 'undefined') && !(typeof obj['signedTransactionInfo'] === "string" || obj['signedTransactionInfo'] instanceof String)) { 24 | return false 25 | } 26 | return true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /models/AppTransactionInfoResponse.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | import { Validator } from "./Validator" 4 | 5 | /** 6 | * A response that contains signed app transaction information for a customer. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/apptransactioninforesponse AppTransactionInfoResponse} 9 | */ 10 | export interface AppTransactionInfoResponse { 11 | /** 12 | * A customer’s app transaction information, signed by Apple, in JSON Web Signature (JWS) format. 13 | * 14 | * {@link https://developer.apple.com/documentation/appstoreserverapi/jwsapptransaction JWSAppTransaction} 15 | **/ 16 | signedAppTransactionInfo?: string 17 | } 18 | 19 | export class AppTransactionInfoResponseValidator implements Validator { 20 | validate(obj: any): obj is AppTransactionInfoResponse { 21 | if ((typeof obj['signedAppTransactionInfo'] !== 'undefined') && !(typeof obj['signedAppTransactionInfo'] === "string" || obj['signedAppTransactionInfo'] instanceof String)) { 22 | return false 23 | } 24 | return true 25 | } 26 | } -------------------------------------------------------------------------------- /models/SendTestNotificationResponse.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { Validator } from "./Validator" 4 | 5 | /** 6 | * A response that contains the test notification token. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/sendtestnotificationresponse SendTestNotificationResponse} 9 | */ 10 | export interface SendTestNotificationResponse { 11 | 12 | /** 13 | * A unique identifier for a notification test that the App Store server sends to your server. 14 | * 15 | * {@link https://developer.apple.com/documentation/appstoreserverapi/testnotificationtoken testNotificationToken} 16 | **/ 17 | testNotificationToken?: string 18 | } 19 | 20 | 21 | export class SendTestNotificationResponseValidator implements Validator { 22 | validate(obj: any): obj is SendTestNotificationResponse { 23 | if ((typeof obj['testNotificationToken'] !== 'undefined') && !(typeof obj['testNotificationToken'] === "string" || obj['testNotificationToken'] instanceof String)) { 24 | return false 25 | } 26 | return true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /models/MassExtendRenewalDateResponse.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { Validator } from "./Validator" 4 | 5 | /** 6 | * A response that indicates the server successfully received the subscription-renewal-date extension request. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/massextendrenewaldateresponse MassExtendRenewalDateResponse} 9 | */ 10 | export interface MassExtendRenewalDateResponse { 11 | 12 | /** 13 | * A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. 14 | * 15 | * {@link https://developer.apple.com/documentation/appstoreserverapi/requestidentifier requestIdentifier} 16 | **/ 17 | requestIdentifier?: string 18 | } 19 | 20 | 21 | export class MassExtendRenewalDateResponseValidator implements Validator { 22 | validate(obj: any): obj is MassExtendRenewalDateResponse { 23 | if ((typeof obj['requestIdentifier'] !== 'undefined') && !(typeof obj['requestIdentifier'] === "string" || obj['requestIdentifier'] instanceof String)) { 24 | return false 25 | } 26 | return true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /models/RealtimeResponseBody.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | import { AlternateProduct } from "./AlternateProduct" 4 | import { Message } from "./Message" 5 | import { PromotionalOffer } from "./PromotionalOffer" 6 | 7 | /** 8 | * A response you provide to choose, in real time, a retention message the system displays to the customer. 9 | * 10 | * {@link https://developer.apple.com/documentation/retentionmessaging/realtimeresponsebody RealtimeResponseBody} 11 | */ 12 | export interface RealtimeResponseBody { 13 | 14 | /** 15 | * A retention message that's text-based and can include an optional image. 16 | * 17 | * {@link https://developer.apple.com/documentation/retentionmessaging/message message} 18 | **/ 19 | message?: Message 20 | 21 | /** 22 | * A retention message with a switch-plan option. 23 | * 24 | * {@link https://developer.apple.com/documentation/retentionmessaging/alternateproduct alternateProduct} 25 | **/ 26 | alternateProduct?: AlternateProduct 27 | 28 | /** 29 | * A retention message that includes a promotional offer. 30 | * 31 | * {@link https://developer.apple.com/documentation/retentionmessaging/promotionaloffer promotionalOffer} 32 | **/ 33 | promotionalOffer?: PromotionalOffer 34 | } 35 | -------------------------------------------------------------------------------- /models/PromotionalOffer.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | import { PromotionalOfferSignatureV1 } from "./PromotionalOfferSignatureV1" 4 | 5 | /** 6 | * A promotional offer and message you provide in a real-time response to your Get Retention Message endpoint. 7 | * 8 | * {@link https://developer.apple.com/documentation/retentionmessaging/promotionaloffer promotionalOffer} 9 | */ 10 | export interface PromotionalOffer { 11 | 12 | /** 13 | * The identifier of the message to display to the customer, along with the promotional offer. 14 | * 15 | * {@link https://developer.apple.com/documentation/retentionmessaging/messageidentifier messageIdentifier} 16 | **/ 17 | messageIdentifier?: string 18 | 19 | /** 20 | * The promotional offer signature in V2 format. 21 | * 22 | * {@link https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev2 promotionalOfferSignatureV2} 23 | **/ 24 | promotionalOfferSignatureV2?: string 25 | 26 | /** 27 | * The promotional offer signature in V1 format. 28 | * 29 | * {@link https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev1 promotionalOfferSignatureV1} 30 | **/ 31 | promotionalOfferSignatureV1?: PromotionalOfferSignatureV1 32 | } 33 | -------------------------------------------------------------------------------- /models/ExtendRenewalDateRequest.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { ExtendReasonCode, ExtendReasonCodeValidator } from "./ExtendReasonCode" 4 | 5 | /** 6 | * The request body that contains subscription-renewal-extension data for an individual subscription. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/extendrenewaldaterequest ExtendRenewalDateRequest} 9 | */ 10 | export interface ExtendRenewalDateRequest { 11 | 12 | /** 13 | * The number of days to extend the subscription renewal date. 14 | * 15 | * {@link https://developer.apple.com/documentation/appstoreserverapi/extendbydays extendByDays} 16 | * maximum: 90 17 | **/ 18 | extendByDays?: number 19 | 20 | /** 21 | * The reason code for the subscription date extension 22 | * 23 | * {@link https://developer.apple.com/documentation/appstoreserverapi/extendreasoncode extendReasonCode} 24 | **/ 25 | extendReasonCode?: ExtendReasonCode 26 | 27 | /** 28 | * A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. 29 | * 30 | * {@link https://developer.apple.com/documentation/appstoreserverapi/requestidentifier requestIdentifier} 31 | **/ 32 | requestIdentifier?: string 33 | } -------------------------------------------------------------------------------- /models/NotificationTypeV2.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { StringValidator, Validator } from "./Validator"; 4 | 5 | /** 6 | * The type that describes the in-app purchase or external purchase event for which the App Store sends the version 2 notification. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/notificationtype notificationType} 9 | */ 10 | export enum NotificationTypeV2 { 11 | SUBSCRIBED = "SUBSCRIBED", 12 | DID_CHANGE_RENEWAL_PREF = "DID_CHANGE_RENEWAL_PREF", 13 | DID_CHANGE_RENEWAL_STATUS = "DID_CHANGE_RENEWAL_STATUS", 14 | OFFER_REDEEMED = "OFFER_REDEEMED", 15 | DID_RENEW = "DID_RENEW", 16 | EXPIRED = "EXPIRED", 17 | DID_FAIL_TO_RENEW = "DID_FAIL_TO_RENEW", 18 | GRACE_PERIOD_EXPIRED = "GRACE_PERIOD_EXPIRED", 19 | PRICE_INCREASE = "PRICE_INCREASE", 20 | REFUND = "REFUND", 21 | REFUND_DECLINED = "REFUND_DECLINED", 22 | CONSUMPTION_REQUEST = "CONSUMPTION_REQUEST", 23 | RENEWAL_EXTENDED = "RENEWAL_EXTENDED", 24 | REVOKE = "REVOKE", 25 | TEST = "TEST", 26 | RENEWAL_EXTENSION = "RENEWAL_EXTENSION", 27 | REFUND_REVERSED = "REFUND_REVERSED", 28 | EXTERNAL_PURCHASE_TOKEN = "EXTERNAL_PURCHASE_TOKEN", 29 | ONE_TIME_CHARGE = "ONE_TIME_CHARGE", 30 | } 31 | 32 | export class NotificationTypeV2Validator extends StringValidator {} -------------------------------------------------------------------------------- /models/UploadMessageRequestBody.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | import { UploadMessageImage } from "./UploadMessageImage" 4 | 5 | /** 6 | * The request body for uploading a message, which includes the message text and an optional image reference. 7 | * 8 | * {@link https://developer.apple.com/documentation/retentionmessaging/uploadmessagerequestbody UploadMessageRequestBody} 9 | */ 10 | export interface UploadMessageRequestBody { 11 | 12 | /** 13 | * The header text of the retention message that the system displays to customers. 14 | * 15 | * **Maximum length: 66 characters** 16 | * 17 | * {@link https://developer.apple.com/documentation/retentionmessaging/header header} 18 | **/ 19 | header: string 20 | 21 | /** 22 | * The body text of the retention message that the system displays to customers. 23 | * 24 | * **Maximum length: 144 characters** 25 | * 26 | * {@link https://developer.apple.com/documentation/retentionmessaging/body body} 27 | **/ 28 | body: string 29 | 30 | /** 31 | * The optional image identifier and its alternative text to appear as part of a text-based message with an image. 32 | * 33 | * {@link https://developer.apple.com/documentation/retentionmessaging/uploadmessageimage UploadMessageImage} 34 | **/ 35 | image?: UploadMessageImage 36 | } 37 | -------------------------------------------------------------------------------- /tests/unit-tests/receipt_utility.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { ReceiptUtility } from "../../receipt_utility" 4 | import { readFile } from "../util" 5 | 6 | 7 | describe('Receipt Utility Tets', () => { 8 | it('should not extract a transaction id from an xcode receipt without a transaction', async () => { 9 | const receipt = readFile('tests/resources/xcode/xcode-app-receipt-empty') 10 | const receipt_utility = new ReceiptUtility() 11 | const extracted_transaction_id = receipt_utility.extractTransactionIdFromAppReceipt(receipt) 12 | expect(extracted_transaction_id).toBeNull() 13 | }) 14 | it('should extract a transaction id from an xcode receipt with a transaction', async () => { 15 | const receipt = readFile('tests/resources/xcode/xcode-app-receipt-with-transaction') 16 | const receipt_utility = new ReceiptUtility() 17 | const extracted_transaction_id = receipt_utility.extractTransactionIdFromAppReceipt(receipt) 18 | expect(extracted_transaction_id).toBe("0") 19 | }) 20 | it('should extract a transaction id from an xcode transaction receipt', async () => { 21 | const receipt = readFile('tests/resources/mock_signed_data/legacyTransaction') 22 | const receipt_utility = new ReceiptUtility() 23 | const extracted_transaction_id = receipt_utility.extractTransactionIdFromTransactionReceipt(receipt) 24 | expect(extracted_transaction_id).toBe("33993399") 25 | }) 26 | }) -------------------------------------------------------------------------------- /tests/resources/xcode/xcode-signed-renewal-info: -------------------------------------------------------------------------------- 1 | eyJraWQiOiJBcHBsZV9YY29kZV9LZXkiLCJ0eXAiOiJKV1QiLCJ4NWMiOlsiTUlJQnpEQ0NBWEdnQXdJQkFnSUJBVEFLQmdncWhrak9QUVFEQWpCSU1TSXdJQVlEVlFRREV4bFRkRzl5WlV0cGRDQlVaWE4wYVc1bklHbHVJRmhqYjJSbE1TSXdJQVlEVlFRS0V4bFRkRzl5WlV0cGRDQlVaWE4wYVc1bklHbHVJRmhqYjJSbE1CNFhEVEl6TVRBeE9UQXhORFV6TmxvWERUSTBNVEF4T0RBeE5EVXpObG93U0RFaU1DQUdBMVVFQXhNWlUzUnZjbVZMYVhRZ1ZHVnpkR2x1WnlCcGJpQllZMjlrWlRFaU1DQUdBMVVFQ2hNWlUzUnZjbVZMYVhRZ1ZHVnpkR2x1WnlCcGJpQllZMjlrWlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQktYRVFnWWpDb3VQdFRzdEdyS3BZOEk1M25IN3JiREhuY0lMR25vZ1NBdWxJSTNzXC91Zk0wZzlEYzNCY3I0OTdBVWd6R1R2V3Bpd0p4cGVCMzcxTmdWK2pUREJLTUJJR0ExVWRFd0VCXC93UUlNQVlCQWY4Q0FRQXdKQVlEVlIwUkJCMHdHNEVaVTNSdmNtVkxhWFFnVkdWemRHbHVaeUJwYmlCWVkyOWtaVEFPQmdOVkhROEJBZjhFQkFNQ0I0QXdDZ1lJS29aSXpqMEVBd0lEU1FBd1JnSWhBTVp2VllKNjRDRitoMmZtc213dnpBY2VQcklEMTNycElKR0JFVytXZ3BwdEFpRUF4V2l5NCtUMXp0MzdWc3UwdmI2WXVtMCtOTHREcUhsSzZycE1jdjZKZm5BPSJdLCJhbGciOiJFUzI1NiJ9.eyJkZXZpY2VWZXJpZmljYXRpb24iOiJ1K1cxb1FUcXZGSE9RK1pCZTRRMHhQTUMyOGtxRUZ2YmJzRVBwTEtEVlJGdjFHSkdlZ21yTkhWb09ZTU9QdmIyIiwicHJvZHVjdElkIjoicGFzcy5wcmVtaXVtIiwiZGV2aWNlVmVyaWZpY2F0aW9uTm9uY2UiOiIzNDM5OTE5ZS04N2M5LTQ3YjYtYWVlZS0yODIzZjdhOWQzYzMiLCJyZW5ld2FsRGF0ZSI6MTcwMDM1ODMzNjA0OS43Mjk3LCJvcmlnaW5hbFRyYW5zYWN0aW9uSWQiOiIwIiwicmVjZW50U3Vic2NyaXB0aW9uU3RhcnREYXRlIjoxNjk3Njc5OTM2MDQ5LjcyOTcsImF1dG9SZW5ld1N0YXR1cyI6MSwic2lnbmVkRGF0ZSI6MTY5NzY3OTkzNjcxMS4wNzQ3LCJlbnZpcm9ubWVudCI6Ilhjb2RlIiwiYXV0b1JlbmV3UHJvZHVjdElkIjoicGFzcy5wcmVtaXVtIn0.WnT3aB9Lwjbr0ICUGn_5CdglzedVd7eOkrqirhcWFvwJZzN1FajuMV6gFEbgD82aL0Ix6HGZcwkNDlVNLvYOEQ -------------------------------------------------------------------------------- /tests/resources/xcode/xcode-signed-app-transaction: -------------------------------------------------------------------------------- 1 | eyJ4NWMiOlsiTUlJQnpEQ0NBWEdnQXdJQkFnSUJBVEFLQmdncWhrak9QUVFEQWpCSU1TSXdJQVlEVlFRREV4bFRkRzl5WlV0cGRDQlVaWE4wYVc1bklHbHVJRmhqYjJSbE1TSXdJQVlEVlFRS0V4bFRkRzl5WlV0cGRDQlVaWE4wYVc1bklHbHVJRmhqYjJSbE1CNFhEVEl6TVRBeE9UQXhORFV6TmxvWERUSTBNVEF4T0RBeE5EVXpObG93U0RFaU1DQUdBMVVFQXhNWlUzUnZjbVZMYVhRZ1ZHVnpkR2x1WnlCcGJpQllZMjlrWlRFaU1DQUdBMVVFQ2hNWlUzUnZjbVZMYVhRZ1ZHVnpkR2x1WnlCcGJpQllZMjlrWlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQktYRVFnWWpDb3VQdFRzdEdyS3BZOEk1M25IN3JiREhuY0lMR25vZ1NBdWxJSTNzXC91Zk0wZzlEYzNCY3I0OTdBVWd6R1R2V3Bpd0p4cGVCMzcxTmdWK2pUREJLTUJJR0ExVWRFd0VCXC93UUlNQVlCQWY4Q0FRQXdKQVlEVlIwUkJCMHdHNEVaVTNSdmNtVkxhWFFnVkdWemRHbHVaeUJwYmlCWVkyOWtaVEFPQmdOVkhROEJBZjhFQkFNQ0I0QXdDZ1lJS29aSXpqMEVBd0lEU1FBd1JnSWhBTVp2VllKNjRDRitoMmZtc213dnpBY2VQcklEMTNycElKR0JFVytXZ3BwdEFpRUF4V2l5NCtUMXp0MzdWc3UwdmI2WXVtMCtOTHREcUhsSzZycE1jdjZKZm5BPSJdLCJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkFwcGxlX1hjb2RlX0tleSJ9.eyJidW5kbGVJZCI6ImNvbS5leGFtcGxlLm5hdHVyZWxhYi5iYWNreWFyZGJpcmRzLmV4YW1wbGUiLCJhcHBsaWNhdGlvblZlcnNpb24iOiIxIiwiZGV2aWNlVmVyaWZpY2F0aW9uTm9uY2UiOiI0OGM4YjkyZC1jZTBkLTQyMjktYmVkZi1lNjFiNGY5Y2ZjOTIiLCJyZWNlaXB0VHlwZSI6Ilhjb2RlIiwicmVjZWlwdENyZWF0aW9uRGF0ZSI6MTY5NzY4MDEyMjI1Ny40NDcsImRldmljZVZlcmlmaWNhdGlvbiI6ImNZVXNYYzUzRWJZYzBwT2VYRzVkNlwvMzFMR0hlVkdmODRzcVNOME9ySmk1dVwvajJIODlXV0tnUzhOMGhNc01sZiIsInJlcXVlc3REYXRlIjoxNjk3NjgwMTIyMjU3LjQ0Nywib3JpZ2luYWxBcHBsaWNhdGlvblZlcnNpb24iOiIxIiwib3JpZ2luYWxQdXJjaGFzZURhdGUiOi02MjEzNTc2OTYwMDAwMH0.Dpdk_VsO2MUCevwyS407alJpPc1Nq_UIP9EiDHaQBxlyi35NFnsKUVNuFNcGWrGRCCImnb4QGBKHfQC2i4sPCg -------------------------------------------------------------------------------- /models/GetImageListResponse.ts: -------------------------------------------------------------------------------- 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 { 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 | } 37 | -------------------------------------------------------------------------------- /models/GetMessageListResponse.ts: -------------------------------------------------------------------------------- 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 { 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 | } 37 | -------------------------------------------------------------------------------- /models/GetImageListResponseItem.ts: -------------------------------------------------------------------------------- 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 { 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 | } 40 | -------------------------------------------------------------------------------- /models/PromotionalOfferSignatureV1.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | /** 4 | * The promotional offer signature you generate using an earlier signature version. 5 | * 6 | * {@link https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev1 promotionalOfferSignatureV1} 7 | */ 8 | export interface PromotionalOfferSignatureV1 { 9 | 10 | /** 11 | * The Base64-encoded cryptographic signature you generate using the offer parameters. 12 | **/ 13 | encodedSignature: string 14 | 15 | /** 16 | * The subscription's product identifier. 17 | * 18 | * {@link https://developer.apple.com/documentation/retentionmessaging/productid productId} 19 | **/ 20 | productId: string 21 | 22 | /** 23 | * A one-time-use UUID antireplay value you generate. 24 | * 25 | * **Note: Use lowercase.** 26 | **/ 27 | nonce: string 28 | 29 | /** 30 | * The UNIX time, in milliseconds, when you generate the signature. 31 | **/ 32 | timestamp: number 33 | 34 | /** 35 | * A string that identifies the private key you use to generate the signature. 36 | * 37 | * @return keyId 38 | **/ 39 | keyId: string 40 | 41 | /** 42 | * The subscription offer identifier that you set up in App Store Connect. 43 | **/ 44 | offerIdentifier: string 45 | 46 | /** 47 | * A UUID that you provide to associate with the transaction if the customer accepts the promotional offer. 48 | * 49 | * **Note: Use lowercase.** 50 | **/ 51 | appAccountToken?: string 52 | } 53 | -------------------------------------------------------------------------------- /models/GetMessageListResponseItem.ts: -------------------------------------------------------------------------------- 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 { 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 | } 40 | -------------------------------------------------------------------------------- /models/SubscriptionGroupIdentifierItem.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { LastTransactionsItem } from "./LastTransactionsItem"; 4 | import { Validator } from "./Validator"; 5 | 6 | /** 7 | * Information for auto-renewable subscriptions, including signed transaction information and signed renewal information, for one subscription group. 8 | * 9 | * {@link https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifieritem SubscriptionGroupIdentifierItem} 10 | */ 11 | export interface SubscriptionGroupIdentifierItem { 12 | /** 13 | * The identifier of the subscription group that the subscription belongs to. 14 | * 15 | * {@link https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifier subscriptionGroupIdentifier} 16 | **/ 17 | subscriptionGroupIdentifier?: string 18 | 19 | /** 20 | * An array of the most recent App Store-signed transaction information and App Store-signed renewal information for all auto-renewable subscriptions in the subscription group. 21 | * 22 | * {@link https://developer.apple.com/documentation/appstoreserverapi/lasttransactionsitem lastTransactionsItem} 23 | **/ 24 | lastTransactions?: LastTransactionsItem[] 25 | } 26 | 27 | 28 | export class SubscriptionGroupIdentifierItemValidator implements Validator { 29 | validate(obj: any): obj is SubscriptionGroupIdentifierItem { 30 | if ((typeof obj['subscriptionGroupIdentifier'] !== 'undefined') && !(typeof obj['subscriptionGroupIdentifier'] === "string" || obj['subscriptionGroupIdentifier'] instanceof String)) { 31 | return false 32 | } 33 | return true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /models/SendAttemptItem.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { SendAttemptResult, SendAttemptResultValidator } from "./SendAttemptResult"; 4 | import { Validator } from "./Validator" 5 | 6 | /** 7 | * The success or error information and the date the App Store server records when it attempts to send a server notification to your server. 8 | * 9 | * {@link https://developer.apple.com/documentation/appstoreserverapi/sendattemptitem sendAttemptItem} 10 | */ 11 | export interface SendAttemptItem { 12 | 13 | /** 14 | * The date the App Store server attempts to send a notification. 15 | * 16 | * {@link https://developer.apple.com/documentation/appstoreserverapi/attemptdate attemptDate} 17 | **/ 18 | attemptDate?: number 19 | 20 | /** 21 | * The success or error information the App Store server records when it attempts to send an App Store server notification to your server. 22 | * 23 | * {@link https://developer.apple.com/documentation/appstoreserverapi/sendattemptresult sendAttemptResult} 24 | **/ 25 | sendAttemptResult?: SendAttemptResult | string 26 | } 27 | 28 | export class SendAttemptItemValidator implements Validator { 29 | static readonly sendAttemptResultValidator = new SendAttemptResultValidator() 30 | validate(obj: any): obj is SendAttemptItem { 31 | if ((typeof obj['attemptDate'] !== 'undefined') && !(typeof obj['attemptDate'] === "number")) { 32 | return false 33 | } 34 | if ((typeof obj['sendAttemptResult'] !== 'undefined') && !SendAttemptItemValidator.sendAttemptResultValidator.validate(obj['sendAttemptResult'])) { 35 | return false 36 | } 37 | return true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing! 4 | 5 | ## Reporting Bugs 6 | 7 | Please report bugs by creating [Github issues](https://docs.github.com/en/issues/tracking-your-work-with-issues/about-issues). 8 | To help the community understand the bug and get it fixed faster, please provide the following information when creating a new issue: 9 | - A clear and descriptive title 10 | - The exact steps to reproduce the bug 11 | - The observed behavior and expected behavior 12 | 13 | If possible, also include payloads, commands, screenshots, etc to help the community identify the problem. Do not include any personal or sensitive data. 14 | 15 | ## Suggesting Improvements 16 | 17 | You can suggest improvements also by creating Github issues. 18 | When creating a new suggestion, please provide the following information: 19 | - A clear and descriptive title 20 | - A description of the proposed improvement in as many details as possible 21 | - Explain why the improvement is important 22 | 23 | ## Documentation Contribution 24 | 25 | Documentation contribution will make it easier for the community to work on the project. 26 | You may add README/diagrams to the components, or improve the existing docs. For major doc changes, we encourage you to create issues before contributing. Let us know what you are planning to change before the contribution. 27 | 28 | ## Code Contribution 29 | 30 | For minor changes (like small bug fixes or typo correction), feel free to open up a PR directly. 31 | For new features or major changes, we encourage you to create a Github issue first, and get agreement before starting on the implementation. This is to save you time in case there's duplicate effort or unforeseen risk. 32 | 33 | ## Project Licensing 34 | 35 | All contributions (including Pull Requests) to this project are provided under the terms of the project’s [LICENSE](LICENSE.txt) -------------------------------------------------------------------------------- /tests/util.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import * as fs from 'fs'; 4 | import { Environment } from '../models/Environment'; 5 | import { SignedDataVerifier } from '../jws_verification'; 6 | import { ECKeyPairOptions, generateKeyPairSync } from 'crypto'; 7 | import jsonwebtoken = require('jsonwebtoken'); 8 | 9 | export function readFile(path: string): string { 10 | return fs.readFileSync(path, { 11 | encoding: 'utf8' 12 | }) 13 | } 14 | 15 | export function readBytes(path: string): Buffer { 16 | return fs.readFileSync(path) 17 | } 18 | 19 | export function getSignedPayloadVerifier(environment: Environment, bundleId: string, appAppleId: number): SignedDataVerifier { 20 | return new SignedDataVerifier([readBytes('tests/resources/certs/testCA.der')], false, environment, bundleId, appAppleId) 21 | } 22 | 23 | export function getSignedPayloadVerifierWithDefaultAppAppleId(environment: Environment, bundleId: string): SignedDataVerifier { 24 | return getSignedPayloadVerifier(environment, bundleId, 1234) 25 | } 26 | 27 | export function getDefaultSignedPayloadVerifier(): SignedDataVerifier { 28 | return getSignedPayloadVerifierWithDefaultAppAppleId(Environment.LOCAL_TESTING, "com.example") 29 | } 30 | 31 | export function createSignedDataFromJson(path: string): string { 32 | const fileContents = readFile(path) 33 | const keyPairOptions: ECKeyPairOptions<'pem', 'pem'> = { 34 | namedCurve: 'prime256v1', 35 | publicKeyEncoding: { 36 | type: 'spki', 37 | format: 'pem' 38 | }, 39 | privateKeyEncoding: { 40 | type: 'pkcs8', 41 | format: 'pem' 42 | } 43 | } 44 | const keypair = generateKeyPairSync("ec", keyPairOptions) 45 | const privateKey = keypair.privateKey 46 | return jsonwebtoken.sign(fileContents, privateKey, { algorithm: 'ES256'}); 47 | } -------------------------------------------------------------------------------- /tests/resources/xcode/xcode-signed-transaction: -------------------------------------------------------------------------------- 1 | eyJraWQiOiJBcHBsZV9YY29kZV9LZXkiLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlCekRDQ0FYR2dBd0lCQWdJQkFUQUtCZ2dxaGtqT1BRUURBakJJTVNJd0lBWURWUVFERXhsVGRHOXlaVXRwZENCVVpYTjBhVzVuSUdsdUlGaGpiMlJsTVNJd0lBWURWUVFLRXhsVGRHOXlaVXRwZENCVVpYTjBhVzVuSUdsdUlGaGpiMlJsTUI0WERUSXpNVEF4T1RBeE5EVXpObG9YRFRJME1UQXhPREF4TkRVek5sb3dTREVpTUNBR0ExVUVBeE1aVTNSdmNtVkxhWFFnVkdWemRHbHVaeUJwYmlCWVkyOWtaVEVpTUNBR0ExVUVDaE1aVTNSdmNtVkxhWFFnVkdWemRHbHVaeUJwYmlCWVkyOWtaVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCS1hFUWdZakNvdVB0VHN0R3JLcFk4STUzbkg3cmJESG5jSUxHbm9nU0F1bElJM3NcL3VmTTBnOURjM0JjcjQ5N0FVZ3pHVHZXcGl3SnhwZUIzNzFOZ1YralREQktNQklHQTFVZEV3RUJcL3dRSU1BWUJBZjhDQVFBd0pBWURWUjBSQkIwd0c0RVpVM1J2Y21WTGFYUWdWR1Z6ZEdsdVp5QnBiaUJZWTI5a1pUQU9CZ05WSFE4QkFmOEVCQU1DQjRBd0NnWUlLb1pJemowRUF3SURTUUF3UmdJaEFNWnZWWUo2NENGK2gyZm1zbXd2ekFjZVBySUQxM3JwSUpHQkVXK1dncHB0QWlFQXhXaXk0K1QxenQzN1ZzdTB2YjZZdW0wK05MdERxSGxLNnJwTWN2NkpmbkE9Il19.eyJpbkFwcE93bmVyc2hpcFR5cGUiOiJQVVJDSEFTRUQiLCJwdXJjaGFzZURhdGUiOjE2OTc2Nzk5MzYwNDkuNzI5Nywic3Vic2NyaXB0aW9uR3JvdXBJZGVudGlmaWVyIjoiNkYzQTkzQUIiLCJzaWduZWREYXRlIjoxNjk3Njc5OTM2MDU2LjQ4NSwib3JpZ2luYWxQdXJjaGFzZURhdGUiOjE2OTc2Nzk5MzYwNDkuNzI5NywiaXNVcGdyYWRlZCI6ZmFsc2UsImRldmljZVZlcmlmaWNhdGlvbiI6InNHRG5wZytvemI4dXdEU3VDRFoyb1ZabzFDS3JiQjh1alI4VnhDeGh5a1J3eUJJSzZ4NlhDeUVSbTh5V3J6RTgiLCJvZmZlclR5cGUiOjEsInF1YW50aXR5IjoxLCJ0cmFuc2FjdGlvbklkIjoiMCIsInR5cGUiOiJBdXRvLVJlbmV3YWJsZSBTdWJzY3JpcHRpb24iLCJ0cmFuc2FjdGlvblJlYXNvbiI6IlBVUkNIQVNFIiwicHJvZHVjdElkIjoicGFzcy5wcmVtaXVtIiwiZXhwaXJlc0RhdGUiOjE3MDAzNTgzMzYwNDkuNzI5NywiZW52aXJvbm1lbnQiOiJYY29kZSIsInN0b3JlZnJvbnRJZCI6IjE0MzQ0MSIsIm9yaWdpbmFsVHJhbnNhY3Rpb25JZCI6IjAiLCJidW5kbGVJZCI6ImNvbS5leGFtcGxlLm5hdHVyZWxhYi5iYWNreWFyZGJpcmRzLmV4YW1wbGUiLCJkZXZpY2VWZXJpZmljYXRpb25Ob25jZSI6IjdlZGVhODdkLTk4ZjAtNDJkMC05NjgyLTQ5Y2E4MTAyMmY3MyIsIndlYk9yZGVyTGluZUl0ZW1JZCI6IjAiLCJzdG9yZWZyb250IjoiVVNBIn0.rkJYnvujStteRkMHhoIR2ThmNFnyKcx5XxIakXYdh-1oKtEVEU5zQAiONaLDpBDO5JhLLrTbfp7LS5tMiqmgHw -------------------------------------------------------------------------------- /models/MassExtendRenewalDateRequest.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { ExtendReasonCode, ExtendReasonCodeValidator } from "./ExtendReasonCode"; 4 | 5 | /** 6 | * The request body that contains subscription-renewal-extension data to apply for all eligible active subscribers. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/massextendrenewaldaterequest MassExtendRenewalDateRequest} 9 | */ 10 | export interface MassExtendRenewalDateRequest { 11 | /** 12 | * The number of days to extend the subscription renewal date. 13 | * 14 | * {@link https://developer.apple.com/documentation/appstoreserverapi/extendbydays extendByDays} 15 | * maximum: 90 16 | **/ 17 | extendByDays?: number 18 | 19 | /** 20 | * The reason code for the subscription-renewal-date extension. 21 | * 22 | * {@link https://developer.apple.com/documentation/appstoreserverapi/extendreasoncode extendReasonCode} 23 | **/ 24 | extendReasonCode?: ExtendReasonCode 25 | 26 | /** 27 | * A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. 28 | * 29 | * {@link https://developer.apple.com/documentation/appstoreserverapi/requestidentifier requestIdentifier} 30 | **/ 31 | requestIdentifier?: string 32 | 33 | /** 34 | * A list of storefront country codes you provide to limit the storefronts for a subscription-renewal-date extension. 35 | * 36 | * {@link https://developer.apple.com/documentation/appstoreserverapi/storefrontcountrycodes storefrontCountryCodes} 37 | **/ 38 | storefrontCountryCodes?: string[]; 39 | 40 | /** 41 | * The unique identifier for the product, that you create in App Store Connect. 42 | * 43 | * {@link https://developer.apple.com/documentation/appstoreserverapi/productid productId} 44 | **/ 45 | productId?: string 46 | } -------------------------------------------------------------------------------- /models/OrderLookupResponse.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { OrderLookupStatus, OrderLookupStatusValidator } from "./OrderLookupStatus"; 4 | import { Validator } from "./Validator"; 5 | 6 | /** 7 | * A response that includes the order lookup status and an array of signed transactions for the in-app purchases in the order. 8 | * 9 | * {@link https://developer.apple.com/documentation/appstoreserverapi/orderlookupresponse OrderLookupResponse} 10 | */ 11 | export interface OrderLookupResponse { 12 | /** 13 | * The status that indicates whether the order ID is valid. 14 | * 15 | * {@link https://developer.apple.com/documentation/appstoreserverapi/orderlookupstatus OrderLookupStatus} 16 | **/ 17 | status?: OrderLookupStatus | number 18 | 19 | /** 20 | * An array of in-app purchase transactions that are part of order, signed by Apple, in JSON Web Signature format. 21 | * 22 | * {@link https://developer.apple.com/documentation/appstoreserverapi/jwstransaction JWSTransaction} 23 | **/ 24 | signedTransactions?: string[]; 25 | } 26 | 27 | 28 | export class OrderLookupResponseValidator implements Validator { 29 | static readonly statusValidator = new OrderLookupStatusValidator() 30 | validate(obj: any): obj is OrderLookupResponse { 31 | if ((typeof obj['status'] !== 'undefined') && !(OrderLookupResponseValidator.statusValidator.validate(obj['status']))) { 32 | return false 33 | } 34 | if (typeof obj['signedTransactions'] !== 'undefined') { 35 | if (!Array.isArray(obj['signedTransactions'])) { 36 | return false 37 | } 38 | for (const signedTransaction of obj['signedTransactions']) { 39 | if (!(typeof signedTransaction === "string" || signedTransaction instanceof String)) { 40 | return false 41 | } 42 | } 43 | } 44 | return true 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /promotional_offer.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { KeyObject, createPrivateKey, createSign } from "crypto"; 4 | 5 | export class PromotionalOfferSignatureCreator { 6 | 7 | private signingKey: KeyObject; 8 | private keyId: string; 9 | private bundleId: string; 10 | 11 | public constructor(signingKey: string, keyId: string, bundleId: string) { 12 | this.signingKey = createPrivateKey(signingKey); 13 | this.keyId = keyId 14 | this.bundleId = bundleId 15 | } 16 | 17 | /** 18 | * Create a promotional offer signature 19 | * 20 | * {@link https://developer.apple.com/documentation/storekit/in-app_purchase/original_api_for_in-app_purchase/subscriptions_and_offers/generating_a_signature_for_promotional_offers Generating a signature for promotional offers} 21 | * @param productIdentifier The subscription product identifier 22 | * @param subscriptionOfferID The subscription discount identifier 23 | * @param appAccountToken An optional string value that you define; may be an empty string 24 | * @param nonce A one-time UUID value that your server generates. Generate a new nonce for every signature. 25 | * @param timestamp A timestamp your server generates in UNIX time format, in milliseconds. The timestamp keeps the offer active for 24 hours. 26 | * @return The Base64 encoded signature 27 | */ 28 | public createSignature(productIdentifier: string, subscriptionOfferID: string, appAccountToken: string, nonce: string, timestamp: number): string { 29 | const payload = this.bundleId + '\u2063' + 30 | this.keyId + '\u2063' + 31 | productIdentifier + '\u2063' + 32 | subscriptionOfferID + '\u2063' + 33 | appAccountToken.toLowerCase() + '\u2063'+ 34 | nonce.toLowerCase() + '\u2063' + 35 | timestamp; 36 | const sign = createSign('SHA256') 37 | sign.update(payload) 38 | sign.end() 39 | return sign.sign(this.signingKey).toString('base64') 40 | } 41 | } -------------------------------------------------------------------------------- /tests/resources/xcode/xcode-app-receipt-empty: -------------------------------------------------------------------------------- 1 | MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwGggCSABIHhMYHeMA8CAQACAQEEBwwFWGNvZGUwCwIBAQIBAQQDAgEAMDUCAQICAQEELQwrY29tLmV4YW1wbGUubmF0dXJlbGFiLmJhY2t5YXJkYmlyZHMuZXhhbXBsZTALAgEDAgEBBAMMATEwEAIBBAIBAQQI0bz+zwQAAAAwHAIBBQIBAQQU4nEwK24WxZhKi0PSGTYgWoXOIqMwCgIBCAIBAQQCFgAwHgIBDAIBAQQWFhQyMDIzLTEwLTE5VDAxOjE4OjU0WjAeAgEVAgEBBBYWFDQwMDEtMDEtMDFUMDA6MDA6MDBaAAAAAAAAoIIDeDCCA3QwggJcoAMCAQICAQEwDQYJKoZIhvcNAQELBQAwXzERMA8GA1UEAwwIU3RvcmVLaXQxETAPBgNVBAoMCFN0b3JlS2l0MREwDwYDVQQLDAhTdG9yZUtpdDELMAkGA1UEBhMCVVMxFzAVBgkqhkiG9w0BCQEWCFN0b3JlS2l0MB4XDTIwMDQwMTE3NTIzNVoXDTQwMDMyNzE3NTIzNVowXzERMA8GA1UEAwwIU3RvcmVLaXQxETAPBgNVBAoMCFN0b3JlS2l0MREwDwYDVQQLDAhTdG9yZUtpdDELMAkGA1UEBhMCVVMxFzAVBgkqhkiG9w0BCQEWCFN0b3JlS2l0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA23+QPCxzD9uXJkuTuwr4oSE+yGHZJMheH3U+2pPbMRqRgLm/5QzLPLsORGIm+gQptknnb+Ab5g1ozSVuw3YI9UoLrnp0PMSpC7PPYg/7tLz324ReKOtHDfHti6z1n7AJOKNue8smUAoa4YnRcnYLOUzLT27As1+3lbq5qF1KdKvvb0GlfgmNuj09zXBX2O3v1dp3yJMEHO8JiHhlzoHyjXLnBxpuJhL3MrENuziQawbE/A3llVDNkci6JfRYyYzhcdtKRfMtGZYDVoGmRO51d1tTz3isXbo+X1ArXCmM3cLXKhffIrTX5Hior6htp8HaaC1mzM8pC1As48L75l8SwQIDAQABozswOTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIChDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEAsgDgPPHo6WK9wNYdQJ5XuTiQd3ZS0qhLcG64Z5n7s4pVn+8dKLhfKtFznzVHN7tG03YQ8vBp7M1imXH5YIqESDjEvYtnJbmrbDNlrdjCmnhID+nMwScNxs9kPG2AWTOMyjYGKhEbjUnOCP9mwEcoS+tawSsJViylqgkDezIx3OiFeEjOwMUSEWoPDK4vBcpvemR/ICx15kyxEtP94x9eDX24WNegfOR/Y6uXmivDKtjQsuHVWg05G29nKKkSg9aHeG2ZvV6zCuCYzvbqw45taeu3QIE9hz1wUdHEXY2l3H9qWBreYHY3Uuz/rBldDBUvig/1icjXKx0e7CuRBac9TzGCAY8wggGLAgEBMGQwXzERMA8GA1UEAwwIU3RvcmVLaXQxETAPBgNVBAoMCFN0b3JlS2l0MREwDwYDVQQLDAhTdG9yZUtpdDELMAkGA1UEBhMCVVMxFzAVBgkqhkiG9w0BCQEWCFN0b3JlS2l0AgEBMA0GCWCGSAFlAwQCAQUAMA0GCSqGSIb3DQEBCwUABIIBAIjP3bmY+TrOM0e8n7PeH3OEies1+spNT1n8om4424n/NyIJ9XRyj1QGxshxh6p2BQuUQV8mkWKpHYQJqPobVEcl72ndbHSfzkH2vM57jy/2bCopLt+zWQl0QMA9iKEB3G075wgyD6lcSveZnER/4J6E9+tO6O3R2YFVziwL2UmNR1XgfOhKyNwCfSV1CyVVoSUkkZI7fJ1S6Pce2nLKM1pf+oCWr5vAySd9E4givt/YagGJF+3RHZMEcrqHnnP8kQKi99xnXcIfYyK6VMD9uBb2+4N7MCRDhoY/8+vX9I75paW0UicS6MwacJPueNxLaAboOP4nFSlYhEhZuLiZrdIAAAAAAAA= -------------------------------------------------------------------------------- /models/CheckTestNotificationResponse.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { SendAttemptItem, SendAttemptItemValidator } from "./SendAttemptItem"; 4 | import { Validator } from "./Validator"; 5 | 6 | /** 7 | * A response that contains the contents of the test notification sent by the App Store server and the result from your server. 8 | * 9 | * {@link https://developer.apple.com/documentation/appstoreserverapi/checktestnotificationresponse CheckTestNotificationResponse} 10 | */ 11 | export interface CheckTestNotificationResponse { 12 | 13 | /** 14 | * A cryptographically signed payload, in JSON Web Signature (JWS) format, containing the response body for a version 2 notification. 15 | * 16 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/signedpayload signedPayload} 17 | **/ 18 | signedPayload?: string 19 | 20 | /** 21 | * An array of information the App Store server records for its attempts to send the TEST notification to your server. The array may contain a maximum of six sendAttemptItem objects. 22 | * 23 | * {@link https://developer.apple.com/documentation/appstoreserverapi/sendattemptitem sendAttemptItem} 24 | **/ 25 | sendAttempts?: SendAttemptItem[] 26 | } 27 | 28 | export class CheckTestNotificationResponseValidator implements Validator { 29 | static readonly sendAttemptItemValidator = new SendAttemptItemValidator() 30 | validate(obj: any): obj is CheckTestNotificationResponse { 31 | if ((typeof obj['signedPayload'] !== 'undefined') && !(typeof obj['signedPayload'] === "string" || obj['signedPayload'] instanceof String)) { 32 | return false 33 | } 34 | if (typeof obj['sendAttempts'] !== 'undefined') { 35 | if (!Array.isArray(obj['sendAttempts'])) { 36 | return false 37 | } 38 | for (const sendAttempt of obj['sendAttempts']) { 39 | if (!(CheckTestNotificationResponseValidator.sendAttemptItemValidator.validate(sendAttempt))) { 40 | return false 41 | } 42 | } 43 | } 44 | return true 45 | } 46 | } -------------------------------------------------------------------------------- /models/NotificationHistoryResponseItem.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { SendAttemptItem, SendAttemptItemValidator } from "./SendAttemptItem" 4 | import { Validator } from "./Validator" 5 | 6 | /** 7 | * The App Store server notification history record, including the signed notification payload and the result of the server’s first send attempt. 8 | * 9 | * {@link https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryresponseitem notificationHistoryResponseItem} 10 | */ 11 | export interface NotificationHistoryResponseItem { 12 | 13 | /** 14 | * A cryptographically signed payload, in JSON Web Signature (JWS) format, containing the response body for a version 2 notification. 15 | * 16 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/signedpayload signedPayload} 17 | **/ 18 | signedPayload?: string 19 | 20 | /** 21 | * An array of information the App Store server records for its attempts to send a notification to your server. The maximum number of entries in the array is six. 22 | * 23 | * {@link https://developer.apple.com/documentation/appstoreserverapi/sendattemptitem sendAttemptItem} 24 | **/ 25 | sendAttempts?: SendAttemptItem[] 26 | } 27 | 28 | export class NotificationHistoryResponseItemValidator implements Validator { 29 | static readonly sendAttemptItemValidator = new SendAttemptItemValidator() 30 | validate(obj: any): obj is NotificationHistoryResponseItem { 31 | if ((typeof obj['signedPayload'] !== 'undefined') && !(typeof obj['signedPayload'] === "string" || obj['signedPayload'] instanceof String)) { 32 | return false 33 | } 34 | if (typeof obj['sendAttempts'] !== 'undefined') { 35 | if (!Array.isArray(obj['sendAttempts'])) { 36 | return false 37 | } 38 | for (const sendAttempt of obj['sendAttempts']) { 39 | if (!(NotificationHistoryResponseItemValidator.sendAttemptItemValidator.validate(sendAttempt))) { 40 | return false 41 | } 42 | } 43 | } 44 | return true 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/resources/xcode/xcode-app-receipt-with-transaction: -------------------------------------------------------------------------------- 1 | MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwGggCSABIIBdjGCAXIwDwIBAAIBAQQHDAVYY29kZTALAgEBAgEBBAMCAQAwNQIBAgIBAQQtDCtjb20uZXhhbXBsZS5uYXR1cmVsYWIuYmFja3lhcmRiaXJkcy5leGFtcGxlMAsCAQMCAQEEAwwBMTAQAgEEAgEBBAjyv/X7DwAAADAcAgEFAgEBBBQWU6vLoHZxeVVlaOg/UEG2OOKahTAKAgEIAgEBBAIWADAeAgEMAgEBBBYWFDIwMjMtMTAtMTlUMDE6NDU6NDBaMIGRAgERAgEBBIGIMYGFMAwCAgalAgEBBAMCAQEwFwICBqYCAQEEDgwMcGFzcy5wcmVtaXVtMAwCAganAgEBBAMMATAwHwICBqgCAQEEFhYUMjAyMy0xMC0xOVQwMTo0NTozNlowHwICBqwCAQEEFhYUMjAyMy0xMS0xOVQwMTo0NTozNlowDAICBrcCAQEEAwIBATAeAgEVAgEBBBYWFDQwMDEtMDEtMDFUMDA6MDA6MDBaAAAAAAAAoIIDeDCCA3QwggJcoAMCAQICAQEwDQYJKoZIhvcNAQELBQAwXzERMA8GA1UEAwwIU3RvcmVLaXQxETAPBgNVBAoMCFN0b3JlS2l0MREwDwYDVQQLDAhTdG9yZUtpdDELMAkGA1UEBhMCVVMxFzAVBgkqhkiG9w0BCQEWCFN0b3JlS2l0MB4XDTIwMDQwMTE3NTIzNVoXDTQwMDMyNzE3NTIzNVowXzERMA8GA1UEAwwIU3RvcmVLaXQxETAPBgNVBAoMCFN0b3JlS2l0MREwDwYDVQQLDAhTdG9yZUtpdDELMAkGA1UEBhMCVVMxFzAVBgkqhkiG9w0BCQEWCFN0b3JlS2l0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA23+QPCxzD9uXJkuTuwr4oSE+yGHZJMheH3U+2pPbMRqRgLm/5QzLPLsORGIm+gQptknnb+Ab5g1ozSVuw3YI9UoLrnp0PMSpC7PPYg/7tLz324ReKOtHDfHti6z1n7AJOKNue8smUAoa4YnRcnYLOUzLT27As1+3lbq5qF1KdKvvb0GlfgmNuj09zXBX2O3v1dp3yJMEHO8JiHhlzoHyjXLnBxpuJhL3MrENuziQawbE/A3llVDNkci6JfRYyYzhcdtKRfMtGZYDVoGmRO51d1tTz3isXbo+X1ArXCmM3cLXKhffIrTX5Hior6htp8HaaC1mzM8pC1As48L75l8SwQIDAQABozswOTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIChDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEAsgDgPPHo6WK9wNYdQJ5XuTiQd3ZS0qhLcG64Z5n7s4pVn+8dKLhfKtFznzVHN7tG03YQ8vBp7M1imXH5YIqESDjEvYtnJbmrbDNlrdjCmnhID+nMwScNxs9kPG2AWTOMyjYGKhEbjUnOCP9mwEcoS+tawSsJViylqgkDezIx3OiFeEjOwMUSEWoPDK4vBcpvemR/ICx15kyxEtP94x9eDX24WNegfOR/Y6uXmivDKtjQsuHVWg05G29nKKkSg9aHeG2ZvV6zCuCYzvbqw45taeu3QIE9hz1wUdHEXY2l3H9qWBreYHY3Uuz/rBldDBUvig/1icjXKx0e7CuRBac9TzGCAY8wggGLAgEBMGQwXzERMA8GA1UEAwwIU3RvcmVLaXQxETAPBgNVBAoMCFN0b3JlS2l0MREwDwYDVQQLDAhTdG9yZUtpdDELMAkGA1UEBhMCVVMxFzAVBgkqhkiG9w0BCQEWCFN0b3JlS2l0AgEBMA0GCWCGSAFlAwQCAQUAMA0GCSqGSIb3DQEBCwUABIIBAMNY9TpOCg59NnKdDA6Xc4D74lEaa+YwQqD/z8ajAGxpw3efoQRvx8Q1qR6IVs9BcRYGyJmsFrau19QeSIRjjqaxhV8ZbRFenWp0Yps6OCPVHw94Ej3AstAL/8WIArBM1OS6OZJESJdQz5xpwavWLGm1rU2730glMdHzHfm2h0wNp/0BKV0ugV9SRQN4RsyAMNS+rCO1mtSDI6nx8E+dEVMIa4mUg+yhXRlg6KzdzKWnr9vDtRVmhdq0ANfP+jfvncsyC+d/c3cAsXOK066hKFwYWTKaRZ7M2eXus5TcU83/aaovHyKVyKKCRnKuP7VPt9d5eWLSg/7v2ctHJtjmhqsAAAAAAAA= -------------------------------------------------------------------------------- /tests/resources/mock_signed_data/transactionInfo: -------------------------------------------------------------------------------- 1 | eyJ4NWMiOlsiTUlJQm9EQ0NBVWFnQXdJQkFnSUJDekFLQmdncWhrak9QUVFEQWpCTk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1SVXdFd1lEVlFRS0RBeEpiblJsY20xbFpHbGhkR1V3SGhjTk1qTXdNVEEwTVRZek56TXhXaGNOTXpJeE1qTXhNVFl6TnpNeFdqQkZNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVNNQkFHQTFVRUJ3d0pRM1Z3WlhKMGFXNXZNUTB3Q3dZRFZRUUtEQVJNWldGbU1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRTRyV0J4R21GYm5QSVBRSTB6c0JLekx4c2o4cEQydnFicjB5UElTVXgyV1F5eG1yTnFsOWZoSzhZRUV5WUZWNysrcDVpNFlVU1Ivbzl1UUlnQ1BJaHJLTWZNQjB3Q1FZRFZSMFRCQUl3QURBUUJnb3Foa2lHOTJOa0Jnc0JCQUlUQURBS0JnZ3Foa2pPUFFRREFnTklBREJGQWlFQWtpRVprb0ZNa2o0Z1huK1E5alhRWk1qWjJnbmpaM2FNOE5ZcmdmVFVpdlFDSURKWVowRmFMZTduU0lVMkxXTFRrNXRYVENjNEU4R0pTWWYvc1lSeEVGaWUiLCJNSUlCbHpDQ0FUMmdBd0lCQWdJQkJqQUtCZ2dxaGtqT1BRUURBakEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TkRFMk1qWXdNVm9YRFRNeU1USXpNVEUyTWpZd01Wb3dUVEVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekVWTUJNR0ExVUVDZ3dNU1c1MFpYSnRaV1JwWVhSbE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRUZRM2xYMnNxTjlHSXdBaWlNUURRQy9reW5TZ1g0N1J3dmlET3RNWFh2eUtkUWU2Q1BzUzNqbzJ1UkR1RXFBeFdlT2lDcmpsRFdzeXo1d3dkVTBndGFxTWxNQ013RHdZRFZSMFRCQWd3QmdFQi93SUJBREFRQmdvcWhraUc5Mk5rQmdJQkJBSVRBREFLQmdncWhrak9QUVFEQWdOSUFEQkZBaUVBdm56TWNWMjY4Y1JiMS9GcHlWMUVoVDNXRnZPenJCVVdQNi9Ub1RoRmF2TUNJRmJhNXQ2WUt5MFIySkR0eHF0T2pKeTY2bDZWN2QvUHJBRE5wa21JUFcraSIsIk1JSUJYRENDQVFJQ0NRQ2ZqVFVHTERuUjlqQUtCZ2dxaGtqT1BRUURBekEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TkRFMk1qQXpNbG9YRFRNek1ERXdNVEUyTWpBek1sb3dOakVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCSFB2d1pmb0tMS2FPclgvV2U0cU9iWFNuYTVUZFdIVlo2aElSQTF3MG9jM1FDVDBJbzJwbHlEQjMvTVZsazJ0YzRLR0U4VGlxVzdpYlE2WmM5VjY0azB3Q2dZSUtvWkl6ajBFQXdNRFNBQXdSUUloQU1USGhXdGJBUU4waFN4SVhjUDRDS3JEQ0gvZ3N4V3B4NmpUWkxUZVorRlBBaUIzNW53azVxMHpjSXBlZnZZSjBNVS95R0dIU1dlejBicTBwRFlVTy9ubUR3PT0iXSwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJlbnZpcm9ubWVudCI6IlNhbmRib3giLCJidW5kbGVJZCI6ImNvbS5leGFtcGxlIiwic2lnbmVkRGF0ZSI6MTY3Mjk1NjE1NDAwMH0.PnHWpeIJZ8f2Q218NSGLo_aR0IBEJvC6PxmxKXh-qfYTrZccx2suGl223OSNAX78e4Ylf2yJCG2N-FfU-NIhZQ -------------------------------------------------------------------------------- /models/StatusResponse.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { Environment, EnvironmentValidator } from "./Environment"; 4 | import { SubscriptionGroupIdentifierItem } from "./SubscriptionGroupIdentifierItem"; 5 | import { Validator } from "./Validator"; 6 | 7 | /** 8 | * A response that contains status information for all of a customer’s auto-renewable subscriptions in your app. 9 | * 10 | * {@link https://developer.apple.com/documentation/appstoreserverapi/statusresponse StatusResponse} 11 | */ 12 | export interface StatusResponse { 13 | /** 14 | * The server environment, sandbox or production, in which the App Store generated the response. 15 | * 16 | * {@link https://developer.apple.com/documentation/appstoreserverapi/environment environment} 17 | **/ 18 | environment?: Environment | string 19 | 20 | /** 21 | * The bundle identifier of an app. 22 | * 23 | * {@link https://developer.apple.com/documentation/appstoreserverapi/bundleid bundleId} 24 | **/ 25 | bundleId?: string 26 | 27 | /** 28 | * The unique identifier of an app in the App Store. 29 | * 30 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/appappleid appAppleId} 31 | **/ 32 | appAppleId?: number 33 | 34 | /** 35 | * An array of information for auto-renewable subscriptions, including App Store-signed transaction information and App Store-signed renewal information. 36 | * 37 | **/ 38 | data?: SubscriptionGroupIdentifierItem[] 39 | } 40 | 41 | 42 | export class StatusResponseValidator implements Validator { 43 | static readonly environmentValidator = new EnvironmentValidator() 44 | validate(obj: any): obj is StatusResponse { 45 | if ((typeof obj['environment'] !== 'undefined') && !(StatusResponseValidator.environmentValidator.validate(obj['environment']))) { 46 | return false 47 | } 48 | if ((typeof obj['bundleId'] !== 'undefined') && !(typeof obj['bundleId'] === "string" || obj['bundleId'] instanceof String)) { 49 | return false 50 | } 51 | if ((typeof obj['appAppleId'] !== 'undefined') && !(typeof obj['appAppleId'] === "number")) { 52 | return false 53 | } 54 | return true 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/resources/mock_signed_data/renewalInfo: -------------------------------------------------------------------------------- 1 | eyJ4NWMiOlsiTUlJQm9EQ0NBVWFnQXdJQkFnSUJEREFLQmdncWhrak9QUVFEQXpCRk1Rc3dDUVlEVlFRR0V3SlZVekVMTUFrR0ExVUVDQXdDUTBFeEVqQVFCZ05WQkFjTUNVTjFjR1Z5ZEdsdWJ6RVZNQk1HQTFVRUNnd01TVzUwWlhKdFpXUnBZWFJsTUI0WERUSXpNREV3TlRJeE16RXpORm9YRFRNek1ERXdNVEl4TXpFek5Gb3dQVEVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh4RFRBTEJnTlZCQW9NQkV4bFlXWXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBVGl0WUhFYVlWdWM4ZzlBalRPd0VyTXZHeVB5a1BhK3B1dlRJOGhKVEhaWkRMR2FzMnFYMStFcnhnUVRKZ1ZYdjc2bm1MaGhSSkgrajI1QWlBSThpR3NveTh3TFRBSkJnTlZIUk1FQWpBQU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3TklBREJGQWlCWDRjK1QwRnA1bko1UVJDbFJmdTVQU0J5UnZOUHR1YVRzazB2UEIzV0FJQUloQU5nYWF1QWovWVA5czBBa0VoeUpoeFFPLzZRMnpvdVorSDFDSU9laG5NelEiLCJNSUlCbnpDQ0FVV2dBd0lCQWdJQkN6QUtCZ2dxaGtqT1BRUURBekEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TlRJeE16RXdOVm9YRFRNek1ERXdNVEl4TXpFd05Wb3dSVEVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh4RlRBVEJnTlZCQW9NREVsdWRHVnliV1ZrYVdGMFpUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJCVU41VjlyS2pmUmlNQUlvakVBMEF2NU1wMG9GK08wY0w0Z3pyVEYxNzhpblVIdWdqN0V0NDZOcmtRN2hLZ01WbmpvZ3E0NVExck1zK2NNSFZOSUxXcWpOVEF6TUE4R0ExVWRFd1FJTUFZQkFmOENBUUF3RGdZRFZSMFBBUUgvQkFRREFnRUdNQkFHQ2lxR1NJYjNZMlFHQWdFRUFnVUFNQW9HQ0NxR1NNNDlCQU1EQTBnQU1FVUNJUUNtc0lLWXM0MXVsbHNzSFg0clZ2ZVVUMFo3SXM1L2hMSzFsRlBUdHVuM2hBSWdjMisyUkc1K2dOY0ZWY3MrWEplRWw0R1orb2psM1JPT21sbCt5ZTdkeW5RPSIsIk1JSUJnakNDQVNtZ0F3SUJBZ0lKQUxVYzVBTGlINXBiTUFvR0NDcUdTTTQ5QkFNRE1EWXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSERBbERkWEJsY25ScGJtOHdIaGNOTWpNd01UQTFNakV6TURJeVdoY05Nek13TVRBeU1qRXpNREl5V2pBMk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWMrL0JsK2dvc3BvNnRmOVo3aW81dGRLZHJsTjFZZFZucUVoRURYRFNoemRBSlBRaWphbVhJTUhmOHhXV1RhMXpnb1lUeE9LcGJ1SnREcGx6MVhyaVRhTWdNQjR3REFZRFZSMFRCQVV3QXdFQi96QU9CZ05WSFE4QkFmOEVCQU1DQVFZd0NnWUlLb1pJemowRUF3TURSd0F3UkFJZ2VtV1FYbk1BZFRhZDJKREpXbmc5VTR1QkJMNW1BN1dJMDVIN29IN2M2aVFDSUhpUnFNak5melVBeWl1OWg2ck9VL0sraVRSMEkvM1kvTlNXc1hIWCthY2MiXSwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJlbnZpcm9ubWVudCI6IlNhbmRib3giLCJzaWduZWREYXRlIjoxNjcyOTU2MTU0MDAwfQ.FbK2OL-t6l4892W7fzWyus_g9mIl2CzWLbVt7Kgcnt6zzVulF8bzovgpe0v_y490blROGixy8KDoe2dSU53-Xw -------------------------------------------------------------------------------- /models/RefundHistoryResponse.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { Validator } from "./Validator"; 4 | 5 | /** 6 | * A response that contains an array of signed JSON Web Signature (JWS) refunded transactions, and paging information. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/refundhistoryresponse RefundHistoryResponse} 9 | */ 10 | export interface RefundHistoryResponse { 11 | 12 | /** 13 | * A list of up to 20 JWS transactions, or an empty array if the customer hasn't received any refunds in your app. The transactions are sorted in ascending order by revocationDate. 14 | * 15 | * {@link https://developer.apple.com/documentation/appstoreserverapi/jwstransaction JWSTransaction} 16 | **/ 17 | signedTransactions?: string[]; 18 | 19 | /** 20 | * A token you use in a query to request the next set of transactions for the customer. 21 | * 22 | * {@link https://developer.apple.com/documentation/appstoreserverapi/revision revision} 23 | **/ 24 | revision?: string 25 | 26 | /** 27 | * A Boolean value indicating whether the App Store has more transaction data. 28 | * 29 | * {@link https://developer.apple.com/documentation/appstoreserverapi/hasmore hasMore} 30 | **/ 31 | hasMore?: boolean 32 | } 33 | 34 | 35 | export class RefundHistoryResponseValidator implements Validator { 36 | validate(obj: any): obj is RefundHistoryResponse { 37 | if (typeof obj['signedTransactions'] !== 'undefined') { 38 | if (!Array.isArray(obj['signedTransactions'])) { 39 | return false 40 | } 41 | for (const signedTransaction of obj['signedTransactions']) { 42 | if (!(typeof signedTransaction === "string" || signedTransaction instanceof String)) { 43 | return false 44 | } 45 | } 46 | } 47 | if ((typeof obj['revision'] !== 'undefined') && !(typeof obj['revision'] === "string" || obj['revision'] instanceof String)) { 48 | return false 49 | } 50 | if ((typeof obj['hasMore'] !== 'undefined') && !(typeof obj['hasMore'] === "boolean" || obj['hasMore'] instanceof Boolean)) { 51 | return false 52 | } 53 | return true 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /models/ExternalPurchaseToken.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Apple Inc. Licensed under MIT License. 2 | 3 | import { Validator } from "./Validator" 4 | 5 | /** 6 | * The payload data that contains an external purchase token. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/externalpurchasetoken externalPurchaseToken} 9 | */ 10 | export interface ExternalPurchaseToken { 11 | 12 | /** 13 | * The field of an external purchase token that uniquely identifies the token. 14 | * 15 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/externalpurchaseid externalPurchaseId} 16 | **/ 17 | externalPurchaseId?: string 18 | 19 | /** 20 | * The field of an external purchase token that contains the UNIX date, in milliseconds, when the system created the token. 21 | * 22 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/tokencreationdate tokenCreationDate} 23 | **/ 24 | tokenCreationDate?: number 25 | 26 | /** 27 | * The unique identifier of an app in the App Store. 28 | * 29 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/appappleid appAppleId} 30 | **/ 31 | appAppleId?: number 32 | 33 | /** 34 | * The bundle identifier of an app. 35 | * 36 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/bundleid bundleId} 37 | **/ 38 | bundleId?: string 39 | } 40 | 41 | 42 | export class ExternalPurchaseTokenValidator implements Validator { 43 | validate(obj: any): obj is ExternalPurchaseToken { 44 | if ((typeof obj['externalPurchaseId'] !== 'undefined') && !(typeof obj['externalPurchaseId'] === "string" || obj['externalPurchaseId'] instanceof String)) { 45 | return false 46 | } 47 | if ((typeof obj['tokenCreationDate'] !== 'undefined') && !(typeof obj['tokenCreationDate'] === "number")) { 48 | return false 49 | } 50 | if ((typeof obj['appAppleId'] !== 'undefined') && !(typeof obj['appAppleId'] === "number")) { 51 | return false 52 | } 53 | if ((typeof obj['bundleId'] !== 'undefined') && !(typeof obj['bundleId'] === "string" || obj['bundleId'] instanceof String)) { 54 | return false 55 | } 56 | return true 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/resources/mock_signed_data/wrongBundleId: -------------------------------------------------------------------------------- 1 | eyJ4NWMiOlsiTUlJQm9EQ0NBVWFnQXdJQkFnSUJEREFLQmdncWhrak9QUVFEQXpCRk1Rc3dDUVlEVlFRR0V3SlZVekVMTUFrR0ExVUVDQXdDUTBFeEVqQVFCZ05WQkFjTUNVTjFjR1Z5ZEdsdWJ6RVZNQk1HQTFVRUNnd01TVzUwWlhKdFpXUnBZWFJsTUI0WERUSXpNREV3TlRJeE16RXpORm9YRFRNek1ERXdNVEl4TXpFek5Gb3dQVEVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh4RFRBTEJnTlZCQW9NQkV4bFlXWXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBVGl0WUhFYVlWdWM4ZzlBalRPd0VyTXZHeVB5a1BhK3B1dlRJOGhKVEhaWkRMR2FzMnFYMStFcnhnUVRKZ1ZYdjc2bm1MaGhSSkgrajI1QWlBSThpR3NveTh3TFRBSkJnTlZIUk1FQWpBQU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3TklBREJGQWlCWDRjK1QwRnA1bko1UVJDbFJmdTVQU0J5UnZOUHR1YVRzazB2UEIzV0FJQUloQU5nYWF1QWovWVA5czBBa0VoeUpoeFFPLzZRMnpvdVorSDFDSU9laG5NelEiLCJNSUlCbnpDQ0FVV2dBd0lCQWdJQkN6QUtCZ2dxaGtqT1BRUURBekEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TlRJeE16RXdOVm9YRFRNek1ERXdNVEl4TXpFd05Wb3dSVEVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh4RlRBVEJnTlZCQW9NREVsdWRHVnliV1ZrYVdGMFpUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJCVU41VjlyS2pmUmlNQUlvakVBMEF2NU1wMG9GK08wY0w0Z3pyVEYxNzhpblVIdWdqN0V0NDZOcmtRN2hLZ01WbmpvZ3E0NVExck1zK2NNSFZOSUxXcWpOVEF6TUE4R0ExVWRFd1FJTUFZQkFmOENBUUF3RGdZRFZSMFBBUUgvQkFRREFnRUdNQkFHQ2lxR1NJYjNZMlFHQWdFRUFnVUFNQW9HQ0NxR1NNNDlCQU1EQTBnQU1FVUNJUUNtc0lLWXM0MXVsbHNzSFg0clZ2ZVVUMFo3SXM1L2hMSzFsRlBUdHVuM2hBSWdjMisyUkc1K2dOY0ZWY3MrWEplRWw0R1orb2psM1JPT21sbCt5ZTdkeW5RPSIsIk1JSUJnakNDQVNtZ0F3SUJBZ0lKQUxVYzVBTGlINXBiTUFvR0NDcUdTTTQ5QkFNRE1EWXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSERBbERkWEJsY25ScGJtOHdIaGNOTWpNd01UQTFNakV6TURJeVdoY05Nek13TVRBeU1qRXpNREl5V2pBMk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWMrL0JsK2dvc3BvNnRmOVo3aW81dGRLZHJsTjFZZFZucUVoRURYRFNoemRBSlBRaWphbVhJTUhmOHhXV1RhMXpnb1lUeE9LcGJ1SnREcGx6MVhyaVRhTWdNQjR3REFZRFZSMFRCQVV3QXdFQi96QU9CZ05WSFE4QkFmOEVCQU1DQVFZd0NnWUlLb1pJemowRUF3TURSd0F3UkFJZ2VtV1FYbk1BZFRhZDJKREpXbmc5VTR1QkJMNW1BN1dJMDVIN29IN2M2aVFDSUhpUnFNak5melVBeWl1OWg2ck9VL0sraVRSMEkvM1kvTlNXc1hIWCthY2MiXSwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJkYXRhIjp7ImJ1bmRsZUlkIjoiY29tLmV4YW1wbGUud3JvbmcifSwibm90aWZpY2F0aW9uVVVJRCI6IjlhZDU2YmQyLTBiYzYtNDJlMC1hZjI0LWZkOTk2ZDg3YTFlNiIsIm5vdGlmaWNhdGlvblR5cGUiOiJURVNUIn0.WWE31hTB_mcv2O_lf-xI-MNY3d8txc0MzpqFx4QnYDfFIxB95Lo2Fm3r46YSjLLdL7xCWdEJrJP5bHgRCejAGg -------------------------------------------------------------------------------- /tests/resources/mock_signed_data/missingX5CHeaderClaim: -------------------------------------------------------------------------------- 1 | eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsIng1Y3dyb25nIjpbIk1JSUJvRENDQVVhZ0F3SUJBZ0lCRERBS0JnZ3Foa2pPUFFRREF6QkZNUXN3Q1FZRFZRUUdFd0pWVXpFTE1Ba0dBMVVFQ0F3Q1EwRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekVWTUJNR0ExVUVDZ3dNU1c1MFpYSnRaV1JwWVhSbE1CNFhEVEl6TURFd05USXhNekV6TkZvWERUTXpNREV3TVRJeE16RXpORm93UFRFTE1Ba0dBMVVFQmhNQ1ZWTXhDekFKQmdOVkJBZ01Ba05CTVJJd0VBWURWUVFIREFsRGRYQmxjblJwYm04eERUQUxCZ05WQkFvTUJFeGxZV1l3V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVRpdFlIRWFZVnVjOGc5QWpUT3dFck12R3lQeWtQYStwdXZUSThoSlRIWlpETEdhczJxWDErRXJ4Z1FUSmdWWHY3Nm5tTGhoUkpIK2oyNUFpQUk4aUdzb3k4d0xUQUpCZ05WSFJNRUFqQUFNQTRHQTFVZER3RUIvd1FFQXdJSGdEQVFCZ29xaGtpRzkyTmtCZ3NCQkFJRkFEQUtCZ2dxaGtqT1BRUURBd05JQURCRkFpQlg0YytUMEZwNW5KNVFSQ2xSZnU1UFNCeVJ2TlB0dWFUc2swdlBCM1dBSUFJaEFOZ2FhdUFqL1lQOXMwQWtFaHlKaHhRTy82UTJ6b3VaK0gxQ0lPZWhuTXpRIiwiTUlJQm56Q0NBVVdnQXdJQkFnSUJDekFLQmdncWhrak9QUVFEQXpBMk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1CNFhEVEl6TURFd05USXhNekV3TlZvWERUTXpNREV3TVRJeE16RXdOVm93UlRFTE1Ba0dBMVVFQmhNQ1ZWTXhDekFKQmdOVkJBZ01Ba05CTVJJd0VBWURWUVFIREFsRGRYQmxjblJwYm04eEZUQVRCZ05WQkFvTURFbHVkR1Z5YldWa2FXRjBaVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCQlVONVY5cktqZlJpTUFJb2pFQTBBdjVNcDBvRitPMGNMNGd6clRGMTc4aW5VSHVnajdFdDQ2TnJrUTdoS2dNVm5qb2dxNDVRMXJNcytjTUhWTklMV3FqTlRBek1BOEdBMVVkRXdRSU1BWUJBZjhDQVFBd0RnWURWUjBQQVFIL0JBUURBZ0VHTUJBR0NpcUdTSWIzWTJRR0FnRUVBZ1VBTUFvR0NDcUdTTTQ5QkFNREEwZ0FNRVVDSVFDbXNJS1lzNDF1bGxzc0hYNHJWdmVVVDBaN0lzNS9oTEsxbEZQVHR1bjNoQUlnYzIrMlJHNStnTmNGVmNzK1hKZUVsNEdaK29qbDNST09tbGwreWU3ZHluUT0iLCJNSUlCZ2pDQ0FTbWdBd0lCQWdJSkFMVWM1QUxpSDVwYk1Bb0dDQ3FHU000OUJBTURNRFl4Q3pBSkJnTlZCQVlUQWxWVE1STXdFUVlEVlFRSURBcERZV3hwWm05eWJtbGhNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh3SGhjTk1qTXdNVEExTWpFek1ESXlXaGNOTXpNd01UQXlNakV6TURJeVdqQTJNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVNNQkFHQTFVRUJ3d0pRM1Z3WlhKMGFXNXZNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVjKy9CbCtnb3NwbzZ0ZjlaN2lvNXRkS2RybE4xWWRWbnFFaEVEWERTaHpkQUpQUWlqYW1YSU1IZjh4V1dUYTF6Z29ZVHhPS3BidUp0RHBsejFYcmlUYU1nTUI0d0RBWURWUjBUQkFVd0F3RUIvekFPQmdOVkhROEJBZjhFQkFNQ0FRWXdDZ1lJS29aSXpqMEVBd01EUndBd1JBSWdlbVdRWG5NQWRUYWQySkRKV25nOVU0dUJCTDVtQTdXSTA1SDdvSDdjNmlRQ0lIaVJxTWpOZnpVQXlpdTloNnJPVS9LK2lUUjBJLzNZL05TV3NYSFgrYWNjIl19.eyJkYXRhIjp7ImJ1bmRsZUlkIjoiY29tLmV4YW1wbGUifSwibm90aWZpY2F0aW9uVVVJRCI6IjlhZDU2YmQyLTBiYzYtNDJlMC1hZjI0LWZkOTk2ZDg3YTFlNiIsIm5vdGlmaWNhdGlvblR5cGUiOiJURVNUIn0.1TFhjDR4WwQJNgizVGYXz3WE3ajxTdH1wKLQQ71MtrkadSxxOo3yPo_6L9Z03unIU7YK-NRNzSIb5bh5WqTprQ -------------------------------------------------------------------------------- /tests/resources/mock_signed_data/testNotification: -------------------------------------------------------------------------------- 1 | eyJ4NWMiOlsiTUlJQm9EQ0NBVWFnQXdJQkFnSUJDekFLQmdncWhrak9QUVFEQWpCTk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1SVXdFd1lEVlFRS0RBeEpiblJsY20xbFpHbGhkR1V3SGhjTk1qTXdNVEEwTVRZek56TXhXaGNOTXpJeE1qTXhNVFl6TnpNeFdqQkZNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVNNQkFHQTFVRUJ3d0pRM1Z3WlhKMGFXNXZNUTB3Q3dZRFZRUUtEQVJNWldGbU1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRTRyV0J4R21GYm5QSVBRSTB6c0JLekx4c2o4cEQydnFicjB5UElTVXgyV1F5eG1yTnFsOWZoSzhZRUV5WUZWNysrcDVpNFlVU1Ivbzl1UUlnQ1BJaHJLTWZNQjB3Q1FZRFZSMFRCQUl3QURBUUJnb3Foa2lHOTJOa0Jnc0JCQUlUQURBS0JnZ3Foa2pPUFFRREFnTklBREJGQWlFQWtpRVprb0ZNa2o0Z1huK1E5alhRWk1qWjJnbmpaM2FNOE5ZcmdmVFVpdlFDSURKWVowRmFMZTduU0lVMkxXTFRrNXRYVENjNEU4R0pTWWYvc1lSeEVGaWUiLCJNSUlCbHpDQ0FUMmdBd0lCQWdJQkJqQUtCZ2dxaGtqT1BRUURBakEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TkRFMk1qWXdNVm9YRFRNeU1USXpNVEUyTWpZd01Wb3dUVEVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekVWTUJNR0ExVUVDZ3dNU1c1MFpYSnRaV1JwWVhSbE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRUZRM2xYMnNxTjlHSXdBaWlNUURRQy9reW5TZ1g0N1J3dmlET3RNWFh2eUtkUWU2Q1BzUzNqbzJ1UkR1RXFBeFdlT2lDcmpsRFdzeXo1d3dkVTBndGFxTWxNQ013RHdZRFZSMFRCQWd3QmdFQi93SUJBREFRQmdvcWhraUc5Mk5rQmdJQkJBSVRBREFLQmdncWhrak9QUVFEQWdOSUFEQkZBaUVBdm56TWNWMjY4Y1JiMS9GcHlWMUVoVDNXRnZPenJCVVdQNi9Ub1RoRmF2TUNJRmJhNXQ2WUt5MFIySkR0eHF0T2pKeTY2bDZWN2QvUHJBRE5wa21JUFcraSIsIk1JSUJYRENDQVFJQ0NRQ2ZqVFVHTERuUjlqQUtCZ2dxaGtqT1BRUURBekEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TkRFMk1qQXpNbG9YRFRNek1ERXdNVEUyTWpBek1sb3dOakVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCSFB2d1pmb0tMS2FPclgvV2U0cU9iWFNuYTVUZFdIVlo2aElSQTF3MG9jM1FDVDBJbzJwbHlEQjMvTVZsazJ0YzRLR0U4VGlxVzdpYlE2WmM5VjY0azB3Q2dZSUtvWkl6ajBFQXdNRFNBQXdSUUloQU1USGhXdGJBUU4waFN4SVhjUDRDS3JEQ0gvZ3N4V3B4NmpUWkxUZVorRlBBaUIzNW53azVxMHpjSXBlZnZZSjBNVS95R0dIU1dlejBicTBwRFlVTy9ubUR3PT0iXSwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJkYXRhIjp7ImFwcEFwcGxlSWQiOjEyMzQsImVudmlyb25tZW50IjoiU2FuZGJveCIsImJ1bmRsZUlkIjoiY29tLmV4YW1wbGUifSwibm90aWZpY2F0aW9uVVVJRCI6IjlhZDU2YmQyLTBiYzYtNDJlMC1hZjI0LWZkOTk2ZDg3YTFlNiIsInNpZ25lZERhdGUiOjE2ODEzMTQzMjQwMDAsIm5vdGlmaWNhdGlvblR5cGUiOiJURVNUIn0.VVXYwuNm2Y3XsOUva-BozqatRCsDuykA7xIe_CCRw6aIAAxJ1nb2sw871jfZ6dcgNhUuhoZ93hfbc1v_5zB7Og -------------------------------------------------------------------------------- /models/ExtendRenewalDateResponse.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { Validator } from "./Validator" 4 | 5 | /** 6 | * A response that indicates whether an individual renewal-date extension succeeded, and related details. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/extendrenewaldateresponse ExtendRenewalDateResponse} 9 | */ 10 | export interface ExtendRenewalDateResponse { 11 | 12 | /** 13 | * The original transaction identifier of a purchase. 14 | * 15 | * {@link https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid originalTransactionId} 16 | **/ 17 | originalTransactionId?: string 18 | 19 | /** 20 | * The unique identifier of subscription-purchase events across devices, including renewals. 21 | * 22 | * {@link https://developer.apple.com/documentation/appstoreserverapi/weborderlineitemid webOrderLineItemId} 23 | **/ 24 | webOrderLineItemId?: string 25 | 26 | /** 27 | * A Boolean value that indicates whether the subscription-renewal-date extension succeeded. 28 | * 29 | * {@link https://developer.apple.com/documentation/appstoreserverapi/success success} 30 | **/ 31 | success?: boolean 32 | 33 | /** 34 | * The new subscription expiration date for a subscription-renewal extension. 35 | * 36 | * {@link https://developer.apple.com/documentation/appstoreserverapi/effectivedate effectiveDate} 37 | **/ 38 | effectiveDate?: number 39 | } 40 | 41 | 42 | export class ExtendRenewalDateResponseValidator implements Validator { 43 | validate(obj: any): obj is ExtendRenewalDateResponse { 44 | if ((typeof obj['originalTransactionId'] !== 'undefined') && !(typeof obj['originalTransactionId'] === "string" || obj['originalTransactionId'] instanceof String)) { 45 | return false 46 | } 47 | if ((typeof obj['webOrderLineItemId'] !== 'undefined') && !(typeof obj['webOrderLineItemId'] === "string" || obj['webOrderLineItemId'] instanceof String)) { 48 | return false 49 | } 50 | if ((typeof obj['success'] !== 'undefined') && !(typeof obj['success'] === "boolean" || obj['success'] instanceof Boolean)) { 51 | return false 52 | } 53 | if ((typeof obj['effectiveDate'] !== 'undefined') && !(typeof obj['effectiveDate'] === "number")) { 54 | return false 55 | } 56 | return true 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /models/NotificationHistoryResponse.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { NotificationHistoryResponseItem } from "./NotificationHistoryResponseItem"; 4 | import { Validator } from "./Validator"; 5 | 6 | /** 7 | * A response that contains the App Store Server Notifications history for your app. 8 | * 9 | * {@link https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryresponse NotificationHistoryResponse} 10 | */ 11 | export interface NotificationHistoryResponse { 12 | 13 | /** 14 | * A pagination token that you return to the endpoint on a subsequent call to receive the next set of results. 15 | * 16 | * {@link https://developer.apple.com/documentation/appstoreserverapi/paginationtoken paginationToken} 17 | **/ 18 | paginationToken?: string 19 | 20 | /** 21 | * A Boolean value indicating whether the App Store has more transaction data. 22 | * 23 | * {@link https://developer.apple.com/documentation/appstoreserverapi/hasmore hasMore} 24 | **/ 25 | hasMore?: boolean 26 | 27 | /** 28 | * An array of App Store server notification history records. 29 | * 30 | * {@link https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryresponseitem notificationHistoryResponseItem} 31 | **/ 32 | notificationHistory?: NotificationHistoryResponseItem[]; 33 | } 34 | 35 | 36 | export class NotificationHistoryResponseValidator implements Validator { 37 | static readonly notificationHistoryResponseItemValidator = new NotificationHistoryResponseValidator() 38 | validate(obj: any): obj is NotificationHistoryResponse { 39 | if ((typeof obj['paginationToken'] !== 'undefined') && !(typeof obj['paginationToken'] === "string" || obj['paginationToken'] instanceof String)) { 40 | return false 41 | } 42 | if ((typeof obj['hasMore'] !== 'undefined') && !(typeof obj['hasMore'] === "boolean" || obj['hasMore'] instanceof Boolean)) { 43 | return false 44 | } 45 | if (typeof obj['notificationHistory'] !== 'undefined') { 46 | if (!Array.isArray(obj['notificationHistory'])) { 47 | return false 48 | } 49 | for (const notificationHistoryResponseItem of obj['notificationHistory']) { 50 | if (!(NotificationHistoryResponseValidator.notificationHistoryResponseItemValidator.validate(notificationHistoryResponseItem))) { 51 | return false 52 | } 53 | } 54 | } 55 | return true 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /models/LastTransactionsItem.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { Status, StatusValidator } from "./Status" 4 | import { Validator } from "./Validator" 5 | 6 | /** 7 | * The most recent App Store-signed transaction information and App Store-signed renewal information for an auto-renewable subscription. 8 | * 9 | * {@link https://developer.apple.com/documentation/appstoreserverapi/lasttransactionsitem lastTransactionsItem} 10 | */ 11 | export interface LastTransactionsItem { 12 | 13 | /** 14 | * The status of the auto-renewable subscription. 15 | * 16 | * {@link https://developer.apple.com/documentation/appstoreserverapi/status status} 17 | **/ 18 | status?: Status | number 19 | 20 | /** 21 | * The original transaction identifier of a purchase. 22 | * 23 | * {@link https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid originalTransactionId} 24 | **/ 25 | originalTransactionId?: string 26 | 27 | /** 28 | * Transaction information signed by the App Store, in JSON Web Signature (JWS) format. 29 | * 30 | * {@link https://developer.apple.com/documentation/appstoreserverapi/jwstransaction JWSTransaction} 31 | **/ 32 | signedTransactionInfo?: string 33 | 34 | /** 35 | * Subscription renewal information, signed by the App Store, in JSON Web Signature (JWS) format. 36 | * 37 | * {@link https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfo JWSRenewalInfo} 38 | **/ 39 | signedRenewalInfo?: string 40 | } 41 | 42 | 43 | export class LastTransactionsItemValidator implements Validator { 44 | static readonly statusValidator = new StatusValidator() 45 | validate(obj: any): obj is LastTransactionsItem { 46 | if ((typeof obj['status'] !== 'undefined') && !(LastTransactionsItemValidator.statusValidator.validate(obj['status']))) { 47 | return false 48 | } 49 | if ((typeof obj['originalTransactionId'] !== 'undefined') && !(typeof obj['originalTransactionId'] === "string" || obj['originalTransactionId'] instanceof String)) { 50 | return false 51 | } 52 | if ((typeof obj['signedTransactionInfo'] !== 'undefined') && !(typeof obj['signedTransactionInfo'] === "string" || obj['signedTransactionInfo'] instanceof String)) { 53 | return false 54 | } 55 | if ((typeof obj['signedRenewalInfo'] !== 'undefined') && !(typeof obj['signedRenewalInfo'] === "string" || obj['signedRenewalInfo'] instanceof String)) { 56 | return false 57 | } 58 | return true 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /models/NotificationHistoryRequest.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { NotificationTypeV2 } from "./NotificationTypeV2"; 4 | import { Subtype } from "./Subtype"; 5 | 6 | /** 7 | * The request body for notification history. 8 | * 9 | * {@link https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryrequest NotificationHistoryRequest} 10 | */ 11 | export interface NotificationHistoryRequest { 12 | 13 | /** 14 | * The start date of the timespan for the requested App Store Server Notification history records. The startDate needs to precede the endDate. Choose a startDate that’s within the past 180 days from the current date. 15 | * 16 | * {@link https://developer.apple.com/documentation/appstoreserverapi/startdate startDate} 17 | **/ 18 | startDate?: number 19 | 20 | /** 21 | * The end date of the timespan for the requested App Store Server Notification history records. Choose an endDate that’s later than the startDate. If you choose an endDate in the future, the endpoint automatically uses the current date as the endDate. 22 | * 23 | * {@link https://developer.apple.com/documentation/appstoreserverapi/enddate endDate} 24 | **/ 25 | endDate?: number 26 | 27 | /** 28 | * A notification type. Provide this field to limit the notification history records to those with this one notification type. For a list of notifications types, see notificationType. 29 | * Include either the transactionId or the notificationType in your query, but not both. 30 | * 31 | * {@link https://developer.apple.com/documentation/appstoreserverapi/notificationtype notificationType} 32 | **/ 33 | notificationType?: NotificationTypeV2; 34 | 35 | /** 36 | * A notification subtype. Provide this field to limit the notification history records to those with this one notification subtype. For a list of subtypes, see subtype. If you specify a notificationSubtype, you need to also specify its related notificationType. 37 | * 38 | * {@link https://developer.apple.com/documentation/appstoreserverapi/notificationsubtype notificationSubtype} 39 | **/ 40 | notificationSubtype?: Subtype 41 | 42 | /** 43 | * The transaction identifier, which may be an original transaction identifier, of any transaction belonging to the customer. Provide this field to limit the notification history request to this one customer. 44 | * Include either the transactionId or the notificationType in your query, but not both. 45 | * 46 | * {@link https://developer.apple.com/documentation/appstoreserverapi/transactionid transactionId} 47 | **/ 48 | transactionId?: string 49 | 50 | /** 51 | * A Boolean value you set to true to request only the notifications that haven’t reached your server successfully. The response also includes notifications that the App Store server is currently retrying to send to your server. 52 | * 53 | * {@link https://developer.apple.com/documentation/appstoreserverapi/onlyfailures onlyFailures} 54 | **/ 55 | onlyFailures?: boolean 56 | } -------------------------------------------------------------------------------- /models/MassExtendRenewalDateStatusResponse.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { Validator } from "./Validator" 4 | 5 | /** 6 | * A response that indicates the current status of a request to extend the subscription renewal date to all eligible subscribers. 7 | * 8 | * {@link https://developer.apple.com/documentation/appstoreserverapi/massextendrenewaldatestatusresponse MassExtendRenewalDateStatusResponse} 9 | */ 10 | export interface MassExtendRenewalDateStatusResponse { 11 | 12 | /** 13 | * A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. 14 | * 15 | * {@link https://developer.apple.com/documentation/appstoreserverapi/requestidentifier requestIdentifier} 16 | **/ 17 | requestIdentifier?: string 18 | 19 | /** 20 | * A Boolean value that indicates whether the App Store completed the request to extend a subscription renewal date to active subscribers. 21 | * 22 | * {@link https://developer.apple.com/documentation/appstoreserverapi/complete complete} 23 | **/ 24 | complete?: boolean 25 | 26 | /** 27 | * The UNIX time, in milliseconds, that the App Store completes a request to extend a subscription renewal date for eligible subscribers. 28 | * 29 | * {@link https://developer.apple.com/documentation/appstoreserverapi/completedate completeDate} 30 | **/ 31 | completeDate?: number 32 | 33 | /** 34 | * The count of subscriptions that successfully receive a subscription-renewal-date extension. 35 | * 36 | * {@link https://developer.apple.com/documentation/appstoreserverapi/succeededcount succeededCount} 37 | **/ 38 | succeededCount?: number 39 | 40 | /** 41 | * The count of subscriptions that fail to receive a subscription-renewal-date extension. 42 | * 43 | * {@link https://developer.apple.com/documentation/appstoreserverapi/failedcount failedCount} 44 | **/ 45 | failedCount?: number 46 | } 47 | 48 | 49 | export class MassExtendRenewalDateStatusResponseValidator implements Validator { 50 | validate(obj: any): obj is MassExtendRenewalDateStatusResponse { 51 | if ((typeof obj['requestIdentifier'] !== 'undefined') && !(typeof obj['requestIdentifier'] === "string" || obj['requestIdentifier'] instanceof String)) { 52 | return false 53 | } 54 | if ((typeof obj['completeDate'] !== 'undefined') && !(typeof obj['completeDate'] === "number")) { 55 | return false 56 | } 57 | if ((typeof obj['complete'] !== 'undefined') && !(typeof obj['complete'] === "boolean" || obj['complete'] instanceof Boolean)) { 58 | return false 59 | } 60 | if ((typeof obj['succeededCount'] !== 'undefined') && !(typeof obj['succeededCount'] === "number")) { 61 | return false 62 | } 63 | if ((typeof obj['failedCount'] !== 'undefined') && !(typeof obj['failedCount'] === "number")) { 64 | return false 65 | } 66 | return true 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /models/TransactionHistoryRequest.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { InAppOwnershipType } from "./InAppOwnershipType" 4 | 5 | export interface TransactionHistoryRequest { 6 | 7 | /** 8 | * An optional start date of the timespan for the transaction history records you’re requesting. The startDate must precede the endDate if you specify both dates. To be included in results, the transaction’s purchaseDate must be equal to or greater than the startDate. 9 | * 10 | * {@link https://developer.apple.com/documentation/appstoreserverapi/startdate startDate} 11 | */ 12 | startDate?: number 13 | 14 | /** 15 | * An optional end date of the timespan for the transaction history records you’re requesting. Choose an endDate that’s later than the startDate if you specify both dates. Using an endDate in the future is valid. To be included in results, the transaction’s purchaseDate must be less than the endDate. 16 | * 17 | * {@link https://developer.apple.com/documentation/appstoreserverapi/enddate endDate} 18 | */ 19 | endDate?: number 20 | 21 | /** 22 | * An optional filter that indicates the product identifier to include in the transaction history. Your query may specify more than one productID. 23 | * 24 | * {@link https://developer.apple.com/documentation/appstoreserverapi/productid productId} 25 | */ 26 | productIds?: string[] 27 | 28 | /** 29 | * An optional filter that indicates the product type to include in the transaction history. Your query may specify more than one productType. 30 | */ 31 | productTypes?: ProductType[] 32 | 33 | /** 34 | * An optional sort order for the transaction history records. The response sorts the transaction records by their recently modified date. The default value is ASCENDING, so you receive the oldest records first. 35 | */ 36 | sort?: Order 37 | 38 | /** 39 | * An optional filter that indicates the subscription group identifier to include in the transaction history. Your query may specify more than one subscriptionGroupIdentifier. 40 | * 41 | * {@link https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifier subscriptionGroupIdentifier} 42 | */ 43 | subscriptionGroupIdentifiers?: string[] 44 | 45 | /** 46 | * An optional filter that limits the transaction history by the in-app ownership type. 47 | * 48 | * {@link https://developer.apple.com/documentation/appstoreserverapi/inappownershiptype inAppOwnershipType} 49 | */ 50 | inAppOwnershipType?: InAppOwnershipType 51 | 52 | /** 53 | * An optional Boolean value that indicates whether the response includes only revoked transactions when the value is true, or contains only nonrevoked transactions when the value is false. By default, the request doesn't include this parameter. 54 | */ 55 | revoked?: boolean 56 | } 57 | 58 | export enum ProductType { 59 | AUTO_RENEWABLE = "AUTO_RENEWABLE", 60 | NON_RENEWABLE = "NON_RENEWABLE", 61 | CONSUMABLE = "CONSUMABLE", 62 | NON_CONSUMABLE = "NON_CONSUMABLE" 63 | } 64 | 65 | export enum Order { 66 | ASCENDING = "ASCENDING", 67 | DESCENDING = "DESCENDING" 68 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 2.0.0 4 | - Support Retention Messaging API [https://github.com/apple/app-store-server-library-node/pull/341] 5 | - This changes internal details of AppStoreServerAPIClient, which is a breaking change for subclassing clients 6 | - Incorporate changes for App Store Server API v1.17 [https://github.com/apple/app-store-server-library-node/pull/344] from @riyazpanjwani 7 | - Add a new VerificationStatus case for retryable OCSP network failures [https://github.com/apple/app-store-server-library-node/pull/345] 8 | - Incorporate changes for App Store Server API v1.18 [https://github.com/apple/app-store-server-library-node/pull/348] from @izanger 9 | - This changes OfferType's case SUBSCRIPTION_OFFER_CODE to OFFER_CODE, which is a breaking change 10 | 11 | ## Version 1.6.0 12 | - Incorporate changes for App Store Server API v1.16 [https://github.com/apple/app-store-server-library-node/pull/275] 13 | 14 | ## Version 1.5.0 15 | - Incorporate changes for App Store Server API v1.15 and App Store Server Notifications v2.15 [https://github.com/apple/app-store-server-library-node/pull/236] 16 | - Add verified chain caching to improve performance [https://github.com/apple/app-store-server-library-node/pull/235] 17 | - Expose VerificationStatus and VerificationException [https://github.com/apple/app-store-server-library-node/pull/222] 18 | - Typo corrections [https://github.com/apple/app-store-server-library-node/pull/199] from @hakusai22 19 | 20 | ## Version 1.4.0 21 | - Incorporate changes for App Store Server API v1.13 and App Store Server Notifications v2.13 [https://github.com/apple/app-store-server-library-node/pull/163] 22 | - Add missing export for OfferDiscountType [https://github.com/apple/app-store-server-library-node/pull/159] from @coltkenn2658 23 | 24 | ## Version 1.3.0 25 | - Incorporate changes for App Store Server API v1.12 and App Store Server Notifications v2.12 [https://github.com/apple/app-store-server-library-node/pull/146] 26 | 27 | ## Version 1.2.0 28 | - Incorporate changes for App Store Server API v1.11 and App Store Server Notifications v2.11 [https://github.com/apple/app-store-server-library-node/pull/132] 29 | - Various documentation and quality of life improvements, including contributions from @yidinghan 30 | 31 | ## Version 1.1.0 32 | - Support App Store Server Notifications v2.10 [https://github.com/apple/app-store-server-library-node/pull/107] 33 | - Require appAppleId in SignedDataVerifier for the Production environment [https://github.com/apple/app-store-server-library-node/pull/86] 34 | 35 | ## Version 1.0.1 36 | - Bump jsrsasign package version [https://github.com/apple/app-store-server-library-node/pull/62] 37 | 38 | ## Version 1.0.0 39 | - Add error message to APIException [https://github.com/apple/app-store-server-library-node/pull/48] 40 | 41 | ## Version 0.2.0 42 | - Add support for reading unknown enum values [https://github.com/apple/app-store-server-library-node/pull/35] 43 | - Add support for Xcode and LocalTesting environments [https://github.com/apple/app-store-server-library-node/pull/34] 44 | - Add error codes from App Store Server API v1.9 [https://github.com/apple/app-store-server-library-node/pull/33] 45 | - Add new fields from App Store Server API v1.10 [https://github.com/apple/app-store-server-library-node/pull/27] 46 | - Document error codes [https://github.com/apple/app-store-server-library-node/pull/23] 47 | - Correct naming of SignedDataVerifier to match other libraries [https://github.com/apple/app-store-server-library-node/pull/40] 48 | -------------------------------------------------------------------------------- /models/DecodedRealtimeRequestBody.ts: -------------------------------------------------------------------------------- 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 { 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 | } 92 | -------------------------------------------------------------------------------- /models/HistoryResponse.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { Environment, EnvironmentValidator } from "./Environment"; 4 | import { Validator } from "./Validator"; 5 | 6 | /** 7 | * A response that contains the customer’s transaction history for an app. 8 | * 9 | * {@link https://developer.apple.com/documentation/appstoreserverapi/historyresponse HistoryResponse} 10 | */ 11 | export interface HistoryResponse { 12 | /** 13 | * A token you use in a query to request the next set of transactions for the customer. 14 | * 15 | * {@link https://developer.apple.com/documentation/appstoreserverapi/revision revision} 16 | **/ 17 | revision?: string 18 | 19 | /** 20 | * A Boolean value indicating whether the App Store has more transaction data. 21 | * 22 | * {@link https://developer.apple.com/documentation/appstoreserverapi/hasmore hasMore} 23 | **/ 24 | hasMore?: boolean 25 | 26 | /** 27 | * The bundle identifier of an app. 28 | * 29 | * {@link https://developer.apple.com/documentation/appstoreserverapi/bundleid bundleId} 30 | **/ 31 | bundleId?: string 32 | 33 | /** 34 | * The unique identifier of an app in the App Store. 35 | * 36 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/appappleid appAppleId} 37 | **/ 38 | appAppleId?: number 39 | 40 | /** 41 | * The server environment in which you’re making the request, whether sandbox or production. 42 | * 43 | * {@link https://developer.apple.com/documentation/appstoreserverapi/environment environment} 44 | **/ 45 | environment?: Environment | string 46 | 47 | /** 48 | * An array of in-app purchase transactions for the customer, signed by Apple, in JSON Web Signature format. 49 | * 50 | * {@link https://developer.apple.com/documentation/appstoreserverapi/jwstransaction JWSTransaction} 51 | **/ 52 | signedTransactions?: string[]; 53 | } 54 | 55 | 56 | export class HistoryResponseValidator implements Validator { 57 | static readonly environmentValidator = new EnvironmentValidator() 58 | validate(obj: any): obj is HistoryResponse { 59 | if ((typeof obj['revision'] !== 'undefined') && !(typeof obj['revision'] === "string" || obj['revision'] instanceof String)) { 60 | return false 61 | } 62 | if ((typeof obj['hasMore'] !== 'undefined') && !(typeof obj['hasMore'] === "boolean" || obj['hasMore'] instanceof Boolean)) { 63 | return false 64 | } 65 | if ((typeof obj['bundleId'] !== 'undefined') && !(typeof obj['bundleId'] === "string" || obj['bundleId'] instanceof String)) { 66 | return false 67 | } 68 | if ((typeof obj['appAppleId'] !== 'undefined') && !(typeof obj['appAppleId'] === "number")) { 69 | return false 70 | } 71 | if ((typeof obj['environment'] !== 'undefined') && !(HistoryResponseValidator.environmentValidator.validate(obj['environment']))) { 72 | return false 73 | } 74 | if (typeof obj['signedTransactions'] !== 'undefined') { 75 | if (!Array.isArray(obj['signedTransactions'])) { 76 | return false 77 | } 78 | for (const signedTransaction of obj['signedTransactions']) { 79 | if (!(typeof signedTransaction === "string" || signedTransaction instanceof String)) { 80 | return false 81 | } 82 | } 83 | } 84 | return true 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /models/Summary.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { Environment, EnvironmentValidator } from "./Environment"; 4 | import { Validator } from "./Validator"; 5 | 6 | /** 7 | * The payload data for a subscription-renewal-date extension notification. 8 | * 9 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/summary summary} 10 | */ 11 | export interface Summary { 12 | /** 13 | * The server environment that the notification applies to, either sandbox or production. 14 | * 15 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/environment environment} 16 | **/ 17 | environment?: Environment | string 18 | 19 | /** 20 | * The unique identifier of an app in the App Store. 21 | * 22 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/appappleid appAppleId} 23 | **/ 24 | appAppleId?: number 25 | 26 | /** 27 | * The bundle identifier of an app. 28 | * 29 | * {@link https://developer.apple.com/documentation/appstoreserverapi/bundleid bundleId} 30 | **/ 31 | bundleId?: string 32 | 33 | /** 34 | * The unique identifier for the product, that you create in App Store Connect. 35 | * 36 | * {@link https://developer.apple.com/documentation/appstoreserverapi/productid productId} 37 | **/ 38 | productId?: string 39 | 40 | /** 41 | * A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. 42 | * 43 | * {@link https://developer.apple.com/documentation/appstoreserverapi/requestidentifier requestIdentifier} 44 | **/ 45 | requestIdentifier?: string 46 | 47 | /** 48 | * A list of storefront country codes you provide to limit the storefronts for a subscription-renewal-date extension. 49 | * 50 | * {@link https://developer.apple.com/documentation/appstoreserverapi/storefrontcountrycodes storefrontCountryCodes} 51 | **/ 52 | storefrontCountryCodes?: string[]; 53 | 54 | /** 55 | * The count of subscriptions that successfully receive a subscription-renewal-date extension. 56 | * 57 | * {@link https://developer.apple.com/documentation/appstoreserverapi/succeededcount succeededCount} 58 | **/ 59 | succeededCount?: number 60 | 61 | /** 62 | * The count of subscriptions that fail to receive a subscription-renewal-date extension. 63 | * 64 | * {@link https://developer.apple.com/documentation/appstoreserverapi/failedcount failedCount} 65 | **/ 66 | failedCount?: number 67 | } 68 | 69 | 70 | export class SummaryValidator implements Validator { 71 | static readonly environmentValidator = new EnvironmentValidator() 72 | validate(obj: any): obj is Summary { 73 | if ((typeof obj['environment'] !== 'undefined') && !(SummaryValidator.environmentValidator.validate(obj['environment']))) { 74 | return false 75 | } 76 | if ((typeof obj['appAppleId'] !== 'undefined') && !(typeof obj['appAppleId'] === "number")) { 77 | return false 78 | } 79 | if ((typeof obj['bundleId'] !== 'undefined') && !(typeof obj['bundleId'] === "string" || obj['bundleId'] instanceof String)) { 80 | return false 81 | } 82 | if ((typeof obj['productId'] !== 'undefined') && !(typeof obj['productId'] === "string" || obj['productId'] instanceof String)) { 83 | return false 84 | } 85 | if ((typeof obj['requestIdentifier'] !== 'undefined') && !(typeof obj['requestIdentifier'] === "string" || obj['requestIdentifier'] instanceof String)) { 86 | return false 87 | } 88 | if ((typeof obj['succeededCount'] !== 'undefined') && !(typeof obj['succeededCount'] === "number")) { 89 | return false 90 | } 91 | if ((typeof obj['failedCount'] !== 'undefined') && !(typeof obj['failedCount'] === "number")) { 92 | return false 93 | } 94 | return true 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /models/ConsumptionRequest.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { AccountTenure } from "./AccountTenure" 4 | import { ConsumptionStatus } from "./ConsumptionStatus" 5 | import { DeliveryStatus } from "./DeliveryStatus" 6 | import { LifetimeDollarsPurchased } from "./LifetimeDollarsPurchased" 7 | import { LifetimeDollarsRefunded } from "./LifetimeDollarsRefunded" 8 | import { Platform } from "./Platform" 9 | import { PlayTime } from "./PlayTime" 10 | import { RefundPreference } from "./RefundPreference" 11 | import { UserStatus } from "./UserStatus" 12 | 13 | /** 14 | * The request body containing consumption information. 15 | * 16 | * {@link https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest ConsumptionRequest} 17 | */ 18 | export interface ConsumptionRequest { 19 | 20 | /** 21 | * A Boolean value that indicates whether the customer consented to provide consumption data to the App Store. 22 | * 23 | * {@link https://developer.apple.com/documentation/appstoreserverapi/customerconsented customerConsented} 24 | **/ 25 | customerConsented?: boolean 26 | 27 | /** 28 | * A value that indicates the extent to which the customer consumed the in-app purchase. 29 | * 30 | * {@link https://developer.apple.com/documentation/appstoreserverapi/consumptionstatus consumptionStatus} 31 | **/ 32 | consumptionStatus?: ConsumptionStatus | number 33 | 34 | /** 35 | * A value that indicates the platform on which the customer consumed the in-app purchase. 36 | * 37 | * {@link https://developer.apple.com/documentation/appstoreserverapi/platform platform} 38 | **/ 39 | platform?: Platform | number 40 | 41 | /** 42 | * A Boolean value that indicates whether you provided, prior to its purchase, a free sample or trial of the content, or information about its functionality. 43 | * 44 | * {@link https://developer.apple.com/documentation/appstoreserverapi/samplecontentprovided sampleContentProvided} 45 | **/ 46 | sampleContentProvided?: boolean 47 | 48 | /** 49 | * A value that indicates whether the app successfully delivered an in-app purchase that works properly. 50 | * 51 | * {@link https://developer.apple.com/documentation/appstoreserverapi/deliverystatus deliveryStatus} 52 | **/ 53 | deliveryStatus?: DeliveryStatus | number 54 | 55 | /** 56 | * The UUID that an app optionally generates to map a customer’s in-app purchase with its resulting App Store transaction. 57 | * 58 | * {@link https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken appAccountToken} 59 | **/ 60 | appAccountToken?: string 61 | 62 | /** 63 | * The age of the customer’s account. 64 | * 65 | * {@link https://developer.apple.com/documentation/appstoreserverapi/accounttenure accountTenure} 66 | **/ 67 | accountTenure?: AccountTenure | number 68 | 69 | /** 70 | * A value that indicates the amount of time that the customer used the app. 71 | * 72 | * {@link https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest ConsumptionRequest} 73 | **/ 74 | playTime?: PlayTime | number 75 | 76 | /** 77 | * A value that indicates the total amount, in USD, of refunds the customer has received, in your app, across all platforms. 78 | * 79 | * {@link https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarsrefunded lifetimeDollarsRefunded} 80 | **/ 81 | lifetimeDollarsRefunded?: LifetimeDollarsRefunded | number 82 | 83 | /** 84 | * A value that indicates the total amount, in USD, of in-app purchases the customer has made in your app, across all platforms. 85 | * 86 | * {@link https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarspurchased lifetimeDollarsPurchased} 87 | **/ 88 | lifetimeDollarsPurchased?: LifetimeDollarsPurchased | number 89 | 90 | /** 91 | * The status of the customer’s account. 92 | * 93 | * {@link https://developer.apple.com/documentation/appstoreserverapi/userstatus userStatus} 94 | **/ 95 | userStatus?: UserStatus | number 96 | 97 | /** 98 | * A value that indicates your preference, based on your operational logic, as to whether Apple should grant the refund. 99 | * 100 | * {@link https://developer.apple.com/documentation/appstoreserverapi/refundpreference refundPreference} 101 | **/ 102 | refundPreference?: RefundPreference | number 103 | } -------------------------------------------------------------------------------- /models/Data.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { ConsumptionRequestReason, ConsumptionRequestReasonValidator } from "./ConsumptionRequestReason" 4 | import { Environment, EnvironmentValidator } from "./Environment" 5 | import { Status, StatusValidator } from "./Status" 6 | import { Validator } from "./Validator" 7 | 8 | /** 9 | * The app metadata and the signed renewal and transaction information. 10 | * 11 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/data data} 12 | */ 13 | export interface Data { 14 | 15 | /** 16 | * The server environment that the notification applies to, either sandbox or production. 17 | * 18 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/environment environment} 19 | **/ 20 | environment?: Environment | string 21 | 22 | /** 23 | * The unique identifier of an app in the App Store. 24 | * 25 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/appappleid appAppleId} 26 | **/ 27 | appAppleId?: number 28 | 29 | /** 30 | * The bundle identifier of an app. 31 | * 32 | * {@link https://developer.apple.com/documentation/appstoreserverapi/bundleid bundleId} 33 | **/ 34 | bundleId?: string 35 | 36 | /** 37 | * The version of the build that identifies an iteration of the bundle. 38 | * 39 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/bundleversion bundleVersion} 40 | **/ 41 | bundleVersion?: string 42 | 43 | /** 44 | * Transaction information signed by the App Store, in JSON Web Signature (JWS) format. 45 | * 46 | * {@link https://developer.apple.com/documentation/appstoreserverapi/jwstransaction JWSTransaction} 47 | **/ 48 | signedTransactionInfo?: string 49 | 50 | /** 51 | * Subscription renewal information, signed by the App Store, in JSON Web Signature (JWS) format. 52 | * 53 | * {@link https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfo JWSRenewalInfo} 54 | **/ 55 | signedRenewalInfo?: string 56 | 57 | /** 58 | * The status of an auto-renewable subscription as of the signedDate in the responseBodyV2DecodedPayload. 59 | * 60 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/status status} 61 | **/ 62 | status?: Status | number 63 | 64 | /** 65 | * The reason the customer requested the refund. 66 | * 67 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/consumptionrequestreason consumptionRequestReason} 68 | **/ 69 | consumptionRequestReason?: ConsumptionRequestReason | string 70 | } 71 | 72 | 73 | export class DataValidator implements Validator { 74 | static readonly environmentValidator = new EnvironmentValidator() 75 | static readonly statusValidator = new StatusValidator() 76 | static readonly consumptionRequestReasonValidator = new ConsumptionRequestReasonValidator() 77 | validate(obj: any): obj is Data { 78 | if ((typeof obj['environment'] !== 'undefined') && !(DataValidator.environmentValidator.validate(obj['environment']))) { 79 | return false 80 | } 81 | if ((typeof obj['appAppleId'] !== 'undefined') && !(typeof obj['appAppleId'] === "number")) { 82 | return false 83 | } 84 | if ((typeof obj['bundleId'] !== 'undefined') && !(typeof obj['bundleId'] === "string" || obj['bundleId'] instanceof String)) { 85 | return false 86 | } 87 | if ((typeof obj['bundleVersion'] !== 'undefined') && !(typeof obj['bundleVersion'] === "string" || obj['bundleVersion'] instanceof String)) { 88 | return false 89 | } 90 | if ((typeof obj['signedTransactionInfo'] !== 'undefined') && !(typeof obj['signedTransactionInfo'] === "string" || obj['signedTransactionInfo'] instanceof String)) { 91 | return false 92 | } 93 | if ((typeof obj['signedRenewalInfo'] !== 'undefined') && !(typeof obj['signedRenewalInfo'] === "string" || obj['signedRenewalInfo'] instanceof String)) { 94 | return false 95 | } 96 | if ((typeof obj['status'] !== 'undefined') && !(DataValidator.statusValidator.validate(obj['status']))) { 97 | return false 98 | } 99 | if ((typeof obj['consumptionRequestReason'] !== 'undefined') && !(DataValidator.consumptionRequestReasonValidator.validate(obj['consumptionRequestReason']))) { 100 | return false 101 | } 102 | return true 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/unit-tests/jws_signature_creator.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | import { AdvancedCommerceInAppRequest, AdvancedCommerceInAppSignatureCreator, IntroductoryOfferEligibilitySignatureCreator, PromotionalOfferV2SignatureCreator } from "../../jws_signature_creator"; 4 | import { readFile } from "../util" 5 | import jsonwebtoken = require('jsonwebtoken'); 6 | 7 | interface TestInAppRequest extends AdvancedCommerceInAppRequest { 8 | testValue: string 9 | } 10 | 11 | describe("JWS Signature Creator Checks", () => { 12 | it('should create a promotional offer signature', async () => { 13 | const signatureCreator = new PromotionalOfferV2SignatureCreator(readFile('tests/resources/certs/testSigningKey.p8'), "keyId", "issuerId", "bundleId"); 14 | const signature = signatureCreator.createSignature('productId', 'offerIdentifier', 'transactionId') 15 | expect(signature).toBeTruthy() 16 | let header = JSON.parse(Buffer.from(signature.split('.')[0], 'base64url').toString()) 17 | let payload = JSON.parse(Buffer.from(signature.split('.')[1], 'base64url').toString()) 18 | 19 | // Header 20 | expect('JWT').toBe(header['typ']) 21 | expect('ES256').toBe(header['alg']) 22 | expect('keyId').toBe(header['kid']) 23 | // Payload 24 | expect('issuerId').toBe(payload['iss']) 25 | expect(payload['iat']).toBeTruthy() 26 | expect(payload['exp']).toBeUndefined() 27 | expect('promotional-offer').toBe(payload['aud']) 28 | expect('bundleId').toBe(payload['bid']) 29 | expect(payload['nonce']).toBeTruthy() 30 | expect('productId').toBe(payload['productId']) 31 | expect('offerIdentifier').toBe(payload['offerIdentifier']) 32 | expect('transactionId').toBe(payload['transactionId']) 33 | }) 34 | 35 | it('should create a promotional offer signature without a transaction id', async () => { 36 | const signatureCreator = new PromotionalOfferV2SignatureCreator(readFile('tests/resources/certs/testSigningKey.p8'), "keyId", "issuerId", "bundleId"); 37 | const signature = signatureCreator.createSignature('productId', 'offerIdentifier') 38 | let payload = JSON.parse(Buffer.from(signature.split('.')[1], 'base64url').toString()) 39 | expect(payload['transactionId']).toBeUndefined() 40 | }) 41 | 42 | it('should create a introductory eligibility offer signature', async () => { 43 | const signatureCreator = new IntroductoryOfferEligibilitySignatureCreator(readFile('tests/resources/certs/testSigningKey.p8'), "keyId", "issuerId", "bundleId"); 44 | const signature = signatureCreator.createSignature('productId', true, 'transactionId') 45 | expect(signature).toBeTruthy() 46 | let header = JSON.parse(Buffer.from(signature.split('.')[0], 'base64url').toString()) 47 | let payload = JSON.parse(Buffer.from(signature.split('.')[1], 'base64url').toString()) 48 | 49 | // Header 50 | expect('JWT').toBe(header['typ']) 51 | expect('ES256').toBe(header['alg']) 52 | expect('keyId').toBe(header['kid']) 53 | // Payload 54 | expect('issuerId').toBe(payload['iss']) 55 | expect(payload['iat']).toBeTruthy() 56 | expect(payload['exp']).toBeUndefined() 57 | expect('introductory-offer-eligibility').toBe(payload['aud']) 58 | expect('bundleId').toBe(payload['bid']) 59 | expect(payload['nonce']).toBeTruthy() 60 | expect('productId').toBe(payload['productId']) 61 | expect(true).toBe(payload['allowIntroductoryOffer']) 62 | expect('transactionId').toBe(payload['transactionId']) 63 | }) 64 | 65 | it('should create an Advanced Commerce in app signature', async () => { 66 | const signatureCreator = new AdvancedCommerceInAppSignatureCreator(readFile('tests/resources/certs/testSigningKey.p8'), "keyId", "issuerId", "bundleId"); 67 | let request: TestInAppRequest = { 68 | testValue: "testValue" 69 | } 70 | const signature = signatureCreator.createSignature(request) 71 | expect(signature).toBeTruthy() 72 | let header = JSON.parse(Buffer.from(signature.split('.')[0], 'base64url').toString()) 73 | let payload = JSON.parse(Buffer.from(signature.split('.')[1], 'base64url').toString()) 74 | 75 | // Header 76 | expect('JWT').toBe(header['typ']) 77 | expect('ES256').toBe(header['alg']) 78 | expect('keyId').toBe(header['kid']) 79 | // Payload 80 | expect('issuerId').toBe(payload['iss']) 81 | expect(payload['iat']).toBeTruthy() 82 | expect(payload['exp']).toBeUndefined() 83 | expect('advanced-commerce-api').toBe(payload['aud']) 84 | expect('bundleId').toBe(payload['bid']) 85 | expect(payload['nonce']).toBeTruthy() 86 | let parsedRequestJson = Buffer.from(payload['request'], 'base64').toString('utf-8') 87 | let parsedRequest = JSON.parse(parsedRequestJson) 88 | expect(parsedRequest['testValue']).toBe('testValue') 89 | }) 90 | }) -------------------------------------------------------------------------------- /receipt_utility.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { ASN1HEX } from 'jsrsasign'; 4 | 5 | const IN_APP_TYPE_ID = 17; 6 | const TRANSACTION_IDENTIFIER_TYPE_ID = 1703; 7 | const ORIGINAL_TRANSACTION_IDENTIFIER_TYPE_ID = 1705; 8 | 9 | export class ReceiptUtility { 10 | 11 | /** 12 | * Extracts a transaction id from an encoded App Receipt. Throws if the receipt does not match the expected format. 13 | * *NO validation* is performed on the receipt, and any data returned should only be used to call the App Store Server API. 14 | * @param appReceipt The unmodified app receipt 15 | * @returns A transaction id from the array of in-app purchases, null if the receipt contains no in-app purchases 16 | */ 17 | extractTransactionIdFromAppReceipt(appReceipt: string): string | null { 18 | // Xcode receipts use indefinite length encoding, not supported by all parsers 19 | // Indefinite length encoding is only entered, but never left during parsing for receipts 20 | // We therefore round up indefinite length encoding to be the remaining length 21 | const prevGetVblenFunction = ASN1HEX.getVblen 22 | ASN1HEX.getVblen = function(s, idx) { 23 | const c = ASN1HEX.getL(s, idx) 24 | const oldResult = prevGetVblenFunction(s, idx) 25 | // Round up to the remaining length in the string, measured in bytes (2 hex values per byte) 26 | if (oldResult === 0 && c === '80') { 27 | return (s.length - idx) / 2 28 | } 29 | return oldResult 30 | } 31 | const prevGetLblen = ASN1HEX.getLblen 32 | ASN1HEX.getLblen = function(s, idx) { 33 | const oldResult = prevGetLblen(s, idx) 34 | // The length for the length byte for 80 00 is 1 35 | if (oldResult == -1) { 36 | return 1 37 | } 38 | return oldResult 39 | } 40 | try { 41 | let receiptInfo = ASN1HEX.getVbyList(Buffer.from(appReceipt, 'base64').toString('hex'), 0, [1, 0, 2, 1, 0]) as string 42 | if (receiptInfo.length > 2 && receiptInfo.startsWith('04')) { 43 | // We are still in an Octet String, Xcode wraps with an extra Octet, decode it here 44 | receiptInfo = ASN1HEX.getV(receiptInfo, 0) 45 | } 46 | let index = 0; 47 | while(ASN1HEX.getVbyList(receiptInfo, 0, [index, 0])) { 48 | const val = ASN1HEX.getVbyList(receiptInfo, 0, [index, 0]) as string 49 | if (IN_APP_TYPE_ID === parseInt(val, 16)) { 50 | const inAppInfo = ASN1HEX.getVbyList(receiptInfo, 0, [index, 2]) as string 51 | let inAppIndex = 0; 52 | while(ASN1HEX.getVbyList(inAppInfo, 0, [inAppIndex, 0])) { 53 | const val = ASN1HEX.getVbyList(inAppInfo, 0, [inAppIndex, 0]) as string 54 | if (TRANSACTION_IDENTIFIER_TYPE_ID === parseInt(val, 16) || ORIGINAL_TRANSACTION_IDENTIFIER_TYPE_ID === parseInt(val, 16)) { 55 | const transactionIdUTF8String = ASN1HEX.getVbyList(inAppInfo, 0, [inAppIndex, 2]) as string 56 | const transactionId = ASN1HEX.getVbyList(transactionIdUTF8String, 0, []) as string 57 | return Buffer.from(transactionId, 'hex').toString() 58 | } 59 | inAppIndex = inAppIndex + 1 60 | } 61 | } 62 | index = index + 1 63 | } 64 | return null 65 | } finally { 66 | ASN1HEX.getLblen = prevGetLblen 67 | ASN1HEX.getVblen = prevGetVblenFunction 68 | } 69 | } 70 | 71 | /** 72 | * Extracts a transaction id from an encoded transactional receipt. Throws if the receipt does not match the expected format. 73 | * *NO validation* is performed on the receipt, and any data returned should only be used to call the App Store Server API. 74 | * @param transactionReceipt The unmodified transactionReceipt 75 | * @return A transaction id, or null if no transactionId is found in the receipt 76 | */ 77 | extractTransactionIdFromTransactionReceipt(transactionReceipt: string): string | null { 78 | const topLevel = Buffer.from(transactionReceipt, 'base64').toString() 79 | const topLevelRegex = /"purchase-info"\s+=\s+"([a-zA-Z0-9+/=]+)";/ 80 | const topLevelMatchResult = topLevel.match(topLevelRegex) 81 | if (!topLevelMatchResult || topLevelMatchResult?.length !== 2) { 82 | return null 83 | } 84 | 85 | const purchaseInfo = Buffer.from(topLevelMatchResult[1], 'base64').toString() 86 | const purchaseInfoRegex = /"transaction-id"\s+=\s+"([a-zA-Z0-9+/=]+)";/ 87 | const purchaseInfoMatchResult = purchaseInfo.match(purchaseInfoRegex) 88 | if (!purchaseInfoMatchResult || purchaseInfoMatchResult?.length !== 2) { 89 | return null 90 | } 91 | return purchaseInfoMatchResult[1] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /models/ResponseBodyV2DecodedPayload.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { Data, DataValidator } from "./Data"; 4 | import { DecodedSignedData } from "./DecodedSignedData"; 5 | import { ExternalPurchaseToken, ExternalPurchaseTokenValidator } from "./ExternalPurchaseToken"; 6 | import { NotificationTypeV2, NotificationTypeV2Validator } from "./NotificationTypeV2"; 7 | import { Subtype, SubtypeValidator } from "./Subtype"; 8 | import { Summary, SummaryValidator } from "./Summary"; 9 | import { Validator } from "./Validator"; 10 | 11 | /** 12 | * A decoded payload containing the version 2 notification data. 13 | * 14 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/responsebodyv2decodedpayload ResponseBodyV2DecodedPayload} 15 | */ 16 | export interface ResponseBodyV2DecodedPayload extends DecodedSignedData { 17 | 18 | /** 19 | * The in-app purchase event for which the App Store sends this version 2 notification. 20 | * 21 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/notificationtype notificationType} 22 | **/ 23 | notificationType?: NotificationTypeV2 | string; 24 | 25 | /** 26 | * Additional information that identifies the notification event. The subtype field is present only for specific version 2 notifications. 27 | * 28 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/subtype subtype} 29 | **/ 30 | subtype?: Subtype | string 31 | 32 | /** 33 | * A unique identifier for the notification. 34 | * 35 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/notificationuuid notificationUUID} 36 | **/ 37 | notificationUUID?: string 38 | 39 | /** 40 | * The object that contains the app metadata and signed renewal and transaction information. 41 | * The data, summary, and externalPurchaseToken fields are mutually exclusive. The payload contains only one of these fields. 42 | * 43 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/data data} 44 | **/ 45 | data?: Data 46 | 47 | /** 48 | * A string that indicates the notification’s App Store Server Notifications version number. 49 | * 50 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/version version} 51 | **/ 52 | version?: string 53 | 54 | /** 55 | * The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature data. 56 | * 57 | * {@link https://developer.apple.com/documentation/appstoreserverapi/signeddate signedDate} 58 | **/ 59 | signedDate?: number 60 | 61 | /** 62 | * The summary data that appears when the App Store server completes your request to extend a subscription renewal date for eligible subscribers. 63 | * The data, summary, and externalPurchaseToken fields are mutually exclusive. The payload contains only one of these fields. 64 | * 65 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/summary summary} 66 | **/ 67 | summary?: Summary 68 | 69 | /** 70 | * This field appears when the notificationType is EXTERNAL_PURCHASE_TOKEN. 71 | * The data, summary, and externalPurchaseToken fields are mutually exclusive. The payload contains only one of these fields. 72 | * 73 | * {@link https://developer.apple.com/documentation/appstoreservernotifications/externalpurchasetoken externalPurchaseToken} 74 | **/ 75 | externalPurchaseToken?: ExternalPurchaseToken 76 | } 77 | 78 | 79 | export class ResponseBodyV2DecodedPayloadValidator implements Validator { 80 | static readonly notificationTypeValidator = new NotificationTypeV2Validator() 81 | static readonly subtypeValidator = new SubtypeValidator() 82 | static readonly dataValidator = new DataValidator() 83 | static readonly summaryValidator = new SummaryValidator() 84 | static readonly externalPurchaseTokenValidator = new ExternalPurchaseTokenValidator() 85 | validate(obj: any): obj is ResponseBodyV2DecodedPayload { 86 | if ((typeof obj['notificationType'] !== 'undefined') && !(ResponseBodyV2DecodedPayloadValidator.notificationTypeValidator.validate(obj['notificationType']))) { 87 | return false 88 | } 89 | if ((typeof obj['subtype'] !== 'undefined') && !(ResponseBodyV2DecodedPayloadValidator.subtypeValidator.validate(obj['subtype']))) { 90 | return false 91 | } 92 | if ((typeof obj['notificationUUID'] !== 'undefined') && !(typeof obj['notificationUUID'] === "string" || obj['notificationUUID'] instanceof String)) { 93 | return false 94 | } 95 | if ((typeof obj['data'] !== 'undefined') && !(ResponseBodyV2DecodedPayloadValidator.dataValidator.validate(obj['data']))) { 96 | return false 97 | } 98 | if ((typeof obj['version'] !== 'undefined') && !(typeof obj['version'] === "string" || obj['version'] instanceof String)) { 99 | return false 100 | } 101 | if ((typeof obj['signedDate'] !== 'undefined') && !(typeof obj['signedDate'] === "number")) { 102 | return false 103 | } 104 | if ((typeof obj['summary'] !== 'undefined') && !(ResponseBodyV2DecodedPayloadValidator.summaryValidator.validate(obj['summary']))) { 105 | return false 106 | } 107 | if ((typeof obj['externalPurchaseToken'] !== 'undefined') && !(ResponseBodyV2DecodedPayloadValidator.externalPurchaseTokenValidator.validate(obj['externalPurchaseToken']))) { 108 | return false 109 | } 110 | return true 111 | } 112 | } 113 | 114 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [opensource-conduct@group.apple.com](mailto:opensource-conduct@group.apple.com). 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 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), [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). 3 | 4 | ## Table of Contents 5 | 1. [Installation](#installation) 6 | 2. [Documentation](#documentation) 7 | 3. [Usage](#usage) 8 | 4. [Support](#support) 9 | 10 | ## Installation 11 | 12 | ### Requirements 13 | 14 | - Node 16+ 15 | 16 | ### NPM/Yarn 17 | ```bash 18 | # With NPM 19 | npm install @apple/app-store-server-library --save 20 | # With Yarn 21 | yarn add @apple/app-store-server-library 22 | ``` 23 | 24 | ## Documentation 25 | 26 | [Documentation](https://apple.github.io/app-store-server-library-node/) 27 | 28 | [WWDC Video](https://developer.apple.com/videos/play/wwdc2023/10143/) 29 | 30 | ### Obtaining an In-App Purchase key from App Store Connect 31 | 32 | To use the App Store Server API or create promotional offer signatures, a signing key downloaded from App Store Connect is required. To obtain this key, you must have the Admin role. Go to Users and Access > Integrations > In-App Purchase. Here you can create and manage keys, as well as find your issuer ID. When using a key, you'll need the key ID and issuer ID as well. 33 | 34 | ### Obtaining Apple Root Certificates 35 | 36 | Download and store the root certificates found in the Apple Root Certificates section of the [Apple PKI](https://www.apple.com/certificateauthority/) site. Provide these certificates as an array to a SignedDataVerifier to allow verifying the signed data comes from Apple. 37 | 38 | ## Usage 39 | 40 | ### API Usage 41 | 42 | ```typescript 43 | import { AppStoreServerAPIClient, Environment, SendTestNotificationResponse } from "@apple/app-store-server-library" 44 | 45 | const issuerId = "99b16628-15e4-4668-972b-eeff55eeff55" 46 | const keyId = "ABCDEFGHIJ" 47 | const bundleId = "com.example" 48 | const filePath = "/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8" 49 | const encodedKey = readFile(filePath) // Specific implementation may vary 50 | const environment = Environment.SANDBOX 51 | 52 | const client = new AppStoreServerAPIClient(encodedKey, keyId, issuerId, bundleId, environment) 53 | 54 | try { 55 | const response: SendTestNotificationResponse = await client.requestTestNotification() 56 | console.log(response) 57 | } catch (e) { 58 | console.error(e) 59 | } 60 | ``` 61 | 62 | ### Verification Usage 63 | 64 | ```typescript 65 | import { SignedDataVerifier } from "@apple/app-store-server-library" 66 | 67 | const bundleId = "com.example" 68 | const appleRootCAs: Buffer[] = loadRootCAs() // Specific implementation may vary 69 | const enableOnlineChecks = true 70 | const environment = Environment.SANDBOX 71 | const appAppleId = undefined // appAppleId is required when the environment is Production 72 | const verifier = new SignedDataVerifier(appleRootCAs, enableOnlineChecks, environment, bundleId, appAppleId) 73 | 74 | const notificationPayload = "ey..." 75 | const verifiedNotification = await verifier.verifyAndDecodeNotification(notificationPayload) 76 | console.log(verifiedNotification) 77 | ``` 78 | 79 | ### Receipt Usage 80 | 81 | ```typescript 82 | import { AppStoreServerAPIClient, Environment, GetTransactionHistoryVersion, ReceiptUtility, Order, ProductType, HistoryResponse, TransactionHistoryRequest } from "@apple/app-store-server-library" 83 | 84 | const issuerId = "99b16628-15e4-4668-972b-eeff55eeff55" 85 | const keyId = "ABCDEFGHIJ" 86 | const bundleId = "com.example" 87 | const filePath = "/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8" 88 | const encodedKey = readFile(filePath) // Specific implementation may vary 89 | const environment = Environment.SANDBOX 90 | 91 | const client = 92 | new AppStoreServerAPIClient(encodedKey, keyId, issuerId, bundleId, environment) 93 | 94 | const appReceipt = "MI..." 95 | const receiptUtil = new ReceiptUtility() 96 | const transactionId = receiptUtil.extractTransactionIdFromAppReceipt(appReceipt) 97 | if (transactionId != null) { 98 | const transactionHistoryRequest: TransactionHistoryRequest = { 99 | sort: Order.ASCENDING, 100 | revoked: false, 101 | productTypes: [ProductType.AUTO_RENEWABLE] 102 | } 103 | let response: HistoryResponse | null = null 104 | let transactions: string[] = [] 105 | do { 106 | const revisionToken = response !== null && response.revision !== null ? response.revision : null 107 | response = await client.getTransactionHistory(transactionId, revisionToken, transactionHistoryRequest, GetTransactionHistoryVersion.V2) 108 | if (response.signedTransactions) { 109 | transactions = transactions.concat(response.signedTransactions) 110 | } 111 | } while (response.hasMore) 112 | console.log(transactions) 113 | } 114 | ``` 115 | 116 | ### Promotional Offer Signature Creation 117 | 118 | ```typescript 119 | import { PromotionalOfferSignatureCreator } from "@apple/app-store-server-library" 120 | 121 | const keyId = "ABCDEFGHIJ" 122 | const bundleId = "com.example" 123 | const filePath = "/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8" 124 | const encodedKey = readFile(filePath) // Specific implementation may vary 125 | 126 | const productId = "" 127 | const subscriptionOfferId = "" 128 | const appAccountToken = "" 129 | const nonce = "" 130 | const timestamp = Date.now() 131 | const signatureCreator = new PromotionalOfferSignatureCreator(encodedKey, keyId, bundleId) 132 | 133 | const signature = signatureCreator.createSignature(productId, subscriptionOfferId, appAccountToken, nonce, timestamp) 134 | console.log(signature) 135 | ``` 136 | 137 | ## Support 138 | 139 | Only the latest major version of the library will receive updates, including security updates. Therefore, it is recommended to update to new major versions. 140 | -------------------------------------------------------------------------------- /jws_signature_creator.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | import jsonwebtoken = require('jsonwebtoken'); 4 | import { randomUUID } from 'crypto'; 5 | 6 | class BaseSignatureCreator { 7 | private audience: string 8 | private signingKey: string 9 | private keyId: string 10 | private issuerId: string 11 | private bundleId: string 12 | 13 | public constructor(audience: string, signingKey: string, keyId: string, issuerId: string, bundleId: string) { 14 | this.audience = audience 15 | this.issuerId = issuerId 16 | this.keyId = keyId 17 | this.bundleId = bundleId 18 | this.signingKey = signingKey 19 | } 20 | 21 | protected internalCreateSignature(featureSpecificClaims: { [key: string]: any }) { 22 | var claims = featureSpecificClaims 23 | 24 | claims['bid'] = this.bundleId 25 | claims['nonce'] = randomUUID() 26 | 27 | return jsonwebtoken.sign(claims, this.signingKey, { algorithm: 'ES256', keyid: this.keyId, issuer: this.issuerId, audience: this.audience}) 28 | } 29 | } 30 | 31 | export class PromotionalOfferV2SignatureCreator extends BaseSignatureCreator { 32 | /** 33 | * Create a PromotionalOfferV2SignatureCreator 34 | * 35 | * @param signingKey Your private key downloaded from App Store Connect 36 | * @param keyId Your private key ID from App Store Connect 37 | * @param issuerId Your issuer ID from the Keys page in App Store Connect 38 | * @param bundleId Your app's bundle ID 39 | */ 40 | public constructor(signingKey: string, keyId: string, issuerId: string, bundleId: string) { 41 | super('promotional-offer', signingKey, keyId, issuerId, bundleId) 42 | } 43 | 44 | /** 45 | * Create a promotional offer V2 signature. 46 | * 47 | * @param productId The unique identifier of the product 48 | * @param offerIdentifier The promotional offer identifier that you set up in App Store Connect 49 | * @param transactionId The unique identifier of any transaction that belongs to the customer. You can use the customer's appTransactionId, even for customers who haven't made any In-App Purchases in your app. This field is optional, but recommended. 50 | * @return The signed JWS. 51 | * {@link https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests Generating JWS to sign App Store requests} 52 | */ 53 | public createSignature(productId: string, offerIdentifier: string, transactionId: string | undefined = undefined) { 54 | let featureSpecificClaims: { [key: string]: any } = {} 55 | featureSpecificClaims['productId'] = productId 56 | featureSpecificClaims['offerIdentifier'] = offerIdentifier 57 | if (transactionId != null) { 58 | featureSpecificClaims['transactionId'] = transactionId 59 | } 60 | return super.internalCreateSignature(featureSpecificClaims) 61 | } 62 | } 63 | 64 | export class IntroductoryOfferEligibilitySignatureCreator extends BaseSignatureCreator { 65 | /** 66 | * Create a IntroductoryOfferEligibilitySignatureCreator 67 | * 68 | * @param signingKey Your private key downloaded from App Store Connect 69 | * @param keyId Your private key ID from App Store Connect 70 | * @param issuerId Your issuer ID from the Keys page in App Store Connect 71 | * @param bundleId Your app's bundle ID 72 | */ 73 | public constructor(signingKey: string, keyId: string, issuerId: string, bundleId: string) { 74 | super('introductory-offer-eligibility', signingKey, keyId, issuerId, bundleId) 75 | } 76 | 77 | /** 78 | * Create an introductory offer eligibility signature. 79 | * 80 | * @param productId The unique identifier of the product 81 | * @param allowIntroductoryOffer A boolean value that determines whether the customer is eligible for an introductory offer 82 | * @param transactionId The unique identifier of any transaction that belongs to the customer. You can use the customer's appTransactionId, even for customers who haven't made any In-App Purchases in your app. 83 | * @return The signed JWS. 84 | * {@link https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests Generating JWS to sign App Store requests} 85 | */ 86 | public createSignature(productId: string, allowIntroductoryOffer: boolean, transactionId: string) { 87 | let featureSpecificClaims: { [key: string]: any } = {} 88 | featureSpecificClaims['productId'] = productId 89 | featureSpecificClaims['allowIntroductoryOffer'] = allowIntroductoryOffer 90 | featureSpecificClaims['transactionId'] = transactionId 91 | return super.internalCreateSignature(featureSpecificClaims) 92 | } 93 | } 94 | 95 | export interface AdvancedCommerceInAppRequest {} 96 | 97 | export class AdvancedCommerceInAppSignatureCreator extends BaseSignatureCreator { 98 | /** 99 | * Create a AdvancedCommerceInAppSignatureCreator 100 | * 101 | * @param signingKey Your private key downloaded from App Store Connect 102 | * @param keyId Your private key ID from App Store Connect 103 | * @param issuerId Your issuer ID from the Keys page in App Store Connect 104 | * @param bundleId Your app's bundle ID 105 | */ 106 | public constructor(signingKey: string, keyId: string, issuerId: string, bundleId: string) { 107 | super('advanced-commerce-api', signingKey, keyId, issuerId, bundleId) 108 | } 109 | 110 | /** 111 | * Create an Advanced Commerce in-app signed request. 112 | * 113 | * @param AdvancedCommerceInAppRequest The request to be signed. 114 | * @return The signed JWS. 115 | * {@link https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests Generating JWS to sign App Store requests} 116 | */ 117 | public createSignature(AdvancedCommerceInAppRequest: AdvancedCommerceInAppRequest) { 118 | let jsonRequest = JSON.stringify(AdvancedCommerceInAppRequest) 119 | 120 | let base64Request = Buffer.from(jsonRequest, 'utf-8').toString('base64') 121 | 122 | let featureSpecificClaims: { [key: string]: string } = {} 123 | featureSpecificClaims['request'] = base64Request 124 | return super.internalCreateSignature(featureSpecificClaims) 125 | } 126 | } -------------------------------------------------------------------------------- /models/AppTransaction.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import { Environment, EnvironmentValidator } from "./Environment" 4 | import { PurchasePlatform, PurchasePlatformValidator } from "./PurchasePlatform" 5 | import { Validator } from "./Validator" 6 | 7 | /** 8 | * Information that represents the customer’s purchase of the app, cryptographically signed by the App Store. 9 | * 10 | * {@link https://developer.apple.com/documentation/storekit/apptransaction AppTransaction} 11 | */ 12 | export interface AppTransaction { 13 | 14 | /** 15 | * The server environment that signs the app transaction. 16 | * 17 | * {@link https://developer.apple.com/documentation/storekit/apptransaction/3963901-environment environment} 18 | */ 19 | receiptType?: Environment | string 20 | 21 | /** 22 | * The unique identifier the App Store uses to identify the app. 23 | * 24 | * {@link https://developer.apple.com/documentation/storekit/apptransaction/3954436-appid appId} 25 | */ 26 | appAppleId?: number 27 | 28 | /** 29 | * The bundle identifier that the app transaction applies to. 30 | * 31 | * {@link https://developer.apple.com/documentation/storekit/apptransaction/3954439-bundleid bundleId} 32 | */ 33 | bundleId?: string 34 | 35 | /** 36 | * The app version that the app transaction applies to. 37 | * 38 | * {@link https://developer.apple.com/documentation/storekit/apptransaction/3954437-appversion appVersion} 39 | */ 40 | applicationVersion?: string 41 | 42 | /** 43 | * The version external identifier of the app 44 | * 45 | * {@link https://developer.apple.com/documentation/storekit/apptransaction/3954438-appversionid appVersionID} 46 | */ 47 | versionExternalIdentifier?: number 48 | 49 | /** 50 | * The date that the App Store signed the JWS app transaction. 51 | * 52 | * {@link https://developer.apple.com/documentation/storekit/apptransaction/3954449-signeddate signedDate} 53 | */ 54 | receiptCreationDate?: number 55 | 56 | /** 57 | * The date the user originally purchased the app from the App Store. 58 | * 59 | * {@link https://developer.apple.com/documentation/storekit/apptransaction/3954448-originalpurchasedate originalPurchaseDate} 60 | */ 61 | originalPurchaseDate?: number 62 | 63 | /** 64 | * The app version that the user originally purchased from the App Store. 65 | * 66 | * {@link https://developer.apple.com/documentation/storekit/apptransaction/3954447-originalappversion originalAppVersion} 67 | */ 68 | originalApplicationVersion?: string 69 | 70 | /** 71 | The Base64 device verification value to use to verify whether the app transaction belongs to the device. 72 | 73 | {@link https://developer.apple.com/documentation/storekit/apptransaction/3954441-deviceverification deviceVerification} 74 | */ 75 | deviceVerification?: string 76 | 77 | /** 78 | * The UUID used to compute the device verification value. 79 | * 80 | * {@link https://developer.apple.com/documentation/storekit/apptransaction/3954442-deviceverificationnonce deviceVerificationNonce} 81 | */ 82 | deviceVerificationNonce?: string 83 | 84 | /** 85 | * The date the customer placed an order for the app before it’s available in the App Store. 86 | * 87 | * {@link https://developer.apple.com/documentation/storekit/apptransaction/4013175-preorderdate preorderDate} 88 | */ 89 | preorderDate?: number 90 | 91 | /** 92 | * The unique identifier of the app download transaction. 93 | * 94 | * {@link https://developer.apple.com/documentation/storekit/apptransaction/apptransactionid appTransactionId} 95 | */ 96 | appTransactionId?: string 97 | 98 | /** 99 | * The platform on which the customer originally purchased the app. 100 | * 101 | * {@link https://developer.apple.com/documentation/storekit/apptransaction/originalplatform-4mogz originalPlatform} 102 | */ 103 | originalPlatform?: PurchasePlatform | string 104 | } 105 | 106 | export class AppTransactionValidator implements Validator { 107 | static readonly environmentValidator = new EnvironmentValidator() 108 | static readonly originalPlatformValidator = new PurchasePlatformValidator() 109 | validate(obj: any): obj is AppTransaction { 110 | if ((typeof obj['appAppleId'] !== 'undefined') && !(typeof obj['appAppleId'] === "number")) { 111 | return false 112 | } 113 | if ((typeof obj['bundleId'] !== 'undefined') && !(typeof obj['bundleId'] === "string" || obj['bundleId'] instanceof String)) { 114 | return false 115 | } 116 | if ((typeof obj['applicationVersion'] !== 'undefined') && !(typeof obj['applicationVersion'] === "string" || obj['applicationVersion'] instanceof String)) { 117 | return false 118 | } 119 | if ((typeof obj['versionExternalIdentifier'] !== 'undefined') && !(typeof obj['versionExternalIdentifier'] === "number")) { 120 | return false 121 | } 122 | if ((typeof obj['receiptCreationDate'] !== 'undefined') && !(typeof obj['receiptCreationDate'] === "number")) { 123 | return false 124 | } 125 | if ((typeof obj['originalPurchaseDate'] !== 'undefined') && !(typeof obj['originalPurchaseDate'] === "number")) { 126 | return false 127 | } 128 | if ((typeof obj['originalApplicationVersion'] !== 'undefined') && !(typeof obj['originalApplicationVersion'] === "string" || obj['originalApplicationVersion'] instanceof String)) { 129 | return false 130 | } 131 | if ((typeof obj['deviceVerification'] !== 'undefined') && !(typeof obj['deviceVerification'] === "string" || obj['deviceVerification'] instanceof String)) { 132 | return false 133 | } 134 | if ((typeof obj['deviceVerificationNonce'] !== 'undefined') && !(typeof obj['deviceVerificationNonce'] === "string" || obj['deviceVerificationNonce'] instanceof String)) { 135 | return false 136 | } 137 | if ((typeof obj['preorderDate'] !== 'undefined') && !(typeof obj['preorderDate'] === "number")) { 138 | return false 139 | } 140 | if ((typeof obj['environment'] !== 'undefined') && !(AppTransactionValidator.environmentValidator.validate(obj['environment']))) { 141 | return false 142 | } 143 | if ((typeof obj['appTransactionId'] !== 'undefined') && !(typeof obj['appTransactionId'] === "string" || obj['appTransactionId'] instanceof String)) { 144 | return false 145 | } 146 | if ((typeof obj['originalPlatform'] !== 'undefined') && !(AppTransactionValidator.originalPlatformValidator.validate(obj['originalPlatform']))) { 147 | return false 148 | } 149 | return true 150 | } 151 | } -------------------------------------------------------------------------------- /tests/unit-tests/RealtimeResponseBody.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | import { RealtimeResponseBody } from "../../models/RealtimeResponseBody"; 4 | import { Message } from "../../models/Message"; 5 | import { AlternateProduct } from "../../models/AlternateProduct"; 6 | import { PromotionalOffer } from "../../models/PromotionalOffer"; 7 | import { PromotionalOfferSignatureV1 } from "../../models/PromotionalOfferSignatureV1"; 8 | 9 | describe('RealtimeResponseBody', () => { 10 | it('should serialize RealtimeResponseBody with Message', () => { 11 | // Create a RealtimeResponseBody with a Message 12 | const messageId = "a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890" 13 | const message: Message = { 14 | messageIdentifier: messageId 15 | } 16 | const responseBody: RealtimeResponseBody = { 17 | message: message 18 | } 19 | 20 | // Serialize to JSON 21 | const json = JSON.stringify(responseBody) 22 | const jsonObj = JSON.parse(json) 23 | 24 | // Validate JSON structure 25 | expect(jsonObj).toHaveProperty("message") 26 | expect(jsonObj.message).toHaveProperty("messageIdentifier") 27 | expect(jsonObj.message.messageIdentifier).toBe("a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890") 28 | expect(jsonObj).not.toHaveProperty("alternateProduct") 29 | expect(jsonObj).not.toHaveProperty("promotionalOffer") 30 | 31 | // Deserialize back 32 | const deserialized: RealtimeResponseBody = JSON.parse(json) 33 | 34 | // Verify 35 | expect(deserialized.message).toBeDefined() 36 | expect(deserialized.message?.messageIdentifier).toBe(messageId) 37 | expect(deserialized.alternateProduct).toBeUndefined() 38 | expect(deserialized.promotionalOffer).toBeUndefined() 39 | }) 40 | 41 | it('should serialize RealtimeResponseBody with AlternateProduct', () => { 42 | // Create a RealtimeResponseBody with an AlternateProduct 43 | const messageId = "b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901" 44 | const productId = "com.example.alternate.product" 45 | const alternateProduct: AlternateProduct = { 46 | messageIdentifier: messageId, 47 | productId: productId 48 | } 49 | const responseBody: RealtimeResponseBody = { 50 | alternateProduct: alternateProduct 51 | } 52 | 53 | // Serialize to JSON 54 | const json = JSON.stringify(responseBody) 55 | const jsonObj = JSON.parse(json) 56 | 57 | // Validate JSON structure 58 | expect(jsonObj).toHaveProperty("alternateProduct") 59 | expect(jsonObj.alternateProduct).toHaveProperty("messageIdentifier") 60 | expect(jsonObj.alternateProduct).toHaveProperty("productId") 61 | expect(jsonObj.alternateProduct.messageIdentifier).toBe("b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901") 62 | expect(jsonObj.alternateProduct.productId).toBe("com.example.alternate.product") 63 | expect(jsonObj).not.toHaveProperty("message") 64 | expect(jsonObj).not.toHaveProperty("promotionalOffer") 65 | 66 | // Deserialize back 67 | const deserialized: RealtimeResponseBody = JSON.parse(json) 68 | 69 | // Verify 70 | expect(deserialized.message).toBeUndefined() 71 | expect(deserialized.alternateProduct).toBeDefined() 72 | expect(deserialized.alternateProduct?.messageIdentifier).toBe(messageId) 73 | expect(deserialized.alternateProduct?.productId).toBe(productId) 74 | expect(deserialized.promotionalOffer).toBeUndefined() 75 | }) 76 | 77 | it('should serialize RealtimeResponseBody with PromotionalOffer V2', () => { 78 | // Create a RealtimeResponseBody with a PromotionalOffer (V2 signature) 79 | const messageId = "c3d4e5f6-a789-0123-c3d4-e5f6a7890123" 80 | const signatureV2 = "signature2" 81 | const promotionalOffer: PromotionalOffer = { 82 | messageIdentifier: messageId, 83 | promotionalOfferSignatureV2: signatureV2 84 | } 85 | const responseBody: RealtimeResponseBody = { 86 | promotionalOffer: promotionalOffer 87 | } 88 | 89 | // Serialize to JSON 90 | const json = JSON.stringify(responseBody) 91 | const jsonObj = JSON.parse(json) 92 | 93 | // Validate JSON structure 94 | expect(jsonObj).toHaveProperty("promotionalOffer") 95 | expect(jsonObj.promotionalOffer).toHaveProperty("messageIdentifier") 96 | expect(jsonObj.promotionalOffer).toHaveProperty("promotionalOfferSignatureV2") 97 | expect(jsonObj.promotionalOffer.messageIdentifier).toBe("c3d4e5f6-a789-0123-c3d4-e5f6a7890123") 98 | expect(jsonObj.promotionalOffer.promotionalOfferSignatureV2).toBe("signature2") 99 | expect(jsonObj.promotionalOffer).not.toHaveProperty("promotionalOfferSignatureV1") 100 | expect(jsonObj).not.toHaveProperty("message") 101 | expect(jsonObj).not.toHaveProperty("alternateProduct") 102 | 103 | // Deserialize back 104 | const deserialized: RealtimeResponseBody = JSON.parse(json) 105 | 106 | // Verify 107 | expect(deserialized.message).toBeUndefined() 108 | expect(deserialized.alternateProduct).toBeUndefined() 109 | expect(deserialized.promotionalOffer).toBeDefined() 110 | expect(deserialized.promotionalOffer?.messageIdentifier).toBe(messageId) 111 | expect(deserialized.promotionalOffer?.promotionalOfferSignatureV2).toBe(signatureV2) 112 | expect(deserialized.promotionalOffer?.promotionalOfferSignatureV1).toBeUndefined() 113 | }) 114 | 115 | it('should serialize RealtimeResponseBody with PromotionalOffer V1', () => { 116 | // Create a RealtimeResponseBody with a PromotionalOffer (V1 signature) 117 | const messageId = "d4e5f6a7-8901-2345-d4e5-f6a789012345" 118 | const nonce = "e5f6a789-0123-4567-e5f6-a78901234567" 119 | const appAccountToken = "f6a78901-2345-6789-f6a7-890123456789" 120 | const signatureV1: PromotionalOfferSignatureV1 = { 121 | encodedSignature: "base64encodedSignature", 122 | productId: "com.example.product", 123 | nonce: nonce, 124 | timestamp: 1698148900000, 125 | keyId: "keyId123", 126 | offerIdentifier: "offer123", 127 | appAccountToken: appAccountToken 128 | } 129 | 130 | const promotionalOffer: PromotionalOffer = { 131 | messageIdentifier: messageId, 132 | promotionalOfferSignatureV1: signatureV1 133 | } 134 | const responseBody: RealtimeResponseBody = { 135 | promotionalOffer: promotionalOffer 136 | } 137 | 138 | // Serialize to JSON 139 | const json = JSON.stringify(responseBody) 140 | const jsonObj = JSON.parse(json) 141 | 142 | // Validate JSON structure 143 | expect(jsonObj).toHaveProperty("promotionalOffer") 144 | expect(jsonObj.promotionalOffer).toHaveProperty("messageIdentifier") 145 | expect(jsonObj.promotionalOffer).toHaveProperty("promotionalOfferSignatureV1") 146 | expect(jsonObj.promotionalOffer.messageIdentifier).toBe("d4e5f6a7-8901-2345-d4e5-f6a789012345") 147 | 148 | const v1Node = jsonObj.promotionalOffer.promotionalOfferSignatureV1 149 | expect(v1Node).toHaveProperty("encodedSignature") 150 | expect(v1Node).toHaveProperty("productId") 151 | expect(v1Node).toHaveProperty("nonce") 152 | expect(v1Node).toHaveProperty("timestamp") 153 | expect(v1Node).toHaveProperty("keyId") 154 | expect(v1Node).toHaveProperty("offerIdentifier") 155 | expect(v1Node).toHaveProperty("appAccountToken") 156 | expect(v1Node.encodedSignature).toBe("base64encodedSignature") 157 | expect(v1Node.productId).toBe("com.example.product") 158 | expect(v1Node.nonce).toBe("e5f6a789-0123-4567-e5f6-a78901234567") 159 | expect(v1Node.timestamp).toBe(1698148900000) 160 | expect(v1Node.keyId).toBe("keyId123") 161 | expect(v1Node.offerIdentifier).toBe("offer123") 162 | expect(v1Node.appAccountToken).toBe("f6a78901-2345-6789-f6a7-890123456789") 163 | 164 | expect(jsonObj.promotionalOffer).not.toHaveProperty("promotionalOfferSignatureV2") 165 | expect(jsonObj).not.toHaveProperty("message") 166 | expect(jsonObj).not.toHaveProperty("alternateProduct") 167 | 168 | // Deserialize back 169 | const deserialized: RealtimeResponseBody = JSON.parse(json) 170 | 171 | // Verify 172 | expect(deserialized.message).toBeUndefined() 173 | expect(deserialized.alternateProduct).toBeUndefined() 174 | expect(deserialized.promotionalOffer).toBeDefined() 175 | expect(deserialized.promotionalOffer?.messageIdentifier).toBe(messageId) 176 | expect(deserialized.promotionalOffer?.promotionalOfferSignatureV2).toBeUndefined() 177 | expect(deserialized.promotionalOffer?.promotionalOfferSignatureV1).toBeDefined() 178 | 179 | const deserializedV1 = deserialized.promotionalOffer?.promotionalOfferSignatureV1 180 | expect(deserializedV1?.productId).toBe("com.example.product") 181 | expect(deserializedV1?.offerIdentifier).toBe("offer123") 182 | expect(deserializedV1?.nonce).toBe(nonce) 183 | expect(deserializedV1?.timestamp).toBe(1698148900000) 184 | expect(deserializedV1?.keyId).toBe("keyId123") 185 | expect(deserializedV1?.appAccountToken).toBe(appAccountToken) 186 | expect(deserializedV1?.encodedSignature).toBe("base64encodedSignature") 187 | }) 188 | 189 | it('should serialize RealtimeResponseBody with correct field names', () => { 190 | // Test that JSON serialization uses correct field names 191 | const messageId = "12345678-1234-1234-1234-123456789012" 192 | const message: Message = { 193 | messageIdentifier: messageId 194 | } 195 | const responseBody: RealtimeResponseBody = { 196 | message: message 197 | } 198 | 199 | const json = JSON.stringify(responseBody) 200 | 201 | // Verify JSON contains correct field names 202 | expect(json).toContain('"message"') 203 | expect(json).toContain('"messageIdentifier"') 204 | expect(json).toContain('"12345678-1234-1234-1234-123456789012"') 205 | }) 206 | }) 207 | --------------------------------------------------------------------------------