@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/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');
@@ -479,7 +477,9 @@ async function decodeAppleNotification(payload, skipJwsVerification = false) {
479
477
  environment,
480
478
  status,
481
479
  willRenew,
482
- expiresAt: tx.expiresDate ? new Date(tx.expiresDate).toISOString() : null
480
+ expiresAt: tx.expiresDate ? new Date(tx.expiresDate).toISOString() : null,
481
+ appAccountToken: tx.appAccountToken ?? null,
482
+ inAppOwnershipType: tx.inAppOwnershipType ?? null
483
483
  };
484
484
  }
485
485
  var appleJwtMintPromises = /* @__PURE__ */ new Map();
@@ -625,6 +625,91 @@ async function fetchAppleSubscriptionStatus(originalTransactionId, config, optio
625
625
  willRenew: renewal?.autoRenewStatus === 1
626
626
  };
627
627
  }
628
+ async function fetchAppleTransactionHistory(originalTransactionId, config, options) {
629
+ if (config.mockMode) return null;
630
+ let jwt;
631
+ try {
632
+ jwt = await makeAppleApiJwt(config);
633
+ } catch (err) {
634
+ log.warn("[onesub/apple] Cannot fetch transaction history \u2014 JWT mint failed:", err);
635
+ return null;
636
+ }
637
+ const host = options?.sandbox ? "api.storekit-sandbox.itunes.apple.com" : "api.storekit.itunes.apple.com";
638
+ const results = [];
639
+ let revision;
640
+ for (; ; ) {
641
+ const url = new URL(`https://${host}/inApps/v2/history/${encodeURIComponent(originalTransactionId)}`);
642
+ if (revision) url.searchParams.set("revision", revision);
643
+ let page;
644
+ try {
645
+ const resp = await fetchWithTimeout(url.toString(), {
646
+ headers: { Authorization: `Bearer ${jwt}` }
647
+ });
648
+ if (!resp.ok) {
649
+ const text = await resp.text();
650
+ log.warn(`[onesub/apple] Transaction History API error ${resp.status}: ${text}`);
651
+ return null;
652
+ }
653
+ page = await resp.json();
654
+ } catch (err) {
655
+ log.warn("[onesub/apple] Transaction History API network error:", err);
656
+ return null;
657
+ }
658
+ for (const signed of page.signedTransactions ?? []) {
659
+ try {
660
+ const tx = await decodeJws(signed, config.skipJwsVerification);
661
+ if (!tx.originalTransactionId || !tx.transactionId || !tx.productId) continue;
662
+ results.push({
663
+ originalTransactionId: tx.originalTransactionId,
664
+ transactionId: tx.transactionId,
665
+ productId: tx.productId,
666
+ type: tx.type ?? "Unknown",
667
+ purchasedAt: tx.purchaseDate ? new Date(tx.purchaseDate).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
668
+ expiresAt: tx.expiresDate ? new Date(tx.expiresDate).toISOString() : null,
669
+ appAccountToken: tx.appAccountToken ?? null,
670
+ inAppOwnershipType: tx.inAppOwnershipType ?? null,
671
+ revocationDate: tx.revocationDate ? new Date(tx.revocationDate).toISOString() : null
672
+ });
673
+ } catch {
674
+ }
675
+ }
676
+ if (!page.hasMore) break;
677
+ revision = page.revision;
678
+ }
679
+ return results;
680
+ }
681
+ async function signApplePromotionalOffer(input, config) {
682
+ const { offerKeyId, offerPrivateKey } = config;
683
+ if (!offerKeyId || !offerPrivateKey) {
684
+ throw new Error("[onesub/apple] Promotional Offer signing requires config.apple.offerKeyId and config.apple.offerPrivateKey");
685
+ }
686
+ const nonce = crypto.randomUUID().toLowerCase();
687
+ const timestamp = Date.now();
688
+ const separator = "\u2063";
689
+ const message = [
690
+ input.bundleId,
691
+ offerKeyId,
692
+ input.productId,
693
+ input.offerId,
694
+ input.applicationUsername,
695
+ nonce,
696
+ String(timestamp)
697
+ ].join(separator);
698
+ if (!offerPrivateKey.includes("BEGIN") || !offerPrivateKey.includes("END")) {
699
+ throw new Error("[onesub/apple] offerPrivateKey does not appear to be PEM-encoded");
700
+ }
701
+ let signatureBuffer;
702
+ try {
703
+ const sign = crypto.createSign("SHA256");
704
+ sign.update(message);
705
+ sign.end();
706
+ signatureBuffer = sign.sign(offerPrivateKey);
707
+ } catch (err) {
708
+ throw new Error(`[onesub/apple] Failed to sign promotional offer \u2014 check that offerPrivateKey is a valid ES256 PEM key: ${err.message}`);
709
+ }
710
+ const signature = signatureBuffer.toString("base64");
711
+ return { keyId: offerKeyId, nonce, timestamp, signature };
712
+ }
628
713
  var GOOGLE_NOTIFICATION_TYPE = {
629
714
  SUBSCRIPTION_RECOVERED: 1,
630
715
  SUBSCRIPTION_RENEWED: 2,
@@ -687,8 +772,8 @@ async function getAccessToken(serviceAccountKey) {
687
772
  })
