@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
JavaScript
;
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
});