@onesub/server 0.12.0 → 0.12.1
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 +258 -288
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +259 -286
- package/dist/index.js.map +1 -1
- package/dist/providers/google.d.ts.map +1 -1
- 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/__tests__/admin-customer-detail.test.js +0 -146
- package/dist/__tests__/admin-customer-detail.test.js.map +0 -1
- package/dist/__tests__/admin-list-subscriptions.test.js +0 -192
- package/dist/__tests__/admin-list-subscriptions.test.js.map +0 -1
- package/dist/__tests__/admin-subscription-detail.test.js +0 -112
- package/dist/__tests__/admin-subscription-detail.test.js.map +0 -1
- package/dist/__tests__/apple-root-ca.test.js +0 -41
- package/dist/__tests__/apple-root-ca.test.js.map +0 -1
- package/dist/__tests__/apple-status-api.test.js +0 -345
- package/dist/__tests__/apple-status-api.test.js.map +0 -1
- package/dist/__tests__/cache.test.js +0 -34
- package/dist/__tests__/cache.test.js.map +0 -1
- package/dist/__tests__/entitlements.test.js +0 -268
- package/dist/__tests__/entitlements.test.js.map +0 -1
- package/dist/__tests__/error-codes.test.js +0 -168
- package/dist/__tests__/error-codes.test.js.map +0 -1
- package/dist/__tests__/fetch-hardening.test.js +0 -192
- package/dist/__tests__/fetch-hardening.test.js.map +0 -1
- package/dist/__tests__/google-paused.test.js +0 -126
- package/dist/__tests__/google-paused.test.js.map +0 -1
- package/dist/__tests__/google-price-change.test.js +0 -186
- package/dist/__tests__/google-price-change.test.js.map +0 -1
- package/dist/__tests__/google-subscriptions-v2.test.js +0 -206
- package/dist/__tests__/google-subscriptions-v2.test.js.map +0 -1
- package/dist/__tests__/lifecycle-scenarios.test.js +0 -579
- package/dist/__tests__/lifecycle-scenarios.test.js.map +0 -1
- package/dist/__tests__/lifecycle-states.test.js +0 -423
- package/dist/__tests__/lifecycle-states.test.js.map +0 -1
- package/dist/__tests__/linked-purchase-token.test.js +0 -227
- package/dist/__tests__/linked-purchase-token.test.js.map +0 -1
- package/dist/__tests__/metrics.test.js +0 -340
- package/dist/__tests__/metrics.test.js.map +0 -1
- package/dist/__tests__/mock-provider.test.js +0 -188
- package/dist/__tests__/mock-provider.test.js.map +0 -1
- package/dist/__tests__/openapi.test.js +0 -32
- package/dist/__tests__/openapi.test.js.map +0 -1
- package/dist/__tests__/paused-auto-resume.test.js +0 -162
- package/dist/__tests__/paused-auto-resume.test.js.map +0 -1
- package/dist/__tests__/providers.test.js +0 -321
- package/dist/__tests__/providers.test.js.map +0 -1
- package/dist/__tests__/redis-store.test.js +0 -117
- package/dist/__tests__/redis-store.test.js.map +0 -1
- package/dist/__tests__/refund-policy.test.js +0 -317
- package/dist/__tests__/refund-policy.test.js.map +0 -1
- package/dist/__tests__/schema.test.js +0 -46
- package/dist/__tests__/schema.test.js.map +0 -1
- package/dist/__tests__/store.test.js +0 -172
- package/dist/__tests__/store.test.js.map +0 -1
- package/dist/__tests__/test-utils.js +0 -30
- package/dist/__tests__/test-utils.js.map +0 -1
- package/dist/__tests__/webhook-events.test.js +0 -36
- package/dist/__tests__/webhook-events.test.js.map +0 -1
- package/dist/__tests__/webhook-queue.test.js +0 -26
- package/dist/__tests__/webhook-queue.test.js.map +0 -1
- package/dist/__tests__/webhook-refund.test.js +0 -313
- package/dist/__tests__/webhook-refund.test.js.map +0 -1
- package/dist/cache.js +0 -43
- package/dist/cache.js.map +0 -1
- package/dist/errors.js +0 -19
- package/dist/errors.js.map +0 -1
- package/dist/http.js +0 -36
- package/dist/http.js.map +0 -1
- package/dist/logger.js +0 -20
- package/dist/logger.js.map +0 -1
- package/dist/openapi.js +0 -196
- package/dist/openapi.js.map +0 -1
- package/dist/providers/apple-root-ca.js +0 -38
- package/dist/providers/apple-root-ca.js.map +0 -1
- package/dist/providers/apple.js +0 -510
- package/dist/providers/apple.js.map +0 -1
- package/dist/providers/google.js +0 -522
- package/dist/providers/google.js.map +0 -1
- package/dist/providers/mock.js +0 -93
- package/dist/providers/mock.js.map +0 -1
- package/dist/routes/admin.js +0 -287
- package/dist/routes/admin.js.map +0 -1
- package/dist/routes/entitlements.js +0 -130
- package/dist/routes/entitlements.js.map +0 -1
- package/dist/routes/metrics.js +0 -281
- package/dist/routes/metrics.js.map +0 -1
- package/dist/routes/purchase.js +0 -187
- package/dist/routes/purchase.js.map +0 -1
- package/dist/routes/status.js +0 -55
- package/dist/routes/status.js.map +0 -1
- package/dist/routes/validate.js +0 -70
- package/dist/routes/validate.js.map +0 -1
- package/dist/routes/webhook.js +0 -461
- package/dist/routes/webhook.js.map +0 -1
- package/dist/store.js +0 -145
- package/dist/store.js.map +0 -1
- package/dist/stores/postgres.js +0 -329
- package/dist/stores/postgres.js.map +0 -1
- package/dist/stores/redis.js +0 -255
- package/dist/stores/redis.js.map +0 -1
- package/dist/stores/schema.js +0 -67
- package/dist/stores/schema.js.map +0 -1
- package/dist/tracing.js +0 -74
- package/dist/tracing.js.map +0 -1
- package/dist/webhook-events.js +0 -57
- package/dist/webhook-events.js.map +0 -1
- package/dist/webhook-queue.js +0 -108
- package/dist/webhook-queue.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
-
|
|
5
3
|
var express = require('express');
|
|
6
4
|
var shared = require('@onesub/shared');
|
|
7
5
|
var zod = require('zod');
|
|
@@ -732,6 +730,7 @@ async function fetchProductPurchase(packageName, productId, purchaseToken, acces
|
|
|
732
730
|
async function acknowledgeGoogleSubscription(purchaseToken, productId, config) {
|
|
733
731
|
if (config.mockMode) return;
|
|
734
732
|
if (!config.serviceAccountKey) return;
|
|
733
|
+
if (!config.packageName) return;
|
|
735
734
|
let accessToken;
|
|
736
735
|
try {
|
|
737
736
|
accessToken = await getCachedAccessToken(config.serviceAccountKey);
|
|
@@ -757,6 +756,7 @@ async function acknowledgeGoogleSubscription(purchaseToken, productId, config) {
|
|
|
757
756
|
async function acknowledgeGoogleProduct(purchaseToken, productId, config) {
|
|
758
757
|
if (config.mockMode) return;
|
|
759
758
|
if (!config.serviceAccountKey) return;
|
|
759
|
+
if (!config.packageName) return;
|
|
760
760
|
let accessToken;
|
|
761
761
|
try {
|
|
762
762
|
accessToken = await getCachedAccessToken(config.serviceAccountKey);
|
|
@@ -781,6 +781,7 @@ async function acknowledgeGoogleProduct(purchaseToken, productId, config) {
|
|
|
781
781
|
}
|
|
782
782
|
async function consumeGoogleProductReceipt(purchaseToken, productId, config) {
|
|
783
783
|
if (!config.serviceAccountKey) return;
|
|
784
|
+
if (!config.packageName) return;
|
|
784
785
|
let accessToken;
|
|
785
786
|
try {
|
|
786
787
|
accessToken = await getCachedAccessToken(config.serviceAccountKey);
|
|
@@ -828,6 +829,10 @@ async function validateGoogleReceipt(receipt, productId, config) {
|
|
|
828
829
|
log.warn("[onesub/google] No serviceAccountKey provided \u2014 cannot call Play API");
|
|
829
830
|
return null;
|
|
830
831
|
}
|
|
832
|
+
if (!config.packageName) {
|
|
833
|
+
log.warn("[onesub/google] No packageName provided \u2014 cannot call Play API");
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
831
836
|
let purchase;
|
|
832
837
|
try {
|
|
833
838
|
const token = await getCachedAccessToken(config.serviceAccountKey);
|
|
@@ -880,6 +885,10 @@ async function validateGoogleProductReceipt(purchaseToken, productId, config, ty
|
|
|
880
885
|
log.warn("[onesub/google] No serviceAccountKey \u2014 cannot validate product receipt");
|
|
881
886
|
return null;
|
|
882
887
|
}
|
|
888
|
+
if (!config.packageName) {
|
|
889
|
+
log.warn("[onesub/google] No packageName \u2014 cannot validate product receipt");
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
883
892
|
let purchase;
|
|
884
893
|
try {
|
|
885
894
|
const token = await getCachedAccessToken(config.serviceAccountKey);
|
|
@@ -1072,41 +1081,9 @@ function createStatusRouter(store) {
|
|
|
1072
1081
|
});
|
|
1073
1082
|
return router;
|
|
1074
1083
|
}
|
|
1075
|
-
var
|
|
1076
|
-
var
|
|
1077
|
-
|
|
1078
|
-
if (!googleJwks) {
|
|
1079
|
-
googleJwks = jose.createRemoteJWKSet(new URL(GOOGLE_JWKS_URL));
|
|
1080
|
-
}
|
|
1081
|
-
return googleJwks;
|
|
1082
|
-
}
|
|
1083
|
-
async function verifyGooglePushToken(req, expectedAudience) {
|
|
1084
|
-
const authHeader = req.headers["authorization"];
|
|
1085
|
-
if (typeof authHeader !== "string" || !authHeader.startsWith("Bearer ")) {
|
|
1086
|
-
return false;
|
|
1087
|
-
}
|
|
1088
|
-
const token = authHeader.slice("Bearer ".length).trim();
|
|
1089
|
-
try {
|
|
1090
|
-
await jose.jwtVerify(token, getGoogleJwks(), { audience: expectedAudience });
|
|
1091
|
-
return true;
|
|
1092
|
-
} catch {
|
|
1093
|
-
return false;
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
var APPLE_ACTIVE_TYPES = /* @__PURE__ */ new Set([
|
|
1097
|
-
"SUBSCRIBED",
|
|
1098
|
-
"DID_RENEW",
|
|
1099
|
-
"DID_RECOVER",
|
|
1100
|
-
"OFFER_REDEEMED"
|
|
1101
|
-
]);
|
|
1102
|
-
var APPLE_CANCELED_TYPES = /* @__PURE__ */ new Set([
|
|
1103
|
-
"REVOKE",
|
|
1104
|
-
"REFUND",
|
|
1105
|
-
"CONSUMPTION_REQUEST"
|
|
1106
|
-
]);
|
|
1107
|
-
var APPLE_EXPIRED_TYPES = /* @__PURE__ */ new Set([
|
|
1108
|
-
"EXPIRED"
|
|
1109
|
-
]);
|
|
1084
|
+
var APPLE_ACTIVE_TYPES = /* @__PURE__ */ new Set(["SUBSCRIBED", "DID_RENEW", "DID_RECOVER", "OFFER_REDEEMED"]);
|
|
1085
|
+
var APPLE_CANCELED_TYPES = /* @__PURE__ */ new Set(["REVOKE", "REFUND", "CONSUMPTION_REQUEST"]);
|
|
1086
|
+
var APPLE_EXPIRED_TYPES = /* @__PURE__ */ new Set(["EXPIRED"]);
|
|
1110
1087
|
function mapAppleNotificationStatus(notificationType, subtype) {
|
|
1111
1088
|
if (notificationType === "DID_FAIL_TO_RENEW") {
|
|
1112
1089
|
return subtype === "GRACE_PERIOD" ? shared.SUBSCRIPTION_STATUS.GRACE_PERIOD : shared.SUBSCRIPTION_STATUS.ON_HOLD;
|
|
@@ -1117,283 +1094,278 @@ function mapAppleNotificationStatus(notificationType, subtype) {
|
|
|
1117
1094
|
if (APPLE_ACTIVE_TYPES.has(notificationType)) return shared.SUBSCRIPTION_STATUS.ACTIVE;
|
|
1118
1095
|
return null;
|
|
1119
1096
|
}
|
|
1120
|
-
function
|
|
1121
|
-
const
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
if (
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
log.info("[onesub/webhook/apple] dedupe: already processed", payload.notificationUUID);
|
|
1143
|
-
res.status(200).json({ received: true, deduped: true });
|
|
1144
|
-
return;
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
const decoded = await decodeAppleNotification(payload, config.apple?.skipJwsVerification);
|
|
1148
|
-
if (!decoded) {
|
|
1149
|
-
res.status(200).json({ received: true });
|
|
1097
|
+
async function handleAppleWebhook(req, res, config, store, purchaseStore, webhookEventStore) {
|
|
1098
|
+
const body = req.body;
|
|
1099
|
+
if (!body.signedPayload) {
|
|
1100
|
+
sendError(res, 400, shared.ONESUB_ERROR_CODE.MISSING_SIGNED_PAYLOAD, "Missing signedPayload");
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
let payload;
|
|
1104
|
+
try {
|
|
1105
|
+
payload = await decodeJws(
|
|
1106
|
+
body.signedPayload,
|
|
1107
|
+
config.apple?.skipJwsVerification
|
|
1108
|
+
);
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
log.error("[onesub/webhook/apple] Failed to decode signedPayload:", err);
|
|
1111
|
+
sendError(res, 400, shared.ONESUB_ERROR_CODE.INVALID_SIGNED_PAYLOAD, "Invalid signedPayload");
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
if (webhookEventStore && typeof payload.notificationUUID === "string") {
|
|
1115
|
+
const fresh = await webhookEventStore.markIfNew("apple", payload.notificationUUID);
|
|
1116
|
+
if (!fresh) {
|
|
1117
|
+
log.info("[onesub/webhook/apple] dedupe: already processed", payload.notificationUUID);
|
|
1118
|
+
res.status(200).json({ received: true, deduped: true });
|
|
1150
1119
|
return;
|
|
1151
1120
|
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1121
|
+
}
|
|
1122
|
+
const decoded = await decodeAppleNotification(payload, config.apple?.skipJwsVerification);
|
|
1123
|
+
if (!decoded) {
|
|
1124
|
+
res.status(200).json({ received: true });
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
const {
|
|
1128
|
+
originalTransactionId,
|
|
1129
|
+
transactionId,
|
|
1130
|
+
type,
|
|
1131
|
+
productId,
|
|
1132
|
+
bundleId,
|
|
1133
|
+
environment,
|
|
1134
|
+
status,
|
|
1135
|
+
willRenew,
|
|
1136
|
+
expiresAt
|
|
1137
|
+
} = decoded;
|
|
1138
|
+
const notificationType = payload.notificationType;
|
|
1139
|
+
const subtype = payload.subtype;
|
|
1140
|
+
const mapped = mapAppleNotificationStatus(notificationType, subtype);
|
|
1141
|
+
const finalStatus = mapped ?? status;
|
|
1142
|
+
if (notificationType === "CONSUMPTION_REQUEST" && config.apple?.consumptionInfoProvider && transactionId && productId) {
|
|
1143
|
+
const provider = config.apple.consumptionInfoProvider;
|
|
1144
|
+
const appleConfig = config.apple;
|
|
1145
|
+
void (async () => {
|
|
1146
|
+
try {
|
|
1147
|
+
const info = await provider({
|
|
1148
|
+
transactionId,
|
|
1149
|
+
originalTransactionId,
|
|
1150
|
+
productId,
|
|
1151
|
+
bundleId: bundleId ?? appleConfig.bundleId,
|
|
1152
|
+
environment
|
|
1153
|
+
});
|
|
1154
|
+
if (info) {
|
|
1155
|
+
await sendAppleConsumptionResponse(transactionId, info, appleConfig, {
|
|
1156
|
+
sandbox: environment === "Sandbox"
|
|
1178
1157
|
});
|
|
1179
|
-
if (info) {
|
|
1180
|
-
await sendAppleConsumptionResponse(transactionId, info, appleConfig, {
|
|
1181
|
-
sandbox: environment === "Sandbox"
|
|
1182
|
-
});
|
|
1183
|
-
}
|
|
1184
|
-
} catch (err) {
|
|
1185
|
-
log.warn("[onesub/webhook/apple] consumptionInfoProvider failed:", err);
|
|
1186
1158
|
}
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
const isOneTimePurchase = type === "Consumable" || type === "Non-Consumable";
|
|
1190
|
-
const isRefundOrRevoke = APPLE_CANCELED_TYPES.has(notificationType);
|
|
1191
|
-
try {
|
|
1192
|
-
if (isOneTimePurchase && isRefundOrRevoke) {
|
|
1193
|
-
const lookupId = transactionId ?? originalTransactionId;
|
|
1194
|
-
const removed = await purchaseStore.deletePurchaseByTransactionId(lookupId);
|
|
1195
|
-
if (!removed) {
|
|
1196
|
-
log.warn(
|
|
1197
|
-
"[onesub/webhook/apple] IAP refund for unknown transaction:",
|
|
1198
|
-
lookupId
|
|
1199
|
-
);
|
|
1200
|
-
}
|
|
1201
|
-
res.status(200).json({ received: true });
|
|
1202
|
-
return;
|
|
1159
|
+
} catch (err) {
|
|
1160
|
+
log.warn("[onesub/webhook/apple] consumptionInfoProvider failed:", err);
|
|
1203
1161
|
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
expiresAt: expiresAt ?? existing.expiresAt
|
|
1215
|
-
};
|
|
1216
|
-
await store.save(updated);
|
|
1217
|
-
} else if (config.apple?.issuerId && config.apple?.keyId && config.apple?.privateKey) {
|
|
1218
|
-
const fresh = await fetchAppleSubscriptionStatus(originalTransactionId, config.apple, {
|
|
1219
|
-
sandbox: environment === "Sandbox"
|
|
1220
|
-
});
|
|
1221
|
-
if (fresh) {
|
|
1222
|
-
fresh.userId = originalTransactionId;
|
|
1223
|
-
await store.save(fresh);
|
|
1224
|
-
} else {
|
|
1225
|
-
log.warn(
|
|
1226
|
-
"[onesub/webhook/apple] Unknown transaction and Status API returned no record:",
|
|
1227
|
-
originalTransactionId
|
|
1228
|
-
);
|
|
1229
|
-
}
|
|
1230
|
-
} else {
|
|
1231
|
-
log.warn(
|
|
1232
|
-
"[onesub/webhook/apple] Received notification for unknown transaction (no API creds to recover):",
|
|
1233
|
-
originalTransactionId
|
|
1234
|
-
);
|
|
1162
|
+
})();
|
|
1163
|
+
}
|
|
1164
|
+
const isOneTimePurchase = type === "Consumable" || type === "Non-Consumable";
|
|
1165
|
+
const isRefundOrRevoke = APPLE_CANCELED_TYPES.has(notificationType);
|
|
1166
|
+
try {
|
|
1167
|
+
if (isOneTimePurchase && isRefundOrRevoke) {
|
|
1168
|
+
const lookupId = transactionId ?? originalTransactionId;
|
|
1169
|
+
const removed = await purchaseStore.deletePurchaseByTransactionId(lookupId);
|
|
1170
|
+
if (!removed) {
|
|
1171
|
+
log.warn("[onesub/webhook/apple] IAP refund for unknown transaction:", lookupId);
|
|
1235
1172
|
}
|
|
1236
1173
|
res.status(200).json({ received: true });
|
|
1237
|
-
} catch (err) {
|
|
1238
|
-
log.error("[onesub/webhook/apple] Store update error:", err);
|
|
1239
|
-
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to update subscription");
|
|
1240
|
-
}
|
|
1241
|
-
});
|
|
1242
|
-
router.post(shared.ROUTES.WEBHOOK_GOOGLE, async (req, res) => {
|
|
1243
|
-
if (config.google?.pushAudience) {
|
|
1244
|
-
const authenticated = await verifyGooglePushToken(req, config.google.pushAudience);
|
|
1245
|
-
if (!authenticated) {
|
|
1246
|
-
sendError(res, 401, shared.ONESUB_ERROR_CODE.UNAUTHORIZED, "Unauthorized");
|
|
1247
|
-
return;
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
const body = req.body;
|
|
1251
|
-
if (!body.message?.data) {
|
|
1252
|
-
sendError(res, 400, shared.ONESUB_ERROR_CODE.MISSING_MESSAGE_DATA, "Missing message.data");
|
|
1253
1174
|
return;
|
|
1254
1175
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1176
|
+
const existing = await store.getByTransactionId(originalTransactionId);
|
|
1177
|
+
if (existing) {
|
|
1178
|
+
const isSubscriptionRefund = isRefundOrRevoke && !isOneTimePurchase && notificationType !== "CONSUMPTION_REQUEST";
|
|
1179
|
+
const keepEntitlement = isSubscriptionRefund && config.refundPolicy === "until_expiry";
|
|
1180
|
+
const updated = keepEntitlement ? { ...existing, willRenew: false } : {
|
|
1181
|
+
...existing,
|
|
1182
|
+
status: finalStatus,
|
|
1183
|
+
willRenew,
|
|
1184
|
+
expiresAt: expiresAt ?? existing.expiresAt
|
|
1185
|
+
};
|
|
1186
|
+
await store.save(updated);
|
|
1187
|
+
} else if (config.apple?.issuerId && config.apple?.keyId && config.apple?.privateKey) {
|
|
1188
|
+
const fresh = await fetchAppleSubscriptionStatus(originalTransactionId, config.apple, {
|
|
1189
|
+
sandbox: environment === "Sandbox"
|
|
1190
|
+
});
|
|
1191
|
+
if (fresh) {
|
|
1192
|
+
fresh.userId = originalTransactionId;
|
|
1193
|
+
await store.save(fresh);
|
|
1194
|
+
} else {
|
|
1266
1195
|
log.warn(
|
|
1267
|
-
"[onesub/webhook/
|
|
1268
|
-
|
|
1269
|
-
"!==",
|
|
1270
|
-
config.google.packageName
|
|
1196
|
+
"[onesub/webhook/apple] Unknown transaction and Status API returned no record:",
|
|
1197
|
+
originalTransactionId
|
|
1271
1198
|
);
|
|
1272
|
-
sendError(res, 400, shared.ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
|
|
1273
|
-
return;
|
|
1274
|
-
}
|
|
1275
|
-
try {
|
|
1276
|
-
if (voided.productType === 1) {
|
|
1277
|
-
const existing = await store.getByTransactionId(voided.purchaseToken);
|
|
1278
|
-
if (existing) {
|
|
1279
|
-
const updated = config.refundPolicy === "until_expiry" ? { ...existing, willRenew: false } : { ...existing, status: shared.SUBSCRIPTION_STATUS.CANCELED };
|
|
1280
|
-
await store.save(updated);
|
|
1281
|
-
} else {
|
|
1282
|
-
log.warn(
|
|
1283
|
-
"[onesub/webhook/google] voided subscription for unknown purchaseToken:",
|
|
1284
|
-
voided.purchaseToken
|
|
1285
|
-
);
|
|
1286
|
-
}
|
|
1287
|
-
} else {
|
|
1288
|
-
const removed = await purchaseStore.deletePurchaseByTransactionId(voided.orderId);
|
|
1289
|
-
if (!removed) {
|
|
1290
|
-
log.warn(
|
|
1291
|
-
"[onesub/webhook/google] voided IAP for unknown orderId:",
|
|
1292
|
-
voided.orderId
|
|
1293
|
-
);
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
res.status(200).json({ received: true });
|
|
1297
|
-
} catch (err) {
|
|
1298
|
-
log.error("[onesub/webhook/google] voided notification error:", err);
|
|
1299
|
-
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process voided notification");
|
|
1300
1199
|
}
|
|
1200
|
+
} else {
|
|
1201
|
+
log.warn(
|
|
1202
|
+
"[onesub/webhook/apple] Received notification for unknown transaction (no API creds to recover):",
|
|
1203
|
+
originalTransactionId
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
res.status(200).json({ received: true });
|
|
1207
|
+
} catch (err) {
|
|
1208
|
+
log.error("[onesub/webhook/apple] Store update error:", err);
|
|
1209
|
+
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to update subscription");
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
var GOOGLE_JWKS_URL = "https://www.googleapis.com/oauth2/v3/certs";
|
|
1213
|
+
var googleJwks = null;
|
|
1214
|
+
function getGoogleJwks() {
|
|
1215
|
+
if (!googleJwks) googleJwks = jose.createRemoteJWKSet(new URL(GOOGLE_JWKS_URL));
|
|
1216
|
+
return googleJwks;
|
|
1217
|
+
}
|
|
1218
|
+
async function verifyGooglePushToken(req, expectedAudience) {
|
|
1219
|
+
const authHeader = req.headers["authorization"];
|
|
1220
|
+
if (typeof authHeader !== "string" || !authHeader.startsWith("Bearer ")) return false;
|
|
1221
|
+
try {
|
|
1222
|
+
await jose.jwtVerify(authHeader.slice("Bearer ".length).trim(), getGoogleJwks(), {
|
|
1223
|
+
audience: expectedAudience
|
|
1224
|
+
});
|
|
1225
|
+
return true;
|
|
1226
|
+
} catch {
|
|
1227
|
+
return false;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
async function handleGoogleWebhook(req, res, config, store, purchaseStore, webhookEventStore) {
|
|
1231
|
+
if (config.google?.pushAudience) {
|
|
1232
|
+
const authenticated = await verifyGooglePushToken(req, config.google.pushAudience);
|
|
1233
|
+
if (!authenticated) {
|
|
1234
|
+
sendError(res, 401, shared.ONESUB_ERROR_CODE.UNAUTHORIZED, "Unauthorized");
|
|
1301
1235
|
return;
|
|
1302
1236
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1237
|
+
}
|
|
1238
|
+
const body = req.body;
|
|
1239
|
+
if (!body.message?.data) {
|
|
1240
|
+
sendError(res, 400, shared.ONESUB_ERROR_CODE.MISSING_MESSAGE_DATA, "Missing message.data");
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
if (webhookEventStore && typeof body.message.messageId === "string") {
|
|
1244
|
+
const fresh = await webhookEventStore.markIfNew("google", body.message.messageId);
|
|
1245
|
+
if (!fresh) {
|
|
1246
|
+
log.info("[onesub/webhook/google] dedupe: already processed", body.message.messageId);
|
|
1247
|
+
res.status(200).json({ received: true, deduped: true });
|
|
1306
1248
|
return;
|
|
1307
1249
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
"!==",
|
|
1314
|
-
config.google.packageName
|
|
1315
|
-
);
|
|
1250
|
+
}
|
|
1251
|
+
const voided = decodeGoogleVoidedNotification(body);
|
|
1252
|
+
if (voided) {
|
|
1253
|
+
if (config.google?.packageName && voided.packageName !== config.google.packageName) {
|
|
1254
|
+
log.warn("[onesub/webhook/google] voided package name mismatch:", voided.packageName, "!==", config.google.packageName);
|
|
1316
1255
|
sendError(res, 400, shared.ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
|
|
1317
1256
|
return;
|
|
1318
1257
|
}
|
|
1319
1258
|
try {
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
} else if (isGooglePausedNotification(notificationType)) {
|
|
1329
|
-
finalStatus = shared.SUBSCRIPTION_STATUS.PAUSED;
|
|
1330
|
-
} else if (isGooglePriceChangeConfirmedNotification(notificationType)) {
|
|
1331
|
-
finalStatus = shared.SUBSCRIPTION_STATUS.ACTIVE;
|
|
1332
|
-
} else if (isGoogleCanceledNotification(notificationType)) {
|
|
1333
|
-
finalStatus = shared.SUBSCRIPTION_STATUS.CANCELED;
|
|
1334
|
-
} else if (isGoogleExpiredNotification(notificationType)) {
|
|
1335
|
-
finalStatus = shared.SUBSCRIPTION_STATUS.EXPIRED;
|
|
1259
|
+
if (voided.productType === 1) {
|
|
1260
|
+
const existing = await store.getByTransactionId(voided.purchaseToken);
|
|
1261
|
+
if (existing) {
|
|
1262
|
+
const updated = config.refundPolicy === "until_expiry" ? { ...existing, willRenew: false } : { ...existing, status: shared.SUBSCRIPTION_STATUS.CANCELED };
|
|
1263
|
+
await store.save(updated);
|
|
1264
|
+
} else {
|
|
1265
|
+
log.warn("[onesub/webhook/google] voided subscription for unknown purchaseToken:", voided.purchaseToken);
|
|
1266
|
+
}
|
|
1336
1267
|
} else {
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
void Promise.resolve().then(() => hook({ purchaseToken, subscriptionId, packageName })).catch((err) => log.warn("[onesub/webhook/google] onPriceChangeConfirmed hook failed:", err));
|
|
1268
|
+
const removed = await purchaseStore.deletePurchaseByTransactionId(voided.orderId);
|
|
1269
|
+
if (!removed) {
|
|
1270
|
+
log.warn("[onesub/webhook/google] voided IAP for unknown orderId:", voided.orderId);
|
|
1271
|
+
}
|
|
1342
1272
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1273
|
+
res.status(200).json({ received: true });
|
|
1274
|
+
} catch (err) {
|
|
1275
|
+
log.error("[onesub/webhook/google] voided notification error:", err);
|
|
1276
|
+
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process voided notification");
|
|
1277
|
+
}
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
const notification = decodeGoogleNotification(body);
|
|
1281
|
+
if (!notification) {
|
|
1282
|
+
res.status(200).json({ received: true });
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
const { notificationType, purchaseToken, subscriptionId, packageName } = notification;
|
|
1286
|
+
if (config.google?.packageName && packageName !== config.google.packageName) {
|
|
1287
|
+
log.warn("[onesub/webhook/google] Package name mismatch:", packageName, "!==", config.google.packageName);
|
|
1288
|
+
sendError(res, 400, shared.ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
try {
|
|
1292
|
+
const existing = await store.getByTransactionId(purchaseToken);
|
|
1293
|
+
let finalStatus;
|
|
1294
|
+
if (isGoogleActiveNotification(notificationType)) {
|
|
1295
|
+
finalStatus = shared.SUBSCRIPTION_STATUS.ACTIVE;
|
|
1296
|
+
} else if (isGoogleGracePeriodNotification(notificationType)) {
|
|
1297
|
+
finalStatus = shared.SUBSCRIPTION_STATUS.GRACE_PERIOD;
|
|
1298
|
+
} else if (isGoogleOnHoldNotification(notificationType)) {
|
|
1299
|
+
finalStatus = shared.SUBSCRIPTION_STATUS.ON_HOLD;
|
|
1300
|
+
} else if (isGooglePausedNotification(notificationType)) {
|
|
1301
|
+
finalStatus = shared.SUBSCRIPTION_STATUS.PAUSED;
|
|
1302
|
+
} else if (isGooglePriceChangeConfirmedNotification(notificationType)) {
|
|
1303
|
+
finalStatus = shared.SUBSCRIPTION_STATUS.ACTIVE;
|
|
1304
|
+
} else if (isGoogleCanceledNotification(notificationType)) {
|
|
1305
|
+
finalStatus = shared.SUBSCRIPTION_STATUS.CANCELED;
|
|
1306
|
+
} else if (isGoogleExpiredNotification(notificationType)) {
|
|
1307
|
+
finalStatus = shared.SUBSCRIPTION_STATUS.EXPIRED;
|
|
1308
|
+
} else {
|
|
1309
|
+
finalStatus = shared.SUBSCRIPTION_STATUS.ACTIVE;
|
|
1310
|
+
}
|
|
1311
|
+
if (isGooglePriceChangeConfirmedNotification(notificationType) && config.google?.onPriceChangeConfirmed) {
|
|
1312
|
+
const hook = config.google.onPriceChangeConfirmed;
|
|
1313
|
+
void Promise.resolve().then(() => hook({ purchaseToken, subscriptionId, packageName })).catch((err) => log.warn("[onesub/webhook/google] onPriceChangeConfirmed hook failed:", err));
|
|
1314
|
+
}
|
|
1315
|
+
if (existing) {
|
|
1316
|
+
let updated = { ...existing, status: finalStatus };
|
|
1317
|
+
if (config.google?.serviceAccountKey) {
|
|
1318
|
+
const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, config.google);
|
|
1319
|
+
if (fresh) {
|
|
1320
|
+
const preserveNotificationStatus = finalStatus === shared.SUBSCRIPTION_STATUS.GRACE_PERIOD || finalStatus === shared.SUBSCRIPTION_STATUS.ON_HOLD;
|
|
1321
|
+
updated = {
|
|
1322
|
+
...existing,
|
|
1323
|
+
status: preserveNotificationStatus ? finalStatus : fresh.status,
|
|
1324
|
+
expiresAt: fresh.expiresAt,
|
|
1325
|
+
willRenew: fresh.willRenew,
|
|
1326
|
+
autoResumeTime: fresh.autoResumeTime,
|
|
1327
|
+
linkedPurchaseToken: fresh.linkedPurchaseToken ?? existing.linkedPurchaseToken
|
|
1328
|
+
};
|
|
1363
1329
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
);
|
|
1376
|
-
} else {
|
|
1377
|
-
fresh.userId = purchaseToken;
|
|
1378
|
-
}
|
|
1379
|
-
} else {
|
|
1380
|
-
fresh.userId = purchaseToken;
|
|
1330
|
+
}
|
|
1331
|
+
await store.save(updated);
|
|
1332
|
+
} else {
|
|
1333
|
+
if (config.google?.serviceAccountKey) {
|
|
1334
|
+
const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, config.google);
|
|
1335
|
+
if (fresh) {
|
|
1336
|
+
if (fresh.linkedPurchaseToken) {
|
|
1337
|
+
const previous = await store.getByTransactionId(fresh.linkedPurchaseToken);
|
|
1338
|
+
fresh.userId = previous ? previous.userId : purchaseToken;
|
|
1339
|
+
if (previous) {
|
|
1340
|
+
log.info(`[onesub/webhook/google] inherited userId ${previous.userId} from linkedPurchaseToken ${fresh.linkedPurchaseToken} \u2192 new token ${purchaseToken}`);
|
|
1381
1341
|
}
|
|
1382
|
-
|
|
1342
|
+
} else {
|
|
1343
|
+
fresh.userId = purchaseToken;
|
|
1383
1344
|
}
|
|
1384
|
-
|
|
1385
|
-
log.warn(
|
|
1386
|
-
"[onesub/webhook/google] Unknown purchase token and no serviceAccountKey to re-fetch:",
|
|
1387
|
-
purchaseToken
|
|
1388
|
-
);
|
|
1345
|
+
await store.save(fresh);
|
|
1389
1346
|
}
|
|
1347
|
+
} else {
|
|
1348
|
+
log.warn("[onesub/webhook/google] Unknown purchase token and no serviceAccountKey to re-fetch:", purchaseToken);
|
|
1390
1349
|
}
|
|
1391
|
-
res.status(200).json({ received: true });
|
|
1392
|
-
} catch (err) {
|
|
1393
|
-
log.error("[onesub/webhook/google] Error handling notification:", err);
|
|
1394
|
-
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process notification");
|
|
1395
1350
|
}
|
|
1396
|
-
|
|
1351
|
+
res.status(200).json({ received: true });
|
|
1352
|
+
} catch (err) {
|
|
1353
|
+
log.error("[onesub/webhook/google] Error handling notification:", err);
|
|
1354
|
+
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process notification");
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// src/routes/webhook.ts
|
|
1359
|
+
function createWebhookRouter(config, store, purchaseStore, webhookEventStore) {
|
|
1360
|
+
const router = express.Router();
|
|
1361
|
+
router.post(
|
|
1362
|
+
shared.ROUTES.WEBHOOK_APPLE,
|
|
1363
|
+
(req, res) => handleAppleWebhook(req, res, config, store, purchaseStore, webhookEventStore)
|
|
1364
|
+
);
|
|
1365
|
+
router.post(
|
|
1366
|
+
shared.ROUTES.WEBHOOK_GOOGLE,
|
|
1367
|
+
(req, res) => handleGoogleWebhook(req, res, config, store, purchaseStore, webhookEventStore)
|
|
1368
|
+
);
|
|
1397
1369
|
return router;
|
|
1398
1370
|
}
|
|
1399
1371
|
var NO_PURCHASE = { valid: false, purchase: null };
|
|
@@ -3057,7 +3029,6 @@ function createOneSubServer(config) {
|
|
|
3057
3029
|
app.use(createOneSubMiddleware(config));
|
|
3058
3030
|
return app;
|
|
3059
3031
|
}
|
|
3060
|
-
var index_default = createOneSubMiddleware;
|
|
3061
3032
|
var isMain = typeof process !== "undefined" && process.argv[1] != null && new URL((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))).pathname.endsWith(process.argv[1].replace(/\\/g, "/"));
|
|
3062
3033
|
if (isMain) {
|
|
3063
3034
|
const config = {
|
|
@@ -3099,7 +3070,6 @@ exports.RedisSubscriptionStore = RedisSubscriptionStore;
|
|
|
3099
3070
|
exports.RedisWebhookEventStore = RedisWebhookEventStore;
|
|
3100
3071
|
exports.createOneSubMiddleware = createOneSubMiddleware;
|
|
3101
3072
|
exports.createOneSubServer = createOneSubServer;
|
|
3102
|
-
exports.default = index_default;
|
|
3103
3073
|
exports.evaluateEntitlement = evaluateEntitlement;
|
|
3104
3074
|
exports.fetchAppleSubscriptionStatus = fetchAppleSubscriptionStatus;
|
|
3105
3075
|
exports.getDefaultCache = getDefaultCache;
|