@onesub/server 0.12.1 → 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.
Files changed (118) hide show
  1. package/dist/__tests__/admin-customer-detail.test.js +146 -0
  2. package/dist/__tests__/admin-customer-detail.test.js.map +1 -0
  3. package/dist/__tests__/admin-list-subscriptions.test.js +192 -0
  4. package/dist/__tests__/admin-list-subscriptions.test.js.map +1 -0
  5. package/dist/__tests__/admin-subscription-detail.test.js +112 -0
  6. package/dist/__tests__/admin-subscription-detail.test.js.map +1 -0
  7. package/dist/__tests__/apple-root-ca.test.js +41 -0
  8. package/dist/__tests__/apple-root-ca.test.js.map +1 -0
  9. package/dist/__tests__/apple-status-api.test.js +345 -0
  10. package/dist/__tests__/apple-status-api.test.js.map +1 -0
  11. package/dist/__tests__/cache.test.js +34 -0
  12. package/dist/__tests__/cache.test.js.map +1 -0
  13. package/dist/__tests__/entitlements.test.js +268 -0
  14. package/dist/__tests__/entitlements.test.js.map +1 -0
  15. package/dist/__tests__/error-codes.test.js +168 -0
  16. package/dist/__tests__/error-codes.test.js.map +1 -0
  17. package/dist/__tests__/fetch-hardening.test.js +192 -0
  18. package/dist/__tests__/fetch-hardening.test.js.map +1 -0
  19. package/dist/__tests__/google-paused.test.js +126 -0
  20. package/dist/__tests__/google-paused.test.js.map +1 -0
  21. package/dist/__tests__/google-price-change.test.js +186 -0
  22. package/dist/__tests__/google-price-change.test.js.map +1 -0
  23. package/dist/__tests__/google-subscriptions-v2.test.js +206 -0
  24. package/dist/__tests__/google-subscriptions-v2.test.js.map +1 -0
  25. package/dist/__tests__/lifecycle-scenarios.test.js +579 -0
  26. package/dist/__tests__/lifecycle-scenarios.test.js.map +1 -0
  27. package/dist/__tests__/lifecycle-states.test.js +423 -0
  28. package/dist/__tests__/lifecycle-states.test.js.map +1 -0
  29. package/dist/__tests__/linked-purchase-token.test.js +227 -0
  30. package/dist/__tests__/linked-purchase-token.test.js.map +1 -0
  31. package/dist/__tests__/metrics.test.js +340 -0
  32. package/dist/__tests__/metrics.test.js.map +1 -0
  33. package/dist/__tests__/mock-provider.test.js +188 -0
  34. package/dist/__tests__/mock-provider.test.js.map +1 -0
  35. package/dist/__tests__/openapi.test.js +32 -0
  36. package/dist/__tests__/openapi.test.js.map +1 -0
  37. package/dist/__tests__/paused-auto-resume.test.js +162 -0
  38. package/dist/__tests__/paused-auto-resume.test.js.map +1 -0
  39. package/dist/__tests__/providers.test.js +321 -0
  40. package/dist/__tests__/providers.test.js.map +1 -0
  41. package/dist/__tests__/redis-store.test.js +117 -0
  42. package/dist/__tests__/redis-store.test.js.map +1 -0
  43. package/dist/__tests__/refund-policy.test.js +317 -0
  44. package/dist/__tests__/refund-policy.test.js.map +1 -0
  45. package/dist/__tests__/schema.test.js +46 -0
  46. package/dist/__tests__/schema.test.js.map +1 -0
  47. package/dist/__tests__/store.test.js +172 -0
  48. package/dist/__tests__/store.test.js.map +1 -0
  49. package/dist/__tests__/test-utils.js +30 -0
  50. package/dist/__tests__/test-utils.js.map +1 -0
  51. package/dist/__tests__/webhook-events.test.js +36 -0
  52. package/dist/__tests__/webhook-events.test.js.map +1 -0
  53. package/dist/__tests__/webhook-queue.test.js +26 -0
  54. package/dist/__tests__/webhook-queue.test.js.map +1 -0
  55. package/dist/__tests__/webhook-refund.test.js +313 -0
  56. package/dist/__tests__/webhook-refund.test.js.map +1 -0
  57. package/dist/cache.js +43 -0
  58. package/dist/cache.js.map +1 -0
  59. package/dist/errors.js +19 -0
  60. package/dist/errors.js.map +1 -0
  61. package/dist/http.js +36 -0
  62. package/dist/http.js.map +1 -0
  63. package/dist/index.cjs +231 -6
  64. package/dist/index.cjs.map +1 -1
  65. package/dist/index.d.ts +1 -1
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +231 -8
  68. package/dist/index.js.map +1 -1
  69. package/dist/logger.js +20 -0
  70. package/dist/logger.js.map +1 -0
  71. package/dist/openapi.js +196 -0
  72. package/dist/openapi.js.map +1 -0
  73. package/dist/providers/apple-root-ca.js +38 -0
  74. package/dist/providers/apple-root-ca.js.map +1 -0
  75. package/dist/providers/apple.d.ts +76 -0
  76. package/dist/providers/apple.d.ts.map +1 -1
  77. package/dist/providers/apple.js +510 -0
  78. package/dist/providers/apple.js.map +1 -0
  79. package/dist/providers/google.d.ts +25 -0
  80. package/dist/providers/google.d.ts.map +1 -1
  81. package/dist/providers/google.js +522 -0
  82. package/dist/providers/google.js.map +1 -0
  83. package/dist/providers/mock.js +93 -0
  84. package/dist/providers/mock.js.map +1 -0
  85. package/dist/routes/admin.d.ts.map +1 -1
  86. package/dist/routes/admin.js +287 -0
  87. package/dist/routes/admin.js.map +1 -0
  88. package/dist/routes/apple-offer.d.ts +21 -0
  89. package/dist/routes/apple-offer.d.ts.map +1 -0
  90. package/dist/routes/entitlements.js +130 -0
  91. package/dist/routes/entitlements.js.map +1 -0
  92. package/dist/routes/metrics.js +281 -0
  93. package/dist/routes/metrics.js.map +1 -0
  94. package/dist/routes/purchase.js +187 -0
  95. package/dist/routes/purchase.js.map +1 -0
  96. package/dist/routes/status.js +55 -0
  97. package/dist/routes/status.js.map +1 -0
  98. package/dist/routes/validate.js +70 -0
  99. package/dist/routes/validate.js.map +1 -0
  100. package/dist/routes/webhook-apple.d.ts.map +1 -1
  101. package/dist/routes/webhook-google.d.ts.map +1 -1
  102. package/dist/routes/webhook.js +461 -0
  103. package/dist/routes/webhook.js.map +1 -0
  104. package/dist/store.js +145 -0
  105. package/dist/store.js.map +1 -0
  106. package/dist/stores/postgres.js +329 -0
  107. package/dist/stores/postgres.js.map +1 -0
  108. package/dist/stores/redis.js +255 -0
  109. package/dist/stores/redis.js.map +1 -0
  110. package/dist/stores/schema.js +67 -0
  111. package/dist/stores/schema.js.map +1 -0
  112. package/dist/tracing.js +74 -0
  113. package/dist/tracing.js.map +1 -0
  114. package/dist/webhook-events.js +57 -0
  115. package/dist/webhook-events.js.map +1 -0
  116. package/dist/webhook-queue.js +108 -0
  117. package/dist/webhook-queue.js.map +1 -0
  118. package/package.json +2 -2
