@tamtamchik/app-store-receipt-parser 2.0.0 → 2.1.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/README.md +13 -7
- package/dist/index.js +17 -7
- package/dist/index.mjs +7 -7
- package/package.json +9 -9
- package/src/mappings.ts +20 -18
- package/src/parser.ts +18 -12
- package/src/utils.ts +1 -1
- package/src/verifications.ts +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# Apple Receipt Parser
|
|
2
2
|
|
|
3
|
-
[![Buy Me A Coffee][ico-coffee]][link-coffee]
|
|
4
3
|
[![Latest Version on NPM][ico-version]][link-npm]
|
|
5
4
|
[![Scrutinizer build][ico-scrutinizer-build]][link-scrutinizer]
|
|
6
5
|
[![Scrutinizer quality][ico-scrutinizer-quality]][link-scrutinizer]
|
|
@@ -10,11 +9,13 @@
|
|
|
10
9
|
|
|
11
10
|
A lightweight TypeScript library for extracting transaction IDs from Apple's ASN.1 encoded Unified Receipts.
|
|
12
11
|
|
|
13
|
-
>
|
|
14
|
-
>
|
|
15
|
-
> It
|
|
12
|
+
> [!IMPORTANT]
|
|
13
|
+
> This library is not a full-fledged receipt parser.
|
|
14
|
+
> It only extracts some information from Apple's ASN.1 encoded Unified Receipts.
|
|
15
|
+
> It does not work with the old-style transaction receipts.
|
|
16
16
|
|
|
17
|
-
>
|
|
17
|
+
> [!NOTE]
|
|
18
|
+
> 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).
|
|
18
19
|
|
|
19
20
|
## Installation
|
|
20
21
|
|
|
@@ -41,6 +42,7 @@ const data = parseReceipt(receiptString);
|
|
|
41
42
|
|
|
42
43
|
console.log(data);
|
|
43
44
|
// {
|
|
45
|
+
// ENVIRONMENT: 'ProductionSandbox',
|
|
44
46
|
// APP_VERSION: '1',
|
|
45
47
|
// ORIGINAL_APP_VERSION: '1.0',
|
|
46
48
|
// OPAQUE_VALUE: 'c4dd4054b0b61a07beb585f6a842e048',
|
|
@@ -76,10 +78,11 @@ console.log(data);
|
|
|
76
78
|
## Special Thanks
|
|
77
79
|
|
|
78
80
|
- [@Jurajzovinec](https://github.com/Jurajzovinec) for his superb contribution to the project.
|
|
81
|
+
- [@fechy](https://github.com/fechy) for bringing environment variable support to the lib.
|
|
79
82
|
|
|
80
83
|
## Contributing
|
|
81
84
|
|
|
82
|
-
Pull requests are always welcome. If you have bigger changes
|
|
85
|
+
Pull requests are always welcome. If you have bigger changes, please open an issue first to discuss your ideas.
|
|
83
86
|
|
|
84
87
|
## License
|
|
85
88
|
|
|
@@ -87,7 +90,10 @@ Apple Receipt Parser is [MIT licensed](./LICENSE).
|
|
|
87
90
|
|
|
88
91
|
## Third-Party Licenses
|
|
89
92
|
|
|
90
|
-
This project uses `ASN1.js`,
|
|
93
|
+
This project uses `ASN1.js`, licensed under the BSD-3-Clause License. The license text can be found in [LICENSE](./LICENSE).
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
[![Buy Me A Coffee][ico-coffee]][link-coffee]
|
|
91
97
|
|
|
92
98
|
[ico-coffee]: https://img.shields.io/badge/Buy%20Me%20A-Coffee-%236F4E37.svg?style=flat-square
|
|
93
99
|
[ico-version]: https://img.shields.io/npm/v/@tamtamchik/app-store-receipt-parser.svg?style=flat-square
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
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
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -25,10 +35,11 @@ __export(src_exports, {
|
|
|
25
35
|
module.exports = __toCommonJS(src_exports);
|
|
26
36
|
|
|
27
37
|
// src/parser.ts
|
|
28
|
-
var
|
|
38
|
+
var ASN1 = __toESM(require("asn1js"));
|
|
29
39
|
|
|
30
40
|
// src/mappings.ts
|
|
31
41
|
var RECEIPT_FIELDS_MAP = /* @__PURE__ */ new Map([
|
|
42
|
+
[0, "ENVIRONMENT"],
|
|
32
43
|
[2, "BUNDLE_ID"],
|
|
33
44
|
[3, "APP_VERSION"],
|
|
34
45
|
[4, "OPAQUE_VALUE"],
|
|
@@ -108,12 +119,10 @@ function verifyFieldSchema(sequence) {
|
|
|
108
119
|
return fieldVerification;
|
|
109
120
|
}
|
|
110
121
|
|
|
111
|
-
// src/utils.ts
|
|
112
|
-
var uniqueArrayValues = (array) => Array.from(new Set(array));
|
|
113
|
-
|
|
114
122
|
// src/parser.ts
|
|
123
|
+
var uniqueArrayValues = (array) => Array.from(new Set(array));
|
|
115
124
|
function isReceiptFieldKey(value) {
|
|
116
|
-
return Boolean(
|
|
125
|
+
return Boolean(typeof value === "number" && RECEIPT_FIELDS_MAP.has(value));
|
|
117
126
|
}
|
|
118
127
|
function isParsedReceiptContentComplete(data) {
|
|
119
128
|
for (const fieldKey of RECEIPT_FIELDS_MAP.values()) {
|
|
@@ -125,7 +134,7 @@ function isParsedReceiptContentComplete(data) {
|
|
|
125
134
|
}
|
|
126
135
|
function extractFieldValue(field) {
|
|
127
136
|
const [fieldValue] = field.valueBlock.value;
|
|
128
|
-
if (fieldValue instanceof
|
|
137
|
+
if (fieldValue instanceof ASN1.IA5String || fieldValue instanceof ASN1.Utf8String) {
|
|
129
138
|
return fieldValue.valueBlock.value;
|
|
130
139
|
}
|
|
131
140
|
return field.toJSON().valueBlock.valueHex;
|
|
@@ -152,7 +161,7 @@ function processField(parsed, fieldKey, fieldValue) {
|
|
|
152
161
|
}
|
|
153
162
|
function parseOctetStringContent(parsed, content) {
|
|
154
163
|
const [contentSet] = content.valueBlock.value;
|
|
155
|
-
const contentSetSequences = contentSet.valueBlock.value.filter((v) => v instanceof
|
|
164
|
+
const contentSetSequences = contentSet.valueBlock.value.filter((v) => v instanceof ASN1.Sequence);
|
|
156
165
|
for (const sequence of contentSetSequences) {
|
|
157
166
|
const verifiedSequence = verifyFieldSchema(sequence);
|
|
158
167
|
if (verifiedSequence) {
|
|
@@ -170,6 +179,7 @@ function parseReceipt(receipt) {
|
|
|
170
179
|
const rootSchemaVerification = verifyReceiptSchema(receipt);
|
|
171
180
|
const content = rootSchemaVerification.result[CONTENT_ID];
|
|
172
181
|
const parsed = {
|
|
182
|
+
ENVIRONMENT: "Production",
|
|
173
183
|
IN_APP_ORIGINAL_TRANSACTION_IDS: [],
|
|
174
184
|
IN_APP_TRANSACTION_IDS: []
|
|
175
185
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// src/parser.ts
|
|
2
|
-
import
|
|
2
|
+
import * as ASN1 from "asn1js";
|
|
3
3
|
|
|
4
4
|
// src/mappings.ts
|
|
5
5
|
var RECEIPT_FIELDS_MAP = /* @__PURE__ */ new Map([
|
|
6
|
+
[0, "ENVIRONMENT"],
|
|
6
7
|
[2, "BUNDLE_ID"],
|
|
7
8
|
[3, "APP_VERSION"],
|
|
8
9
|
[4, "OPAQUE_VALUE"],
|
|
@@ -82,12 +83,10 @@ function verifyFieldSchema(sequence) {
|
|
|
82
83
|
return fieldVerification;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
// src/utils.ts
|
|
86
|
-
var uniqueArrayValues = (array) => Array.from(new Set(array));
|
|
87
|
-
|
|
88
86
|
// src/parser.ts
|
|
87
|
+
var uniqueArrayValues = (array) => Array.from(new Set(array));
|
|
89
88
|
function isReceiptFieldKey(value) {
|
|
90
|
-
return Boolean(
|
|
89
|
+
return Boolean(typeof value === "number" && RECEIPT_FIELDS_MAP.has(value));
|
|
91
90
|
}
|
|
92
91
|
function isParsedReceiptContentComplete(data) {
|
|
93
92
|
for (const fieldKey of RECEIPT_FIELDS_MAP.values()) {
|
|
@@ -99,7 +98,7 @@ function isParsedReceiptContentComplete(data) {
|
|
|
99
98
|
}
|
|
100
99
|
function extractFieldValue(field) {
|
|
101
100
|
const [fieldValue] = field.valueBlock.value;
|
|
102
|
-
if (fieldValue instanceof IA5String || fieldValue instanceof Utf8String) {
|
|
101
|
+
if (fieldValue instanceof ASN1.IA5String || fieldValue instanceof ASN1.Utf8String) {
|
|
103
102
|
return fieldValue.valueBlock.value;
|
|
104
103
|
}
|
|
105
104
|
return field.toJSON().valueBlock.valueHex;
|
|
@@ -126,7 +125,7 @@ function processField(parsed, fieldKey, fieldValue) {
|
|
|
126
125
|
}
|
|
127
126
|
function parseOctetStringContent(parsed, content) {
|
|
128
127
|
const [contentSet] = content.valueBlock.value;
|
|
129
|
-
const contentSetSequences = contentSet.valueBlock.value.filter((v) => v instanceof
|
|
128
|
+
const contentSetSequences = contentSet.valueBlock.value.filter((v) => v instanceof ASN1.Sequence);
|
|
130
129
|
for (const sequence of contentSetSequences) {
|
|
131
130
|
const verifiedSequence = verifyFieldSchema(sequence);
|
|
132
131
|
if (verifiedSequence) {
|
|
@@ -144,6 +143,7 @@ function parseReceipt(receipt) {
|
|
|
144
143
|
const rootSchemaVerification = verifyReceiptSchema(receipt);
|
|
145
144
|
const content = rootSchemaVerification.result[CONTENT_ID];
|
|
146
145
|
const parsed = {
|
|
146
|
+
ENVIRONMENT: "Production",
|
|
147
147
|
IN_APP_ORIGINAL_TRANSACTION_IDS: [],
|
|
148
148
|
IN_APP_TRANSACTION_IDS: []
|
|
149
149
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tamtamchik/app-store-receipt-parser",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "A lightweight TypeScript library for extracting transaction IDs from Apple's ASN.1 encoded receipts.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -34,15 +34,15 @@
|
|
|
34
34
|
"asn1js": "3.0.5"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@types/jest": "29.5.
|
|
38
|
-
"@types/node": "20.
|
|
39
|
-
"@typescript-eslint/eslint-plugin": "
|
|
40
|
-
"@typescript-eslint/parser": "
|
|
41
|
-
"eslint": "8.
|
|
37
|
+
"@types/jest": "29.5.12",
|
|
38
|
+
"@types/node": "^20.12.8",
|
|
39
|
+
"@typescript-eslint/eslint-plugin": "^7.8.0",
|
|
40
|
+
"@typescript-eslint/parser": "^7.8.0",
|
|
41
|
+
"eslint": "8.57.0",
|
|
42
42
|
"jest": "29.7.0",
|
|
43
|
-
"ts-jest": "29.1.
|
|
44
|
-
"tsup": "8.0.
|
|
45
|
-
"typescript": "5.
|
|
43
|
+
"ts-jest": "29.1.2",
|
|
44
|
+
"tsup": "8.0.2",
|
|
45
|
+
"typescript": "5.4.5"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "tsup src/index.ts --format cjs,esm --clean",
|
package/src/mappings.ts
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
export type ReceiptFieldsKeyValues =
|
|
2
|
-
|
|
|
3
|
-
|
|
|
4
|
-
|
|
|
5
|
-
|
|
|
6
|
-
|
|
|
7
|
-
|
|
|
8
|
-
|
|
|
9
|
-
|
|
|
10
|
-
|
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
|
2
|
+
| 0 // Environment
|
|
3
|
+
| 2 // Bundle ID
|
|
4
|
+
| 3 // App version
|
|
5
|
+
| 4 // Opaque value
|
|
6
|
+
| 5 // SHA-1 hash
|
|
7
|
+
| 12 // Receipt creation date
|
|
8
|
+
| 18 // Original purchase date
|
|
9
|
+
| 19 // Original app version
|
|
10
|
+
| 1701 // In-app quantity
|
|
11
|
+
| 1702 // In-app product ID
|
|
12
|
+
| 1703 // In-app transaction ID
|
|
13
|
+
| 1704 // In-app purchase date
|
|
14
|
+
| 1705 // In-app original transaction ID
|
|
15
|
+
| 1706 // In-app original purchase date
|
|
16
|
+
| 1708 // In-app expires date
|
|
17
|
+
| 1711 // In-app web order line item ID
|
|
18
|
+
| 1712 // In-app cancellation date
|
|
18
19
|
|
|
19
20
|
export type ReceiptFieldsKeyNames =
|
|
21
|
+
| 'ENVIRONMENT'
|
|
20
22
|
| 'BUNDLE_ID'
|
|
21
23
|
| 'APP_VERSION'
|
|
22
24
|
| 'OPAQUE_VALUE'
|
|
@@ -39,6 +41,7 @@ export type ReceiptFieldsKeyNames =
|
|
|
39
41
|
* @see https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html
|
|
40
42
|
*/
|
|
41
43
|
export const RECEIPT_FIELDS_MAP: ReadonlyMap<ReceiptFieldsKeyValues, ReceiptFieldsKeyNames> = new Map([
|
|
44
|
+
[0, 'ENVIRONMENT'],
|
|
42
45
|
[2, 'BUNDLE_ID'],
|
|
43
46
|
[3, 'APP_VERSION'],
|
|
44
47
|
[4, 'OPAQUE_VALUE'],
|
|
@@ -55,5 +58,4 @@ export const RECEIPT_FIELDS_MAP: ReadonlyMap<ReceiptFieldsKeyValues, ReceiptFiel
|
|
|
55
58
|
[1708, 'IN_APP_EXPIRES_DATE'],
|
|
56
59
|
[1711, 'IN_APP_WEB_ORDER_LINE_ITEM_ID'],
|
|
57
60
|
[1712, 'IN_APP_CANCELLATION_DATE'],
|
|
58
|
-
])
|
|
59
|
-
|
|
61
|
+
])
|
package/src/parser.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as ASN1 from 'asn1js'
|
|
2
2
|
|
|
3
3
|
import { RECEIPT_FIELDS_MAP, ReceiptFieldsKeyNames, ReceiptFieldsKeyValues } from './mappings'
|
|
4
4
|
import { CONTENT_ID, FIELD_TYPE_ID, FIELD_VALUE_ID, IN_APP } from './constants'
|
|
5
5
|
import { verifyFieldSchema, verifyReceiptSchema } from './verifications'
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
export type Environment = 'Production' | 'ProductionSandbox' | string
|
|
8
|
+
|
|
9
|
+
const uniqueArrayValues = (array: string[]) => Array.from(new Set(array))
|
|
7
10
|
|
|
8
11
|
export type ParsedReceipt = Partial<Record<ReceiptFieldsKeyNames, string>> & {
|
|
12
|
+
ENVIRONMENT: Environment
|
|
9
13
|
IN_APP_ORIGINAL_TRANSACTION_IDS: string[]
|
|
10
14
|
IN_APP_TRANSACTION_IDS: string[]
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
function isReceiptFieldKey (value: unknown): value is ReceiptFieldsKeyValues {
|
|
14
|
-
return Boolean(
|
|
18
|
+
return Boolean(typeof value === 'number' && RECEIPT_FIELDS_MAP.has(value as ReceiptFieldsKeyValues))
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
function isParsedReceiptContentComplete (data: ParsedReceipt): data is ParsedReceipt {
|
|
@@ -24,10 +28,10 @@ function isParsedReceiptContentComplete (data: ParsedReceipt): data is ParsedRec
|
|
|
24
28
|
return true
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
function extractFieldValue (field: OctetString): string {
|
|
31
|
+
function extractFieldValue (field: ASN1.OctetString): string {
|
|
28
32
|
const [fieldValue] = field.valueBlock.value
|
|
29
33
|
|
|
30
|
-
if (fieldValue instanceof IA5String || fieldValue instanceof Utf8String) {
|
|
34
|
+
if (fieldValue instanceof ASN1.IA5String || fieldValue instanceof ASN1.Utf8String) {
|
|
31
35
|
return fieldValue.valueBlock.value
|
|
32
36
|
}
|
|
33
37
|
|
|
@@ -46,7 +50,7 @@ function appendField (parsed: ParsedReceipt, name: ReceiptFieldsKeyNames, value:
|
|
|
46
50
|
parsed[name] = value
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
function processField (parsed: ParsedReceipt, fieldKey: number, fieldValue: OctetString) {
|
|
53
|
+
function processField (parsed: ParsedReceipt, fieldKey: number, fieldValue: ASN1.OctetString) {
|
|
50
54
|
if (fieldKey === IN_APP) {
|
|
51
55
|
parseOctetStringContent(parsed, fieldValue)
|
|
52
56
|
return
|
|
@@ -60,16 +64,17 @@ function processField (parsed: ParsedReceipt, fieldKey: number, fieldValue: Octe
|
|
|
60
64
|
appendField(parsed, name, extractFieldValue(fieldValue))
|
|
61
65
|
}
|
|
62
66
|
|
|
63
|
-
function parseOctetStringContent (parsed: ParsedReceipt, content: OctetString) {
|
|
64
|
-
const [contentSet] = content.valueBlock.value as Set[]
|
|
65
|
-
const contentSetSequences = contentSet.valueBlock.value
|
|
67
|
+
function parseOctetStringContent (parsed: ParsedReceipt, content: ASN1.OctetString) {
|
|
68
|
+
const [contentSet] = content.valueBlock.value as ASN1.Set[]
|
|
69
|
+
const contentSetSequences = contentSet.valueBlock.value
|
|
70
|
+
.filter(v => v instanceof ASN1.Sequence) as ASN1.Sequence[]
|
|
66
71
|
|
|
67
72
|
for (const sequence of contentSetSequences) {
|
|
68
73
|
const verifiedSequence = verifyFieldSchema(sequence)
|
|
69
74
|
if (verifiedSequence) {
|
|
70
75
|
// We are confident to use "as" assertion because Integer type is guaranteed by positive verification above
|
|
71
|
-
const fieldKey = (verifiedSequence.result[FIELD_TYPE_ID] as Integer).valueBlock.valueDec
|
|
72
|
-
const fieldValueOctetString = verifiedSequence.result[FIELD_VALUE_ID] as OctetString
|
|
76
|
+
const fieldKey = (verifiedSequence.result[FIELD_TYPE_ID] as ASN1.Integer).valueBlock.valueDec
|
|
77
|
+
const fieldValueOctetString = verifiedSequence.result[FIELD_VALUE_ID] as ASN1.OctetString
|
|
73
78
|
|
|
74
79
|
processField(parsed, fieldKey, fieldValueOctetString)
|
|
75
80
|
}
|
|
@@ -84,8 +89,9 @@ function postprocessParsedReceipt (parsed: ParsedReceipt) {
|
|
|
84
89
|
export function parseReceipt (receipt: string): ParsedReceipt {
|
|
85
90
|
const rootSchemaVerification = verifyReceiptSchema(receipt)
|
|
86
91
|
|
|
87
|
-
const content = rootSchemaVerification.result[CONTENT_ID] as OctetString
|
|
92
|
+
const content = rootSchemaVerification.result[CONTENT_ID] as ASN1.OctetString
|
|
88
93
|
const parsed: ParsedReceipt = {
|
|
94
|
+
ENVIRONMENT: 'Production',
|
|
89
95
|
IN_APP_ORIGINAL_TRANSACTION_IDS: [],
|
|
90
96
|
IN_APP_TRANSACTION_IDS: [],
|
|
91
97
|
}
|
package/src/utils.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
|