@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.
Files changed (120) hide show
  1. package/dist/__tests__/webhook-apple.test.d.ts +12 -0
  2. package/dist/__tests__/webhook-apple.test.d.ts.map +1 -0
  3. package/dist/__tests__/webhook-google.test.d.ts +13 -0
  4. package/dist/__tests__/webhook-google.test.d.ts.map +1 -0
  5. package/dist/index.cjs +258 -288
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.ts +0 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +259 -286
  10. package/dist/index.js.map +1 -1
  11. package/dist/providers/google.d.ts.map +1 -1
  12. package/dist/routes/webhook-apple.d.ts +6 -0
  13. package/dist/routes/webhook-apple.d.ts.map +1 -0
  14. package/dist/routes/webhook-google.d.ts +7 -0
  15. package/dist/routes/webhook-google.d.ts.map +1 -0
  16. package/dist/routes/webhook.d.ts +4 -17
  17. package/dist/routes/webhook.d.ts.map +1 -1
  18. package/package.json +2 -2
  19. package/dist/__tests__/admin-customer-detail.test.js +0 -146
  20. package/dist/__tests__/admin-customer-detail.test.js.map +0 -1
  21. package/dist/__tests__/admin-list-subscriptions.test.js +0 -192
  22. package/dist/__tests__/admin-list-subscriptions.test.js.map +0 -1
  23. package/dist/__tests__/admin-subscription-detail.test.js +0 -112
  24. package/dist/__tests__/admin-subscription-detail.test.js.map +0 -1
  25. package/dist/__tests__/apple-root-ca.test.js +0 -41
  26. package/dist/__tests__/apple-root-ca.test.js.map +0 -1
  27. package/dist/__tests__/apple-status-api.test.js +0 -345
  28. package/dist/__tests__/apple-status-api.test.js.map +0 -1
  29. package/dist/__tests__/cache.test.js +0 -34
  30. package/dist/__tests__/cache.test.js.map +0 -1
  31. package/dist/__tests__/entitlements.test.js +0 -268
  32. package/dist/__tests__/entitlements.test.js.map +0 -1
  33. package/dist/__tests__/error-codes.test.js +0 -168
  34. package/dist/__tests__/error-codes.test.js.map +0 -1
  35. package/dist/__tests__/fetch-hardening.test.js +0 -192
  36. package/dist/__tests__/fetch-hardening.test.js.map +0 -1
  37. package/dist/__tests__/google-paused.test.js +0 -126
  38. package/dist/__tests__/google-paused.test.js.map +0 -1
  39. package/dist/__tests__/google-price-change.test.js +0 -186
  40. package/dist/__tests__/google-price-change.test.js.map +0 -1
  41. package/dist/__tests__/google-subscriptions-v2.test.js +0 -206
  42. package/dist/__tests__/google-subscriptions-v2.test.js.map +0 -1
  43. package/dist/__tests__/lifecycle-scenarios.test.js +0 -579
  44. package/dist/__tests__/lifecycle-scenarios.test.js.map +0 -1
  45. package/dist/__tests__/lifecycle-states.test.js +0 -423
  46. package/dist/__tests__/lifecycle-states.test.js.map +0 -1
  47. package/dist/__tests__/linked-purchase-token.test.js +0 -227
  48. package/dist/__tests__/linked-purchase-token.test.js.map +0 -1
  49. package/dist/__tests__/metrics.test.js +0 -340
  50. package/dist/__tests__/metrics.test.js.map +0 -1
  51. package/dist/__tests__/mock-provider.test.js +0 -188
  52. package/dist/__tests__/mock-provider.test.js.map +0 -1
  53. package/dist/__tests__/openapi.test.js +0 -32
  54. package/dist/__tests__/openapi.test.js.map +0 -1
  55. package/dist/__tests__/paused-auto-resume.test.js +0 -162
  56. package/dist/__tests__/paused-auto-resume.test.js.map +0 -1
  57. package/dist/__tests__/providers.test.js +0 -321
  58. package/dist/__tests__/providers.test.js.map +0 -1
  59. package/dist/__tests__/redis-store.test.js +0 -117
  60. package/dist/__tests__/redis-store.test.js.map +0 -1
  61. package/dist/__tests__/refund-policy.test.js +0 -317
  62. package/dist/__tests__/refund-policy.test.js.map +0 -1
  63. package/dist/__tests__/schema.test.js +0 -46
  64. package/dist/__tests__/schema.test.js.map +0 -1
  65. package/dist/__tests__/store.test.js +0 -172
  66. package/dist/__tests__/store.test.js.map +0 -1
  67. package/dist/__tests__/test-utils.js +0 -30
  68. package/dist/__tests__/test-utils.js.map +0 -1
  69. package/dist/__tests__/webhook-events.test.js +0 -36
  70. package/dist/__tests__/webhook-events.test.js.map +0 -1
  71. package/dist/__tests__/webhook-queue.test.js +0 -26
  72. package/dist/__tests__/webhook-queue.test.js.map +0 -1
  73. package/dist/__tests__/webhook-refund.test.js +0 -313
  74. package/dist/__tests__/webhook-refund.test.js.map +0 -1
  75. package/dist/cache.js +0 -43
  76. package/dist/cache.js.map +0 -1
  77. package/dist/errors.js +0 -19
  78. package/dist/errors.js.map +0 -1
  79. package/dist/http.js +0 -36
  80. package/dist/http.js.map +0 -1
  81. package/dist/logger.js +0 -20
  82. package/dist/logger.js.map +0 -1
  83. package/dist/openapi.js +0 -196
  84. package/dist/openapi.js.map +0 -1
  85. package/dist/providers/apple-root-ca.js +0 -38
  86. package/dist/providers/apple-root-ca.js.map +0 -1
  87. package/dist/providers/apple.js +0 -510
  88. package/dist/providers/apple.js.map +0 -1
  89. package/dist/providers/google.js +0 -522
  90. package/dist/providers/google.js.map +0 -1
  91. package/dist/providers/mock.js +0 -93
  92. package/dist/providers/mock.js.map +0 -1
  93. package/dist/routes/admin.js +0 -287
  94. package/dist/routes/admin.js.map +0 -1
  95. package/dist/routes/entitlements.js +0 -130
  96. package/dist/routes/entitlements.js.map +0 -1
  97. package/dist/routes/metrics.js +0 -281
  98. package/dist/routes/metrics.js.map +0 -1
  99. package/dist/routes/purchase.js +0 -187
  100. package/dist/routes/purchase.js.map +0 -1
  101. package/dist/routes/status.js +0 -55
  102. package/dist/routes/status.js.map +0 -1
  103. package/dist/routes/validate.js +0 -70
  104. package/dist/routes/validate.js.map +0 -1
  105. package/dist/routes/webhook.js +0 -461
  106. package/dist/routes/webhook.js.map +0 -1
  107. package/dist/store.js +0 -145
  108. package/dist/store.js.map +0 -1
  109. package/dist/stores/postgres.js +0 -329
  110. package/dist/stores/postgres.js.map +0 -1
  111. package/dist/stores/redis.js +0 -255
  112. package/dist/stores/redis.js.map +0 -1
  113. package/dist/stores/schema.js +0 -67
  114. package/dist/stores/schema.js.map +0 -1
  115. package/dist/tracing.js +0 -74
  116. package/dist/tracing.js.map +0 -1
  117. package/dist/webhook-events.js +0 -57
  118. package/dist/webhook-events.js.map +0 -1
  119. package/dist/webhook-queue.js +0 -108
  120. 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 GOOGLE_JWKS_URL = "https://www.googleapis.com/oauth2/v3/certs";
