@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.js
CHANGED
|
@@ -167,6 +167,8 @@ var APPLE_ROOT_CA_PEMS = [APPLE_ROOT_CA_G3_PEM];
|
|
|
167
167
|
var LOG_CONTINUATION = "\n | ";
|
|
168
168
|
var BARE_VALUE = /^[\w.:/@+-]+$/;
|
|
169
169
|
var LINE_TERMINATORS = /\r\n|\r|\n|\u2028|\u2029/;
|
|
170
|
+
var MAX_VALUE_CHARS = 512;
|
|
171
|
+
var PLAY_TOKEN_IN_URL = /\/tokens\/[^/:"'\s\\]+/g;
|
|
170
172
|
var MAX_CAUSE_DEPTH = 2;
|
|
171
173
|
var MAX_AGGREGATE_ERRORS = 3;
|
|
172
174
|
function esc(value) {
|
|
@@ -175,6 +177,13 @@ function esc(value) {
|
|
|
175
177
|
function escQuoted(value) {
|
|
176
178
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
177
179
|
}
|
|
180
|
+
function clamp(value) {
|
|
181
|
+
if (value.length <= MAX_VALUE_CHARS) return value;
|
|
182
|
+
return `${value.slice(0, MAX_VALUE_CHARS)}\u2026+${value.length - MAX_VALUE_CHARS} more`;
|
|
183
|
+
}
|
|
184
|
+
function sanitize(value) {
|
|
185
|
+
return clamp(value.replace(PLAY_TOKEN_IN_URL, "/tokens/[redacted]"));
|
|
186
|
+
}
|
|
178
187
|
function renderValue(value) {
|
|
179
188
|
if (value === void 0) return void 0;
|
|
180
189
|
if (value === null) return "null";
|
|
@@ -184,17 +193,19 @@ function renderValue(value) {
|
|
|
184
193
|
return String(value);
|
|
185
194
|
case "bigint":
|
|
186
195
|
return `${value}n`;
|
|
187
|
-
case "string":
|
|
188
|
-
|
|
196
|
+
case "string": {
|
|
197
|
+
const safe = sanitize(value);
|
|
198
|
+
return BARE_VALUE.test(safe) ? safe : `"${escQuoted(safe)}"`;
|
|
199
|
+
}
|
|
189
200
|
case "object":
|
|
190
201
|
try {
|
|
191
|
-
return `"${escQuoted(JSON.stringify(value) ?? "null")}"`;
|
|
202
|
+
return `"${escQuoted(sanitize(JSON.stringify(value) ?? "null"))}"`;
|
|
192
203
|
} catch {
|
|
193
204
|
return '"[unserialisable]"';
|
|
194
205
|
}
|
|
195
206
|
default:
|
|
196
207
|
try {
|
|
197
|
-
return `"${escQuoted(String(value))}"`;
|
|
208
|
+
return `"${escQuoted(sanitize(String(value)))}"`;
|
|
198
209
|
} catch {
|
|
199
210
|
return '"[unrenderable]"';
|
|
200
211
|
}
|
|
@@ -442,7 +453,7 @@ function outcomePasses(outcome, tag) {
|
|
|
442
453
|
throw new Error(`[onesub/mock/${tag}] simulated upstream network error`);
|
|
443
454
|
}
|
|
444
455
|
if (outcome.kind === "valid" || outcome.kind === "sandbox") return true;
|
|
445
|
-
log.warn(
|
|
456
|
+
log.warn("[onesub/mock] receipt rejected", { provider: tag, outcome: outcome.kind });
|
|
446
457
|
return false;
|
|
447
458
|
}
|
|
448
459
|
function deterministicTransactionId(prefix, receipt) {
|
|
@@ -1494,7 +1505,10 @@ function buildAppRegistry(config) {
|
|
|
1494
1505
|
// that would validate one app's receipt against another's credentials.
|
|
1495
1506
|
(config.apple || config.google || apps.length === 1 ? apps[0] : void 0);
|
|
1496
1507
|
if (apps.length > 1) {
|
|
1497
|
-
log.info("[onesub] Multi-app mode
|
|
1508
|
+
log.info("[onesub] Multi-app mode", {
|
|
1509
|
+
appIds: apps.map(appName),
|
|
1510
|
+
defaultAppId: defaultApp ? appName(defaultApp) : null
|
|
1511
|
+
});
|
|
1498
1512
|
}
|
|
1499
1513
|
function match(hint) {
|
|
1500
1514
|
if (hint.appId) {
|
|
@@ -1502,13 +1516,13 @@ function buildAppRegistry(config) {
|
|
|
1502
1516
|
(a) => a.id === hint.appId || a.apple?.bundleId === hint.appId || a.google?.packageName === hint.appId
|
|
1503
1517
|
);
|
|
1504
1518
|
if (byId) return byId;
|
|
1505
|
-
log.warn("[onesub] Unknown appId
|
|
1519
|
+
log.warn("[onesub] Unknown appId", { appId: hint.appId });
|
|
1506
1520
|
return void 0;
|
|
1507
1521
|
}
|
|
1508
1522
|
if (hint.bundleId) {
|
|
1509
1523
|
const byBundle = apps.find((a) => a.apple?.bundleId === hint.bundleId);
|
|
1510
1524
|
if (byBundle) return byBundle;
|
|
1511
|
-
log.warn("[onesub] No app configured for bundleId
|
|
1525
|
+
log.warn("[onesub] No app configured for bundleId", { bundleId: hint.bundleId });
|
|
1512
1526
|
return void 0;
|
|
1513
1527
|
}
|
|
1514
1528
|
return defaultApp;
|
|
@@ -1596,9 +1610,10 @@ function createValidateRouter(config, store) {
|
|
|
1596
1610
|
delete sub.boundAccountId;
|
|
1597
1611
|
const bindingMismatch = platform === "apple" ? boundAccountId && boundAccountId.toLowerCase() !== userId.toLowerCase() : boundAccountId && boundAccountId !== userId;
|
|
1598
1612
|
if (bindingMismatch) {
|
|
1599
|
-
log.warn(
|
|
1600
|
-
|
|
1601
|
-
|
|
1613
|
+
log.warn("[onesub/validate] account binding mismatch \u2014 receipt token does not match userId", {
|
|
1614
|
+
originalTransactionId: sub.originalTransactionId,
|
|
1615
|
+
userId
|
|
1616
|
+
});
|
|
1602
1617
|
sendError(
|
|
1603
1618
|
res,
|
|
1604
1619
|
409,
|
|
@@ -1611,7 +1626,7 @@ function createValidateRouter(config, store) {
|
|
|
1611
1626
|
const isSandbox = sub.sandbox === true;
|
|
1612
1627
|
delete sub.sandbox;
|
|
1613
1628
|
if (isSandbox && getTestOverride(userId) === false) {
|
|
1614
|
-
log.warn(
|
|
1629
|
+
log.warn("[onesub/validate] sandbox test override active \u2014 forcing not-entitled", { userId });
|
|
1615
1630
|
sub.status = SUBSCRIPTION_STATUS.EXPIRED;
|
|
1616
1631
|
sub.willRenew = false;
|
|
1617
1632
|
}
|
|
@@ -1623,7 +1638,7 @@ function createValidateRouter(config, store) {
|
|
|
1623
1638
|
const response = { valid: true, subscription: sub };
|
|
1624
1639
|
res.status(200).json(response);
|
|
1625
1640
|
} catch (err2) {
|
|
1626
|
-
log.error("[onesub/validate] Unexpected error
|
|
1641
|
+
log.error("[onesub/validate] Unexpected error", { userId, productId, platform, err: err2 });
|
|
1627
1642
|
sendError(res, 500, ONESUB_ERROR_CODE.INTERNAL_ERROR, "Internal server error during receipt validation", NO_SUB);
|
|
1628
1643
|
}
|
|
1629
1644
|
});
|
|
@@ -1655,7 +1670,7 @@ function createStatusRouter(store) {
|
|
|
1655
1670
|
const response = { active, subscription: sub };
|
|
1656
1671
|
res.status(200).json(response);
|
|
1657
1672
|
} catch (err2) {
|
|
1658
|
-
log.error("[onesub/status] Store error
|
|
1673
|
+
log.error("[onesub/status] Store error", { userId, err: err2 });
|
|
1659
1674
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error", NO_SUB2);
|
|
1660
1675
|
}
|
|
1661
1676
|
});
|
|
@@ -1763,7 +1778,7 @@ async function processAppleNotification(work, config, store, purchaseStore) {
|
|
|
1763
1778
|
});
|
|
1764
1779
|
}
|
|
1765
1780
|
} catch (err2) {
|
|
1766
|
-
log.warn("[onesub/webhook/apple] consumptionInfoProvider failed
|
|
1781
|
+
log.warn("[onesub/webhook/apple] consumptionInfoProvider failed", { transactionId, err: err2 });
|
|
1767
1782
|
}
|
|
1768
1783
|
})();
|
|
1769
1784
|
}
|
|
@@ -1776,7 +1791,7 @@ async function processAppleNotification(work, config, store, purchaseStore) {
|
|
|
1776
1791
|
const lookupId = transactionId ?? originalTransactionId;
|
|
1777
1792
|
const removed = await purchaseStore.deletePurchaseByTransactionId(lookupId);
|
|
1778
1793
|
if (!removed) {
|
|
1779
|
-
log.warn("[onesub/webhook/apple] IAP refund for unknown transaction
|
|
1794
|
+
log.warn("[onesub/webhook/apple] IAP refund for unknown transaction", { transactionId: lookupId });
|
|
1780
1795
|
}
|
|
1781
1796
|
return;
|
|
1782
1797
|
}
|
|
@@ -1786,7 +1801,10 @@ async function processAppleNotification(work, config, store, purchaseStore) {
|
|
|
1786
1801
|
const keepEntitlement = isSubscriptionRefund && config.refundPolicy === "until_expiry";
|
|
1787
1802
|
const correctedUserId = appAccountToken && existing.userId === originalTransactionId ? appAccountToken : existing.userId;
|
|
1788
1803
|
if (correctedUserId !== existing.userId) {
|
|
1789
|
-
log.info("[onesub/webhook/apple] correcting userId from originalTransactionId to appAccountToken
|
|
1804
|
+
log.info("[onesub/webhook/apple] correcting userId from originalTransactionId to appAccountToken", {
|
|
1805
|
+
originalTransactionId,
|
|
1806
|
+
userId: correctedUserId
|
|
1807
|
+
});
|
|
1790
1808
|
}
|
|
1791
1809
|
const updated = keepEntitlement ? { ...existing, userId: correctedUserId, willRenew: false } : {
|
|
1792
1810
|
...existing,
|
|
@@ -1804,20 +1822,22 @@ async function processAppleNotification(work, config, store, purchaseStore) {
|
|
|
1804
1822
|
fresh.userId = appAccountToken ?? originalTransactionId;
|
|
1805
1823
|
if (inAppOwnershipType === "FAMILY_SHARED") {
|
|
1806
1824
|
const source = appAccountToken ? "appAccountToken" : "originalTransactionId (fallback)";
|
|
1807
|
-
log.info(
|
|
1825
|
+
log.info("[onesub/webhook/apple] FAMILY_SHARED", {
|
|
1826
|
+
originalTransactionId,
|
|
1827
|
+
userId: fresh.userId,
|
|
1828
|
+
userIdSource: source
|
|
1829
|
+
});
|
|
1808
1830
|
}
|
|
1809
1831
|
await store.save(fresh);
|
|
1810
1832
|
} else {
|
|
1811
|
-
log.warn(
|
|
1812
|
-
"[onesub/webhook/apple] Unknown transaction and Status API returned no record:",
|
|
1833
|
+
log.warn("[onesub/webhook/apple] Unknown transaction and Status API returned no record", {
|
|
1813
1834
|
originalTransactionId
|
|
1814
|
-
);
|
|
1835
|
+
});
|
|
1815
1836
|
}
|
|
1816
1837
|
} else {
|
|
1817
|
-
log.warn(
|
|
1818
|
-
"[onesub/webhook/apple] Received notification for unknown transaction (no API creds to recover):",
|
|
1838
|
+
log.warn("[onesub/webhook/apple] Received notification for unknown transaction (no API creds to recover)", {
|
|
1819
1839
|
originalTransactionId
|
|
1820
|
-
);
|
|
1840
|
+
});
|
|
1821
1841
|
}
|
|
1822
1842
|
}
|
|
1823
1843
|
async function handleAppleWebhook(req, res, config, store, purchaseStore, webhookEventStore, webhookQueue) {
|
|
@@ -1833,7 +1853,7 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1833
1853
|
config.apple?.skipJwsVerification
|
|
1834
1854
|
);
|
|
1835
1855
|
} catch (err2) {
|
|
1836
|
-
log.error("[onesub/webhook/apple] Failed to decode signedPayload
|
|
1856
|
+
log.error("[onesub/webhook/apple] Failed to decode signedPayload", { err: err2 });
|
|
1837
1857
|
sendError(res, 400, ONESUB_ERROR_CODE.INVALID_SIGNED_PAYLOAD, "Invalid signedPayload");
|
|
1838
1858
|
return;
|
|
1839
1859
|
}
|
|
@@ -1841,7 +1861,9 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1841
1861
|
if (webhookEventStore && typeof payload.notificationUUID === "string") {
|
|
1842
1862
|
const fresh = await webhookEventStore.markIfNew("apple", payload.notificationUUID);
|
|
1843
1863
|
if (!fresh) {
|
|
1844
|
-
log.info("[onesub/webhook/apple] dedupe: already processed",
|
|
1864
|
+
log.info("[onesub/webhook/apple] dedupe: already processed", {
|
|
1865
|
+
notificationUUID: payload.notificationUUID
|
|
1866
|
+
});
|
|
1845
1867
|
res.status(200).json({ received: true, deduped: true });
|
|
1846
1868
|
return;
|
|
1847
1869
|
}
|
|
@@ -1854,7 +1876,7 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1854
1876
|
}
|
|
1855
1877
|
const notifiedApp = getAppRegistry(config).configFor({ bundleId: decoded.bundleId });
|
|
1856
1878
|
if (decoded.bundleId && !notifiedApp.apple) {
|
|
1857
|
-
log.warn("[onesub/webhook/apple] No app configured for bundleId
|
|
1879
|
+
log.warn("[onesub/webhook/apple] No app configured for bundleId", { bundleId: decoded.bundleId });
|
|
1858
1880
|
sendError(res, 400, ONESUB_ERROR_CODE.BUNDLE_ID_MISMATCH, "Bundle ID mismatch");
|
|
1859
1881
|
return;
|
|
1860
1882
|
}
|
|
@@ -1874,7 +1896,10 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1874
1896
|
});
|
|
1875
1897
|
res.status(200).json({ received: true, queued: true });
|
|
1876
1898
|
} catch (err2) {
|
|
1877
|
-
log.error("[onesub/webhook/apple] Failed to enqueue webhook job
|
|
1899
|
+
log.error("[onesub/webhook/apple] Failed to enqueue webhook job", {
|
|
1900
|
+
notificationUUID: payload.notificationUUID,
|
|
1901
|
+
err: err2
|
|
1902
|
+
});
|
|
1878
1903
|
await unmarkWebhookEvent(webhookEventStore, "apple", markedEventId);
|
|
1879
1904
|
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to update subscription");
|
|
1880
1905
|
}
|
|
@@ -1884,7 +1909,10 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1884
1909
|
await processAppleNotification(work, config, store, purchaseStore);
|
|
1885
1910
|
res.status(200).json({ received: true });
|
|
1886
1911
|
} catch (err2) {
|
|
1887
|
-
log.error("[onesub/webhook/apple] Store update error
|
|
1912
|
+
log.error("[onesub/webhook/apple] Store update error", {
|
|
1913
|
+
notificationUUID: payload.notificationUUID,
|
|
1914
|
+
err: err2
|
|
1915
|
+
});
|
|
1888
1916
|
await unmarkWebhookEvent(webhookEventStore, "apple", markedEventId);
|
|
1889
1917
|
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to update subscription");
|
|
1890
1918
|
}
|
|
@@ -1919,30 +1947,41 @@ async function verifyGooglePushToken(req, expectedAudience, expectedServiceAccou
|
|
|
1919
1947
|
}
|
|
1920
1948
|
var GOOGLE_FAILURE_MESSAGES = {
|
|
1921
1949
|
voided: {
|
|
1922
|
-
logPrefix: "[onesub/webhook/google] voided notification error
|
|
1950
|
+
logPrefix: "[onesub/webhook/google] voided notification error",
|
|
1923
1951
|
message: "Failed to process voided notification"
|
|
1924
1952
|
},
|
|
1925
1953
|
oneTimeProduct: {
|
|
1926
|
-
logPrefix: "[onesub/webhook/google] oneTimeProduct error
|
|
1954
|
+
logPrefix: "[onesub/webhook/google] oneTimeProduct error",
|
|
1927
1955
|
message: "Failed to process oneTimeProduct notification"
|
|
1928
1956
|
},
|
|
1929
1957
|
subscription: {
|
|
1930
|
-
logPrefix: "[onesub/webhook/google] Error handling notification
|
|
1958
|
+
logPrefix: "[onesub/webhook/google] Error handling notification",
|
|
1931
1959
|
message: "Failed to process notification"
|
|
1932
1960
|
}
|
|
1933
1961
|
};
|
|
1934
1962
|
function servesGoogle(config) {
|
|
1935
1963
|
return getAppRegistry(config).apps.some((app) => !!app.google);
|
|
1936
1964
|
}
|
|
1965
|
+
function googleWebhookIsAuthenticated(config) {
|
|
1966
|
+
return getAppRegistry(config).apps.some((a) => a.google?.pushAudience);
|
|
1967
|
+
}
|
|
1968
|
+
function googleWebhookAllowsUnauthenticated(config) {
|
|
1969
|
+
return getAppRegistry(config).apps.some((a) => a.google?.allowUnauthenticatedWebhook);
|
|
1970
|
+
}
|
|
1937
1971
|
function warnIfGoogleWebhookOpen(config) {
|
|
1938
1972
|
if (process.env["NODE_ENV"] !== "production") return;
|
|
1939
1973
|
if (!servesGoogle(config)) return;
|
|
1940
1974
|
const apps = getAppRegistry(config).apps;
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1975
|
+
if (!googleWebhookIsAuthenticated(config)) {
|
|
1976
|
+
if (googleWebhookAllowsUnauthenticated(config)) {
|
|
1977
|
+
log.warn(
|
|
1978
|
+
"[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."
|
|
1979
|
+
);
|
|
1980
|
+
} else {
|
|
1981
|
+
log.warn(
|
|
1982
|
+
"[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."
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1946
1985
|
}
|
|
1947
1986
|
if (!apps.some((a) => a.google?.packageName)) {
|
|
1948
1987
|
log.warn(
|
|
@@ -1969,12 +2008,14 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
1969
2008
|
const updated = config.refundPolicy === "until_expiry" ? { ...existing2, willRenew: false } : { ...existing2, status: SUBSCRIPTION_STATUS.CANCELED };
|
|
1970
2009
|
await store.save(updated);
|
|
1971
2010
|
} else {
|
|
1972
|
-
log.warn("[onesub/webhook/google] voided subscription for unknown purchaseToken
|
|
2011
|
+
log.warn("[onesub/webhook/google] voided subscription for unknown purchaseToken", {
|
|
2012
|
+
purchaseToken: voided.purchaseToken
|
|
2013
|
+
});
|
|
1973
2014
|
}
|
|
1974
2015
|
} else {
|
|
1975
2016
|
const removed = await purchaseStore.deletePurchaseByTransactionId(voided.orderId);
|
|
1976
2017
|
if (!removed) {
|
|
1977
|
-
log.warn("[onesub/webhook/google] voided IAP for unknown orderId
|
|
2018
|
+
log.warn("[onesub/webhook/google] voided IAP for unknown orderId", { orderId: voided.orderId });
|
|
1978
2019
|
}
|
|
1979
2020
|
}
|
|
1980
2021
|
return;
|
|
@@ -1982,15 +2023,18 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
1982
2023
|
if (work.kind === "oneTimeProduct") {
|
|
1983
2024
|
const { notificationType: notificationType2, purchaseToken: purchaseToken2, sku } = work.oneTimeProduct;
|
|
1984
2025
|
if (notificationType2 === 1) {
|
|
1985
|
-
log.info("[onesub/webhook/google] oneTimeProduct PURCHASED
|
|
2026
|
+
log.info("[onesub/webhook/google] oneTimeProduct PURCHASED", { productId: sku });
|
|
1986
2027
|
const googleCfg = googleFor(work.oneTimeProduct.packageName);
|
|
1987
2028
|
if (googleCfg?.serviceAccountKey && googleCfg.packageName) {
|
|
1988
2029
|
void acknowledgeGoogleProduct(purchaseToken2, sku, googleCfg).catch(
|
|
1989
|
-
(err2) => log.warn(
|
|
2030
|
+
(err2) => log.warn("[onesub/webhook/google] oneTimeProduct ack failed \u2014 3-day auto-refund risk", {
|
|
2031
|
+
productId: sku,
|
|
2032
|
+
err: err2
|
|
2033
|
+
})
|
|
1990
2034
|
);
|
|
1991
2035
|
}
|
|
1992
2036
|
} else {
|
|
1993
|
-
log.info("[onesub/webhook/google] oneTimeProduct CANCELED (pre-completion)
|
|
2037
|
+
log.info("[onesub/webhook/google] oneTimeProduct CANCELED (pre-completion)", { productId: sku });
|
|
1994
2038
|
}
|
|
1995
2039
|
return;
|
|
1996
2040
|
}
|
|
@@ -2017,7 +2061,7 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
2017
2061
|
const subGoogleCfg = googleFor(packageName);
|
|
2018
2062
|
if (isGooglePriceChangeConfirmedNotification(notificationType) && subGoogleCfg?.onPriceChangeConfirmed) {
|
|
2019
2063
|
const hook = subGoogleCfg.onPriceChangeConfirmed;
|
|
2020
|
-
void Promise.resolve().then(() => hook({ purchaseToken, subscriptionId, packageName })).catch((err2) => log.warn("[onesub/webhook/google] onPriceChangeConfirmed hook failed
|
|
2064
|
+
void Promise.resolve().then(() => hook({ purchaseToken, subscriptionId, packageName })).catch((err2) => log.warn("[onesub/webhook/google] onPriceChangeConfirmed hook failed", { err: err2 }));
|
|
2021
2065
|
}
|
|
2022
2066
|
if (existing) {
|
|
2023
2067
|
let updated = { ...existing, status: finalStatus };
|
|
@@ -2046,7 +2090,11 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
2046
2090
|
const previous = await store.getByTransactionId(fresh.linkedPurchaseToken);
|
|
2047
2091
|
fresh.userId = previous ? previous.userId : boundAccountId ?? purchaseToken;
|
|
2048
2092
|
if (previous) {
|
|
2049
|
-
log.info(
|
|
2093
|
+
log.info("[onesub/webhook/google] inherited userId from linkedPurchaseToken", {
|
|
2094
|
+
userId: previous.userId,
|
|
2095
|
+
linkedPurchaseToken: fresh.linkedPurchaseToken,
|
|
2096
|
+
purchaseToken
|
|
2097
|
+
});
|
|
2050
2098
|
}
|
|
2051
2099
|
} else {
|
|
2052
2100
|
fresh.userId = boundAccountId ?? purchaseToken;
|
|
@@ -2054,13 +2102,20 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
|
|
|
2054
2102
|
await store.save(fresh);
|
|
2055
2103
|
}
|
|
2056
2104
|
} else {
|
|
2057
|
-
log.warn("[onesub/webhook/google] Unknown purchase token and no serviceAccountKey to re-fetch
|
|
2105
|
+
log.warn("[onesub/webhook/google] Unknown purchase token and no serviceAccountKey to re-fetch", {
|
|
2106
|
+
purchaseToken
|
|
2107
|
+
});
|
|
2058
2108
|
}
|
|
2059
2109
|
}
|
|
2060
2110
|
}
|
|
2061
2111
|
async function handleGoogleWebhook(req, res, config, store, purchaseStore, webhookEventStore, webhookQueue) {
|
|
2062
2112
|
const pushIdentities = getAppRegistry(config).apps.map((app) => app.google).filter((g) => !!g?.pushAudience);
|
|
2063
|
-
if (pushIdentities.length
|
|
2113
|
+
if (pushIdentities.length === 0) {
|
|
2114
|
+
if (process.env["NODE_ENV"] === "production" && !googleWebhookAllowsUnauthenticated(config)) {
|
|
2115
|
+
sendError(res, 401, ONESUB_ERROR_CODE.UNAUTHORIZED, "Unauthorized");
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
} else {
|
|
2064
2119
|
let authenticated = false;
|
|
2065
2120
|
for (const google of pushIdentities) {
|
|
2066
2121
|
if (await verifyGooglePushToken(req, google.pushAudience, google.pushServiceAccountEmail)) {
|
|
@@ -2082,7 +2137,9 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2082
2137
|
if (webhookEventStore && typeof body.message.messageId === "string") {
|
|
2083
2138
|
const fresh = await webhookEventStore.markIfNew("google", body.message.messageId);
|
|
2084
2139
|
if (!fresh) {
|
|
2085
|
-
log.info("[onesub/webhook/google] dedupe: already processed",
|
|
2140
|
+
log.info("[onesub/webhook/google] dedupe: already processed", {
|
|
2141
|
+
messageId: body.message.messageId
|
|
2142
|
+
});
|
|
2086
2143
|
res.status(200).json({ received: true, deduped: true });
|
|
2087
2144
|
return;
|
|
2088
2145
|
}
|
|
@@ -2093,7 +2150,9 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2093
2150
|
const voided = decodeGoogleVoidedNotification(body);
|
|
2094
2151
|
if (voided) {
|
|
2095
2152
|
if (!servesPackage(voided.packageName)) {
|
|
2096
|
-
log.warn("[onesub/webhook/google] voided package name not served
|
|
2153
|
+
log.warn("[onesub/webhook/google] voided package name not served", {
|
|
2154
|
+
packageName: voided.packageName
|
|
2155
|
+
});
|
|
2097
2156
|
sendError(res, 400, ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
|
|
2098
2157
|
return;
|
|
2099
2158
|
}
|
|
@@ -2102,7 +2161,9 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2102
2161
|
const oneTimeProduct = decodeGoogleOneTimeProductNotification(body);
|
|
2103
2162
|
if (oneTimeProduct) {
|
|
2104
2163
|
if (!servesPackage(oneTimeProduct.packageName)) {
|
|
2105
|
-
log.warn("[onesub/webhook/google] oneTimeProduct package name not served
|
|
2164
|
+
log.warn("[onesub/webhook/google] oneTimeProduct package name not served", {
|
|
2165
|
+
packageName: oneTimeProduct.packageName
|
|
2166
|
+
});
|
|
2106
2167
|
sendError(res, 400, ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
|
|
2107
2168
|
return;
|
|
2108
2169
|
}
|
|
@@ -2114,7 +2175,9 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2114
2175
|
return;
|
|
2115
2176
|
}
|
|
2116
2177
|
if (!servesPackage(notification.packageName)) {
|
|
2117
|
-
log.warn("[onesub/webhook/google] Package name not served
|
|
2178
|
+
log.warn("[onesub/webhook/google] Package name not served", {
|
|
2179
|
+
packageName: notification.packageName
|
|
2180
|
+
});
|
|
2118
2181
|
sendError(res, 400, ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
|
|
2119
2182
|
return;
|
|
2120
2183
|
}
|
|
@@ -2132,7 +2195,11 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2132
2195
|
});
|
|
2133
2196
|
res.status(200).json({ received: true, queued: true });
|
|
2134
2197
|
} catch (err2) {
|
|
2135
|
-
log.error("[onesub/webhook/google] Failed to enqueue webhook job
|
|
2198
|
+
log.error("[onesub/webhook/google] Failed to enqueue webhook job", {
|
|
2199
|
+
kind: work.kind,
|
|
2200
|
+
messageId: body.message.messageId,
|
|
2201
|
+
err: err2
|
|
2202
|
+
});
|
|
2136
2203
|
await unmarkWebhookEvent(webhookEventStore, "google", markedEventId);
|
|
2137
2204
|
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, GOOGLE_FAILURE_MESSAGES[work.kind].message);
|
|
2138
2205
|
}
|
|
@@ -2143,7 +2210,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
2143
2210
|
res.status(200).json({ received: true });
|
|
2144
2211
|
} catch (err2) {
|
|
2145
2212
|
const { logPrefix, message } = GOOGLE_FAILURE_MESSAGES[work.kind];
|
|
2146
|
-
log.error(logPrefix, err2);
|
|
2213
|
+
log.error(logPrefix, { kind: work.kind, err: err2 });
|
|
2147
2214
|
await unmarkWebhookEvent(webhookEventStore, "google", markedEventId);
|
|
2148
2215
|
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, message);
|
|
2149
2216
|
}
|
|
@@ -2255,9 +2322,10 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2255
2322
|
}
|
|
2256
2323
|
const bindingMismatch = platform === "apple" ? boundAccountId && boundAccountId.toLowerCase() !== userId.toLowerCase() : boundAccountId && boundAccountId !== userId;
|
|
2257
2324
|
if (bindingMismatch) {
|
|
2258
|
-
log.warn(
|
|
2259
|
-
|
|
2260
|
-
|
|
2325
|
+
log.warn("[onesub/purchase] account binding mismatch \u2014 token does not match userId", {
|
|
2326
|
+
transactionId,
|
|
2327
|
+
userId
|
|
2328
|
+
});
|
|
2261
2329
|
sendError(
|
|
2262
2330
|
res,
|
|
2263
2331
|
409,
|
|
@@ -2274,9 +2342,11 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2274
2342
|
if (existing.userId !== userId) {
|
|
2275
2343
|
if (type === PURCHASE_TYPE.NON_CONSUMABLE) {
|
|
2276
2344
|
await purchaseStore.reassignPurchase(transactionId, userId);
|
|
2277
|
-
log.info(
|
|
2278
|
-
|
|
2279
|
-
|
|
2345
|
+
log.info("[onesub/purchase] reassigned transaction to a new user", {
|
|
2346
|
+
transactionId,
|
|
2347
|
+
fromUserId: existing.userId,
|
|
2348
|
+
userId
|
|
2349
|
+
});
|
|
2280
2350
|
} else {
|
|
2281
2351
|
sendError(
|
|
2282
2352
|
res,
|
|
@@ -2320,7 +2390,7 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2320
2390
|
};
|
|
2321
2391
|
res.status(200).json(response);
|
|
2322
2392
|
} catch (err2) {
|
|
2323
|
-
log.error("[onesub/purchase/validate] Unexpected error
|
|
2393
|
+
log.error("[onesub/purchase/validate] Unexpected error", { userId, productId, platform, type, err: err2 });
|
|
2324
2394
|
sendError(res, 500, ONESUB_ERROR_CODE.INTERNAL_ERROR, "Internal server error during purchase validation", NO_PURCHASE);
|
|
2325
2395
|
}
|
|
2326
2396
|
});
|
|
@@ -2335,7 +2405,7 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2335
2405
|
const response = { purchases };
|
|
2336
2406
|
res.status(200).json(response);
|
|
2337
2407
|
} catch (err2) {
|
|
2338
|
-
log.error("[onesub/purchase/status] Store error
|
|
2408
|
+
log.error("[onesub/purchase/status] Store error", { userId, productId, err: err2 });
|
|
2339
2409
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error", { purchases: [] });
|
|
2340
2410
|
}
|
|
2341
2411
|
});
|
|
@@ -2400,7 +2470,7 @@ function createEntitlementRouter(config, store, purchaseStore) {
|
|
|
2400
2470
|
const response = { id, ...status };
|
|
2401
2471
|
res.status(200).json(response);
|
|
2402
2472
|
} catch (err2) {
|
|
2403
|
-
log.error("[onesub/entitlement] evaluation error
|
|
2473
|
+
log.error("[onesub/entitlement] evaluation error", { userId, entitlement: id, err: err2 });
|
|
2404
2474
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2405
2475
|
}
|
|
2406
2476
|
});
|
|
@@ -2424,7 +2494,7 @@ function createEntitlementRouter(config, store, purchaseStore) {
|
|
|
2424
2494
|
};
|
|
2425
2495
|
res.status(200).json(response);
|
|
2426
2496
|
} catch (err2) {
|
|
2427
|
-
log.error("[onesub/entitlements] evaluation error
|
|
2497
|
+
log.error("[onesub/entitlements] evaluation error", { userId, err: err2 });
|
|
2428
2498
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error", { entitlements: {} });
|
|
2429
2499
|
}
|
|
2430
2500
|
});
|
|
@@ -2538,7 +2608,7 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2538
2608
|
};
|
|
2539
2609
|
res.status(200).json(response);
|
|
2540
2610
|
} catch (err2) {
|
|
2541
|
-
log.error("[onesub/admin/subscriptions] list error
|
|
2611
|
+
log.error("[onesub/admin/subscriptions] list error", { err: err2 });
|
|
2542
2612
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2543
2613
|
}
|
|
2544
2614
|
});
|
|
@@ -2558,7 +2628,10 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2558
2628
|
}
|
|
2559
2629
|
res.status(200).json(sub);
|
|
2560
2630
|
} catch (err2) {
|
|
2561
|
-
log.error("[onesub/admin/subscriptions/:transactionId] detail error
|
|
2631
|
+
log.error("[onesub/admin/subscriptions/:transactionId] detail error", {
|
|
2632
|
+
transactionId: params.transactionId,
|
|
2633
|
+
err: err2
|
|
2634
|
+
});
|
|
2562
2635
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2563
2636
|
}
|
|
2564
2637
|
});
|
|
@@ -2591,7 +2664,7 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2591
2664
|
};
|
|
2592
2665
|
res.status(200).json(response);
|
|
2593
2666
|
} catch (err2) {
|
|
2594
|
-
log.error("[onesub/admin/customers/:userId] error
|
|
2667
|
+
log.error("[onesub/admin/customers/:userId] error", { userId: params.userId, err: err2 });
|
|
2595
2668
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2596
2669
|
}
|
|
2597
2670
|
});
|
|
@@ -2621,7 +2694,10 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2621
2694
|
await store.save(fresh);
|
|
2622
2695
|
res.status(200).json({ ok: true, subscription: fresh });
|
|
2623
2696
|
} catch (err2) {
|
|
2624
|
-
log.error("[onesub/admin/sync-apple] error
|
|
2697
|
+
log.error("[onesub/admin/sync-apple] error", {
|
|
2698
|
+
originalTransactionId: params.originalTransactionId,
|
|
2699
|
+
err: err2
|
|
2700
|
+
});
|
|
2625
2701
|
sendError(res, 500, ONESUB_ERROR_CODE.INTERNAL_ERROR, "Internal server error");
|
|
2626
2702
|
}
|
|
2627
2703
|
});
|
|
@@ -2636,9 +2712,10 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2636
2712
|
const body = parseOrSend(res, overrideBodySchema, req.body);
|
|
2637
2713
|
if (!body) return;
|
|
2638
2714
|
setTestOverride(params.userId, body.entitled);
|
|
2639
|
-
log.warn(
|
|
2640
|
-
|
|
2641
|
-
|
|
2715
|
+
log.warn("[onesub/admin] sandbox test override set", {
|
|
2716
|
+
userId: params.userId,
|
|
2717
|
+
entitled: body.entitled
|
|
2718
|
+
});
|
|
2642
2719
|
res.json({ ok: true, userId: params.userId, entitled: body.entitled, sandboxOnly: true });
|
|
2643
2720
|
});
|
|
2644
2721
|
router.delete(ROUTES.ADMIN_TEST_OVERRIDE, (req, res) => {
|
|
@@ -2655,7 +2732,7 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2655
2732
|
const items = await webhookQueue.listDeadLetters();
|
|
2656
2733
|
res.status(200).json({ items });
|
|
2657
2734
|
} catch (err2) {
|
|
2658
|
-
log.error("[onesub/admin/webhook-deadletters] error
|
|
2735
|
+
log.error("[onesub/admin/webhook-deadletters] error", { err: err2 });
|
|
2659
2736
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2660
2737
|
}
|
|
2661
2738
|
});
|
|
@@ -2669,7 +2746,7 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2669
2746
|
await webhookQueue.replayDeadLetter(params.id);
|
|
2670
2747
|
res.status(200).json({ ok: true });
|
|
2671
2748
|
} catch (err2) {
|
|
2672
|
-
log.error("[onesub/admin/webhook-replay] error
|
|
2749
|
+
log.error("[onesub/admin/webhook-replay] error", { deadLetterId: params.id, err: err2 });
|
|
2673
2750
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2674
2751
|
}
|
|
2675
2752
|
});
|
|
@@ -2839,7 +2916,7 @@ function createMetricsRouter(config, store, purchaseStore) {
|
|
|
2839
2916
|
);
|
|
2840
2917
|
res.status(200).json(response);
|
|
2841
2918
|
} catch (err2) {
|
|
2842
|
-
log.error("[onesub/metrics/active] error
|
|
2919
|
+
log.error("[onesub/metrics/active] error", { err: err2 });
|
|
2843
2920
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2844
2921
|
}
|
|
2845
2922
|
});
|
|
@@ -2906,7 +2983,7 @@ function createMetricsRouter(config, store, purchaseStore) {
|
|
|
2906
2983
|
};
|
|
2907
2984
|
res.status(200).json(response);
|
|
2908
2985
|
} catch (err2) {
|
|
2909
|
-
log.error(
|
|
2986
|
+
log.error("[onesub/metrics] error", { route: name, err: err2 });
|
|
2910
2987
|
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2911
2988
|
}
|
|
2912
2989
|
};
|
|
@@ -2978,7 +3055,7 @@ function createAppleOfferRouter(config) {
|
|
|
2978
3055
|
);
|
|
2979
3056
|
res.status(200).json(result);
|
|
2980
3057
|
} catch (err2) {
|
|
2981
|
-
log.error("[onesub/apple/offer] signing error
|
|
3058
|
+
log.error("[onesub/apple/offer] signing error", { productId: body.productId, err: err2 });
|
|
2982
3059
|
sendError(res, 500, ONESUB_ERROR_CODE.INTERNAL_ERROR, "Offer signing failed");
|
|
2983
3060
|
}
|
|
2984
3061
|
});
|
|
@@ -3050,7 +3127,7 @@ async function createPgPool(connectionString, label) {
|
|
|
3050
3127
|
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
3051
3128
|
const pool = new Pool({ connectionString, max: 10 });
|
|
3052
3129
|
pool.on("error", (err2) => {
|
|
3053
|
-
log.error(
|
|
3130
|
+
log.error("[onesub] Postgres pool error (idle client)", { store: label, err: err2 });
|
|
3054
3131
|
});
|
|
3055
3132
|
return pool;
|
|
3056
3133
|
}
|
|
@@ -3791,7 +3868,7 @@ var BullMQWebhookQueue = class {
|
|
|
3791
3868
|
const { Queue } = await this.getBullMQ();
|
|
3792
3869
|
const queue = new Queue(this.queueName, { connection: this.connection });
|
|
3793
3870
|
queue.on?.("error", (err2) => {
|
|
3794
|
-
log.error("[onesub] BullMQ queue error
|
|
3871
|
+
log.error("[onesub] BullMQ queue error", { err: err2 });
|
|
3795
3872
|
});
|
|
3796
3873
|
return queue;
|
|
3797
3874
|
})();
|
|
@@ -3815,13 +3892,13 @@ var BullMQWebhookQueue = class {
|
|
|
3815
3892
|
}
|
|
3816
3893
|
);
|
|
3817
3894
|
worker.on?.("error", (err2) => {
|
|
3818
|
-
log.error("[onesub] BullMQ worker error
|
|
3895
|
+
log.error("[onesub] BullMQ worker error", { err: err2 });
|
|
3819
3896
|
});
|
|
3820
3897
|
return worker;
|
|
3821
3898
|
})();
|
|
3822
3899
|
this.workerPromise.catch((err2) => {
|
|
3823
3900
|
this.workerStartupError = err2 instanceof Error ? err2 : new Error(String(err2));
|
|
3824
|
-
log.error("[onesub] BullMQ worker failed to start
|
|
3901
|
+
log.error("[onesub] BullMQ worker failed to start", { err: err2 });
|
|
3825
3902
|
});
|
|
3826
3903
|
}
|
|
3827
3904
|
}
|