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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The Node.js server library for the [App Store Server API](https://developer.appl

## Installation

#### Requirements
### Requirements

- Node 16+

Expand Down Expand Up @@ -125,12 +125,12 @@ const encodedKey = readFile(filePath) // Specific implementation may vary

const productId = "<product_id>"
const subscriptionOfferId = "<subscription_offer_id>"
const applicationUsername = "<application_username>"
const appAccountToken = "<app_account_token>"
const nonce = "<nonce>"
const timestamp = Date.now()
const signatureCreator = new PromotionalOfferSignatureCreator(encodedKey, keyId, bundleId)

const signature = signatureCreator.createSignature(productId, subscriptionOfferId, applicationUsername, nonce, timestamp)
const signature = signatureCreator.createSignature(productId, subscriptionOfferId, appAccountToken, nonce, timestamp)
console.log(signature)
```

Expand Down
9 changes: 9 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export { OrderLookupStatus } from './models/OrderLookupStatus'
export { Platform } from './models/Platform'
export { PlayTime } from './models/PlayTime'
export { PriceIncreaseStatus } from './models/PriceIncreaseStatus'
export { PurchasePlatform } from './models/PurchasePlatform'
export { RefundHistoryResponse } from './models/RefundHistoryResponse'
export { ResponseBodyV2 } from './models/ResponseBodyV2'
export { ResponseBodyV2DecodedPayload } from './models/ResponseBodyV2DecodedPayload'
Expand All @@ -71,6 +72,7 @@ export { TransactionReason } from './models/TransactionReason'
export { Type } from './models/Type'
export { UserStatus } from './models/UserStatus'
export { PromotionalOfferSignatureCreator } from './promotional_offer'
export { PromotionalOfferV2SignatureCreator, AdvancedCommerceInAppSignatureCreator, AdvancedCommerceInAppRequest, IntroductoryOfferEligibilitySignatureCreator } from './jws_signature_creator'
export { DecodedSignedData } from './models/DecodedSignedData'
export { AppTransaction } from './models/AppTransaction'

Expand Down Expand Up @@ -685,6 +687,13 @@ export enum APIError {
*/
INVALID_TRANSACTION_TYPE_NOT_SUPPORTED = 4000047,

/**
* An error that indicates the endpoint doesn't support an app transaction ID.
*
* {@link https://developer.apple.com/documentation/appstoreserverapi/apptransactionidnotsupportederror AppTransactionIdNotSupportedError}
*/
APP_TRANSACTION_ID_NOT_SUPPORTED_ERROR = 4000048,

/**
* An error that indicates the subscription doesn't qualify for a renewal-date extension due to its subscription state.
*
Expand Down
126 changes: 126 additions & 0 deletions jws_signature_creator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.

import jsonwebtoken = require('jsonwebtoken');
import { randomUUID } from 'crypto';

class BaseSignatureCreator {
private audience: string
private signingKey: string
private keyId: string
private issuerId: string
private bundleId: string

public constructor(audience: string, signingKey: string, keyId: string, issuerId: string, bundleId: string) {
this.audience = audience
this.issuerId = issuerId
this.keyId = keyId
this.bundleId = bundleId
this.signingKey = signingKey
}

protected internalCreateSignature(featureSpecificClaims: { [key: string]: any }) {
var claims = featureSpecificClaims

claims['bid'] = this.bundleId
claims['nonce'] = randomUUID()

return jsonwebtoken.sign(claims, this.signingKey, { algorithm: 'ES256', keyid: this.keyId, issuer: this.issuerId, audience: this.audience})
}
}

export class PromotionalOfferV2SignatureCreator extends BaseSignatureCreator {
/**
* Create a PromotionalOfferV2SignatureCreator
*
* @param signingKey Your private key downloaded from App Store Connect
* @param keyId Your private key ID from App Store Connect
* @param issuerId Your issuer ID from the Keys page in App Store Connect
* @param bundleId Your app's bundle ID
*/
public constructor(signingKey: string, keyId: string, issuerId: string, bundleId: string) {
super('promotional-offer', signingKey, keyId, issuerId, bundleId)
}

/**
* Create a promotional offer V2 signature.
*
* @param productId The unique identifier of the product
* @param offerIdentifier The promotional offer identifier that you set up in App Store Connect
* @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.
* @return The signed JWS.
* {@link https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests Generating JWS to sign App Store requests}
*/
public createSignature(productId: string, offerIdentifier: string, transactionId: string | undefined = undefined) {
let featureSpecificClaims: { [key: string]: any } = {}
featureSpecificClaims['productId'] = productId
featureSpecificClaims['offerIdentifier'] = offerIdentifier
if (transactionId != null) {
featureSpecificClaims['transactionId'] = transactionId
}
return super.internalCreateSignature(featureSpecificClaims)
}
}

export class IntroductoryOfferEligibilitySignatureCreator extends BaseSignatureCreator {
/**
* Create a IntroductoryOfferEligibilitySignatureCreator
*
* @param signingKey Your private key downloaded from App Store Connect
* @param keyId Your private key ID from App Store Connect
* @param issuerId Your issuer ID from the Keys page in App Store Connect
* @param bundleId Your app's bundle ID
*/
public constructor(signingKey: string, keyId: string, issuerId: string, bundleId: string) {
super('introductory-offer-eligibility', signingKey, keyId, issuerId, bundleId)
}

/**
* Create an introductory offer eligibility signature.
*
* @param productId The unique identifier of the product
* @param allowIntroductoryOffer A boolean value that determines whether the customer is eligible for an introductory offer
* @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.
* @return The signed JWS.
* {@link https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests Generating JWS to sign App Store requests}
*/
public createSignature(productId: string, allowIntroductoryOffer: boolean, transactionId: string) {
let featureSpecificClaims: { [key: string]: any } = {}
featureSpecificClaims['productId'] = productId
featureSpecificClaims['allowIntroductoryOffer'] = allowIntroductoryOffer
featureSpecificClaims['transactionId'] = transactionId
return super.internalCreateSignature(featureSpecificClaims)
}
}

export interface AdvancedCommerceInAppRequest {}

export class AdvancedCommerceInAppSignatureCreator extends BaseSignatureCreator {
/**
* Create a AdvancedCommerceInAppSignatureCreator
*
* @param signingKey Your private key downloaded from App Store Connect
* @param keyId Your private key ID from App Store Connect
* @param issuerId Your issuer ID from the Keys page in App Store Connect
* @param bundleId Your app's bundle ID
*/
public constructor(signingKey: string, keyId: string, issuerId: string, bundleId: string) {
super('advanced-commerce-api', signingKey, keyId, issuerId, bundleId)
}

/**
* Create an Advanced Commerce in-app signed request.
*
* @param AdvancedCommerceInAppRequest The request to be signed.
* @return The signed JWS.
* {@link https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests Generating JWS to sign App Store requests}
*/
public createSignature(AdvancedCommerceInAppRequest: AdvancedCommerceInAppRequest) {
let jsonRequest = JSON.stringify(AdvancedCommerceInAppRequest)

let base64Request = Buffer.from(jsonRequest, 'utf-8').toString('base64')

let featureSpecificClaims: { [key: string]: string } = {}
featureSpecificClaims['request'] = base64Request
return super.internalCreateSignature(featureSpecificClaims)
}
}
22 changes: 22 additions & 0 deletions models/AppTransaction.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) 2023 Apple Inc. Licensed under MIT License.

import { Environment, EnvironmentValidator } from "./Environment"
import { PurchasePlatform, PurchasePlatformValidator } from "./PurchasePlatform"
import { Validator } from "./Validator"

/**
Expand Down Expand Up @@ -86,10 +87,25 @@ export interface AppTransaction {
* {@link https://developer.apple.com/documentation/storekit/apptransaction/4013175-preorderdate preorderDate}
*/
preorderDate?: number

/**
* The unique identifier of the app download transaction.
*
* {@link https://developer.apple.com/documentation/storekit/apptransaction/apptransactionid appTransactionId}
*/
appTransactionId?: string

/**
* The platform on which the customer originally purchased the app.
*
* {@link https://developer.apple.com/documentation/storekit/apptransaction/originalplatform-4mogz originalPlatform}
*/
originalPlatform?: PurchasePlatform | string
}

export class AppTransactionValidator implements Validator<AppTransaction> {
static readonly environmentValidator = new EnvironmentValidator()
static readonly originalPlatformValidator = new PurchasePlatformValidator()
validate(obj: any): obj is AppTransaction {
if ((typeof obj['appAppleId'] !== 'undefined') && !(typeof obj['appAppleId'] === "number")) {
return false
Expand Down Expand Up @@ -124,6 +140,12 @@ export class AppTransactionValidator implements Validator<AppTransaction> {
if ((typeof obj['environment'] !== 'undefined') && !(AppTransactionValidator.environmentValidator.validate(obj['environment']))) {
return false
}
if ((typeof obj['appTransactionId'] !== 'undefined') && !(typeof obj['appTransactionId'] === "string" || obj['appTransactionId'] instanceof String)) {
return false
}
if ((typeof obj['originalPlatform'] !== 'undefined') && !(AppTransactionValidator.originalPlatformValidator.validate(obj['originalPlatform']))) {
return false
}
return true
}
}
32 changes: 31 additions & 1 deletion models/JWSRenewalInfoDecodedPayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,28 @@ export interface JWSRenewalInfoDecodedPayload extends DecodedSignedData {
*
* {@link https://developer.apple.com/documentation/appstoreserverapi/eligiblewinbackofferids eligibleWinBackOfferIds}
**/
eligibleWinBackOfferIds?: string[];
eligibleWinBackOfferIds?: string[]

/**
* The UUID that an app optionally generates to map a customer’s in-app purchase with its resulting App Store transaction.
*
* {@link https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken appAccountToken}
**/
appAccountToken?: string

/**
* The unique identifier of the app download transaction.
*
* {@link https://developer.apple.com/documentation/appstoreserverapi/appTransactionId appTransactionId}
**/
appTransactionId?: string

/**
* The duration of the offer.
*
* {@link https://developer.apple.com/documentation/appstoreserverapi/offerPeriod offerPeriod}
**/
offerPeriod?: string
}


Expand Down Expand Up @@ -213,6 +234,15 @@ export class JWSRenewalInfoDecodedPayloadValidator implements Validator<JWSRenew
}
}
}
if ((typeof obj['appAccountToken'] !== 'undefined') && !(typeof obj['appAccountToken'] === "string" || obj['appAccountToken'] instanceof String)) {
return false
}
if ((typeof obj['appTransactionId'] !== 'undefined') && !(typeof obj['appTransactionId'] === "string" || obj['appTransactionId'] instanceof String)) {
return false
}
if ((typeof obj['offerPeriod'] !== 'undefined') && !(typeof obj['offerPeriod'] === "string" || obj['offerPeriod'] instanceof String)) {
return false
}
return true
}
}
20 changes: 20 additions & 0 deletions models/JWSTransactionDecodedPayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,20 @@ export interface JWSTransactionDecodedPayload extends DecodedSignedData {
* {@link https://developer.apple.com/documentation/appstoreserverapi/offerdiscounttype offerDiscountType}
**/
offerDiscountType?: OfferDiscountType | string

/**
* The unique identifier of the app download transaction.
*
* {@link https://developer.apple.com/documentation/appstoreserverapi/appTransactionId appTransactionId}
**/
appTransactionId?: string

/**
* The duration of the offer.
*
* {@link https://developer.apple.com/documentation/appstoreserverapi/offerPeriod offerPeriod}
**/
offerPeriod?: string
}


Expand Down Expand Up @@ -288,6 +302,12 @@ export class JWSTransactionDecodedPayloadValidator implements Validator<JWSTrans
if ((typeof obj['offerDiscountType'] !== 'undefined') && !(JWSTransactionDecodedPayloadValidator.offerDiscountTypeValidator.validate(obj['offerDiscountType']))) {
return false
}
if ((typeof obj['appTransactionId'] !== 'undefined') && !(typeof obj['appTransactionId'] === "string" || obj['appTransactionId'] instanceof String)) {
return false
}
if ((typeof obj['offerPeriod'] !== 'undefined') && !(typeof obj['offerPeriod'] === "string" || obj['offerPeriod'] instanceof String)) {
return false
}
return true
}
}
17 changes: 17 additions & 0 deletions models/PurchasePlatform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.

import { StringValidator } from "./Validator";

/**
* Values that represent Apple platforms.
*
* {@link https://developer.apple.com/documentation/storekit/appstore/platform AppStore.Platform}
*/
export enum PurchasePlatform {
IOS = "iOS",
MAC_OS = "macOS",
TV_OS = "tvOS",
VISION_OS = "visionOS"
}

export class PurchasePlatformValidator extends StringValidator {}
6 changes: 3 additions & 3 deletions promotional_offer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@ export class PromotionalOfferSignatureCreator {
* {@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}
* @param productIdentifier The subscription product identifier
* @param subscriptionOfferID The subscription discount identifier
* @param applicationUsername An optional string value that you define; may be an empty string
* @param appAccountToken An optional string value that you define; may be an empty string
* @param nonce A one-time UUID value that your server generates. Generate a new nonce for every signature.
* @param timestamp A timestamp your server generates in UNIX time format, in milliseconds. The timestamp keeps the offer active for 24 hours.
* @return The Base64 encoded signature
*/
public createSignature(productIdentifier: string, subscriptionOfferID: string, applicationUsername: string, nonce: string, timestamp: number): string {
public createSignature(productIdentifier: string, subscriptionOfferID: string, appAccountToken: string, nonce: string, timestamp: number): string {
const payload = this.bundleId + '\u2063' +
this.keyId + '\u2063' +
productIdentifier + '\u2063' +
subscriptionOfferID + '\u2063' +
applicationUsername.toLowerCase() + '\u2063'+
appAccountToken.toLowerCase() + '\u2063'+
nonce.toLowerCase() + '\u2063' +
timestamp;
const sign = createSign('SHA256')
Expand Down
4 changes: 3 additions & 1 deletion tests/resources/models/appTransaction.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@
"originalApplicationVersion": "1.1.2",
"deviceVerification": "device_verification_value",
"deviceVerificationNonce": "48ccfa42-7431-4f22-9908-7e88983e105a",
"preorderDate": 1698148700000
"preorderDate": 1698148700000,
"appTransactionId": "71134",
"originalPlatform": "iOS"
}
5 changes: 4 additions & 1 deletion tests/resources/models/signedRenewalInfo.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,8 @@
"eligibleWinBackOfferIds": [
"eligible1",
"eligible2"
]
],
"appTransactionId": "71134",
"offerPeriod": "P1Y",
"appAccountToken": "7e3fb20b-4cdb-47cc-936d-99d65f608138"
}
4 changes: 3 additions & 1 deletion tests/resources/models/signedTransaction.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,7 @@
"storefrontId":"143441",
"price": 10990,
"currency": "USD",
"offerDiscountType": "PAY_AS_YOU_GO"
"offerDiscountType": "PAY_AS_YOU_GO",
"appTransactionId": "71134",
"offerPeriod": "P1Y"
}
Loading