688
773
  ).toString("base64url");
689
774
  const signingInput = `${header}.${payload}`;
690
- const { createSign } = await import('crypto');
691
- const sign = createSign("RSA-SHA256");
775
+ const { createSign: createSign2 } = await import('crypto');
776
+ const sign = createSign2("RSA-SHA256");
692
777
  sign.update(signingInput);
693
778
  const signature = sign.sign(key.private_key, "base64url");
694
779
  const assertion = `${signingInput}.${signature}`;
@@ -732,6 +817,7 @@ async function fetchProductPurchase(packageName, productId, purchaseToken, acces
732
817
  async function acknowledgeGoogleSubscription(purchaseToken, productId, config) {
733
818
  if (config.mockMode) return;
734
819
  if (!config.serviceAccountKey) return;
820
+ if (!config.packageName) return;
735
821
  let accessToken;
736
822
  try {
737
823
  accessToken = await getCachedAccessToken(config.serviceAccountKey);
@@ -757,6 +843,7 @@ async function acknowledgeGoogleSubscription(purchaseToken, productId, config) {
757
843
  async function acknowledgeGoogleProduct(purchaseToken, productId, config) {
758
844
  if (config.mockMode) return;
759
845
  if (!config.serviceAccountKey) return;
846
+ if (!config.packageName) return;
760
847
  let accessToken;
761
848
  try {
762
849
  accessToken = await getCachedAccessToken(config.serviceAccountKey);
@@ -781,6 +868,7 @@ async function acknowledgeGoogleProduct(purchaseToken, productId, config) {
781
868
  }
782
869
  async function consumeGoogleProductReceipt(purchaseToken, productId, config) {
783
870
  if (!config.serviceAccountKey) return;
871
+ if (!config.packageName) return;
784
872
  let accessToken;
785
873
  try {
786
874
  accessToken = await getCachedAccessToken(config.serviceAccountKey);
@@ -828,6 +916,10 @@ async function validateGoogleReceipt(receipt, productId, config) {
828
916
  log.warn("[onesub/google] No serviceAccountKey provided \u2014 cannot call Play API");
829
917
  return null;
830
918
  }
919
+ if (!config.packageName) {
920
+ log.warn("[onesub/google] No packageName provided \u2014 cannot call Play API");
921
+ return null;
922
+ }
831
923
  let purchase;
832
924
  try {
833
925
  const token = await getCachedAccessToken(config.serviceAccountKey);
@@ -880,6 +972,10 @@ async function validateGoogleProductReceipt(purchaseToken, productId, config, ty
880
972
  log.warn("[onesub/google] No serviceAccountKey \u2014 cannot validate product receipt");
881
973
  return null;
882
974
  }
975
+ if (!config.packageName) {
976
+ log.warn("[onesub/google] No packageName \u2014 cannot validate product receipt");
977
+ return null;
978
+ }
883
979
  let purchase;
884
980
  try {
885
981
  const token = await getCachedAccessToken(config.serviceAccountKey);
@@ -949,6 +1045,22 @@ function decodeGoogleVoidedNotification(payload) {
949
1045
  packageName: notification.packageName
950
1046
  };
951
1047
  }
1048
+ function decodeGoogleOneTimeProductNotification(payload) {
1049
+ let notification;
1050
+ try {
1051
+ const json = Buffer.from(payload.message.data, "base64").toString("utf-8");
1052
+ notification = JSON.parse(json);
1053
+ } catch {
1054
+ return null;
1055
+ }
1056
+ if (!notification.oneTimeProductNotification) return null;
1057
+ const { notificationType, purchaseToken, sku } = notification.oneTimeProductNotification;
1058
+ if (notificationType !== 1 && notificationType !== 2) {
1059
+ log.warn("[onesub/google] Unknown oneTimeProductNotification type:", notificationType);
1060
+ return null;
1061
+ }
1062
+ return { notificationType, purchaseToken, sku, packageName: notification.packageName };
1063
+ }
952
1064
  function isGoogleActiveNotification(notificationType) {
953
1065
  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;
954
1066
  }
@@ -1072,41 +1184,9 @@ function createStatusRouter(store) {
1072
1184
  });
1073
1185
  return router;
1074
1186
  }
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
- ]);
1187
+ var APPLE_ACTIVE_TYPES = /* @__PURE__ */ new Set(["SUBSCRIBED", "DID_RENEW", "DID_RECOVER", "OFFER_REDEEMED"]);
1188
+ var APPLE_CANCELED_TYPES = /* @__PURE__ */ new Set(["REVOKE", "REFUND", "CONSUMPTION_REQUEST"]);
1189
+ var APPLE_EXPIRED_TYPES = /* @__PURE__ */ new Set(["EXPIRED"]);
1110
1190
  function mapAppleNotificationStatus(notificationType, subtype) {
1111
1191
  if (notificationType === "DID_FAIL_TO_RENEW") {
1112
1192
  return subtype === "GRACE_PERIOD" ? shared.SUBSCRIPTION_STATUS.GRACE_PERIOD : shared.SUBSCRIPTION_STATUS.ON_HOLD;
@@ -1117,283 +1197,315 @@ function mapAppleNotificationStatus(notificationType, subtype) {
1117
1197
  if (APPLE_ACTIVE_TYPES.has(notificationType)) return shared.SUBSCRIPTION_STATUS.ACTIVE;
1118
1198
  return null;
1119
1199
  }
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");
1200
+ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhookEventStore) {
1201
+ const body = req.body;
1202
+ if (!body.signedPayload) {
1203
+ sendError(res, 400, shared.ONESUB_ERROR_CODE.MISSING_SIGNED_PAYLOAD, "Missing signedPayload");
1204
+ return;
1205
+ }
1206
+ let payload;
1207
+ try {
1208
+ payload = await decodeJws(
1209
+ body.signedPayload,
1210
+ config.apple?.skipJwsVerification
1211
+ );
1212
+ } catch (err) {
1213
+ log.error("[onesub/webhook/apple] Failed to decode signedPayload:", err);
1214
+ sendError(res, 400, shared.ONESUB_ERROR_CODE.INVALID_SIGNED_PAYLOAD, "Invalid signedPayload");
1215
+ return;
1216
+ }
1217
+ if (webhookEventStore && typeof payload.notificationUUID === "string") {
1218
+ const fresh = await webhookEventStore.markIfNew("apple", payload.notificationUUID);
1219
+ if (!fresh) {
1220
+ log.info("[onesub/webhook/apple] dedupe: already processed", payload.notificationUUID);
1221
+ res.status(200).json({ received: true, deduped: true });
1137
1222
  return;
1138
1223
  }
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;
1224
+ }
1225
+ const decoded = await decodeAppleNotification(payload, config.apple?.skipJwsVerification);
1226
+ if (!decoded) {
1227
+ res.status(200).json({ received: true });
1228
+ return;
1229
+ }
1230
+ const {
1231
+ originalTransactionId,
1232
+ transactionId,
1233
+ type,
1234
+ productId,
1235
+ bundleId,
1236
+ environment,
1237
+ status,
1238
+ willRenew,
1239
+ expiresAt,
1240
+ appAccountToken,
1241
+ inAppOwnershipType
1242
+ } = decoded;
1243
+ const notificationType = payload.notificationType;
1244
+ const subtype = payload.subtype;
1245
+ const mapped = mapAppleNotificationStatus(notificationType, subtype);
1246
+ const finalStatus = mapped ?? status;
1247
+ if (notificationType === "CONSUMPTION_REQUEST" && config.apple?.consumptionInfoProvider && transactionId && productId) {
1248
+ const provider = config.apple.consumptionInfoProvider;
1249
+ const appleConfig = config.apple;
1250
+ void (async () => {
1251
+ try {
1252
+ const info = await provider({
1253
+ transactionId,
1254
+ originalTransactionId,
1255
+ productId,
1256
+ bundleId: bundleId ?? appleConfig.bundleId,
1257
+ environment
1258
+ });
1259
+ if (info) {
1260
+ await sendAppleConsumptionResponse(transactionId, info, appleConfig, {
1261
+ sandbox: environment === "Sandbox"
1262
+ });
1263
+ }
1264
+ } catch (err) {
1265
+ log.warn("[onesub/webhook/apple] consumptionInfoProvider failed:", err);
1266
+ }
1267
+ })();
1268
+ }
1269
+ const isOneTimePurchase = type === "Consumable" || type === "Non-Consumable";
1270
+ const isRefundOrRevoke = APPLE_CANCELED_TYPES.has(notificationType);
1271
+ try {
1272
+ if (isOneTimePurchase && isRefundOrRevoke) {
1273
+ const lookupId = transactionId ?? originalTransactionId;
1274
+ const removed = await purchaseStore.deletePurchaseByTransactionId(lookupId);
1275
+ if (!removed) {
1276
+ log.warn("[onesub/webhook/apple] IAP refund for unknown transaction:", lookupId);
1145
1277
  }
1146
- }
1147
- const decoded = await decodeAppleNotification(payload, config.apple?.skipJwsVerification);
1148
- if (!decoded) {
1149
1278
  res.status(200).json({ received: true });
1150
1279
  return;
1151
1280
  }
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
1178
- });
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
- }
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;
1281
+ const existing = await store.getByTransactionId(originalTransactionId);
1282
+ if (existing) {
1283
+ const isSubscriptionRefund = isRefundOrRevoke && !isOneTimePurchase && notificationType !== "CONSUMPTION_REQUEST";
1284
+ const keepEntitlement = isSubscriptionRefund && config.refundPolicy === "until_expiry";
1285
+ const correctedUserId = appAccountToken && existing.userId === originalTransactionId ? appAccountToken : existing.userId;
1286
+ if (correctedUserId !== existing.userId) {
1287
+ log.info("[onesub/webhook/apple] correcting userId from originalTransactionId to appAccountToken:", correctedUserId);
1203
1288
  }
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
- );
1289
+ const updated = keepEntitlement ? { ...existing, userId: correctedUserId, willRenew: false } : {
1290
+ ...existing,
1291
+ userId: correctedUserId,
1292
+ status: finalStatus,
1293
+ willRenew,
1294
+ expiresAt: expiresAt ?? existing.expiresAt
1295
+ };
1296
+ await store.save(updated);
1297
+ } else if (config.apple?.issuerId && config.apple?.keyId && config.apple?.privateKey) {
1298
+ const fresh = await fetchAppleSubscriptionStatus(originalTransactionId, config.apple, {
1299
+ sandbox: environment === "Sandbox"
1300
+ });
1301
+ if (fresh) {
1302
+ fresh.userId = appAccountToken ?? originalTransactionId;
1303
+ if (inAppOwnershipType === "FAMILY_SHARED") {
1304
+ const source = appAccountToken ? "appAccountToken" : "originalTransactionId (fallback)";
1305
+ log.info(`[onesub/webhook/apple] FAMILY_SHARED \u2014 userId: ${fresh.userId} (source: ${source})`);
1229
1306
  }
1307
+ await store.save(fresh);
1230
1308
  } else {
1231
1309
  log.warn(
1232
- "[onesub/webhook/apple] Received notification for unknown transaction (no API creds to recover):",
1310
+ "[onesub/webhook/apple] Unknown transaction and Status API returned no record:",
1233
1311
  originalTransactionId
1234
1312
  );
1235
1313
  }
1236
- 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");
1314
+ } else {
1315
+ log.warn(
1316
+ "[onesub/webhook/apple] Received notification for unknown transaction (no API creds to recover):",
1317
+ originalTransactionId
1318
+ );
1240
1319
  }
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
- }
1320
+ res.status(200).json({ received: true });
1321
+ } catch (err) {
1322
+ log.error("[onesub/webhook/apple] Store update error:", err);
1323
+ sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to update subscription");
1324
+ }
1325
+ }
1326
+ var GOOGLE_JWKS_URL = "https://www.googleapis.com/oauth2/v3/certs";
1327
+ var googleJwks = null;
1328
+ function getGoogleJwks() {
1329
+ if (!googleJwks) googleJwks = jose.createRemoteJWKSet(new URL(GOOGLE_JWKS_URL));
1330
+ return googleJwks;
1331
+ }
1332
+ async function verifyGooglePushToken(req, expectedAudience) {
1333
+ const authHeader = req.headers["authorization"];
1334
+ if (typeof authHeader !== "string" || !authHeader.startsWith("Bearer ")) return false;
1335
+ try {
1336
+ await jose.jwtVerify(authHeader.slice("Bearer ".length).trim(), getGoogleJwks(), {
1337
+ audience: expectedAudience
1338
+ });
1339
+ return true;
1340
+ } catch {
1341
+ return false;
1342
+ }
1343
+ }
1344
+ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webhookEventStore) {
1345
+ if (config.google?.pushAudience) {
1346
+ const authenticated = await verifyGooglePushToken(req, config.google.pushAudience);
1347
+ if (!authenticated) {
1348
+ sendError(res, 401, shared.ONESUB_ERROR_CODE.UNAUTHORIZED, "Unauthorized");
1349
+ return;
1249
1350
  }
1250
- const body = req.body;
1251
- if (!body.message?.data) {
1252
- sendError(res, 400, shared.ONESUB_ERROR_CODE.MISSING_MESSAGE_DATA, "Missing message.data");
1351
+ }
1352
+ const body = req.body;
1353
+ if (!body.message?.data) {
1354
+ sendError(res, 400, shared.ONESUB_ERROR_CODE.MISSING_MESSAGE_DATA, "Missing message.data");
1355
+ return;
1356
+ }
1357
+ if (webhookEventStore && typeof body.message.messageId === "string") {
1358
+ const fresh = await webhookEventStore.markIfNew("google", body.message.messageId);
1359
+ if (!fresh) {
1360
+ log.info("[onesub/webhook/google] dedupe: already processed", body.message.messageId);
1361
+ res.status(200).json({ received: true, deduped: true });
1253
1362
  return;
1254
1363
  }
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
- }
1364
+ }
1365
+ const voided = decodeGoogleVoidedNotification(body);
1366
+ if (voided) {
1367
+ if (config.google?.packageName && voided.packageName !== config.google.packageName) {
1368
+ log.warn("[onesub/webhook/google] voided package name mismatch:", voided.packageName, "!==", config.google.packageName);
1369
+ sendError(res, 400, shared.ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
1370
+ return;
1262
1371
  }
1263
- const voided = decodeGoogleVoidedNotification(body);
1264
- if (voided) {
1265
- if (config.google?.packageName && voided.packageName !== config.google.packageName) {
1266
- log.warn(
1267
- "[onesub/webhook/google] voided package name mismatch:",
1268
- voided.packageName,
1269
- "!==",
1270
- config.google.packageName
1271
- );
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
- }
1372
+ try {
1373
+ if (voided.productType === 1) {
1374
+ const existing = await store.getByTransactionId(voided.purchaseToken);
1375
+ if (existing) {
1376
+ const updated = config.refundPolicy === "until_expiry" ? { ...existing, willRenew: false } : { ...existing, status: shared.SUBSCRIPTION_STATUS.CANCELED };
1377
+ await store.save(updated);
1287
1378
  } 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
- }
1379
+ log.warn("[onesub/webhook/google] voided subscription for unknown purchaseToken:", voided.purchaseToken);
1380
+ }
1381
+ } else {
1382
+ const removed = await purchaseStore.deletePurchaseByTransactionId(voided.orderId);
1383
+ if (!removed) {
1384
+ log.warn("[onesub/webhook/google] voided IAP for unknown orderId:", voided.orderId);
1295
1385
  }
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
1386
  }
1301
- return;
1302
- }
1303
- const notification = decodeGoogleNotification(body);
1304
- if (!notification) {
1305
1387
  res.status(200).json({ received: true });
1306
- return;
1388
+ } catch (err) {
1389
+ log.error("[onesub/webhook/google] voided notification error:", err);
1390
+ sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process voided notification");
1307
1391
  }
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
- );
1392
+ return;
1393
+ }
1394
+ const oneTimeProduct = decodeGoogleOneTimeProductNotification(body);
1395
+ if (oneTimeProduct) {
1396
+ if (config.google?.packageName && oneTimeProduct.packageName !== config.google.packageName) {
1397
+ log.warn("[onesub/webhook/google] oneTimeProduct package name mismatch:", oneTimeProduct.packageName, "!==", config.google.packageName);
1316
1398
  sendError(res, 400, shared.ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
1317
1399
  return;
1318
1400
  }
1319
1401
  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;
1402
+ const { notificationType: notificationType2, purchaseToken: purchaseToken2, sku } = oneTimeProduct;
1403
+ if (notificationType2 === 1) {
1404
+ log.info("[onesub/webhook/google] oneTimeProduct PURCHASED:", sku);
1405
+ if (config.google?.serviceAccountKey && config.google.packageName) {
1406
+ void acknowledgeGoogleProduct(purchaseToken2, sku, config.google).catch(
1407
+ (err) => log.warn(`[onesub/webhook/google] oneTimeProduct ack failed for SKU ${sku} \u2014 3-day auto-refund risk:`, err)
1408
+ );
1409
+ }
1336
1410
  } else {
1337
- finalStatus = shared.SUBSCRIPTION_STATUS.ACTIVE;
1411
+ log.info("[onesub/webhook/google] oneTimeProduct CANCELED (pre-completion):", sku);
1338
1412
  }
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));
1342
- }
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
- }
1413
+ res.status(200).json({ received: true });
1414
+ } catch (err) {
1415
+ log.error("[onesub/webhook/google] oneTimeProduct error:", err);
1416
+ sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process oneTimeProduct notification");
1417
+ }
1418
+ return;
1419
+ }
1420
+ const notification = decodeGoogleNotification(body);
1421
+ if (!notification) {
1422
+ res.status(200).json({ received: true });
1423
+ return;
1424
+ }
1425
+ const { notificationType, purchaseToken, subscriptionId, packageName } = notification;
1426
+ if (config.google?.packageName && packageName !== config.google.packageName) {
1427
+ log.warn("[onesub/webhook/google] Package name mismatch:", packageName, "!==", config.google.packageName);
1428
+ sendError(res, 400, shared.ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
1429
+ return;
1430
+ }
1431
+ try {
1432
+ const existing = await store.getByTransactionId(purchaseToken);
1433
+ let finalStatus;
1434
+ if (isGoogleActiveNotification(notificationType)) {
1435
+ finalStatus = shared.SUBSCRIPTION_STATUS.ACTIVE;
1436
+ } else if (isGoogleGracePeriodNotification(notificationType)) {
1437
+ finalStatus = shared.SUBSCRIPTION_STATUS.GRACE_PERIOD;
1438
+ } else if (isGoogleOnHoldNotification(notificationType)) {
1439
+ finalStatus = shared.SUBSCRIPTION_STATUS.ON_HOLD;
1440
+ } else if (isGooglePausedNotification(notificationType)) {
1441
+ finalStatus = shared.SUBSCRIPTION_STATUS.PAUSED;
1442
+ } else if (isGooglePriceChangeConfirmedNotification(notificationType)) {
1443
+ finalStatus = shared.SUBSCRIPTION_STATUS.ACTIVE;
1444
+ } else if (isGoogleCanceledNotification(notificationType)) {
1445
+ finalStatus = shared.SUBSCRIPTION_STATUS.CANCELED;
1446
+ } else if (isGoogleExpiredNotification(notificationType)) {
1447
+ finalStatus = shared.SUBSCRIPTION_STATUS.EXPIRED;
1448
+ } else {
1449
+ finalStatus = shared.SUBSCRIPTION_STATUS.ACTIVE;
1450
+ }
1451
+ if (isGooglePriceChangeConfirmedNotification(notificationType) && config.google?.onPriceChangeConfirmed) {
1452
+ const hook = config.google.onPriceChangeConfirmed;
1453
+ void Promise.resolve().then(() => hook({ purchaseToken, subscriptionId, packageName })).catch((err) => log.warn("[onesub/webhook/google] onPriceChangeConfirmed hook failed:", err));
1454
+ }
1455
+ if (existing) {
1456
+ let updated = { ...existing, status: finalStatus };
1457
+ if (config.google?.serviceAccountKey) {
1458
+ const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, config.google);
1459
+ if (fresh) {
1460
+ const preserveNotificationStatus = finalStatus === shared.SUBSCRIPTION_STATUS.GRACE_PERIOD || finalStatus === shared.SUBSCRIPTION_STATUS.ON_HOLD;
1461
+ updated = {
1462
+ ...existing,
1463
+ status: preserveNotificationStatus ? finalStatus : fresh.status,
1464
+ expiresAt: fresh.expiresAt,
1465
+ willRenew: fresh.willRenew,
1466
+ autoResumeTime: fresh.autoResumeTime,
1467
+ linkedPurchaseToken: fresh.linkedPurchaseToken ?? existing.linkedPurchaseToken
1468
+ };
1363
1469
  }
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;
1470
+ }
1471
+ await store.save(updated);
1472
+ } else {
1473
+ if (config.google?.serviceAccountKey) {
1474
+ const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, config.google);
1475
+ if (fresh) {
1476
+ if (fresh.linkedPurchaseToken) {
1477
+ const previous = await store.getByTransactionId(fresh.linkedPurchaseToken);
1478
+ fresh.userId = previous ? previous.userId : purchaseToken;
1479
+ if (previous) {
1480
+ log.info(`[onesub/webhook/google] inherited userId ${previous.userId} from linkedPurchaseToken ${fresh.linkedPurchaseToken} \u2192 new token ${purchaseToken}`);
1381
1481
  }
1382
- await store.save(fresh);
1482
+ } else {
1483
+ fresh.userId = purchaseToken;
1383
1484
  }
1384
- } else {
1385
- log.warn(
1386
- "[onesub/webhook/google] Unknown purchase token and no serviceAccountKey to re-fetch:",
1387
- purchaseToken
1388
- );
1485
+ await store.save(fresh);
1389
1486
  }
1487
+ } else {
1488
+ log.warn("[onesub/webhook/google] Unknown purchase token and no serviceAccountKey to re-fetch:", purchaseToken);
1390
1489
  }
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
1490
  }
