@onesub/server 0.12.1 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/admin-customer-detail.test.js +146 -0
- package/dist/__tests__/admin-customer-detail.test.js.map +1 -0
- package/dist/__tests__/admin-list-subscriptions.test.js +192 -0
- package/dist/__tests__/admin-list-subscriptions.test.js.map +1 -0
- package/dist/__tests__/admin-subscription-detail.test.js +112 -0
- package/dist/__tests__/admin-subscription-detail.test.js.map +1 -0
- package/dist/__tests__/apple-root-ca.test.js +41 -0
- package/dist/__tests__/apple-root-ca.test.js.map +1 -0
- package/dist/__tests__/apple-status-api.test.js +345 -0
- package/dist/__tests__/apple-status-api.test.js.map +1 -0
- package/dist/__tests__/cache.test.js +34 -0
- package/dist/__tests__/cache.test.js.map +1 -0
- package/dist/__tests__/entitlements.test.js +268 -0
- package/dist/__tests__/entitlements.test.js.map +1 -0
- package/dist/__tests__/error-codes.test.js +173 -0
- package/dist/__tests__/error-codes.test.js.map +1 -0
- package/dist/__tests__/fetch-hardening.test.js +192 -0
- package/dist/__tests__/fetch-hardening.test.js.map +1 -0
- package/dist/__tests__/google-paused.test.js +126 -0
- package/dist/__tests__/google-paused.test.js.map +1 -0
- package/dist/__tests__/google-price-change.test.js +186 -0
- package/dist/__tests__/google-price-change.test.js.map +1 -0
- package/dist/__tests__/google-subscriptions-v2.test.js +206 -0
- package/dist/__tests__/google-subscriptions-v2.test.js.map +1 -0
- package/dist/__tests__/lifecycle-scenarios.test.js +579 -0
- package/dist/__tests__/lifecycle-scenarios.test.js.map +1 -0
- package/dist/__tests__/lifecycle-states.test.js +423 -0
- package/dist/__tests__/lifecycle-states.test.js.map +1 -0
- package/dist/__tests__/linked-purchase-token.test.js +227 -0
- package/dist/__tests__/linked-purchase-token.test.js.map +1 -0
- package/dist/__tests__/metrics.test.js +340 -0
- package/dist/__tests__/metrics.test.js.map +1 -0
- package/dist/__tests__/mock-provider.test.js +188 -0
- package/dist/__tests__/mock-provider.test.js.map +1 -0
- package/dist/__tests__/openapi.test.js +32 -0
- package/dist/__tests__/openapi.test.js.map +1 -0
- package/dist/__tests__/paused-auto-resume.test.js +162 -0
- package/dist/__tests__/paused-auto-resume.test.js.map +1 -0
- package/dist/__tests__/providers.test.js +321 -0
- package/dist/__tests__/providers.test.js.map +1 -0
- package/dist/__tests__/redis-store.test.js +166 -0
- package/dist/__tests__/redis-store.test.js.map +1 -0
- package/dist/__tests__/refund-policy.test.js +317 -0
- package/dist/__tests__/refund-policy.test.js.map +1 -0
- package/dist/__tests__/schema.test.js +46 -0
- package/dist/__tests__/schema.test.js.map +1 -0
- package/dist/__tests__/store.test.js +172 -0
- package/dist/__tests__/store.test.js.map +1 -0
- package/dist/__tests__/test-utils.js +30 -0
- package/dist/__tests__/test-utils.js.map +1 -0
- package/dist/__tests__/webhook-apple.test.js +247 -0
- package/dist/__tests__/webhook-apple.test.js.map +1 -0
- package/dist/__tests__/webhook-events.test.js +36 -0
- package/dist/__tests__/webhook-events.test.js.map +1 -0
- package/dist/__tests__/webhook-google.test.js +255 -0
- package/dist/__tests__/webhook-google.test.js.map +1 -0
- package/dist/__tests__/webhook-queue.test.js +26 -0
- package/dist/__tests__/webhook-queue.test.js.map +1 -0
- package/dist/__tests__/webhook-refund.test.js +313 -0
- package/dist/__tests__/webhook-refund.test.js.map +1 -0
- package/dist/cache.js +39 -0
- package/dist/cache.js.map +1 -0
- package/dist/errors.js +19 -0
- package/dist/errors.js.map +1 -0
- package/dist/http.js +36 -0
- package/dist/http.js.map +1 -0
- package/dist/index.cjs +241 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +241 -11
- package/dist/index.js.map +1 -1
- package/dist/logger.js +20 -0
- package/dist/logger.js.map +1 -0
- package/dist/openapi.js +196 -0
- package/dist/openapi.js.map +1 -0
- package/dist/providers/apple-root-ca.js +38 -0
- package/dist/providers/apple-root-ca.js.map +1 -0
- package/dist/providers/apple.d.ts +76 -0
- package/dist/providers/apple.d.ts.map +1 -1
- package/dist/providers/apple.js +630 -0
- package/dist/providers/apple.js.map +1 -0
- package/dist/providers/google.d.ts +25 -0
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +564 -0
- package/dist/providers/google.js.map +1 -0
- package/dist/providers/mock.js +93 -0
- package/dist/providers/mock.js.map +1 -0
- package/dist/routes/admin.d.ts.map +1 -1
- package/dist/routes/admin.js +327 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/apple-offer.d.ts +21 -0
- package/dist/routes/apple-offer.d.ts.map +1 -0
- package/dist/routes/apple-offer.js +72 -0
- package/dist/routes/apple-offer.js.map +1 -0
- package/dist/routes/entitlements.js +130 -0
- package/dist/routes/entitlements.js.map +1 -0
- package/dist/routes/metrics.js +281 -0
- package/dist/routes/metrics.js.map +1 -0
- package/dist/routes/purchase.d.ts.map +1 -1
- package/dist/routes/purchase.js +204 -0
- package/dist/routes/purchase.js.map +1 -0
- package/dist/routes/status.js +55 -0
- package/dist/routes/status.js.map +1 -0
- package/dist/routes/validate.js +70 -0
- package/dist/routes/validate.js.map +1 -0
- package/dist/routes/webhook-apple.d.ts.map +1 -1
- package/dist/routes/webhook-apple.js +145 -0
- package/dist/routes/webhook-apple.js.map +1 -0
- package/dist/routes/webhook-google.d.ts.map +1 -1
- package/dist/routes/webhook-google.js +204 -0
- package/dist/routes/webhook-google.js.map +1 -0
- package/dist/routes/webhook.js +19 -0
- package/dist/routes/webhook.js.map +1 -0
- package/dist/store.js +145 -0
- package/dist/store.js.map +1 -0
- package/dist/stores/postgres.js +329 -0
- package/dist/stores/postgres.js.map +1 -0
- package/dist/stores/redis.js +297 -0
- package/dist/stores/redis.js.map +1 -0
- package/dist/stores/schema.js +67 -0
- package/dist/stores/schema.js.map +1 -0
- package/dist/tracing.js +74 -0
- package/dist/tracing.js.map +1 -0
- package/dist/webhook-events.js +58 -0
- package/dist/webhook-events.js.map +1 -0
- package/dist/webhook-queue.js +108 -0
- package/dist/webhook-queue.js.map +1 -0
- package/package.json +4 -4
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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 =
|
|
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
|
|
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 });
|
|
@@ -1389,9 +1529,16 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
1389
1529
|
const { platform, receipt, userId, productId, type } = body;
|
|
1390
1530
|
try {
|
|
1391
1531
|
if (type === PURCHASE_TYPE.NON_CONSUMABLE) {
|
|
1392
|
-
const
|
|
1393
|
-
|
|
1394
|
-
|
|
1532
|
+
const owned = (await purchaseStore.getPurchasesByUserId(userId)).find(
|
|
1533
|
+
(p) => p.productId === productId
|
|
1534
|
+
);
|
|
1535
|
+
if (owned) {
|
|
1536
|
+
const response2 = {
|
|
1537
|
+
valid: true,
|
|
1538
|
+
purchase: { ...owned, userId },
|
|
1539
|
+
action: "restored"
|
|
1540
|
+
};
|
|
1541
|
+
res.status(200).json(response2);
|
|
1395
1542
|
return;
|
|
1396
1543
|
}
|
|
1397
1544
|
}
|
|
@@ -1781,6 +1928,38 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
1781
1928
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, err.message ?? "customer error");
|
|
1782
1929
|
}
|
|
1783
1930
|
});
|
|
1931
|
+
const syncAppleParamsSchema = z.object({
|
|
1932
|
+
originalTransactionId: z.string().min(1).max(256)
|
|
1933
|
+
});
|
|
1934
|
+
router.post(ROUTES.ADMIN_SYNC_APPLE, async (req, res) => {
|
|
1935
|
+
let params;
|
|
1936
|
+
try {
|
|
1937
|
+
params = syncAppleParamsSchema.parse(req.params);
|
|
1938
|
+
} catch {
|
|
1939
|
+
sendError(res, 400, 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, 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, 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, ONESUB_ERROR_CODE.INTERNAL_ERROR, err.message ?? "sync error");
|
|
1961
|
+
}
|
|
1962
|
+
});
|
|
1784
1963
|
if (webhookQueue?.listDeadLetters) {
|
|
1785
1964
|
router.get("/onesub/admin/webhook-deadletters", async (_req, res) => {
|
|
1786
1965
|
try {
|
|
@@ -2026,6 +2205,55 @@ function createMetricsRouter(config, store, purchaseStore) {
|
|
|
2026
2205
|
});
|
|
2027
2206
|
return router;
|
|
2028
2207
|
}
|
|
2208
|
+
var OFFER_SECRET_HEADER = "x-onesub-offer-secret";
|
|
2209
|
+
var offerBodySchema = z.object({
|
|
2210
|
+
productId: z.string().min(1).max(256),
|
|
2211
|
+
offerId: z.string().min(1).max(256),
|
|
2212
|
+
applicationUsername: 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 = 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, 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 z.ZodError) {
|
|
2231
|
+
sendZodError(res, err);
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
throw err;
|
|
2235
|
+
}
|
|
2236
|
+
if (!apple.bundleId) {
|
|
2237
|
+
sendError(res, 400, 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, ONESUB_ERROR_CODE.INTERNAL_ERROR, err.message ?? "Offer signing failed");
|
|
2253
|
+
}
|
|
2254
|
+
});
|
|
2255
|
+
return router;
|
|
2256
|
+
}
|
|
2029
2257
|
|
|
2030
2258
|
// src/stores/schema.ts
|
|
2031
2259
|
var SUBSCRIPTIONS_SCHEMA_SQL = `
|
|
@@ -3012,6 +3240,8 @@ function createOneSubMiddleware(config) {
|
|
|
3012
3240
|
if (entitlementRouter) router.use(entitlementRouter);
|
|
3013
3241
|
const metricsRouter = createMetricsRouter(config, store, purchaseStore);
|
|
3014
3242
|
if (metricsRouter) router.use(metricsRouter);
|
|
3243
|
+
const appleOfferRouter = createAppleOfferRouter(config);
|
|
3244
|
+
if (appleOfferRouter) router.use(appleOfferRouter);
|
|
3015
3245
|
return router;
|
|
3016
3246
|
}
|
|
3017
3247
|
function createOneSubServer(config) {
|
|
@@ -3047,6 +3277,6 @@ if (isMain) {
|
|
|
3047
3277
|
});
|
|
3048
3278
|
}
|
|
3049
3279
|
|
|
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 };
|
|
3280
|
+
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
3281
|
//# sourceMappingURL=index.js.map
|
|
3052
3282
|
//# sourceMappingURL=index.js.map
|