@tamtamchik/app-store-receipt-parser 2.2.3 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,136 @@
1
+ # Changelog
2
+
3
+ ## 2.3.1 - 2026-06-12
4
+
5
+ ### Fixed
6
+
7
+ - Decoded ASN.1 `INTEGER` field values to decimal strings instead of raw hex.
8
+ - Preserved `Environment` literal type hints and exported the `Environment` type from the package entrypoint.
9
+ - Shipped generated type declarations instead of hand-written ones.
10
+ - Allowed parsing receipts without in-app purchases.
11
+
12
+ ### Changed
13
+
14
+ - Documented last-wins semantics of top-level `IN_APP_*` fields.
15
+ - Added a warning that receipt signatures are not verified.
16
+ - Updated the README example to decoded integer values.
17
+
18
+ ### Maintenance
19
+
20
+ - Replaced `tsup` with `tsdown` for the build.
21
+ - Simplified the Dependabot configuration.
22
+ - Updated development dependencies.
23
+
24
+ ## 2.3.0 - 2026-05-27
25
+
26
+ ### Added
27
+
28
+ - Added `IN_APP_RECEIPTS` to parsed receipts, exposing each supported in-app purchase receipt as a structured object.
29
+ - Exported the `InAppReceipt` type from the package entrypoint.
30
+
31
+ ### Changed
32
+
33
+ - Updated package metadata and documentation to describe selected receipt field parsing beyond transaction ID extraction.
34
+ - Updated the package version to `2.3.0`.
35
+
36
+ ### Maintenance
37
+
38
+ - Updated dependency lockfile entries already merged into `main`.
39
+ - Updated transitive development dependency resolution for `brace-expansion` to clear npm audit findings.
40
+ - Pinned GitHub Actions workflow actions by commit SHA and constrained Dependabot update cadence.
41
+
42
+ ## 2.2.3 - 2026-04-15
43
+
44
+ ### Added
45
+
46
+ - Added a security policy.
47
+ - Added GitHub Actions CI for linting, building, and testing.
48
+
49
+ ### Changed
50
+
51
+ - Migrated tests from Jest to the Node.js test runner.
52
+ - Split linting into `lint` and `lint:fix` scripts.
53
+
54
+ ### Fixed
55
+
56
+ - Upgraded `asn1js` from `3.0.6` to `3.0.7`.
57
+
58
+ ### Maintenance
59
+
60
+ - Updated development dependencies and GitHub Actions versions.
61
+ - Consolidated ESLint dependencies.
62
+
63
+ ## 2.2.2 - 2025-05-02
64
+
65
+ ### Fixed
66
+
67
+ - Upgraded `asn1js` from `3.0.5` to `3.0.6`.
68
+
69
+ ### Maintenance
70
+
71
+ - Updated dependencies.
72
+
73
+ ## 2.2.1 - 2024-09-25
74
+
75
+ ### Fixed
76
+
77
+ - Fixed exported TypeScript types.
78
+
79
+ ## 2.2.0 - 2024-09-25
80
+
81
+ ### Changed
82
+
83
+ - Refactored parser and verifier internals into classes.
84
+ - Updated TypeScript and ESLint configuration.
85
+
86
+ ### Maintenance
87
+
88
+ - Updated dependencies.
89
+
90
+ ## 2.1.2 - 2024-05-20
91
+
92
+ ### Maintenance
93
+
94
+ - Updated the Scrutinizer Node.js version.
95
+
96
+ ## 2.1.1 - 2024-05-06
97
+
98
+ ### Maintenance
99
+
100
+ - Updated dependencies.
101
+
102
+ ## 2.1.0 - 2024-04-11
103
+
104
+ ### Added
105
+
106
+ - Added mapping for the `ENVIRONMENT` receipt field.
107
+ - Added `Production` as the default parsed environment.
108
+
109
+ ### Fixed
110
+
111
+ - Fixed parsed receipt completeness checks for the `ENVIRONMENT` field.
112
+
113
+ ### Maintenance
114
+
115
+ - Updated dependencies and documentation.
116
+
117
+ ## 2.0.0 - 2024-01-10
118
+
119
+ ### Changed
120
+
121
+ - Refactored the parser implementation for the `2.x` release line.
122
+ - Updated documentation and project structure.
123
+
124
+ ## 1.0.1 - 2023-11-14
125
+
126
+ ### Maintenance
127
+
128
+ - Simplified tests.
129
+ - Moved CI from CircleCI to Scrutinizer.
130
+
131
+ ## 1.0.0 - 2023-08-19
132
+
133
+ ### Added
134
+
135
+ - Added the initial receipt parsing functionality.
136
+ - Added tests, publishing configuration, and README documentation.
package/README.md CHANGED
@@ -7,13 +7,19 @@
7
7
  [![Software License][ico-license]](./LICENSE)
8
8
  [![Total Downloads][ico-downloads]][link-downloads]
9
9
 
10
- A lightweight TypeScript library for extracting transaction IDs from Apple's ASN.1 encoded Unified Receipts.
10
+ A lightweight TypeScript library for extracting selected fields from Apple's ASN.1 encoded Unified Receipts.
11
11
 
12
12
  > [!IMPORTANT]
13
13
  > This library is not a full-fledged receipt parser.
14
- > It only extracts some information from Apple's ASN.1 encoded Unified Receipts.
14
+ > It extracts supported fields from Apple's ASN.1 encoded Unified Receipts, including in-app purchase receipts.
15
15
  > It does not work with the old-style transaction receipts.
16
16
 
