@onesub/server 0.24.0 → 0.25.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/__tests__/field-vocabulary.test.d.ts +25 -0
- package/dist/__tests__/field-vocabulary.test.d.ts.map +1 -0
- package/dist/__tests__/provider-log-fields.test.d.ts +27 -0
- package/dist/__tests__/provider-log-fields.test.d.ts.map +1 -0
- package/dist/index.cjs +135 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +135 -64
- package/dist/index.js.map +1 -1
- package/dist/log-format.d.ts +16 -4
- package/dist/log-format.d.ts.map +1 -1
- package/dist/providers/apple.d.ts.map +1 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -617,22 +617,23 @@ async function validateAppleReceipt(receipt, config) {
|
|
|
617
617
|
} catch (err2) {
|
|
618
618
|
const preview = receipt.slice(0, 60);
|
|
619
619
|
const parts = receipt.split(".").length;
|
|
620
|
-
log.warn(
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
620
|
+
log.warn("[onesub/apple] Failed to decode receipt as JWS", {
|
|
621
|
+
receiptPreview: preview,
|
|
622
|
+
receiptLength: receipt.length,
|
|
623
|
+
jwsParts: parts,
|
|
624
|
+
err: err2
|
|
625
|
+
});
|
|
625
626
|
return null;
|
|
626
627
|
}
|
|
627
628
|
if (!tx.originalTransactionId || !tx.productId || !tx.expiresDate) {
|
|
628
629
|
return null;
|
|
629
630
|
}
|
|
630
631
|
if (!tx.bundleId || tx.bundleId !== config.bundleId) {
|
|
631
|
-
log.warn("[onesub/apple] Bundle ID mismatch
|
|
632
|
+
log.warn("[onesub/apple] Bundle ID mismatch", { bundleId: tx.bundleId, expected: config.bundleId });
|
|
632
633
|
return null;
|
|
633
634
|
}
|
|
634
635
|
if (process.env["NODE_ENV"] === "production" && tx.environment !== "Production" && process.env["ONESUB_ALLOW_SANDBOX"] !== "true") {
|
|
635
|
-
log.warn("[onesub/apple] Sandbox receipt rejected in production
|
|
636
|
+
log.warn("[onesub/apple] Sandbox receipt rejected in production", { environment: tx.environment });
|
|
636
637
|
return null;
|
|
637
638
|
}
|
|
638
639
|
const status = deriveStatus(tx, null);
|
|
@@ -672,23 +673,25 @@ async function validateAppleConsumableReceipt(signedTransaction, config, expecte
|
|
|
672
673
|
const preview = signedTransaction.slice(0, 60);
|
|
673
674
|
const parts = signedTransaction.split(".").length;
|
|
674
675
|
const looksLikeJws = parts === 3;
|
|
675
|
-
log.warn(
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
676
|
+
log.warn("[onesub/apple] Failed to decode consumable JWS", {
|
|
677
|
+
receiptPreview: preview,
|
|
678
|
+
receiptLength: signedTransaction.length,
|
|
679
|
+
jwsParts: parts,
|
|
680
|
+
looksLikeJws,
|
|
681
|
+
err: err2
|
|
682
|
+
});
|
|
680
683
|
return null;
|
|
681
684
|
}
|
|
682
685
|
if (!tx.bundleId || tx.bundleId !== config.bundleId) {
|
|
683
|
-
log.warn("[onesub/apple] Bundle ID mismatch
|
|
686
|
+
log.warn("[onesub/apple] Bundle ID mismatch", { bundleId: tx.bundleId, expected: config.bundleId });
|
|
684
687
|
return null;
|
|
685
688
|
}
|
|
686
689
|
if (tx.type !== "Consumable" && tx.type !== "Non-Consumable") {
|
|
687
|
-
log.warn("[onesub/apple] Invalid purchase type for product validation
|
|
690
|
+
log.warn("[onesub/apple] Invalid purchase type for product validation", { type: tx.type });
|
|
688
691
|
return null;
|
|
689
692
|
}
|
|
690
693
|
if (process.env["NODE_ENV"] === "production" && tx.environment !== "Production" && process.env["ONESUB_ALLOW_SANDBOX"] !== "true") {
|
|
691
|
-
log.warn("[onesub/apple] Sandbox receipt rejected in production
|
|
694
|
+
log.warn("[onesub/apple] Sandbox receipt rejected in production", { environment: tx.environment });
|
|
692
695
|
return null;
|
|
693
696
|
}
|
|
694
697
|
if (!tx.productId) {
|
|
@@ -696,21 +699,28 @@ async function validateAppleConsumableReceipt(signedTransaction, config, expecte
|
|
|
696
699
|
return null;
|
|
697
700
|
}
|
|
698
701
|
if (expectedProductId && tx.productId !== expectedProductId) {
|
|
699
|
-
log.warn("[onesub/apple] Product ID mismatch
|
|
702
|
+
log.warn("[onesub/apple] Product ID mismatch", { productId: tx.productId, expected: expectedProductId });
|
|
700
703
|
return null;
|
|
701
704
|
}
|
|
702
705
|
if (tx.revocationDate) {
|
|
703
|
-
log.warn("[onesub/apple] Purchase was revoked/refunded"
|
|
706
|
+
log.warn("[onesub/apple] Purchase was revoked/refunded", {
|
|
707
|
+
productId: tx.productId,
|
|
708
|
+
transactionId: tx.transactionId
|
|
709
|
+
});
|
|
704
710
|
return null;
|
|
705
711
|
}
|
|
706
712
|
const maxAgeMs = (config.productReceiptMaxAgeHours ?? 72) * 60 * 60 * 1e3;
|
|
707
713
|
if (tx.purchaseDate && Date.now() - tx.purchaseDate > maxAgeMs) {
|
|
708
|
-
log.warn(
|
|
714
|
+
log.warn("[onesub/apple] Consumable receipt too old", {
|
|
715
|
+
productId: tx.productId,
|
|
716
|
+
maxAgeHours: config.productReceiptMaxAgeHours ?? 72,
|
|
717
|
+
purchaseDate: new Date(tx.purchaseDate).toISOString()
|
|
718
|
+
});
|
|
709
719
|
return null;
|
|
710
720
|
}
|
|
711
721
|
const transactionId = tx.transactionId ?? tx.originalTransactionId;
|
|
712
722
|
if (!transactionId) {
|
|
713
|
-
log.warn("[onesub/apple] No transactionId in consumable transaction");
|
|
723
|
+
log.warn("[onesub/apple] No transactionId in consumable transaction", { productId: tx.productId });
|
|
714
724
|
return null;
|
|
715
725
|
}
|
|
716
726
|
return {
|
|
@@ -789,7 +799,7 @@ async function sendAppleConsumptionResponse(transactionId, body, config, options
|
|
|
789
799
|
try {
|
|
790
800
|
jwt = await makeAppleApiJwt(config);
|
|
791
801
|
} catch (err2) {
|
|
792
|
-
log.warn("[onesub/apple] Cannot send consumption response \u2014 JWT mint failed
|
|
802
|
+
log.warn("[onesub/apple] Cannot send consumption response \u2014 JWT mint failed", { transactionId, err: err2 });
|
|
793
803
|
return;
|
|
794
804
|
}
|
|
795
805
|
const host = options?.sandbox ? "api.storekit-sandbox.itunes.apple.com" : "api.storekit.itunes.apple.com";
|
|
@@ -805,10 +815,14 @@ async function sendAppleConsumptionResponse(transactionId, body, config, options
|
|
|
805
815
|
});
|
|
806
816
|
if (!resp.ok) {
|
|
807
817
|
const text = await resp.text();
|
|
808
|
-
log.warn(
|
|
818
|
+
log.warn("[onesub/apple] Consumption response API error", {
|
|
819
|
+
httpStatus: resp.status,
|
|
820
|
+
transactionId,
|
|
821
|
+
responseBody: text
|
|
822
|
+
});
|
|
809
823
|
}
|
|
810
824
|
} catch (err2) {
|
|
811
|
-
log.warn("[onesub/apple] Consumption response network error
|
|
825
|
+
log.warn("[onesub/apple] Consumption response network error", { transactionId, err: err2 });
|
|
812
826
|
}
|
|
813
827
|
}
|
|
814
828
|
var APPLE_SUBSCRIPTION_STATUS_CODE = {
|
|
@@ -839,7 +853,10 @@ async function fetchAppleSubscriptionStatus(originalTransactionId, config, optio
|
|
|
839
853
|
try {
|
|
840
854
|
jwt = await makeAppleApiJwt(config);
|
|
841
855
|
} catch (err2) {
|
|
842
|
-
log.warn("[onesub/apple] Cannot fetch subscription status \u2014 JWT mint failed
|
|
856
|
+
log.warn("[onesub/apple] Cannot fetch subscription status \u2014 JWT mint failed", {
|
|
857
|
+
originalTransactionId,
|
|
858
|
+
err: err2
|
|
859
|
+
});
|
|
843
860
|
return null;
|
|
844
861
|
}
|
|
845
862
|
const host = options?.sandbox ? "api.storekit-sandbox.itunes.apple.com" : "api.storekit.itunes.apple.com";
|
|
@@ -851,24 +868,31 @@ async function fetchAppleSubscriptionStatus(originalTransactionId, config, optio
|
|
|
851
868
|
});
|
|
852
869
|
if (!resp.ok) {
|
|
853
870
|
const text = await resp.text();
|
|
854
|
-
log.warn(
|
|
871
|
+
log.warn("[onesub/apple] Status API error", {
|
|
872
|
+
httpStatus: resp.status,
|
|
873
|
+
originalTransactionId,
|
|
874
|
+
responseBody: text
|
|
875
|
+
});
|
|
855
876
|
return null;
|
|
856
877
|
}
|
|
857
878
|
body = await resp.json();
|
|
858
879
|
} catch (err2) {
|
|
859
|
-
log.warn("[onesub/apple] Status API network error
|
|
880
|
+
log.warn("[onesub/apple] Status API network error", { originalTransactionId, err: err2 });
|
|
860
881
|
return null;
|
|
861
882
|
}
|
|
862
883
|
const entry = body.data?.flatMap((g) => g.lastTransactions ?? []).find((t) => t.originalTransactionId === originalTransactionId);
|
|
863
884
|
if (!entry || entry.status == null || !entry.signedTransactionInfo) {
|
|
864
|
-
log.warn("[onesub/apple] Status API returned no matching transaction
|
|
885
|
+
log.warn("[onesub/apple] Status API returned no matching transaction", { originalTransactionId });
|
|
865
886
|
return null;
|
|
866
887
|
}
|
|
867
888
|
let tx;
|
|
868
889
|
try {
|
|
869
890
|
tx = await decodeJws(entry.signedTransactionInfo, config.skipJwsVerification);
|
|
870
891
|
} catch (err2) {
|
|
871
|
-
log.warn("[onesub/apple] Failed to decode signedTransactionInfo from Status API
|
|
892
|
+
log.warn("[onesub/apple] Failed to decode signedTransactionInfo from Status API", {
|
|
893
|
+
originalTransactionId,
|
|
894
|
+
err: err2
|
|
895
|
+
});
|
|
872
896
|
return null;
|
|
873
897
|
}
|
|
874
898
|
let renewal = null;
|
|
@@ -879,7 +903,10 @@ async function fetchAppleSubscriptionStatus(originalTransactionId, config, optio
|
|
|
879
903
|
}
|
|
880
904
|
}
|
|
881
905
|
if (!tx.productId || !tx.expiresDate) {
|
|
882
|
-
log.warn("[onesub/apple] Status API transaction missing productId or expiresDate"
|
|
906
|
+
log.warn("[onesub/apple] Status API transaction missing productId or expiresDate", {
|
|
907
|
+
originalTransactionId,
|
|
908
|
+
productId: tx.productId
|
|
909
|
+
});
|
|
883
910
|
return null;
|
|
884
911
|
}
|
|
885
912
|
const status = mapAppleStatusCode(entry.status);
|
|
@@ -903,7 +930,10 @@ async function fetchAppleTransactionHistory(originalTransactionId, config, optio
|
|
|
903
930
|
try {
|
|
904
931
|
jwt = await makeAppleApiJwt(config);
|
|
905
932
|
} catch (err2) {
|
|
906
|
-
log.warn("[onesub/apple] Cannot fetch transaction history \u2014 JWT mint failed
|
|
933
|
+
log.warn("[onesub/apple] Cannot fetch transaction history \u2014 JWT mint failed", {
|
|
934
|
+
originalTransactionId,
|
|
935
|
+
err: err2
|
|
936
|
+
});
|
|
907
937
|
return null;
|
|
908
938
|
}
|
|
909
939
|
const host = options?.sandbox ? "api.storekit-sandbox.itunes.apple.com" : "api.storekit.itunes.apple.com";
|
|
@@ -920,12 +950,16 @@ async function fetchAppleTransactionHistory(originalTransactionId, config, optio
|
|
|
920
950
|
});
|
|
921
951
|
if (!resp.ok) {
|
|
922
952
|
const text = await resp.text();
|
|
923
|
-
log.warn(
|
|
953
|
+
log.warn("[onesub/apple] Transaction History API error", {
|
|
954
|
+
httpStatus: resp.status,
|
|
955
|
+
originalTransactionId,
|
|
956
|
+
responseBody: text
|
|
957
|
+
});
|
|
924
958
|
return null;
|
|
925
959
|
}
|
|
926
960
|
page = await resp.json();
|
|
927
961
|
} catch (err2) {
|
|
928
|
-
log.warn("[onesub/apple] Transaction History API network error
|
|
962
|
+
log.warn("[onesub/apple] Transaction History API network error", { originalTransactionId, err: err2 });
|
|
929
963
|
return null;
|
|
930
964
|
}
|
|
931
965
|
for (const signed of page.signedTransactions ?? []) {
|
|
@@ -950,14 +984,16 @@ async function fetchAppleTransactionHistory(originalTransactionId, config, optio
|
|
|
950
984
|
if (!page.hasMore) break;
|
|
951
985
|
if (!page.revision || page.revision === revision) {
|
|
952
986
|
log.warn(
|
|
953
|
-
"[onesub/apple] Transaction History API returned hasMore without a new revision cursor \u2014 stopping pagination (partial history returned)"
|
|
987
|
+
"[onesub/apple] Transaction History API returned hasMore without a new revision cursor \u2014 stopping pagination (partial history returned)",
|
|
988
|
+
{ originalTransactionId, pageCount }
|
|
954
989
|
);
|
|
955
990
|
break;
|
|
956
991
|
}
|
|
957
992
|
if (pageCount >= MAX_HISTORY_PAGES) {
|
|
958
|
-
log.warn(
|
|
959
|
-
|
|
960
|
-
|
|
993
|
+
log.warn("[onesub/apple] Transaction History pagination hit the page cap \u2014 stopping (partial history returned)", {
|
|
994
|
+
originalTransactionId,
|
|
995
|
+
maxPages: MAX_HISTORY_PAGES
|
|
996
|
+
});
|
|
961
997
|
break;
|
|
962
998
|
}
|
|
963
999
|
revision = page.revision;
|
|
@@ -1107,7 +1143,7 @@ async function acknowledgeGoogleSubscription(purchaseToken, productId, config) {
|
|
|
1107
1143
|
try {
|
|
1108
1144
|
accessToken = await getCachedAccessToken(config.serviceAccountKey);
|
|
1109
1145
|
} catch (err2) {
|
|
1110
|
-
log.warn("[onesub/google] Could not get access token for subscription ack
|
|
1146
|
+
log.warn("[onesub/google] Could not get access token for subscription ack", { productId, err: err2 });
|
|
1111
1147
|
return;
|
|
1112
1148
|
}
|
|
1113
1149
|
const url = `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${encodeURIComponent(config.packageName)}/purchases/subscriptions/${encodeURIComponent(productId)}/tokens/${encodeURIComponent(purchaseToken)}:acknowledge`;
|
|
@@ -1119,10 +1155,14 @@ async function acknowledgeGoogleSubscription(purchaseToken, productId, config) {
|
|
|
1119
1155
|
});
|
|
1120
1156
|
if (!resp.ok) {
|
|
1121
1157
|
const body = await resp.text();
|
|
1122
|
-
log.warn(
|
|
1158
|
+
log.warn("[onesub/google] Subscription acknowledge API error \u2014 auto-refund risk", {
|
|
1159
|
+
httpStatus: resp.status,
|
|
1160
|
+
productId,
|
|
1161
|
+
responseBody: body
|
|
1162
|
+
});
|
|
1123
1163
|
}
|
|
1124
1164
|
} catch (err2) {
|
|
1125
|
-
log.warn("[onesub/google] Subscription acknowledge network error \u2014 auto-refund risk
|
|
1165
|
+
log.warn("[onesub/google] Subscription acknowledge network error \u2014 auto-refund risk", { productId, err: err2 });
|
|
1126
1166
|
}
|
|
1127
1167
|
}
|
|
1128
1168
|
async function acknowledgeGoogleProduct(purchaseToken, productId, config) {
|
|
@@ -1133,7 +1173,7 @@ async function acknowledgeGoogleProduct(purchaseToken, productId, config) {
|
|
|
1133
1173
|
try {
|
|
1134
1174
|
accessToken = await getCachedAccessToken(config.serviceAccountKey);
|
|
1135
1175
|
} catch (err2) {
|
|
1136
|
-
log.warn("[onesub/google] Could not get access token for product ack
|
|
1176
|
+
log.warn("[onesub/google] Could not get access token for product ack", { productId, err: err2 });
|
|
1137
1177
|
return;
|
|
1138
1178
|
}
|
|
1139
1179
|
const url = `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${encodeURIComponent(config.packageName)}/purchases/products/${encodeURIComponent(productId)}/tokens/${encodeURIComponent(purchaseToken)}:acknowledge`;
|
|
@@ -1145,10 +1185,14 @@ async function acknowledgeGoogleProduct(purchaseToken, productId, config) {
|
|
|
1145
1185
|
});
|
|
1146
1186
|
if (!resp.ok) {
|
|
1147
1187
|
const body = await resp.text();
|
|
1148
|
-
log.warn(
|
|
1188
|
+
log.warn("[onesub/google] Product acknowledge API error \u2014 auto-refund risk", {
|
|
1189
|
+
httpStatus: resp.status,
|
|
1190
|
+
productId,
|
|
1191
|
+
responseBody: body
|
|
1192
|
+
});
|
|
1149
1193
|
}
|
|
1150
1194
|
} catch (err2) {
|
|
1151
|
-
log.warn("[onesub/google] Product acknowledge network error \u2014 auto-refund risk
|
|
1195
|
+
log.warn("[onesub/google] Product acknowledge network error \u2014 auto-refund risk", { productId, err: err2 });
|
|
1152
1196
|
}
|
|
1153
1197
|
}
|
|
1154
1198
|
async function consumeGoogleProductReceipt(purchaseToken, productId, config) {
|
|
@@ -1158,7 +1202,7 @@ async function consumeGoogleProductReceipt(purchaseToken, productId, config) {
|
|
|
1158
1202
|
try {
|
|
1159
1203
|
accessToken = await getCachedAccessToken(config.serviceAccountKey);
|
|
1160
1204
|
} catch (err2) {
|
|
1161
|
-
log.warn("[onesub/google] Could not get access token for consume
|
|
1205
|
+
log.warn("[onesub/google] Could not get access token for consume", { productId, err: err2 });
|
|
1162
1206
|
return;
|
|
1163
1207
|
}
|
|
1164
1208
|
const url = `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${encodeURIComponent(config.packageName)}/purchases/products/${encodeURIComponent(productId)}/tokens/${encodeURIComponent(purchaseToken)}:consume`;
|
|
@@ -1169,10 +1213,14 @@ async function consumeGoogleProductReceipt(purchaseToken, productId, config) {
|
|
|
1169
1213
|
});
|
|
1170
1214
|
if (!resp.ok) {
|
|
1171
1215
|
const body = await resp.text();
|
|
1172
|
-
log.warn(
|
|
1216
|
+
log.warn("[onesub/google] Consume API error \u2014 auto-refund risk", {
|
|
1217
|
+
httpStatus: resp.status,
|
|
1218
|
+
productId,
|
|
1219
|
+
responseBody: body
|
|
1220
|
+
});
|
|
1173
1221
|
}
|
|
1174
1222
|
} catch (err2) {
|
|
1175
|
-
log.warn("[onesub/google] Consume network error \u2014 auto-refund risk
|
|
1223
|
+
log.warn("[onesub/google] Consume network error \u2014 auto-refund risk", { productId, err: err2 });
|
|
1176
1224
|
}
|
|
1177
1225
|
}
|
|
1178
1226
|
function deriveStatusV2(state) {
|
|
@@ -1198,11 +1246,11 @@ function deriveStatusV2(state) {
|
|
|
1198
1246
|
async function validateGoogleReceipt(receipt, productId, config) {
|
|
1199
1247
|
if (config.mockMode) return mockValidateGoogleSubscription(receipt, productId);
|
|
1200
1248
|
if (!config.serviceAccountKey) {
|
|
1201
|
-
log.warn("[onesub/google] No serviceAccountKey provided \u2014 cannot call Play API");
|
|
1249
|
+
log.warn("[onesub/google] No serviceAccountKey provided \u2014 cannot call Play API", { productId });
|
|
1202
1250
|
return null;
|
|
1203
1251
|
}
|
|
1204
1252
|
if (!config.packageName) {
|
|
1205
|
-
log.warn("[onesub/google] No packageName provided \u2014 cannot call Play API");
|
|
1253
|
+
log.warn("[onesub/google] No packageName provided \u2014 cannot call Play API", { productId });
|
|
1206
1254
|
return null;
|
|
1207
1255
|
}
|
|
1208
1256
|
let purchase;
|
|
@@ -1210,25 +1258,27 @@ async function validateGoogleReceipt(receipt, productId, config) {
|
|
|
1210
1258
|
const token = await getCachedAccessToken(config.serviceAccountKey);
|
|
1211
1259
|
purchase = await fetchSubscriptionPurchaseV2(config.packageName, receipt, token);
|
|
1212
1260
|
} catch (err2) {
|
|
1213
|
-
log.error("[onesub/google] Receipt validation failed
|
|
1261
|
+
log.error("[onesub/google] Receipt validation failed", {
|
|
1262
|
+
productId,
|
|
1263
|
+
packageName: config.packageName,
|
|
1264
|
+
err: err2
|
|
1265
|
+
});
|
|
1214
1266
|
return null;
|
|
1215
1267
|
}
|
|
1216
1268
|
const status = deriveStatusV2(purchase.subscriptionState);
|
|
1217
1269
|
if (!status) {
|
|
1218
|
-
log.warn(
|
|
1219
|
-
|
|
1220
|
-
purchase.subscriptionState
|
|
1221
|
-
);
|
|
1270
|
+
log.warn("[onesub/google] Unrecognised or pending subscriptionState \u2014 rejecting", {
|
|
1271
|
+
productId,
|
|
1272
|
+
subscriptionState: purchase.subscriptionState
|
|
1273
|
+
});
|
|
1222
1274
|
return null;
|
|
1223
1275
|
}
|
|
1224
1276
|
const lineItem = purchase.lineItems?.find((item) => item.productId === productId);
|
|
1225
1277
|
if (!lineItem) {
|
|
1226
|
-
log.warn(
|
|
1227
|
-
"[onesub/google] productId not found in subscription lineItems:",
|
|
1278
|
+
log.warn("[onesub/google] productId not found in subscription lineItems", {
|
|
1228
1279
|
productId,
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
);
|
|
1280
|
+
availableProductIds: purchase.lineItems?.map((i) => i.productId) ?? []
|
|
1281
|
+
});
|
|
1232
1282
|
return null;
|
|
1233
1283
|
}
|
|
1234
1284
|
const expiresAt = lineItem.expiryTime ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1260,11 +1310,11 @@ async function validateGoogleReceipt(receipt, productId, config) {
|
|
|
1260
1310
|
async function validateGoogleProductReceipt(purchaseToken, productId, config, type = "non_consumable") {
|
|
1261
1311
|
if (config.mockMode) return mockValidateGoogleProduct(purchaseToken, productId);
|
|
1262
1312
|
if (!config.serviceAccountKey) {
|
|
1263
|
-
log.warn("[onesub/google] No serviceAccountKey \u2014 cannot validate product receipt");
|
|
1313
|
+
log.warn("[onesub/google] No serviceAccountKey \u2014 cannot validate product receipt", { productId });
|
|
1264
1314
|
return null;
|
|
1265
1315
|
}
|
|
1266
1316
|
if (!config.packageName) {
|
|
1267
|
-
log.warn("[onesub/google] No packageName \u2014 cannot validate product receipt");
|
|
1317
|
+
log.warn("[onesub/google] No packageName \u2014 cannot validate product receipt", { productId });
|
|
1268
1318
|
return null;
|
|
1269
1319
|
}
|
|
1270
1320
|
let purchase;
|
|
@@ -1272,27 +1322,44 @@ async function validateGoogleProductReceipt(purchaseToken, productId, config, ty
|
|
|
1272
1322
|
const token = await getCachedAccessToken(config.serviceAccountKey);
|
|
1273
1323
|
purchase = await fetchProductPurchase(config.packageName, productId, purchaseToken, token);
|
|
1274
1324
|
} catch (err2) {
|
|
1275
|
-
log.error("[onesub/google] Product receipt validation failed
|
|
1325
|
+
log.error("[onesub/google] Product receipt validation failed", {
|
|
1326
|
+
productId,
|
|
1327
|
+
packageName: config.packageName,
|
|
1328
|
+
type,
|
|
1329
|
+
err: err2
|
|
1330
|
+
});
|
|
1276
1331
|
return null;
|
|
1277
1332
|
}
|
|
1278
1333
|
if (purchase.purchaseState !== 0) {
|
|
1279
|
-
log.warn("[onesub/google] Purchase not completed
|
|
1334
|
+
log.warn("[onesub/google] Purchase not completed", {
|
|
1335
|
+
productId,
|
|
1336
|
+
purchaseState: purchase.purchaseState,
|
|
1337
|
+
orderId: purchase.orderId
|
|
1338
|
+
});
|
|
1280
1339
|
return null;
|
|
1281
1340
|
}
|
|
1282
1341
|
if (type === "consumable" && purchase.consumptionState === 1) {
|
|
1283
|
-
log.warn("[onesub/google] Consumable already consumed \u2014 possible replay attack"
|
|
1342
|
+
log.warn("[onesub/google] Consumable already consumed \u2014 possible replay attack", {
|
|
1343
|
+
productId,
|
|
1344
|
+
orderId: purchase.orderId
|
|
1345
|
+
});
|
|
1284
1346
|
return null;
|
|
1285
1347
|
}
|
|
1286
1348
|
if (purchase.purchaseTimeMillis) {
|
|
1287
1349
|
const purchaseTime = parseInt(purchase.purchaseTimeMillis, 10);
|
|
1288
1350
|
const maxAgeMs = (config.productReceiptMaxAgeHours ?? 72) * 60 * 60 * 1e3;
|
|
1289
1351
|
if (Date.now() - purchaseTime > maxAgeMs) {
|
|
1290
|
-
log.warn(
|
|
1352
|
+
log.warn("[onesub/google] Product receipt too old", {
|
|
1353
|
+
productId,
|
|
1354
|
+
orderId: purchase.orderId,
|
|
1355
|
+
maxAgeHours: config.productReceiptMaxAgeHours ?? 72,
|
|
1356
|
+
purchaseDate: new Date(purchaseTime).toISOString()
|
|
1357
|
+
});
|
|
1291
1358
|
return null;
|
|
1292
1359
|
}
|
|
1293
1360
|
}
|
|
1294
1361
|
if (!purchase.orderId) {
|
|
1295
|
-
log.warn("[onesub/google] No orderId in product purchase");
|
|
1362
|
+
log.warn("[onesub/google] No orderId in product purchase", { productId });
|
|
1296
1363
|
return null;
|
|
1297
1364
|
}
|
|
1298
1365
|
return {
|
|
@@ -1349,7 +1416,11 @@ function decodeGoogleOneTimeProductNotification(payload) {
|
|
|
1349
1416
|
if (!notification.oneTimeProductNotification) return null;
|
|
1350
1417
|
const { notificationType, purchaseToken, sku } = notification.oneTimeProductNotification;
|
|
1351
1418
|
if (notificationType !== 1 && notificationType !== 2) {
|
|
1352
|
-
log.warn("[onesub/google] Unknown oneTimeProductNotification type
|
|
1419
|
+
log.warn("[onesub/google] Unknown oneTimeProductNotification type", {
|
|
1420
|
+
notificationType,
|
|
1421
|
+
productId: sku,
|
|
1422
|
+
packageName: notification.packageName
|
|
1423
|
+
});
|
|
1353
1424
|
return null;
|
|
1354
1425
|
}
|
|
1355
1426
|
return { notificationType, purchaseToken, sku, packageName: notification.packageName };
|