1076
- var googleJwks = null;
1077
- function getGoogleJwks() {
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 createWebhookRouter(config, store, purchaseStore, webhookEventStore) {
1121
- const router = express.Router();
1122
- router.post(shared.ROUTES.WEBHOOK_APPLE, async (req, res) => {
1123
- const body = req.body;
1124
- if (!body.signedPayload) {
1125
- sendError(res, 400, shared.ONESUB_ERROR_CODE.MISSING_SIGNED_PAYLOAD, "Missing signedPayload");
1126
- return;
1127
- }
1128
- let payload;
1129
- try {
1130
- payload = await decodeJws(
1131
- body.signedPayload,
1132
- config.apple?.skipJwsVerification
1133
- );
1134
- } catch (err) {
1135
- log.error("[onesub/webhook/apple] Failed to decode signedPayload:", err);
1136
- sendError(res, 400, shared.ONESUB_ERROR_CODE.INVALID_SIGNED_PAYLOAD, "Invalid signedPayload");
1137
- return;
1138
- }
1139
- if (webhookEventStore && typeof payload.notificationUUID === "string") {
1140
- const fresh = await webhookEventStore.markIfNew("apple", payload.notificationUUID);
1141
- if (!fresh) {
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
- const {
1153
- originalTransactionId,
1154
- transactionId,
1155
- type,
1156
- productId,
1157
- bundleId,
1158
- environment,
1159
- status,
1160
- willRenew,
1161
- expiresAt
1162
- } = decoded;
1163
- const notificationType = payload.notificationType;
1164
- const subtype = payload.subtype;
1165
- const mapped = mapAppleNotificationStatus(notificationType, subtype);
1166
- const finalStatus = mapped ?? status;
1167
- if (notificationType === "CONSUMPTION_REQUEST" && config.apple?.consumptionInfoProvider && transactionId && productId) {
1168
- const provider = config.apple.consumptionInfoProvider;
1169
- const appleConfig = config.apple;
1170
- void (async () => {
1171
- try {
1172
- const info = await provider({
1173
- transactionId,
1174
- originalTransactionId,
1175
- productId,
1176
- bundleId: bundleId ?? appleConfig.bundleId,
1177
- environment
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
- const existing = await store.getByTransactionId(originalTransactionId);
1205
- if (existing) {
1206
- const isSubscriptionRefund = isRefundOrRevoke && !isOneTimePurchase && notificationType !== "CONSUMPTION_REQUEST";
1207
- const keepEntitlement = isSubscriptionRefund && config.refundPolicy === "until_expiry";
1208
- const updated = keepEntitlement ? { ...existing, willRenew: false } : {
1209
- ...existing,
1210
- status: finalStatus,
1211
- willRenew,
1212
- // expiresAt may be absent on non-subscription payloads — keep the
1213
- // previously-stored value rather than overwriting with null/empty.
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
- if (webhookEventStore && typeof body.message.messageId === "string") {
1256
- const fresh = await webhookEventStore.markIfNew("google", body.message.messageId);
1257
- if (!fresh) {
1258
- log.info("[onesub/webhook/google] dedupe: already processed", body.message.messageId);
1259
- res.status(200).json({ received: true, deduped: true });
1260
- return;
1261
- }
1262
- }
1263
- const voided = decodeGoogleVoidedNotification(body);
1264
- if (voided) {
1265
- if (config.google?.packageName && voided.packageName !== config.google.packageName) {
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/google] voided package name mismatch:",
1268
- voided.packageName,
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
- const notification = decodeGoogleNotification(body);
1304
- if (!notification) {
1305
- res.status(200).json({ received: true });
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
- const { notificationType, purchaseToken, subscriptionId, packageName } = notification;
1309
- if (config.google?.packageName && packageName !== config.google.packageName) {
1310
- log.warn(
1311
- "[onesub/webhook/google] Package name mismatch:",
1312
- packageName,
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
- const existing = await store.getByTransactionId(purchaseToken);
1321
- let finalStatus;
1322
- if (isGoogleActiveNotification(notificationType)) {
1323
- finalStatus = shared.SUBSCRIPTION_STATUS.ACTIVE;
1324
- } else if (isGoogleGracePeriodNotification(notificationType)) {
1325
- finalStatus = shared.SUBSCRIPTION_STATUS.GRACE_PERIOD;
1326
- } else if (isGoogleOnHoldNotification(notificationType)) {
1327
- finalStatus = shared.SUBSCRIPTION_STATUS.ON_HOLD;
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
- finalStatus = shared.SUBSCRIPTION_STATUS.ACTIVE;
1338
- }
1339
- if (isGooglePriceChangeConfirmedNotification(notificationType) && config.google?.onPriceChangeConfirmed) {
1340
- const hook = config.google.onPriceChangeConfirmed;
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
- if (existing) {
1344
- let updated = { ...existing, status: finalStatus };
1345
- if (config.google?.serviceAccountKey) {
1346
- const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, config.google);
1347
- if (fresh) {
1348
- const preserveNotificationStatus = finalStatus === shared.SUBSCRIPTION_STATUS.GRACE_PERIOD || finalStatus === shared.SUBSCRIPTION_STATUS.ON_HOLD;
1349
- updated = {
1350
- ...existing,
1351
- status: preserveNotificationStatus ? finalStatus : fresh.status,
1352
- expiresAt: fresh.expiresAt,
1353
- willRenew: fresh.willRenew,
1354
- // Propagate v2-only fields that fresh may have updated. Without
1355
- // these, paused→active transitions left autoResumeTime stale and
1356
- // upgrade chains lost their linkedPurchaseToken on subsequent
1357
- // notifications. Use ?? undefined so cleared values become undefined
1358
- // (e.g. resume from paused → autoResumeTime should disappear).
1359
- autoResumeTime: fresh.autoResumeTime,
1360
- linkedPurchaseToken: fresh.linkedPurchaseToken ?? existing.linkedPurchaseToken
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
- await store.save(updated);
1365
- } else {
1366
- if (config.google?.serviceAccountKey) {
1367
- const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, config.google);
1368
- if (fresh) {
1369
- if (fresh.linkedPurchaseToken) {
1370
- const previous = await store.getByTransactionId(fresh.linkedPurchaseToken);
1371
- if (previous) {
1372
- fresh.userId = previous.userId;
1373
- log.info(
1374
- `[onesub/webhook/google] inherited userId ${previous.userId} from linkedPurchaseToken ${fresh.linkedPurchaseToken} \u2192 new token ${purchaseToken}`
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
- await store.save(fresh);
1342
+ } else {
1343
+ fresh.userId = purchaseToken;
1383
1344
  }
1384
- } else {
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;