@onesub/server 0.25.0 → 0.27.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/README.md +3 -1
- package/dist/__tests__/google-webhook-open-warning.test.d.ts +15 -7
- package/dist/__tests__/google-webhook-open-warning.test.d.ts.map +1 -1
- package/dist/__tests__/google-webhook-unauthenticated.test.d.ts +30 -0
- package/dist/__tests__/google-webhook-unauthenticated.test.d.ts.map +1 -0
- 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 +155 -78
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +155 -78
- 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 +0 -17
- package/dist/routes/webhook-google.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -174,6 +174,8 @@ var APPLE_ROOT_CA_PEMS = [APPLE_ROOT_CA_G3_PEM];
|
|
|
174
174
|
var LOG_CONTINUATION = "\n | ";
|
|
175
175
|
var BARE_VALUE = /^[\w.:/@+-]+$/;
|
|
176
176
|
var LINE_TERMINATORS = /\r\n|\r|\n|\u2028|\u2029/;
|
|
177
|
+
var MAX_VALUE_CHARS = 512;
|
|
178
|
+
var PLAY_TOKEN_IN_URL = /\/tokens\/[^/:"'\s\\]+/g;
|
|
177
179
|
var MAX_CAUSE_DEPTH = 2;
|
|
178
180
|
var MAX_AGGREGATE_ERRORS = 3;
|
|
179
181
|
function esc(value) {
|
|
@@ -182,6 +184,13 @@ function esc(value) {
|
|
|
182
184
|
function escQuoted(value) {
|
|
183
185
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
184
186
|
}
|
|
187
|
+
function clamp(value) {
|
|
188
|
+
if (value.length <= MAX_VALUE_CHARS) return value;
|
|
189
|
+
return `${value.slice(0, MAX_VALUE_CHARS)}\u2026+${value.length - MAX_VALUE_CHARS} more`;
|
|
190
|
+
}
|
|
191
|
+
function sanitize(value) {
|
|
192
|
+
return clamp(value.replace(PLAY_TOKEN_IN_URL, "/tokens/[redacted]"));
|
|
193
|
+
}
|
|
185
194
|
function renderValue(value) {
|
|
186
195
|
if (value === void 0) return void 0;
|
|
187
196
|
if (value === null) return "null";
|
|
@@ -191,17 +200,19 @@ function renderValue(value) {
|
|
|
191
200
|
return String(value);
|
|
192
201
|
case "bigint":
|
|
193
202
|
return `${value}n`;
|
|
194
|
-
case "string":
|
|
195
|
-
|
|
203
|
+
case "string": {
|
|
204
|
+
const safe = sanitize(value);
|
|
205
|
+
return BARE_VALUE.test(safe) ? safe : `"${escQuoted(safe)}"`;
|
|
206
|
+
}
|
|
196
207
|
case "object":
|
|
197
208
|
try {
|
|
198
|
-
return `"${escQuoted(JSON.stringify(value) ?? "null")}"`;
|
|
209
|
+
return `"${escQuoted(sanitize(JSON.stringify(value) ?? "null"))}"`;
|
|
199
210
|
} catch {
|
|
200
211
|
return '"[unserialisable]"';
|
|
201
212
|
}
|
|
202
213
|
default:
|
|
203
214
|
try {
|
|
204
|
-
return `"${escQuoted(String(value))}"`;
|
|
215
|
+
return `"${escQuoted(sanitize(String(value)))}"`;
|
|
205
216
|
} catch {
|
|
206
217
|
return '"[unrenderable]"';
|
|
207
218
|
}
|
|
@@ -449,7 +460,7 @@ function outcomePasses(outcome, tag) {
|
|
|
449
460
|
throw new Error(`[onesub/mock/${tag}] simulated upstream network error`);
|
|
450
461
|
}
|
|
451
462
|
if (outcome.kind === "valid" || outcome.kind === "sandbox") return true;
|
|
452
|
-
log.warn(
|
|
463
|
+
log.warn("[onesub/mock] receipt rejected", { provider: tag, outcome: outcome.kind });
|
|
453
464
|
return false;
|
|
454
465
|
}
|
|
455
466
|
function deterministicTransactionId(prefix, receipt) {
|
|
@@ -1501,7 +1512,10 @@ function buildAppRegistry(config) {
|
|
|
1501
1512
|
// that would validate one app's receipt against another's credentials.
|
|
1502
1513
|
(config.apple || config.google || apps.length === 1 ? apps[0] : void 0);
|
|
1503
1514
|
if (apps.length > 1) {
|
|
1504
|
-
log.info("[onesub] Multi-app mode
|
|
1515
|
+
log.info("[onesub] Multi-app mode", {
|
|
1516
|
+
appIds: apps.map(appName),
|
|
1517
|
+
defaultAppId: defaultApp ? appName(defaultApp) : null
|
|
1518
|
+
});
|
|
1505
1519
|
}
|
|
1506
1520
|
function match(hint) {
|
|
1507
1521
|
if (hint.appId) {
|
|
@@ -1509,13 +1523,13 @@ function buildAppRegistry(config) {
|
|
|
1509
1523
|
(a) => a.id === hint.appId || a.apple?.bundleId === hint.appId || a.google?.packageName === hint.appId
|
|
1510
1524
|
);
|
|
1511
1525
|
if (byId) return byId;
|
|
1512
|
-
log.warn("[onesub] Unknown appId
|
|
1526
|
+
log.warn("[onesub] Unknown appId", { appId: hint.appId });
|
|
1513
1527
|
return void 0;
|
|
1514
1528
|
}
|
|
1515
1529
|
if (hint.bundleId) {
|
|
1516
1530
|
const byBundle = apps.find((a) => a.apple?.bundleId === hint.bundleId);
|
|
1517
1531
|
if (byBundle) return byBundle;
|
|
1518
|
-
log.warn("[onesub] No app configured for bundleId
|
|
1532
|
+
log.warn("[onesub] No app configured for bundleId", { bundleId: hint.bundleId });
|
|
1519
1533
|
return void 0;
|
|
1520
1534
|
}
|
|
1521
1535
|
return defaultApp;
|
|
@@ -1603,9 +1617,10 @@ function createValidateRouter(config, store) {
|
|
|
1603
1617
|
delete sub.boundAccountId;
|
|
1604
1618
|
const bindingMismatch = platform === "apple" ? boundAccountId && boundAccountId.toLowerCase() !== userId.toLowerCase() : boundAccountId && boundAccountId !== userId;
|
|
1605
1619
|
if (bindingMismatch) {
|
|
1606
|
-
log.warn(
|
|
1607
|
-
|
|
1608
|
-
|
|
1620
|
+
log.warn("[onesub/validate] account binding mismatch \u2014 receipt token does not match userId", {
|
|
1621
|
+
originalTransactionId: sub.originalTransactionId,
|
|
1622
|
+
userId
|
|
1623
|
+
});
|
|
1609
1624
|
sendError(
|
|
1610
1625
|
res,
|
|
1611
1626
|
409,
|
|
@@ -1618,7 +1633,7 @@ function createValidateRouter(config, store) {
|
|
|
1618
1633
|
const isSandbox = sub.sandbox === true;
|
|
1619
1634
|
delete sub.sandbox;
|
|
1620
1635
|
if (isSandbox && getTestOverride(userId) === false) {
|
|
1621
|
-
log.warn(
|
|
1636
|
+
log.warn("[onesub/validate] sandbox test override active \u2014 forcing not-entitled", { userId });
|
|
1622
1637
|
sub.status = shared.SUBSCRIPTION_STATUS.EXPIRED;
|
|
1623
1638
|
sub.willRenew = false;
|
|
1624
1639
|
}
|
|
@@ -1630,7 +1645,7 @@ function createValidateRouter(config, store) {
|
|
|
1630
1645
|
const response = { valid: true, subscription: sub };
|
|
1631
1646
|
res.status(200).json(response);
|
|
1632
1647
|
} catch (err2) {
|
|
1633
|
-
log.error("[onesub/validate] Unexpected error
|
|
1648
|
+
log.error("[onesub/validate] Unexpected error", { userId, productId, platform, err: err2 });
|
|
1634
1649
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.INTERNAL_ERROR, "Internal server error during receipt validation", NO_SUB);
|
|
1635
1650
|
}
|
|
1636
1651
|
});
|
|
@@ -1662,7 +1677,7 @@ function createStatusRouter(store) {
|
|
|
1662
1677
|
const response = { active, subscription: sub };
|
|
1663
1678
|
res.status(200).json(response);
|
|
1664
1679
|
} catch (err2) {
|
|
1665
|
-
log.error("[onesub/status] Store error
|
|
1680
|
+
log.error("[onesub/status] Store error", { userId, err: err2 });
|
|
1666
1681
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error", NO_SUB2);
|
|
1667
1682
|
}
|
|
1668
1683
|
});
|
|
@@ -1770,7 +1785,7 @@ async function processAppleNotification(work, config, store, purchaseStore) {
|
|
|
1770
1785
|
});
|
|
1771
1786
|
}
|
|
1772
1787
|
} catch (err2) {
|
|
1773
|
-
log.warn("[onesub/webhook/apple] consumptionInfoProvider failed
|
|
1788
|
+
log.warn("[onesub/webhook/apple] consumptionInfoProvider failed", { transactionId, err: err2 });
|
|
1774
1789
|
}
|
|
1775
1790
|
})();
|
|
1776
1791
|
}
|
|
@@ -1783,7 +1798,7 @@ async function processAppleNotification(work, config, store, purchaseStore) {
|
|
|
1783
1798
|
const lookupId = transactionId ?? originalTransactionId;
|
|
1784
1799
|
const removed = await purchaseStore.deletePurchaseByTransactionId(lookupId);
|
|
1785
1800
|
if (!removed) {
|
|
1786
|
-
log.warn("[onesub/webhook/apple] IAP refund for unknown transaction
|
|
1801
|
+
log.warn("[onesub/webhook/apple] IAP refund for unknown transaction", { transactionId: lookupId });
|
|
1787
1802
|
}
|
|
1788
1803
|
return;
|
|
1789
1804
|
}
|
|
@@ -1793,7 +1808,10 @@ async function processAppleNotification(work, config, store, purchaseStore) {
|
|
|
1793
1808
|
const keepEntitlement = isSubscriptionRefund && config.refundPolicy === "until_expiry";
|
|
1794
1809
|
const correctedUserId = appAccountToken && existing.userId === originalTransactionId ? appAccountToken : existing.userId;
|
|
1795
1810
|
if (correctedUserId !== existing.userId) {
|
|
1796
|
-
log.info("[onesub/webhook/apple] correcting userId from originalTransactionId to appAccountToken
|
|
1811
|
+
log.info("[onesub/webhook/apple] correcting userId from originalTransactionId to appAccountToken", {
|
|
1812
|
+
originalTransactionId,
|
|
1813
|
+
userId: correctedUserId
|
|
1814
|
+
});
|
|
1797
1815
|
}
|
|
1798
1816
|
const updated = keepEntitlement ? { ...existing, userId: correctedUserId, willRenew: false } : {
|
|
1799
1817
|
...existing,
|
|
@@ -1811,20 +1829,22 @@ async function processAppleNotification(work, config, store, purchaseStore) {
|
|
|
1811
1829
|
fresh.userId = appAccountToken ?? originalTransactionId;
|
|
1812
1830
|
if (inAppOwnershipType === "FAMILY_SHARED") {
|
|
1813
1831
|
const source = appAccountToken ? "appAccountToken" : "originalTransactionId (fallback)";
|
|
1814
|
-
log.info(
|
|
1832
|
+
log.info("[onesub/webhook/apple] FAMILY_SHARED", {
|
|
1833
|
+
originalTransactionId,
|
|
1834
|
+
userId: fresh.userId,
|
|
1835
|
+
userIdSource: source
|
|
1836
|
+
});
|
|
1815
1837
|
}
|
|
1816
1838
|
await store.save(fresh);
|
|
1817
1839
|
} else {
|
|
1818
|
-
log.warn(
|
|
1819
|
-
"[onesub/webhook/apple] Unknown transaction and Status API returned no record:",
|
|
1840
|
+
log.warn("[onesub/webhook/apple] Unknown transaction and Status API returned no record", {
|
|
1820
1841
|
originalTransactionId
|
|
1821
|
-
);
|
|
1842
|
+
});
|
|
1822
1843
|
}
|
|
1823
1844
|
} else {
|
|
1824
|
-
log.warn(
|
|
1825
|
-
"[onesub/webhook/apple] Received notification for unknown transaction (no API creds to recover):",
|
|
1845
|
+
log.warn("[onesub/webhook/apple] Received notification for unknown transaction (no API creds to recover)", {
|
|
1826
1846
|
originalTransactionId
|
|
1827
|
-
);
|
|
1847
|
+
});
|
|
1828
1848
|
}
|
|
1829
1849
|
}
|
|
1830
1850
|
async function handleAppleWebhook(req, res, config, store, purchaseStore, webhookEventStore, webhookQueue) {
|
|
@@ -1840,7 +1860,7 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1840
1860
|
config.apple?.skipJwsVerification
|
|
1841
1861
|
);
|
|
1842
1862
|
} catch (err2) {
|
|
1843
|
-
log.error("[onesub/webhook/apple] Failed to decode signedPayload
|
|
1863
|
+
log.error("[onesub/webhook/apple] Failed to decode signedPayload", { err: err2 });
|
|
1844
1864
|
sendError(res, 400, shared.ONESUB_ERROR_CODE.INVALID_SIGNED_PAYLOAD, "Invalid signedPayload");
|
|
1845
1865
|
return;
|
|
1846
1866
|
}
|
|
@@ -1848,7 +1868,9 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1848
1868
|
if (webhookEventStore && typeof payload.notificationUUID === "string") {
|
|
1849
1869
|
const fresh = await webhookEventStore.markIfNew("apple", payload.notificationUUID);
|
|
1850
1870
|
if (!fresh) {
|
|
1851
|
-
log.info("[onesub/webhook/apple] dedupe: already processed",
|
|
1871
|
+
log.info("[onesub/webhook/apple] dedupe: already processed", {
|
|
1872
|
+
notificationUUID: payload.notificationUUID
|
|
1873
|
+
});
|
|
1852
1874
|
res.status(200).json({ received: true, deduped: true });
|
|
1853
1875
|
return;
|
|
1854
1876
|
}
|
|
@@ -1861,7 +1883,7 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1861
1883
|
}
|
|
1862
1884
|
const notifiedApp = getAppRegistry(config).configFor({ bundleId: decoded.bundleId });
|
|
1863
1885
|
if (decoded.bundleId && !notifiedApp.apple) {
|
|
1864
|
-
log.warn("[onesub/webhook/apple] No app configured for bundleId
|
|
1886
|
+
log.warn("[onesub/webhook/apple] No app configured for bundleId", { bundleId: decoded.bundleId });
|
|
1865
1887
|
sendError(res, 400, shared.ONESUB_ERROR_CODE.BUNDLE_ID_MISMATCH, "Bundle ID mismatch");
|
|
1866
1888
|
return;
|
|
1867
1889
|
}
|
|
@@ -1881,7 +1903,10 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1881
1903
|
});
|
|
1882
1904
|
res.status(200).json({ received: true, queued: true });
|
|
1883
1905
|
} catch (err2) {
|
|
1884
|
-
log.error("[onesub/webhook/apple] Failed to enqueue webhook job
|
|
1906
|
+
log.error("[onesub/webhook/apple] Failed to enqueue webhook job", {
|
|
1907
|
+
notificationUUID: payload.notificationUUID,
|
|
1908
|
+
err: err2
|
|
1909
|
+
});
|
|
1885
1910
|
await unmarkWebhookEvent(webhookEventStore, "apple", markedEventId);
|
|
1886
1911
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to update subscription");
|
|
1887
1912
|
}
|
|
@@ -1891,7 +1916,10 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1891
1916
|
await processAppleNotification(work, config, store, purchaseStore);
|
|
1892
1917
|
res.status(200).json({ received: true });
|
|
1893
1918
|
} catch (err2) {
|
|
1894
|
-
log.error("[onesub/webhook/apple] Store update error
|
|
1919
|
+
log.error("[onesub/webhook/apple] Store update error", {
|
|
1920
|
+
notificationUUID: payload.notificationUUID,
|
|
1921
|
+
err: err2
|
|
1922
|
+
});
|
|
1895
1923
|
await unmarkWebhookEvent(webhookEventStore, "apple", markedEventId);
|
|
1896
1924
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to update subscription");
|
|
1897
1925
|
}
|
|
@@ -1926,30 +1954,41 @@ async function verifyGooglePushToken(req, expectedAudience, expectedServiceAccou
|
|
|
1926
1954
|
}
|
|
1927
1955
|
var GOOGLE_FAILURE_MESSAGES = {
|
|
1928
1956
|
voided: {
|
|
1929
|
-
logPrefix: "[onesub/webhook/google] voided notification error
|
|
1957
|
+
logPrefix: "[onesub/webhook/google] voided notification error",
|
|
1930
1958
|
message: "Failed to process voided notification"
|
|
1931
1959
|
},
|
|
1932
1960
|
oneTimeProduct: {
|
|
1933
|
-
logPrefix: "[onesub/webhook/google] oneTimeProduct error
|
|
1961
|
+
logPrefix: "[onesub/webhook/google] oneTimeProduct error",
|
|
1934
1962
|
message: "Failed to process oneTimeProduct notification"
|
|
1935
1963
|
},
|
|
1936
1964
|
subscription: {
|
|
1937
|
-
logPrefix: "[onesub/webhook/google] Error handling notification
|
|
1965
|
+
logPrefix: "[onesub/webhook/google] Error handling notification",
|
|
1938
1966
|
message: "Failed to process notification"
|
|
1939
1967
|
}
|
|
1940
1968
|
};
|
|
1941
1969
|
function servesGoogle(config) {
|
|
1942
1970
|
return getAppRegistry(config).apps.some((app) => !!app.google);
|
|
1943
1971
|
}
|
|
1972
|
+
function googleWebhookIsAuthenticated(config) {
|
|
1973
|
+
return getAppRegistry(config).apps.some((a) => a.google?.pushAudience);
|
|
1974
|
+
}
|
|
1975
|
+
function googleWebhookAllowsUnauthenticated(config) {
|
|
1976
|
+
return getAppRegistry(config).apps.some((a) => a.google?.allowUnauthenticatedWebhook);
|
|
1977
|
+
}
|
|
1944
1978
|
function warnIfGoogleWebhookOpen(config) {
|
|
1945
1979
|
if (process.env["NODE_ENV"] !== "production") return;
|
|
1946
1980
|
if (!servesGoogle(config)) return;
|
|
1947
1981
|
const apps = getAppRegistry(config).apps;
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1982
|
+
if (!googleWebhookIsAuthenticated(config)) {
|
|
1983
|
+
if (googleWebhookAllowsUnauthenticated(config)) {
|
|
1984
|
+
log.warn(
|
|
1985
|
+
"[onesub] SECURITY: POST /onesub/webhook/google runs unauthenticated by explicit opt-in \u2014 google.allowUnauthenticatedWebhook is set and no app sets google.pushAudience. Anything that can reach this endpoint can cancel a subscription or delete a one-time purchase, so the request must already be authenticated in front of this server."
|
|
1986
|
+
);
|
|
1987
|
+
} else {
|
|
1988
|
+
log.warn(
|
|
1989
|
+
"[onesub] POST /onesub/webhook/google will reject every request with 401 \u2014 no configured app sets google.pushAudience, so the Pub/Sub OIDC token cannot be verified. Set google.pushAudience (and google.pushServiceAccountEmail), or set google.allowUnauthenticatedWebhook when something in front of this server already authenticates the request."
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1953
1992
|
}
|
|
1954
1993
|
if (!apps.some((a) => a.google?.packageName)) {
|
|
1955
1994
|
log.warn(
|
|
@@ -1976,12 +2015,14 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
1976
2015
|
const updated = config.refundPolicy === "until_expiry" ? { ...existing2, willRenew: false } : { ...existing2, status: shared.SUBSCRIPTION_STATUS.CANCELED };
|
|
1977
2016
|
await store.save(updated);
|
|
1978
2017
|
} else {
|
|
1979
|
-
log.warn("[onesub/webhook/google] voided subscription for unknown purchaseToken
|
|
2018
|
+
log.warn("[onesub/webhook/google] voided subscription for unknown purchaseToken", {
|
|
2019
|
+
purchaseToken: voided.purchaseToken
|
|
2020
|
+
});
|
|
1980
2021
|
}
|
|
1981
2022
|
} else {
|
|
1982
2023
|
const removed = await purchaseStore.deletePurchaseByTransactionId(voided.orderId);
|
|
1983
2024
|
if (!removed) {
|
|
1984
|
-
log.warn("[onesub/webhook/google] voided IAP for unknown orderId
|
|
2025
|
+
log.warn("[onesub/webhook/google] voided IAP for unknown orderId", { orderId: voided.orderId });
|
|
1985
2026
|
}
|
|
1986
2027
|
}
|
|
1987
2028
|
return;
|
|
@@ -1989,15 +2030,18 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
1989
2030
|
if (work.kind === "oneTimeProduct") {
|
|
1990
2031
|
const { notificationType: notificationType2, purchaseToken: purchaseToken2, sku } = work.oneTimeProduct;
|
|
1991
2032
|
if (notificationType2 === 1) {
|
|
1992
|
-
log.info("[onesub/webhook/google] oneTimeProduct PURCHASED
|
|
2033
|
+
log.info("[onesub/webhook/google] oneTimeProduct PURCHASED", { productId: sku });
|
|
1993
2034
|
const googleCfg = googleFor(work.oneTimeProduct.packageName);
|
|
1994
2035
|
if (googleCfg?.serviceAccountKey && googleCfg.packageName) {
|
|
1995
2036
|
void acknowledgeGoogleProduct(purchaseToken2, sku, googleCfg).catch(
|
|
1996
|
-
(err2) => log.warn(
|
|
2037
|
+
(err2) => log.warn("[onesub/webhook/google] oneTimeProduct ack failed \u2014 3-day auto-refund risk", {
|
|
2038
|
+
productId: sku,
|
|
2039
|
+
err: err2
|
|
2040
|
+
})
|
|
1997
2041
|
);
|
|
1998
2042
|
}
|
|
1999
2043
|
} else {
|
|
2000
|
-
log.info("[onesub/webhook/google] oneTimeProduct CANCELED (pre-completion)
|
|
2044
|
+
log.info("[onesub/webhook/google] oneTimeProduct CANCELED (pre-completion)", { productId: sku });
|
|
2001
2045
|
}
|
|
2002
2046
|
return;
|
|
2003
2047
|
}
|
|
@@ -2024,7 +2068,7 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
2024
2068
|
const subGoogleCfg = googleFor(packageName);
|
|
2025
2069
|
if (isGooglePriceChangeConfirmedNotification(notificationType) && subGoogleCfg?.onPriceChangeConfirmed) {
|
|
2026
2070
|
const hook = subGoogleCfg.onPriceChangeConfirmed;
|
|
2027
|
-
void Promise.resolve().then(() => hook({ purchaseToken, subscriptionId, packageName })).catch((err2) => log.warn("[onesub/webhook/google] onPriceChangeConfirmed hook failed
|
|
2071
|
+
void Promise.resolve().then(() => hook({ purchaseToken, subscriptionId, packageName })).catch((err2) => log.warn("[onesub/webhook/google] onPriceChangeConfirmed hook failed", { err: err2 }));
|
|
2028
2072
|
}
|
|
2029
2073
|
if (existing) {
|
|
2030
2074
|
let updated = { ...existing, status: finalStatus };
|
|
@@ -2053,7 +2097,11 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
2053
2097
|
const previous = await store.getByTransactionId(fresh.linkedPurchaseToken);
|
|
2054
2098
|
fresh.userId = previous ? previous.userId : boundAccountId ?? purchaseToken;
|
|
2055
2099
|
if (previous) {
|
|
2056
|
-
log.info(
|
|
2100
|
+
log.info("[onesub/webhook/google] inherited userId from linkedPurchaseToken", {
|
|
2101
|
+
userId: previous.userId,
|
|
2102
|
+
linkedPurchaseToken: fresh.linkedPurchaseToken,
|
|
2103
|
+
purchaseToken
|
|
2104
|
+
});
|
|
2057
2105
|
}
|
|
2058
2106
|
} else {
|
|
2059
2107
|
fresh.userId = boundAccountId ?? purchaseToken;
|
|
@@ -2061,13 +2109,20 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
2061
2109
|
await store.save(fresh);
|
|
2062
2110
|
}
|
|
2063
2111
|
} else {
|
|
2064
|
-
log.warn("[onesub/webhook/google] Unknown purchase token and no serviceAccountKey to re-fetch
|
|
2112
|
+
log.warn("[onesub/webhook/google] Unknown purchase token and no serviceAccountKey to re-fetch", {
|
|
2113
|
+
purchaseToken
|
|
2114
|
+
});
|
|
2065
2115
|
}
|
|
2066
2116
|
}
|
|
2067
2117
|
}
|
|
2068
2118
|
async function handleGoogleWebhook(req, res, config, store, purchaseStore, webhookEventStore, webhookQueue) {
|
|
2069
2119
|
const pushIdentities = getAppRegistry(config).apps.map((app) => app.google).filter((g) => !!g?.pushAudience);
|
|
2070
|
-
if (pushIdentities.length
|
|
2120
|
+
if (pushIdentities.length === 0) {
|
|
2121
|
+
if (process.env["NODE_ENV"] === "production" && !googleWebhookAllowsUnauthenticated(config)) {
|
|
2122
|
+
sendError(res, 401, shared.ONESUB_ERROR_CODE.UNAUTHORIZED, "Unauthorized");
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
} else {
|
|
2071
2126
|
let authenticated = false;
|
|
2072
2127
|
for (const google of pushIdentities) {
|
|
2073
2128
|
if (await verifyGooglePushToken(req, google.pushAudience, google.pushServiceAccountEmail)) {
|
|
@@ -2089,7 +2144,9 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2089
2144
|
if (webhookEventStore && typeof body.message.messageId === "string") {
|
|
2090
2145
|
const fresh = await webhookEventStore.markIfNew("google", body.message.messageId);
|
|
2091
2146
|
if (!fresh) {
|
|
2092
|
-
log.info("[onesub/webhook/google] dedupe: already processed",
|
|
2147
|
+
log.info("[onesub/webhook/google] dedupe: already processed", {
|
|
2148
|
+
messageId: body.message.messageId
|
|
2149
|
+
});
|
|
2093
2150
|
res.status(200).json({ received: true, deduped: true });
|
|
2094
2151
|
return;
|
|
2095
2152
|
}
|
|
@@ -2100,7 +2157,9 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2100
2157
|
const voided = decodeGoogleVoidedNotification(body);
|
|
2101
2158
|
if (voided) {
|
|
2102
2159
|
if (!servesPackage(voided.packageName)) {
|
|
2103
|
-
log.warn("[onesub/webhook/google] voided package name not served
|
|
2160
|
+
log.warn("[onesub/webhook/google] voided package name not served", {
|
|
2161
|
+
packageName: voided.packageName
|
|
2162
|
+
});
|
|
2104
2163
|
sendError(res, 400, shared.ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
|
|
2105
2164
|
return;
|
|
2106
2165
|
}
|
|
@@ -2109,7 +2168,9 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2109
2168
|
const oneTimeProduct = decodeGoogleOneTimeProductNotification(body);
|
|
2110
2169
|
if (oneTimeProduct) {
|
|
2111
2170
|
if (!servesPackage(oneTimeProduct.packageName)) {
|
|
2112
|
-
log.warn("[onesub/webhook/google] oneTimeProduct package name not served
|
|
2171
|
+
log.warn("[onesub/webhook/google] oneTimeProduct package name not served", {
|
|
2172
|
+
packageName: oneTimeProduct.packageName
|
|
2173
|
+
});
|
|
2113
2174
|
sendError(res, 400, shared.ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
|
|
2114
2175
|
return;
|
|
2115
2176
|
}
|
|
@@ -2121,7 +2182,9 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2121
2182
|
return;
|
|
2122
2183
|
}
|
|
2123
2184
|
if (!servesPackage(notification.packageName)) {
|
|
2124
|
-
log.warn("[onesub/webhook/google] Package name not served
|
|
2185
|
+
log.warn("[onesub/webhook/google] Package name not served", {
|
|
2186
|
+
packageName: notification.packageName
|
|
2187
|
+
});
|
|
2125
2188
|
sendError(res, 400, shared.ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
|
|
2126
2189
|
return;
|
|
2127
2190
|
}
|
|
@@ -2139,7 +2202,11 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2139
2202
|
});
|
|
2140
2203
|
res.status(200).json({ received: true, queued: true });
|
|
2141
2204
|
} catch (err2) {
|
|
2142
|
-
log.error("[onesub/webhook/google] Failed to enqueue webhook job
|
|
2205
|
+
log.error("[onesub/webhook/google] Failed to enqueue webhook job", {
|
|
2206
|
+
kind: work.kind,
|
|
2207
|
+
messageId: body.message.messageId,
|
|
2208
|
+
err: err2
|
|
2209
|
+
});
|
|
2143
2210
|
await unmarkWebhookEvent(webhookEventStore, "google", markedEventId);
|
|
2144
2211
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, GOOGLE_FAILURE_MESSAGES[work.kind].message);
|
|
2145
2212
|
}
|
|
@@ -2150,7 +2217,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2150
2217
|
res.status(200).json({ received: true });
|
|
2151
2218
|
} catch (err2) {
|
|
2152
2219
|
const { logPrefix, message } = GOOGLE_FAILURE_MESSAGES[work.kind];
|
|
2153
|
-
log.error(logPrefix, err2);
|
|
2220
|
+
log.error(logPrefix, { kind: work.kind, err: err2 });
|
|
2154
2221
|
await unmarkWebhookEvent(webhookEventStore, "google", markedEventId);
|
|
2155
2222
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, message);
|
|
2156
2223
|
}
|
|
@@ -2262,9 +2329,10 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2262
2329
|
}
|
|
2263
2330
|
const bindingMismatch = platform === "apple" ? boundAccountId && boundAccountId.toLowerCase() !== userId.toLowerCase() : boundAccountId && boundAccountId !== userId;
|
|
2264
2331
|
if (bindingMismatch) {
|
|
2265
|
-
log.warn(
|
|
2266
|
-
|
|
2267
|
-
|
|
2332
|
+
log.warn("[onesub/purchase] account binding mismatch \u2014 token does not match userId", {
|
|
2333
|
+
transactionId,
|
|
2334
|
+
userId
|
|
2335
|
+
});
|
|
2268
2336
|
sendError(
|
|
2269
2337
|
res,
|
|
2270
2338
|
409,
|
|
@@ -2281,9 +2349,11 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2281
2349
|
if (existing.userId !== userId) {
|
|
2282
2350
|
if (type === shared.PURCHASE_TYPE.NON_CONSUMABLE) {
|
|
2283
2351
|
await purchaseStore.reassignPurchase(transactionId, userId);
|
|
2284
|
-
log.info(
|
|
2285
|
-
|
|
2286
|
-
|
|
2352
|
+
log.info("[onesub/purchase] reassigned transaction to a new user", {
|
|
2353
|
+
transactionId,
|
|
2354
|
+
fromUserId: existing.userId,
|
|
2355
|
+
userId
|
|
2356
|
+
});
|
|
2287
2357
|
} else {
|
|
2288
2358
|
sendError(
|
|
2289
2359
|
res,
|
|
@@ -2327,7 +2397,7 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2327
2397
|
};
|
|
2328
2398
|
res.status(200).json(response);
|
|
2329
2399
|
} catch (err2) {
|
|
2330
|
-
log.error("[onesub/purchase/validate] Unexpected error
|
|
2400
|
+
log.error("[onesub/purchase/validate] Unexpected error", { userId, productId, platform, type, err: err2 });
|
|
2331
2401
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.INTERNAL_ERROR, "Internal server error during purchase validation", NO_PURCHASE);
|
|
2332
2402
|
}
|
|
2333
2403
|
});
|
|
@@ -2342,7 +2412,7 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2342
2412
|
const response = { purchases };
|
|
2343
2413
|
res.status(200).json(response);
|
|
2344
2414
|
} catch (err2) {
|
|
2345
|
-
log.error("[onesub/purchase/status] Store error
|
|
2415
|
+
log.error("[onesub/purchase/status] Store error", { userId, productId, err: err2 });
|
|
2346
2416
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error", { purchases: [] });
|
|
2347
2417
|
}
|
|
2348
2418
|
});
|
|
@@ -2407,7 +2477,7 @@ function createEntitlementRouter(config, store, purchaseStore) {
|
|
|
2407
2477
|
const response = { id, ...status };
|
|
2408
2478
|
res.status(200).json(response);
|
|
2409
2479
|
} catch (err2) {
|
|
2410
|
-
log.error("[onesub/entitlement] evaluation error
|
|
2480
|
+
log.error("[onesub/entitlement] evaluation error", { userId, entitlement: id, err: err2 });
|
|
2411
2481
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2412
2482
|
}
|
|
2413
2483
|
});
|
|
@@ -2431,7 +2501,7 @@ function createEntitlementRouter(config, store, purchaseStore) {
|
|
|
2431
2501
|
};
|
|
2432
2502
|
res.status(200).json(response);
|
|
2433
2503
|
} catch (err2) {
|
|
2434
|
-
log.error("[onesub/entitlements] evaluation error
|
|
2504
|
+
log.error("[onesub/entitlements] evaluation error", { userId, err: err2 });
|
|
2435
2505
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error", { entitlements: {} });
|
|
2436
2506
|
}
|
|
2437
2507
|
});
|
|
@@ -2545,7 +2615,7 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2545
2615
|
};
|
|
2546
2616
|
res.status(200).json(response);
|
|
2547
2617
|
} catch (err2) {
|
|
2548
|
-
log.error("[onesub/admin/subscriptions] list error
|
|
2618
|
+
log.error("[onesub/admin/subscriptions] list error", { err: err2 });
|
|
2549
2619
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2550
2620
|
}
|
|
2551
2621
|
});
|
|
@@ -2565,7 +2635,10 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2565
2635
|
}
|
|
2566
2636
|
res.status(200).json(sub);
|
|
2567
2637
|
} catch (err2) {
|
|
2568
|
-
log.error("[onesub/admin/subscriptions/:transactionId] detail error
|
|
2638
|
+
log.error("[onesub/admin/subscriptions/:transactionId] detail error", {
|
|
2639
|
+
transactionId: params.transactionId,
|
|
2640
|
+
err: err2
|
|
2641
|
+
});
|
|
2569
2642
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2570
2643
|
}
|
|
2571
2644
|
});
|
|
@@ -2598,7 +2671,7 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2598
2671
|
};
|
|
2599
2672
|
res.status(200).json(response);
|
|
2600
2673
|
} catch (err2) {
|
|
2601
|
-
log.error("[onesub/admin/customers/:userId] error
|
|
2674
|
+
log.error("[onesub/admin/customers/:userId] error", { userId: params.userId, err: err2 });
|
|
2602
2675
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2603
2676
|
}
|
|
2604
2677
|
});
|
|
@@ -2628,7 +2701,10 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2628
2701
|
await store.save(fresh);
|
|
2629
2702
|
res.status(200).json({ ok: true, subscription: fresh });
|
|
2630
2703
|
} catch (err2) {
|
|
2631
|
-
log.error("[onesub/admin/sync-apple] error
|
|
2704
|
+
log.error("[onesub/admin/sync-apple] error", {
|
|
2705
|
+
originalTransactionId: params.originalTransactionId,
|
|
2706
|
+
err: err2
|
|
2707
|
+
});
|
|
2632
2708
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.INTERNAL_ERROR, "Internal server error");
|
|
2633
2709
|
}
|
|
2634
2710
|
});
|
|
@@ -2643,9 +2719,10 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2643
2719
|
const body = parseOrSend(res, overrideBodySchema, req.body);
|
|
2644
2720
|
if (!body) return;
|
|
2645
2721
|
setTestOverride(params.userId, body.entitled);
|
|
2646
|
-
log.warn(
|
|
2647
|
-
|
|
2648
|
-
|
|
2722
|
+
log.warn("[onesub/admin] sandbox test override set", {
|
|
2723
|
+
userId: params.userId,
|
|
2724
|
+
entitled: body.entitled
|
|
2725
|
+
});
|
|
2649
2726
|
res.json({ ok: true, userId: params.userId, entitled: body.entitled, sandboxOnly: true });
|
|
2650
2727
|
});
|
|
2651
2728
|
router.delete(shared.ROUTES.ADMIN_TEST_OVERRIDE, (req, res) => {
|
|
@@ -2662,7 +2739,7 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2662
2739
|
const items = await webhookQueue.listDeadLetters();
|
|
2663
2740
|
res.status(200).json({ items });
|
|
2664
2741
|
} catch (err2) {
|
|
2665
|
-
log.error("[onesub/admin/webhook-deadletters] error
|
|
2742
|
+
log.error("[onesub/admin/webhook-deadletters] error", { err: err2 });
|
|
2666
2743
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2667
2744
|
}
|
|
2668
2745
|
});
|
|
@@ -2676,7 +2753,7 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2676
2753
|
await webhookQueue.replayDeadLetter(params.id);
|
|
2677
2754
|
res.status(200).json({ ok: true });
|
|
2678
2755
|
} catch (err2) {
|
|
2679
|
-
log.error("[onesub/admin/webhook-replay] error
|
|
2756
|
+
log.error("[onesub/admin/webhook-replay] error", { deadLetterId: params.id, err: err2 });
|
|
2680
2757
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2681
2758
|
}
|
|
2682
2759
|
});
|
|
@@ -2846,7 +2923,7 @@ function createMetricsRouter(config, store, purchaseStore) {
|
|
|
2846
2923
|
);
|
|
2847
2924
|
res.status(200).json(response);
|
|
2848
2925
|
} catch (err2) {
|
|
2849
|
-
log.error("[onesub/metrics/active] error
|
|
2926
|
+
log.error("[onesub/metrics/active] error", { err: err2 });
|
|
2850
2927
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2851
2928
|
}
|
|
2852
2929
|
});
|
|
@@ -2913,7 +2990,7 @@ function createMetricsRouter(config, store, purchaseStore) {
|
|
|
2913
2990
|
};
|
|
2914
2991
|
res.status(200).json(response);
|
|
2915
2992
|
} catch (err2) {
|
|
2916
|
-
log.error(
|
|
2993
|
+
log.error("[onesub/metrics] error", { route: name, err: err2 });
|
|
2917
2994
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2918
2995
|
}
|
|
2919
2996
|
};
|
|
@@ -2985,7 +3062,7 @@ function createAppleOfferRouter(config) {
|
|
|
2985
3062
|
);
|
|
2986
3063
|
res.status(200).json(result);
|
|
2987
3064
|
} catch (err2) {
|
|
2988
|
-
log.error("[onesub/apple/offer] signing error
|
|
3065
|
+
log.error("[onesub/apple/offer] signing error", { productId: body.productId, err: err2 });
|
|
2989
3066
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.INTERNAL_ERROR, "Offer signing failed");
|
|
2990
3067
|
}
|
|
2991
3068
|
});
|
|
@@ -3057,7 +3134,7 @@ async function createPgPool(connectionString, label) {
|
|
|
3057
3134
|
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
3058
3135
|
const pool = new Pool({ connectionString, max: 10 });
|
|
3059
3136
|
pool.on("error", (err2) => {
|
|
3060
|
-
log.error(
|
|
3137
|
+
log.error("[onesub] Postgres pool error (idle client)", { store: label, err: err2 });
|
|
3061
3138
|
});
|
|
3062
3139
|
return pool;
|
|
3063
3140
|
}
|
|
@@ -3798,7 +3875,7 @@ var BullMQWebhookQueue = class {
|
|
|
3798
3875
|
const { Queue } = await this.getBullMQ();
|
|
3799
3876
|
const queue = new Queue(this.queueName, { connection: this.connection });
|
|
3800
3877
|
queue.on?.("error", (err2) => {
|
|
3801
|
-
log.error("[onesub] BullMQ queue error
|
|
3878
|
+
log.error("[onesub] BullMQ queue error", { err: err2 });
|
|
3802
3879
|
});
|
|
3803
3880
|
return queue;
|
|
3804
3881
|
})();
|
|
@@ -3822,13 +3899,13 @@ var BullMQWebhookQueue = class {
|
|
|
3822
3899
|
}
|
|
3823
3900
|
);
|
|
3824
3901
|
worker.on?.("error", (err2) => {
|
|
3825
|
-
log.error("[onesub] BullMQ worker error
|
|
3902
|
+
log.error("[onesub] BullMQ worker error", { err: err2 });
|
|
3826
3903
|
});
|
|
3827
3904
|
return worker;
|
|
3828
3905
|
})();
|
|
3829
3906
|
this.workerPromise.catch((err2) => {
|
|
3830
3907
|
this.workerStartupError = err2 instanceof Error ? err2 : new Error(String(err2));
|
|
3831
|
-
log.error("[onesub] BullMQ worker failed to start
|
|
3908
|
+
log.error("[onesub] BullMQ worker failed to start", { err: err2 });
|
|
3832
3909
|
});
|
|
3833
3910
|
}
|
|
3834
3911
|
}
|