package/dist/index.d.ts CHANGED
@@ -81,7 +81,7 @@ export { InProcessWebhookQueue, BullMQWebhookQueue } from './webhook-queue.js';
81
81
  export { ONESUB_OPENAPI, openapiHandler } from './openapi.js';
82
82
  export type { OpenAPIDoc } from './openapi.js';
83
83
  export { withSpan } from './tracing.js';
84
- export { validateAppleReceipt, fetchAppleSubscriptionStatus } from './providers/apple.js';
84
+ export { validateAppleReceipt, fetchAppleSubscriptionStatus, fetchAppleTransactionHistory, signApplePromotionalOffer, } from './providers/apple.js';
85
85
  export { validateGoogleReceipt } from './providers/google.js';
86
86
  export { evaluateEntitlement } from './routes/entitlements.js';
87
87
  export { setLogger, log } from './logger.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAGzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AASnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;GAEG;AACH,MAAM,WAAW,sBAAuB,SAAQ,kBAAkB;IAChE;;;OAGG;IACH,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;;OAIG;IACH,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CA6C7E;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,sBAAsB,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAW7F;AAGD,OAAO,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAC9E,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AACxF,OAAO,EACL,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,sBAAsB,GACvB,MAAM,mBAAmB,CAAC;AAG3B,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAGpF,YAAY,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,yBAAyB,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACxF,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACrG,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAI/E,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9D,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAI/C,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAGxC,OAAO,EAAE,oBAAoB,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAC;AAC1F,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAI9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAI/D,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAGzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAUnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;GAEG;AACH,MAAM,WAAW,sBAAuB,SAAQ,kBAAkB;IAChE;;;OAGG;IACH,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;;OAIG;IACH,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAiD7E;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,sBAAsB,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAW7F;AAGD,OAAO,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAC9E,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AACxF,OAAO,EACL,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,sBAAsB,GACvB,MAAM,mBAAmB,CAAC;AAG3B,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAGpF,YAAY,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,yBAAyB,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACxF,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACrG,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAI/E,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9D,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAI/C,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAGxC,OAAO,EACL,oBAAoB,EACpB,4BAA4B,EAC5B,4BAA4B,EAC5B,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAI9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAI/D,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import express, { Router } from 'express';
