@tamtamchik/app-store-receipt-parser 2.1.1 → 2.2.0

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/dist/index.js CHANGED
@@ -34,10 +34,14 @@ __export(src_exports, {
34
34
  });
35
35
  module.exports = __toCommonJS(src_exports);
36
36
 
37
- // src/parser.ts
37
+ // src/ReceiptParser.ts
38
38
  var ASN1 = __toESM(require("asn1js"));
39
39
 
40
- // src/mappings.ts
40
+ // src/constants.ts
41
+ var IN_APP = 17;
42
+ var CONTENT_ID = "pkcs7_content";
43
+ var FIELD_TYPE_ID = "FieldType";
44
+ var FIELD_VALUE_ID = "FieldTypeOctetString";
41
45
  var RECEIPT_FIELDS_MAP = /* @__PURE__ */ new Map([
42
46
  [0, "ENVIRONMENT"],
43
47
  [2, "BUNDLE_ID"],
@@ -58,143 +62,166 @@ var RECEIPT_FIELDS_MAP = /* @__PURE__ */ new Map([
58
62
  [1712, "IN_APP_CANCELLATION_DATE"]
59
63
  ]);
60
64
 
61
- // src/constants.ts
62
- var IN_APP = 17;
63
- var CONTENT_ID = "pkcs7_content";
64
- var FIELD_TYPE_ID = "FieldType";
65
- var FIELD_VALUE_ID = "FieldTypeOctetString";
66
-
67
- // src/verifications.ts
65
+ // src/ReceiptVerifier.ts
68
66
  var import_asn1js = require("asn1js");
69
- var receiptSchema = new import_asn1js.Sequence({
70
- value: [
71
- new import_asn1js.ObjectIdentifier(),
72
- new import_asn1js.Constructed({
73
- idBlock: { tagClass: 3, tagNumber: 0 },
67
+ var ReceiptVerifier = class {
68
+ receiptSchema;
69
+ fieldSchema;
70
+ constructor() {
71
+ this.receiptSchema = new import_asn1js.Sequence({
74
72
  value: [
75
- new import_asn1js.Sequence({
73
+ new import_asn1js.ObjectIdentifier(),
74
+ new import_asn1js.Constructed({
75
+ idBlock: { tagClass: 3, tagNumber: 0 },
76
76
  value: [
77
- new import_asn1js.Integer(),
78
- new import_asn1js.Set({
79
- value: [
80
- new import_asn1js.Sequence({
81
- value: [new import_asn1js.ObjectIdentifier(), new import_asn1js.Any()]
82
- })
83
- ]
84
- }),
85
77
  new import_asn1js.Sequence({
86
78
  value: [
87
- new import_asn1js.ObjectIdentifier(),
88
- new import_asn1js.Constructed({
89
- idBlock: { tagClass: 3, tagNumber: 0 },
90
- value: [new import_asn1js.OctetString({ name: CONTENT_ID })]
79
+ new import_asn1js.Integer(),
80
+ new import_asn1js.Set({
81
+ value: [
82
+ new import_asn1js.Sequence({
83
+ value: [new import_asn1js.ObjectIdentifier(), new import_asn1js.Any()]
84
+ })
85
+ ]
86
+ }),
87
+ new import_asn1js.Sequence({
88
+ value: [
89
+ new import_asn1js.ObjectIdentifier(),
90
+ new import_asn1js.Constructed({
91
+ idBlock: { tagClass: 3, tagNumber: 0 },
92
+ value: [new import_asn1js.OctetString({ name: CONTENT_ID })]
93
+ })
94
+ ]
91
95
  })
92
96
  ]
93
97
  })
94
98
  ]
95
99
  })
96
100
  ]
97
- })
98
- ]
99
- });
100
- var fieldSchema = new import_asn1js.Sequence({
101
- value: [
102
- new import_asn1js.Integer({ name: FIELD_TYPE_ID }),
103
- new import_asn1js.Integer(),
104
- new import_asn1js.OctetString({ name: FIELD_VALUE_ID })
105
- ]
106
- });
107
- function verifyReceiptSchema(receipt) {
108
- const receiptVerification = (0, import_asn1js.verifySchema)(Buffer.from(receipt, "base64"), receiptSchema);
109
- if (!receiptVerification.verified) {
110
- throw new Error("Receipt verification failed.");
101
+ });
102
+ this.fieldSchema = new import_asn1js.Sequence({
103
+ value: [
104
+ new import_asn1js.Integer({ name: FIELD_TYPE_ID }),
105
+ new import_asn1js.Integer(),
106
+ new import_asn1js.OctetString({ name: FIELD_VALUE_ID })
107
+ ]
108
+ });
111
109
  }
112
- return receiptVerification;
113
- }
114
- function verifyFieldSchema(sequence) {
115
- const fieldVerification = (0, import_asn1js.verifySchema)(sequence.toBER(), fieldSchema);
116
- if (!fieldVerification.verified) {
117
- return null;
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;
118
116
  }
119
- return fieldVerification;
120
- }
121
-
122
- // src/parser.ts
123
- var uniqueArrayValues = (array) => Array.from(new Set(array));
124
- function isReceiptFieldKey(value) {
125
- return Boolean(typeof value === "number" && RECEIPT_FIELDS_MAP.has(value));
126
- }
127
- function isParsedReceiptContentComplete(data) {
128
- for (const fieldKey of RECEIPT_FIELDS_MAP.values()) {
129
- if (!(fieldKey in data)) {
130
- return false;
117
+ verifyFieldSchema(sequence) {
118
+ const fieldVerification = (0, import_asn1js.verifySchema)(sequence.toBER(), this.fieldSchema);
119
+ if (!fieldVerification.verified) {
120
+ return null;
131
121
  }
122
+ return fieldVerification;
132
123
  }
133
- return true;
134
- }
135
- function extractFieldValue(field) {
136
- const [fieldValue] = field.valueBlock.value;
137
- if (fieldValue instanceof ASN1.IA5String || fieldValue instanceof ASN1.Utf8String) {
138
- return fieldValue.valueBlock.value;
124
+ };
125
+
126
+ // src/ReceiptParser.ts
127
+ var ReceiptParser = class {
128
+ parsed;
129
+ receiptVerifier;
130
+ constructor() {
131
+ this.receiptVerifier = new ReceiptVerifier();
132
+ this.parsed = this.createInitialParsedReceipt();
139
133
  }
140
- return field.toJSON().valueBlock.valueHex;
141
- }
142
- function appendField(parsed, name, value) {
143
- if (name === "IN_APP_ORIGINAL_TRANSACTION_ID") {
144
- parsed.IN_APP_ORIGINAL_TRANSACTION_IDS.push(value);
134
+ parseReceipt(receipt) {
135
+ if (receipt.trim() === "") {
136
+ throw new Error("Receipt must be a non-empty string.");
137
+ }
138
+ const rootSchemaVerification = this.receiptVerifier.verifyReceiptSchema(receipt);
139
+ const content = rootSchemaVerification.result[CONTENT_ID];
140
+ this.parseReceiptContent(content);
141
+ this.validateParsedFields();
142
+ this.deduplicateArrayFields();
143
+ return this.parsed;
145
144
  }
146
- if (name === "IN_APP_TRANSACTION_ID") {
147
- parsed.IN_APP_TRANSACTION_IDS.push(value);
145
+ createInitialParsedReceipt() {
146
+ return {
147
+ ENVIRONMENT: "Production",
148
+ IN_APP_ORIGINAL_TRANSACTION_IDS: [],
149
+ IN_APP_TRANSACTION_IDS: []
150
+ };
148
151
  }
149
- parsed[name] = value;
150
- }
151
- function processField(parsed, fieldKey, fieldValue) {
152
- if (fieldKey === IN_APP) {
153
- parseOctetStringContent(parsed, fieldValue);
154
- return;
152
+ parseReceiptContent(content) {
153
+ const sequences = this.extractSequencesFromContent(content);
154
+ sequences.forEach(this.processSequence.bind(this));
155
155
  }
156
- if (!isReceiptFieldKey(fieldKey)) {
157
- return;
156
+ extractSequencesFromContent(content) {
157
+ const [contentSet] = content.valueBlock.value;
158
+ return contentSet.valueBlock.value.filter((v) => v instanceof ASN1.Sequence);
158
159
  }
159
- const name = RECEIPT_FIELDS_MAP.get(fieldKey);
160
- appendField(parsed, name, extractFieldValue(fieldValue));
161
- }
162
- function parseOctetStringContent(parsed, content) {
163
- const [contentSet] = content.valueBlock.value;
164
- const contentSetSequences = contentSet.valueBlock.value.filter((v) => v instanceof ASN1.Sequence);
165
- for (const sequence of contentSetSequences) {
166
- const verifiedSequence = verifyFieldSchema(sequence);
160
+ processSequence(sequence) {
161
+ const verifiedSequence = this.receiptVerifier.verifyFieldSchema(sequence);
167
162
  if (verifiedSequence) {
168
- const fieldKey = verifiedSequence.result[FIELD_TYPE_ID].valueBlock.valueDec;
169
- const fieldValueOctetString = verifiedSequence.result[FIELD_VALUE_ID];
170
- processField(parsed, fieldKey, fieldValueOctetString);
163
+ this.handleVerifiedSequence(verifiedSequence);
171
164
  }
172
165
  }
173
- }
174
- function postprocessParsedReceipt(parsed) {
175
- parsed.IN_APP_ORIGINAL_TRANSACTION_IDS = uniqueArrayValues(parsed.IN_APP_ORIGINAL_TRANSACTION_IDS);
176
- parsed.IN_APP_TRANSACTION_IDS = uniqueArrayValues(parsed.IN_APP_TRANSACTION_IDS);
177
- }
178
- function parseReceipt(receipt) {
179
- const rootSchemaVerification = verifyReceiptSchema(receipt);
180
- const content = rootSchemaVerification.result[CONTENT_ID];
181
- const parsed = {
182
- ENVIRONMENT: "Production",
183
- IN_APP_ORIGINAL_TRANSACTION_IDS: [],
184
- IN_APP_TRANSACTION_IDS: []
185
- };
186
- parseOctetStringContent(parsed, content);
187
- if (!isParsedReceiptContentComplete(parsed)) {
188
- const missingProps = [];
189
- for (const fieldKey of RECEIPT_FIELDS_MAP.values()) {
190
- if (!(fieldKey in parsed)) {
191
- missingProps.push(fieldKey);
192
- }
166
+ handleVerifiedSequence(verifiedSequence) {
167
+ const fieldKey = verifiedSequence.result[FIELD_TYPE_ID].valueBlock.valueDec;
168
+ const fieldValue = verifiedSequence.result[FIELD_VALUE_ID];
169
+ const handler = this.getFieldHandler(fieldKey);
170
+ handler(fieldValue);
171
+ }
172
+ getFieldHandler(fieldKey) {
173
+ if (fieldKey === IN_APP) {
174
+ return this.parseReceiptContent.bind(this);
175
+ }
176
+ if (this.isValidReceiptFieldKey(fieldKey)) {
177
+ const name = RECEIPT_FIELDS_MAP.get(fieldKey);
178
+ return (fieldValue) => {
179
+ this.addFieldToReceipt(name, this.extractStringValue(fieldValue));
180
+ };
193
181
  }
194
- throw new Error(`Missing required fields: ${missingProps.join(", ")}`);
182
+ return () => {
183
+ };
195
184
  }
196
- postprocessParsedReceipt(parsed);
197
- return parsed;
185
+ isValidReceiptFieldKey(value) {
186
+ return typeof value === "number" && RECEIPT_FIELDS_MAP.has(value);
187
+ }
188
+ extractStringValue(field) {
189
+ const [fieldValue] = field.valueBlock.value;
190
+ if (fieldValue instanceof ASN1.IA5String || fieldValue instanceof ASN1.Utf8String) {
191
+ return fieldValue.valueBlock.value;
192
+ }
193
+ return field.toJSON().valueBlock.valueHex;
194
+ }
195
+ addFieldToReceipt(name, value) {
196
+ this.addToArrayFieldIfApplicable(name, value);
197
+ this.parsed[name] = value;
198
+ }
199
+ addToArrayFieldIfApplicable(name, value) {
200
+ const arrayFields = {
201
+ "IN_APP_ORIGINAL_TRANSACTION_ID": "IN_APP_ORIGINAL_TRANSACTION_IDS",
202
+ "IN_APP_TRANSACTION_ID": "IN_APP_TRANSACTION_IDS"
203
+ };
204
+ const arrayFieldName = arrayFields[name];
205
+ if (arrayFieldName) {
206
+ this.parsed[arrayFieldName].push(value);
207
+ }
208
+ }
209
+ validateParsedFields() {
210
+ const missingFields = Array.from(RECEIPT_FIELDS_MAP.values()).filter((fieldKey) => !(fieldKey in this.parsed));
211
+ if (missingFields.length > 0) {
212
+ throw new Error(`Missing required fields: ${missingFields.join(", ")}`);
213
+ }
214
+ }
215
+ deduplicateArrayFields() {
216
+ this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS);
217
+ this.parsed.IN_APP_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_TRANSACTION_IDS);
218
+ }
219
+ removeDuplicates(array) {
220
+ return [...new Set(array)];
221
+ }
222
+ };
223
+ function parseReceipt(receipt) {
224
+ return new ReceiptParser().parseReceipt(receipt);
198
225
  }
199
226
  // Annotate the CommonJS export names for ESM import in node:
200
227
  0 && (module.exports = {
package/dist/index.mjs CHANGED
@@ -1,7 +1,11 @@
1
- // src/parser.ts
1
+ // src/ReceiptParser.ts
2
2
  import * as ASN1 from "asn1js";
3
3
 
4
- // src/mappings.ts
4
+ // src/constants.ts
5
+ var IN_APP = 17;
6
+ var CONTENT_ID = "pkcs7_content";
7
+ var FIELD_TYPE_ID = "FieldType";
8
+ var FIELD_VALUE_ID = "FieldTypeOctetString";
5
9
  var RECEIPT_FIELDS_MAP = /* @__PURE__ */ new Map([
6
10
  [0, "ENVIRONMENT"],
7
11
  [2, "BUNDLE_ID"],
@@ -22,143 +26,166 @@ var RECEIPT_FIELDS_MAP = /* @__PURE__ */ new Map([
22
26
  [1712, "IN_APP_CANCELLATION_DATE"]
23
27
  ]);
24
28
 
25
- // src/constants.ts
26
- var IN_APP = 17;
27
- var CONTENT_ID = "pkcs7_content";
28
- var FIELD_TYPE_ID = "FieldType";
29
- var FIELD_VALUE_ID = "FieldTypeOctetString";
30
-
31
- // src/verifications.ts
29
+ // src/ReceiptVerifier.ts
32
30
  import { Any, Constructed, Integer, ObjectIdentifier, OctetString, Sequence, Set as Set2, verifySchema } from "asn1js";
33
- var receiptSchema = new Sequence({
34
- value: [
35
- new ObjectIdentifier(),
36
- new Constructed({
37
- idBlock: { tagClass: 3, tagNumber: 0 },
31
+ var ReceiptVerifier = class {
32
+ receiptSchema;
33
+ fieldSchema;
34
+ constructor() {
35
+ this.receiptSchema = new Sequence({
38
36
  value: [
39
- new Sequence({
37
+ new ObjectIdentifier(),
38
+ new Constructed({
39
+ idBlock: { tagClass: 3, tagNumber: 0 },
40
40
  value: [
41
- new Integer(),
42
- new Set2({
43
- value: [
44
- new Sequence({
45
- value: [new ObjectIdentifier(), new Any()]
46
- })
47
- ]
48
- }),
49
41
  new Sequence({
50
42
  value: [
51
- new ObjectIdentifier(),
52
- new Constructed({
53
- idBlock: { tagClass: 3, tagNumber: 0 },
54
- value: [new OctetString({ name: CONTENT_ID })]
43
+ new Integer(),
44
+ new Set2({
45
+ value: [
46
+ new Sequence({
47
+ value: [new ObjectIdentifier(), new Any()]
48
+ })
49
+ ]
50
+ }),
51
+ new Sequence({
52
+ value: [
53
+ new ObjectIdentifier(),
54
+ new Constructed({
55
+ idBlock: { tagClass: 3, tagNumber: 0 },
56
+ value: [new OctetString({ name: CONTENT_ID })]
57
+ })
58
+ ]
55
59
  })
56
60
  ]
57
61
  })
58
62
  ]
59
63
  })
60
64
  ]
61
- })
62
- ]
63
- });
64
- var fieldSchema = new Sequence({
65
- value: [
66
- new Integer({ name: FIELD_TYPE_ID }),
67
- new Integer(),
68
- new OctetString({ name: FIELD_VALUE_ID })
69
- ]
70
- });
71
- function verifyReceiptSchema(receipt) {
72
- const receiptVerification = verifySchema(Buffer.from(receipt, "base64"), receiptSchema);
73
- if (!receiptVerification.verified) {
74
- throw new Error("Receipt verification failed.");
75
- }
76
- return receiptVerification;
77
- }
78
- function verifyFieldSchema(sequence) {
79
- const fieldVerification = verifySchema(sequence.toBER(), fieldSchema);
80
- if (!fieldVerification.verified) {
81
- return null;
65
+ });
66
+ this.fieldSchema = new Sequence({
67
+ value: [
68
+ new Integer({ name: FIELD_TYPE_ID }),
69
+ new Integer(),
70
+ new OctetString({ name: FIELD_VALUE_ID })
71
+ ]
72
+ });
82
73
  }
83
- return fieldVerification;
84
- }
85
-
86
- // src/parser.ts
87
- var uniqueArrayValues = (array) => Array.from(new Set(array));
88
- function isReceiptFieldKey(value) {
89
- return Boolean(typeof value === "number" && RECEIPT_FIELDS_MAP.has(value));
90
- }
91
- function isParsedReceiptContentComplete(data) {
92
- for (const fieldKey of RECEIPT_FIELDS_MAP.values()) {
93
- if (!(fieldKey in data)) {
94
- return false;
74
+ verifyReceiptSchema(receipt) {
75
+ const receiptVerification = verifySchema(Buffer.from(receipt, "base64"), this.receiptSchema);
76
+ if (!receiptVerification.verified) {
77
+ throw new Error("Receipt verification failed.");
95
78
  }
79
+ return receiptVerification;
96
80
  }
97
- return true;
98
- }
99
- function extractFieldValue(field) {
100
- const [fieldValue] = field.valueBlock.value;
101
- if (fieldValue instanceof ASN1.IA5String || fieldValue instanceof ASN1.Utf8String) {
102
- return fieldValue.valueBlock.value;
81
+ verifyFieldSchema(sequence) {
82
+ const fieldVerification = verifySchema(sequence.toBER(), this.fieldSchema);
83
+ if (!fieldVerification.verified) {
84
+ return null;
85
+ }
86
+ return fieldVerification;
103
87
  }
104
- return field.toJSON().valueBlock.valueHex;
105
- }
106
- function appendField(parsed, name, value) {
107
- if (name === "IN_APP_ORIGINAL_TRANSACTION_ID") {
108
- parsed.IN_APP_ORIGINAL_TRANSACTION_IDS.push(value);
88
+ };
89
+
90
+ // src/ReceiptParser.ts
91
+ var ReceiptParser = class {
92
+ parsed;
93
+ receiptVerifier;
94
+ constructor() {
95
+ this.receiptVerifier = new ReceiptVerifier();
96
+ this.parsed = this.createInitialParsedReceipt();
109
97
  }
110
- if (name === "IN_APP_TRANSACTION_ID") {
111
- parsed.IN_APP_TRANSACTION_IDS.push(value);
98
+ parseReceipt(receipt) {
99
+ if (receipt.trim() === "") {
100
+ throw new Error("Receipt must be a non-empty string.");
101
+ }
102
+ const rootSchemaVerification = this.receiptVerifier.verifyReceiptSchema(receipt);
103
+ const content = rootSchemaVerification.result[CONTENT_ID];
104
+ this.parseReceiptContent(content);
105
+ this.validateParsedFields();
106
+ this.deduplicateArrayFields();
107
+ return this.parsed;
112
108
  }
113
- parsed[name] = value;
114
- }
115
- function processField(parsed, fieldKey, fieldValue) {
116
- if (fieldKey === IN_APP) {
117
- parseOctetStringContent(parsed, fieldValue);
118
- return;
109
+ createInitialParsedReceipt() {
110
+ return {
111
+ ENVIRONMENT: "Production",
112
+ IN_APP_ORIGINAL_TRANSACTION_IDS: [],
113
+ IN_APP_TRANSACTION_IDS: []
114
+ };
119
115
  }
120
- if (!isReceiptFieldKey(fieldKey)) {
121
- return;
116
+ parseReceiptContent(content) {
117
+ const sequences = this.extractSequencesFromContent(content);
118
+ sequences.forEach(this.processSequence.bind(this));
122
119
  }
123
- const name = RECEIPT_FIELDS_MAP.get(fieldKey);
124
- appendField(parsed, name, extractFieldValue(fieldValue));
125
- }
126
- function parseOctetStringContent(parsed, content) {
127
- const [contentSet] = content.valueBlock.value;
128
- const contentSetSequences = contentSet.valueBlock.value.filter((v) => v instanceof ASN1.Sequence);
129
- for (const sequence of contentSetSequences) {
130
- const verifiedSequence = verifyFieldSchema(sequence);
120
+ extractSequencesFromContent(content) {
121
+ const [contentSet] = content.valueBlock.value;
122
+ return contentSet.valueBlock.value.filter((v) => v instanceof ASN1.Sequence);
123
+ }
124
+ processSequence(sequence) {
125
+ const verifiedSequence = this.receiptVerifier.verifyFieldSchema(sequence);
131
126
  if (verifiedSequence) {
132
- const fieldKey = verifiedSequence.result[FIELD_TYPE_ID].valueBlock.valueDec;
133
- const fieldValueOctetString = verifiedSequence.result[FIELD_VALUE_ID];
134
- processField(parsed, fieldKey, fieldValueOctetString);
127
+ this.handleVerifiedSequence(verifiedSequence);
135
128
  }
136
129
  }
137
- }
138
- function postprocessParsedReceipt(parsed) {
139
- parsed.IN_APP_ORIGINAL_TRANSACTION_IDS = uniqueArrayValues(parsed.IN_APP_ORIGINAL_TRANSACTION_IDS);
140
- parsed.IN_APP_TRANSACTION_IDS = uniqueArrayValues(parsed.IN_APP_TRANSACTION_IDS);
141
- }
142
- function parseReceipt(receipt) {
143
- const rootSchemaVerification = verifyReceiptSchema(receipt);
144
- const content = rootSchemaVerification.result[CONTENT_ID];
145
- const parsed = {
146
- ENVIRONMENT: "Production",
147
- IN_APP_ORIGINAL_TRANSACTION_IDS: [],
148
- IN_APP_TRANSACTION_IDS: []
149
- };
150
- parseOctetStringContent(parsed, content);
151
- if (!isParsedReceiptContentComplete(parsed)) {
152
- const missingProps = [];
153
- for (const fieldKey of RECEIPT_FIELDS_MAP.values()) {
154
- if (!(fieldKey in parsed)) {
155
- missingProps.push(fieldKey);
156
- }
130
+ handleVerifiedSequence(verifiedSequence) {
131
+ const fieldKey = verifiedSequence.result[FIELD_TYPE_ID].valueBlock.valueDec;
132
+ const fieldValue = verifiedSequence.result[FIELD_VALUE_ID];
133
+ const handler = this.getFieldHandler(fieldKey);
134
+ handler(fieldValue);
135
+ }
136
+ getFieldHandler(fieldKey) {
137
+ if (fieldKey === IN_APP) {
138
+ return this.parseReceiptContent.bind(this);
139
+ }
140
+ if (this.isValidReceiptFieldKey(fieldKey)) {
141
+ const name = RECEIPT_FIELDS_MAP.get(fieldKey);
142
+ return (fieldValue) => {
143
+ this.addFieldToReceipt(name, this.extractStringValue(fieldValue));
144
+ };
157
145
  }
158
- throw new Error(`Missing required fields: ${missingProps.join(", ")}`);
146
+ return () => {
147
+ };
159
148
  }
160
- postprocessParsedReceipt(parsed);
161
- return parsed;
149
+ isValidReceiptFieldKey(value) {
150
+ return typeof value === "number" && RECEIPT_FIELDS_MAP.has(value);
151
+ }
152
+ extractStringValue(field) {
153
+ const [fieldValue] = field.valueBlock.value;
154
+ if (fieldValue instanceof ASN1.IA5String || fieldValue instanceof ASN1.Utf8String) {
155
+ return fieldValue.valueBlock.value;
156
+ }
157
+ return field.toJSON().valueBlock.valueHex;
158
+ }
159
+ addFieldToReceipt(name, value) {
160
+ this.addToArrayFieldIfApplicable(name, value);
161
+ this.parsed[name] = value;
162
+ }
163
+ addToArrayFieldIfApplicable(name, value) {
164
+ const arrayFields = {
165
+ "IN_APP_ORIGINAL_TRANSACTION_ID": "IN_APP_ORIGINAL_TRANSACTION_IDS",
166
+ "IN_APP_TRANSACTION_ID": "IN_APP_TRANSACTION_IDS"
167
+ };
168
+ const arrayFieldName = arrayFields[name];
169
+ if (arrayFieldName) {
170
+ this.parsed[arrayFieldName].push(value);
171
+ }
172
+ }
173
+ validateParsedFields() {
174
+ const missingFields = Array.from(RECEIPT_FIELDS_MAP.values()).filter((fieldKey) => !(fieldKey in this.parsed));
175
+ if (missingFields.length > 0) {
176
+ throw new Error(`Missing required fields: ${missingFields.join(", ")}`);
177
+ }
178
+ }
179
+ deduplicateArrayFields() {
180
+ this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS);
181
+ this.parsed.IN_APP_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_TRANSACTION_IDS);
182
+ }
183
+ removeDuplicates(array) {
184
+ return [...new Set(array)];
185
+ }
186
+ };
187
+ function parseReceipt(receipt) {
188
+ return new ReceiptParser().parseReceipt(receipt);
162
189
  }
163
190
  export {
164
191
  parseReceipt
@@ -0,0 +1,58 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import globals from "globals";
5
+ import stylisticTs from '@stylistic/eslint-plugin-ts'
6
+ import tsParser from "@typescript-eslint/parser";
7
+ import js from "@eslint/js";
8
+ import { FlatCompat } from "@eslint/eslintrc";
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+ const compat = new FlatCompat({
13
+ baseDirectory: __dirname,
14
+ recommendedConfig: js.configs.recommended,
15
+ allConfig: js.configs.all
16
+ });
17
+
18
+ export default [
19
+ ...compat.extends("plugin:@typescript-eslint/recommended"),
20
+
21
+ {
22
+ plugins: {
23
+ "@stylistic/ts": stylisticTs,
24
+ },
25
+ },
26
+
27
+ {
28
+ languageOptions: {
29
+ globals: {
30
+ ...globals.node,
31
+ ...globals.jest,
32
+ },
33
+
34
+ parser: tsParser,
35
+ ecmaVersion: 2021,
36
+ sourceType: "module",
37
+ },
38
+
39
+ rules: {
40
+ indent: ["error", 2, {
41
+ SwitchCase: 1,
42
+ }],
43
+
44
+ semi: "off",
45
+ quotes: "off",
46
+ "object-curly-spacing": "off",
47
+
48
+ "@typescript-eslint/array-type": ["error", {
49
+ default: "array",
50
+ }],
51
+
52
+ "@stylistic/ts/ban-ts-comment": 0,
53
+ "@stylistic/ts/comma-dangle": ["error", "always-multiline"],
54
+ "@stylistic/ts/quotes": ["error", "single"],
55
+ "@stylistic/ts/semi": ["error", "never"],
56
+ "@stylistic/ts/object-curly-spacing": ["error", "always"],
57
+ },
58
+ }];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamtamchik/app-store-receipt-parser",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
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",
@@ -31,18 +31,22 @@
31
31
  }
32
32
  },
33
33
  "dependencies": {
34
- "asn1js": "3.0.5"
34
+ "asn1js": "^3.0.5"
35
35
  },
36
36
  "devDependencies": {
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
- "jest": "29.7.0",
43
- "ts-jest": "29.1.2",
44
- "tsup": "8.0.2",
45
- "typescript": "5.4.5"
37
+ "@eslint/eslintrc": "^3.1.0",
38
+ "@eslint/js": "^9.11.1",
39
+ "@stylistic/eslint-plugin-ts": "^2.8.0",
40
+ "@types/jest": "^29.5.12",
41
+ "@types/node": "^22.7.1",
42
+ "@typescript-eslint/eslint-plugin": "^8.7.0",
43
+ "@typescript-eslint/parser": "^8.7.0",
44
+ "eslint": "^9.11.1",
45
+ "globals": "^15.9.0",
46
+ "jest": "^29.7.0",
47
+ "ts-jest": "^29.1.2",
48
+ "tsup": "^8.0.2",
49
+ "typescript": "^5.4.5"
46
50
  },
47
51
  "scripts": {
48
52
  "build": "tsup src/index.ts --format cjs,esm --clean",
@@ -0,0 +1,145 @@
1
+ import * as ASN1 from 'asn1js'
2
+
3
+ import {
4
+ CONTENT_ID,
5
+ FIELD_TYPE_ID,
6
+ FIELD_VALUE_ID,
7
+ IN_APP,
8
+ RECEIPT_FIELDS_MAP,
9
+ ReceiptFieldsKeyNames, ReceiptFieldsKeyValues,
10
+ } from './constants'
11
+
12
+ import { ReceiptVerifier } from './ReceiptVerifier'
13
+
14
+ export type Environment = 'Production' | 'ProductionSandbox' | string
15
+
16
+ export type ParsedReceipt = Partial<Record<ReceiptFieldsKeyNames, string>> & {
17
+ ENVIRONMENT: Environment
18
+ IN_APP_ORIGINAL_TRANSACTION_IDS: string[]
19
+ IN_APP_TRANSACTION_IDS: string[]
20
+ }
21
+
22
+ class ReceiptParser {
23
+ private readonly parsed: ParsedReceipt
24
+ private readonly receiptVerifier: ReceiptVerifier
25
+
26
+ constructor() {
27
+ this.receiptVerifier = new ReceiptVerifier()
28
+ this.parsed = this.createInitialParsedReceipt()
29
+ }
30
+
31
+ public parseReceipt(receipt: string): ParsedReceipt {
32
+ if (receipt.trim() === '') {
33
+ throw new Error('Receipt must be a non-empty string.')
34
+ }
35
+
36
+ const rootSchemaVerification = this.receiptVerifier.verifyReceiptSchema(receipt)
37
+ const content = rootSchemaVerification.result[CONTENT_ID] as ASN1.OctetString
38
+
39
+ this.parseReceiptContent(content)
40
+ this.validateParsedFields()
41
+ this.deduplicateArrayFields()
42
+
43
+ return this.parsed
44
+ }
45
+
46
+ private createInitialParsedReceipt(): ParsedReceipt {
47
+ return {
48
+ ENVIRONMENT: 'Production',
49
+ IN_APP_ORIGINAL_TRANSACTION_IDS: [],
50
+ IN_APP_TRANSACTION_IDS: [],
51
+ }
52
+ }
53
+
54
+ private parseReceiptContent(content: ASN1.OctetString): void {
55
+ const sequences = this.extractSequencesFromContent(content)
56
+ sequences.forEach(this.processSequence.bind(this))
57
+ }
58
+
59
+ private extractSequencesFromContent(content: ASN1.OctetString): ASN1.Sequence[] {
60
+ const [contentSet] = content.valueBlock.value as ASN1.Set[]
61
+ return contentSet.valueBlock.value
62
+ .filter(v => v instanceof ASN1.Sequence) as ASN1.Sequence[]
63
+ }
64
+
65
+ private processSequence(sequence: ASN1.Sequence): void {
66
+ const verifiedSequence = this.receiptVerifier.verifyFieldSchema(sequence)
67
+ if (verifiedSequence) {
68
+ this.handleVerifiedSequence(verifiedSequence)
69
+ }
70
+ }
71
+
72
+ private handleVerifiedSequence(verifiedSequence: ASN1.CompareSchemaSuccess): void {
73
+ const fieldKey = (verifiedSequence.result[FIELD_TYPE_ID] as ASN1.Integer).valueBlock.valueDec
74
+ const fieldValue = verifiedSequence.result[FIELD_VALUE_ID] as ASN1.OctetString
75
+
76
+ const handler = this.getFieldHandler(fieldKey)
77
+ handler(fieldValue)
78
+ }
79
+
80
+ private getFieldHandler(fieldKey: number): (fieldValue: ASN1.OctetString) => void {
81
+ if (fieldKey === IN_APP) {
82
+ return this.parseReceiptContent.bind(this)
83
+ }
84
+ if (this.isValidReceiptFieldKey(fieldKey)) {
85
+ const name = RECEIPT_FIELDS_MAP.get(fieldKey)!
86
+ return (fieldValue: ASN1.OctetString) => {
87
+ this.addFieldToReceipt(name, this.extractStringValue(fieldValue))
88
+ }
89
+ }
90
+ return () => {}
91
+ }
92
+
93
+ private isValidReceiptFieldKey(value: unknown): value is ReceiptFieldsKeyValues {
94
+ return typeof value === 'number' && RECEIPT_FIELDS_MAP.has(value as ReceiptFieldsKeyValues)
95
+ }
96
+
97
+ private extractStringValue(field: ASN1.OctetString): string {
98
+ const [fieldValue] = field.valueBlock.value
99
+
100
+ if (fieldValue instanceof ASN1.IA5String || fieldValue instanceof ASN1.Utf8String) {
101
+ return fieldValue.valueBlock.value
102
+ }
103
+
104
+ return field.toJSON().valueBlock.valueHex
105
+ }
106
+
107
+ private addFieldToReceipt(name: ReceiptFieldsKeyNames, value: string): void {
108
+ this.addToArrayFieldIfApplicable(name, value)
109
+ this.parsed[name] = value
110
+ }
111
+
112
+ private addToArrayFieldIfApplicable(name: ReceiptFieldsKeyNames, value: string): void {
113
+ const arrayFields: Record<string, keyof ParsedReceipt> = {
114
+ 'IN_APP_ORIGINAL_TRANSACTION_ID': 'IN_APP_ORIGINAL_TRANSACTION_IDS',
115
+ 'IN_APP_TRANSACTION_ID': 'IN_APP_TRANSACTION_IDS',
116
+ }
117
+
118
+ const arrayFieldName = arrayFields[name]
119
+ if (arrayFieldName) {
120
+ (this.parsed[arrayFieldName] as string[]).push(value)
121
+ }
122
+ }
123
+
124
+ private validateParsedFields(): void {
125
+ const missingFields = Array.from(RECEIPT_FIELDS_MAP.values())
126
+ .filter(fieldKey => !(fieldKey in this.parsed))
127
+
128
+ if (missingFields.length > 0) {
129
+ throw new Error(`Missing required fields: ${missingFields.join(', ')}`)
130
+ }
131
+ }
132
+
133
+ private deduplicateArrayFields(): void {
134
+ this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_ORIGINAL_TRANSACTION_IDS)
135
+ this.parsed.IN_APP_TRANSACTION_IDS = this.removeDuplicates(this.parsed.IN_APP_TRANSACTION_IDS)
136
+ }
137
+
138
+ private removeDuplicates(array: string[]): string[] {
139
+ return [...new Set(array)]
140
+ }
141
+ }
142
+
143
+ export function parseReceipt(receipt: string): ParsedReceipt {
144
+ return new ReceiptParser().parseReceipt(receipt)
145
+ }
@@ -0,0 +1,69 @@
1
+ import { Any, Constructed, Integer, ObjectIdentifier, OctetString, Sequence, Set, verifySchema } from 'asn1js'
2
+
3
+ import { CONTENT_ID, FIELD_TYPE_ID, FIELD_VALUE_ID } from './constants'
4
+
5
+ export class ReceiptVerifier {
6
+ private readonly receiptSchema: Sequence
7
+ private readonly fieldSchema: Sequence
8
+
9
+ constructor() {
10
+ this.receiptSchema = new Sequence({
11
+ value: [
12
+ new ObjectIdentifier(),
13
+ new Constructed({
14
+ idBlock: { tagClass: 3, tagNumber: 0 },
15
+ value: [
16
+ new Sequence({
17
+ value: [
18
+ new Integer(),
19
+ new Set({
20
+ value: [
21
+ new Sequence({
22
+ value: [new ObjectIdentifier(), new Any()],
23
+ }),
24
+ ],
25
+ }),
26
+ new Sequence({
27
+ value: [
28
+ new ObjectIdentifier(),
29
+ new Constructed({
30
+ idBlock: { tagClass: 3, tagNumber: 0 },
31
+ value: [new OctetString({ name: CONTENT_ID })],
32
+ }),
33
+ ],
34
+ }),
35
+ ],
36
+ }),
37
+ ],
38
+ }),
39
+ ],
40
+ })
41
+
42
+ this.fieldSchema = new Sequence({
43
+ value: [
44
+ new Integer({ name: FIELD_TYPE_ID }),
45
+ new Integer(),
46
+ new OctetString({ name: FIELD_VALUE_ID }),
47
+ ],
48
+ })
49
+ }
50
+
51
+ public verifyReceiptSchema(receipt: string) {
52
+ const receiptVerification = verifySchema(Buffer.from(receipt, 'base64'), this.receiptSchema)
53
+ if (!receiptVerification.verified) {
54
+ throw new Error('Receipt verification failed.')
55
+ }
56
+
57
+ return receiptVerification
58
+ }
59
+
60
+ public verifyFieldSchema(sequence: Sequence) {
61
+ const fieldVerification = verifySchema(sequence.toBER(), this.fieldSchema)
62
+ if (!fieldVerification.verified) {
63
+ // Return null if the field schema verification fails, so we can skip the field
64
+ return null
65
+ }
66
+
67
+ return fieldVerification
68
+ }
69
+ }
package/src/constants.ts CHANGED
@@ -9,3 +9,65 @@ export const FIELD_TYPE_ID: string = 'FieldType'
9
9
 
10
10
  /** Identifies field value information encoded as Octet string */
11
11
  export const FIELD_VALUE_ID: string = 'FieldTypeOctetString'
12
+
13
+ export type ReceiptFieldsKeyValues =
14
+ | 0 // Environment
15
+ | 2 // Bundle ID
16
+ | 3 // App version
17
+ | 4 // Opaque value
18
+ | 5 // SHA-1 hash
19
+ | 12 // Receipt creation date
20
+ | 18 // Original purchase date
21
+ | 19 // Original app version
22
+ | 1701 // In-app quantity
23
+ | 1702 // In-app product ID
24
+ | 1703 // In-app transaction ID
25
+ | 1704 // In-app purchase date
26
+ | 1705 // In-app original transaction ID
27
+ | 1706 // In-app original purchase date
28
+ | 1708 // In-app expires date
29
+ | 1711 // In-app web order line item ID
30
+ | 1712 // In-app cancellation date
31
+
32
+ export type ReceiptFieldsKeyNames =
33
+ | 'ENVIRONMENT'
34
+ | 'BUNDLE_ID'
35
+ | 'APP_VERSION'
36
+ | 'OPAQUE_VALUE'
37
+ | 'SHA1_HASH'
38
+ | 'RECEIPT_CREATION_DATE'
39
+ | 'ORIGINAL_PURCHASE_DATE'
40
+ | 'ORIGINAL_APP_VERSION'
41
+ | 'IN_APP_QUANTITY'
42
+ | 'IN_APP_PRODUCT_ID'
43
+ | 'IN_APP_TRANSACTION_ID'
44
+ | 'IN_APP_PURCHASE_DATE'
45
+ | 'IN_APP_ORIGINAL_TRANSACTION_ID'
46
+ | 'IN_APP_ORIGINAL_PURCHASE_DATE'
47
+ | 'IN_APP_EXPIRES_DATE'
48
+ | 'IN_APP_WEB_ORDER_LINE_ITEM_ID'
49
+ | 'IN_APP_CANCELLATION_DATE'
50
+
51
+ /**
52
+ * Receipt fields
53
+ * @see https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html
54
+ */
55
+ export const RECEIPT_FIELDS_MAP: ReadonlyMap<ReceiptFieldsKeyValues, ReceiptFieldsKeyNames> = new Map([
56
+ [0, 'ENVIRONMENT'],
57
+ [2, 'BUNDLE_ID'],
58
+ [3, 'APP_VERSION'],
59
+ [4, 'OPAQUE_VALUE'],
60
+ [5, 'SHA1_HASH'],
61
+ [12, 'RECEIPT_CREATION_DATE'],
62
+ [18, 'ORIGINAL_PURCHASE_DATE'],
63
+ [19, 'ORIGINAL_APP_VERSION'],
64
+ [1701, 'IN_APP_QUANTITY'],
65
+ [1702, 'IN_APP_PRODUCT_ID'],
66
+ [1703, 'IN_APP_TRANSACTION_ID'],
67
+ [1704, 'IN_APP_PURCHASE_DATE'],
68
+ [1705, 'IN_APP_ORIGINAL_TRANSACTION_ID'],
69
+ [1706, 'IN_APP_ORIGINAL_PURCHASE_DATE'],
70
+ [1708, 'IN_APP_EXPIRES_DATE'],
71
+ [1711, 'IN_APP_WEB_ORDER_LINE_ITEM_ID'],
72
+ [1712, 'IN_APP_CANCELLATION_DATE'],
73
+ ])
package/src/index.ts CHANGED
@@ -1,2 +1,2 @@
1
- export type { ParsedReceipt } from './parser'
2
- export { parseReceipt } from './parser'
1
+ export type { ParsedReceipt } from './ReceiptParser'
2
+ export { parseReceipt } from './ReceiptParser'
package/tsconfig.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "esModuleInterop": true,
7
7
  "declaration": true,
8
8
  "outDir": "./dist",
9
- "rootDir": "./src",
9
+ "rootDir": ".",
10
10
  "moduleResolution": "node"
11
11
  },
12
12
  "include": [
package/src/mappings.ts DELETED
@@ -1,61 +0,0 @@
1
- export type ReceiptFieldsKeyValues =
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
-
20
- export type ReceiptFieldsKeyNames =
21
- | 'ENVIRONMENT'
22
- | 'BUNDLE_ID'
23
- | 'APP_VERSION'
24
- | 'OPAQUE_VALUE'
25
- | 'SHA1_HASH'
26
- | 'RECEIPT_CREATION_DATE'
27
- | 'ORIGINAL_PURCHASE_DATE'
28
- | 'ORIGINAL_APP_VERSION'
29
- | 'IN_APP_QUANTITY'
30
- | 'IN_APP_PRODUCT_ID'
31
- | 'IN_APP_TRANSACTION_ID'
32
- | 'IN_APP_PURCHASE_DATE'
33
- | 'IN_APP_ORIGINAL_TRANSACTION_ID'
34
- | 'IN_APP_ORIGINAL_PURCHASE_DATE'
35
- | 'IN_APP_EXPIRES_DATE'
36
- | 'IN_APP_WEB_ORDER_LINE_ITEM_ID'
37
- | 'IN_APP_CANCELLATION_DATE'
38
-
39
- /**
40
- * Receipt fields
41
- * @see https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html
42
- */
43
- export const RECEIPT_FIELDS_MAP: ReadonlyMap<ReceiptFieldsKeyValues, ReceiptFieldsKeyNames> = new Map([
44
- [0, 'ENVIRONMENT'],
45
- [2, 'BUNDLE_ID'],
46
- [3, 'APP_VERSION'],
47
- [4, 'OPAQUE_VALUE'],
48
- [5, 'SHA1_HASH'],
49
- [12, 'RECEIPT_CREATION_DATE'],
50
- [18, 'ORIGINAL_PURCHASE_DATE'],
51
- [19, 'ORIGINAL_APP_VERSION'],
52
- [1701, 'IN_APP_QUANTITY'],
53
- [1702, 'IN_APP_PRODUCT_ID'],
54
- [1703, 'IN_APP_TRANSACTION_ID'],
55
- [1704, 'IN_APP_PURCHASE_DATE'],
56
- [1705, 'IN_APP_ORIGINAL_TRANSACTION_ID'],
57
- [1706, 'IN_APP_ORIGINAL_PURCHASE_DATE'],
58
- [1708, 'IN_APP_EXPIRES_DATE'],
59
- [1711, 'IN_APP_WEB_ORDER_LINE_ITEM_ID'],
60
- [1712, 'IN_APP_CANCELLATION_DATE'],
61
- ])
package/src/parser.ts DELETED
@@ -1,116 +0,0 @@
1
- import * as ASN1 from 'asn1js'
2
-
3
- import { RECEIPT_FIELDS_MAP, ReceiptFieldsKeyNames, ReceiptFieldsKeyValues } from './mappings'
4
- import { CONTENT_ID, FIELD_TYPE_ID, FIELD_VALUE_ID, IN_APP } from './constants'
5
- import { verifyFieldSchema, verifyReceiptSchema } from './verifications'
6
-
7
- export type Environment = 'Production' | 'ProductionSandbox' | string
8
-
9
- const uniqueArrayValues = (array: string[]) => Array.from(new Set(array))
10
-
11
- export type ParsedReceipt = Partial<Record<ReceiptFieldsKeyNames, string>> & {
12
- ENVIRONMENT: Environment
13
- IN_APP_ORIGINAL_TRANSACTION_IDS: string[]
14
- IN_APP_TRANSACTION_IDS: string[]
15
- }
16
-
17
- function isReceiptFieldKey (value: unknown): value is ReceiptFieldsKeyValues {
18
- return Boolean(typeof value === 'number' && RECEIPT_FIELDS_MAP.has(value as ReceiptFieldsKeyValues))
19
- }
20
-
21
- function isParsedReceiptContentComplete (data: ParsedReceipt): data is ParsedReceipt {
22
- for (const fieldKey of RECEIPT_FIELDS_MAP.values()) {
23
- if (!(fieldKey in data)) {
24
- return false
25
- }
26
- }
27
-
28
- return true
29
- }
30
-
31
- function extractFieldValue (field: ASN1.OctetString): string {
32
- const [fieldValue] = field.valueBlock.value
33
-
34
- if (fieldValue instanceof ASN1.IA5String || fieldValue instanceof ASN1.Utf8String) {
35
- return fieldValue.valueBlock.value
36
- }
37
-
38
- return field.toJSON().valueBlock.valueHex
39
- }
40
-
41
- function appendField (parsed: ParsedReceipt, name: ReceiptFieldsKeyNames, value: string) {
42
- if (name === 'IN_APP_ORIGINAL_TRANSACTION_ID') {
43
- parsed.IN_APP_ORIGINAL_TRANSACTION_IDS.push(value)
44
- }
45
-
46
- if (name === 'IN_APP_TRANSACTION_ID') {
47
- parsed.IN_APP_TRANSACTION_IDS.push(value)
48
- }
49
-
50
- parsed[name] = value
51
- }
52
-
53
- function processField (parsed: ParsedReceipt, fieldKey: number, fieldValue: ASN1.OctetString) {
54
- if (fieldKey === IN_APP) {
55
- parseOctetStringContent(parsed, fieldValue)
56
- return
57
- }
58
-
59
- if (!isReceiptFieldKey(fieldKey)) {
60
- return
61
- }
62
-
63
- const name = RECEIPT_FIELDS_MAP.get(fieldKey)!
64
- appendField(parsed, name, extractFieldValue(fieldValue))
65
- }
66
-
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[]
71
-
72
- for (const sequence of contentSetSequences) {
73
- const verifiedSequence = verifyFieldSchema(sequence)
74
- if (verifiedSequence) {
75
- // We are confident to use "as" assertion because Integer type is guaranteed by positive verification above
76
- const fieldKey = (verifiedSequence.result[FIELD_TYPE_ID] as ASN1.Integer).valueBlock.valueDec
77
- const fieldValueOctetString = verifiedSequence.result[FIELD_VALUE_ID] as ASN1.OctetString
78
-
79
- processField(parsed, fieldKey, fieldValueOctetString)
80
- }
81
- }
82
- }
83
-
84
- function postprocessParsedReceipt (parsed: ParsedReceipt) {
85
- parsed.IN_APP_ORIGINAL_TRANSACTION_IDS = uniqueArrayValues(parsed.IN_APP_ORIGINAL_TRANSACTION_IDS)
86
- parsed.IN_APP_TRANSACTION_IDS = uniqueArrayValues(parsed.IN_APP_TRANSACTION_IDS)
87
- }
88
-
89
- export function parseReceipt (receipt: string): ParsedReceipt {
90
- const rootSchemaVerification = verifyReceiptSchema(receipt)
91
-
92
- const content = rootSchemaVerification.result[CONTENT_ID] as ASN1.OctetString
93
- const parsed: ParsedReceipt = {
94
- ENVIRONMENT: 'Production',
95
- IN_APP_ORIGINAL_TRANSACTION_IDS: [],
96
- IN_APP_TRANSACTION_IDS: [],
97
- }
98
-
99
- parseOctetStringContent(parsed, content)
100
-
101
- // Verify if the parsed content contains all the required fields
102
- if (!isParsedReceiptContentComplete(parsed)) {
103
- const missingProps = []
104
- for (const fieldKey of RECEIPT_FIELDS_MAP.values()) {
105
- if (!(fieldKey in parsed)) {
106
- missingProps.push(fieldKey)
107
- }
108
- }
109
-
110
- throw new Error(`Missing required fields: ${missingProps.join(', ')}`)
111
- }
112
-
113
- postprocessParsedReceipt(parsed)
114
-
115
- return parsed as ParsedReceipt
116
- }
package/src/utils.ts DELETED
@@ -1 +0,0 @@
1
-
@@ -1,62 +0,0 @@
1
- import { Any, Constructed, Integer, ObjectIdentifier, OctetString, Sequence, Set, verifySchema } from 'asn1js'
2
-
3
- import { CONTENT_ID, FIELD_TYPE_ID, FIELD_VALUE_ID } from './constants'
4
-
5
- const receiptSchema = new Sequence({
6
- value: [
7
- new ObjectIdentifier(),
8
- new Constructed({
9
- idBlock: { tagClass: 3, tagNumber: 0 },
10
- value: [
11
- new Sequence({
12
- value: [
13
- new Integer(),
14
- new Set({
15
- value: [
16
- new Sequence({
17
- value: [new ObjectIdentifier(), new Any()],
18
- }),
19
- ],
20
- }),
21
- new Sequence({
22
- value: [
23
- new ObjectIdentifier(),
24
- new Constructed({
25
- idBlock: { tagClass: 3, tagNumber: 0 },
26
- value: [new OctetString({ name: CONTENT_ID })],
27
- }),
28
- ],
29
- }),
30
- ],
31
- }),
32
- ],
33
- }),
34
- ],
35
- })
36
-
37
- const fieldSchema = new Sequence({
38
- value: [
39
- new Integer({ name: FIELD_TYPE_ID }),
40
- new Integer(),
41
- new OctetString({ name: FIELD_VALUE_ID }),
42
- ],
43
- })
44
-
45
- export function verifyReceiptSchema (receipt: string) {
46
- const receiptVerification = verifySchema(Buffer.from(receipt, 'base64'), receiptSchema)
47
- if (!receiptVerification.verified) {
48
- throw new Error('Receipt verification failed.')
49
- }
50
-
51
- return receiptVerification
52
- }
53
-
54
- export function verifyFieldSchema (sequence: Sequence) {
55
- const fieldVerification = verifySchema(sequence.toBER(), fieldSchema)
56
- if (!fieldVerification.verified) {
57
- // Return null if the field schema verification fails, so we can skip the field
58
- return null
59
- }
60
-
61
- return fieldVerification
62
- }