@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/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
- return BARE_VALUE.test(value) ? value : `"${escQuoted(value)}"`;
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(`[onesub/mock/${tag}] receipt rejected`, { outcome: outcome.kind });
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:", apps.map(appName).join(", "), "| default:", defaultApp ? appName(defaultApp) : "(none)");
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:", hint.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:", hint.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
- `[onesub/validate] account binding mismatch for ${sub.originalTransactionId}: receipt token does not match userId ${userId}`
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(`[onesub/validate] sandbox test override active for ${userId}: forcing not-entitled`);
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:", err2);
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:", err2);
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:", err2);
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:", lookupId);
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:", correctedUserId);
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(`[onesub/webhook/apple] FAMILY_SHARED \u2014 userId: ${fresh.userId} (source: ${source})`);
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:", err2);
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", payload.notificationUUID);
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:", decoded.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:", err2);
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:", err2);
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
- const googleApps = apps.map((a) => a.google).filter((g) => !!g);
1949
- if (!googleApps.some((g) => g.pushAudience)) {
1950
- log.warn(
1951
- "[onesub] SECURITY: POST /onesub/webhook/google accepts unauthenticated requests \u2014 no configured app sets google.pushAudience, so the Pub/Sub OIDC token is never verified. A caller who knows a purchaseToken or orderId can cancel a subscription or delete a one-time purchase. Set google.pushAudience (and google.pushServiceAccountEmail)."
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:", voided.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:", voided.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:", sku);
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(`[onesub/webhook/google] oneTimeProduct ack failed for SKU ${sku} \u2014 3-day auto-refund risk:`, err2)
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):", sku);
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:", err2));
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(`[onesub/webhook/google] inherited userId ${previous.userId} from linkedPurchaseToken ${fresh.linkedPurchaseToken} \u2192 new token ${purchaseToken}`);
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:", purchaseToken);
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 > 0) {
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", body.message.messageId);
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:", voided.packageName);
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:", oneTimeProduct.packageName);
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:", notification.packageName);
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:", err2);
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
- `[onesub/purchase] account binding mismatch for transaction ${transactionId}: token does not match userId ${userId}`
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
- `[onesub/purchase] reassigned transaction ${transactionId} from ${existing.userId} to ${userId}`
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:", err2);
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:", err2);
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:", err2);
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:", err2);
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:", err2);
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:", err2);
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:", err2);
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:", err2);
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
- `[onesub/admin] sandbox test override set for ${params.userId}: entitled=${body.entitled}`
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:", err2);
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:", err2);
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:", err2);
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(`[onesub/metrics/${name}] error:`, err2);
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:", err2);
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(`[onesub] ${label} pool error (idle client):`, err2);
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:", err2);
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:", err2);
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:", err2);
3908
+ log.error("[onesub] BullMQ worker failed to start", { err: err2 });
3832
3909
  });
3833
3910
  }
3834
3911
  }