@onesub/server 0.12.0 → 0.13.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__/webhook-apple.test.d.ts +12 -0
- package/dist/__tests__/webhook-apple.test.d.ts.map +1 -0
- package/dist/__tests__/webhook-google.test.d.ts +13 -0
- package/dist/__tests__/webhook-google.test.d.ts.map +1 -0
- package/dist/index.cjs +479 -284
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +479 -283
- package/dist/index.js.map +1 -1
- package/dist/providers/apple.d.ts +76 -0
- package/dist/providers/apple.d.ts.map +1 -1
- package/dist/providers/google.d.ts +25 -0
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/routes/admin.d.ts.map +1 -1
- package/dist/routes/apple-offer.d.ts +21 -0
- package/dist/routes/apple-offer.d.ts.map +1 -0
- package/dist/routes/webhook-apple.d.ts +6 -0
- package/dist/routes/webhook-apple.d.ts.map +1 -0
- package/dist/routes/webhook-google.d.ts +7 -0
- package/dist/routes/webhook-google.d.ts.map +1 -0
- package/dist/routes/webhook.d.ts +4 -17
- package/dist/routes/webhook.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import express, { Router } from 'express';
|
|
|
2
2
|
import { PURCHASE_TYPE, ROUTES, DEFAULT_PORT, ONESUB_ERROR_CODE, SUBSCRIPTION_STATUS, MOCK_RECEIPT_PREFIX } from '@onesub/shared';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { decodeJwt, decodeProtectedHeader, importX509, jwtVerify, importPKCS8, SignJWT, createRemoteJWKSet } from 'jose';
|
|
5
|
-
import { X509Certificate, createHash } from 'crypto';
|
|
5
|
+
import { X509Certificate, randomUUID, createSign, createHash } from 'crypto';
|
|
6
6
|
|
|
7
7
|
// src/index.ts
|
|
8
8
|
|
|
@@ -470,7 +470,9 @@ async function decodeAppleNotification(payload, skipJwsVerification = false) {
|
|
|
470
470
|
environment,
|
|
471
471
|
status,
|
|
472
472
|
willRenew,
|
|
473
|
-
expiresAt: tx.expiresDate ? new Date(tx.expiresDate).toISOString() : null
|
|
473
|
+
expiresAt: tx.expiresDate ? new Date(tx.expiresDate).toISOString() : null,
|
|
474
|
+
appAccountToken: tx.appAccountToken ?? null,
|
|
475
|
+
inAppOwnershipType: tx.inAppOwnershipType ?? null
|
|
474
476
|
};
|
|
475
477
|
}
|
|
476
478
|
var appleJwtMintPromises = /* @__PURE__ */ new Map();
|
|
@@ -616,6 +618,91 @@ async function fetchAppleSubscriptionStatus(originalTransactionId, config, optio
|
|
|
616
618
|
willRenew: renewal?.autoRenewStatus === 1
|
|
617
619
|
};
|
|
618
620
|
}
|
|
621
|
+
async function fetchAppleTransactionHistory(originalTransactionId, config, options) {
|
|
622
|
+
if (config.mockMode) return null;
|
|
623
|
+
let jwt;
|
|
624
|
+
try {
|
|
625
|
+
jwt = await makeAppleApiJwt(config);
|
|
626
|
+
} catch (err) {
|
|
627
|
+
log.warn("[onesub/apple] Cannot fetch transaction history \u2014 JWT mint failed:", err);
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
const host = options?.sandbox ? "api.storekit-sandbox.itunes.apple.com" : "api.storekit.itunes.apple.com";
|
|
631
|
+
const results = [];
|
|
632
|
+
let revision;
|
|
633
|
+
for (; ; ) {
|
|
634
|
+
const url = new URL(`https://${host}/inApps/v2/history/${encodeURIComponent(originalTransactionId)}`);
|
|
635
|
+
if (revision) url.searchParams.set("revision", revision);
|
|
636
|
+
let page;
|
|
637
|
+
try {
|
|
638
|
+
const resp = await fetchWithTimeout(url.toString(), {
|
|
639
|
+
headers: { Authorization: `Bearer ${jwt}` }
|
|
640
|
+
});
|
|
641
|
+
if (!resp.ok) {
|
|
642
|
+
const text = await resp.text();
|
|
643
|
+
log.warn(`[onesub/apple] Transaction History API error ${resp.status}: ${text}`);
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
page = await resp.json();
|
|
647
|
+
} catch (err) {
|
|
648
|
+
log.warn("[onesub/apple] Transaction History API network error:", err);
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
for (const signed of page.signedTransactions ?? []) {
|
|
652
|
+
try {
|
|
653
|
+
const tx = await decodeJws(signed, config.skipJwsVerification);
|
|
654
|
+
if (!tx.originalTransactionId || !tx.transactionId || !tx.productId) continue;
|
|
655
|
+
results.push({
|
|
656
|
+
originalTransactionId: tx.originalTransactionId,
|
|
657
|
+
transactionId: tx.transactionId,
|
|
658
|
+
productId: tx.productId,
|
|
659
|
+
type: tx.type ?? "Unknown",
|
|
660
|
+
purchasedAt: tx.purchaseDate ? new Date(tx.purchaseDate).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
661
|
+
expiresAt: tx.expiresDate ? new Date(tx.expiresDate).toISOString() : null,
|
|
662
|
+
appAccountToken: tx.appAccountToken ?? null,
|
|
663
|
+
inAppOwnershipType: tx.inAppOwnershipType ?? null,
|
|
664
|
+
revocationDate: tx.revocationDate ? new Date(tx.revocationDate).toISOString() : null
|
|
665
|
+
});
|
|
666
|
+
} catch {
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (!page.hasMore) break;
|
|
670
|
+
revision = page.revision;
|
|
671
|
+
}
|
|
672
|
+
return results;
|
|
673
|
+
}
|
|
674
|
+
async function signApplePromotionalOffer(input, config) {
|
|
675
|
+
const { offerKeyId, offerPrivateKey } = config;
|
|
676
|
+
if (!offerKeyId || !offerPrivateKey) {
|
|
677
|
+
throw new Error("[onesub/apple] Promotional Offer signing requires config.apple.offerKeyId and config.apple.offerPrivateKey");
|
|
678
|
+
}
|
|
679
|
+
const nonce = randomUUID().toLowerCase();
|
|
680
|
+
const timestamp = Date.now();
|
|
681
|
+
const separator = "\u2063";
|
|
682
|
+
const message = [
|
|
683
|
+
input.bundleId,
|
|
684
|
+
offerKeyId,
|
|
685
|
+
input.productId,
|
|
686
|
+
input.offerId,
|
|
687
|
+
input.applicationUsername,
|
|
688
|
+
nonce,
|
|
689
|
+
String(timestamp)
|
|
690
|
+
].join(separator);
|
|
691
|
+
if (!offerPrivateKey.includes("BEGIN") || !offerPrivateKey.includes("END")) {
|
|
692
|
+
throw new Error("[onesub/apple] offerPrivateKey does not appear to be PEM-encoded");
|
|
693
|
+
}
|
|
694
|
+
let signatureBuffer;
|
|
695
|
+
try {
|
|
696
|
+
const sign = createSign("SHA256");
|
|
697
|
+
sign.update(message);
|
|
698
|
+
sign.end();
|
|
699
|
+
signatureBuffer = sign.sign(offerPrivateKey);
|
|
700
|
+
} catch (err) {
|
|
701
|
+
throw new Error(`[onesub/apple] Failed to sign promotional offer \u2014 check that offerPrivateKey is a valid ES256 PEM key: ${err.message}`);
|
|
702
|
+
}
|
|
703
|
+
const signature = signatureBuffer.toString("base64");
|
|
704
|
+
return { keyId: offerKeyId, nonce, timestamp, signature };
|
|
705
|
+
}
|
|
619
706
|
var GOOGLE_NOTIFICATION_TYPE = {
|
|
620
707
|
SUBSCRIPTION_RECOVERED: 1,
|
|
621
708
|
SUBSCRIPTION_RENEWED: 2,
|
|
@@ -678,8 +765,8 @@ async function getAccessToken(serviceAccountKey) {
|
|
|
678
765
|
})
|
|
679
766
|
).toString("base64url");
|
|
680
767
|
const signingInput = `${header}.${payload}`;
|
|
681
|
-
const { createSign } = await import('crypto');
|
|
682
|
-
const sign =
|
|
768
|
+
const { createSign: createSign2 } = await import('crypto');
|
|
769
|
+
const sign = createSign2("RSA-SHA256");
|
|
683
770
|
sign.update(signingInput);
|
|
684
771
|
const signature = sign.sign(key.private_key, "base64url");
|
|
685
772
|
const assertion = `${signingInput}.${signature}`;
|
|
@@ -723,6 +810,7 @@ async function fetchProductPurchase(packageName, productId, purchaseToken, acces
|
|
|
723
810
|
async function acknowledgeGoogleSubscription(purchaseToken, productId, config) {
|
|
724
811
|
if (config.mockMode) return;
|
|
725
812
|
if (!config.serviceAccountKey) return;
|
|
813
|
+
if (!config.packageName) return;
|
|
726
814
|
let accessToken;
|
|
727
815
|
try {
|
|
728
816
|
accessToken = await getCachedAccessToken(config.serviceAccountKey);
|
|
@@ -748,6 +836,7 @@ async function acknowledgeGoogleSubscription(purchaseToken, productId, config) {
|
|
|
748
836
|
async function acknowledgeGoogleProduct(purchaseToken, productId, config) {
|
|
749
837
|
if (config.mockMode) return;
|
|
750
838
|
if (!config.serviceAccountKey) return;
|
|
839
|
+
if (!config.packageName) return;
|
|
751
840
|
let accessToken;
|
|
752
841
|
try {
|
|
753
842
|
accessToken = await getCachedAccessToken(config.serviceAccountKey);
|
|
@@ -772,6 +861,7 @@ async function acknowledgeGoogleProduct(purchaseToken, productId, config) {
|
|
|
772
861
|
}
|
|
773
862
|
async function consumeGoogleProductReceipt(purchaseToken, productId, config) {
|
|
774
863
|
if (!config.serviceAccountKey) return;
|
|
864
|
+
if (!config.packageName) return;
|
|
775
865
|
let accessToken;
|
|
776
866
|
try {
|
|
777
867
|
accessToken = await getCachedAccessToken(config.serviceAccountKey);
|
|
@@ -819,6 +909,10 @@ async function validateGoogleReceipt(receipt, productId, config) {
|
|
|
819
909
|
log.warn("[onesub/google] No serviceAccountKey provided \u2014 cannot call Play API");
|
|
820
910
|
return null;
|
|
821
911
|
}
|
|
912
|
+
if (!config.packageName) {
|
|
913
|
+
log.warn("[onesub/google] No packageName provided \u2014 cannot call Play API");
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
822
916
|
let purchase;
|
|
823
917
|
try {
|
|
824
918
|
const token = await getCachedAccessToken(config.serviceAccountKey);
|
|
@@ -871,6 +965,10 @@ async function validateGoogleProductReceipt(purchaseToken, productId, config, ty
|
|
|
871
965
|
log.warn("[onesub/google] No serviceAccountKey \u2014 cannot validate product receipt");
|
|
872
966
|
return null;
|
|
873
967
|
}
|
|
968
|
+
if (!config.packageName) {
|
|
969
|
+
log.warn("[onesub/google] No packageName \u2014 cannot validate product receipt");
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
874
972
|
let purchase;
|
|
875
973
|
try {
|
|
876
974
|
const token = await getCachedAccessToken(config.serviceAccountKey);
|
|
@@ -940,6 +1038,22 @@ function decodeGoogleVoidedNotification(payload) {
|
|
|
940
1038
|
packageName: notification.packageName
|
|
941
1039
|
};
|
|
942
1040
|
}
|
|
1041
|
+
function decodeGoogleOneTimeProductNotification(payload) {
|
|
1042
|
+
let notification;
|
|
1043
|
+
try {
|
|
1044
|
+
const json = Buffer.from(payload.message.data, "base64").toString("utf-8");
|
|
1045
|
+
notification = JSON.parse(json);
|
|
1046
|
+
} catch {
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
if (!notification.oneTimeProductNotification) return null;
|
|
1050
|
+
const { notificationType, purchaseToken, sku } = notification.oneTimeProductNotification;
|
|
1051
|
+
if (notificationType !== 1 && notificationType !== 2) {
|
|
1052
|
+
log.warn("[onesub/google] Unknown oneTimeProductNotification type:", notificationType);
|
|
1053
|
+
return null;
|
|
1054
|
+
}
|
|
1055
|
+
return { notificationType, purchaseToken, sku, packageName: notification.packageName };
|
|
1056
|
+
}
|
|
943
1057
|
function isGoogleActiveNotification(notificationType) {
|
|
944
1058
|
return notificationType === GOOGLE_NOTIFICATION_TYPE.SUBSCRIPTION_PURCHASED || notificationType === GOOGLE_NOTIFICATION_TYPE.SUBSCRIPTION_RENEWED || notificationType === GOOGLE_NOTIFICATION_TYPE.SUBSCRIPTION_RECOVERED || notificationType === GOOGLE_NOTIFICATION_TYPE.SUBSCRIPTION_RESTARTED;
|
|
945
1059
|
}
|
|
@@ -1063,41 +1177,9 @@ function createStatusRouter(store) {
|
|
|
1063
1177
|
});
|
|
1064
1178
|
return router;
|
|
1065
1179
|
}
|
|
1066
|
-
var
|
|
1067
|
-
var
|
|
1068
|
-
|
|
1069
|
-
if (!googleJwks) {
|
|
1070
|
-
googleJwks = createRemoteJWKSet(new URL(GOOGLE_JWKS_URL));
|
|
1071
|
-
}
|
|
1072
|
-
return googleJwks;
|
|
1073
|
-
}
|
|
1074
|
-
async function verifyGooglePushToken(req, expectedAudience) {
|
|
1075
|
-
const authHeader = req.headers["authorization"];
|
|
1076
|
-
if (typeof authHeader !== "string" || !authHeader.startsWith("Bearer ")) {
|
|
1077
|
-
return false;
|
|
1078
|
-
}
|
|
1079
|
-
const token = authHeader.slice("Bearer ".length).trim();
|
|
1080
|
-
try {
|
|
1081
|
-
await jwtVerify(token, getGoogleJwks(), { audience: expectedAudience });
|
|
1082
|
-
return true;
|
|
1083
|
-
} catch {
|
|
1084
|
-
return false;
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
var APPLE_ACTIVE_TYPES = /* @__PURE__ */ new Set([
|
|
1088
|
-
"SUBSCRIBED",
|
|
1089
|
-
"DID_RENEW",
|
|
1090
|
-
"DID_RECOVER",
|
|
1091
|
-
"OFFER_REDEEMED"
|
|
1092
|
-
]);
|
|
1093
|
-
var APPLE_CANCELED_TYPES = /* @__PURE__ */ new Set([
|
|
1094
|
-
"REVOKE",
|
|
1095
|
-
"REFUND",
|
|
1096
|
-
"CONSUMPTION_REQUEST"
|
|
1097
|
-
]);
|
|
1098
|
-
var APPLE_EXPIRED_TYPES = /* @__PURE__ */ new Set([
|
|
1099
|
-
"EXPIRED"
|
|
1100
|
-
]);
|
|
1180
|
+
var APPLE_ACTIVE_TYPES = /* @__PURE__ */ new Set(["SUBSCRIBED", "DID_RENEW", "DID_RECOVER", "OFFER_REDEEMED"]);
|
|
1181
|
+
var APPLE_CANCELED_TYPES = /* @__PURE__ */ new Set(["REVOKE", "REFUND", "CONSUMPTION_REQUEST"]);
|
|
1182
|
+
var APPLE_EXPIRED_TYPES = /* @__PURE__ */ new Set(["EXPIRED"]);
|
|
1101
1183
|
function mapAppleNotificationStatus(notificationType, subtype) {
|
|
1102
1184
|
if (notificationType === "DID_FAIL_TO_RENEW") {
|
|
1103
1185
|
return subtype === "GRACE_PERIOD" ? SUBSCRIPTION_STATUS.GRACE_PERIOD : SUBSCRIPTION_STATUS.ON_HOLD;
|
|
@@ -1108,283 +1190,315 @@ function mapAppleNotificationStatus(notificationType, subtype) {
|
|
|
1108
1190
|
if (APPLE_ACTIVE_TYPES.has(notificationType)) return SUBSCRIPTION_STATUS.ACTIVE;
|
|
1109
1191
|
return null;
|
|
1110
1192
|
}
|
|
1111
|
-
function
|
|
1112
|
-
const
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1193
|
+
async function handleAppleWebhook(req, res, config, store, purchaseStore, webhookEventStore) {
|
|
1194
|
+
const body = req.body;
|
|
1195
|
+
if (!body.signedPayload) {
|
|
1196
|
+
sendError(res, 400, ONESUB_ERROR_CODE.MISSING_SIGNED_PAYLOAD, "Missing signedPayload");
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
let payload;
|
|
1200
|
+
try {
|
|
1201
|
+
payload = await decodeJws(
|
|
1202
|
+
body.signedPayload,
|
|
1203
|
+
config.apple?.skipJwsVerification
|
|
1204
|
+
);
|
|
1205
|
+
} catch (err) {
|
|
1206
|
+
log.error("[onesub/webhook/apple] Failed to decode signedPayload:", err);
|
|
1207
|
+
sendError(res, 400, ONESUB_ERROR_CODE.INVALID_SIGNED_PAYLOAD, "Invalid signedPayload");
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
if (webhookEventStore && typeof payload.notificationUUID === "string") {
|
|
1211
|
+
const fresh = await webhookEventStore.markIfNew("apple", payload.notificationUUID);
|
|
1212
|
+
if (!fresh) {
|
|
1213
|
+
log.info("[onesub/webhook/apple] dedupe: already processed", payload.notificationUUID);
|
|
1214
|
+
res.status(200).json({ received: true, deduped: true });
|
|
1128
1215
|
return;
|
|
1129
1216
|
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1217
|
+
}
|
|
1218
|
+
const decoded = await decodeAppleNotification(payload, config.apple?.skipJwsVerification);
|
|
1219
|
+
if (!decoded) {
|
|
1220
|
+
res.status(200).json({ received: true });
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
const {
|
|
1224
|
+
originalTransactionId,
|
|
1225
|
+
transactionId,
|
|
1226
|
+
type,
|
|
1227
|
+
productId,
|
|
1228
|
+
bundleId,
|
|
1229
|
+
environment,
|
|
1230
|
+
status,
|
|
1231
|
+
willRenew,
|
|
1232
|
+
expiresAt,
|
|
1233
|
+
appAccountToken,
|
|
1234
|
+
inAppOwnershipType
|
|
1235
|
+
} = decoded;
|
|
1236
|
+
const notificationType = payload.notificationType;
|
|
1237
|
+
const subtype = payload.subtype;
|
|
1238
|
+
const mapped = mapAppleNotificationStatus(notificationType, subtype);
|
|
1239
|
+
const finalStatus = mapped ?? status;
|
|
1240
|
+
if (notificationType === "CONSUMPTION_REQUEST" && config.apple?.consumptionInfoProvider && transactionId && productId) {
|
|
1241
|
+
const provider = config.apple.consumptionInfoProvider;
|
|
1242
|
+
const appleConfig = config.apple;
|
|
1243
|
+
void (async () => {
|
|
1244
|
+
try {
|
|
1245
|
+
const info = await provider({
|
|
1246
|
+
transactionId,
|
|
1247
|
+
originalTransactionId,
|
|
1248
|
+
productId,
|
|
1249
|
+
bundleId: bundleId ?? appleConfig.bundleId,
|
|
1250
|
+
environment
|
|
1251
|
+
});
|
|
1252
|
+
if (info) {
|
|
1253
|
+
await sendAppleConsumptionResponse(transactionId, info, appleConfig, {
|
|
1254
|
+
sandbox: environment === "Sandbox"
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
} catch (err) {
|
|
1258
|
+
log.warn("[onesub/webhook/apple] consumptionInfoProvider failed:", err);
|
|
1259
|
+
}
|
|
1260
|
+
})();
|
|
1261
|
+
}
|
|
1262
|
+
const isOneTimePurchase = type === "Consumable" || type === "Non-Consumable";
|
|
1263
|
+
const isRefundOrRevoke = APPLE_CANCELED_TYPES.has(notificationType);
|
|
1264
|
+
try {
|
|
1265
|
+
if (isOneTimePurchase && isRefundOrRevoke) {
|
|
1266
|
+
const lookupId = transactionId ?? originalTransactionId;
|
|
1267
|
+
const removed = await purchaseStore.deletePurchaseByTransactionId(lookupId);
|
|
1268
|
+
if (!removed) {
|
|
1269
|
+
log.warn("[onesub/webhook/apple] IAP refund for unknown transaction:", lookupId);
|
|
1136
1270
|
}
|
|
1137
|
-
}
|
|
1138
|
-
const decoded = await decodeAppleNotification(payload, config.apple?.skipJwsVerification);
|
|
1139
|
-
if (!decoded) {
|
|
1140
1271
|
res.status(200).json({ received: true });
|
|
1141
1272
|
return;
|
|
1142
1273
|
}
|
|
1143
|
-
const
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
status,
|
|
1151
|
-
willRenew,
|
|
1152
|
-
expiresAt
|
|
1153
|
-
} = decoded;
|
|
1154
|
-
const notificationType = payload.notificationType;
|
|
1155
|
-
const subtype = payload.subtype;
|
|
1156
|
-
const mapped = mapAppleNotificationStatus(notificationType, subtype);
|
|
1157
|
-
const finalStatus = mapped ?? status;
|
|
1158
|
-
if (notificationType === "CONSUMPTION_REQUEST" && config.apple?.consumptionInfoProvider && transactionId && productId) {
|
|
1159
|
-
const provider = config.apple.consumptionInfoProvider;
|
|
1160
|
-
const appleConfig = config.apple;
|
|
1161
|
-
void (async () => {
|
|
1162
|
-
try {
|
|
1163
|
-
const info = await provider({
|
|
1164
|
-
transactionId,
|
|
1165
|
-
originalTransactionId,
|
|
1166
|
-
productId,
|
|
1167
|
-
bundleId: bundleId ?? appleConfig.bundleId,
|
|
1168
|
-
environment
|
|
1169
|
-
});
|
|
1170
|
-
if (info) {
|
|
1171
|
-
await sendAppleConsumptionResponse(transactionId, info, appleConfig, {
|
|
1172
|
-
sandbox: environment === "Sandbox"
|
|
1173
|
-
});
|
|
1174
|
-
}
|
|
1175
|
-
} catch (err) {
|
|
1176
|
-
log.warn("[onesub/webhook/apple] consumptionInfoProvider failed:", err);
|
|
1177
|
-
}
|
|
1178
|
-
})();
|
|
1179
|
-
}
|
|
1180
|
-
const isOneTimePurchase = type === "Consumable" || type === "Non-Consumable";
|
|
1181
|
-
const isRefundOrRevoke = APPLE_CANCELED_TYPES.has(notificationType);
|
|
1182
|
-
try {
|
|
1183
|
-
if (isOneTimePurchase && isRefundOrRevoke) {
|
|
1184
|
-
const lookupId = transactionId ?? originalTransactionId;
|
|
1185
|
-
const removed = await purchaseStore.deletePurchaseByTransactionId(lookupId);
|
|
1186
|
-
if (!removed) {
|
|
1187
|
-
log.warn(
|
|
1188
|
-
"[onesub/webhook/apple] IAP refund for unknown transaction:",
|
|
1189
|
-
lookupId
|
|
1190
|
-
);
|
|
1191
|
-
}
|
|
1192
|
-
res.status(200).json({ received: true });
|
|
1193
|
-
return;
|
|
1274
|
+
const existing = await store.getByTransactionId(originalTransactionId);
|
|
1275
|
+
if (existing) {
|
|
1276
|
+
const isSubscriptionRefund = isRefundOrRevoke && !isOneTimePurchase && notificationType !== "CONSUMPTION_REQUEST";
|
|
1277
|
+
const keepEntitlement = isSubscriptionRefund && config.refundPolicy === "until_expiry";
|
|
1278
|
+
const correctedUserId = appAccountToken && existing.userId === originalTransactionId ? appAccountToken : existing.userId;
|
|
1279
|
+
if (correctedUserId !== existing.userId) {
|
|
1280
|
+
log.info("[onesub/webhook/apple] correcting userId from originalTransactionId to appAccountToken:", correctedUserId);
|
|
1194
1281
|
}
|
|
1195
|
-
const
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
if (fresh) {
|
|
1213
|
-
fresh.userId = originalTransactionId;
|
|
1214
|
-
await store.save(fresh);
|
|
1215
|
-
} else {
|
|
1216
|
-
log.warn(
|
|
1217
|
-
"[onesub/webhook/apple] Unknown transaction and Status API returned no record:",
|
|
1218
|
-
originalTransactionId
|
|
1219
|
-
);
|
|
1282
|
+
const updated = keepEntitlement ? { ...existing, userId: correctedUserId, willRenew: false } : {
|
|
1283
|
+
...existing,
|
|
1284
|
+
userId: correctedUserId,
|
|
1285
|
+
status: finalStatus,
|
|
1286
|
+
willRenew,
|
|
1287
|
+
expiresAt: expiresAt ?? existing.expiresAt
|
|
1288
|
+
};
|
|
1289
|
+
await store.save(updated);
|
|
1290
|
+
} else if (config.apple?.issuerId && config.apple?.keyId && config.apple?.privateKey) {
|
|
1291
|
+
const fresh = await fetchAppleSubscriptionStatus(originalTransactionId, config.apple, {
|
|
1292
|
+
sandbox: environment === "Sandbox"
|
|
1293
|
+
});
|
|
1294
|
+
if (fresh) {
|
|
1295
|
+
fresh.userId = appAccountToken ?? originalTransactionId;
|
|
1296
|
+
if (inAppOwnershipType === "FAMILY_SHARED") {
|
|
1297
|
+
const source = appAccountToken ? "appAccountToken" : "originalTransactionId (fallback)";
|
|
1298
|
+
log.info(`[onesub/webhook/apple] FAMILY_SHARED \u2014 userId: ${fresh.userId} (source: ${source})`);
|
|
1220
1299
|
}
|
|
1300
|
+
await store.save(fresh);
|
|
1221
1301
|
} else {
|
|
1222
1302
|
log.warn(
|
|
1223
|
-
"[onesub/webhook/apple]
|
|
1303
|
+
"[onesub/webhook/apple] Unknown transaction and Status API returned no record:",
|
|
1224
1304
|
originalTransactionId
|
|
1225
1305
|
);
|
|
1226
1306
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1307
|
+
} else {
|
|
1308
|
+
log.warn(
|
|
1309
|
+
"[onesub/webhook/apple] Received notification for unknown transaction (no API creds to recover):",
|
|
1310
|
+
originalTransactionId
|
|
1311
|
+
);
|
|
1231
1312
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1313
|
+
res.status(200).json({ received: true });
|
|
1314
|
+
} catch (err) {
|
|
1315
|
+
log.error("[onesub/webhook/apple] Store update error:", err);
|
|
1316
|
+
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to update subscription");
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
var GOOGLE_JWKS_URL = "https://www.googleapis.com/oauth2/v3/certs";
|
|
1320
|
+
var googleJwks = null;
|
|
1321
|
+
function getGoogleJwks() {
|
|
1322
|
+
if (!googleJwks) googleJwks = createRemoteJWKSet(new URL(GOOGLE_JWKS_URL));
|
|
1323
|
+
return googleJwks;
|
|
1324
|
+
}
|
|
1325
|
+
async function verifyGooglePushToken(req, expectedAudience) {
|
|
1326
|
+
const authHeader = req.headers["authorization"];
|
|
1327
|
+
if (typeof authHeader !== "string" || !authHeader.startsWith("Bearer ")) return false;
|
|
1328
|
+
try {
|
|
1329
|
+
await jwtVerify(authHeader.slice("Bearer ".length).trim(), getGoogleJwks(), {
|
|
1330
|
+
audience: expectedAudience
|
|
1331
|
+
});
|
|
1332
|
+
return true;
|
|
1333
|
+
} catch {
|
|
1334
|
+
return false;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
async function handleGoogleWebhook(req, res, config, store, purchaseStore, webhookEventStore) {
|
|
1338
|
+
if (config.google?.pushAudience) {
|
|
1339
|
+
const authenticated = await verifyGooglePushToken(req, config.google.pushAudience);
|
|
1340
|
+
if (!authenticated) {
|
|
1341
|
+
sendError(res, 401, ONESUB_ERROR_CODE.UNAUTHORIZED, "Unauthorized");
|
|
1342
|
+
return;
|
|
1240
1343
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1344
|
+
}
|
|
1345
|
+
const body = req.body;
|
|
1346
|
+
if (!body.message?.data) {
|
|
1347
|
+
sendError(res, 400, ONESUB_ERROR_CODE.MISSING_MESSAGE_DATA, "Missing message.data");
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
if (webhookEventStore && typeof body.message.messageId === "string") {
|
|
1351
|
+
const fresh = await webhookEventStore.markIfNew("google", body.message.messageId);
|
|
1352
|
+
if (!fresh) {
|
|
1353
|
+
log.info("[onesub/webhook/google] dedupe: already processed", body.message.messageId);
|
|
1354
|
+
res.status(200).json({ received: true, deduped: true });
|
|
1244
1355
|
return;
|
|
1245
1356
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1357
|
+
}
|
|
1358
|
+
const voided = decodeGoogleVoidedNotification(body);
|
|
1359
|
+
if (voided) {
|
|
1360
|
+
if (config.google?.packageName && voided.packageName !== config.google.packageName) {
|
|
1361
|
+
log.warn("[onesub/webhook/google] voided package name mismatch:", voided.packageName, "!==", config.google.packageName);
|
|
1362
|
+
sendError(res, 400, ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
|
|
1363
|
+
return;
|
|
1253
1364
|
}
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
"
|
|
1259
|
-
|
|
1260
|
-
"!==",
|
|
1261
|
-
config.google.packageName
|
|
1262
|
-
);
|
|
1263
|
-
sendError(res, 400, ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
|
|
1264
|
-
return;
|
|
1265
|
-
}
|
|
1266
|
-
try {
|
|
1267
|
-
if (voided.productType === 1) {
|
|
1268
|
-
const existing = await store.getByTransactionId(voided.purchaseToken);
|
|
1269
|
-
if (existing) {
|
|
1270
|
-
const updated = config.refundPolicy === "until_expiry" ? { ...existing, willRenew: false } : { ...existing, status: SUBSCRIPTION_STATUS.CANCELED };
|
|
1271
|
-
await store.save(updated);
|
|
1272
|
-
} else {
|
|
1273
|
-
log.warn(
|
|
1274
|
-
"[onesub/webhook/google] voided subscription for unknown purchaseToken:",
|
|
1275
|
-
voided.purchaseToken
|
|
1276
|
-
);
|
|
1277
|
-
}
|
|
1365
|
+
try {
|
|
1366
|
+
if (voided.productType === 1) {
|
|
1367
|
+
const existing = await store.getByTransactionId(voided.purchaseToken);
|
|
1368
|
+
if (existing) {
|
|
1369
|
+
const updated = config.refundPolicy === "until_expiry" ? { ...existing, willRenew: false } : { ...existing, status: SUBSCRIPTION_STATUS.CANCELED };
|
|
1370
|
+
await store.save(updated);
|
|
1278
1371
|
} else {
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
}
|
|
1372
|
+
log.warn("[onesub/webhook/google] voided subscription for unknown purchaseToken:", voided.purchaseToken);
|
|
1373
|
+
}
|
|
1374
|
+
} else {
|
|
1375
|
+
const removed = await purchaseStore.deletePurchaseByTransactionId(voided.orderId);
|
|
1376
|
+
if (!removed) {
|
|
1377
|
+
log.warn("[onesub/webhook/google] voided IAP for unknown orderId:", voided.orderId);
|
|
1286
1378
|
}
|
|
1287
|
-
res.status(200).json({ received: true });
|
|
1288
|
-
} catch (err) {
|
|
1289
|
-
log.error("[onesub/webhook/google] voided notification error:", err);
|
|
1290
|
-
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process voided notification");
|
|
1291
1379
|
}
|
|
1292
|
-
return;
|
|
1293
|
-
}
|
|
1294
|
-
const notification = decodeGoogleNotification(body);
|
|
1295
|
-
if (!notification) {
|
|
1296
1380
|
res.status(200).json({ received: true });
|
|
1297
|
-
|
|
1381
|
+
} catch (err) {
|
|
1382
|
+
log.error("[onesub/webhook/google] voided notification error:", err);
|
|
1383
|
+
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process voided notification");
|
|
1298
1384
|
}
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
config.google.packageName
|
|
1306
|
-
);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
const oneTimeProduct = decodeGoogleOneTimeProductNotification(body);
|
|
1388
|
+
if (oneTimeProduct) {
|
|
1389
|
+
if (config.google?.packageName && oneTimeProduct.packageName !== config.google.packageName) {
|
|
1390
|
+
log.warn("[onesub/webhook/google] oneTimeProduct package name mismatch:", oneTimeProduct.packageName, "!==", config.google.packageName);
|
|
1307
1391
|
sendError(res, 400, ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
|
|
1308
1392
|
return;
|
|
1309
1393
|
}
|
|
1310
1394
|
try {
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
} else if (isGooglePausedNotification(notificationType)) {
|
|
1320
|
-
finalStatus = SUBSCRIPTION_STATUS.PAUSED;
|
|
1321
|
-
} else if (isGooglePriceChangeConfirmedNotification(notificationType)) {
|
|
1322
|
-
finalStatus = SUBSCRIPTION_STATUS.ACTIVE;
|
|
1323
|
-
} else if (isGoogleCanceledNotification(notificationType)) {
|
|
1324
|
-
finalStatus = SUBSCRIPTION_STATUS.CANCELED;
|
|
1325
|
-
} else if (isGoogleExpiredNotification(notificationType)) {
|
|
1326
|
-
finalStatus = SUBSCRIPTION_STATUS.EXPIRED;
|
|
1395
|
+
const { notificationType: notificationType2, purchaseToken: purchaseToken2, sku } = oneTimeProduct;
|
|
1396
|
+
if (notificationType2 === 1) {
|
|
1397
|
+
log.info("[onesub/webhook/google] oneTimeProduct PURCHASED:", sku);
|
|
1398
|
+
if (config.google?.serviceAccountKey && config.google.packageName) {
|
|
1399
|
+
void acknowledgeGoogleProduct(purchaseToken2, sku, config.google).catch(
|
|
1400
|
+
(err) => log.warn(`[onesub/webhook/google] oneTimeProduct ack failed for SKU ${sku} \u2014 3-day auto-refund risk:`, err)
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1327
1403
|
} else {
|
|
1328
|
-
|
|
1329
|
-
}
|
|
1330
|
-
if (isGooglePriceChangeConfirmedNotification(notificationType) && config.google?.onPriceChangeConfirmed) {
|
|
1331
|
-
const hook = config.google.onPriceChangeConfirmed;
|
|
1332
|
-
void Promise.resolve().then(() => hook({ purchaseToken, subscriptionId, packageName })).catch((err) => log.warn("[onesub/webhook/google] onPriceChangeConfirmed hook failed:", err));
|
|
1404
|
+
log.info("[onesub/webhook/google] oneTimeProduct CANCELED (pre-completion):", sku);
|
|
1333
1405
|
}
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1406
|
+
res.status(200).json({ received: true });
|
|
1407
|
+
} catch (err) {
|
|
1408
|
+
log.error("[onesub/webhook/google] oneTimeProduct error:", err);
|
|
1409
|
+
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process oneTimeProduct notification");
|
|
1410
|
+
}
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
const notification = decodeGoogleNotification(body);
|
|
1414
|
+
if (!notification) {
|
|
1415
|
+
res.status(200).json({ received: true });
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
const { notificationType, purchaseToken, subscriptionId, packageName } = notification;
|
|
1419
|
+
if (config.google?.packageName && packageName !== config.google.packageName) {
|
|
1420
|
+
log.warn("[onesub/webhook/google] Package name mismatch:", packageName, "!==", config.google.packageName);
|
|
1421
|
+
sendError(res, 400, ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
try {
|
|
1425
|
+
const existing = await store.getByTransactionId(purchaseToken);
|
|
1426
|
+
let finalStatus;
|
|
1427
|
+
if (isGoogleActiveNotification(notificationType)) {
|
|
1428
|
+
finalStatus = SUBSCRIPTION_STATUS.ACTIVE;
|
|
1429
|
+
} else if (isGoogleGracePeriodNotification(notificationType)) {
|
|
1430
|
+
finalStatus = SUBSCRIPTION_STATUS.GRACE_PERIOD;
|
|
1431
|
+
} else if (isGoogleOnHoldNotification(notificationType)) {
|
|
1432
|
+
finalStatus = SUBSCRIPTION_STATUS.ON_HOLD;
|
|
1433
|
+
} else if (isGooglePausedNotification(notificationType)) {
|
|
1434
|
+
finalStatus = SUBSCRIPTION_STATUS.PAUSED;
|
|
1435
|
+
} else if (isGooglePriceChangeConfirmedNotification(notificationType)) {
|
|
1436
|
+
finalStatus = SUBSCRIPTION_STATUS.ACTIVE;
|
|
1437
|
+
} else if (isGoogleCanceledNotification(notificationType)) {
|
|
1438
|
+
finalStatus = SUBSCRIPTION_STATUS.CANCELED;
|
|
1439
|
+
} else if (isGoogleExpiredNotification(notificationType)) {
|
|
1440
|
+
finalStatus = SUBSCRIPTION_STATUS.EXPIRED;
|
|
1441
|
+
} else {
|
|
1442
|
+
finalStatus = SUBSCRIPTION_STATUS.ACTIVE;
|
|
1443
|
+
}
|
|
1444
|
+
if (isGooglePriceChangeConfirmedNotification(notificationType) && config.google?.onPriceChangeConfirmed) {
|
|
1445
|
+
const hook = config.google.onPriceChangeConfirmed;
|
|
1446
|
+
void Promise.resolve().then(() => hook({ purchaseToken, subscriptionId, packageName })).catch((err) => log.warn("[onesub/webhook/google] onPriceChangeConfirmed hook failed:", err));
|
|
1447
|
+
}
|
|
1448
|
+
if (existing) {
|
|
1449
|
+
let updated = { ...existing, status: finalStatus };
|
|
1450
|
+
if (config.google?.serviceAccountKey) {
|
|
1451
|
+
const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, config.google);
|
|
1452
|
+
if (fresh) {
|
|
1453
|
+
const preserveNotificationStatus = finalStatus === SUBSCRIPTION_STATUS.GRACE_PERIOD || finalStatus === SUBSCRIPTION_STATUS.ON_HOLD;
|
|
1454
|
+
updated = {
|
|
1455
|
+
...existing,
|
|
1456
|
+
status: preserveNotificationStatus ? finalStatus : fresh.status,
|
|
1457
|
+
expiresAt: fresh.expiresAt,
|
|
1458
|
+
willRenew: fresh.willRenew,
|
|
1459
|
+
autoResumeTime: fresh.autoResumeTime,
|
|
1460
|
+
linkedPurchaseToken: fresh.linkedPurchaseToken ?? existing.linkedPurchaseToken
|
|
1461
|
+
};
|
|
1354
1462
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
);
|
|
1367
|
-
} else {
|
|
1368
|
-
fresh.userId = purchaseToken;
|
|
1369
|
-
}
|
|
1370
|
-
} else {
|
|
1371
|
-
fresh.userId = purchaseToken;
|
|
1463
|
+
}
|
|
1464
|
+
await store.save(updated);
|
|
1465
|
+
} else {
|
|
1466
|
+
if (config.google?.serviceAccountKey) {
|
|
1467
|
+
const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, config.google);
|
|
1468
|
+
if (fresh) {
|
|
1469
|
+
if (fresh.linkedPurchaseToken) {
|
|
1470
|
+
const previous = await store.getByTransactionId(fresh.linkedPurchaseToken);
|
|
1471
|
+
fresh.userId = previous ? previous.userId : purchaseToken;
|
|
1472
|
+
if (previous) {
|
|
1473
|
+
log.info(`[onesub/webhook/google] inherited userId ${previous.userId} from linkedPurchaseToken ${fresh.linkedPurchaseToken} \u2192 new token ${purchaseToken}`);
|
|
1372
1474
|
}
|
|
1373
|
-
|
|
1475
|
+
} else {
|
|
1476
|
+
fresh.userId = purchaseToken;
|
|
1374
1477
|
}
|
|
1375
|
-
|
|
1376
|
-
log.warn(
|
|
1377
|
-
"[onesub/webhook/google] Unknown purchase token and no serviceAccountKey to re-fetch:",
|
|
1378
|
-
purchaseToken
|
|
1379
|
-
);
|
|
1478
|
+
await store.save(fresh);
|
|
1380
1479
|
}
|
|
1480
|
+
} else {
|
|
1481
|
+
log.warn("[onesub/webhook/google] Unknown purchase token and no serviceAccountKey to re-fetch:", purchaseToken);
|
|
1381
1482
|
}
|
|
1382
|
-
res.status(200).json({ received: true });
|
|
1383
|
-
} catch (err) {
|
|
1384
|
-
log.error("[onesub/webhook/google] Error handling notification:", err);
|
|
1385
|
-
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process notification");
|
|
1386
1483
|
}
|
|
1387
|
-
|
|
1484
|
+
res.status(200).json({ received: true });
|
|
1485
|
+
} catch (err) {
|
|
1486
|
+
log.error("[onesub/webhook/google] Error handling notification:", err);
|
|
1487
|
+
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process notification");
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// src/routes/webhook.ts
|
|
1492
|
+
function createWebhookRouter(config, store, purchaseStore, webhookEventStore) {
|
|
1493
|
+
const router = Router();
|
|
1494
|
+
router.post(
|
|
1495
|
+
ROUTES.WEBHOOK_APPLE,
|
|
1496
|
+
(req, res) => handleAppleWebhook(req, res, config, store, purchaseStore, webhookEventStore)
|
|
1497
|
+
);
|
|
1498
|
+
router.post(
|
|
1499
|
+
ROUTES.WEBHOOK_GOOGLE,
|
|
1500
|
+
(req, res) => handleGoogleWebhook(req, res, config, store, purchaseStore, webhookEventStore)
|
|
1501
|
+
);
|
|
1388
1502
|
return router;
|
|
1389
1503
|
}
|
|
1390
1504
|
var NO_PURCHASE = { valid: false, purchase: null };
|
|
@@ -1807,6 +1921,38 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
1807
1921
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, err.message ?? "customer error");
|
|
1808
1922
|
}
|
|
1809
1923
|
});
|
|
1924
|
+
const syncAppleParamsSchema = z.object({
|
|
1925
|
+
originalTransactionId: z.string().min(1).max(256)
|
|
1926
|
+
});
|
|
1927
|
+
router.post(ROUTES.ADMIN_SYNC_APPLE, async (req, res) => {
|
|
1928
|
+
let params;
|
|
1929
|
+
try {
|
|
1930
|
+
params = syncAppleParamsSchema.parse(req.params);
|
|
1931
|
+
} catch {
|
|
1932
|
+
sendError(res, 400, ONESUB_ERROR_CODE.INVALID_INPUT, "originalTransactionId required");
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
if (!config.apple?.issuerId || !config.apple?.keyId || !config.apple?.privateKey) {
|
|
1936
|
+
sendError(res, 400, ONESUB_ERROR_CODE.APPLE_CONFIG_MISSING, "Apple API credentials not configured");
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
try {
|
|
1940
|
+
const { originalTransactionId } = params;
|
|
1941
|
+
const existingForEnv = await store.getByTransactionId(originalTransactionId);
|
|
1942
|
+
const isSandbox = req.query["sandbox"] === "true" || existingForEnv?.environment === "Sandbox";
|
|
1943
|
+
const fresh = await fetchAppleSubscriptionStatus(originalTransactionId, config.apple, { sandbox: isSandbox });
|
|
1944
|
+
if (!fresh) {
|
|
1945
|
+
sendError(res, 404, ONESUB_ERROR_CODE.TRANSACTION_NOT_FOUND, "Apple Status API returned no record");
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
const existing = existingForEnv;
|
|
1949
|
+
fresh.userId = existing?.userId ?? originalTransactionId;
|
|
1950
|
+
await store.save(fresh);
|
|
1951
|
+
res.status(200).json({ ok: true, subscription: fresh });
|
|
1952
|
+
} catch (err) {
|
|
1953
|
+
sendError(res, 500, ONESUB_ERROR_CODE.INTERNAL_ERROR, err.message ?? "sync error");
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1810
1956
|
if (webhookQueue?.listDeadLetters) {
|
|
1811
1957
|
router.get("/onesub/admin/webhook-deadletters", async (_req, res) => {
|
|
1812
1958
|
try {
|
|
@@ -2052,6 +2198,55 @@ function createMetricsRouter(config, store, purchaseStore) {
|
|
|
2052
2198
|
});
|
|
2053
2199
|
return router;
|
|
2054
2200
|
}
|
|
2201
|
+
var OFFER_SECRET_HEADER = "x-onesub-offer-secret";
|
|
2202
|
+
var offerBodySchema = z.object({
|
|
2203
|
+
productId: z.string().min(1).max(256),
|
|
2204
|
+
offerId: z.string().min(1).max(256),
|
|
2205
|
+
applicationUsername: z.string().min(1).max(256)
|
|
2206
|
+
});
|
|
2207
|
+
function createAppleOfferRouter(config) {
|
|
2208
|
+
const apple = config.apple;
|
|
2209
|
+
if (!apple?.offerKeyId || !apple?.offerPrivateKey) return null;
|
|
2210
|
+
const router = Router();
|
|
2211
|
+
router.post("/onesub/apple/offer-signature", async (req, res) => {
|
|
2212
|
+
if (config.adminSecret) {
|
|
2213
|
+
const provided = req.headers[OFFER_SECRET_HEADER];
|
|
2214
|
+
if (typeof provided !== "string" || provided !== config.adminSecret) {
|
|
2215
|
+
sendError(res, 401, ONESUB_ERROR_CODE.UNAUTHORIZED, "Unauthorized");
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
let body;
|
|
2220
|
+
try {
|
|
2221
|
+
body = offerBodySchema.parse(req.body);
|
|
2222
|
+
} catch (err) {
|
|
2223
|
+
if (err instanceof z.ZodError) {
|
|
2224
|
+
sendZodError(res, err);
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
throw err;
|
|
2228
|
+
}
|
|
2229
|
+
if (!apple.bundleId) {
|
|
2230
|
+
sendError(res, 400, ONESUB_ERROR_CODE.APPLE_CONFIG_MISSING, "config.apple.bundleId is required for offer signing");
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
try {
|
|
2234
|
+
const result = await signApplePromotionalOffer(
|
|
2235
|
+
{
|
|
2236
|
+
bundleId: apple.bundleId,
|
|
2237
|
+
productId: body.productId,
|
|
2238
|
+
offerId: body.offerId,
|
|
2239
|
+
applicationUsername: body.applicationUsername
|
|
2240
|
+
},
|
|
2241
|
+
apple
|
|
2242
|
+
);
|
|
2243
|
+
res.status(200).json(result);
|
|
2244
|
+
} catch (err) {
|
|
2245
|
+
sendError(res, 500, ONESUB_ERROR_CODE.INTERNAL_ERROR, err.message ?? "Offer signing failed");
|
|
2246
|
+
}
|
|
2247
|
+
});
|
|
2248
|
+
return router;
|
|
2249
|
+
}
|
|
2055
2250
|
|
|
2056
2251
|
// src/stores/schema.ts
|
|
2057
2252
|
var SUBSCRIPTIONS_SCHEMA_SQL = `
|
|
@@ -3038,6 +3233,8 @@ function createOneSubMiddleware(config) {
|
|
|
3038
3233
|
if (entitlementRouter) router.use(entitlementRouter);
|
|
3039
3234
|
const metricsRouter = createMetricsRouter(config, store, purchaseStore);
|
|
3040
3235
|
if (metricsRouter) router.use(metricsRouter);
|
|
3236
|
+
const appleOfferRouter = createAppleOfferRouter(config);
|
|
3237
|
+
if (appleOfferRouter) router.use(appleOfferRouter);
|
|
3041
3238
|
return router;
|
|
3042
3239
|
}
|
|
3043
3240
|
function createOneSubServer(config) {
|
|
@@ -3048,7 +3245,6 @@ function createOneSubServer(config) {
|
|
|
3048
3245
|
app.use(createOneSubMiddleware(config));
|
|
3049
3246
|
return app;
|
|
3050
3247
|
}
|
|
3051
|
-
var index_default = createOneSubMiddleware;
|
|
3052
3248
|
var isMain = typeof process !== "undefined" && process.argv[1] != null && new URL(import.meta.url).pathname.endsWith(process.argv[1].replace(/\\/g, "/"));
|
|
3053
3249
|
if (isMain) {
|
|
3054
3250
|
const config = {
|
|
@@ -3074,6 +3270,6 @@ if (isMain) {
|
|
|
3074
3270
|
});
|
|
3075
3271
|
}
|
|
3076
3272
|
|
|
3077
|
-
export { BullMQWebhookQueue, CacheWebhookEventStore, InMemoryCacheAdapter, InMemoryPurchaseStore, InMemorySubscriptionStore, InMemoryWebhookEventStore, InProcessWebhookQueue, ONESUB_OPENAPI, PostgresPurchaseStore, PostgresSubscriptionStore, RedisCacheAdapter, RedisPurchaseStore, RedisSubscriptionStore, RedisWebhookEventStore, createOneSubMiddleware, createOneSubServer,
|
|
3273
|
+
export { BullMQWebhookQueue, CacheWebhookEventStore, InMemoryCacheAdapter, InMemoryPurchaseStore, InMemorySubscriptionStore, InMemoryWebhookEventStore, InProcessWebhookQueue, ONESUB_OPENAPI, PostgresPurchaseStore, PostgresSubscriptionStore, RedisCacheAdapter, RedisPurchaseStore, RedisSubscriptionStore, RedisWebhookEventStore, createOneSubMiddleware, createOneSubServer, evaluateEntitlement, fetchAppleSubscriptionStatus, fetchAppleTransactionHistory, getDefaultCache, log, openapiHandler, setDefaultCache, setLogger, signApplePromotionalOffer, validateAppleReceipt, validateGoogleReceipt, withSpan };
|
|
3078
3274
|
//# sourceMappingURL=index.js.map
|
|
3079
3275
|
//# sourceMappingURL=index.js.map
|