2
2
  import { PURCHASE_TYPE, ROUTES, DEFAULT_PORT, ONESUB_ERROR_CODE, SUBSCRIPTION_STATUS, MOCK_RECEIPT_PREFIX } from '@onesub/shared';
3
3
  import { z } from 'zod';
4
4
  import { decodeJwt, decodeProtectedHeader, importX509, jwtVerify, importPKCS8, SignJWT, createRemoteJWKSet } from 'jose';
5
- import { X509Certificate, createHash } from 'crypto';
5
+ import { X509Certificate, randomUUID, createSign, createHash } from 'crypto';
6
6
 
7
7
  // src/index.ts
8
8
 
@@ -470,7 +470,9 @@ async function decodeAppleNotification(payload, skipJwsVerification = false) {
470
470
  environment,
471
471
  status,
472
472
  willRenew,
473
- expiresAt: tx.expiresDate ? new Date(tx.expiresDate).toISOString() : null
473
+ expiresAt: tx.expiresDate ? new Date(tx.expiresDate).toISOString() : null,
474
+ appAccountToken: tx.appAccountToken ?? null,
475
+ inAppOwnershipType: tx.inAppOwnershipType ?? null
474
476
  };
475
477
  }
476
478
  var appleJwtMintPromises = /* @__PURE__ */ new Map();
@@ -616,6 +618,91 @@ async function fetchAppleSubscriptionStatus(originalTransactionId, config, optio
616
618
  willRenew: renewal?.autoRenewStatus === 1
617
619
  };
618
620
  }
