@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.cjs
CHANGED
|
@@ -449,7 +449,7 @@ function outcomePasses(outcome, tag) {
|
|
|
449
449
|
throw new Error(`[onesub/mock/${tag}] simulated upstream network error`);
|
|
450
450
|
}
|
|
451
451
|
if (outcome.kind === "valid" || outcome.kind === "sandbox") return true;
|
|
452
|
-
log.warn(
|
|
452
|
+
log.warn("[onesub/mock] receipt rejected", { provider: tag, outcome: outcome.kind });
|
|
453
453
|
return false;
|
|
454
454
|
}
|
|
455
455
|
function deterministicTransactionId(prefix, receipt) {
|
|
@@ -624,22 +624,23 @@ async function validateAppleReceipt(receipt, config) {
|
|
|
624
624
|
} catch (err2) {
|
|
625
625
|
const preview = receipt.slice(0, 60);
|
|
626
626
|
const parts = receipt.split(".").length;
|
|
627
|
-
log.warn(
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
627
|
+
log.warn("[onesub/apple] Failed to decode receipt as JWS", {
|
|
628
|
+
receiptPreview: preview,
|
|
629
|
+
receiptLength: receipt.length,
|
|
630
|
+
jwsParts: parts,
|
|
631
|
+
err: err2
|
|
632
|
+
});
|
|
632
633
|
return null;
|
|
633
634
|
}
|
|
634
635
|
if (!tx.originalTransactionId || !tx.productId || !tx.expiresDate) {
|
|
635
636
|
return null;
|
|
636
637
|
}
|
|
637
638
|
if (!tx.bundleId || tx.bundleId !== config.bundleId) {
|
|
638
|
-
log.warn("[onesub/apple] Bundle ID mismatch
|
|
639
|
+
log.warn("[onesub/apple] Bundle ID mismatch", { bundleId: tx.bundleId, expected: config.bundleId });
|
|
639
640
|
return null;
|
|
640
641
|
}
|
|
641
642
|
if (process.env["NODE_ENV"] === "production" && tx.environment !== "Production" && process.env["ONESUB_ALLOW_SANDBOX"] !== "true") {
|
|
642
|
-
log.warn("[onesub/apple] Sandbox receipt rejected in production
|
|
643
|
+
log.warn("[onesub/apple] Sandbox receipt rejected in production", { environment: tx.environment });
|
|
643
644
|
return null;
|
|
644
645
|
}
|
|
645
646
|
const status = deriveStatus(tx, null);
|
|
@@ -679,23 +680,25 @@ async function validateAppleConsumableReceipt(signedTransaction, config, expecte
|
|
|
679
680
|
const preview = signedTransaction.slice(0, 60);
|
|
680
681
|
const parts = signedTransaction.split(".").length;
|
|
681
682
|
const looksLikeJws = parts === 3;
|
|
682
|
-
log.warn(
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
683
|
+
log.warn("[onesub/apple] Failed to decode consumable JWS", {
|
|
684
|
+
receiptPreview: preview,
|
|
685
|
+
receiptLength: signedTransaction.length,
|
|
686
|
+
jwsParts: parts,
|
|
687
|
+
looksLikeJws,
|
|
688
|
+
err: err2
|
|
689
|
+
});
|
|
687
690
|
return null;
|
|
688
691
|
}
|
|
689
692
|
if (!tx.bundleId || tx.bundleId !== config.bundleId) {
|
|
690
|
-
log.warn("[onesub/apple] Bundle ID mismatch
|
|
693
|
+
log.warn("[onesub/apple] Bundle ID mismatch", { bundleId: tx.bundleId, expected: config.bundleId });
|
|
691
694
|
return null;
|
|
692
695
|
}
|
|
693
696
|
if (tx.type !== "Consumable" && tx.type !== "Non-Consumable") {
|
|
694
|
-
log.warn("[onesub/apple] Invalid purchase type for product validation
|
|
697
|
+
log.warn("[onesub/apple] Invalid purchase type for product validation", { type: tx.type });
|
|
695
698
|
return null;
|
|
696
699
|
}
|
|
697
700
|
if (process.env["NODE_ENV"] === "production" && tx.environment !== "Production" && process.env["ONESUB_ALLOW_SANDBOX"] !== "true") {
|
|
698
|
-
log.warn("[onesub/apple] Sandbox receipt rejected in production
|
|
701
|
+
log.warn("[onesub/apple] Sandbox receipt rejected in production", { environment: tx.environment });
|
|
699
702
|
return null;
|
|
700
703
|
}
|
|
701
704
|
if (!tx.productId) {
|
|
@@ -703,21 +706,28 @@ async function validateAppleConsumableReceipt(signedTransaction, config, expecte
|
|
|
703
706
|
return null;
|
|
704
707
|
}
|
|
705
708
|
if (expectedProductId && tx.productId !== expectedProductId) {
|
|
706
|
-
log.warn("[onesub/apple] Product ID mismatch
|
|
709
|
+
log.warn("[onesub/apple] Product ID mismatch", { productId: tx.productId, expected: expectedProductId });
|
|
707
710
|
return null;
|
|
708
711
|
}
|
|
709
712
|
if (tx.revocationDate) {
|
|
710
|
-
log.warn("[onesub/apple] Purchase was revoked/refunded"
|
|
713
|
+
log.warn("[onesub/apple] Purchase was revoked/refunded", {
|
|
714
|
+
productId: tx.productId,
|
|
715
|
+
transactionId: tx.transactionId
|
|
716
|
+
});
|
|
711
717
|
return null;
|
|
712
718
|
}
|
|
713
719
|
const maxAgeMs = (config.productReceiptMaxAgeHours ?? 72) * 60 * 60 * 1e3;
|
|
714
720
|
if (tx.purchaseDate && Date.now() - tx.purchaseDate > maxAgeMs) {
|
|
715
|
-
log.warn(
|
|
721
|
+
log.warn("[onesub/apple] Consumable receipt too old", {
|
|
722
|
+
productId: tx.productId,
|
|
723
|
+
maxAgeHours: config.productReceiptMaxAgeHours ?? 72,
|
|
724
|
+
purchaseDate: new Date(tx.purchaseDate).toISOString()
|
|
725
|
+
});
|
|
716
726
|
return null;
|
|
717
727
|
}
|
|
718
728
|
const transactionId = tx.transactionId ?? tx.originalTransactionId;
|
|
719
729
|
if (!transactionId) {
|
|
720
|
-
log.warn("[onesub/apple] No transactionId in consumable transaction");
|
|
730
|
+
log.warn("[onesub/apple] No transactionId in consumable transaction", { productId: tx.productId });
|
|
721
731
|
return null;
|
|
722
732
|
}
|
|
723
733
|
return {
|
|
@@ -796,7 +806,7 @@ async function sendAppleConsumptionResponse(transactionId, body, config, options
|
|
|
796
806
|
try {
|
|
797
807
|
jwt = await makeAppleApiJwt(config);
|
|
798
808
|
} catch (err2) {
|
|
799
|
-
log.warn("[onesub/apple] Cannot send consumption response \u2014 JWT mint failed
|
|
809
|
+
log.warn("[onesub/apple] Cannot send consumption response \u2014 JWT mint failed", { transactionId, err: err2 });
|
|
800
810
|
return;
|
|
801
811
|
}
|
|
802
812
|
const host = options?.sandbox ? "api.storekit-sandbox.itunes.apple.com" : "api.storekit.itunes.apple.com";
|
|
@@ -812,10 +822,14 @@ async function sendAppleConsumptionResponse(transactionId, body, config, options
|
|
|
812
822
|
});
|
|
813
823
|
if (!resp.ok) {
|
|
814
824
|
const text = await resp.text();
|
|
815
|
-
log.warn(
|
|
825
|
+
log.warn("[onesub/apple] Consumption response API error", {
|
|
826
|
+
httpStatus: resp.status,
|
|
827
|
+
transactionId,
|
|
828
|
+
responseBody: text
|
|
829
|
+
});
|
|
816
830
|
}
|
|
817
831
|
} catch (err2) {
|
|
818
|
-
log.warn("[onesub/apple] Consumption response network error
|
|
832
|
+
log.warn("[onesub/apple] Consumption response network error", { transactionId, err: err2 });
|
|
819
833
|
}
|
|
820
834
|
}
|
|
821
835
|
var APPLE_SUBSCRIPTION_STATUS_CODE = {
|
|
@@ -846,7 +860,10 @@ async function fetchAppleSubscriptionStatus(originalTransactionId, config, optio
|
|
|
846
860
|
try {
|
|
847
861
|
jwt = await makeAppleApiJwt(config);
|
|
848
862
|
} catch (err2) {
|
|
849
|
-
log.warn("[onesub/apple] Cannot fetch subscription status \u2014 JWT mint failed
|
|
863
|
+
log.warn("[onesub/apple] Cannot fetch subscription status \u2014 JWT mint failed", {
|
|
864
|
+
originalTransactionId,
|
|
865
|
+
err: err2
|
|
866
|
+
});
|
|
850
867
|
return null;
|
|
851
868
|
}
|
|
852
869
|
const host = options?.sandbox ? "api.storekit-sandbox.itunes.apple.com" : "api.storekit.itunes.apple.com";
|
|
@@ -858,24 +875,31 @@ async function fetchAppleSubscriptionStatus(originalTransactionId, config, optio
|
|
|
858
875
|
});
|
|
859
876
|
if (!resp.ok) {
|
|
860
877
|
const text = await resp.text();
|
|
861
|
-
log.warn(
|
|
878
|
+
log.warn("[onesub/apple] Status API error", {
|
|
879
|
+
httpStatus: resp.status,
|
|
880
|
+
originalTransactionId,
|
|
881
|
+
responseBody: text
|
|
882
|
+
});
|
|
862
883
|
return null;
|
|
863
884
|
}
|
|
864
885
|
body = await resp.json();
|
|
865
886
|
} catch (err2) {
|
|
866
|
-
log.warn("[onesub/apple] Status API network error
|
|
887
|
+
log.warn("[onesub/apple] Status API network error", { originalTransactionId, err: err2 });
|
|
867
888
|
return null;
|
|
868
889
|
}
|
|
869
890
|
const entry = body.data?.flatMap((g) => g.lastTransactions ?? []).find((t) => t.originalTransactionId === originalTransactionId);
|
|
870
891
|
if (!entry || entry.status == null || !entry.signedTransactionInfo) {
|
|
871
|
-
log.warn("[onesub/apple] Status API returned no matching transaction
|
|
892
|
+
log.warn("[onesub/apple] Status API returned no matching transaction", { originalTransactionId });
|
|
872
893
|
return null;
|
|
873
894
|
}
|
|
874
895
|
let tx;
|
|
875
896
|
try {
|
|
876
897
|
tx = await decodeJws(entry.signedTransactionInfo, config.skipJwsVerification);
|
|
877
898
|
} catch (err2) {
|
|
878
|
-
log.warn("[onesub/apple] Failed to decode signedTransactionInfo from Status API
|
|
899
|
+
log.warn("[onesub/apple] Failed to decode signedTransactionInfo from Status API", {
|
|
900
|
+
originalTransactionId,
|
|
901
|
+
err: err2
|
|
902
|
+
});
|
|
879
903
|
return null;
|
|
880
904
|
}
|
|
881
905
|
let renewal = null;
|
|
@@ -886,7 +910,10 @@ async function fetchAppleSubscriptionStatus(originalTransactionId, config, optio
|
|
|
886
910
|
}
|
|
887
911
|
}
|
|
888
912
|
if (!tx.productId || !tx.expiresDate) {
|
|
889
|
-
log.warn("[onesub/apple] Status API transaction missing productId or expiresDate"
|
|
913
|
+
log.warn("[onesub/apple] Status API transaction missing productId or expiresDate", {
|
|
914
|
+
originalTransactionId,
|
|
915
|
+
productId: tx.productId
|
|
916
|
+
});
|
|
890
917
|
return null;
|
|
891
918
|
}
|
|
892
919
|
const status = mapAppleStatusCode(entry.status);
|
|
@@ -910,7 +937,10 @@ async function fetchAppleTransactionHistory(originalTransactionId, config, optio
|
|
|
910
937
|
try {
|
|
911
938
|
jwt = await makeAppleApiJwt(config);
|
|
912
939
|
} catch (err2) {
|
|
913
|
-
log.warn("[onesub/apple] Cannot fetch transaction history \u2014 JWT mint failed
|
|
940
|
+
log.warn("[onesub/apple] Cannot fetch transaction history \u2014 JWT mint failed", {
|
|
941
|
+
originalTransactionId,
|
|
942
|
+
err: err2
|
|
943
|
+
});
|
|
914
944
|
return null;
|
|
915
945
|
}
|
|
916
946
|
const host = options?.sandbox ? "api.storekit-sandbox.itunes.apple.com" : "api.storekit.itunes.apple.com";
|
|
@@ -927,12 +957,16 @@ async function fetchAppleTransactionHistory(originalTransactionId, config, optio
|
|
|
927
957
|
});
|
|
928
958
|
if (!resp.ok) {
|
|
929
959
|
const text = await resp.text();
|
|
930
|
-
log.warn(
|
|
960
|
+
log.warn("[onesub/apple] Transaction History API error", {
|
|
961
|
+
httpStatus: resp.status,
|
|
962
|
+
originalTransactionId,
|
|
963
|
+
responseBody: text
|
|
964
|
+
});
|
|
931
965
|
return null;
|
|
932
966
|
}
|
|
933
967
|
page = await resp.json();
|
|
934
968
|
} catch (err2) {
|
|
935
|
-
log.warn("[onesub/apple] Transaction History API network error
|
|
969
|
+
log.warn("[onesub/apple] Transaction History API network error", { originalTransactionId, err: err2 });
|
|
936
970
|
return null;
|
|
937
971
|
}
|
|
938
972
|
for (const signed of page.signedTransactions ?? []) {
|
|
@@ -957,14 +991,16 @@ async function fetchAppleTransactionHistory(originalTransactionId, config, optio
|
|
|
957
991
|
if (!page.hasMore) break;
|
|
958
992
|
if (!page.revision || page.revision === revision) {
|
|
959
993
|
log.warn(
|
|
960
|
-
"[onesub/apple] Transaction History API returned hasMore without a new revision cursor \u2014 stopping pagination (partial history returned)"
|
|
994
|
+
"[onesub/apple] Transaction History API returned hasMore without a new revision cursor \u2014 stopping pagination (partial history returned)",
|
|
995
|
+
{ originalTransactionId, pageCount }
|
|
961
996
|
);
|
|
962
997
|
break;
|
|
963
998
|
}
|
|
964
999
|
if (pageCount >= MAX_HISTORY_PAGES) {
|
|
965
|
-
log.warn(
|
|
966
|
-
|
|
967
|
-
|
|
1000
|
+
log.warn("[onesub/apple] Transaction History pagination hit the page cap \u2014 stopping (partial history returned)", {
|
|
1001
|
+
originalTransactionId,
|
|
1002
|
+
maxPages: MAX_HISTORY_PAGES
|
|
1003
|
+
});
|
|
968
1004
|
break;
|
|
969
1005
|
}
|
|
970
1006
|
revision = page.revision;
|
|
@@ -1114,7 +1150,7 @@ async function acknowledgeGoogleSubscription(purchaseToken, productId, config) {
|
|
|
1114
1150
|
try {
|
|
1115
1151
|
accessToken = await getCachedAccessToken(config.serviceAccountKey);
|
|
1116
1152
|
} catch (err2) {
|
|
1117
|
-
log.warn("[onesub/google] Could not get access token for subscription ack
|
|
1153
|
+
log.warn("[onesub/google] Could not get access token for subscription ack", { productId, err: err2 });
|
|
1118
1154
|
return;
|
|
1119
1155
|
}
|
|
1120
1156
|
const url = `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${encodeURIComponent(config.packageName)}/purchases/subscriptions/${encodeURIComponent(productId)}/tokens/${encodeURIComponent(purchaseToken)}:acknowledge`;
|
|
@@ -1126,10 +1162,14 @@ async function acknowledgeGoogleSubscription(purchaseToken, productId, config) {
|
|
|
1126
1162
|
});
|
|
1127
1163
|
if (!resp.ok) {
|
|
1128
1164
|
const body = await resp.text();
|
|
1129
|
-
log.warn(
|
|
1165
|
+
log.warn("[onesub/google] Subscription acknowledge API error \u2014 auto-refund risk", {
|
|
1166
|
+
httpStatus: resp.status,
|
|
1167
|
+
productId,
|
|
1168
|
+
responseBody: body
|
|
1169
|
+
});
|
|
1130
1170
|
}
|
|
1131
1171
|
} catch (err2) {
|
|
1132
|
-
log.warn("[onesub/google] Subscription acknowledge network error \u2014 auto-refund risk
|
|
1172
|
+
log.warn("[onesub/google] Subscription acknowledge network error \u2014 auto-refund risk", { productId, err: err2 });
|
|
1133
1173
|
}
|
|
1134
1174
|
}
|
|
1135
1175
|
async function acknowledgeGoogleProduct(purchaseToken, productId, config) {
|
|
@@ -1140,7 +1180,7 @@ async function acknowledgeGoogleProduct(purchaseToken, productId, config) {
|
|
|
1140
1180
|
try {
|
|
1141
1181
|
accessToken = await getCachedAccessToken(config.serviceAccountKey);
|
|
1142
1182
|
} catch (err2) {
|
|
1143
|
-
log.warn("[onesub/google] Could not get access token for product ack
|
|
1183
|
+
log.warn("[onesub/google] Could not get access token for product ack", { productId, err: err2 });
|
|
1144
1184
|
return;
|
|
1145
1185
|
}
|
|
1146
1186
|
const url = `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${encodeURIComponent(config.packageName)}/purchases/products/${encodeURIComponent(productId)}/tokens/${encodeURIComponent(purchaseToken)}:acknowledge`;
|
|
@@ -1152,10 +1192,14 @@ async function acknowledgeGoogleProduct(purchaseToken, productId, config) {
|
|
|
1152
1192
|
});
|
|
1153
1193
|
if (!resp.ok) {
|
|
1154
1194
|
const body = await resp.text();
|
|
1155
|
-
log.warn(
|
|
1195
|
+
log.warn("[onesub/google] Product acknowledge API error \u2014 auto-refund risk", {
|
|
1196
|
+
httpStatus: resp.status,
|
|
1197
|
+
productId,
|
|
1198
|
+
responseBody: body
|
|
1199
|
+
});
|
|
1156
1200
|
}
|
|
1157
1201
|
} catch (err2) {
|
|
1158
|
-
log.warn("[onesub/google] Product acknowledge network error \u2014 auto-refund risk
|
|
1202
|
+
log.warn("[onesub/google] Product acknowledge network error \u2014 auto-refund risk", { productId, err: err2 });
|
|
1159
1203
|
}
|
|
1160
1204
|
}
|
|
1161
1205
|
async function consumeGoogleProductReceipt(purchaseToken, productId, config) {
|
|
@@ -1165,7 +1209,7 @@ async function consumeGoogleProductReceipt(purchaseToken, productId, config) {
|
|
|
1165
1209
|
try {
|
|
1166
1210
|
accessToken = await getCachedAccessToken(config.serviceAccountKey);
|
|
1167
1211
|
} catch (err2) {
|
|
1168
|
-
log.warn("[onesub/google] Could not get access token for consume
|
|
1212
|
+
log.warn("[onesub/google] Could not get access token for consume", { productId, err: err2 });
|
|
1169
1213
|
return;
|
|
1170
1214
|
}
|
|
1171
1215
|
const url = `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${encodeURIComponent(config.packageName)}/purchases/products/${encodeURIComponent(productId)}/tokens/${encodeURIComponent(purchaseToken)}:consume`;
|
|
@@ -1176,10 +1220,14 @@ async function consumeGoogleProductReceipt(purchaseToken, productId, config) {
|
|
|
1176
1220
|
});
|
|
1177
1221
|
if (!resp.ok) {
|
|
1178
1222
|
const body = await resp.text();
|
|
1179
|
-
log.warn(
|
|
1223
|
+
log.warn("[onesub/google] Consume API error \u2014 auto-refund risk", {
|
|
1224
|
+
httpStatus: resp.status,
|
|
1225
|
+
productId,
|
|
1226
|
+
responseBody: body
|
|
1227
|
+
});
|
|
1180
1228
|
}
|
|
1181
1229
|
} catch (err2) {
|
|
1182
|
-
log.warn("[onesub/google] Consume network error \u2014 auto-refund risk
|
|
1230
|
+
log.warn("[onesub/google] Consume network error \u2014 auto-refund risk", { productId, err: err2 });
|
|
1183
1231
|
}
|
|
1184
1232
|
}
|
|
1185
1233
|
function deriveStatusV2(state) {
|
|
@@ -1205,11 +1253,11 @@ function deriveStatusV2(state) {
|
|
|
1205
1253
|
async function validateGoogleReceipt(receipt, productId, config) {
|
|
1206
1254
|
if (config.mockMode) return mockValidateGoogleSubscription(receipt, productId);
|
|
1207
1255
|
if (!config.serviceAccountKey) {
|
|
1208
|
-
log.warn("[onesub/google] No serviceAccountKey provided \u2014 cannot call Play API");
|
|
1256
|
+
log.warn("[onesub/google] No serviceAccountKey provided \u2014 cannot call Play API", { productId });
|
|
1209
1257
|
return null;
|
|
1210
1258
|
}
|
|
1211
1259
|
if (!config.packageName) {
|
|
1212
|
-
log.warn("[onesub/google] No packageName provided \u2014 cannot call Play API");
|
|
1260
|
+
log.warn("[onesub/google] No packageName provided \u2014 cannot call Play API", { productId });
|
|
1213
1261
|
return null;
|
|
1214
1262
|
}
|
|
1215
1263
|
let purchase;
|
|
@@ -1217,25 +1265,27 @@ async function validateGoogleReceipt(receipt, productId, config) {
|
|
|
1217
1265
|
const token = await getCachedAccessToken(config.serviceAccountKey);
|
|
1218
1266
|
purchase = await fetchSubscriptionPurchaseV2(config.packageName, receipt, token);
|
|
1219
1267
|
} catch (err2) {
|
|
1220
|
-
log.error("[onesub/google] Receipt validation failed
|
|
1268
|
+
log.error("[onesub/google] Receipt validation failed", {
|
|
1269
|
+
productId,
|
|
1270
|
+
packageName: config.packageName,
|
|
1271
|
+
err: err2
|
|
1272
|
+
});
|
|
1221
1273
|
return null;
|
|
1222
1274
|
}
|
|
1223
1275
|
const status = deriveStatusV2(purchase.subscriptionState);
|
|
1224
1276
|
if (!status) {
|
|
1225
|
-
log.warn(
|
|
1226
|
-
|
|
1227
|
-
purchase.subscriptionState
|
|
1228
|
-
);
|
|
1277
|
+
log.warn("[onesub/google] Unrecognised or pending subscriptionState \u2014 rejecting", {
|
|
1278
|
+
productId,
|
|
1279
|
+
subscriptionState: purchase.subscriptionState
|
|
1280
|
+
});
|
|
1229
1281
|
return null;
|
|
1230
1282
|
}
|
|
1231
1283
|
const lineItem = purchase.lineItems?.find((item) => item.productId === productId);
|
|
1232
1284
|
if (!lineItem) {
|
|
1233
|
-
log.warn(
|
|
1234
|
-
"[onesub/google] productId not found in subscription lineItems:",
|
|
1285
|
+
log.warn("[onesub/google] productId not found in subscription lineItems", {
|
|
1235
1286
|
productId,
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
);
|
|
1287
|
+
availableProductIds: purchase.lineItems?.map((i) => i.productId) ?? []
|
|
1288
|
+
});
|
|
1239
1289
|
return null;
|
|
1240
1290
|
}
|
|
1241
1291
|
const expiresAt = lineItem.expiryTime ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1267,11 +1317,11 @@ async function validateGoogleReceipt(receipt, productId, config) {
|
|
|
1267
1317
|
async function validateGoogleProductReceipt(purchaseToken, productId, config, type = "non_consumable") {
|
|
1268
1318
|
if (config.mockMode) return mockValidateGoogleProduct(purchaseToken, productId);
|
|
1269
1319
|
if (!config.serviceAccountKey) {
|
|
1270
|
-
log.warn("[onesub/google] No serviceAccountKey \u2014 cannot validate product receipt");
|
|
1320
|
+
log.warn("[onesub/google] No serviceAccountKey \u2014 cannot validate product receipt", { productId });
|
|
1271
1321
|
return null;
|
|
1272
1322
|
}
|
|
1273
1323
|
if (!config.packageName) {
|
|
1274
|
-
log.warn("[onesub/google] No packageName \u2014 cannot validate product receipt");
|
|
1324
|
+
log.warn("[onesub/google] No packageName \u2014 cannot validate product receipt", { productId });
|
|
1275
1325
|
return null;
|
|
1276
1326
|
}
|
|
1277
1327
|
let purchase;
|
|
@@ -1279,27 +1329,44 @@ async function validateGoogleProductReceipt(purchaseToken, productId, config, ty
|
|
|
1279
1329
|
const token = await getCachedAccessToken(config.serviceAccountKey);
|
|
1280
1330
|
purchase = await fetchProductPurchase(config.packageName, productId, purchaseToken, token);
|
|
1281
1331
|
} catch (err2) {
|
|
1282
|
-
log.error("[onesub/google] Product receipt validation failed
|
|
1332
|
+
log.error("[onesub/google] Product receipt validation failed", {
|
|
1333
|
+
productId,
|
|
1334
|
+
packageName: config.packageName,
|
|
1335
|
+
type,
|
|
1336
|
+
err: err2
|
|
1337
|
+
});
|
|
1283
1338
|
return null;
|
|
1284
1339
|
}
|
|
1285
1340
|
if (purchase.purchaseState !== 0) {
|
|
1286
|
-
log.warn("[onesub/google] Purchase not completed
|
|
1341
|
+
log.warn("[onesub/google] Purchase not completed", {
|
|
1342
|
+
productId,
|
|
1343
|
+
purchaseState: purchase.purchaseState,
|
|
1344
|
+
orderId: purchase.orderId
|
|
1345
|
+
});
|
|
1287
1346
|
return null;
|
|
1288
1347
|
}
|
|
1289
1348
|
if (type === "consumable" && purchase.consumptionState === 1) {
|
|
1290
|
-
log.warn("[onesub/google] Consumable already consumed \u2014 possible replay attack"
|
|
1349
|
+
log.warn("[onesub/google] Consumable already consumed \u2014 possible replay attack", {
|
|
1350
|
+
productId,
|
|
1351
|
+
orderId: purchase.orderId
|
|
1352
|
+
});
|
|
1291
1353
|
return null;
|
|
1292
1354
|
}
|
|
1293
1355
|
if (purchase.purchaseTimeMillis) {
|
|
1294
1356
|
const purchaseTime = parseInt(purchase.purchaseTimeMillis, 10);
|
|
1295
1357
|
const maxAgeMs = (config.productReceiptMaxAgeHours ?? 72) * 60 * 60 * 1e3;
|
|
1296
1358
|
if (Date.now() - purchaseTime > maxAgeMs) {
|
|
1297
|
-
log.warn(
|
|
1359
|
+
log.warn("[onesub/google] Product receipt too old", {
|
|
1360
|
+
productId,
|
|
1361
|
+
orderId: purchase.orderId,
|
|
1362
|
+
maxAgeHours: config.productReceiptMaxAgeHours ?? 72,
|
|
1363
|
+
purchaseDate: new Date(purchaseTime).toISOString()
|
|
1364
|
+
});
|
|
1298
1365
|
return null;
|
|
1299
1366
|
}
|
|
1300
1367
|
}
|
|
1301
1368
|
if (!purchase.orderId) {
|
|
1302
|
-
log.warn("[onesub/google] No orderId in product purchase");
|
|
1369
|
+
log.warn("[onesub/google] No orderId in product purchase", { productId });
|
|
1303
1370
|
return null;
|
|
1304
1371
|
}
|
|
1305
1372
|
return {
|
|
@@ -1356,7 +1423,11 @@ function decodeGoogleOneTimeProductNotification(payload) {
|
|
|
1356
1423
|
if (!notification.oneTimeProductNotification) return null;
|
|
1357
1424
|
const { notificationType, purchaseToken, sku } = notification.oneTimeProductNotification;
|
|
1358
1425
|
if (notificationType !== 1 && notificationType !== 2) {
|
|
1359
|
-
log.warn("[onesub/google] Unknown oneTimeProductNotification type
|
|
1426
|
+
log.warn("[onesub/google] Unknown oneTimeProductNotification type", {
|
|
1427
|
+
notificationType,
|
|
1428
|
+
productId: sku,
|
|
1429
|
+
packageName: notification.packageName
|
|
1430
|
+
});
|
|
1360
1431
|
return null;
|
|
1361
1432
|
}
|
|
1362
1433
|
return { notificationType, purchaseToken, sku, packageName: notification.packageName };
|
|
@@ -1430,7 +1501,10 @@ function buildAppRegistry(config) {
|
|
|
1430
1501
|
// that would validate one app's receipt against another's credentials.
|
|
1431
1502
|
(config.apple || config.google || apps.length === 1 ? apps[0] : void 0);
|
|
1432
1503
|
if (apps.length > 1) {
|
|
1433
|
-
log.info("[onesub] Multi-app mode
|
|
1504
|
+
log.info("[onesub] Multi-app mode", {
|
|
1505
|
+
appIds: apps.map(appName),
|
|
1506
|
+
defaultAppId: defaultApp ? appName(defaultApp) : null
|
|
1507
|
+
});
|
|
1434
1508
|
}
|
|
1435
1509
|
function match(hint) {
|
|
1436
1510
|
if (hint.appId) {
|
|
@@ -1438,13 +1512,13 @@ function buildAppRegistry(config) {
|
|
|
1438
1512
|
(a) => a.id === hint.appId || a.apple?.bundleId === hint.appId || a.google?.packageName === hint.appId
|
|
1439
1513
|
);
|
|
1440
1514
|
if (byId) return byId;
|
|
1441
|
-
log.warn("[onesub] Unknown appId
|
|
1515
|
+
log.warn("[onesub] Unknown appId", { appId: hint.appId });
|
|
1442
1516
|
return void 0;
|
|
1443
1517
|
}
|
|
1444
1518
|
if (hint.bundleId) {
|
|
1445
1519
|
const byBundle = apps.find((a) => a.apple?.bundleId === hint.bundleId);
|
|
1446
1520
|
if (byBundle) return byBundle;
|
|
1447
|
-
log.warn("[onesub] No app configured for bundleId
|
|
1521
|
+
log.warn("[onesub] No app configured for bundleId", { bundleId: hint.bundleId });
|
|
1448
1522
|
return void 0;
|
|
1449
1523
|
}
|
|
1450
1524
|
return defaultApp;
|
|
@@ -1532,9 +1606,10 @@ function createValidateRouter(config, store) {
|
|
|
1532
1606
|
delete sub.boundAccountId;
|
|
1533
1607
|
const bindingMismatch = platform === "apple" ? boundAccountId && boundAccountId.toLowerCase() !== userId.toLowerCase() : boundAccountId && boundAccountId !== userId;
|
|
1534
1608
|
if (bindingMismatch) {
|
|
1535
|
-
log.warn(
|
|
1536
|
-
|
|
1537
|
-
|
|
1609
|
+
log.warn("[onesub/validate] account binding mismatch \u2014 receipt token does not match userId", {
|
|
1610
|
+
originalTransactionId: sub.originalTransactionId,
|
|
1611
|
+
userId
|
|
1612
|
+
});
|
|
1538
1613
|
sendError(
|
|
1539
1614
|
res,
|
|
1540
1615
|
409,
|
|
@@ -1547,7 +1622,7 @@ function createValidateRouter(config, store) {
|
|
|
1547
1622
|
const isSandbox = sub.sandbox === true;
|
|
1548
1623
|
delete sub.sandbox;
|
|
1549
1624
|
if (isSandbox && getTestOverride(userId) === false) {
|
|
1550
|
-
log.warn(
|
|
1625
|
+
log.warn("[onesub/validate] sandbox test override active \u2014 forcing not-entitled", { userId });
|
|
1551
1626
|
sub.status = shared.SUBSCRIPTION_STATUS.EXPIRED;
|
|
1552
1627
|
sub.willRenew = false;
|
|
1553
1628
|
}
|
|
@@ -1559,7 +1634,7 @@ function createValidateRouter(config, store) {
|
|
|
1559
1634
|
const response = { valid: true, subscription: sub };
|
|
1560
1635
|
res.status(200).json(response);
|
|
1561
1636
|
} catch (err2) {
|
|
1562
|
-
log.error("[onesub/validate] Unexpected error
|
|
1637
|
+
log.error("[onesub/validate] Unexpected error", { userId, productId, platform, err: err2 });
|
|
1563
1638
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.INTERNAL_ERROR, "Internal server error during receipt validation", NO_SUB);
|
|
1564
1639
|
}
|
|
1565
1640
|
});
|
|
@@ -1591,7 +1666,7 @@ function createStatusRouter(store) {
|
|
|
1591
1666
|
const response = { active, subscription: sub };
|
|
1592
1667
|
res.status(200).json(response);
|
|
1593
1668
|
} catch (err2) {
|
|
1594
|
-
log.error("[onesub/status] Store error
|
|
1669
|
+
log.error("[onesub/status] Store error", { userId, err: err2 });
|
|
1595
1670
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error", NO_SUB2);
|
|
1596
1671
|
}
|
|
1597
1672
|
});
|
|
@@ -1699,7 +1774,7 @@ async function processAppleNotification(work, config, store, purchaseStore) {
|
|
|
1699
1774
|
});
|
|
1700
1775
|
}
|
|
1701
1776
|
} catch (err2) {
|
|
1702
|
-
log.warn("[onesub/webhook/apple] consumptionInfoProvider failed
|
|
1777
|
+
log.warn("[onesub/webhook/apple] consumptionInfoProvider failed", { transactionId, err: err2 });
|
|
1703
1778
|
}
|
|
1704
1779
|
})();
|
|
1705
1780
|
}
|
|
@@ -1712,7 +1787,7 @@ async function processAppleNotification(work, config, store, purchaseStore) {
|
|
|
1712
1787
|
const lookupId = transactionId ?? originalTransactionId;
|
|
1713
1788
|
const removed = await purchaseStore.deletePurchaseByTransactionId(lookupId);
|
|
1714
1789
|
if (!removed) {
|
|
1715
|
-
log.warn("[onesub/webhook/apple] IAP refund for unknown transaction
|
|
1790
|
+
log.warn("[onesub/webhook/apple] IAP refund for unknown transaction", { transactionId: lookupId });
|
|
1716
1791
|
}
|
|
1717
1792
|
return;
|
|
1718
1793
|
}
|
|
@@ -1722,7 +1797,10 @@ async function processAppleNotification(work, config, store, purchaseStore) {
|
|
|
1722
1797
|
const keepEntitlement = isSubscriptionRefund && config.refundPolicy === "until_expiry";
|
|
1723
1798
|
const correctedUserId = appAccountToken && existing.userId === originalTransactionId ? appAccountToken : existing.userId;
|
|
1724
1799
|
if (correctedUserId !== existing.userId) {
|
|
1725
|
-
log.info("[onesub/webhook/apple] correcting userId from originalTransactionId to appAccountToken
|
|
1800
|
+
log.info("[onesub/webhook/apple] correcting userId from originalTransactionId to appAccountToken", {
|
|
1801
|
+
originalTransactionId,
|
|
1802
|
+
userId: correctedUserId
|
|
1803
|
+
});
|
|
1726
1804
|
}
|
|
1727
1805
|
const updated = keepEntitlement ? { ...existing, userId: correctedUserId, willRenew: false } : {
|
|
1728
1806
|
...existing,
|
|
@@ -1740,20 +1818,22 @@ async function processAppleNotification(work, config, store, purchaseStore) {
|
|
|
1740
1818
|
fresh.userId = appAccountToken ?? originalTransactionId;
|
|
1741
1819
|
if (inAppOwnershipType === "FAMILY_SHARED") {
|
|
1742
1820
|
const source = appAccountToken ? "appAccountToken" : "originalTransactionId (fallback)";
|
|
1743
|
-
log.info(
|
|
1821
|
+
log.info("[onesub/webhook/apple] FAMILY_SHARED", {
|
|
1822
|
+
originalTransactionId,
|
|
1823
|
+
userId: fresh.userId,
|
|
1824
|
+
userIdSource: source
|
|
1825
|
+
});
|
|
1744
1826
|
}
|
|
1745
1827
|
await store.save(fresh);
|
|
1746
1828
|
} else {
|
|
1747
|
-
log.warn(
|
|
1748
|
-
"[onesub/webhook/apple] Unknown transaction and Status API returned no record:",
|
|
1829
|
+
log.warn("[onesub/webhook/apple] Unknown transaction and Status API returned no record", {
|
|
1749
1830
|
originalTransactionId
|
|
1750
|
-
);
|
|
1831
|
+
});
|
|
1751
1832
|
}
|
|
1752
1833
|
} else {
|
|
1753
|
-
log.warn(
|
|
1754
|
-
"[onesub/webhook/apple] Received notification for unknown transaction (no API creds to recover):",
|
|
1834
|
+
log.warn("[onesub/webhook/apple] Received notification for unknown transaction (no API creds to recover)", {
|
|
1755
1835
|
originalTransactionId
|
|
1756
|
-
);
|
|
1836
|
+
});
|
|
1757
1837
|
}
|
|
1758
1838
|
}
|
|
1759
1839
|
async function handleAppleWebhook(req, res, config, store, purchaseStore, webhookEventStore, webhookQueue) {
|
|
@@ -1769,7 +1849,7 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1769
1849
|
config.apple?.skipJwsVerification
|
|
1770
1850
|
);
|
|
1771
1851
|
} catch (err2) {
|
|
1772
|
-
log.error("[onesub/webhook/apple] Failed to decode signedPayload
|
|
1852
|
+
log.error("[onesub/webhook/apple] Failed to decode signedPayload", { err: err2 });
|
|
1773
1853
|
sendError(res, 400, shared.ONESUB_ERROR_CODE.INVALID_SIGNED_PAYLOAD, "Invalid signedPayload");
|
|
1774
1854
|
return;
|
|
1775
1855
|
}
|
|
@@ -1777,7 +1857,9 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1777
1857
|
if (webhookEventStore && typeof payload.notificationUUID === "string") {
|
|
1778
1858
|
const fresh = await webhookEventStore.markIfNew("apple", payload.notificationUUID);
|
|
1779
1859
|
if (!fresh) {
|
|
1780
|
-
log.info("[onesub/webhook/apple] dedupe: already processed",
|
|
1860
|
+
log.info("[onesub/webhook/apple] dedupe: already processed", {
|
|
1861
|
+
notificationUUID: payload.notificationUUID
|
|
1862
|
+
});
|
|
1781
1863
|
res.status(200).json({ received: true, deduped: true });
|
|
1782
1864
|
return;
|
|
1783
1865
|
}
|
|
@@ -1790,7 +1872,7 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1790
1872
|
}
|
|
1791
1873
|
const notifiedApp = getAppRegistry(config).configFor({ bundleId: decoded.bundleId });
|
|
1792
1874
|
if (decoded.bundleId && !notifiedApp.apple) {
|
|
1793
|
-
log.warn("[onesub/webhook/apple] No app configured for bundleId
|
|
1875
|
+
log.warn("[onesub/webhook/apple] No app configured for bundleId", { bundleId: decoded.bundleId });
|
|
1794
1876
|
sendError(res, 400, shared.ONESUB_ERROR_CODE.BUNDLE_ID_MISMATCH, "Bundle ID mismatch");
|
|
1795
1877
|
return;
|
|
1796
1878
|
}
|
|
@@ -1810,7 +1892,10 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1810
1892
|
});
|
|
1811
1893
|
res.status(200).json({ received: true, queued: true });
|
|
1812
1894
|
} catch (err2) {
|
|
1813
|
-
log.error("[onesub/webhook/apple] Failed to enqueue webhook job
|
|
1895
|
+
log.error("[onesub/webhook/apple] Failed to enqueue webhook job", {
|
|
1896
|
+
notificationUUID: payload.notificationUUID,
|
|
1897
|
+
err: err2
|
|
1898
|
+
});
|
|
1814
1899
|
await unmarkWebhookEvent(webhookEventStore, "apple", markedEventId);
|
|
1815
1900
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to update subscription");
|
|
1816
1901
|
}
|
|
@@ -1820,7 +1905,10 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1820
1905
|
await processAppleNotification(work, config, store, purchaseStore);
|
|
1821
1906
|
res.status(200).json({ received: true });
|
|
1822
1907
|
} catch (err2) {
|
|
1823
|
-
log.error("[onesub/webhook/apple] Store update error
|
|
1908
|
+
log.error("[onesub/webhook/apple] Store update error", {
|
|
1909
|
+
notificationUUID: payload.notificationUUID,
|
|
1910
|
+
err: err2
|
|
1911
|
+
});
|
|
1824
1912
|
await unmarkWebhookEvent(webhookEventStore, "apple", markedEventId);
|
|
1825
1913
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to update subscription");
|
|
1826
1914
|
}
|
|
@@ -1855,15 +1943,15 @@ async function verifyGooglePushToken(req, expectedAudience, expectedServiceAccou
|
|
|
1855
1943
|
}
|
|
1856
1944
|
var GOOGLE_FAILURE_MESSAGES = {
|
|
1857
1945
|
voided: {
|
|
1858
|
-
logPrefix: "[onesub/webhook/google] voided notification error
|
|
1946
|
+
logPrefix: "[onesub/webhook/google] voided notification error",
|
|
1859
1947
|
message: "Failed to process voided notification"
|
|
1860
1948
|
},
|
|
1861
1949
|
oneTimeProduct: {
|
|
1862
|
-
logPrefix: "[onesub/webhook/google] oneTimeProduct error
|
|
1950
|
+
logPrefix: "[onesub/webhook/google] oneTimeProduct error",
|
|
1863
1951
|
message: "Failed to process oneTimeProduct notification"
|
|
1864
1952
|
},
|
|
1865
1953
|
subscription: {
|
|
1866
|
-
logPrefix: "[onesub/webhook/google] Error handling notification
|
|
1954
|
+
logPrefix: "[onesub/webhook/google] Error handling notification",
|
|
1867
1955
|
message: "Failed to process notification"
|
|
1868
1956
|
}
|
|
1869
1957
|
};
|
|
@@ -1905,12 +1993,14 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
1905
1993
|
const updated = config.refundPolicy === "until_expiry" ? { ...existing2, willRenew: false } : { ...existing2, status: shared.SUBSCRIPTION_STATUS.CANCELED };
|
|
1906
1994
|
await store.save(updated);
|
|
1907
1995
|
} else {
|
|
1908
|
-
log.warn("[onesub/webhook/google] voided subscription for unknown purchaseToken
|
|
1996
|
+
log.warn("[onesub/webhook/google] voided subscription for unknown purchaseToken", {
|
|
1997
|
+
purchaseToken: voided.purchaseToken
|
|
1998
|
+
});
|
|
1909
1999
|
}
|
|
1910
2000
|
} else {
|
|
1911
2001
|
const removed = await purchaseStore.deletePurchaseByTransactionId(voided.orderId);
|
|
1912
2002
|
if (!removed) {
|
|
1913
|
-
log.warn("[onesub/webhook/google] voided IAP for unknown orderId
|
|
2003
|
+
log.warn("[onesub/webhook/google] voided IAP for unknown orderId", { orderId: voided.orderId });
|
|
1914
2004
|
}
|
|
1915
2005
|
}
|
|
1916
2006
|
return;
|
|
@@ -1918,15 +2008,18 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
1918
2008
|
if (work.kind === "oneTimeProduct") {
|
|
1919
2009
|
const { notificationType: notificationType2, purchaseToken: purchaseToken2, sku } = work.oneTimeProduct;
|
|
1920
2010
|
if (notificationType2 === 1) {
|
|
1921
|
-
log.info("[onesub/webhook/google] oneTimeProduct PURCHASED
|
|
2011
|
+
log.info("[onesub/webhook/google] oneTimeProduct PURCHASED", { productId: sku });
|
|
1922
2012
|
const googleCfg = googleFor(work.oneTimeProduct.packageName);
|
|
1923
2013
|
if (googleCfg?.serviceAccountKey && googleCfg.packageName) {
|
|
1924
2014
|
void acknowledgeGoogleProduct(purchaseToken2, sku, googleCfg).catch(
|
|
1925
|
-
(err2) => log.warn(
|
|
2015
|
+
(err2) => log.warn("[onesub/webhook/google] oneTimeProduct ack failed \u2014 3-day auto-refund risk", {
|
|
2016
|
+
productId: sku,
|
|
2017
|
+
err: err2
|
|
2018
|
+
})
|
|
1926
2019
|
);
|
|
1927
2020
|
}
|
|
1928
2021
|
} else {
|
|
1929
|
-
log.info("[onesub/webhook/google] oneTimeProduct CANCELED (pre-completion)
|
|
2022
|
+
log.info("[onesub/webhook/google] oneTimeProduct CANCELED (pre-completion)", { productId: sku });
|
|
1930
2023
|
}
|
|
1931
2024
|
return;
|
|
1932
2025
|
}
|
|
@@ -1953,7 +2046,7 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
1953
2046
|
const subGoogleCfg = googleFor(packageName);
|
|
1954
2047
|
if (isGooglePriceChangeConfirmedNotification(notificationType) && subGoogleCfg?.onPriceChangeConfirmed) {
|
|
1955
2048
|
const hook = subGoogleCfg.onPriceChangeConfirmed;
|
|
1956
|
-
void Promise.resolve().then(() => hook({ purchaseToken, subscriptionId, packageName })).catch((err2) => log.warn("[onesub/webhook/google] onPriceChangeConfirmed hook failed
|
|
2049
|
+
void Promise.resolve().then(() => hook({ purchaseToken, subscriptionId, packageName })).catch((err2) => log.warn("[onesub/webhook/google] onPriceChangeConfirmed hook failed", { err: err2 }));
|
|
1957
2050
|
}
|
|
1958
2051
|
if (existing) {
|
|
1959
2052
|
let updated = { ...existing, status: finalStatus };
|
|
@@ -1982,7 +2075,11 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
1982
2075
|
const previous = await store.getByTransactionId(fresh.linkedPurchaseToken);
|
|
1983
2076
|
fresh.userId = previous ? previous.userId : boundAccountId ?? purchaseToken;
|
|
1984
2077
|
if (previous) {
|
|
1985
|
-
log.info(
|
|
2078
|
+
log.info("[onesub/webhook/google] inherited userId from linkedPurchaseToken", {
|
|
2079
|
+
userId: previous.userId,
|
|
2080
|
+
linkedPurchaseToken: fresh.linkedPurchaseToken,
|
|
2081
|
+
purchaseToken
|
|
2082
|
+
});
|
|
1986
2083
|
}
|
|
1987
2084
|
} else {
|
|
1988
2085
|
fresh.userId = boundAccountId ?? purchaseToken;
|
|
@@ -1990,7 +2087,9 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
1990
2087
|
await store.save(fresh);
|
|
1991
2088
|
}
|
|
1992
2089
|
} else {
|
|
1993
|
-
log.warn("[onesub/webhook/google] Unknown purchase token and no serviceAccountKey to re-fetch
|
|
2090
|
+
log.warn("[onesub/webhook/google] Unknown purchase token and no serviceAccountKey to re-fetch", {
|
|
2091
|
+
purchaseToken
|
|
2092
|
+
});
|
|
1994
2093
|
}
|
|
1995
2094
|
}
|
|
1996
2095
|
}
|
|
@@ -2018,7 +2117,9 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2018
2117
|
if (webhookEventStore && typeof body.message.messageId === "string") {
|
|
2019
2118
|
const fresh = await webhookEventStore.markIfNew("google", body.message.messageId);
|
|
2020
2119
|
if (!fresh) {
|
|
2021
|
-
log.info("[onesub/webhook/google] dedupe: already processed",
|
|
2120
|
+
log.info("[onesub/webhook/google] dedupe: already processed", {
|
|
2121
|
+
messageId: body.message.messageId
|
|
2122
|
+
});
|
|
2022
2123
|
res.status(200).json({ received: true, deduped: true });
|
|
2023
2124
|
return;
|
|
2024
2125
|
}
|
|
@@ -2029,7 +2130,9 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2029
2130
|
const voided = decodeGoogleVoidedNotification(body);
|
|
2030
2131
|
if (voided) {
|
|
2031
2132
|
if (!servesPackage(voided.packageName)) {
|
|
2032
|
-
log.warn("[onesub/webhook/google] voided package name not served
|
|
2133
|
+
log.warn("[onesub/webhook/google] voided package name not served", {
|
|
2134
|
+
packageName: voided.packageName
|
|
2135
|
+
});
|
|
2033
2136
|
sendError(res, 400, shared.ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
|
|
2034
2137
|
return;
|
|
2035
2138
|
}
|
|
@@ -2038,7 +2141,9 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2038
2141
|
const oneTimeProduct = decodeGoogleOneTimeProductNotification(body);
|
|
2039
2142
|
if (oneTimeProduct) {
|
|
2040
2143
|
if (!servesPackage(oneTimeProduct.packageName)) {
|
|
2041
|
-
log.warn("[onesub/webhook/google] oneTimeProduct package name not served
|
|
2144
|
+
log.warn("[onesub/webhook/google] oneTimeProduct package name not served", {
|
|
2145
|
+
packageName: oneTimeProduct.packageName
|
|
2146
|
+
});
|
|
2042
2147
|
sendError(res, 400, shared.ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
|
|
2043
2148
|
return;
|
|
2044
2149
|
}
|
|
@@ -2050,7 +2155,9 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2050
2155
|
return;
|
|
2051
2156
|
}
|
|
2052
2157
|
if (!servesPackage(notification.packageName)) {
|
|
2053
|
-
log.warn("[onesub/webhook/google] Package name not served
|
|
2158
|
+
log.warn("[onesub/webhook/google] Package name not served", {
|
|
2159
|
+
packageName: notification.packageName
|
|
2160
|
+
});
|
|
2054
2161
|
sendError(res, 400, shared.ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
|
|
2055
2162
|
return;
|
|
2056
2163
|
}
|
|
@@ -2068,7 +2175,11 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2068
2175
|
});
|
|
2069
2176
|
res.status(200).json({ received: true, queued: true });
|
|
2070
2177
|
} catch (err2) {
|
|
2071
|
-
log.error("[onesub/webhook/google] Failed to enqueue webhook job
|
|
2178
|
+
log.error("[onesub/webhook/google] Failed to enqueue webhook job", {
|
|
2179
|
+
kind: work.kind,
|
|
2180
|
+
messageId: body.message.messageId,
|
|
2181
|
+
err: err2
|
|
2182
|
+
});
|
|
2072
2183
|
await unmarkWebhookEvent(webhookEventStore, "google", markedEventId);
|
|
2073
2184
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, GOOGLE_FAILURE_MESSAGES[work.kind].message);
|
|
2074
2185
|
}
|
|
@@ -2079,7 +2190,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2079
2190
|
res.status(200).json({ received: true });
|
|
2080
2191
|
} catch (err2) {
|
|
2081
2192
|
const { logPrefix, message } = GOOGLE_FAILURE_MESSAGES[work.kind];
|
|
2082
|
-
log.error(logPrefix, err2);
|
|
2193
|
+
log.error(logPrefix, { kind: work.kind, err: err2 });
|
|
2083
2194
|
await unmarkWebhookEvent(webhookEventStore, "google", markedEventId);
|
|
2084
2195
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, message);
|
|
2085
2196
|
}
|
|
@@ -2191,9 +2302,10 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2191
2302
|
}
|
|
2192
2303
|
const bindingMismatch = platform === "apple" ? boundAccountId && boundAccountId.toLowerCase() !== userId.toLowerCase() : boundAccountId && boundAccountId !== userId;
|
|
2193
2304
|
if (bindingMismatch) {
|
|
2194
|
-
log.warn(
|
|
2195
|
-
|
|
2196
|
-
|
|
2305
|
+
log.warn("[onesub/purchase] account binding mismatch \u2014 token does not match userId", {
|
|
2306
|
+
transactionId,
|
|
2307
|
+
userId
|
|
2308
|
+
});
|
|
2197
2309
|
sendError(
|
|
2198
2310
|
res,
|
|
2199
2311
|
409,
|
|
@@ -2210,9 +2322,11 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2210
2322
|
if (existing.userId !== userId) {
|
|
2211
2323
|
if (type === shared.PURCHASE_TYPE.NON_CONSUMABLE) {
|
|
2212
2324
|
await purchaseStore.reassignPurchase(transactionId, userId);
|
|
2213
|
-
log.info(
|
|
2214
|
-
|
|
2215
|
-
|
|
2325
|
+
log.info("[onesub/purchase] reassigned transaction to a new user", {
|
|
2326
|
+
transactionId,
|
|
2327
|
+
fromUserId: existing.userId,
|
|
2328
|
+
userId
|
|
2329
|
+
});
|
|
2216
2330
|
} else {
|
|
2217
2331
|
sendError(
|
|
2218
2332
|
res,
|
|
@@ -2256,7 +2370,7 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2256
2370
|
};
|
|
2257
2371
|
res.status(200).json(response);
|
|
2258
2372
|
} catch (err2) {
|
|
2259
|
-
log.error("[onesub/purchase/validate] Unexpected error
|
|
2373
|
+
log.error("[onesub/purchase/validate] Unexpected error", { userId, productId, platform, type, err: err2 });
|
|
2260
2374
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.INTERNAL_ERROR, "Internal server error during purchase validation", NO_PURCHASE);
|
|
2261
2375
|
}
|
|
2262
2376
|
});
|
|
@@ -2271,7 +2385,7 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2271
2385
|
const response = { purchases };
|
|
2272
2386
|
res.status(200).json(response);
|
|
2273
2387
|
} catch (err2) {
|
|
2274
|
-
log.error("[onesub/purchase/status] Store error
|
|
2388
|
+
log.error("[onesub/purchase/status] Store error", { userId, productId, err: err2 });
|
|
2275
2389
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error", { purchases: [] });
|
|
2276
2390
|
}
|
|
2277
2391
|
});
|
|
@@ -2336,7 +2450,7 @@ function createEntitlementRouter(config, store, purchaseStore) {
|
|
|
2336
2450
|
const response = { id, ...status };
|
|
2337
2451
|
res.status(200).json(response);
|
|
2338
2452
|
} catch (err2) {
|
|
2339
|
-
log.error("[onesub/entitlement] evaluation error
|
|
2453
|
+
log.error("[onesub/entitlement] evaluation error", { userId, entitlement: id, err: err2 });
|
|
2340
2454
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2341
2455
|
}
|
|
2342
2456
|
});
|
|
@@ -2360,7 +2474,7 @@ function createEntitlementRouter(config, store, purchaseStore) {
|
|
|
2360
2474
|
};
|
|
2361
2475
|
res.status(200).json(response);
|
|
2362
2476
|
} catch (err2) {
|
|
2363
|
-
log.error("[onesub/entitlements] evaluation error
|
|
2477
|
+
log.error("[onesub/entitlements] evaluation error", { userId, err: err2 });
|
|
2364
2478
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error", { entitlements: {} });
|
|
2365
2479
|
}
|
|
2366
2480
|
});
|
|
@@ -2474,7 +2588,7 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2474
2588
|
};
|
|
2475
2589
|
res.status(200).json(response);
|
|
2476
2590
|
} catch (err2) {
|
|
2477
|
-
log.error("[onesub/admin/subscriptions] list error
|
|
2591
|
+
log.error("[onesub/admin/subscriptions] list error", { err: err2 });
|
|
2478
2592
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2479
2593
|
}
|
|
2480
2594
|
});
|
|
@@ -2494,7 +2608,10 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2494
2608
|
}
|
|
2495
2609
|
res.status(200).json(sub);
|
|
2496
2610
|
} catch (err2) {
|
|
2497
|
-
log.error("[onesub/admin/subscriptions/:transactionId] detail error
|
|
2611
|
+
log.error("[onesub/admin/subscriptions/:transactionId] detail error", {
|
|
2612
|
+
transactionId: params.transactionId,
|
|
2613
|
+
err: err2
|
|
2614
|
+
});
|
|
2498
2615
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2499
2616
|
}
|
|
2500
2617
|
});
|
|
@@ -2527,7 +2644,7 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2527
2644
|
};
|
|
2528
2645
|
res.status(200).json(response);
|
|
2529
2646
|
} catch (err2) {
|
|
2530
|
-
log.error("[onesub/admin/customers/:userId] error
|
|
2647
|
+
log.error("[onesub/admin/customers/:userId] error", { userId: params.userId, err: err2 });
|
|
2531
2648
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2532
2649
|
}
|
|
2533
2650
|
});
|
|
@@ -2557,7 +2674,10 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2557
2674
|
await store.save(fresh);
|
|
2558
2675
|
res.status(200).json({ ok: true, subscription: fresh });
|
|
2559
2676
|
} catch (err2) {
|
|
2560
|
-
log.error("[onesub/admin/sync-apple] error
|
|
2677
|
+
log.error("[onesub/admin/sync-apple] error", {
|
|
2678
|
+
originalTransactionId: params.originalTransactionId,
|
|
2679
|
+
err: err2
|
|
2680
|
+
});
|
|
2561
2681
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.INTERNAL_ERROR, "Internal server error");
|
|
2562
2682
|
}
|
|
2563
2683
|
});
|
|
@@ -2572,9 +2692,10 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2572
2692
|
const body = parseOrSend(res, overrideBodySchema, req.body);
|
|
2573
2693
|
if (!body) return;
|
|
2574
2694
|
setTestOverride(params.userId, body.entitled);
|
|
2575
|
-
log.warn(
|
|
2576
|
-
|
|
2577
|
-
|
|
2695
|
+
log.warn("[onesub/admin] sandbox test override set", {
|
|
2696
|
+
userId: params.userId,
|
|
2697
|
+
entitled: body.entitled
|
|
2698
|
+
});
|
|
2578
2699
|
res.json({ ok: true, userId: params.userId, entitled: body.entitled, sandboxOnly: true });
|
|
2579
2700
|
});
|
|
2580
2701
|
router.delete(shared.ROUTES.ADMIN_TEST_OVERRIDE, (req, res) => {
|
|
@@ -2591,7 +2712,7 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2591
2712
|
const items = await webhookQueue.listDeadLetters();
|
|
2592
2713
|
res.status(200).json({ items });
|
|
2593
2714
|
} catch (err2) {
|
|
2594
|
-
log.error("[onesub/admin/webhook-deadletters] error
|
|
2715
|
+
log.error("[onesub/admin/webhook-deadletters] error", { err: err2 });
|
|
2595
2716
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2596
2717
|
}
|
|
2597
2718
|
});
|
|
@@ -2605,7 +2726,7 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2605
2726
|
await webhookQueue.replayDeadLetter(params.id);
|
|
2606
2727
|
res.status(200).json({ ok: true });
|
|
2607
2728
|
} catch (err2) {
|
|
2608
|
-
log.error("[onesub/admin/webhook-replay] error
|
|
2729
|
+
log.error("[onesub/admin/webhook-replay] error", { deadLetterId: params.id, err: err2 });
|
|
2609
2730
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2610
2731
|
}
|
|
2611
2732
|
});
|
|
@@ -2775,7 +2896,7 @@ function createMetricsRouter(config, store, purchaseStore) {
|
|
|
2775
2896
|
);
|
|
2776
2897
|
res.status(200).json(response);
|
|
2777
2898
|
} catch (err2) {
|
|
2778
|
-
log.error("[onesub/metrics/active] error
|
|
2899
|
+
log.error("[onesub/metrics/active] error", { err: err2 });
|
|
2779
2900
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2780
2901
|
}
|
|
2781
2902
|
});
|
|
@@ -2842,7 +2963,7 @@ function createMetricsRouter(config, store, purchaseStore) {
|
|
|
2842
2963
|
};
|
|
2843
2964
|
res.status(200).json(response);
|
|
2844
2965
|
} catch (err2) {
|
|
2845
|
-
log.error(
|
|
2966
|
+
log.error("[onesub/metrics] error", { route: name, err: err2 });
|
|
2846
2967
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2847
2968
|
}
|
|
2848
2969
|
};
|
|
@@ -2914,7 +3035,7 @@ function createAppleOfferRouter(config) {
|
|
|
2914
3035
|
);
|
|
2915
3036
|
res.status(200).json(result);
|
|
2916
3037
|
} catch (err2) {
|
|
2917
|
-
log.error("[onesub/apple/offer] signing error
|
|
3038
|
+
log.error("[onesub/apple/offer] signing error", { productId: body.productId, err: err2 });
|
|
2918
3039
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.INTERNAL_ERROR, "Offer signing failed");
|
|
2919
3040
|
}
|
|
2920
3041
|
});
|
|
@@ -2986,7 +3107,7 @@ async function createPgPool(connectionString, label) {
|
|
|
2986
3107
|
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
2987
3108
|
const pool = new Pool({ connectionString, max: 10 });
|
|
2988
3109
|
pool.on("error", (err2) => {
|
|
2989
|
-
log.error(
|
|
3110
|
+
log.error("[onesub] Postgres pool error (idle client)", { store: label, err: err2 });
|
|
2990
3111
|
});
|
|
2991
3112
|
return pool;
|
|
2992
3113
|
}
|
|
@@ -3727,7 +3848,7 @@ var BullMQWebhookQueue = class {
|
|
|
3727
3848
|
const { Queue } = await this.getBullMQ();
|
|
3728
3849
|
const queue = new Queue(this.queueName, { connection: this.connection });
|
|
3729
3850
|
queue.on?.("error", (err2) => {
|
|
3730
|
-
log.error("[onesub] BullMQ queue error
|
|
3851
|
+
log.error("[onesub] BullMQ queue error", { err: err2 });
|
|
3731
3852
|
});
|
|
3732
3853
|
return queue;
|
|
3733
3854
|
})();
|
|
@@ -3751,13 +3872,13 @@ var BullMQWebhookQueue = class {
|
|
|
3751
3872
|
}
|
|
3752
3873
|
);
|
|
3753
3874
|
worker.on?.("error", (err2) => {
|
|
3754
|
-
log.error("[onesub] BullMQ worker error
|
|
3875
|
+
log.error("[onesub] BullMQ worker error", { err: err2 });
|
|
3755
3876
|
});
|
|
3756
3877
|
return worker;
|
|
3757
3878
|
})();
|
|
3758
3879
|
this.workerPromise.catch((err2) => {
|
|
3759
3880
|
this.workerStartupError = err2 instanceof Error ? err2 : new Error(String(err2));
|
|
3760
|
-
log.error("[onesub] BullMQ worker failed to start
|
|
3881
|
+
log.error("[onesub] BullMQ worker failed to start", { err: err2 });
|
|
3761
3882
|
});
|
|
3762
3883
|
}
|
|
3763
3884
|
}
|