17
+ > [!CAUTION]
18
+ > This library does **not** verify the receipt's PKCS#7 signature — it only checks that the ASN.1
19
+ > structure has the expected shape. The extracted data is not cryptographically trustworthy on its own.
20
+ > Do not grant entitlements based on it without verifying the receipt signature (or using Apple's
21
+ > [App Store Server API](https://developer.apple.com/documentation/appstoreserverapi)) separately.
22
+
17
23
  > [!NOTE]
18
24
  > Documentation for the version 1.x of the library can be found [here](https://github.com/tamtamchik/app-store-receipt-parser/tree/1.x/README.md).
19
25
 
@@ -33,6 +39,9 @@ yarn add @tamtamchik/app-store-receipt-parser
33
39
 
34
40
  ## Usage
35
41
 
42
+ The result includes selected top-level receipt fields, aggregate in-app transaction IDs,
43
+ and structured in-app receipt data in `IN_APP_RECEIPTS`.
44
+
36
45
  ```typescript
37
46
  import { parseReceipt } from '@tamtamchik/app-store-receipt-parser';
38
47
 
@@ -52,8 +61,8 @@ console.log(data);
52
61
  // ORIGINAL_PURCHASE_DATE: '2013-08-01T07:00:00Z',
53
62
  // IN_APP_EXPIRES_DATE: '2015-08-10T07:19:32Z',
54
63
  // IN_APP_CANCELLATION_DATE: '',
55
- // IN_APP_QUANTITY: '020101',
56
- // IN_APP_WEB_ORDER_LINE_ITEM_ID: '0207038d7ea69472c9',
64
+ // IN_APP_QUANTITY: '1',
65
+ // IN_APP_WEB_ORDER_LINE_ITEM_ID: '1000000030274249',
57
66
  // IN_APP_PRODUCT_ID: 'monthly',
58
67
  // IN_APP_TRANSACTION_ID: '1000000166967782',
59
68
  // IN_APP_TRANSACTION_IDS: [
@@ -65,6 +74,20 @@ console.log(data);
65
74
  // '1000000166967484',
66
75
  // '1000000166967782'
67
76
  // ],
77
+ // IN_APP_RECEIPTS: [
78
+ // // ...
79
+ // {
80
+ // IN_APP_EXPIRES_DATE: '2015-08-10T07:19:32Z',
81
+ // IN_APP_CANCELLATION_DATE: '',
82
+ // IN_APP_QUANTITY: '1',
83
+ // IN_APP_WEB_ORDER_LINE_ITEM_ID: '1000000030274249',
84
+ // IN_APP_PRODUCT_ID: 'monthly',
85
+ // IN_APP_TRANSACTION_ID: '1000000166967782',
86
+ // IN_APP_ORIGINAL_TRANSACTION_ID: '1000000166965150',
87
+ // IN_APP_PURCHASE_DATE: '2015-08-10T07:14:32Z',
88
+ // IN_APP_ORIGINAL_PURCHASE_DATE: '2015-08-10T07:12:34Z'
89
+ // }
90
+ // ],
68
91
  // IN_APP_ORIGINAL_TRANSACTION_ID: '1000000166965150',
69
92
  // IN_APP_ORIGINAL_TRANSACTION_IDS: [
70
93
  // '1000000166865231',
@@ -75,6 +98,12 @@ console.log(data);
75
98
  // }
76
99
  ```
77
100
 
101
+ > [!WARNING]
102
+ > Top-level scalar `IN_APP_*` fields (e.g. `IN_APP_PRODUCT_ID`, `IN_APP_TRANSACTION_ID`) hold the value
103
+ > from the **last** in-app purchase block encountered in the receipt, and Apple does not guarantee the
104
+ > order of those blocks. They are kept for backward compatibility — for reliable per-purchase data use
105
+ > `IN_APP_RECEIPTS`.
106
+
78
107
  ## Special Thanks
79
108
 
80
109
  - [@Jurajzovinec](https://github.com/Jurajzovinec) for his superb contribution to the project.
@@ -0,0 +1,15 @@
1
+ //#region src/constants.d.ts
2
+ type ReceiptFieldsKeyNames = 'ENVIRONMENT' | 'BUNDLE_ID' | 'APP_VERSION' | 'OPAQUE_VALUE' | 'SHA1_HASH' | 'RECEIPT_CREATION_DATE' | 'ORIGINAL_PURCHASE_DATE' | 'ORIGINAL_APP_VERSION' | 'IN_APP_QUANTITY' | 'IN_APP_PRODUCT_ID' | 'IN_APP_TRANSACTION_ID' | 'IN_APP_PURCHASE_DATE' | 'IN_APP_ORIGINAL_TRANSACTION_ID' | 'IN_APP_ORIGINAL_PURCHASE_DATE' | 'IN_APP_EXPIRES_DATE' | 'IN_APP_WEB_ORDER_LINE_ITEM_ID' | 'IN_APP_CANCELLATION_DATE';
3
+ //#endregion
4
+ //#region src/ReceiptParser.d.ts
5
+ type Environment = 'Production' | 'ProductionSandbox' | (string & {});
6
+ type InAppReceipt = Partial<Record<ReceiptFieldsKeyNames, string>>;
7
+ type ParsedReceipt = Partial<Record<ReceiptFieldsKeyNames, string>> & {
8
+ ENVIRONMENT: Environment;
9
+ IN_APP_RECEIPTS: InAppReceipt[];
10
+ IN_APP_ORIGINAL_TRANSACTION_IDS: string[];
11
+ IN_APP_TRANSACTION_IDS: string[];
12
+ };
13
+ declare function parseReceipt(receipt: string): ParsedReceipt;
14
+ //#endregion
15
+ export { type Environment, type InAppReceipt, type ParsedReceipt, parseReceipt };
@@ -0,0 +1,15 @@
1
+ //#region src/constants.d.ts
2
+ type ReceiptFieldsKeyNames = 'ENVIRONMENT' | 'BUNDLE_ID' | 'APP_VERSION' | 'OPAQUE_VALUE' | 'SHA1_HASH' | 'RECEIPT_CREATION_DATE' | 'ORIGINAL_PURCHASE_DATE' | 'ORIGINAL_APP_VERSION' | 'IN_APP_QUANTITY' | 'IN_APP_PRODUCT_ID' | 'IN_APP_TRANSACTION_ID' | 'IN_APP_PURCHASE_DATE' | 'IN_APP_ORIGINAL_TRANSACTION_ID' | 'IN_APP_ORIGINAL_PURCHASE_DATE' | 'IN_APP_EXPIRES_DATE' | 'IN_APP_WEB_ORDER_LINE_ITEM_ID' | 'IN_APP_CANCELLATION_DATE';
3
+ //#endregion
4
+ //#region src/ReceiptParser.d.ts
5
+ type Environment = 'Production' | 'ProductionSandbox' | (string & {});
6
+ type InAppReceipt = Partial<Record<ReceiptFieldsKeyNames, string>>;
7
+ type ParsedReceipt = Partial<Record<ReceiptFieldsKeyNames, string>> & {
8
+ ENVIRONMENT: Environment;
9
+ IN_APP_RECEIPTS: InAppReceipt[];
10
+ IN_APP_ORIGINAL_TRANSACTION_IDS: string[];
11
+ IN_APP_TRANSACTION_IDS: string[];
12
+ };
13
+ declare function parseReceipt(receipt: string): ParsedReceipt;
14
+ //#endregion
15
+ export { type Environment, type InAppReceipt, type ParsedReceipt, parseReceipt };
package/dist/index.js CHANGED
@@ -1,229 +1,188 @@
1
- "use strict";
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
2
3
  var __create = Object.create;
3
4
  var __defProp = Object.defineProperty;
4
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
7
  var __getProtoOf = Object.getPrototypeOf;
7
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, { get: all[name], enumerable: true });
11
- };
12
9
  var __copyProps = (to, from, except, desc) => {
13
- if (from && typeof from === "object" || typeof from === "function") {
14
- for (let key of __getOwnPropNames(from))
15
- if (!__hasOwnProp.call(to, key) && key !== except)
16
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
- }
18
- return to;
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
19
18
  };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
-
30
- // src/index.ts
31
- var index_exports = {};
32
- __export(index_exports, {
33
- parseReceipt: () => parseReceipt
34
- });
35
- module.exports = __toCommonJS(index_exports);
36
-
37
- // src/ReceiptParser.ts
38
- var ASN1 = __toESM(require("asn1js"));
39
-
40
- // src/constants.ts
41
- var IN_APP = 17;
42
- var CONTENT_ID = "pkcs7_content";
43
- var FIELD_TYPE_ID = "FieldType";
44
- var FIELD_VALUE_ID = "FieldTypeOctetString";
45
- var RECEIPT_FIELDS_MAP = /* @__PURE__ */ new Map([
46
- [0, "ENVIRONMENT"],
47
- [2, "BUNDLE_ID"],
48
- [3, "APP_VERSION"],
49
- [4, "OPAQUE_VALUE"],
50
- [5, "SHA1_HASH"],
51
- [12, "RECEIPT_CREATION_DATE"],
52
- [18, "ORIGINAL_PURCHASE_DATE"],
53
- [19, "ORIGINAL_APP_VERSION"],
54
- [1701, "IN_APP_QUANTITY"],
55
- [1702, "IN_APP_PRODUCT_ID"],
56
- [1703, "IN_APP_TRANSACTION_ID"],
57
- [1704, "IN_APP_PURCHASE_DATE"],
58
- [1705, "IN_APP_ORIGINAL_TRANSACTION_ID"],
59
- [1706, "IN_APP_ORIGINAL_PURCHASE_DATE"],
60
- [1708, "IN_APP_EXPIRES_DATE"],
61
- [1711, "IN_APP_WEB_ORDER_LINE_ITEM_ID"],
62
- [1712, "IN_APP_CANCELLATION_DATE"]
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let asn1js = require("asn1js");
25
+ asn1js = __toESM(asn1js);
26
+ //#region src/constants.ts
27
+ /** Identifies pkcs7 content information encoded as Octet string */
28
+ const CONTENT_ID = "pkcs7_content";
29
+ /** Identifies field type id information */
30
+ const FIELD_TYPE_ID = "FieldType";
31
+ /** Identifies field value information encoded as Octet string */
32
+ const FIELD_VALUE_ID = "FieldTypeOctetString";
33
+ /**
34
+ * Receipt fields
35
+ * @see https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html
36
+ */
37
+ const RECEIPT_FIELDS_MAP = new Map([
38
+ [0, "ENVIRONMENT"],
39
+ [2, "BUNDLE_ID"],
40
+ [3, "APP_VERSION"],
41
+ [4, "OPAQUE_VALUE"],
42
+ [5, "SHA1_HASH"],
43
+ [12, "RECEIPT_CREATION_DATE"],
44
+ [18, "ORIGINAL_PURCHASE_DATE"],
45
+ [19, "ORIGINAL_APP_VERSION"],
46
+ [1701, "IN_APP_QUANTITY"],
47
+ [1702, "IN_APP_PRODUCT_ID"],
48
+ [1703, "IN_APP_TRANSACTION_ID"],
49
+ [1704, "IN_APP_PURCHASE_DATE"],
50
+ [1705, "IN_APP_ORIGINAL_TRANSACTION_ID"],
51
+ [1706, "IN_APP_ORIGINAL_PURCHASE_DATE"],
52
+ [1708, "IN_APP_EXPIRES_DATE"],
53
+ [1711, "IN_APP_WEB_ORDER_LINE_ITEM_ID"],
54
+ [1712, "IN_APP_CANCELLATION_DATE"]
63
55
  ]);
64
-
65
- // src/ReceiptVerifier.ts
66
- var import_asn1js = require("asn1js");
56
+ //#endregion
57
+ //#region src/ReceiptVerifier.ts
67
58
  var ReceiptVerifier = class {
68
- receiptSchema;
69
- fieldSchema;
70
- constructor() {
71
- this.receiptSchema = new import_asn1js.Sequence({
72
- value: [
73
- new import_asn1js.ObjectIdentifier(),
74
- new import_asn1js.Constructed({
75
- idBlock: { tagClass: 3, tagNumber: 0 },
76
- value: [
77
- new import_asn1js.Sequence({
78
- value: [
79
- new import_asn1js.Integer(),
80
- new import_asn1js.Set({
81
- value: [
82
- new import_asn1js.Sequence({
83
- value: [new import_asn1js.ObjectIdentifier(), new import_asn1js.Any()]
84
- })
85
- ]
86
- }),
87
- new import_asn1js.Sequence({
88
- value: [
89
- new import_asn1js.ObjectIdentifier(),
90
- new import_asn1js.Constructed({
91
- idBlock: { tagClass: 3, tagNumber: 0 },
92
- value: [new import_asn1js.OctetString({ name: CONTENT_ID })]
93
- })
94
- ]
95
- })
96
- ]
97
- })
98
- ]
99
- })
100
- ]
101
- });
102
- this.fieldSchema = new import_asn1js.Sequence({
103
- value: [
104
- new import_asn1js.Integer({ name: FIELD_TYPE_ID }),
105
- new import_asn1js.Integer(),
106
- new import_asn1js.OctetString({ name: FIELD_VALUE_ID })
107
- ]
108
- });
109
- }
110
- verifyReceiptSchema(receipt) {
111
- const receiptVerification = (0, import_asn1js.verifySchema)(Buffer.from(receipt, "base64"), this.receiptSchema);
112
- if (!receiptVerification.verified) {
113
- throw new Error("Receipt verification failed.");
114
- }
115
- return receiptVerification;
116
- }
117
- verifyFieldSchema(sequence) {
118
- const fieldVerification = (0, import_asn1js.verifySchema)(sequence.toBER(), this.fieldSchema);
119
- if (!fieldVerification.verified) {
120
- return null;
121
- }
122
- return fieldVerification;
123
- }
59
+ receiptSchema;
60
+ fieldSchema;
61
+ constructor() {
62
+ this.receiptSchema = new asn1js.Sequence({ value: [new asn1js.ObjectIdentifier(), new asn1js.Constructed({
63
+ idBlock: {
64
+ tagClass: 3,
65
+ tagNumber: 0
66
+ },
67
+ value: [new asn1js.Sequence({ value: [
68
+ new asn1js.Integer(),
69
+ new asn1js.Set({ value: [new asn1js.Sequence({ value: [new asn1js.ObjectIdentifier(), new asn1js.Any()] })] }),
70
+ new asn1js.Sequence({ value: [new asn1js.ObjectIdentifier(), new asn1js.Constructed({
71
+ idBlock: {
72
+ tagClass: 3,
73
+ tagNumber: 0
74
+ },
75
+ value: [new asn1js.OctetString({ name: CONTENT_ID })]
76
+ })] })
77
+ ] })]
78
+ })] });
79
+ this.fieldSchema = new asn1js.Sequence({ value: [
80
+ new asn1js.Integer({ name: FIELD_TYPE_ID }),
81
+ new asn1js.Integer(),
82
+ new asn1js.OctetString({ name: FIELD_VALUE_ID })
83
+ ] });
84
+ }
85
+ verifyReceiptSchema(receipt) {
86
+ const receiptVerification = (0, asn1js.verifySchema)(Buffer.from(receipt, "base64"), this.receiptSchema);
87
+ if (!receiptVerification.verified) throw new Error("Receipt verification failed.");
88
+ return receiptVerification;
89
+ }
90
+ verifyFieldSchema(sequence) {
91
+ const fieldVerification = (0, asn1js.verifySchema)(sequence.toBER(), this.fieldSchema);
92
+ if (!fieldVerification.verified) return null;
93
+ return fieldVerification;
94
+ }
124
95
  };
125
-
126
- // src/ReceiptParser.ts
96
+ //#endregion
97
+ //#region src/ReceiptParser.ts
127
98
  var ReceiptParser = class {
128
- parsed;
129
- receiptVerifier;
130
- constructor() {
131
- this.receiptVerifier = new ReceiptVerifier();
132
- this.parsed = this.createInitialParsedReceipt();
133
- }
134
- parseReceipt(receipt) {
135
- if (receipt.trim() === "") {
136
- throw new Error("Receipt must be a non-empty string.");
137
- }
138
- const rootSchemaVerification = this.receiptVerifier.verifyReceiptSchema(receipt);
139
- const content = rootSchemaVerification.result[CONTENT_ID];
140
- this.parseReceiptContent(content);
141
- this.validateParsedFields();
142
- this.deduplicateArrayFields();
143
- return this.parsed;
144
- }
145
- createInitialParsedReceipt() {
146
- return {
147
- ENVIRONMENT: "Production",
148
- IN_APP_ORIGINAL_TRANSACTION_IDS: [],
149
- IN_APP_TRANSACTION_IDS: []
150
- };
151
- }
152
- parseReceiptContent(content) {
153
- const sequences = this.extractSequencesFromContent(content);
154
- sequences.forEach(this.processSequence.bind(this));
155
- }
156
- extractSequencesFromContent(content) {
157
- const [contentSet] = content.valueBlock.value;
158
- return contentSet.valueBlock.value.filter((v) => v instanceof ASN1.Sequence);
159
- }
160
- processSequence(sequence) {
161
- const verifiedSequence = this.receiptVerifier.verifyFieldSchema(sequence);
162
- if (verifiedSequence) {
163
- this.handleVerifiedSequence(verifiedSequence);
164
- }
165
- }
166
- handleVerifiedSequence(verifiedSequence) {
167
- const fieldKey = verifiedSequence.result[FIELD_TYPE_ID].valueBlock.valueDec;
168
- const fieldValue = verifiedSequence.result[FIELD_VALUE_ID];
169
- const handler = this.getFieldHandler(fieldKey);
170
- handler(fieldValue);
171
- }
172
- getFieldHandler(fieldKey) {
173
- if (fieldKey === IN_APP) {
174
- return this.parseReceiptContent.bind(this);
175
- }
176
- if (this.isValidReceiptFieldKey(fieldKey)) {
177
- const name = RECEIPT_FIELDS_MAP.get(fieldKey);
178
- return (fieldValue) => {
179
- this.addFieldToReceipt(name, this.extractStringValue(fieldValue));
180
- };
181
- }
182
- return () => {
183
- };
184
- }
185
- isValidReceiptFieldKey(value) {
186
- return typeof value === "number" && RECEIPT_FIELDS_MAP.has(value);
187
- }
188
- extractStringValue(field) {
189
- const [fieldValue] = field.valueBlock.value;
190
- if (fieldValue instanceof ASN1.IA5String || fieldValue instanceof ASN1.Utf8String) {
191
- return fieldValue.valueBlock.value;
192
- }
193
- return field.toJSON().valueBlock.valueHex;
194
- }
195
- addFieldToReceipt(name, value) {
196
- this.addToArrayFieldIfApplicable(name, value);
197
- this.parsed[name] = value;
198
- }
199
- addToArrayFieldIfApplicable(name, value) {
200
- const arrayFields = {
201
- "IN_APP_ORIGINAL_TRANSACTION_ID": "IN_APP_ORIGINAL_TRANSACTION_IDS",
202
- "IN_APP_TRANSACTION_ID": "IN_APP_TRANSACTION_IDS"
203
- };
204
- const arrayFieldName = arrayFields[name];
205
- if (arrayFieldName) {
206
- this.parsed[arrayFieldName].push(value);
207
- }
208
- }
209
- validateParsedFields() {
210
- const missingFields = Array.from(RECEIPT_FIELDS_MAP.values()).filter((fieldKey) => !(fieldKey in this.parsed));
211
- if (missingFields.length > 0) {
212
- throw new Error(`Missing required fields: ${missingFields.join(", ")}`);
213
- }
214
- }
215
- deduplicateArrayFields() {
216
- this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS);
217
- this.parsed.IN_APP_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_TRANSACTION_IDS);
218
- }
219
- removeDuplicates(array) {
220
- return [...new Set(array)];
221
- }
99
+ parsed;
100
+ receiptVerifier;
101
+ constructor() {
102
+ this.receiptVerifier = new ReceiptVerifier();
103
+ this.parsed = this.createInitialParsedReceipt();
104
+ }
105
+ parseReceipt(receipt) {
106
+ if (receipt.trim() === "") throw new Error("Receipt must be a non-empty string.");
107
+ const content = this.receiptVerifier.verifyReceiptSchema(receipt).result[CONTENT_ID];
108
+ this.parseReceiptContent(content);
109
+ this.validateParsedFields();
110
+ this.deduplicateArrayFields();
111
+ return this.parsed;
112
+ }
113
+ createInitialParsedReceipt() {
114
+ return {
115
+ ENVIRONMENT: "Production",
116
+ IN_APP_RECEIPTS: [],
117
+ IN_APP_ORIGINAL_TRANSACTION_IDS: [],
118
+ IN_APP_TRANSACTION_IDS: []
119
+ };
120
+ }
121
+ parseReceiptContent(content, inAppReceipt) {
122
+ this.extractSequencesFromContent(content).forEach((sequence) => this.processSequence(sequence, inAppReceipt));
123
+ }
124
+ extractSequencesFromContent(content) {
125
+ const [contentSet] = content.valueBlock.value;
126
+ return contentSet.valueBlock.value.filter((v) => v instanceof asn1js.Sequence);
127
+ }
128
+ processSequence(sequence, inAppReceipt) {
129
+ const verifiedSequence = this.receiptVerifier.verifyFieldSchema(sequence);
130
+ if (verifiedSequence) this.handleVerifiedSequence(verifiedSequence, inAppReceipt);
131
+ }
132
+ handleVerifiedSequence(verifiedSequence, inAppReceipt) {
133
+ const fieldKey = verifiedSequence.result[FIELD_TYPE_ID].valueBlock.valueDec;
134
+ const fieldValue = verifiedSequence.result[FIELD_VALUE_ID];
135
+ this.getFieldHandler(fieldKey, inAppReceipt)(fieldValue);
136
+ }
137
+ getFieldHandler(fieldKey, inAppReceipt) {
138
+ if (fieldKey === 17) return (fieldValue) => {
139
+ const parsedInAppReceipt = {};
140
+ this.parseReceiptContent(fieldValue, parsedInAppReceipt);
141
+ this.parsed.IN_APP_RECEIPTS.push(parsedInAppReceipt);
142
+ };
143
+ if (this.isValidReceiptFieldKey(fieldKey)) {
144
+ const name = RECEIPT_FIELDS_MAP.get(fieldKey);
145
+ return (fieldValue) => {
146
+ this.addFieldToReceipt(name, this.extractStringValue(fieldValue), inAppReceipt);
147
+ };
148
+ }
149
+ return () => {};
150
+ }
151
+ isValidReceiptFieldKey(value) {
152
+ return typeof value === "number" && RECEIPT_FIELDS_MAP.has(value);
153
+ }
154
+ extractStringValue(field) {
155
+ const [fieldValue] = field.valueBlock.value;
156
+ if (fieldValue instanceof asn1js.IA5String || fieldValue instanceof asn1js.Utf8String) return fieldValue.valueBlock.value;
157
+ if (fieldValue instanceof asn1js.Integer) return fieldValue.toBigInt().toString();
158
+ return field.toJSON().valueBlock.valueHex;
159
+ }
160
+ addFieldToReceipt(name, value, inAppReceipt) {
161
+ this.addToArrayFieldIfApplicable(name, value);
162
+ this.parsed[name] = value;
163
+ if (inAppReceipt) inAppReceipt[name] = value;
164
+ }
165
+ addToArrayFieldIfApplicable(name, value) {
166
+ const arrayFieldName = {
167
+ "IN_APP_ORIGINAL_TRANSACTION_ID": "IN_APP_ORIGINAL_TRANSACTION_IDS",
168
+ "IN_APP_TRANSACTION_ID": "IN_APP_TRANSACTION_IDS"
169
+ }[name];
170
+ if (arrayFieldName) this.parsed[arrayFieldName].push(value);
171
+ }
172
+ validateParsedFields() {
173
+ const missingFields = Array.from(RECEIPT_FIELDS_MAP.values()).filter((fieldKey) => !fieldKey.startsWith("IN_APP_")).filter((fieldKey) => !(fieldKey in this.parsed));
174
+ if (missingFields.length > 0) throw new Error(`Missing required fields: ${missingFields.join(", ")}`);
175
+ }
176
+ deduplicateArrayFields() {
177
+ this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS);
178
+ this.parsed.IN_APP_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_TRANSACTION_IDS);
179
+ }
180
+ removeDuplicates(array) {
181
+ return [...new Set(array)];
182
+ }
222
183
  };
