@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 +136 -0
- package/README.md +33 -4
- package/dist/index.d.mts +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +173 -214
- package/dist/index.mjs +157 -184
- package/package.json +7 -6
- package/src/ReceiptParser.ts +34 -11
- package/src/index.ts +1 -1
- package/tsdown.config.ts +13 -0
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
|
|
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
|
|
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: '
|
|
56
|
-
// IN_APP_WEB_ORDER_LINE_ITEM_ID: '
|
|
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.
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
-
"
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
66
|
-
var import_asn1js = require("asn1js");
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/ReceiptVerifier.ts
|
|
67
58
|
var ReceiptVerifier = class {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/ReceiptParser.ts
|
|
127
98
|
var ReceiptParser = class {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
185
|
+
return new ReceiptParser().parseReceipt(receipt);
|
|
225
186
|
}
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/ReceiptParser.ts
|
|
91
75
|
var ReceiptParser = class {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
162
|
+
return new ReceiptParser().parseReceipt(receipt);
|
|
189
163
|
}
|
|
190
|
-
|
|
191
|
-
|
|
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.
|
|
4
|
-
"description": "A lightweight TypeScript library for extracting
|
|
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": "./
|
|
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": "./
|
|
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
|
-
"
|
|
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": "
|
|
50
|
+
"build": "tsdown",
|
|
50
51
|
"dev": "npm run build -- --watch src",
|
|
51
52
|
"lint": "eslint src",
|
|
52
53
|
"lint:fix": "eslint src --fix",
|
package/src/ReceiptParser.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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(
|
|
87
|
+
private getFieldHandler(
|
|
88
|
+
fieldKey: number,
|
|
89
|
+
inAppReceipt?: InAppReceipt,
|
|
90
|
+
): (fieldValue: ASN1.OctetString) => void {
|
|
81
91
|
if (fieldKey === IN_APP) {
|
|
82
|
-
return
|
|
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'
|
package/tsdown.config.ts
ADDED
|
@@ -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
|
+
})
|