1396
- });
1491
+ res.status(200).json({ received: true });
1492
+ } catch (err) {
1493
+ log.error("[onesub/webhook/google] Error handling notification:", err);
1494
+ sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process notification");
1495
+ }
1496
+ }
1497
+
1498
+ // src/routes/webhook.ts
1499
+ function createWebhookRouter(config, store, purchaseStore, webhookEventStore) {
1500
+ const router = express.Router();
1501
+ router.post(
1502
+ shared.ROUTES.WEBHOOK_APPLE,
1503
+ (req, res) => handleAppleWebhook(req, res, config, store, purchaseStore, webhookEventStore)
1504
+ );
1505
+ router.post(
1506
+ shared.ROUTES.WEBHOOK_GOOGLE,
1507
+ (req, res) => handleGoogleWebhook(req, res, config, store, purchaseStore, webhookEventStore)
1508
+ );
1397
1509
  return router;
1398
1510
  }
1399
1511
  var NO_PURCHASE = { valid: false, purchase: null };
@@ -1816,6 +1928,38 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
1816
1928
  sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, err.message ?? "customer error");
1817
1929
  }
1818
1930
  });
1931
+ const syncAppleParamsSchema = zod.z.object({
1932
+ originalTransactionId: zod.z.string().min(1).max(256)
1933
+ });
1934
+ router.post(shared.ROUTES.ADMIN_SYNC_APPLE, async (req, res) => {
1935
+ let params;
1936
+ try {
1937
+ params = syncAppleParamsSchema.parse(req.params);
1938
+ } catch {
1939
+ sendError(res, 400, shared.ONESUB_ERROR_CODE.INVALID_INPUT, "originalTransactionId required");
1940
+ return;
1941
+ }
1942
+ if (!config.apple?.issuerId || !config.apple?.keyId || !config.apple?.privateKey) {
1943
+ sendError(res, 400, shared.ONESUB_ERROR_CODE.APPLE_CONFIG_MISSING, "Apple API credentials not configured");
1944
+ return;
1945
+ }
1946
+ try {
1947
+ const { originalTransactionId } = params;
1948
+ const existingForEnv = await store.getByTransactionId(originalTransactionId);
1949
+ const isSandbox = req.query["sandbox"] === "true" || existingForEnv?.environment === "Sandbox";
1950
+ const fresh = await fetchAppleSubscriptionStatus(originalTransactionId, config.apple, { sandbox: isSandbox });
1951
+ if (!fresh) {
1952
+ sendError(res, 404, shared.ONESUB_ERROR_CODE.TRANSACTION_NOT_FOUND, "Apple Status API returned no record");
1953
+ return;
1954
+ }
1955
+ const existing = existingForEnv;
1956
+ fresh.userId = existing?.userId ?? originalTransactionId;
1957
+ await store.save(fresh);
1958
+ res.status(200).json({ ok: true, subscription: fresh });
1959
+ } catch (err) {
1960
+ sendError(res, 500, shared.ONESUB_ERROR_CODE.INTERNAL_ERROR, err.message ?? "sync error");
1961
+ }
1962
+ });
1819
1963
  if (webhookQueue?.listDeadLetters) {
1820
1964
  router.get("/onesub/admin/webhook-deadletters", async (_req, res) => {
1821
1965
  try {
@@ -2061,6 +2205,55 @@ function createMetricsRouter(config, store, purchaseStore) {
2061
2205
  });
2062
2206
  return router;
2063
2207
  }
2208
+ var OFFER_SECRET_HEADER = "x-onesub-offer-secret";
2209
+ var offerBodySchema = zod.z.object({
2210
+ productId: zod.z.string().min(1).max(256),
2211
+ offerId: zod.z.string().min(1).max(256),
2212
+ applicationUsername: zod.z.string().min(1).max(256)
2213
+ });
2214
+ function createAppleOfferRouter(config) {
2215
+ const apple = config.apple;
2216
+ if (!apple?.offerKeyId || !apple?.offerPrivateKey) return null;
2217
+ const router = express.Router();
2218
+ router.post("/onesub/apple/offer-signature", async (req, res) => {
2219
+ if (config.adminSecret) {
2220
+ const provided = req.headers[OFFER_SECRET_HEADER];
2221
+ if (typeof provided !== "string" || provided !== config.adminSecret) {
2222
+ sendError(res, 401, shared.ONESUB_ERROR_CODE.UNAUTHORIZED, "Unauthorized");
2223
+ return;
2224
+ }
2225
+ }
2226
+ let body;
2227
+ try {
2228
+ body = offerBodySchema.parse(req.body);
2229
+ } catch (err) {
2230
+ if (err instanceof zod.z.ZodError) {
2231
+ sendZodError(res, err);
2232
+ return;
2233
+ }
2234
+ throw err;
2235
+ }
2236
+ if (!apple.bundleId) {
2237
+ sendError(res, 400, shared.ONESUB_ERROR_CODE.APPLE_CONFIG_MISSING, "config.apple.bundleId is required for offer signing");
2238
+ return;
2239
+ }
2240
+ try {
2241
+ const result = await signApplePromotionalOffer(
2242
+ {
2243
+ bundleId: apple.bundleId,
2244
+ productId: body.productId,
2245
+ offerId: body.offerId,
2246
+ applicationUsername: body.applicationUsername
2247
+ },
2248
+ apple
2249
+ );
2250
+ res.status(200).json(result);
2251
+ } catch (err) {
2252
+ sendError(res, 500, shared.ONESUB_ERROR_CODE.INTERNAL_ERROR, err.message ?? "Offer signing failed");
2253
+ }
2254
+ });
2255
+ return router;
2256
+ }
2064
2257
 
2065
2258
  // src/stores/schema.ts
2066
2259
  var SUBSCRIPTIONS_SCHEMA_SQL = `
@@ -3047,6 +3240,8 @@ function createOneSubMiddleware(config) {
3047
3240
  if (entitlementRouter) router.use(entitlementRouter);
3048
3241
  const metricsRouter = createMetricsRouter(config, store, purchaseStore);
3049
3242
  if (metricsRouter) router.use(metricsRouter);
3243
+ const appleOfferRouter = createAppleOfferRouter(config);
3244
+ if (appleOfferRouter) router.use(appleOfferRouter);
3050
3245
  return router;
3051
3246
  }
3052
3247
  function createOneSubServer(config) {
@@ -3057,7 +3252,6 @@ function createOneSubServer(config) {
3057
3252
  app.use(createOneSubMiddleware(config));
3058
3253
  return app;
3059
3254
  }
3060
- var index_default = createOneSubMiddleware;
3061
3255
  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
3256
  if (isMain) {
3063
3257
  const config = {
@@ -3099,14 +3293,15 @@ exports.RedisSubscriptionStore = RedisSubscriptionStore;
3099
3293
  exports.RedisWebhookEventStore = RedisWebhookEventStore;
3100
3294
  exports.createOneSubMiddleware = createOneSubMiddleware;
3101
3295
  exports.createOneSubServer = createOneSubServer;
3102
- exports.default = index_default;
3103
3296
  exports.evaluateEntitlement = evaluateEntitlement;
3104
3297
  exports.fetchAppleSubscriptionStatus = fetchAppleSubscriptionStatus;
3298
+ exports.fetchAppleTransactionHistory = fetchAppleTransactionHistory;
3105
3299
  exports.getDefaultCache = getDefaultCache;
3106
3300
  exports.log = log;
3107
3301
  exports.openapiHandler = openapiHandler;
3108
3302
  exports.setDefaultCache = setDefaultCache;
3109
3303
  exports.setLogger = setLogger;
3304
+ exports.signApplePromotionalOffer = signApplePromotionalOffer;
3110
3305
  exports.validateAppleReceipt = validateAppleReceipt;
3111
3306
  exports.validateGoogleReceipt = validateGoogleReceipt;
3112
3307
  exports.withSpan = withSpan;