@onesub/server 0.25.0 → 0.26.0

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