← All @molecule/* packages · App templates
@molecule/api-payments-appleProvider bond · payments · API (Node) · v1.0.1 · Apache-2.0
Apple In-App Purchase provider for molecule.dev.
npm install @molecule/api-payments-applenpm · Source on GitHub · Implements @molecule/api-payments
@molecule/api-payments-apple is a provider bond on the API (Node) side: it implements the payments core interface (@molecule/api-payments) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
Works with: @molecule/api-bond, @molecule/api-config, @molecule/api-http, @molecule/api-secrets
Secrets: APPLE_SHARED_SECRET
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Apple In-App Purchase provider for molecule.dev.
Handles verification of Apple App Store receipts for in-app purchases and subscriptions.
provider
npm install @molecule/api-payments-apple @molecule/api-bond @molecule/api-config @molecule/api-http @molecule/api-payments @molecule/api-secrets
InAppPurchaseIn-app purchase record.
interface InAppPurchase {
quantity: string
product_id: string
transaction_id: string
original_transaction_id: string
purchase_date: string
purchase_date_ms: string
purchase_date_pst: string
original_purchase_date: string
original_purchase_date_ms: string
original_purchase_date_pst: string
expires_date?: string
expires_date_ms?: string
expires_date_pst?: string
is_trial_period?: string
is_in_intro_offer_period?: string
cancellation_date?: string
cancellation_date_ms?: string
cancellation_reason?: string
}
NormalizedPurchaseNormalized purchase information (for one-time purchases).
interface NormalizedPurchase {
/**
* The payment provider.
*/
provider: PaymentProviderName
/**
* The purchase/transaction ID.
*/
purchaseId: string
/**
* The product ID.
*/
productId: string
/**
* Whether the purchase is valid.
*/
isValid: boolean
/**
* When the purchase was made (Unix timestamp in ms).
*/
purchaseDate: number
/**
* Raw data from the provider.
*/
rawData: unknown
}
NormalizedSubscriptionNormalized subscription information.
Use this interface to abstract away provider-specific differences.
interface NormalizedSubscription {
/**
* The payment provider.
*/
provider: PaymentProviderName
/**
* The subscription ID from the provider.
*/
subscriptionId: string
/**
* The product/plan ID.
*/
productId: string
/**
* Current subscription status.
*/
status: SubscriptionStatus
/**
* Whether the subscription is currently active.
*/
isActive: boolean
/**
* When the current period started (Unix timestamp in ms).
*/
currentPeriodStart?: number
/**
* When the current period ends (Unix timestamp in ms).
*/
currentPeriodEnd?: number
/**
* Whether the subscription will auto-renew.
*/
willRenew?: boolean
/**
* When the subscription was canceled (if applicable).
*/
canceledAt?: number
/**
* Raw data from the provider.
*/
rawData: unknown
}
PendingRenewalPending renewal information.
interface PendingRenewal {
auto_renew_product_id: string
auto_renew_status: string
expiration_intent?: string
is_in_billing_retry_period?: string
product_id: string
original_transaction_id: string
}
VerifyReceiptResponseReceipt verification response from Apple.
interface VerifyReceiptResponse {
status: number
environment?: 'Production' | 'Sandbox'
receipt?: {
bundle_id: string
application_version: string
in_app?: InAppPurchase[]
}
/**
* The base64-encoded latest receipt. Present in `unified_receipt` of Apple
* server-to-server (v1) notifications; re-submitted to `verifyReceipt` so the
* notification's authenticity is proven against Apple before any entitlement
* is granted (never trust the raw notification body).
*/
latest_receipt?: string
latest_receipt_info?: InAppPurchase[]
pending_renewal_info?: PendingRenewal[]
}
PaymentProviderPayment provider bond interface.
Each payment provider implements the methods relevant to its platform. All methods are optional since different platforms use different flows.
type PaymentProvider = PaymentProviderInterface
SubscriptionStatusSubscription status across providers.
type SubscriptionStatus =
'active' | 'canceled' | 'expired' | 'past_due' | 'trialing' | 'paused' | 'pending' | 'unknown'
decodeAndVerifyJWS(compactJWS, trustedRootDER)Decodes and cryptographically verifies a compact-serialization JWS whose
header carries an x5c certificate chain, per RFC 7515 + RFC 7517 §4.7.
FAILS CLOSED — throws (never silently returns unverified data) when:
.-separated parts, bad base64/JSON)alg isn't ES256 (the only algorithm Apple uses for this
scheme — refusing every other value blocks an "alg confusion" downgrade)x5c chain is missing or emptyx5c[i] must verify against
x5c[i + 1]'s public key — a signature check, not just a name match)trustedRootDER (the last x5c entry must
either equal the trusted root byte-for-byte, or be signed by it)A forged payload cannot produce an x5c chain that both (a) has internally
consistent signatures and (b) terminates at the hardcoded trusted root — an
attacker does not hold Apple's root/intermediate private keys, so every
check above is load-bearing, not defense-in-depth theater.
function decodeAndVerifyJWS(
compactJWS: string,
trustedRootDER: Buffer<ArrayBufferLike>,
): Record<string, unknown>
compactJWS — The header.payload.signature JWS compact string.trustedRootDER — The DER bytes of the CA the chain must terminate at (e.g. APPLE_ROOT_CA_G3_DER from ./appleRootCertificate.js).Returns: The decoded JSON payload — ONLY returned once every check above passes.
describeAppleStatus(status)Maps Apple's documented verifyReceipt status codes to actionable descriptions.
Apple reports failures as bare numeric statuses (e.g. 21004), and a raw
"status 21004" log is ambiguous between a bad receipt and a server-side
misconfiguration — 21004 actually means the APPLE_SHARED_SECRET env var is
missing or wrong, which is an operator fix, not a client bug. Surfacing the
meaning next to the number keeps a failed verification debuggable from the log
line alone.
function describeAppleStatus(status: number): string
status — The numeric status from Apple's verifyReceipt response.Returns: A human-readable explanation of the status (with the fix when it is a config issue).
getAutoRenewStatus(response, originalTransactionId)Reads the auto-renew flag for a subscription out of a receipt response's
pending_renewal_info — the ONLY field Apple uses to report whether
auto-renew is currently on, independent of whether the subscription is
still paid-through (cancellation_date unset) or not.
function getAutoRenewStatus(
response: VerifyReceiptResponse,
originalTransactionId: string,
): boolean | undefined
response — The Apple receipt verification response containing pending_renewal_info.originalTransactionId — The original transaction ID to match in the renewal info array.Returns: true if auto-renew is on, false if off, or undefined if no matching renewal info found (e.g. the response has no pending_renewal_info at all).
getLatestSubscription(response)Extracts the subscription with the latest expiration date from a receipt verification response.
Checks both latest_receipt_info and receipt.in_app arrays.
function getLatestSubscription(response: VerifyReceiptResponse): InAppPurchase | null
response — The Apple receipt verification response.Returns: The in-app purchase entry with the latest expires_date_ms, or null if none found.
isSubscriptionActive(subscription)Checks whether an Apple subscription is currently active (not expired and not canceled).
function isSubscriptionActive(subscription: InAppPurchase | null): boolean
subscription — The in-app purchase entry to check, or null.Returns: true if the subscription's expires_date_ms is in the future and it has not been canceled.
normalizeSubscription(subscription, renewalResponse)Normalizes an Apple in-app purchase entry to the provider-agnostic NormalizedSubscription interface.
Maps Apple-specific fields (expires_date_ms, is_trial_period, cancellation_date) to standard status values.
function normalizeSubscription(
subscription: InAppPurchase,
renewalResponse?: VerifyReceiptResponse,
): NormalizedSubscription
subscription — The Apple in-app purchase entry to normalize.renewalResponse — Optional: the full receipt response subscription was extracted from (via {@link getLatestSubscription}). When provided, willRenew is read from its pending_renewal_info.auto_renew_status (via {@link getAutoRenewStatus}) — the actual auto-renew toggle — instead of being INFERRED from isActive && !cancellation_date. The inferred fallback conflates "not canceled/refunded" with "auto-renew is on": a user who turned OFF auto-renew mid-period (no cancellation_date — they keep access through the paid period) would otherwise report willRenew: true right up until expiry.Returns: A NormalizedSubscription with provider set to 'apple' and dates converted to millisecond timestamps.
parseV2Notification(signedPayload)Parses and authenticates an Apple App Store Server Notifications V2
signedPayload.
function parseV2Notification(signedPayload: string): ParsedNotification | null
signedPayload — The raw signedPayload JWS string from the notification body.Returns: The parsed notification, or null if it cannot be authenticated or carries no actionable entitlement change (e.g. a TEST notification).
verifyReceipt(receiptData, useSandbox)Verifies an App Store receipt.
function verifyReceipt(receiptData: string, useSandbox?: boolean): Promise<VerifyReceiptResponse>
receiptData — Base64-encoded receipt data from the App Store client.useSandbox — When true, sends directly to the sandbox endpoint. When the production endpoint returns status 21007 (a sandbox receipt), it retries against sandbox ONLY if APPLE_ALLOW_SANDBOX_RECEIPTS=true; otherwise the sandbox receipt is rejected (fail-closed default).Returns: The parsed receipt verification response from Apple.
APPLE_ROOT_CA_G3_DERDER bytes of Apple's Root CA - G3 certificate — the trust anchor {@link decodeAndVerifyJWS} pins App Store Server Notifications V2 JWS chains to.
const APPLE_ROOT_CA_G3_DER: Buffer<ArrayBufferLike>
paymentProviderPaymentProvider-compatible adapter for Apple In-App Purchases.
Implements verifyReceipt and parseNotification from the PaymentProvider interface.
const paymentProvider: PaymentProviderInterface
paymentsAppleSecretDefinitionsSecret definitions required by the Apple payments bond.
const paymentsAppleSecretDefinitions: SecretDefinition[]
Implements @molecule/api-payments interface.
Setup function to register this provider with the bond system:
import { bond } from '@molecule/api-bond'
import { paymentProvider } from '@molecule/api-payments-apple'
export function setupPaymentsApple(): void {
bond('payments', 'apple', paymentProvider)
}
Peer dependencies:
@molecule/api-bond ^1.0.1@molecule/api-config ^1.0.1@molecule/api-http ^1.0.1@molecule/api-payments ^1.0.1@molecule/api-secrets ^1.0.1APPLE_SHARED_SECRET (required) — Apple app-specific shared secret
@molecule/api-bond@molecule/api-config@molecule/api-http@molecule/api-payments@molecule/api-secretsScope limits to know BEFORE wiring this bond:
expires_date_ms, so a valid ONE-TIME (consumable/non-consumable) purchase
receipt verifies with Apple but yields null ("no subscription found") —
one-time IAP is not implemented.parseNotification
authenticates v1 notifications (notification_type at the body root) by
re-verifying the embedded receipt with Apple; it authenticates v2
notifications (signedPayload JWS, no top-level notification_type) by
cryptographically verifying the JWS x5c certificate chain against
Apple's Root CA - G3 (see jws.ts / appleRootCertificate.ts) — NO live
call back to Apple, which is Apple's own documented model for v2 (the
signature chain IS the proof). App Store Connect defaults NEW apps to v2;
both are handled the same way downstream (mapped to the same simplified
event vocabulary), so no notification-version configuration is required.verifyReceipt endpoint with
APPLE_SHARED_SECRET for the CLIENT-DRIVEN verify flow
(verifyReceipt/verifyPayment) — this is unrelated to which
notification version App Store Connect sends. Non-zero Apple statuses are
logged with their meaning (see describeAppleStatus) — status 21004
means the shared secret is missing/wrong (an env fix, not a client bug).
A missing APPLE_SHARED_SECRET throws a tagged config-not-configured
error (isConfigNotConfiguredError from @molecule/api-payments) BEFORE
any network call, and verifyReceipt on {@link paymentProvider} rethrows
it rather than swallowing it into the same null a genuinely bad receipt
returns.APPLE_ALLOW_SANDBOX_RECEIPTS=true for local/CI testing only.normalizeSubscription()'s willRenew is INFERRED (isActive && !cancellation_date) unless you pass the receipt response as its second
argument, in which case it reads the ACTUAL auto-renew flag from
pending_renewal_info — a still-active subscriber who turned auto-renew
OFF mid-period has no cancellation_date yet, so the inferred value
alone reports willRenew: true right up until expiry.Integration checklist — drive the real UI (live preview, no mocks; use the provider's TEST mode — test cards/sandbox accounts, never a live charge), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
read_activity tool (filter type 'webhook'); never
mock the event or modify production code to fake an entitlement.