UNPKG

@tamtamchik/app-store-receipt-parser

Version:

A lightweight TypeScript library for extracting transaction IDs from Apple's ASN.1 encoded receipts.

230 lines (224 loc) 7.91 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { parseReceipt: () => parseReceipt }); module.exports = __toCommonJS(index_exports); // src/ReceiptParser.ts var ASN1 = __toESM(require("asn1js")); // src/constants.ts var IN_APP = 17; var CONTENT_ID = "pkcs7_content"; var FIELD_TYPE_ID = "FieldType"; var FIELD_VALUE_ID = "FieldTypeOctetString"; var RECEIPT_FIELDS_MAP = /* @__PURE__ */ new Map([ [0, "ENVIRONMENT"], [2, "BUNDLE_ID"], [3, "APP_VERSION"], [4, "OPAQUE_VALUE"], [5, "SHA1_HASH"], [12, "RECEIPT_CREATION_DATE"], [18, "ORIGINAL_PURCHASE_DATE"], [19, "ORIGINAL_APP_VERSION"], [1701, "IN_APP_QUANTITY"], [1702, "IN_APP_PRODUCT_ID"], [1703, "IN_APP_TRANSACTION_ID"], [1704, "IN_APP_PURCHASE_DATE"], [1705, "IN_APP_ORIGINAL_TRANSACTION_ID"], [1706, "IN_APP_ORIGINAL_PURCHASE_DATE"], [1708, "IN_APP_EXPIRES_DATE"], [1711, "IN_APP_WEB_ORDER_LINE_ITEM_ID"], [1712, "IN_APP_CANCELLATION_DATE"] ]); // src/ReceiptVerifier.ts var import_asn1js = require("asn1js"); var ReceiptVerifier = class { receiptSchema; fieldSchema; constructor() { this.receiptSchema = new import_asn1js.Sequence({ value: [ new import_asn1js.ObjectIdentifier(), new import_asn1js.Constructed({ idBlock: { tagClass: 3, tagNumber: 0 }, value: [ new import_asn1js.Sequence({ value: [ new import_asn1js.Integer(), new import_asn1js.Set({ value: [ new import_asn1js.Sequence({ value: [new import_asn1js.ObjectIdentifier(), new import_asn1js.Any()] }) ] }), new import_asn1js.Sequence({ value: [ new import_asn1js.ObjectIdentifier(), new import_asn1js.Constructed({ idBlock: { tagClass: 3, tagNumber: 0 }, value: [new import_asn1js.OctetString({ name: CONTENT_ID })] }) ] }) ] }) ] }) ] }); this.fieldSchema = new import_asn1js.Sequence({ value: [ new import_asn1js.Integer({ name: FIELD_TYPE_ID }), new import_asn1js.Integer(), new import_asn1js.OctetString({ name: FIELD_VALUE_ID }) ] }); } verifyReceiptSchema(receipt) { const receiptVerification = (0, import_asn1js.verifySchema)(Buffer.from(receipt, "base64"), this.receiptSchema); if (!receiptVerification.verified) { throw new Error("Receipt verification failed."); } return receiptVerification; } verifyFieldSchema(sequence) { const fieldVerification = (0, import_asn1js.verifySchema)(sequence.toBER(), this.fieldSchema); if (!fieldVerification.verified) { return null; } return fieldVerification; } }; // src/ReceiptParser.ts var ReceiptParser = class { parsed; receiptVerifier; constructor() { this.receiptVerifier = new ReceiptVerifier(); this.parsed = this.createInitialParsedReceipt(); } parseReceipt(receipt) { if (receipt.trim() === "") { throw new Error("Receipt must be a non-empty string."); } const rootSchemaVerification = this.receiptVerifier.verifyReceiptSchema(receipt); const content = rootSchemaVerification.result[CONTENT_ID]; this.parseReceiptContent(content); this.validateParsedFields(); this.deduplicateArrayFields(); return this.parsed; } createInitialParsedReceipt() { return { ENVIRONMENT: "Production", IN_APP_ORIGINAL_TRANSACTION_IDS: [], IN_APP_TRANSACTION_IDS: [] }; } parseReceiptContent(content) { const sequences = this.extractSequencesFromContent(content); sequences.forEach(this.processSequence.bind(this)); } extractSequencesFromContent(content) { const [contentSet] = content.valueBlock.value; return contentSet.valueBlock.value.filter((v) => v instanceof ASN1.Sequence); } processSequence(sequence) { const verifiedSequence = this.receiptVerifier.verifyFieldSchema(sequence); if (verifiedSequence) { this.handleVerifiedSequence(verifiedSequence); } } handleVerifiedSequence(verifiedSequence) { const fieldKey = verifiedSequence.result[FIELD_TYPE_ID].valueBlock.valueDec; const fieldValue = verifiedSequence.result[FIELD_VALUE_ID]; const handler = this.getFieldHandler(fieldKey); handler(fieldValue); } getFieldHandler(fieldKey) { if (fieldKey === IN_APP) { return this.parseReceiptContent.bind(this); } if (this.isValidReceiptFieldKey(fieldKey)) { const name = RECEIPT_FIELDS_MAP.get(fieldKey); return (fieldValue) => { this.addFieldToReceipt(name, this.extractStringValue(fieldValue)); }; } return () => { }; } isValidReceiptFieldKey(value) { return typeof value === "number" && RECEIPT_FIELDS_MAP.has(value); } extractStringValue(field) { const [fieldValue] = field.valueBlock.value; if (fieldValue instanceof ASN1.IA5String || fieldValue instanceof ASN1.Utf8String) { return fieldValue.valueBlock.value; } return field.toJSON().valueBlock.valueHex; } addFieldToReceipt(name, value) { this.addToArrayFieldIfApplicable(name, value); this.parsed[name] = value; } addToArrayFieldIfApplicable(name, value) { const arrayFields = { "IN_APP_ORIGINAL_TRANSACTION_ID": "IN_APP_ORIGINAL_TRANSACTION_IDS", "IN_APP_TRANSACTION_ID": "IN_APP_TRANSACTION_IDS" }; const arrayFieldName = arrayFields[name]; if (arrayFieldName) { this.parsed[arrayFieldName].push(value); } } validateParsedFields() { const missingFields = Array.from(RECEIPT_FIELDS_MAP.values()).filter((fieldKey) => !(fieldKey in this.parsed)); if (missingFields.length > 0) { throw new Error(`Missing required fields: ${missingFields.join(", ")}`); } } deduplicateArrayFields() { this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS); this.parsed.IN_APP_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_TRANSACTION_IDS); } removeDuplicates(array) { return [...new Set(array)]; } }; function parseReceipt(receipt) { return new ReceiptParser().parseReceipt(receipt); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { parseReceipt });