@onesub/server 0.25.0 → 0.26.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/__tests__/route-log-fields.test.d.ts +20 -0
- package/dist/__tests__/route-log-fields.test.d.ts.map +1 -0
- package/dist/apps.d.ts.map +1 -1
- package/dist/index.cjs +118 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +118 -68
- package/dist/index.js.map +1 -1
- package/dist/log-format.d.ts +2 -0
- package/dist/log-format.d.ts.map +1 -1
- package/dist/routes/admin.d.ts.map +1 -1
- package/dist/routes/purchase.d.ts.map +1 -1
- package/dist/routes/validate.d.ts.map +1 -1
- package/dist/routes/webhook-apple.d.ts.map +1 -1
- package/dist/routes/webhook-google.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the routes, stores and webhook handlers put on a log line.
|
|
3
|
+
*
|
|
4
|
+
* This is the half of the migration that delivers the point of it. `userId` does not
|
|
5
|
+
* exist in the provider layer — it arrives in a request body — so "show me every
|
|
6
|
+
* rejection for this user" only became answerable when these call sites moved.
|
|
7
|
+
* `provider-log-fields.test.ts` covers Apple and Google; this covers the other 54.
|
|
8
|
+
*
|
|
9
|
+
* Driven through HTTP with supertest rather than by calling handlers, because two of
|
|
10
|
+
* the values under test (`userId`, `bundleId`) are attacker-supplied through the
|
|
11
|
+
* request and the escaping only matters on that path. `bundleId` in particular is
|
|
12
|
+
* read out of an **unverified** JWS by `peekAppleBundleId`, which makes
|
|
13
|
+
* `apps.ts`'s "No app configured" line the most attacker-reachable log site in the
|
|
14
|
+
* package.
|
|
15
|
+
*
|
|
16
|
+
* Exact-line assertions again, for the reason given in `provider-log-fields.test.ts`:
|
|
17
|
+
* a substring match cannot tell a field from a sentence that mentions the value.
|
|
18
|
+
*/
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=route-log-fields.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-log-fields.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/route-log-fields.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG"}
|
package/dist/apps.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apps.d.ts","sourceRoot":"","sources":["../src/apps.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAG1E;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,eAAe,EAAE,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,eAAe,GAAG,SAAS,CAAC;IACjD;;;;OAIG;IACH,SAAS,CAAC,IAAI,EAAE,OAAO,GAAG,kBAAkB,CAAC;CAC9C;AAED,MAAM,WAAW,OAAO;IACtB,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClC,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CACtC;AAQD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,kBAAkB,GAAG,WAAW,CAMtE;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,kBAAkB,GAAG,WAAW,
|
|
1
|
+
{"version":3,"file":"apps.d.ts","sourceRoot":"","sources":["../src/apps.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAG1E;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,eAAe,EAAE,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,eAAe,GAAG,SAAS,CAAC;IACjD;;;;OAIG;IACH,SAAS,CAAC,IAAI,EAAE,OAAO,GAAG,kBAAkB,CAAC;CAC9C;AAED,MAAM,WAAW,OAAO;IACtB,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClC,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CACtC;AAQD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,kBAAkB,GAAG,WAAW,CAMtE;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,kBAAkB,GAAG,WAAW,CAiExE;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUjE"}
|
package/dist/index.cjs
CHANGED
|
@@ -449,7 +449,7 @@ function outcomePasses(outcome, tag) {
|
|
|
449
449
|
throw new Error(`[onesub/mock/${tag}] simulated upstream network error`);
|
|
450
450
|
}
|
|
451
451
|
if (outcome.kind === "valid" || outcome.kind === "sandbox") return true;
|
|
452
|
-
log.warn(
|
|
452
|
+
log.warn("[onesub/mock] receipt rejected", { provider: tag, outcome: outcome.kind });
|
|
453
453
|
return false;
|
|
454
454
|
}
|
|
455
455
|
function deterministicTransactionId(prefix, receipt) {
|
|
@@ -1501,7 +1501,10 @@ function buildAppRegistry(config) {
|
|
|
1501
1501
|
// that would validate one app's receipt against another's credentials.
|
|
1502
1502
|
(config.apple || config.google || apps.length === 1 ? apps[0] : void 0);
|
|
1503
1503
|
if (apps.length > 1) {
|
|
1504
|
-
log.info("[onesub] Multi-app mode
|
|
1504
|
+
log.info("[onesub] Multi-app mode", {
|
|
1505
|
+
appIds: apps.map(appName),
|
|
1506
|
+
defaultAppId: defaultApp ? appName(defaultApp) : null
|
|
1507
|
+
});
|
|
1505
1508
|
}
|
|
1506
1509
|
function match(hint) {
|
|
1507
1510
|
if (hint.appId) {
|
|
@@ -1509,13 +1512,13 @@ function buildAppRegistry(config) {
|
|
|
1509
1512
|
(a) => a.id === hint.appId || a.apple?.bundleId === hint.appId || a.google?.packageName === hint.appId
|
|
1510
1513
|
);
|
|
1511
1514
|
if (byId) return byId;
|
|
1512
|
-
log.warn("[onesub] Unknown appId
|
|
1515
|
+
log.warn("[onesub] Unknown appId", { appId: hint.appId });
|
|
1513
1516
|
return void 0;
|
|
1514
1517
|
}
|
|
1515
1518
|
if (hint.bundleId) {
|
|
1516
1519
|
const byBundle = apps.find((a) => a.apple?.bundleId === hint.bundleId);
|
|
1517
1520
|
if (byBundle) return byBundle;
|
|
1518
|
-
log.warn("[onesub] No app configured for bundleId
|
|
1521
|
+
log.warn("[onesub] No app configured for bundleId", { bundleId: hint.bundleId });
|
|
1519
1522
|
return void 0;
|
|
1520
1523
|
}
|
|
1521
1524
|
return defaultApp;
|
|
@@ -1603,9 +1606,10 @@ function createValidateRouter(config, store) {
|
|
|
1603
1606
|
delete sub.boundAccountId;
|
|
1604
1607
|
const bindingMismatch = platform === "apple" ? boundAccountId && boundAccountId.toLowerCase() !== userId.toLowerCase() : boundAccountId && boundAccountId !== userId;
|
|
1605
1608
|
if (bindingMismatch) {
|
|
1606
|
-
log.warn(
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
+
log.warn("[onesub/validate] account binding mismatch \u2014 receipt token does not match userId", {
|
|
1610
|
+
originalTransactionId: sub.originalTransactionId,
|
|
1611
|
+
userId
|
|
1612
|
+
});
|
|
1609
1613
|
sendError(
|
|
1610
1614
|
res,
|
|
1611
1615
|
409,
|
|
@@ -1618,7 +1622,7 @@ function createValidateRouter(config, store) {
|
|
|
1618
1622
|
const isSandbox = sub.sandbox === true;
|
|
1619
1623
|
delete sub.sandbox;
|
|
1620
1624
|
if (isSandbox && getTestOverride(userId) === false) {
|
|
1621
|
-
log.warn(
|
|
1625
|
+
log.warn("[onesub/validate] sandbox test override active \u2014 forcing not-entitled", { userId });
|
|
1622
1626
|
sub.status = shared.SUBSCRIPTION_STATUS.EXPIRED;
|
|
1623
1627
|
sub.willRenew = false;
|
|
1624
1628
|
}
|
|
@@ -1630,7 +1634,7 @@ function createValidateRouter(config, store) {
|
|
|
1630
1634
|
const response = { valid: true, subscription: sub };
|
|
1631
1635
|
res.status(200).json(response);
|
|
1632
1636
|
} catch (err2) {
|
|
1633
|
-
log.error("[onesub/validate] Unexpected error
|
|
1637
|
+
log.error("[onesub/validate] Unexpected error", { userId, productId, platform, err: err2 });
|
|
1634
1638
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.INTERNAL_ERROR, "Internal server error during receipt validation", NO_SUB);
|
|
1635
1639
|
}
|
|
1636
1640
|
});
|
|
@@ -1662,7 +1666,7 @@ function createStatusRouter(store) {
|
|
|
1662
1666
|
const response = { active, subscription: sub };
|
|
1663
1667
|
res.status(200).json(response);
|
|
1664
1668
|
} catch (err2) {
|
|
1665
|
-
log.error("[onesub/status] Store error
|
|
1669
|
+
log.error("[onesub/status] Store error", { userId, err: err2 });
|
|
1666
1670
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error", NO_SUB2);
|
|
1667
1671
|
}
|
|
1668
1672
|
});
|
|
@@ -1770,7 +1774,7 @@ async function processAppleNotification(work, config, store, purchaseStore) {
|
|
|
1770
1774
|
});
|
|
1771
1775
|
}
|
|
1772
1776
|
} catch (err2) {
|
|
1773
|
-
log.warn("[onesub/webhook/apple] consumptionInfoProvider failed
|
|
1777
|
+
log.warn("[onesub/webhook/apple] consumptionInfoProvider failed", { transactionId, err: err2 });
|
|
1774
1778
|
}
|
|
1775
1779
|
})();
|
|
1776
1780
|
}
|
|
@@ -1783,7 +1787,7 @@ async function processAppleNotification(work, config, store, purchaseStore) {
|
|
|
1783
1787
|
const lookupId = transactionId ?? originalTransactionId;
|
|
1784
1788
|
const removed = await purchaseStore.deletePurchaseByTransactionId(lookupId);
|
|
1785
1789
|
if (!removed) {
|
|
1786
|
-
log.warn("[onesub/webhook/apple] IAP refund for unknown transaction
|
|
1790
|
+
log.warn("[onesub/webhook/apple] IAP refund for unknown transaction", { transactionId: lookupId });
|
|
1787
1791
|
}
|
|
1788
1792
|
return;
|
|
1789
1793
|
}
|
|
@@ -1793,7 +1797,10 @@ async function processAppleNotification(work, config, store, purchaseStore) {
|
|
|
1793
1797
|
const keepEntitlement = isSubscriptionRefund && config.refundPolicy === "until_expiry";
|
|
1794
1798
|
const correctedUserId = appAccountToken && existing.userId === originalTransactionId ? appAccountToken : existing.userId;
|
|
1795
1799
|
if (correctedUserId !== existing.userId) {
|
|
1796
|
-
log.info("[onesub/webhook/apple] correcting userId from originalTransactionId to appAccountToken
|
|
1800
|
+
log.info("[onesub/webhook/apple] correcting userId from originalTransactionId to appAccountToken", {
|
|
1801
|
+
originalTransactionId,
|
|
1802
|
+
userId: correctedUserId
|
|
1803
|
+
});
|
|
1797
1804
|
}
|
|
1798
1805
|
const updated = keepEntitlement ? { ...existing, userId: correctedUserId, willRenew: false } : {
|
|
1799
1806
|
...existing,
|
|
@@ -1811,20 +1818,22 @@ async function processAppleNotification(work, config, store, purchaseStore) {
|
|
|
1811
1818
|
fresh.userId = appAccountToken ?? originalTransactionId;
|
|
1812
1819
|
if (inAppOwnershipType === "FAMILY_SHARED") {
|
|
1813
1820
|
const source = appAccountToken ? "appAccountToken" : "originalTransactionId (fallback)";
|
|
1814
|
-
log.info(
|
|
1821
|
+
log.info("[onesub/webhook/apple] FAMILY_SHARED", {
|
|
1822
|
+
originalTransactionId,
|
|
1823
|
+
userId: fresh.userId,
|
|
1824
|
+
userIdSource: source
|
|
1825
|
+
});
|
|
1815
1826
|
}
|
|
1816
1827
|
await store.save(fresh);
|
|
1817
1828
|
} else {
|
|
1818
|
-
log.warn(
|
|
1819
|
-
"[onesub/webhook/apple] Unknown transaction and Status API returned no record:",
|
|
1829
|
+
log.warn("[onesub/webhook/apple] Unknown transaction and Status API returned no record", {
|
|
1820
1830
|
originalTransactionId
|
|
1821
|
-
);
|
|
1831
|
+
});
|
|
1822
1832
|
}
|
|
1823
1833
|
} else {
|
|
1824
|
-
log.warn(
|
|
1825
|
-
"[onesub/webhook/apple] Received notification for unknown transaction (no API creds to recover):",
|
|
1834
|
+
log.warn("[onesub/webhook/apple] Received notification for unknown transaction (no API creds to recover)", {
|
|
1826
1835
|
originalTransactionId
|
|
1827
|
-
);
|
|
1836
|
+
});
|
|
1828
1837
|
}
|
|
1829
1838
|
}
|
|
1830
1839
|
async function handleAppleWebhook(req, res, config, store, purchaseStore, webhookEventStore, webhookQueue) {
|
|
@@ -1840,7 +1849,7 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1840
1849
|
config.apple?.skipJwsVerification
|
|
1841
1850
|
);
|
|
1842
1851
|
} catch (err2) {
|
|
1843
|
-
log.error("[onesub/webhook/apple] Failed to decode signedPayload
|
|
1852
|
+
log.error("[onesub/webhook/apple] Failed to decode signedPayload", { err: err2 });
|
|
1844
1853
|
sendError(res, 400, shared.ONESUB_ERROR_CODE.INVALID_SIGNED_PAYLOAD, "Invalid signedPayload");
|
|
1845
1854
|
return;
|
|
1846
1855
|
}
|
|
@@ -1848,7 +1857,9 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1848
1857
|
if (webhookEventStore && typeof payload.notificationUUID === "string") {
|
|
1849
1858
|
const fresh = await webhookEventStore.markIfNew("apple", payload.notificationUUID);
|
|
1850
1859
|
if (!fresh) {
|
|
1851
|
-
log.info("[onesub/webhook/apple] dedupe: already processed",
|
|
1860
|
+
log.info("[onesub/webhook/apple] dedupe: already processed", {
|
|
1861
|
+
notificationUUID: payload.notificationUUID
|
|
1862
|
+
});
|
|
1852
1863
|
res.status(200).json({ received: true, deduped: true });
|
|
1853
1864
|
return;
|
|
1854
1865
|
}
|
|
@@ -1861,7 +1872,7 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1861
1872
|
}
|
|
1862
1873
|
const notifiedApp = getAppRegistry(config).configFor({ bundleId: decoded.bundleId });
|
|
1863
1874
|
if (decoded.bundleId && !notifiedApp.apple) {
|
|
1864
|
-
log.warn("[onesub/webhook/apple] No app configured for bundleId
|
|
1875
|
+
log.warn("[onesub/webhook/apple] No app configured for bundleId", { bundleId: decoded.bundleId });
|
|
1865
1876
|
sendError(res, 400, shared.ONESUB_ERROR_CODE.BUNDLE_ID_MISMATCH, "Bundle ID mismatch");
|
|
1866
1877
|
return;
|
|
1867
1878
|
}
|
|
@@ -1881,7 +1892,10 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1881
1892
|
});
|
|
1882
1893
|
res.status(200).json({ received: true, queued: true });
|
|
1883
1894
|
} catch (err2) {
|
|
1884
|
-
log.error("[onesub/webhook/apple] Failed to enqueue webhook job
|
|
1895
|
+
log.error("[onesub/webhook/apple] Failed to enqueue webhook job", {
|
|
1896
|
+
notificationUUID: payload.notificationUUID,
|
|
1897
|
+
err: err2
|
|
1898
|
+
});
|
|
1885
1899
|
await unmarkWebhookEvent(webhookEventStore, "apple", markedEventId);
|
|
1886
1900
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to update subscription");
|
|
1887
1901
|
}
|
|
@@ -1891,7 +1905,10 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1891
1905
|
await processAppleNotification(work, config, store, purchaseStore);
|
|
1892
1906
|
res.status(200).json({ received: true });
|
|
1893
1907
|
} catch (err2) {
|
|
1894
|
-
log.error("[onesub/webhook/apple] Store update error
|
|
1908
|
+
log.error("[onesub/webhook/apple] Store update error", {
|
|
1909
|
+
notificationUUID: payload.notificationUUID,
|
|
1910
|
+
err: err2
|
|
1911
|
+
});
|
|
1895
1912
|
await unmarkWebhookEvent(webhookEventStore, "apple", markedEventId);
|
|
1896
1913
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to update subscription");
|
|
1897
1914
|
}
|
|
@@ -1926,15 +1943,15 @@ async function verifyGooglePushToken(req, expectedAudience, expectedServiceAccou
|
|
|
1926
1943
|
}
|
|
1927
1944
|
var GOOGLE_FAILURE_MESSAGES = {
|
|
1928
1945
|
voided: {
|
|
1929
|
-
logPrefix: "[onesub/webhook/google] voided notification error
|
|
1946
|
+
logPrefix: "[onesub/webhook/google] voided notification error",
|
|
1930
1947
|
message: "Failed to process voided notification"
|
|
1931
1948
|
},
|
|
1932
1949
|
oneTimeProduct: {
|
|
1933
|
-
logPrefix: "[onesub/webhook/google] oneTimeProduct error
|
|
1950
|
+
logPrefix: "[onesub/webhook/google] oneTimeProduct error",
|
|
1934
1951
|
message: "Failed to process oneTimeProduct notification"
|
|
1935
1952
|
},
|
|
1936
1953
|
subscription: {
|
|
1937
|
-
logPrefix: "[onesub/webhook/google] Error handling notification
|
|
1954
|
+
logPrefix: "[onesub/webhook/google] Error handling notification",
|
|
1938
1955
|
message: "Failed to process notification"
|
|
1939
1956
|
}
|
|
1940
1957
|
};
|
|
@@ -1976,12 +1993,14 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
1976
1993
|
const updated = config.refundPolicy === "until_expiry" ? { ...existing2, willRenew: false } : { ...existing2, status: shared.SUBSCRIPTION_STATUS.CANCELED };
|
|
1977
1994
|
await store.save(updated);
|
|
1978
1995
|
} else {
|
|
1979
|
-
log.warn("[onesub/webhook/google] voided subscription for unknown purchaseToken
|
|
1996
|
+
log.warn("[onesub/webhook/google] voided subscription for unknown purchaseToken", {
|
|
1997
|
+
purchaseToken: voided.purchaseToken
|
|
1998
|
+
});
|
|
1980
1999
|
}
|
|
1981
2000
|
} else {
|
|
1982
2001
|
const removed = await purchaseStore.deletePurchaseByTransactionId(voided.orderId);
|
|
1983
2002
|
if (!removed) {
|
|
1984
|
-
log.warn("[onesub/webhook/google] voided IAP for unknown orderId
|
|
2003
|
+
log.warn("[onesub/webhook/google] voided IAP for unknown orderId", { orderId: voided.orderId });
|
|
1985
2004
|
}
|
|
1986
2005
|
}
|
|
1987
2006
|
return;
|
|
@@ -1989,15 +2008,18 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
1989
2008
|
if (work.kind === "oneTimeProduct") {
|
|
1990
2009
|
const { notificationType: notificationType2, purchaseToken: purchaseToken2, sku } = work.oneTimeProduct;
|
|
1991
2010
|
if (notificationType2 === 1) {
|
|
1992
|
-
log.info("[onesub/webhook/google] oneTimeProduct PURCHASED
|
|
2011
|
+
log.info("[onesub/webhook/google] oneTimeProduct PURCHASED", { productId: sku });
|
|
1993
2012
|
const googleCfg = googleFor(work.oneTimeProduct.packageName);
|
|
1994
2013
|
if (googleCfg?.serviceAccountKey && googleCfg.packageName) {
|
|
1995
2014
|
void acknowledgeGoogleProduct(purchaseToken2, sku, googleCfg).catch(
|
|
1996
|
-
(err2) => log.warn(
|
|
2015
|
+
(err2) => log.warn("[onesub/webhook/google] oneTimeProduct ack failed \u2014 3-day auto-refund risk", {
|
|
2016
|
+
productId: sku,
|
|
2017
|
+
err: err2
|
|
2018
|
+
})
|
|
1997
2019
|
);
|
|
1998
2020
|
}
|
|
1999
2021
|
} else {
|
|
2000
|
-
log.info("[onesub/webhook/google] oneTimeProduct CANCELED (pre-completion)
|
|
2022
|
+
log.info("[onesub/webhook/google] oneTimeProduct CANCELED (pre-completion)", { productId: sku });
|
|
2001
2023
|
}
|
|
2002
2024
|
return;
|
|
2003
2025
|
}
|
|
@@ -2024,7 +2046,7 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
2024
2046
|
const subGoogleCfg = googleFor(packageName);
|
|
2025
2047
|
if (isGooglePriceChangeConfirmedNotification(notificationType) && subGoogleCfg?.onPriceChangeConfirmed) {
|
|
2026
2048
|
const hook = subGoogleCfg.onPriceChangeConfirmed;
|
|
2027
|
-
void Promise.resolve().then(() => hook({ purchaseToken, subscriptionId, packageName })).catch((err2) => log.warn("[onesub/webhook/google] onPriceChangeConfirmed hook failed
|
|
2049
|
+
void Promise.resolve().then(() => hook({ purchaseToken, subscriptionId, packageName })).catch((err2) => log.warn("[onesub/webhook/google] onPriceChangeConfirmed hook failed", { err: err2 }));
|
|
2028
2050
|
}
|
|
2029
2051
|
if (existing) {
|
|
2030
2052
|
let updated = { ...existing, status: finalStatus };
|
|
@@ -2053,7 +2075,11 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
2053
2075
|
const previous = await store.getByTransactionId(fresh.linkedPurchaseToken);
|
|
2054
2076
|
fresh.userId = previous ? previous.userId : boundAccountId ?? purchaseToken;
|
|
2055
2077
|
if (previous) {
|
|
2056
|
-
log.info(
|
|
2078
|
+
log.info("[onesub/webhook/google] inherited userId from linkedPurchaseToken", {
|
|
2079
|
+
userId: previous.userId,
|
|
2080
|
+
linkedPurchaseToken: fresh.linkedPurchaseToken,
|
|
2081
|
+
purchaseToken
|
|
2082
|
+
});
|
|
2057
2083
|
}
|
|
2058
2084
|
} else {
|
|
2059
2085
|
fresh.userId = boundAccountId ?? purchaseToken;
|
|
@@ -2061,7 +2087,9 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
2061
2087
|
await store.save(fresh);
|
|
2062
2088
|
}
|
|
2063
2089
|
} else {
|
|
2064
|
-
log.warn("[onesub/webhook/google] Unknown purchase token and no serviceAccountKey to re-fetch
|
|
2090
|
+
log.warn("[onesub/webhook/google] Unknown purchase token and no serviceAccountKey to re-fetch", {
|
|
2091
|
+
purchaseToken
|
|
2092
|
+
});
|
|
2065
2093
|
}
|
|
2066
2094
|
}
|
|
2067
2095
|
}
|
|
@@ -2089,7 +2117,9 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2089
2117
|
if (webhookEventStore && typeof body.message.messageId === "string") {
|
|
2090
2118
|
const fresh = await webhookEventStore.markIfNew("google", body.message.messageId);
|
|
2091
2119
|
if (!fresh) {
|
|
2092
|
-
log.info("[onesub/webhook/google] dedupe: already processed",
|
|
2120
|
+
log.info("[onesub/webhook/google] dedupe: already processed", {
|
|
2121
|
+
messageId: body.message.messageId
|
|
2122
|
+
});
|
|
2093
2123
|
res.status(200).json({ received: true, deduped: true });
|
|
2094
2124
|
return;
|
|
2095
2125
|
}
|
|
@@ -2100,7 +2130,9 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2100
2130
|
const voided = decodeGoogleVoidedNotification(body);
|
|
2101
2131
|
if (voided) {
|
|
2102
2132
|
if (!servesPackage(voided.packageName)) {
|
|
2103
|
-
log.warn("[onesub/webhook/google] voided package name not served
|
|
2133
|
+
log.warn("[onesub/webhook/google] voided package name not served", {
|
|
2134
|
+
packageName: voided.packageName
|
|
2135
|
+
});
|
|
2104
2136
|
sendError(res, 400, shared.ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
|
|
2105
2137
|
return;
|
|
2106
2138
|
}
|
|
@@ -2109,7 +2141,9 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2109
2141
|
const oneTimeProduct = decodeGoogleOneTimeProductNotification(body);
|
|
2110
2142
|
if (oneTimeProduct) {
|
|
2111
2143
|
if (!servesPackage(oneTimeProduct.packageName)) {
|
|
2112
|
-
log.warn("[onesub/webhook/google] oneTimeProduct package name not served
|
|
2144
|
+
log.warn("[onesub/webhook/google] oneTimeProduct package name not served", {
|
|
2145
|
+
packageName: oneTimeProduct.packageName
|
|
2146
|
+
});
|
|
2113
2147
|
sendError(res, 400, shared.ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
|
|
2114
2148
|
return;
|
|
2115
2149
|
}
|
|
@@ -2121,7 +2155,9 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2121
2155
|
return;
|
|
2122
2156
|
}
|
|
2123
2157
|
if (!servesPackage(notification.packageName)) {
|
|
2124
|
-
log.warn("[onesub/webhook/google] Package name not served
|
|
2158
|
+
log.warn("[onesub/webhook/google] Package name not served", {
|
|
2159
|
+
packageName: notification.packageName
|
|
2160
|
+
});
|
|
2125
2161
|
sendError(res, 400, shared.ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
|
|
2126
2162
|
return;
|
|
2127
2163
|
}
|
|
@@ -2139,7 +2175,11 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2139
2175
|
});
|
|
2140
2176
|
res.status(200).json({ received: true, queued: true });
|
|
2141
2177
|
} catch (err2) {
|
|
2142
|
-
log.error("[onesub/webhook/google] Failed to enqueue webhook job
|
|
2178
|
+
log.error("[onesub/webhook/google] Failed to enqueue webhook job", {
|
|
2179
|
+
kind: work.kind,
|
|
2180
|
+
messageId: body.message.messageId,
|
|
2181
|
+
err: err2
|
|
2182
|
+
});
|
|
2143
2183
|
await unmarkWebhookEvent(webhookEventStore, "google", markedEventId);
|
|
2144
2184
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, GOOGLE_FAILURE_MESSAGES[work.kind].message);
|
|
2145
2185
|
}
|
|
@@ -2150,7 +2190,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2150
2190
|
res.status(200).json({ received: true });
|
|
2151
2191
|
} catch (err2) {
|
|
2152
2192
|
const { logPrefix, message } = GOOGLE_FAILURE_MESSAGES[work.kind];
|
|
2153
|
-
log.error(logPrefix, err2);
|
|
2193
|
+
log.error(logPrefix, { kind: work.kind, err: err2 });
|
|
2154
2194
|
await unmarkWebhookEvent(webhookEventStore, "google", markedEventId);
|
|
2155
2195
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, message);
|
|
2156
2196
|
}
|
|
@@ -2262,9 +2302,10 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2262
2302
|
}
|
|
2263
2303
|
const bindingMismatch = platform === "apple" ? boundAccountId && boundAccountId.toLowerCase() !== userId.toLowerCase() : boundAccountId && boundAccountId !== userId;
|
|
2264
2304
|
if (bindingMismatch) {
|
|
2265
|
-
log.warn(
|
|
2266
|
-
|
|
2267
|
-
|
|
2305
|
+
log.warn("[onesub/purchase] account binding mismatch \u2014 token does not match userId", {
|
|
2306
|
+
transactionId,
|
|
2307
|
+
userId
|
|
2308
|
+
});
|
|
2268
2309
|
sendError(
|
|
2269
2310
|
res,
|
|
2270
2311
|
409,
|
|
@@ -2281,9 +2322,11 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2281
2322
|
if (existing.userId !== userId) {
|
|
2282
2323
|
if (type === shared.PURCHASE_TYPE.NON_CONSUMABLE) {
|
|
2283
2324
|
await purchaseStore.reassignPurchase(transactionId, userId);
|
|
2284
|
-
log.info(
|
|
2285
|
-
|
|
2286
|
-
|
|
2325
|
+
log.info("[onesub/purchase] reassigned transaction to a new user", {
|
|
2326
|
+
transactionId,
|
|
2327
|
+
fromUserId: existing.userId,
|
|
2328
|
+
userId
|
|
2329
|
+
});
|
|
2287
2330
|
} else {
|
|
2288
2331
|
sendError(
|
|
2289
2332
|
res,
|
|
@@ -2327,7 +2370,7 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2327
2370
|
};
|
|
2328
2371
|
res.status(200).json(response);
|
|
2329
2372
|
} catch (err2) {
|
|
2330
|
-
log.error("[onesub/purchase/validate] Unexpected error
|
|
2373
|
+
log.error("[onesub/purchase/validate] Unexpected error", { userId, productId, platform, type, err: err2 });
|
|
2331
2374
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.INTERNAL_ERROR, "Internal server error during purchase validation", NO_PURCHASE);
|
|
2332
2375
|
}
|
|
2333
2376
|
});
|
|
@@ -2342,7 +2385,7 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2342
2385
|
const response = { purchases };
|
|
2343
2386
|
res.status(200).json(response);
|
|
2344
2387
|
} catch (err2) {
|
|
2345
|
-
log.error("[onesub/purchase/status] Store error
|
|
2388
|
+
log.error("[onesub/purchase/status] Store error", { userId, productId, err: err2 });
|
|
2346
2389
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error", { purchases: [] });
|
|
2347
2390
|
}
|
|
2348
2391
|
});
|
|
@@ -2407,7 +2450,7 @@ function createEntitlementRouter(config, store, purchaseStore) {
|
|
|
2407
2450
|
const response = { id, ...status };
|
|
2408
2451
|
res.status(200).json(response);
|
|
2409
2452
|
} catch (err2) {
|
|
2410
|
-
log.error("[onesub/entitlement] evaluation error
|
|
2453
|
+
log.error("[onesub/entitlement] evaluation error", { userId, entitlement: id, err: err2 });
|
|
2411
2454
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2412
2455
|
}
|
|
2413
2456
|
});
|
|
@@ -2431,7 +2474,7 @@ function createEntitlementRouter(config, store, purchaseStore) {
|
|
|
2431
2474
|
};
|
|
2432
2475
|
res.status(200).json(response);
|
|
2433
2476
|
} catch (err2) {
|
|
2434
|
-
log.error("[onesub/entitlements] evaluation error
|
|
2477
|
+
log.error("[onesub/entitlements] evaluation error", { userId, err: err2 });
|
|
2435
2478
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error", { entitlements: {} });
|
|
2436
2479
|
}
|
|
2437
2480
|
});
|
|
@@ -2545,7 +2588,7 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2545
2588
|
};
|
|
2546
2589
|
res.status(200).json(response);
|
|
2547
2590
|
} catch (err2) {
|
|
2548
|
-
log.error("[onesub/admin/subscriptions] list error
|
|
2591
|
+
log.error("[onesub/admin/subscriptions] list error", { err: err2 });
|
|
2549
2592
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2550
2593
|
}
|
|
2551
2594
|
});
|
|
@@ -2565,7 +2608,10 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2565
2608
|
}
|
|
2566
2609
|
res.status(200).json(sub);
|
|
2567
2610
|
} catch (err2) {
|
|
2568
|
-
log.error("[onesub/admin/subscriptions/:transactionId] detail error
|
|
2611
|
+
log.error("[onesub/admin/subscriptions/:transactionId] detail error", {
|
|
2612
|
+
transactionId: params.transactionId,
|
|
2613
|
+
err: err2
|
|
2614
|
+
});
|
|
2569
2615
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2570
2616
|
}
|
|
2571
2617
|
});
|
|
@@ -2598,7 +2644,7 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2598
2644
|
};
|
|
2599
2645
|
res.status(200).json(response);
|
|
2600
2646
|
} catch (err2) {
|
|
2601
|
-
log.error("[onesub/admin/customers/:userId] error
|
|
2647
|
+
log.error("[onesub/admin/customers/:userId] error", { userId: params.userId, err: err2 });
|
|
2602
2648
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2603
2649
|
}
|
|
2604
2650
|
});
|
|
@@ -2628,7 +2674,10 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2628
2674
|
await store.save(fresh);
|
|
2629
2675
|
res.status(200).json({ ok: true, subscription: fresh });
|
|
2630
2676
|
} catch (err2) {
|
|
2631
|
-
log.error("[onesub/admin/sync-apple] error
|
|
2677
|
+
log.error("[onesub/admin/sync-apple] error", {
|
|
2678
|
+
originalTransactionId: params.originalTransactionId,
|
|
2679
|
+
err: err2
|
|
2680
|
+
});
|
|
2632
2681
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.INTERNAL_ERROR, "Internal server error");
|
|
2633
2682
|
}
|
|
2634
2683
|
});
|
|
@@ -2643,9 +2692,10 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2643
2692
|
const body = parseOrSend(res, overrideBodySchema, req.body);
|
|
2644
2693
|
if (!body) return;
|
|
2645
2694
|
setTestOverride(params.userId, body.entitled);
|
|
2646
|
-
log.warn(
|
|
2647
|
-
|
|
2648
|
-
|
|
2695
|
+
log.warn("[onesub/admin] sandbox test override set", {
|
|
2696
|
+
userId: params.userId,
|
|
2697
|
+
entitled: body.entitled
|
|
2698
|
+
});
|
|
2649
2699
|
res.json({ ok: true, userId: params.userId, entitled: body.entitled, sandboxOnly: true });
|
|
2650
2700
|
});
|
|
2651
2701
|
router.delete(shared.ROUTES.ADMIN_TEST_OVERRIDE, (req, res) => {
|
|
@@ -2662,7 +2712,7 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2662
2712
|
const items = await webhookQueue.listDeadLetters();
|
|
2663
2713
|
res.status(200).json({ items });
|
|
2664
2714
|
} catch (err2) {
|
|
2665
|
-
log.error("[onesub/admin/webhook-deadletters] error
|
|
2715
|
+
log.error("[onesub/admin/webhook-deadletters] error", { err: err2 });
|
|
2666
2716
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2667
2717
|
}
|
|
2668
2718
|
});
|
|
@@ -2676,7 +2726,7 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2676
2726
|
await webhookQueue.replayDeadLetter(params.id);
|
|
2677
2727
|
res.status(200).json({ ok: true });
|
|
2678
2728
|
} catch (err2) {
|
|
2679
|
-
log.error("[onesub/admin/webhook-replay] error
|
|
2729
|
+
log.error("[onesub/admin/webhook-replay] error", { deadLetterId: params.id, err: err2 });
|
|
2680
2730
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2681
2731
|
}
|
|
2682
2732
|
});
|
|
@@ -2846,7 +2896,7 @@ function createMetricsRouter(config, store, purchaseStore) {
|
|
|
2846
2896
|
);
|
|
2847
2897
|
res.status(200).json(response);
|
|
2848
2898
|
} catch (err2) {
|
|
2849
|
-
log.error("[onesub/metrics/active] error
|
|
2899
|
+
log.error("[onesub/metrics/active] error", { err: err2 });
|
|
2850
2900
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2851
2901
|
}
|
|
2852
2902
|
});
|
|
@@ -2913,7 +2963,7 @@ function createMetricsRouter(config, store, purchaseStore) {
|
|
|
2913
2963
|
};
|
|
2914
2964
|
res.status(200).json(response);
|
|
2915
2965
|
} catch (err2) {
|
|
2916
|
-
log.error(
|
|
2966
|
+
log.error("[onesub/metrics] error", { route: name, err: err2 });
|
|
2917
2967
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2918
2968
|
}
|
|
2919
2969
|
};
|
|
@@ -2985,7 +3035,7 @@ function createAppleOfferRouter(config) {
|
|
|
2985
3035
|
);
|
|
2986
3036
|
res.status(200).json(result);
|
|
2987
3037
|
} catch (err2) {
|
|
2988
|
-
log.error("[onesub/apple/offer] signing error
|
|
3038
|
+
log.error("[onesub/apple/offer] signing error", { productId: body.productId, err: err2 });
|
|
2989
3039
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.INTERNAL_ERROR, "Offer signing failed");
|
|
2990
3040
|
}
|
|
2991
3041
|
});
|
|
@@ -3057,7 +3107,7 @@ async function createPgPool(connectionString, label) {
|
|
|
3057
3107
|
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
3058
3108
|
const pool = new Pool({ connectionString, max: 10 });
|
|
3059
3109
|
pool.on("error", (err2) => {
|
|
3060
|
-
log.error(
|
|
3110
|
+
log.error("[onesub] Postgres pool error (idle client)", { store: label, err: err2 });
|
|
3061
3111
|
});
|
|
3062
3112
|
return pool;
|
|
3063
3113
|
}
|
|
@@ -3798,7 +3848,7 @@ var BullMQWebhookQueue = class {
|
|
|
3798
3848
|
const { Queue } = await this.getBullMQ();
|
|
3799
3849
|
const queue = new Queue(this.queueName, { connection: this.connection });
|
|
3800
3850
|
queue.on?.("error", (err2) => {
|
|
3801
|
-
log.error("[onesub] BullMQ queue error
|
|
3851
|
+
log.error("[onesub] BullMQ queue error", { err: err2 });
|
|
3802
3852
|
});
|
|
3803
3853
|
return queue;
|
|
3804
3854
|
})();
|
|
@@ -3822,13 +3872,13 @@ var BullMQWebhookQueue = class {
|
|
|
3822
3872
|
}
|
|
3823
3873
|
);
|
|
3824
3874
|
worker.on?.("error", (err2) => {
|
|
3825
|
-
log.error("[onesub] BullMQ worker error
|
|
3875
|
+
log.error("[onesub] BullMQ worker error", { err: err2 });
|
|
3826
3876
|
});
|
|
3827
3877
|
return worker;
|
|
3828
3878
|
})();
|
|
3829
3879
|
this.workerPromise.catch((err2) => {
|
|
3830
3880
|
this.workerStartupError = err2 instanceof Error ? err2 : new Error(String(err2));
|
|
3831
|
-
log.error("[onesub] BullMQ worker failed to start
|
|
3881
|
+
log.error("[onesub] BullMQ worker failed to start", { err: err2 });
|
|
3832
3882
|
});
|
|
3833
3883
|
}
|
|
3834
3884
|
}
|