223
184
  function parseReceipt(receipt) {
224
- return new ReceiptParser().parseReceipt(receipt);
185
+ return new ReceiptParser().parseReceipt(receipt);
225
186
  }
226
- // Annotate the CommonJS export names for ESM import in node:
227
- 0 && (module.exports = {
228
- parseReceipt
229
- });
187
+ //#endregion
188
+ exports.parseReceipt = parseReceipt;
package/dist/index.mjs CHANGED
@@ -1,192 +1,165 @@
1
- // src/ReceiptParser.ts
2
1
  import * as ASN1 from "asn1js";
3
-
4
- // src/constants.ts
5
- var IN_APP = 17;
6
- var CONTENT_ID = "pkcs7_content";
7
- var FIELD_TYPE_ID = "FieldType";
8
- var FIELD_VALUE_ID = "FieldTypeOctetString";
9
- var RECEIPT_FIELDS_MAP = /* @__PURE__ */ new Map([
10
- [0, "ENVIRONMENT"],
11
- [2, "BUNDLE_ID"],
12
- [3, "APP_VERSION"],
13
- [4, "OPAQUE_VALUE"],
14
- [5, "SHA1_HASH"],
15
- [12, "RECEIPT_CREATION_DATE"],
16
- [18, "ORIGINAL_PURCHASE_DATE"],
17
- [19, "ORIGINAL_APP_VERSION"],
18
- [1701, "IN_APP_QUANTITY"],
19
- [1702, "IN_APP_PRODUCT_ID"],
20
- [1703, "IN_APP_TRANSACTION_ID"],
21
- [1704, "IN_APP_PURCHASE_DATE"],
22
- [1705, "IN_APP_ORIGINAL_TRANSACTION_ID"],
23
- [1706, "IN_APP_ORIGINAL_PURCHASE_DATE"],
24
- [1708, "IN_APP_EXPIRES_DATE"],
25
- [1711, "IN_APP_WEB_ORDER_LINE_ITEM_ID"],
26
- [1712, "IN_APP_CANCELLATION_DATE"]
2
+ import { Any, Constructed, Integer, ObjectIdentifier, OctetString, Sequence, Set as Set$1, verifySchema } from "asn1js";
3
+ //#region src/constants.ts
4
+ /** Identifies pkcs7 content information encoded as Octet string */
5
+ const CONTENT_ID = "pkcs7_content";
6
+ /** Identifies field type id information */
7
+ const FIELD_TYPE_ID = "FieldType";
8
+ /** Identifies field value information encoded as Octet string */
9
+ const FIELD_VALUE_ID = "FieldTypeOctetString";
10
+ /**
11
+ * Receipt fields
12
+ * @see https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html
13
+ */
14
+ const RECEIPT_FIELDS_MAP = new Map([
15
+ [0, "ENVIRONMENT"],
16
+ [2, "BUNDLE_ID"],
17
+ [3, "APP_VERSION"],
18
+ [4, "OPAQUE_VALUE"],
19
+ [5, "SHA1_HASH"],
20
+ [12, "RECEIPT_CREATION_DATE"],
21
+ [18, "ORIGINAL_PURCHASE_DATE"],
22
+ [19, "ORIGINAL_APP_VERSION"],
23
+ [1701, "IN_APP_QUANTITY"],
24
+ [1702, "IN_APP_PRODUCT_ID"],
25
+ [1703, "IN_APP_TRANSACTION_ID"],
26
+ [1704, "IN_APP_PURCHASE_DATE"],
27
+ [1705, "IN_APP_ORIGINAL_TRANSACTION_ID"],
28
+ [1706, "IN_APP_ORIGINAL_PURCHASE_DATE"],
29
+ [1708, "IN_APP_EXPIRES_DATE"],
30
+ [1711, "IN_APP_WEB_ORDER_LINE_ITEM_ID"],
31
+ [1712, "IN_APP_CANCELLATION_DATE"]
27
32
  ]);
28
-
29
- // src/ReceiptVerifier.ts
30
- import { Any, Constructed, Integer, ObjectIdentifier, OctetString, Sequence, Set as Set2, verifySchema } from "asn1js";
33
+ //#endregion
34
+ //#region src/ReceiptVerifier.ts
31
35
  var ReceiptVerifier = class {
32
- receiptSchema;
33
- fieldSchema;
34
- constructor() {
35
- this.receiptSchema = new Sequence({
36
- value: [
37
- new ObjectIdentifier(),
38
- new Constructed({
39
- idBlock: { tagClass: 3, tagNumber: 0 },
40
- value: [
41
- new Sequence({
42
- value: [
43
- new Integer(),
44
- new Set2({
45
- value: [
46
- new Sequence({
47
- value: [new ObjectIdentifier(), new Any()]
48
- })
49
- ]
50
- }),
51
- new Sequence({
52
- value: [
53
- new ObjectIdentifier(),
54
- new Constructed({
55
- idBlock: { tagClass: 3, tagNumber: 0 },
56
- value: [new OctetString({ name: CONTENT_ID })]
57
- })
58
- ]
59
- })
60
- ]
61
- })
62
- ]
63
- })
64
- ]
65
- });
66
- this.fieldSchema = new Sequence({
67
- value: [
68
- new Integer({ name: FIELD_TYPE_ID }),
69
- new Integer(),
70
- new OctetString({ name: FIELD_VALUE_ID })
71
- ]
72
- });
73
- }
74
- verifyReceiptSchema(receipt) {
75
- const receiptVerification = verifySchema(Buffer.from(receipt, "base64"), this.receiptSchema);
76
- if (!receiptVerification.verified) {
77
- throw new Error("Receipt verification failed.");
78
- }
79
- return receiptVerification;
80
- }
81
- verifyFieldSchema(sequence) {
82
- const fieldVerification = verifySchema(sequence.toBER(), this.fieldSchema);
83
- if (!fieldVerification.verified) {
84
- return null;
85
- }
86
- return fieldVerification;
87
- }
36
+ receiptSchema;
37
+ fieldSchema;
38
+ constructor() {
39
+ this.receiptSchema = new Sequence({ value: [new ObjectIdentifier(), new Constructed({
40
+ idBlock: {
41
+ tagClass: 3,
42
+ tagNumber: 0
43
+ },
44
+ value: [new Sequence({ value: [
45
+ new Integer(),
46
+ new Set$1({ value: [new Sequence({ value: [new ObjectIdentifier(), new Any()] })] }),
47
+ new Sequence({ value: [new ObjectIdentifier(), new Constructed({
48
+ idBlock: {
49
+ tagClass: 3,
50
+ tagNumber: 0
51
+ },
52
+ value: [new OctetString({ name: CONTENT_ID })]
53
+ })] })
54
+ ] })]
55
+ })] });
56
+ this.fieldSchema = new Sequence({ value: [
57
+ new Integer({ name: FIELD_TYPE_ID }),
58
+ new Integer(),
59
+ new OctetString({ name: FIELD_VALUE_ID })
60
+ ] });
61
+ }
62
+ verifyReceiptSchema(receipt) {
63
+ const receiptVerification = verifySchema(Buffer.from(receipt, "base64"), this.receiptSchema);
64
+ if (!receiptVerification.verified) throw new Error("Receipt verification failed.");
65
+ return receiptVerification;
66
+ }
67
+ verifyFieldSchema(sequence) {
68
+ const fieldVerification = verifySchema(sequence.toBER(), this.fieldSchema);
69
+ if (!fieldVerification.verified) return null;
70
+ return fieldVerification;
71
+ }
88
72
  };
89
-
90
- // src/ReceiptParser.ts
73
+ //#endregion
74
+ //#region src/ReceiptParser.ts
91
75
  var ReceiptParser = class {
92
- parsed;
93
- receiptVerifier;
94
- constructor() {
95
- this.receiptVerifier = new ReceiptVerifier();
96
- this.parsed = this.createInitialParsedReceipt();
97
- }
98
- parseReceipt(receipt) {
99
- if (receipt.trim() === "") {
100
- throw new Error("Receipt must be a non-empty string.");
101
- }
102
- const rootSchemaVerification = this.receiptVerifier.verifyReceiptSchema(receipt);
103
- const content = rootSchemaVerification.result[CONTENT_ID];
104
- this.parseReceiptContent(content);
105
- this.validateParsedFields();
106
- this.deduplicateArrayFields();
107
- return this.parsed;
108
- }
109
- createInitialParsedReceipt() {
110
- return {
111
- ENVIRONMENT: "Production",
112
- IN_APP_ORIGINAL_TRANSACTION_IDS: [],
113
- IN_APP_TRANSACTION_IDS: []
114
- };
115
- }
116
- parseReceiptContent(content) {
117
- const sequences = this.extractSequencesFromContent(content);
118
- sequences.forEach(this.processSequence.bind(this));
119
- }
120
- extractSequencesFromContent(content) {
121
- const [contentSet] = content.valueBlock.value;
122
- return contentSet.valueBlock.value.filter((v) => v instanceof ASN1.Sequence);
123
- }
124
- processSequence(sequence) {
125
- const verifiedSequence = this.receiptVerifier.verifyFieldSchema(sequence);
126
- if (verifiedSequence) {
127
- this.handleVerifiedSequence(verifiedSequence);
128
- }
129
- }
130
- handleVerifiedSequence(verifiedSequence) {
131
- const fieldKey = verifiedSequence.result[FIELD_TYPE_ID].valueBlock.valueDec;
132
- const fieldValue = verifiedSequence.result[FIELD_VALUE_ID];
133
- const handler = this.getFieldHandler(fieldKey);
134
- handler(fieldValue);
135
- }
136
- getFieldHandler(fieldKey) {
137
- if (fieldKey === IN_APP) {
138
- return this.parseReceiptContent.bind(this);
139
- }
140
- if (this.isValidReceiptFieldKey(fieldKey)) {
141
- const name = RECEIPT_FIELDS_MAP.get(fieldKey);
142
- return (fieldValue) => {
143
- this.addFieldToReceipt(name, this.extractStringValue(fieldValue));
144
- };
145
- }
146
- return () => {
147
- };
148
- }
149
- isValidReceiptFieldKey(value) {
150
- return typeof value === "number" && RECEIPT_FIELDS_MAP.has(value);
151
- }
152
- extractStringValue(field) {
153
- const [fieldValue] = field.valueBlock.value;
154
- if (fieldValue instanceof ASN1.IA5String || fieldValue instanceof ASN1.Utf8String) {
155
- return fieldValue.valueBlock.value;
156
- }
157
- return field.toJSON().valueBlock.valueHex;
158
- }
159
- addFieldToReceipt(name, value) {
160
- this.addToArrayFieldIfApplicable(name, value);
161
- this.parsed[name] = value;
162
- }
163
- addToArrayFieldIfApplicable(name, value) {
164
- const arrayFields = {
165
- "IN_APP_ORIGINAL_TRANSACTION_ID": "IN_APP_ORIGINAL_TRANSACTION_IDS",
166
- "IN_APP_TRANSACTION_ID": "IN_APP_TRANSACTION_IDS"
167
- };
168
- const arrayFieldName = arrayFields[name];
169
- if (arrayFieldName) {
170
- this.parsed[arrayFieldName].push(value);
171
- }
172
- }
173
- validateParsedFields() {
174
- const missingFields = Array.from(RECEIPT_FIELDS_MAP.values()).filter((fieldKey) => !(fieldKey in this.parsed));
175
- if (missingFields.length > 0) {
176
- throw new Error(`Missing required fields: ${missingFields.join(", ")}`);
177
- }
178
- }
179
- deduplicateArrayFields() {
180
- this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS);
181
- this.parsed.IN_APP_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_TRANSACTION_IDS);
182
- }
183
- removeDuplicates(array) {
184
- return [...new Set(array)];
185
- }
76
+ parsed;
77
+ receiptVerifier;
78
+ constructor() {
79
+ this.receiptVerifier = new ReceiptVerifier();
80
+ this.parsed = this.createInitialParsedReceipt();
81
+ }
82
+ parseReceipt(receipt) {
83
+ if (receipt.trim() === "") throw new Error("Receipt must be a non-empty string.");
84
+ const content = this.receiptVerifier.verifyReceiptSchema(receipt).result[CONTENT_ID];
85
+ this.parseReceiptContent(content);
86
+ this.validateParsedFields();
87
+ this.deduplicateArrayFields();
88
+ return this.parsed;
89
+ }
90
+ createInitialParsedReceipt() {
91
+ return {
92
+ ENVIRONMENT: "Production",
93
+ IN_APP_RECEIPTS: [],
94
+ IN_APP_ORIGINAL_TRANSACTION_IDS: [],
95
+ IN_APP_TRANSACTION_IDS: []
96
+ };
97
+ }
98
+ parseReceiptContent(content, inAppReceipt) {
99
+ this.extractSequencesFromContent(content).forEach((sequence) => this.processSequence(sequence, inAppReceipt));
100
+ }
101
+ extractSequencesFromContent(content) {
102
+ const [contentSet] = content.valueBlock.value;
103
+ return contentSet.valueBlock.value.filter((v) => v instanceof ASN1.Sequence);
104
+ }
105
+ processSequence(sequence, inAppReceipt) {
106
+ const verifiedSequence = this.receiptVerifier.verifyFieldSchema(sequence);
107
+ if (verifiedSequence) this.handleVerifiedSequence(verifiedSequence, inAppReceipt);
108
+ }
109
+ handleVerifiedSequence(verifiedSequence, inAppReceipt) {
110
+ const fieldKey = verifiedSequence.result[FIELD_TYPE_ID].valueBlock.valueDec;
111
+ const fieldValue = verifiedSequence.result[FIELD_VALUE_ID];
112
+ this.getFieldHandler(fieldKey, inAppReceipt)(fieldValue);
113
+ }
114
+ getFieldHandler(fieldKey, inAppReceipt) {
115
+ if (fieldKey === 17) return (fieldValue) => {
116
+ const parsedInAppReceipt = {};
117
+ this.parseReceiptContent(fieldValue, parsedInAppReceipt);
118
+ this.parsed.IN_APP_RECEIPTS.push(parsedInAppReceipt);
119
+ };
120
+ if (this.isValidReceiptFieldKey(fieldKey)) {
121
+ const name = RECEIPT_FIELDS_MAP.get(fieldKey);
122
+ return (fieldValue) => {
123
+ this.addFieldToReceipt(name, this.extractStringValue(fieldValue), inAppReceipt);
124
+ };
125
+ }
126
+ return () => {};
127
+ }
128
+ isValidReceiptFieldKey(value) {
129
+ return typeof value === "number" && RECEIPT_FIELDS_MAP.has(value);
130
+ }
131
+ extractStringValue(field) {
132
+ const [fieldValue] = field.valueBlock.value;
133
+ if (fieldValue instanceof ASN1.IA5String || fieldValue instanceof ASN1.Utf8String) return fieldValue.valueBlock.value;
134
+ if (fieldValue instanceof ASN1.Integer) return fieldValue.toBigInt().toString();
135
+ return field.toJSON().valueBlock.valueHex;
136
+ }
137
+ addFieldToReceipt(name, value, inAppReceipt) {
138
+ this.addToArrayFieldIfApplicable(name, value);
139
+ this.parsed[name] = value;
140
+ if (inAppReceipt) inAppReceipt[name] = value;
141
+ }
142
+ addToArrayFieldIfApplicable(name, value) {
143
+ const arrayFieldName = {
144
+ "IN_APP_ORIGINAL_TRANSACTION_ID": "IN_APP_ORIGINAL_TRANSACTION_IDS",
145
+ "IN_APP_TRANSACTION_ID": "IN_APP_TRANSACTION_IDS"
146
+ }[name];
147
+ if (arrayFieldName) this.parsed[arrayFieldName].push(value);
148
+ }
149
+ validateParsedFields() {
150
+ const missingFields = Array.from(RECEIPT_FIELDS_MAP.values()).filter((fieldKey) => !fieldKey.startsWith("IN_APP_")).filter((fieldKey) => !(fieldKey in this.parsed));
151
+ if (missingFields.length > 0) throw new Error(`Missing required fields: ${missingFields.join(", ")}`);
152
+ }
153
+ deduplicateArrayFields() {
154
+ this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS);
155
+ this.parsed.IN_APP_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_TRANSACTION_IDS);
156
+ }
157
+ removeDuplicates(array) {
158
+ return [...new Set(array)];
159
+ }
186
160
  };