621
+ async function fetchAppleTransactionHistory(originalTransactionId, config, options) {
622
+ if (config.mockMode) return null;
623
+ let jwt;
624
+ try {
625
+ jwt = await makeAppleApiJwt(config);
626
+ } catch (err) {
627
+ log.warn("[onesub/apple] Cannot fetch transaction history \u2014 JWT mint failed:", err);
628
+ return null;
629
+ }
630
+ const host = options?.sandbox ? "api.storekit-sandbox.itunes.apple.com" : "api.storekit.itunes.apple.com";
631
+ const results = [];
632
+ let revision;
633
+ for (; ; ) {
634
+ const url = new URL(`https://${host}/inApps/v2/history/${encodeURIComponent(originalTransactionId)}`);
635
+ if (revision) url.searchParams.set("revision", revision);
636
+ let page;
637
+ try {
638
+ const resp = await fetchWithTimeout(url.toString(), {
639
+ headers: { Authorization: `Bearer ${jwt}` }
640
+ });
641
+ if (!resp.ok) {
642
+ const text = await resp.text();
643
+ log.warn(`[onesub/apple] Transaction History API error ${resp.status}: ${text}`);
644
+ return null;
645
+ }
646
+ page = await resp.json();
647
+ } catch (err) {
648
+ log.warn("[onesub/apple] Transaction History API network error:", err);
649
+ return null;
650
+ }
651
+ for (const signed of page.signedTransactions ?? []) {
652
+ try {
653
+ const tx = await decodeJws(signed, config.skipJwsVerification);
654
+ if (!tx.originalTransactionId || !tx.transactionId || !tx.productId) continue;
655
+ results.push({
656
+ originalTransactionId: tx.originalTransactionId,
657
+ transactionId: tx.transactionId,
658
+ productId: tx.productId,
659
+ type: tx.type ?? "Unknown",
660
+ purchasedAt: tx.purchaseDate ? new Date(tx.purchaseDate).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
661
+ expiresAt: tx.expiresDate ? new Date(tx.expiresDate).toISOString() : null,
662
+ appAccountToken: tx.appAccountToken ?? null,
663
+ inAppOwnershipType: tx.inAppOwnershipType ?? null,
664
+ revocationDate: tx.revocationDate ? new Date(tx.revocationDate).toISOString() : null
665
+ });
666
+ } catch {
667
+ }
668
+ }
669
+ if (!page.hasMore) break;
670
+ revision = page.revision;
671
+ }
672
+ return results;
673
+ }
674
+ async function signApplePromotionalOffer(input, config) {
675
+ const { offerKeyId, offerPrivateKey } = config;
676
+ if (!offerKeyId || !offerPrivateKey) {
677
+ throw new Error("[onesub/apple] Promotional Offer signing requires config.apple.offerKeyId and config.apple.offerPrivateKey");
678
+ }
679
+ const nonce = randomUUID().toLowerCase();
680
+ const timestamp = Date.now();
681
+ const separator = "\u2063";
682
+ const message = [
683
+ input.bundleId,
684
+ offerKeyId,
685
+ input.productId,
686
+ input.offerId,
687
+ input.applicationUsername,
688
+ nonce,
689
+ String(timestamp)
690
+ ].join(separator);
691
+ if (!offerPrivateKey.includes("BEGIN") || !offerPrivateKey.includes("END")) {
692
+ throw new Error("[onesub/apple] offerPrivateKey does not appear to be PEM-encoded");
693
+ }
694
+ let signatureBuffer;
695
+ try {
696
+ const sign = createSign("SHA256");
697
+ sign.update(message);
698
+ sign.end();
699
+ signatureBuffer = sign.sign(offerPrivateKey);
700
+ } catch (err) {
701
+ throw new Error(`[onesub/apple] Failed to sign promotional offer \u2014 check that offerPrivateKey is a valid ES256 PEM key: ${err.message}`);
702
+ }
703
+ const signature = signatureBuffer.toString("base64");
704
+ return { keyId: offerKeyId, nonce, timestamp, signature };
705
+ }
619
706
  var GOOGLE_NOTIFICATION_TYPE = {
620
707
  SUBSCRIPTION_RECOVERED: 1,
621
708
  SUBSCRIPTION_RENEWED: 2,
@@ -678,8 +765,8 @@ async function getAccessToken(serviceAccountKey) {
678
765
  })
679
766
  ).toString("base64url");
680
767
  const signingInput = `${header}.${payload}`;
681
- const { createSign } = await import('crypto');
682
- const sign = createSign("RSA-SHA256");
768
+ const { createSign: createSign2 } = await import('crypto');
769
+ const sign = createSign2("RSA-SHA256");
683
770
  sign.update(signingInput);
684
771
  const signature = sign.sign(key.private_key, "base64url");
685
772
  const assertion = `${signingInput}.${signature}`;
@@ -951,6 +1038,22 @@ function decodeGoogleVoidedNotification(payload) {
951
1038
  packageName: notification.packageName
952
1039
  };
953
1040
  }
