@onesub/server 0.24.0 → 0.26.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/__tests__/route-log-fields.test.d.ts +20 -0
- package/dist/__tests__/route-log-fields.test.d.ts.map +1 -0
- package/dist/apps.d.ts.map +1 -1
- package/dist/index.cjs +253 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +253 -132
- package/dist/index.js.map +1 -1
- package/dist/log-format.d.ts +18 -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/dist/routes/admin.d.ts.map +1 -1
- package/dist/routes/purchase.d.ts.map +1 -1
- package/dist/routes/validate.d.ts.map +1 -1
- package/dist/routes/webhook-apple.d.ts.map +1 -1
- package/dist/routes/webhook-google.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -442,7 +442,7 @@ function outcomePasses(outcome, tag) {
|
|
|
442
442
|
throw new Error(`[onesub/mock/${tag}] simulated upstream network error`);
|
|
443
443
|
}
|
|
444
444
|
if (outcome.kind === "valid" || outcome.kind === "sandbox") return true;
|
|
445
|
-
log.warn(
|
|
445
|
+
log.warn("[onesub/mock] receipt rejected", { provider: tag, outcome: outcome.kind });
|
|
446
446
|
return false;
|
|
447
447
|
}
|
|
448
448
|
function deterministicTransactionId(prefix, receipt) {
|
|
@@ -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 };
|
|
@@ -1423,7 +1494,10 @@ function buildAppRegistry(config) {
|
|
|
1423
1494
|
// that would validate one app's receipt against another's credentials.
|
|
1424
1495
|
(config.apple || config.google || apps.length === 1 ? apps[0] : void 0);
|
|
1425
1496
|
if (apps.length > 1) {
|
|
1426
|
-
log.info("[onesub] Multi-app mode
|
|
1497
|
+
log.info("[onesub] Multi-app mode", {
|
|
1498
|
+
appIds: apps.map(appName),
|
|
1499
|
+
defaultAppId: defaultApp ? appName(defaultApp) : null
|
|
1500
|
+
});
|
|
1427
1501
|
}
|
|
1428
1502
|
function match(hint) {
|
|
1429
1503
|
if (hint.appId) {
|
|
@@ -1431,13 +1505,13 @@ function buildAppRegistry(config) {
|
|
|
1431
1505
|
(a) => a.id === hint.appId || a.apple?.bundleId === hint.appId || a.google?.packageName === hint.appId
|
|
1432
1506
|
);
|
|
1433
1507
|
if (byId) return byId;
|
|
1434
|
-
log.warn("[onesub] Unknown appId
|
|
1508
|
+
log.warn("[onesub] Unknown appId", { appId: hint.appId });
|
|
1435
1509
|
return void 0;
|
|
1436
1510
|
}
|
|
1437
1511
|
if (hint.bundleId) {
|
|
1438
1512
|
const byBundle = apps.find((a) => a.apple?.bundleId === hint.bundleId);
|
|
1439
1513
|
if (byBundle) return byBundle;
|
|
1440
|
-
log.warn("[onesub] No app configured for bundleId
|
|
1514
|
+
log.warn("[onesub] No app configured for bundleId", { bundleId: hint.bundleId });
|
|
1441
1515
|
return void 0;
|
|
1442
1516
|
}
|
|
1443
1517
|
return defaultApp;
|
|
@@ -1525,9 +1599,10 @@ function createValidateRouter(config, store) {
|
|
|
1525
1599
|
delete sub.boundAccountId;
|
|
1526
1600
|
const bindingMismatch = platform === "apple" ? boundAccountId && boundAccountId.toLowerCase() !== userId.toLowerCase() : boundAccountId && boundAccountId !== userId;
|
|
1527
1601
|
if (bindingMismatch) {
|
|
1528
|
-
log.warn(
|
|
1529
|
-
|
|
1530
|
-
|
|
1602
|
+
log.warn("[onesub/validate] account binding mismatch \u2014 receipt token does not match userId", {
|
|
1603
|
+
originalTransactionId: sub.originalTransactionId,
|
|
1604
|
+
userId
|
|
1605
|
+
});
|
|
1531
1606
|
sendError(
|
|
1532
1607
|
res,
|
|
1533
1608
|
409,
|
|
@@ -1540,7 +1615,7 @@ function createValidateRouter(config, store) {
|
|
|
1540
1615
|
const isSandbox = sub.sandbox === true;
|
|
1541
1616
|
delete sub.sandbox;
|
|
1542
1617
|
if (isSandbox && getTestOverride(userId) === false) {
|
|
1543
|
-
log.warn(
|
|
1618
|
+
log.warn("[onesub/validate] sandbox test override active \u2014 forcing not-entitled", { userId });
|
|
1544
1619
|
sub.status = SUBSCRIPTION_STATUS.EXPIRED;
|
|
1545
1620
|
sub.willRenew = false;
|
|
1546
1621
|
}
|
|
@@ -1552,7 +1627,7 @@ function createValidateRouter(config, store) {
|
|
|
1552
1627
|
const response = { valid: true, subscription: sub };
|
|
1553
1628
|
res.status(200).json(response);
|
|
1554
1629
|
} catch (err2) {
|
|
1555
|
-
log.error("[onesub/validate] Unexpected error
|
|
1630
|
+
log.error("[onesub/validate] Unexpected error", { userId, productId, platform, err: err2 });
|
|
1556
1631
|
sendError(res, 500, ONESUB_ERROR_CODE.INTERNAL_ERROR, "Internal server error during receipt validation", NO_SUB);
|
|
1557
1632
|
}
|
|
1558
1633
|
});
|
|
@@ -1584,7 +1659,7 @@ function createStatusRouter(store) {
|
|
|
1584
1659
|
const response = { active, subscription: sub };
|
|
1585
1660
|
res.status(200).json(response);
|
|
1586
1661
|
} catch (err2) {
|
|
1587
|
-
log.error("[onesub/status] Store error
|
|
1662
|
+
log.error("[onesub/status] Store error", { userId, err: err2 });
|
|
1588
1663
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error", NO_SUB2);
|
|
1589
1664
|
}
|
|
1590
1665
|
});
|
|
@@ -1692,7 +1767,7 @@ async function processAppleNotification(work, config, store, purchaseStore) {
|
|
|
1692
1767
|
});
|
|
1693
1768
|
}
|
|
1694
1769
|
} catch (err2) {
|
|
1695
|
-
log.warn("[onesub/webhook/apple] consumptionInfoProvider failed
|
|
1770
|
+
log.warn("[onesub/webhook/apple] consumptionInfoProvider failed", { transactionId, err: err2 });
|
|
1696
1771
|
}
|
|
1697
1772
|
})();
|
|
1698
1773
|
}
|
|
@@ -1705,7 +1780,7 @@ async function processAppleNotification(work, config, store, purchaseStore) {
|
|
|
1705
1780
|
const lookupId = transactionId ?? originalTransactionId;
|
|
1706
1781
|
const removed = await purchaseStore.deletePurchaseByTransactionId(lookupId);
|
|
1707
1782
|
if (!removed) {
|
|
1708
|
-
log.warn("[onesub/webhook/apple] IAP refund for unknown transaction
|
|
1783
|
+
log.warn("[onesub/webhook/apple] IAP refund for unknown transaction", { transactionId: lookupId });
|
|
1709
1784
|
}
|
|
1710
1785
|
return;
|
|
1711
1786
|
}
|
|
@@ -1715,7 +1790,10 @@ async function processAppleNotification(work, config, store, purchaseStore) {
|
|
|
1715
1790
|
const keepEntitlement = isSubscriptionRefund && config.refundPolicy === "until_expiry";
|
|
1716
1791
|
const correctedUserId = appAccountToken && existing.userId === originalTransactionId ? appAccountToken : existing.userId;
|
|
1717
1792
|
if (correctedUserId !== existing.userId) {
|
|
1718
|
-
log.info("[onesub/webhook/apple] correcting userId from originalTransactionId to appAccountToken
|
|
1793
|
+
log.info("[onesub/webhook/apple] correcting userId from originalTransactionId to appAccountToken", {
|
|
1794
|
+
originalTransactionId,
|
|
1795
|
+
userId: correctedUserId
|
|
1796
|
+
});
|
|
1719
1797
|
}
|
|
1720
1798
|
const updated = keepEntitlement ? { ...existing, userId: correctedUserId, willRenew: false } : {
|
|
1721
1799
|
...existing,
|
|
@@ -1733,20 +1811,22 @@ async function processAppleNotification(work, config, store, purchaseStore) {
|
|
|
1733
1811
|
fresh.userId = appAccountToken ?? originalTransactionId;
|
|
1734
1812
|
if (inAppOwnershipType === "FAMILY_SHARED") {
|
|
1735
1813
|
const source = appAccountToken ? "appAccountToken" : "originalTransactionId (fallback)";
|
|
1736
|
-
log.info(
|
|
1814
|
+
log.info("[onesub/webhook/apple] FAMILY_SHARED", {
|
|
1815
|
+
originalTransactionId,
|
|
1816
|
+
userId: fresh.userId,
|
|
1817
|
+
userIdSource: source
|
|
1818
|
+
});
|
|
1737
1819
|
}
|
|
1738
1820
|
await store.save(fresh);
|
|
1739
1821
|
} else {
|
|
1740
|
-
log.warn(
|
|
1741
|
-
"[onesub/webhook/apple] Unknown transaction and Status API returned no record:",
|
|
1822
|
+
log.warn("[onesub/webhook/apple] Unknown transaction and Status API returned no record", {
|
|
1742
1823
|
originalTransactionId
|
|
1743
|
-
);
|
|
1824
|
+
});
|
|
1744
1825
|
}
|
|
1745
1826
|
} else {
|
|
1746
|
-
log.warn(
|
|
1747
|
-
"[onesub/webhook/apple] Received notification for unknown transaction (no API creds to recover):",
|
|
1827
|
+
log.warn("[onesub/webhook/apple] Received notification for unknown transaction (no API creds to recover)", {
|
|
1748
1828
|
originalTransactionId
|
|
1749
|
-
);
|
|
1829
|
+
});
|
|
1750
1830
|
}
|
|
1751
1831
|
}
|
|
1752
1832
|
async function handleAppleWebhook(req, res, config, store, purchaseStore, webhookEventStore, webhookQueue) {
|
|
@@ -1762,7 +1842,7 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1762
1842
|
config.apple?.skipJwsVerification
|
|
1763
1843
|
);
|
|
1764
1844
|
} catch (err2) {
|
|
1765
|
-
log.error("[onesub/webhook/apple] Failed to decode signedPayload
|
|
1845
|
+
log.error("[onesub/webhook/apple] Failed to decode signedPayload", { err: err2 });
|
|
1766
1846
|
sendError(res, 400, ONESUB_ERROR_CODE.INVALID_SIGNED_PAYLOAD, "Invalid signedPayload");
|
|
1767
1847
|
return;
|
|
1768
1848
|
}
|
|
@@ -1770,7 +1850,9 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1770
1850
|
if (webhookEventStore && typeof payload.notificationUUID === "string") {
|
|
1771
1851
|
const fresh = await webhookEventStore.markIfNew("apple", payload.notificationUUID);
|
|
1772
1852
|
if (!fresh) {
|
|
1773
|
-
log.info("[onesub/webhook/apple] dedupe: already processed",
|
|
1853
|
+
log.info("[onesub/webhook/apple] dedupe: already processed", {
|
|
1854
|
+
notificationUUID: payload.notificationUUID
|
|
1855
|
+
});
|
|
1774
1856
|
res.status(200).json({ received: true, deduped: true });
|
|
1775
1857
|
return;
|
|
1776
1858
|
}
|
|
@@ -1783,7 +1865,7 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1783
1865
|
}
|
|
1784
1866
|
const notifiedApp = getAppRegistry(config).configFor({ bundleId: decoded.bundleId });
|
|
1785
1867
|
if (decoded.bundleId && !notifiedApp.apple) {
|
|
1786
|
-
log.warn("[onesub/webhook/apple] No app configured for bundleId
|
|
1868
|
+
log.warn("[onesub/webhook/apple] No app configured for bundleId", { bundleId: decoded.bundleId });
|
|
1787
1869
|
sendError(res, 400, ONESUB_ERROR_CODE.BUNDLE_ID_MISMATCH, "Bundle ID mismatch");
|
|
1788
1870
|
return;
|
|
1789
1871
|
}
|
|
@@ -1803,7 +1885,10 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1803
1885
|
});
|
|
1804
1886
|
res.status(200).json({ received: true, queued: true });
|
|
1805
1887
|
} catch (err2) {
|
|
1806
|
-
log.error("[onesub/webhook/apple] Failed to enqueue webhook job
|
|
1888
|
+
log.error("[onesub/webhook/apple] Failed to enqueue webhook job", {
|
|
1889
|
+
notificationUUID: payload.notificationUUID,
|
|
1890
|
+
err: err2
|
|
1891
|
+
});
|
|
1807
1892
|
await unmarkWebhookEvent(webhookEventStore, "apple", markedEventId);
|
|
1808
1893
|
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to update subscription");
|
|
1809
1894
|
}
|
|
@@ -1813,7 +1898,10 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1813
1898
|
await processAppleNotification(work, config, store, purchaseStore);
|
|
1814
1899
|
res.status(200).json({ received: true });
|
|
1815
1900
|
} catch (err2) {
|
|
1816
|
-
log.error("[onesub/webhook/apple] Store update error
|
|
1901
|
+
log.error("[onesub/webhook/apple] Store update error", {
|
|
1902
|
+
notificationUUID: payload.notificationUUID,
|
|
1903
|
+
err: err2
|
|
1904
|
+
});
|
|
1817
1905
|
await unmarkWebhookEvent(webhookEventStore, "apple", markedEventId);
|
|
1818
1906
|
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to update subscription");
|
|
1819
1907
|
}
|
|
@@ -1848,15 +1936,15 @@ async function verifyGooglePushToken(req, expectedAudience, expectedServiceAccou
|
|
|
1848
1936
|
}
|
|
1849
1937
|
var GOOGLE_FAILURE_MESSAGES = {
|
|
1850
1938
|
voided: {
|
|
1851
|
-
logPrefix: "[onesub/webhook/google] voided notification error
|
|
1939
|
+
logPrefix: "[onesub/webhook/google] voided notification error",
|
|
1852
1940
|
message: "Failed to process voided notification"
|
|
1853
1941
|
},
|
|
1854
1942
|
oneTimeProduct: {
|
|
1855
|
-
logPrefix: "[onesub/webhook/google] oneTimeProduct error
|
|
1943
|
+
logPrefix: "[onesub/webhook/google] oneTimeProduct error",
|
|
1856
1944
|
message: "Failed to process oneTimeProduct notification"
|
|
1857
1945
|
},
|
|
1858
1946
|
subscription: {
|
|
1859
|
-
logPrefix: "[onesub/webhook/google] Error handling notification
|
|
1947
|
+
logPrefix: "[onesub/webhook/google] Error handling notification",
|
|
1860
1948
|
message: "Failed to process notification"
|
|
1861
1949
|
}
|
|
1862
1950
|
};
|
|
@@ -1898,12 +1986,14 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
1898
1986
|
const updated = config.refundPolicy === "until_expiry" ? { ...existing2, willRenew: false } : { ...existing2, status: SUBSCRIPTION_STATUS.CANCELED };
|
|
1899
1987
|
await store.save(updated);
|
|
1900
1988
|
} else {
|
|
1901
|
-
log.warn("[onesub/webhook/google] voided subscription for unknown purchaseToken
|
|
1989
|
+
log.warn("[onesub/webhook/google] voided subscription for unknown purchaseToken", {
|
|
1990
|
+
purchaseToken: voided.purchaseToken
|
|
1991
|
+
});
|
|
1902
1992
|
}
|
|
1903
1993
|
} else {
|
|
1904
1994
|
const removed = await purchaseStore.deletePurchaseByTransactionId(voided.orderId);
|
|
1905
1995
|
if (!removed) {
|
|
1906
|
-
log.warn("[onesub/webhook/google] voided IAP for unknown orderId
|
|
1996
|
+
log.warn("[onesub/webhook/google] voided IAP for unknown orderId", { orderId: voided.orderId });
|
|
1907
1997
|
}
|
|
1908
1998
|
}
|
|
1909
1999
|
return;
|
|
@@ -1911,15 +2001,18 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
1911
2001
|
if (work.kind === "oneTimeProduct") {
|
|
1912
2002
|
const { notificationType: notificationType2, purchaseToken: purchaseToken2, sku } = work.oneTimeProduct;
|
|
1913
2003
|
if (notificationType2 === 1) {
|
|
1914
|
-
log.info("[onesub/webhook/google] oneTimeProduct PURCHASED
|
|
2004
|
+
log.info("[onesub/webhook/google] oneTimeProduct PURCHASED", { productId: sku });
|
|
1915
2005
|
const googleCfg = googleFor(work.oneTimeProduct.packageName);
|
|
1916
2006
|
if (googleCfg?.serviceAccountKey && googleCfg.packageName) {
|
|
1917
2007
|
void acknowledgeGoogleProduct(purchaseToken2, sku, googleCfg).catch(
|
|
1918
|
-
(err2) => log.warn(
|
|
2008
|
+
(err2) => log.warn("[onesub/webhook/google] oneTimeProduct ack failed \u2014 3-day auto-refund risk", {
|
|
2009
|
+
productId: sku,
|
|
2010
|
+
err: err2
|
|
2011
|
+
})
|
|
1919
2012
|
);
|
|
1920
2013
|
}
|
|
1921
2014
|
} else {
|
|
1922
|
-
log.info("[onesub/webhook/google] oneTimeProduct CANCELED (pre-completion)
|
|
2015
|
+
log.info("[onesub/webhook/google] oneTimeProduct CANCELED (pre-completion)", { productId: sku });
|
|
1923
2016
|
}
|
|
1924
2017
|
return;
|
|
1925
2018
|
}
|
|
@@ -1946,7 +2039,7 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
1946
2039
|
const subGoogleCfg = googleFor(packageName);
|
|
1947
2040
|
if (isGooglePriceChangeConfirmedNotification(notificationType) && subGoogleCfg?.onPriceChangeConfirmed) {
|
|
1948
2041
|
const hook = subGoogleCfg.onPriceChangeConfirmed;
|
|
1949
|
-
void Promise.resolve().then(() => hook({ purchaseToken, subscriptionId, packageName })).catch((err2) => log.warn("[onesub/webhook/google] onPriceChangeConfirmed hook failed
|
|
2042
|
+
void Promise.resolve().then(() => hook({ purchaseToken, subscriptionId, packageName })).catch((err2) => log.warn("[onesub/webhook/google] onPriceChangeConfirmed hook failed", { err: err2 }));
|
|
1950
2043
|
}
|
|
1951
2044
|
if (existing) {
|
|
1952
2045
|
let updated = { ...existing, status: finalStatus };
|
|
@@ -1975,7 +2068,11 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
1975
2068
|
const previous = await store.getByTransactionId(fresh.linkedPurchaseToken);
|
|
1976
2069
|
fresh.userId = previous ? previous.userId : boundAccountId ?? purchaseToken;
|
|
1977
2070
|
if (previous) {
|
|
1978
|
-
log.info(
|
|
2071
|
+
log.info("[onesub/webhook/google] inherited userId from linkedPurchaseToken", {
|
|
2072
|
+
userId: previous.userId,
|
|
2073
|
+
linkedPurchaseToken: fresh.linkedPurchaseToken,
|
|
2074
|
+
purchaseToken
|
|
2075
|
+
});
|
|
1979
2076
|
}
|
|
1980
2077
|
} else {
|
|
1981
2078
|
fresh.userId = boundAccountId ?? purchaseToken;
|
|
@@ -1983,7 +2080,9 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
1983
2080
|
await store.save(fresh);
|
|
1984
2081
|
}
|
|
1985
2082
|
} else {
|
|
1986
|
-
log.warn("[onesub/webhook/google] Unknown purchase token and no serviceAccountKey to re-fetch
|
|
2083
|
+
log.warn("[onesub/webhook/google] Unknown purchase token and no serviceAccountKey to re-fetch", {
|
|
2084
|
+
purchaseToken
|
|
2085
|
+
});
|
|
1987
2086
|
}
|
|
1988
2087
|
}
|
|
1989
2088
|
}
|
|
@@ -2011,7 +2110,9 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2011
2110
|
if (webhookEventStore && typeof body.message.messageId === "string") {
|
|
2012
2111
|
const fresh = await webhookEventStore.markIfNew("google", body.message.messageId);
|
|
2013
2112
|
if (!fresh) {
|
|
2014
|
-
log.info("[onesub/webhook/google] dedupe: already processed",
|
|
2113
|
+
log.info("[onesub/webhook/google] dedupe: already processed", {
|
|
2114
|
+
messageId: body.message.messageId
|
|
2115
|
+
});
|
|
2015
2116
|
res.status(200).json({ received: true, deduped: true });
|
|
2016
2117
|
return;
|
|
2017
2118
|
}
|
|
@@ -2022,7 +2123,9 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2022
2123
|
const voided = decodeGoogleVoidedNotification(body);
|
|
2023
2124
|
if (voided) {
|
|
2024
2125
|
if (!servesPackage(voided.packageName)) {
|
|
2025
|
-
log.warn("[onesub/webhook/google] voided package name not served
|
|
2126
|
+
log.warn("[onesub/webhook/google] voided package name not served", {
|
|
2127
|
+
packageName: voided.packageName
|
|
2128
|
+
});
|
|
2026
2129
|
sendError(res, 400, ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
|
|
2027
2130
|
return;
|
|
2028
2131
|
}
|
|
@@ -2031,7 +2134,9 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2031
2134
|
const oneTimeProduct = decodeGoogleOneTimeProductNotification(body);
|
|
2032
2135
|
if (oneTimeProduct) {
|
|
2033
2136
|
if (!servesPackage(oneTimeProduct.packageName)) {
|
|
2034
|
-
log.warn("[onesub/webhook/google] oneTimeProduct package name not served
|
|
2137
|
+
log.warn("[onesub/webhook/google] oneTimeProduct package name not served", {
|
|
2138
|
+
packageName: oneTimeProduct.packageName
|
|
2139
|
+
});
|
|
2035
2140
|
sendError(res, 400, ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
|
|
2036
2141
|
return;
|
|
2037
2142
|
}
|
|
@@ -2043,7 +2148,9 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2043
2148
|
return;
|
|
2044
2149
|
}
|
|
2045
2150
|
if (!servesPackage(notification.packageName)) {
|
|
2046
|
-
log.warn("[onesub/webhook/google] Package name not served
|
|
2151
|
+
log.warn("[onesub/webhook/google] Package name not served", {
|
|
2152
|
+
packageName: notification.packageName
|
|
2153
|
+
});
|
|
2047
2154
|
sendError(res, 400, ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
|
|
2048
2155
|
return;
|
|
2049
2156
|
}
|
|
@@ -2061,7 +2168,11 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2061
2168
|
});
|
|
2062
2169
|
res.status(200).json({ received: true, queued: true });
|
|
2063
2170
|
} catch (err2) {
|
|
2064
|
-
log.error("[onesub/webhook/google] Failed to enqueue webhook job
|
|
2171
|
+
log.error("[onesub/webhook/google] Failed to enqueue webhook job", {
|
|
2172
|
+
kind: work.kind,
|
|
2173
|
+
messageId: body.message.messageId,
|
|
2174
|
+
err: err2
|
|
2175
|
+
});
|
|
2065
2176
|
await unmarkWebhookEvent(webhookEventStore, "google", markedEventId);
|
|
2066
2177
|
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, GOOGLE_FAILURE_MESSAGES[work.kind].message);
|
|
2067
2178
|
}
|
|
@@ -2072,7 +2183,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2072
2183
|
res.status(200).json({ received: true });
|
|
2073
2184
|
} catch (err2) {
|
|
2074
2185
|
const { logPrefix, message } = GOOGLE_FAILURE_MESSAGES[work.kind];
|
|
2075
|
-
log.error(logPrefix, err2);
|
|
2186
|
+
log.error(logPrefix, { kind: work.kind, err: err2 });
|
|
2076
2187
|
await unmarkWebhookEvent(webhookEventStore, "google", markedEventId);
|
|
2077
2188
|
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, message);
|
|
2078
2189
|
}
|
|
@@ -2184,9 +2295,10 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2184
2295
|
}
|
|
2185
2296
|
const bindingMismatch = platform === "apple" ? boundAccountId && boundAccountId.toLowerCase() !== userId.toLowerCase() : boundAccountId && boundAccountId !== userId;
|
|
2186
2297
|
if (bindingMismatch) {
|
|
2187
|
-
log.warn(
|
|
2188
|
-
|
|
2189
|
-
|
|
2298
|
+
log.warn("[onesub/purchase] account binding mismatch \u2014 token does not match userId", {
|
|
2299
|
+
transactionId,
|
|
2300
|
+
userId
|
|
2301
|
+
});
|
|
2190
2302
|
sendError(
|
|
2191
2303
|
res,
|
|
2192
2304
|
409,
|
|
@@ -2203,9 +2315,11 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2203
2315
|
if (existing.userId !== userId) {
|
|
2204
2316
|
if (type === PURCHASE_TYPE.NON_CONSUMABLE) {
|
|
2205
2317
|
await purchaseStore.reassignPurchase(transactionId, userId);
|
|
2206
|
-
log.info(
|
|
2207
|
-
|
|
2208
|
-
|
|
2318
|
+
log.info("[onesub/purchase] reassigned transaction to a new user", {
|
|
2319
|
+
transactionId,
|
|
2320
|
+
fromUserId: existing.userId,
|
|
2321
|
+
userId
|
|
2322
|
+
});
|
|
2209
2323
|
} else {
|
|
2210
2324
|
sendError(
|
|
2211
2325
|
res,
|
|
@@ -2249,7 +2363,7 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2249
2363
|
};
|
|
2250
2364
|
res.status(200).json(response);
|
|
2251
2365
|
} catch (err2) {
|
|
2252
|
-
log.error("[onesub/purchase/validate] Unexpected error
|
|
2366
|
+
log.error("[onesub/purchase/validate] Unexpected error", { userId, productId, platform, type, err: err2 });
|
|
2253
2367
|
sendError(res, 500, ONESUB_ERROR_CODE.INTERNAL_ERROR, "Internal server error during purchase validation", NO_PURCHASE);
|
|
2254
2368
|
}
|
|
2255
2369
|
});
|
|
@@ -2264,7 +2378,7 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2264
2378
|
const response = { purchases };
|
|
2265
2379
|
res.status(200).json(response);
|
|
2266
2380
|
} catch (err2) {
|
|
2267
|
-
log.error("[onesub/purchase/status] Store error
|
|
2381
|
+
log.error("[onesub/purchase/status] Store error", { userId, productId, err: err2 });
|
|
2268
2382
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error", { purchases: [] });
|
|
2269
2383
|
}
|
|
2270
2384
|
});
|
|
@@ -2329,7 +2443,7 @@ function createEntitlementRouter(config, store, purchaseStore) {
|
|
|
2329
2443
|
const response = { id, ...status };
|
|
2330
2444
|
res.status(200).json(response);
|
|
2331
2445
|
} catch (err2) {
|
|
2332
|
-
log.error("[onesub/entitlement] evaluation error
|
|
2446
|
+
log.error("[onesub/entitlement] evaluation error", { userId, entitlement: id, err: err2 });
|
|
2333
2447
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2334
2448
|
}
|
|
2335
2449
|
});
|
|
@@ -2353,7 +2467,7 @@ function createEntitlementRouter(config, store, purchaseStore) {
|
|
|
2353
2467
|
};
|
|
2354
2468
|
res.status(200).json(response);
|
|
2355
2469
|
} catch (err2) {
|
|
2356
|
-
log.error("[onesub/entitlements] evaluation error
|
|
2470
|
+
log.error("[onesub/entitlements] evaluation error", { userId, err: err2 });
|
|
2357
2471
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error", { entitlements: {} });
|
|
2358
2472
|
}
|
|
2359
2473
|
});
|
|
@@ -2467,7 +2581,7 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2467
2581
|
};
|
|
2468
2582
|
res.status(200).json(response);
|
|
2469
2583
|
} catch (err2) {
|
|
2470
|
-
log.error("[onesub/admin/subscriptions] list error
|
|
2584
|
+
log.error("[onesub/admin/subscriptions] list error", { err: err2 });
|
|
2471
2585
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2472
2586
|
}
|
|
2473
2587
|
});
|
|
@@ -2487,7 +2601,10 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2487
2601
|
}
|
|
2488
2602
|
res.status(200).json(sub);
|
|
2489
2603
|
} catch (err2) {
|
|
2490
|
-
log.error("[onesub/admin/subscriptions/:transactionId] detail error
|
|
2604
|
+
log.error("[onesub/admin/subscriptions/:transactionId] detail error", {
|
|
2605
|
+
transactionId: params.transactionId,
|
|
2606
|
+
err: err2
|
|
2607
|
+
});
|
|
2491
2608
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2492
2609
|
}
|
|
2493
2610
|
});
|
|
@@ -2520,7 +2637,7 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2520
2637
|
};
|
|
2521
2638
|
res.status(200).json(response);
|
|
2522
2639
|
} catch (err2) {
|
|
2523
|
-
log.error("[onesub/admin/customers/:userId] error
|
|
2640
|
+
log.error("[onesub/admin/customers/:userId] error", { userId: params.userId, err: err2 });
|
|
2524
2641
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2525
2642
|
}
|
|
2526
2643
|
});
|
|
@@ -2550,7 +2667,10 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2550
2667
|
await store.save(fresh);
|
|
2551
2668
|
res.status(200).json({ ok: true, subscription: fresh });
|
|
2552
2669
|
} catch (err2) {
|
|
2553
|
-
log.error("[onesub/admin/sync-apple] error
|
|
2670
|
+
log.error("[onesub/admin/sync-apple] error", {
|
|
2671
|
+
originalTransactionId: params.originalTransactionId,
|
|
2672
|
+
err: err2
|
|
2673
|
+
});
|
|
2554
2674
|
sendError(res, 500, ONESUB_ERROR_CODE.INTERNAL_ERROR, "Internal server error");
|
|
2555
2675
|
}
|
|
2556
2676
|
});
|
|
@@ -2565,9 +2685,10 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2565
2685
|
const body = parseOrSend(res, overrideBodySchema, req.body);
|
|
2566
2686
|
if (!body) return;
|
|
2567
2687
|
setTestOverride(params.userId, body.entitled);
|
|
2568
|
-
log.warn(
|
|
2569
|
-
|
|
2570
|
-
|
|
2688
|
+
log.warn("[onesub/admin] sandbox test override set", {
|
|
2689
|
+
userId: params.userId,
|
|
2690
|
+
entitled: body.entitled
|
|
2691
|
+
});
|
|
2571
2692
|
res.json({ ok: true, userId: params.userId, entitled: body.entitled, sandboxOnly: true });
|
|
2572
2693
|
});
|
|
2573
2694
|
router.delete(ROUTES.ADMIN_TEST_OVERRIDE, (req, res) => {
|
|
@@ -2584,7 +2705,7 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2584
2705
|
const items = await webhookQueue.listDeadLetters();
|
|
2585
2706
|
res.status(200).json({ items });
|
|
2586
2707
|
} catch (err2) {
|
|
2587
|
-
log.error("[onesub/admin/webhook-deadletters] error
|
|
2708
|
+
log.error("[onesub/admin/webhook-deadletters] error", { err: err2 });
|
|
2588
2709
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2589
2710
|
}
|
|
2590
2711
|
});
|
|
@@ -2598,7 +2719,7 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2598
2719
|
await webhookQueue.replayDeadLetter(params.id);
|
|
2599
2720
|
res.status(200).json({ ok: true });
|
|
2600
2721
|
} catch (err2) {
|
|
2601
|
-
log.error("[onesub/admin/webhook-replay] error
|
|
2722
|
+
log.error("[onesub/admin/webhook-replay] error", { deadLetterId: params.id, err: err2 });
|
|
2602
2723
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2603
2724
|
}
|
|
2604
2725
|
});
|
|
@@ -2768,7 +2889,7 @@ function createMetricsRouter(config, store, purchaseStore) {
|
|
|
2768
2889
|
);
|
|
2769
2890
|
res.status(200).json(response);
|
|
2770
2891
|
} catch (err2) {
|
|
2771
|
-
log.error("[onesub/metrics/active] error
|
|
2892
|
+
log.error("[onesub/metrics/active] error", { err: err2 });
|
|
2772
2893
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2773
2894
|
}
|
|
2774
2895
|
});
|
|
@@ -2835,7 +2956,7 @@ function createMetricsRouter(config, store, purchaseStore) {
|
|
|
2835
2956
|
};
|
|
2836
2957
|
res.status(200).json(response);
|
|
2837
2958
|
} catch (err2) {
|
|
2838
|
-
log.error(
|
|
2959
|
+
log.error("[onesub/metrics] error", { route: name, err: err2 });
|
|
2839
2960
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2840
2961
|
}
|
|
2841
2962
|
};
|
|
@@ -2907,7 +3028,7 @@ function createAppleOfferRouter(config) {
|
|
|
2907
3028
|
);
|
|
2908
3029
|
res.status(200).json(result);
|
|
2909
3030
|
} catch (err2) {
|
|
2910
|
-
log.error("[onesub/apple/offer] signing error
|
|
3031
|
+
log.error("[onesub/apple/offer] signing error", { productId: body.productId, err: err2 });
|
|
2911
3032
|
sendError(res, 500, ONESUB_ERROR_CODE.INTERNAL_ERROR, "Offer signing failed");
|
|
2912
3033
|
}
|
|
2913
3034
|
});
|
|
@@ -2979,7 +3100,7 @@ async function createPgPool(connectionString, label) {
|
|
|
2979
3100
|
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
2980
3101
|
const pool = new Pool({ connectionString, max: 10 });
|
|
2981
3102
|
pool.on("error", (err2) => {
|
|
2982
|
-
log.error(
|
|
3103
|
+
log.error("[onesub] Postgres pool error (idle client)", { store: label, err: err2 });
|
|
2983
3104
|
});
|
|
2984
3105
|
return pool;
|
|
2985
3106
|
}
|
|
@@ -3720,7 +3841,7 @@ var BullMQWebhookQueue = class {
|
|
|
3720
3841
|
const { Queue } = await this.getBullMQ();
|
|
3721
3842
|
const queue = new Queue(this.queueName, { connection: this.connection });
|
|
3722
3843
|
queue.on?.("error", (err2) => {
|
|
3723
|
-
log.error("[onesub] BullMQ queue error
|
|
3844
|
+
log.error("[onesub] BullMQ queue error", { err: err2 });
|
|
3724
3845
|
});
|
|
3725
3846
|
return queue;
|
|
3726
3847
|
})();
|
|
@@ -3744,13 +3865,13 @@ var BullMQWebhookQueue = class {
|
|
|
3744
3865
|
}
|
|
3745
3866
|
);
|
|
3746
3867
|
worker.on?.("error", (err2) => {
|
|
3747
|
-
log.error("[onesub] BullMQ worker error
|
|
3868
|
+
log.error("[onesub] BullMQ worker error", { err: err2 });
|
|
3748
3869
|
});
|
|
3749
3870
|
return worker;
|
|
3750
3871
|
})();
|
|
3751
3872
|
this.workerPromise.catch((err2) => {
|
|
3752
3873
|
this.workerStartupError = err2 instanceof Error ? err2 : new Error(String(err2));
|
|
3753
|
-
log.error("[onesub] BullMQ worker failed to start
|
|
3874
|
+
log.error("[onesub] BullMQ worker failed to start", { err: err2 });
|
|
3754
3875
|
});
|
|
3755
3876
|
}
|
|
3756
3877
|
}
|