187
161
  function parseReceipt(receipt) {
188
- return new ReceiptParser().parseReceipt(receipt);
162
+ return new ReceiptParser().parseReceipt(receipt);
189
163
  }
190
- export {
191
- parseReceipt
192
- };
164
+ //#endregion
165
+ export { parseReceipt };
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@tamtamchik/app-store-receipt-parser",
3
- "version": "2.2.3",
4
- "description": "A lightweight TypeScript library for extracting transaction IDs from Apple's ASN.1 encoded receipts.",
3
+ "version": "2.3.1",
4
+ "description": "A lightweight TypeScript library for extracting selected fields from Apple's ASN.1 encoded receipts.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
7
- "types": "./src/index.ts",
7
+ "types": "./dist/index.d.ts",
8
8
  "author": "Yuri Tkachenko <yuri.tam.tkachenko@gmail.com>",
9
9
  "license": "MIT",
10
10
  "homepage": "https://github.com/tamtamchik/app-store-receipt-parser#readme",
@@ -20,12 +20,13 @@
20
20
  "receipt",
21
21
  "parser",
22
22
  "ASN.1",
23
+ "in-app",
23
24
  "transaction",
24
25
  "typescript"
25
26
  ],
26
27
  "exports": {
27
28
  ".": {
28
- "types": "./src/index.ts",
29
+ "types": "./dist/index.d.ts",
29
30
  "require": "./dist/index.js",
30
31
  "import": "./dist/index.mjs"
31
32
  }
@@ -40,13 +41,13 @@
40
41
  "c8": "^11.0.0",
41
42
  "eslint": "^10.2.0",
42
43
  "globals": "^17.5.0",
43
- "tsup": "^8.0.2",
44
+ "tsdown": "^0.22.2",
44
45
  "tsx": "^4.21.0",
45
46
  "typescript": "^6.0.2",
46
47
  "typescript-eslint": "^8.58.2"
47
48
  },
