@tamtamchik/app-store-receipt-parser 2.3.0 → 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 +21 -0
- package/README.md +16 -4
- package/dist/index.d.mts +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +173 -222
- package/dist/index.mjs +157 -192
- package/package.json +5 -5
- package/src/ReceiptParser.ts +7 -1
- package/src/index.ts +1 -1
- package/tsdown.config.ts +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
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
|
+
|
|
3
24
|
## 2.3.0 - 2026-05-27
|
|
4
25
|
|
|
5
26
|
### Added
|
package/README.md
CHANGED
|
@@ -14,6 +14,12 @@ A lightweight TypeScript library for extracting selected fields from Apple's ASN
|
|
|
14
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
|
|
|
@@ -55,8 +61,8 @@ console.log(data);
|
|
|
55
61
|
// ORIGINAL_PURCHASE_DATE: '2013-08-01T07:00:00Z',
|
|
56
62
|
// IN_APP_EXPIRES_DATE: '2015-08-10T07:19:32Z',
|
|
57
63
|
// IN_APP_CANCELLATION_DATE: '',
|
|
58
|
-
// IN_APP_QUANTITY: '
|
|
59
|
-
// IN_APP_WEB_ORDER_LINE_ITEM_ID: '
|
|
64
|
+
// IN_APP_QUANTITY: '1',
|
|
65
|
+
// IN_APP_WEB_ORDER_LINE_ITEM_ID: '1000000030274249',
|
|
60
66
|
// IN_APP_PRODUCT_ID: 'monthly',
|
|
61
67
|
// IN_APP_TRANSACTION_ID: '1000000166967782',
|
|
62
68
|
// IN_APP_TRANSACTION_IDS: [
|
|
@@ -73,8 +79,8 @@ console.log(data);
|
|
|
73
79
|
// {
|
|
74
80
|
// IN_APP_EXPIRES_DATE: '2015-08-10T07:19:32Z',
|
|
75
81
|
// IN_APP_CANCELLATION_DATE: '',
|
|
76
|
-
// IN_APP_QUANTITY: '
|
|
77
|
-
// IN_APP_WEB_ORDER_LINE_ITEM_ID: '
|
|
82
|
+
// IN_APP_QUANTITY: '1',
|
|
83
|
+
// IN_APP_WEB_ORDER_LINE_ITEM_ID: '1000000030274249',
|
|
78
84
|
// IN_APP_PRODUCT_ID: 'monthly',
|
|
79
85
|
// IN_APP_TRANSACTION_ID: '1000000166967782',
|
|
80
86
|
// IN_APP_ORIGINAL_TRANSACTION_ID: '1000000166965150',
|
|
@@ -92,6 +98,12 @@ console.log(data);
|
|
|
92
98
|
// }
|
|
93
99
|
```
|
|
94
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
|
+
|
|
95
107
|
## Special Thanks
|
|
96
108
|
|
|
97
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,237 +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
|
-
const arrayFieldName = arrayFields[name];
|
|
213
|
-
if (arrayFieldName) {
|
|
214
|
-
this.parsed[arrayFieldName].push(value);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
validateParsedFields() {
|
|
218
|
-
const missingFields = Array.from(RECEIPT_FIELDS_MAP.values()).filter((fieldKey) => !(fieldKey in this.parsed));
|
|
219
|
-
if (missingFields.length > 0) {
|
|
220
|
-
throw new Error(`Missing required fields: ${missingFields.join(", ")}`);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
deduplicateArrayFields() {
|
|
224
|
-
this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS);
|
|
225
|
-
this.parsed.IN_APP_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_TRANSACTION_IDS);
|
|
226
|
-
}
|
|
227
|
-
removeDuplicates(array) {
|
|
228
|
-
return [...new Set(array)];
|
|
229
|
-
}
|
|
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
|
+
}
|
|
230
183
|
};
|
|
231
184
|
function parseReceipt(receipt) {
|
|
232
|
-
|
|
185
|
+
return new ReceiptParser().parseReceipt(receipt);
|
|
233
186
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
parseReceipt
|
|
237
|
-
});
|
|
187
|
+
//#endregion
|
|
188
|
+
exports.parseReceipt = parseReceipt;
|
package/dist/index.mjs
CHANGED
|
@@ -1,200 +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
|
-
const arrayFieldName = arrayFields[name];
|
|
177
|
-
if (arrayFieldName) {
|
|
178
|
-
this.parsed[arrayFieldName].push(value);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
validateParsedFields() {
|
|
182
|
-
const missingFields = Array.from(RECEIPT_FIELDS_MAP.values()).filter((fieldKey) => !(fieldKey in this.parsed));
|
|
183
|
-
if (missingFields.length > 0) {
|
|
184
|
-
throw new Error(`Missing required fields: ${missingFields.join(", ")}`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
deduplicateArrayFields() {
|
|
188
|
-
this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS);
|
|
189
|
-
this.parsed.IN_APP_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_TRANSACTION_IDS);
|
|
190
|
-
}
|
|
191
|
-
removeDuplicates(array) {
|
|
192
|
-
return [...new Set(array)];
|
|
193
|
-
}
|
|
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
|
+
}
|
|
194
160
|
};
|
|
195
161
|
function parseReceipt(receipt) {
|
|
196
|
-
|
|
162
|
+
return new ReceiptParser().parseReceipt(receipt);
|
|
197
163
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
};
|
|
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.3.
|
|
3
|
+
"version": "2.3.1",
|
|
4
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",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"exports": {
|
|
28
28
|
".": {
|
|
29
|
-
"types": "./
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
30
|
"require": "./dist/index.js",
|
|
31
31
|
"import": "./dist/index.mjs"
|
|
32
32
|
}
|
|
@@ -41,13 +41,13 @@
|
|
|
41
41
|
"c8": "^11.0.0",
|
|
42
42
|
"eslint": "^10.2.0",
|
|
43
43
|
"globals": "^17.5.0",
|
|
44
|
-
"
|
|
44
|
+
"tsdown": "^0.22.2",
|
|
45
45
|
"tsx": "^4.21.0",
|
|
46
46
|
"typescript": "^6.0.2",
|
|
47
47
|
"typescript-eslint": "^8.58.2"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
|
-
"build": "
|
|
50
|
+
"build": "tsdown",
|
|
51
51
|
"dev": "npm run build -- --watch src",
|
|
52
52
|
"lint": "eslint src",
|
|
53
53
|
"lint:fix": "eslint src --fix",
|
package/src/ReceiptParser.ts
CHANGED
|
@@ -11,7 +11,7 @@ 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
15
|
|
|
16
16
|
export type InAppReceipt = Partial<Record<ReceiptFieldsKeyNames, string>>
|
|
17
17
|
|
|
@@ -115,6 +115,10 @@ class ReceiptParser {
|
|
|
115
115
|
return fieldValue.valueBlock.value
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
if (fieldValue instanceof ASN1.Integer) {
|
|
119
|
+
return fieldValue.toBigInt().toString()
|
|
120
|
+
}
|
|
121
|
+
|
|
118
122
|
return field.toJSON().valueBlock.valueHex
|
|
119
123
|
}
|
|
120
124
|
|
|
@@ -139,7 +143,9 @@ class ReceiptParser {
|
|
|
139
143
|
}
|
|
140
144
|
|
|
141
145
|
private validateParsedFields(): void {
|
|
146
|
+
// In-app fields are optional: a valid receipt may contain no in-app purchases.
|
|
142
147
|
const missingFields = Array.from(RECEIPT_FIELDS_MAP.values())
|
|
148
|
+
.filter(fieldKey => !fieldKey.startsWith('IN_APP_'))
|
|
143
149
|
.filter(fieldKey => !(fieldKey in this.parsed))
|
|
144
150
|
|
|
145
151
|
if (missingFields.length > 0) {
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type { InAppReceipt, 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
|
+
})
|