@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.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
- return BARE_VALUE.test(value) ? value : `"${escQuoted(value)}"`;
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(`[onesub/mock/${tag}] receipt rejected`, { outcome: outcome.kind });
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:", apps.map(appName).join(", "), "| default:", defaultApp ? appName(defaultApp) : "(none)");
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:", hint.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:", hint.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
- `[onesub/validate] account binding mismatch for ${sub.originalTransactionId}: receipt token does not match userId ${userId}`
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(`[onesub/validate] sandbox test override active for ${userId}: forcing not-entitled`);
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:", err2);
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:", err2);
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:", err2);
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:", lookupId);
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:", correctedUserId);
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(`[onesub/webhook/apple] FAMILY_SHARED \u2014 userId: ${fresh.userId} (source: ${source})`);
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:", err2);
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", payload.notificationUUID);
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:", decoded.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:", err2);
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:", err2);
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
- const googleApps = apps.map((a) => a.google).filter((g) => !!g);
1942
- if (!googleApps.some((g) => g.pushAudience)) {
1943
- log.warn(
1944
- "[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)."
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:", voided.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:", voided.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:", sku);
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(`[onesub/webhook/google] oneTimeProduct ack failed for SKU ${sku} \u2014 3-day auto-refund risk:`, err2)
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):", sku);
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:", err2));
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(`[onesub/webhook/google] inherited userId ${previous.userId} from linkedPurchaseToken ${fresh.linkedPurchaseToken} \u2192 new token ${purchaseToken}`);
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:", purchaseToken);
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 > 0) {
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", body.message.messageId);
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:", voided.packageName);
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:", oneTimeProduct.packageName);
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:", notification.packageName);
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:", err2);
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
- `[onesub/purchase] account binding mismatch for transaction ${transactionId}: token does not match userId ${userId}`
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
- `[onesub/purchase] reassigned transaction ${transactionId} from ${existing.userId} to ${userId}`
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:", err2);
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:", err2);
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:", err2);
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:", err2);
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:", err2);
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:", err2);
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:", err2);
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:", err2);
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
- `[onesub/admin] sandbox test override set for ${params.userId}: entitled=${body.entitled}`
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:", err2);
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:", err2);
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:", err2);
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(`[onesub/metrics/${name}] error:`, err2);
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:", err2);
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(`[onesub] ${label} pool error (idle client):`, err2);
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:", err2);
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:", err2);
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:", err2);
3901
+ log.error("[onesub] BullMQ worker failed to start", { err: err2 });
3825
3902
  });
3826
3903
  }
3827
3904
  }