48
49
  "scripts": {
49
- "build": "tsup src/index.ts --format cjs,esm --clean",
50
+ "build": "tsdown",
50
51
  "dev": "npm run build -- --watch src",
51
52
  "lint": "eslint src",
52
53
  "lint:fix": "eslint src --fix",
@@ -11,10 +11,13 @@ import {
11
11
 
12
12
  import { ReceiptVerifier } from './ReceiptVerifier'
13
13
 
14
- export type Environment = 'Production' | 'ProductionSandbox' | string
14
+ export type Environment = 'Production' | 'ProductionSandbox' | (string & {})
15
+
16
+ export type InAppReceipt = Partial<Record<ReceiptFieldsKeyNames, string>>
15
17
 
16
18
  export type ParsedReceipt = Partial<Record<ReceiptFieldsKeyNames, string>> & {
17
19
  ENVIRONMENT: Environment
20
+ IN_APP_RECEIPTS: InAppReceipt[]
18
21
  IN_APP_ORIGINAL_TRANSACTION_IDS: string[]
19
22
  IN_APP_TRANSACTION_IDS: string[]
20
23
  }
@@ -46,14 +49,15 @@ class ReceiptParser {
46
49
  private createInitialParsedReceipt(): ParsedReceipt {
47
50
  return {
48
51
  ENVIRONMENT: 'Production',
52
+ IN_APP_RECEIPTS: [],
49
53
  IN_APP_ORIGINAL_TRANSACTION_IDS: [],
50
54
  IN_APP_TRANSACTION_IDS: [],
51
55
  }
52
56
  }
53
57
 
54
- private parseReceiptContent(content: ASN1.OctetString): void {
58
+ private parseReceiptContent(content: ASN1.OctetString, inAppReceipt?: InAppReceipt): void {
55
59
  const sequences = this.extractSequencesFromContent(content)
56
- sequences.forEach(this.processSequence.bind(this))
60
+ sequences.forEach(sequence => this.processSequence(sequence, inAppReceipt))
57
61
  }
58
62
 
59
63
  private extractSequencesFromContent(content: ASN1.OctetString): ASN1.Sequence[] {
@@ -62,29 +66,39 @@ class ReceiptParser {
62
66
  .filter(v => v instanceof ASN1.Sequence) as ASN1.Sequence[]
63
67
  }
64
68
 
65
- private processSequence(sequence: ASN1.Sequence): void {
69
+ private processSequence(sequence: ASN1.Sequence, inAppReceipt?: InAppReceipt): void {
66
70
  const verifiedSequence = this.receiptVerifier.verifyFieldSchema(sequence)
67
71
  if (verifiedSequence) {
68
- this.handleVerifiedSequence(verifiedSequence)
72
+ this.handleVerifiedSequence(verifiedSequence, inAppReceipt)
69
73
  }
70
74
  }
71
75
 
72
- private handleVerifiedSequence(verifiedSequence: ASN1.CompareSchemaSuccess): void {
76
+ private handleVerifiedSequence(
77
+ verifiedSequence: ASN1.CompareSchemaSuccess,
78
+ inAppReceipt?: InAppReceipt,
79
+ ): void {
73
80
  const fieldKey = (verifiedSequence.result[FIELD_TYPE_ID] as ASN1.Integer).valueBlock.valueDec
74
81
  const fieldValue = verifiedSequence.result[FIELD_VALUE_ID] as ASN1.OctetString
75
82
 
76
- const handler = this.getFieldHandler(fieldKey)
83
+ const handler = this.getFieldHandler(fieldKey, inAppReceipt)
77
84
  handler(fieldValue)
78
85
  }
79
86
 
80
- private getFieldHandler(fieldKey: number): (fieldValue: ASN1.OctetString) => void {
87
+ private getFieldHandler(
88
+ fieldKey: number,
89
+ inAppReceipt?: InAppReceipt,
90
+ ): (fieldValue: ASN1.OctetString) => void {
81
91
  if (fieldKey === IN_APP) {
82
- return this.parseReceiptContent.bind(this)
92
+ return (fieldValue: ASN1.OctetString) => {
93
+ const parsedInAppReceipt: InAppReceipt = {}
94
+ this.parseReceiptContent(fieldValue, parsedInAppReceipt)
95
+ this.parsed.IN_APP_RECEIPTS.push(parsedInAppReceipt)
96
+ }
83
97
  }
84
98
  if (this.isValidReceiptFieldKey(fieldKey)) {
85
99
  const name = RECEIPT_FIELDS_MAP.get(fieldKey)!
86
100
  return (fieldValue: ASN1.OctetString) => {
87
- this.addFieldToReceipt(name, this.extractStringValue(fieldValue))
101
+ this.addFieldToReceipt(name, this.extractStringValue(fieldValue), inAppReceipt)
88
102
  }
89
103
  }
90
104
  return () => {}
@@ -101,12 +115,19 @@ class ReceiptParser {
101
115
  return fieldValue.valueBlock.value
102
116
  }
103
117
 
118
+ if (fieldValue instanceof ASN1.Integer) {
119
+ return fieldValue.toBigInt().toString()
120
+ }
121
+
104
122
  return field.toJSON().valueBlock.valueHex
105
123
  }
106
124
 
107
- private addFieldToReceipt(name: ReceiptFieldsKeyNames, value: string): void {
125
+ private addFieldToReceipt(name: ReceiptFieldsKeyNames, value: string, inAppReceipt?: InAppReceipt): void {
108
126
  this.addToArrayFieldIfApplicable(name, value)
109
127
  this.parsed[name] = value
128
+ if (inAppReceipt) {
129
+ inAppReceipt[name] = value
130
+ }
110
131
  }
111
132
 
112
133
  private addToArrayFieldIfApplicable(name: ReceiptFieldsKeyNames, value: string): void {
@@ -122,7 +143,9 @@ class ReceiptParser {
122
143
  }
123
144
 
124
145
  private validateParsedFields(): void {
146
+ // In-app fields are optional: a valid receipt may contain no in-app purchases.
125
147
  const missingFields = Array.from(RECEIPT_FIELDS_MAP.values())
148
+ .filter(fieldKey => !fieldKey.startsWith('IN_APP_'))
126
149
  .filter(fieldKey => !(fieldKey in this.parsed))
127
150
 
128
151
  if (missingFields.length > 0) {
package/src/index.ts CHANGED
@@ -1,2 +1,2 @@
1
- export type { ParsedReceipt } from './ReceiptParser'
1
+ export type { Environment, InAppReceipt, ParsedReceipt } from './ReceiptParser'
2
2
  export { parseReceipt } from './ReceiptParser'
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from 'tsdown'
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['cjs', 'esm'],
6
+ dts: true,
7
+ clean: true,
8
+ // Keep the file names the package has always shipped (index.js / index.mjs / index.d.ts)
9
+ outExtensions: ({ format }) => ({
10
+ js: format === 'cjs' ? '.js' : '.mjs',
11
+ dts: format === 'cjs' ? '.d.ts' : '.d.mts',
12
+ }),
13
+ })