@onesub/server 0.13.0 → 0.14.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.
Files changed (42) hide show
  1. package/dist/__tests__/error-codes.test.js +8 -3
  2. package/dist/__tests__/error-codes.test.js.map +1 -1
  3. package/dist/__tests__/redis-store.test.js +58 -9
  4. package/dist/__tests__/redis-store.test.js.map +1 -1
  5. package/dist/__tests__/webhook-apple.test.js +247 -0
  6. package/dist/__tests__/webhook-apple.test.js.map +1 -0
  7. package/dist/__tests__/webhook-google.test.js +255 -0
  8. package/dist/__tests__/webhook-google.test.js.map +1 -0
  9. package/dist/cache.js +0 -4
  10. package/dist/cache.js.map +1 -1
  11. package/dist/index.cjs +36 -7
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.js +36 -7
  14. package/dist/index.js.map +1 -1
  15. package/dist/providers/apple.d.ts +8 -0
  16. package/dist/providers/apple.d.ts.map +1 -1
  17. package/dist/providers/apple.js +121 -1
  18. package/dist/providers/apple.js.map +1 -1
  19. package/dist/providers/google.d.ts +8 -0
  20. package/dist/providers/google.d.ts.map +1 -1
  21. package/dist/providers/google.js +42 -0
  22. package/dist/providers/google.js.map +1 -1
  23. package/dist/providers/mock.d.ts +2 -0
  24. package/dist/providers/mock.d.ts.map +1 -1
  25. package/dist/routes/admin.js +40 -0
  26. package/dist/routes/admin.js.map +1 -1
  27. package/dist/routes/apple-offer.js +72 -0
  28. package/dist/routes/apple-offer.js.map +1 -0
  29. package/dist/routes/purchase.d.ts.map +1 -1
  30. package/dist/routes/purchase.js +21 -4
  31. package/dist/routes/purchase.js.map +1 -1
  32. package/dist/routes/webhook-apple.js +145 -0
  33. package/dist/routes/webhook-apple.js.map +1 -0
  34. package/dist/routes/webhook-google.js +204 -0
  35. package/dist/routes/webhook-google.js.map +1 -0
  36. package/dist/routes/webhook.js +9 -451
  37. package/dist/routes/webhook.js.map +1 -1
  38. package/dist/stores/redis.js +65 -23
  39. package/dist/stores/redis.js.map +1 -1
  40. package/dist/webhook-events.js +9 -8
  41. package/dist/webhook-events.js.map +1 -1
  42. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -248,10 +248,12 @@ function mockValidateAppleProduct(receipt, expectedProductId) {
248
248
  const outcome = classifyMockReceipt(receipt);
249
249
  if (!outcomePasses(outcome, "apple")) return null;
250
250
  const productId = expectedProductId ?? "mock_product";
251
+ const tokenMatch = receipt.match(/#token=([^#]+)/);
251
252
  return {
252
253
  transactionId: deterministicTransactionId(`mock_apple_${productId}`, receipt),
253
254
  productId,
254
- purchasedAt: (/* @__PURE__ */ new Date()).toISOString()
255
+ purchasedAt: (/* @__PURE__ */ new Date()).toISOString(),
256
+ ...tokenMatch ? { appAccountToken: tokenMatch[1] } : {}
255
257
  };
256
258
  }
257
259
  function mockValidateGoogleSubscription(receipt, productId) {
@@ -273,9 +275,11 @@ function mockValidateGoogleSubscription(receipt, productId) {
273
275
  function mockValidateGoogleProduct(receipt, productId) {
274
276
  const outcome = classifyMockReceipt(receipt);
275
277
  if (!outcomePasses(outcome, "google")) return null;
278
+ const tokenMatch = receipt.match(/#token=([^#]+)/);
276
279
  return {
277
280
  transactionId: deterministicTransactionId(`mock_google_${productId}`, receipt),
278
- purchasedAt: (/* @__PURE__ */ new Date()).toISOString()
281
+ purchasedAt: (/* @__PURE__ */ new Date()).toISOString(),
282
+ ...tokenMatch ? { obfuscatedExternalAccountId: tokenMatch[1] } : {}
279
283
  };
280
284
  }
281
285
 
@@ -439,7 +443,8 @@ async function validateAppleConsumableReceipt(signedTransaction, config, expecte
439
443
  return {
440
444
  transactionId,
441
445
  productId: tx.productId,
442
- purchasedAt: tx.purchaseDate ? new Date(tx.purchaseDate).toISOString() : (/* @__PURE__ */ new Date()).toISOString()
446
+ purchasedAt: tx.purchaseDate ? new Date(tx.purchaseDate).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
447
+ appAccountToken: tx.appAccountToken ?? void 0
443
448
  };
444
449
  }
445
450
  async function decodeAppleNotification(payload, skipJwsVerification = false) {
@@ -998,7 +1003,8 @@ async function validateGoogleProductReceipt(purchaseToken, productId, config, ty
998
1003
  }
999
1004
  return {
1000
1005
  transactionId: purchase.orderId,
1001
- purchasedAt: purchase.purchaseTimeMillis ? new Date(parseInt(purchase.purchaseTimeMillis, 10)).toISOString() : (/* @__PURE__ */ new Date()).toISOString()
1006
+ purchasedAt: purchase.purchaseTimeMillis ? new Date(parseInt(purchase.purchaseTimeMillis, 10)).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
1007
+ obfuscatedExternalAccountId: purchase.obfuscatedExternalAccountId
1002
1008
  };
1003
1009
  }
1004
1010
  function decodeGoogleNotification(payload) {
@@ -1529,14 +1535,22 @@ function createPurchaseRouter(config, purchaseStore) {
1529
1535
  const { platform, receipt, userId, productId, type } = body;
1530
1536
  try {
1531
1537
  if (type === PURCHASE_TYPE.NON_CONSUMABLE) {
1532
- const alreadyOwned = await purchaseStore.hasPurchased(userId, productId);
1533
- if (alreadyOwned) {
1534
- sendError(res, 409, ONESUB_ERROR_CODE.NON_CONSUMABLE_ALREADY_OWNED, "NON_CONSUMABLE_ALREADY_OWNED", NO_PURCHASE);
1538
+ const owned = (await purchaseStore.getPurchasesByUserId(userId)).find(
1539
+ (p) => p.productId === productId
1540
+ );
1541
+ if (owned) {
1542
+ const response2 = {
1543
+ valid: true,
1544
+ purchase: { ...owned, userId },
1545
+ action: "restored"
1546
+ };
1547
+ res.status(200).json(response2);
1535
1548
  return;
1536
1549
  }
1537
1550
  }
1538
1551
  let transactionId = null;
1539
1552
  let purchasedAt = (/* @__PURE__ */ new Date()).toISOString();
1553
+ let boundAccountId = null;
1540
1554
  if (platform === "apple") {
1541
1555
  if (!config.apple) {
1542
1556
  sendError(res, 500, ONESUB_ERROR_CODE.APPLE_CONFIG_MISSING, "Apple configuration not provided", NO_PURCHASE);
@@ -1546,6 +1560,7 @@ function createPurchaseRouter(config, purchaseStore) {
1546
1560
  if (result) {
1547
1561
  transactionId = result.transactionId;
1548
1562
  purchasedAt = result.purchasedAt;
1563
+ boundAccountId = result.appAccountToken ?? null;
1549
1564
  }
1550
1565
  } else {
1551
1566
  if (!config.google) {
@@ -1561,12 +1576,26 @@ function createPurchaseRouter(config, purchaseStore) {
1561
1576
  if (result) {
1562
1577
  transactionId = result.transactionId;
1563
1578
  purchasedAt = result.purchasedAt;
1579
+ boundAccountId = result.obfuscatedExternalAccountId ?? null;
1564
1580
  }
1565
1581
  }
1566
1582
  if (!transactionId) {
1567
1583
  sendError(res, 422, ONESUB_ERROR_CODE.RECEIPT_VALIDATION_FAILED, "Receipt validation failed", NO_PURCHASE);
1568
1584
  return;
1569
1585
  }
1586
+ if (boundAccountId && boundAccountId.toLowerCase() !== userId.toLowerCase()) {
1587
+ log.warn(
1588
+ `[onesub/purchase] account binding mismatch for transaction ${transactionId}: token does not match userId ${userId}`
1589
+ );
1590
+ sendError(
1591
+ res,
1592
+ 409,
1593
+ ONESUB_ERROR_CODE.TRANSACTION_BELONGS_TO_OTHER_USER,
1594
+ "TRANSACTION_BELONGS_TO_OTHER_USER",
1595
+ NO_PURCHASE
1596
+ );
1597
+ return;
1598
+ }
1570
1599
  const existing = await purchaseStore.getPurchaseByTransactionId(transactionId);
1571
1600
  let action = "new";
1572
1601
  if (existing) {