@tamtamchik/app-store-receipt-parser 2.1.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 +8 -5
- package/dist/index.js +14 -6
- package/dist/index.mjs +4 -6
- package/package.json +4 -4
- package/src/mappings.ts +18 -19
- package/src/parser.ts +13 -11
- package/src/utils.ts +1 -1
package/README.md
CHANGED
|
@@ -9,11 +9,13 @@
|
|
|
9
9
|
|
|
10
10
|
A lightweight TypeScript library for extracting transaction IDs from Apple's ASN.1 encoded Unified Receipts.
|
|
11
11
|
|
|
12
|
-
>
|
|
12
|
+
> [!IMPORTANT]
|
|
13
|
+
> This library is not a full-fledged receipt parser.
|
|
13
14
|
> It only extracts some information from Apple's ASN.1 encoded Unified Receipts.
|
|
14
|
-
> It does not work with the old-style
|
|
15
|
+
> It does not work with the old-style transaction receipts.
|
|
15
16
|
|
|
16
|
-
>
|
|
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).
|
|
17
19
|
|
|
18
20
|
## Installation
|
|
19
21
|
|
|
@@ -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,7 @@ 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).
|
|
91
94
|
|
|
92
95
|
---
|
|
93
96
|
[![Buy Me A Coffee][ico-coffee]][link-coffee]
|
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,7 +35,7 @@ __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([
|
|
@@ -109,10 +119,8 @@ function verifyFieldSchema(sequence) {
|
|
|
109
119
|
return fieldVerification;
|
|
110
120
|
}
|
|
111
121
|
|
|
112
|
-
// src/utils.ts
|
|
113
|
-
var uniqueArrayValues = (array) => Array.from(new Set(array));
|
|
114
|
-
|
|
115
122
|
// src/parser.ts
|
|
123
|
+
var uniqueArrayValues = (array) => Array.from(new Set(array));
|
|
116
124
|
function isReceiptFieldKey(value) {
|
|
117
125
|
return Boolean(typeof value === "number" && RECEIPT_FIELDS_MAP.has(value));
|
|
118
126
|
}
|
|
@@ -126,7 +134,7 @@ function isParsedReceiptContentComplete(data) {
|
|
|
126
134
|
}
|
|
127
135
|
function extractFieldValue(field) {
|
|
128
136
|
const [fieldValue] = field.valueBlock.value;
|
|
129
|
-
if (fieldValue instanceof
|
|
137
|
+
if (fieldValue instanceof ASN1.IA5String || fieldValue instanceof ASN1.Utf8String) {
|
|
130
138
|
return fieldValue.valueBlock.value;
|
|
131
139
|
}
|
|
132
140
|
return field.toJSON().valueBlock.valueHex;
|
|
@@ -153,7 +161,7 @@ function processField(parsed, fieldKey, fieldValue) {
|
|
|
153
161
|
}
|
|
154
162
|
function parseOctetStringContent(parsed, content) {
|
|
155
163
|
const [contentSet] = content.valueBlock.value;
|
|
156
|
-
const contentSetSequences = contentSet.valueBlock.value.filter((v) => v instanceof
|
|
164
|
+
const contentSetSequences = contentSet.valueBlock.value.filter((v) => v instanceof ASN1.Sequence);
|
|
157
165
|
for (const sequence of contentSetSequences) {
|
|
158
166
|
const verifiedSequence = verifyFieldSchema(sequence);
|
|
159
167
|
if (verifiedSequence) {
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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([
|
|
@@ -83,10 +83,8 @@ function verifyFieldSchema(sequence) {
|
|
|
83
83
|
return fieldVerification;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
// src/utils.ts
|
|
87
|
-
var uniqueArrayValues = (array) => Array.from(new Set(array));
|
|
88
|
-
|
|
89
86
|
// src/parser.ts
|
|
87
|
+
var uniqueArrayValues = (array) => Array.from(new Set(array));
|
|
90
88
|
function isReceiptFieldKey(value) {
|
|
91
89
|
return Boolean(typeof value === "number" && RECEIPT_FIELDS_MAP.has(value));
|
|
92
90
|
}
|
|
@@ -100,7 +98,7 @@ function isParsedReceiptContentComplete(data) {
|
|
|
100
98
|
}
|
|
101
99
|
function extractFieldValue(field) {
|
|
102
100
|
const [fieldValue] = field.valueBlock.value;
|
|
103
|
-
if (fieldValue instanceof IA5String || fieldValue instanceof Utf8String) {
|
|
101
|
+
if (fieldValue instanceof ASN1.IA5String || fieldValue instanceof ASN1.Utf8String) {
|
|
104
102
|
return fieldValue.valueBlock.value;
|
|
105
103
|
}
|
|
106
104
|
return field.toJSON().valueBlock.valueHex;
|
|
@@ -127,7 +125,7 @@ function processField(parsed, fieldKey, fieldValue) {
|
|
|
127
125
|
}
|
|
128
126
|
function parseOctetStringContent(parsed, content) {
|
|
129
127
|
const [contentSet] = content.valueBlock.value;
|
|
130
|
-
const contentSetSequences = contentSet.valueBlock.value.filter((v) => v instanceof
|
|
128
|
+
const contentSetSequences = contentSet.valueBlock.value.filter((v) => v instanceof ASN1.Sequence);
|
|
131
129
|
for (const sequence of contentSetSequences) {
|
|
132
130
|
const verifiedSequence = verifyFieldSchema(sequence);
|
|
133
131
|
if (verifiedSequence) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tamtamchik/app-store-receipt-parser",
|
|
3
|
-
"version": "2.1.
|
|
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",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/jest": "29.5.12",
|
|
38
|
-
"@types/node": "20.12.
|
|
39
|
-
"@typescript-eslint/eslint-plugin": "7.
|
|
40
|
-
"@typescript-eslint/parser": "7.
|
|
38
|
+
"@types/node": "^20.12.8",
|
|
39
|
+
"@typescript-eslint/eslint-plugin": "^7.8.0",
|
|
40
|
+
"@typescript-eslint/parser": "^7.8.0",
|
|
41
41
|
"eslint": "8.57.0",
|
|
42
42
|
"jest": "29.7.0",
|
|
43
43
|
"ts-jest": "29.1.2",
|
package/src/mappings.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
export type ReceiptFieldsKeyValues =
|
|
2
|
-
| 0
|
|
3
|
-
| 2
|
|
4
|
-
| 3
|
|
5
|
-
| 4
|
|
6
|
-
| 5
|
|
7
|
-
| 12
|
|
8
|
-
| 18
|
|
9
|
-
| 19
|
|
10
|
-
| 1701
|
|
11
|
-
| 1702
|
|
12
|
-
| 1703
|
|
13
|
-
| 1704
|
|
14
|
-
| 1705
|
|
15
|
-
| 1706
|
|
16
|
-
| 1708
|
|
17
|
-
| 1711
|
|
18
|
-
| 1712
|
|
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
|
|
19
19
|
|
|
20
20
|
export type ReceiptFieldsKeyNames =
|
|
21
21
|
| 'ENVIRONMENT'
|
|
@@ -58,5 +58,4 @@ export const RECEIPT_FIELDS_MAP: ReadonlyMap<ReceiptFieldsKeyValues, ReceiptFiel
|
|
|
58
58
|
[1708, 'IN_APP_EXPIRES_DATE'],
|
|
59
59
|
[1711, 'IN_APP_WEB_ORDER_LINE_ITEM_ID'],
|
|
60
60
|
[1712, 'IN_APP_CANCELLATION_DATE'],
|
|
61
|
-
])
|
|
62
|
-
|
|
61
|
+
])
|
package/src/parser.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
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
|
-
import { uniqueArrayValues } from './utils'
|
|
7
6
|
|
|
8
7
|
export type Environment = 'Production' | 'ProductionSandbox' | string
|
|
9
8
|
|
|
9
|
+
const uniqueArrayValues = (array: string[]) => Array.from(new Set(array))
|
|
10
|
+
|
|
10
11
|
export type ParsedReceipt = Partial<Record<ReceiptFieldsKeyNames, string>> & {
|
|
11
12
|
ENVIRONMENT: Environment
|
|
12
13
|
IN_APP_ORIGINAL_TRANSACTION_IDS: string[]
|
|
@@ -27,10 +28,10 @@ function isParsedReceiptContentComplete (data: ParsedReceipt): data is ParsedRec
|
|
|
27
28
|
return true
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
function extractFieldValue (field: OctetString): string {
|
|
31
|
+
function extractFieldValue (field: ASN1.OctetString): string {
|
|
31
32
|
const [fieldValue] = field.valueBlock.value
|
|
32
33
|
|
|
33
|
-
if (fieldValue instanceof IA5String || fieldValue instanceof Utf8String) {
|
|
34
|
+
if (fieldValue instanceof ASN1.IA5String || fieldValue instanceof ASN1.Utf8String) {
|
|
34
35
|
return fieldValue.valueBlock.value
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -49,7 +50,7 @@ function appendField (parsed: ParsedReceipt, name: ReceiptFieldsKeyNames, value:
|
|
|
49
50
|
parsed[name] = value
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
function processField (parsed: ParsedReceipt, fieldKey: number, fieldValue: OctetString) {
|
|
53
|
+
function processField (parsed: ParsedReceipt, fieldKey: number, fieldValue: ASN1.OctetString) {
|
|
53
54
|
if (fieldKey === IN_APP) {
|
|
54
55
|
parseOctetStringContent(parsed, fieldValue)
|
|
55
56
|
return
|
|
@@ -63,16 +64,17 @@ function processField (parsed: ParsedReceipt, fieldKey: number, fieldValue: Octe
|
|
|
63
64
|
appendField(parsed, name, extractFieldValue(fieldValue))
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
function parseOctetStringContent (parsed: ParsedReceipt, content: OctetString) {
|
|
67
|
-
const [contentSet] = content.valueBlock.value as Set[]
|
|
68
|
-
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[]
|
|
69
71
|
|
|
70
72
|
for (const sequence of contentSetSequences) {
|
|
71
73
|
const verifiedSequence = verifyFieldSchema(sequence)
|
|
72
74
|
if (verifiedSequence) {
|
|
73
75
|
// We are confident to use "as" assertion because Integer type is guaranteed by positive verification above
|
|
74
|
-
const fieldKey = (verifiedSequence.result[FIELD_TYPE_ID] as Integer).valueBlock.valueDec
|
|
75
|
-
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
|
|
76
78
|
|
|
77
79
|
processField(parsed, fieldKey, fieldValueOctetString)
|
|
78
80
|
}
|
|
@@ -87,7 +89,7 @@ function postprocessParsedReceipt (parsed: ParsedReceipt) {
|
|
|
87
89
|
export function parseReceipt (receipt: string): ParsedReceipt {
|
|
88
90
|
const rootSchemaVerification = verifyReceiptSchema(receipt)
|
|
89
91
|
|
|
90
|
-
const content = rootSchemaVerification.result[CONTENT_ID] as OctetString
|
|
92
|
+
const content = rootSchemaVerification.result[CONTENT_ID] as ASN1.OctetString
|
|
91
93
|
const parsed: ParsedReceipt = {
|
|
92
94
|
ENVIRONMENT: 'Production',
|
|
93
95
|
IN_APP_ORIGINAL_TRANSACTION_IDS: [],
|
package/src/utils.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
|