1041
+ function decodeGoogleOneTimeProductNotification(payload) {
1042
+ let notification;
1043
+ try {
1044
+ const json = Buffer.from(payload.message.data, "base64").toString("utf-8");
1045
+ notification = JSON.parse(json);
1046
+ } catch {
1047
+ return null;
1048
+ }
1049
+ if (!notification.oneTimeProductNotification) return null;
1050
+ const { notificationType, purchaseToken, sku } = notification.oneTimeProductNotification;
1051
+ if (notificationType !== 1 && notificationType !== 2) {
1052
+ log.warn("[onesub/google] Unknown oneTimeProductNotification type:", notificationType);
1053
+ return null;
1054
+ }
1055
+ return { notificationType, purchaseToken, sku, packageName: notification.packageName };
1056
+ }
954
1057
  function isGoogleActiveNotification(notificationType) {
955
1058
  return notificationType === GOOGLE_NOTIFICATION_TYPE.SUBSCRIPTION_PURCHASED || notificationType === GOOGLE_NOTIFICATION_TYPE.SUBSCRIPTION_RENEWED || notificationType === GOOGLE_NOTIFICATION_TYPE.SUBSCRIPTION_RECOVERED || notificationType === GOOGLE_NOTIFICATION_TYPE.SUBSCRIPTION_RESTARTED;
956
1059
  }
@@ -1126,7 +1229,9 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
1126
1229
  environment,
1127
1230
  status,
1128
1231
  willRenew,
1129
- expiresAt
1232
+ expiresAt,
1233
+ appAccountToken,
1234
+ inAppOwnershipType
1130
1235
  } = decoded;
1131
1236
  const notificationType = payload.notificationType;
1132
1237
  const subtype = payload.subtype;
@@ -1170,8 +1275,13 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
1170
1275
  if (existing) {
1171
1276
  const isSubscriptionRefund = isRefundOrRevoke && !isOneTimePurchase && notificationType !== "CONSUMPTION_REQUEST";
1172
1277
  const keepEntitlement = isSubscriptionRefund && config.refundPolicy === "until_expiry";
1173
- const updated = keepEntitlement ? { ...existing, willRenew: false } : {
1278
+ const correctedUserId = appAccountToken && existing.userId === originalTransactionId ? appAccountToken : existing.userId;
1279
+ if (correctedUserId !== existing.userId) {
1280
+ log.info("[onesub/webhook/apple] correcting userId from originalTransactionId to appAccountToken:", correctedUserId);
1281
+ }
1282
+ const updated = keepEntitlement ? { ...existing, userId: correctedUserId, willRenew: false } : {
1174
1283
  ...existing,
1284
+ userId: correctedUserId,
1175
1285
  status: finalStatus,
1176
1286
  willRenew,
1177
1287
  expiresAt: expiresAt ?? existing.expiresAt
@@ -1182,7 +1292,11 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
1182
1292
  sandbox: environment === "Sandbox"
1183
1293
  });
1184
1294
  if (fresh) {
1185
- fresh.userId = originalTransactionId;
1295
+ fresh.userId = appAccountToken ?? originalTransactionId;
1296
+ if (inAppOwnershipType === "FAMILY_SHARED") {
1297
+ const source = appAccountToken ? "appAccountToken" : "originalTransactionId (fallback)";
1298
+ log.info(`[onesub/webhook/apple] FAMILY_SHARED \u2014 userId: ${fresh.userId} (source: ${source})`);
1299
+ }
1186
1300
  await store.save(fresh);
1187
1301
  } else {
1188
1302
  log.warn(
@@ -1270,6 +1384,32 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
1270
1384
  }
1271
1385
  return;
1272
1386
  }
1387
+ const oneTimeProduct = decodeGoogleOneTimeProductNotification(body);
1388
+ if (oneTimeProduct) {
1389
+ if (config.google?.packageName && oneTimeProduct.packageName !== config.google.packageName) {
1390
+ log.warn("[onesub/webhook/google] oneTimeProduct package name mismatch:", oneTimeProduct.packageName, "!==", config.google.packageName);
1391
+ sendError(res, 400, ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
1392
+ return;
1393
+ }
1394
+ try {
1395
+ const { notificationType: notificationType2, purchaseToken: purchaseToken2, sku } = oneTimeProduct;
1396
+ if (notificationType2 === 1) {
1397
+ log.info("[onesub/webhook/google] oneTimeProduct PURCHASED:", sku);
1398
+ if (config.google?.serviceAccountKey && config.google.packageName) {
1399
+ void acknowledgeGoogleProduct(purchaseToken2, sku, config.google).catch(
1400
+ (err) => log.warn(`[onesub/webhook/google] oneTimeProduct ack failed for SKU ${sku} \u2014 3-day auto-refund risk:`, err)
1401
+ );
1402
+ }
1403
+ } else {
1404
+ log.info("[onesub/webhook/google] oneTimeProduct CANCELED (pre-completion):", sku);
1405
+ }
1406
+ res.status(200).json({ received: true });
1407
+ } catch (err) {
1408
+ log.error("[onesub/webhook/google] oneTimeProduct error:", err);
1409
+ sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process oneTimeProduct notification");
1410
+ }
1411
+ return;
1412
+ }
1273
1413
  const notification = decodeGoogleNotification(body);
1274
1414
  if (!notification) {
1275
1415
  res.status(200).json({ received: true });
@@ -1781,6 +1921,38 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
1781
1921
  sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, err.message ?? "customer error");
1782
1922
  }
1783
1923
  });
1924
+ const syncAppleParamsSchema = z.object({
1925
+ originalTransactionId: z.string().min(1).max(256)
1926
+ });
1927
+ router.post(ROUTES.ADMIN_SYNC_APPLE, async (req, res) => {
1928
+ let params;
1929
+ try {
1930
+ params = syncAppleParamsSchema.parse(req.params);
1931
+ } catch {
1932
+ sendError(res, 400, ONESUB_ERROR_CODE.INVALID_INPUT, "originalTransactionId required");
1933
+ return;
1934
+ }
1935
+ if (!config.apple?.issuerId || !config.apple?.keyId || !config.apple?.privateKey) {
1936
+ sendError(res, 400, ONESUB_ERROR_CODE.APPLE_CONFIG_MISSING, "Apple API credentials not configured");
1937
+ return;
1938
+ }
1939
+ try {
1940
+ const { originalTransactionId } = params;
1941
+ const existingForEnv = await store.getByTransactionId(originalTransactionId);
1942
+ const isSandbox = req.query["sandbox"] === "true" || existingForEnv?.environment === "Sandbox";
1943
+ const fresh = await fetchAppleSubscriptionStatus(originalTransactionId, config.apple, { sandbox: isSandbox });
1944
+ if (!fresh) {
1945
+ sendError(res, 404, ONESUB_ERROR_CODE.TRANSACTION_NOT_FOUND, "Apple Status API returned no record");
1946
+ return;
1947
+ }
1948
+ const existing = existingForEnv;
1949
+ fresh.userId = existing?.userId ?? originalTransactionId;
1950
+ await store.save(fresh);
1951
+ res.status(200).json({ ok: true, subscription: fresh });
1952
+ } catch (err) {
1953
+ sendError(res, 500, ONESUB_ERROR_CODE.INTERNAL_ERROR, err.message ?? "sync error");
1954
+ }
1955
+ });
1784
1956
  if (webhookQueue?.listDeadLetters) {
1785
1957
  router.get("/onesub/admin/webhook-deadletters", async (_req, res) => {
1786
1958
  try {
@@ -2026,6 +2198,55 @@ function createMetricsRouter(config, store, purchaseStore) {
2026
2198
  });
2027
2199
  return router;
2028
2200
  }
2201
+ var OFFER_SECRET_HEADER = "x-onesub-offer-secret";
2202
+ var offerBodySchema = z.object({
2203
+ productId: z.string().min(1).max(256),
2204
+ offerId: z.string().min(1).max(256),
2205
+ applicationUsername: z.string().min(1).max(256)
2206
+ });
2207
+ function createAppleOfferRouter(config) {
2208
+ const apple = config.apple;
2209
+ if (!apple?.offerKeyId || !apple?.offerPrivateKey) return null;
2210
+ const router = Router();
2211
+ router.post("/onesub/apple/offer-signature", async (req, res) => {
2212
+ if (config.adminSecret) {
2213
+ const provided = req.headers[OFFER_SECRET_HEADER];
2214
+ if (typeof provided !== "string" || provided !== config.adminSecret) {
2215
+ sendError(res, 401, ONESUB_ERROR_CODE.UNAUTHORIZED, "Unauthorized");
2216
+ return;
2217
+ }
2218
+ }
2219
+ let body;
2220
+ try {
2221
+ body = offerBodySchema.parse(req.body);
2222
+ } catch (err) {
2223
+ if (err instanceof z.ZodError) {
2224
+ sendZodError(res, err);
2225
+ return;
2226
+ }
2227
+ throw err;
2228
+ }
2229
+ if (!apple.bundleId) {
2230
+ sendError(res, 400, ONESUB_ERROR_CODE.APPLE_CONFIG_MISSING, "config.apple.bundleId is required for offer signing");
2231
+ return;
2232
+ }
2233
+ try {
2234
+ const result = await signApplePromotionalOffer(
2235
+ {
2236
+ bundleId: apple.bundleId,
2237
+ productId: body.productId,
2238
+ offerId: body.offerId,
2239
+ applicationUsername: body.applicationUsername
2240
+ },
2241
+ apple
2242
+ );
2243
+ res.status(200).json(result);
2244
+ } catch (err) {
2245
+ sendError(res, 500, ONESUB_ERROR_CODE.INTERNAL_ERROR, err.message ?? "Offer signing failed");
2246
+ }
2247
+ });
2248
+ return router;
2249
+ }
2029
2250
 
2030
2251
  // src/stores/schema.ts
2031
2252
  var SUBSCRIPTIONS_SCHEMA_SQL = `
@@ -3012,6 +3233,8 @@ function createOneSubMiddleware(config) {
3012
3233
  if (entitlementRouter) router.use(entitlementRouter);
3013
3234
  const metricsRouter = createMetricsRouter(config, store, purchaseStore);
3014
3235
  if (metricsRouter) router.use(metricsRouter);
3236
+ const appleOfferRouter = createAppleOfferRouter(config);
3237
+ if (appleOfferRouter) router.use(appleOfferRouter);
3015
3238
  return router;
3016
3239
  }
3017
3240
  function createOneSubServer(config) {
@@ -3047,6 +3270,6 @@ if (isMain) {
3047
3270
  });
3048
3271
  }
3049
3272
 
3050
- export { BullMQWebhookQueue, CacheWebhookEventStore, InMemoryCacheAdapter, InMemoryPurchaseStore, InMemorySubscriptionStore, InMemoryWebhookEventStore, InProcessWebhookQueue, ONESUB_OPENAPI, PostgresPurchaseStore, PostgresSubscriptionStore, RedisCacheAdapter, RedisPurchaseStore, RedisSubscriptionStore, RedisWebhookEventStore, createOneSubMiddleware, createOneSubServer, evaluateEntitlement, fetchAppleSubscriptionStatus, getDefaultCache, log, openapiHandler, setDefaultCache, setLogger, validateAppleReceipt, validateGoogleReceipt, withSpan };
3273
+ export { BullMQWebhookQueue, CacheWebhookEventStore, InMemoryCacheAdapter, InMemoryPurchaseStore, InMemorySubscriptionStore, InMemoryWebhookEventStore, InProcessWebhookQueue, ONESUB_OPENAPI, PostgresPurchaseStore, PostgresSubscriptionStore, RedisCacheAdapter, RedisPurchaseStore, RedisSubscriptionStore, RedisWebhookEventStore, createOneSubMiddleware, createOneSubServer, evaluateEntitlement, fetchAppleSubscriptionStatus, fetchAppleTransactionHistory, getDefaultCache, log, openapiHandler, setDefaultCache, setLogger, signApplePromotionalOffer, validateAppleReceipt, validateGoogleReceipt, withSpan };
3051
3274
  //# sourceMappingURL=index.js.map
3052
3275
  //# sourceMappingURL=index.js.map