@onesub/server 0.24.0 → 0.25.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,25 @@
1
+ /**
2
+ * Holds every `log.*` call site in the package to the field vocabulary declared in
3
+ * `log-format.ts`.
4
+ *
5
+ * Why a source-scanning test rather than more per-call-site assertions. The value of
6
+ * moving data out of the message is that an operator can filter on `productId` — and
7
+ * that value is destroyed by one file calling it `product`, silently, on a path no
8
+ * test happens to exercise. That is not hypothetical: while building
9
+ * `provider-log-fields.test.ts` a mutation renaming `productId` to `product` at the
10
+ * "No orderId in product purchase" site **passed all 13 of its tests**, because that
11
+ * site is not one of the ones asserted. Covering ~100 sites individually to close
12
+ * that gap is not maintainable; checking the vocabulary once, over all of them, is.
13
+ *
14
+ * The vocabulary lives in `log-format.ts`'s module doc and is parsed out of it here,
15
+ * so there is exactly one list. A doc comment is an odd place for an enforced
16
+ * contract, but the alternative — an exported `const` array — would ship a
17
+ * test-only value in the bundle that `npm run size` gates.
18
+ *
19
+ * What this test cannot do: it does not know whether a field is *correctly named*
20
+ * for its value, only that the name is one of the sanctioned ones. `bundleId:
21
+ * tx.productId` passes here. Per-site assertions in `provider-log-fields.test.ts`
22
+ * cover that for the paths that matter.
23
+ */
24
+ export {};
25
+ //# sourceMappingURL=field-vocabulary.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-vocabulary.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/field-vocabulary.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * What the Apple and Google providers actually put on a log line.
3
+ *
4
+ * These paths had **no log assertions at all** before this file, which is worth
5
+ * stating plainly: the 49 call sites in `providers/apple.ts` and
6
+ * `providers/google.ts` could have been migrated to `(message, fields)` — or
7
+ * broken — and the whole suite would have stayed green either way. So this file
8
+ * is not belt-and-braces on top of `log-format.test.ts`; it is the only thing
9
+ * that holds the providers to the contract.
10
+ *
11
+ * It tests through the real provider functions rather than through
12
+ * `formatLogArgs`, because the two failure modes that matter are call-site
13
+ * failures a pure formatter test cannot see:
14
+ *
15
+ * 1. **A value left in the message.** `formatLogArgs` escapes whatever it is
16
+ * given; it cannot know that `bundleId` should have been a field. Only an
17
+ * assertion on the rendered line catches an interpolated value.
18
+ * 2. **A field named differently in two places.** The point of moving values
19
+ * out of the message is that an operator can filter on `productId`. That is
20
+ * lost the moment one file calls it `product` — and nothing but an exact
21
+ * assertion notices.
22
+ *
23
+ * Hence exact-line assertions, not `toContain`. A substring match would pass on
24
+ * `productId=x` *and* on a message that happened to mention it.
25
+ */
26
+ export {};
27
+ //# sourceMappingURL=provider-log-fields.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-log-fields.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/provider-log-fields.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG"}
package/dist/index.cjs CHANGED
@@ -624,22 +624,23 @@ async function validateAppleReceipt(receipt, config) {
624
624
  } catch (err2) {
625
625
  const preview = receipt.slice(0, 60);
626
626
  const parts = receipt.split(".").length;
627
- log.warn(
628
- "[onesub/apple] Failed to decode receipt as JWS:",
629
- err2?.message ?? err2,
630
- `| preview: "${preview}..." (len=${receipt.length}, parts=${parts})`
631
- );
627
+ log.warn("[onesub/apple] Failed to decode receipt as JWS", {
628
+ receiptPreview: preview,
629
+ receiptLength: receipt.length,
630
+ jwsParts: parts,
631
+ err: err2
632
+ });
632
633
  return null;
633
634
  }
634
635
  if (!tx.originalTransactionId || !tx.productId || !tx.expiresDate) {
635
636
  return null;
636
637
  }
637
638
  if (!tx.bundleId || tx.bundleId !== config.bundleId) {
638
- log.warn("[onesub/apple] Bundle ID mismatch:", tx.bundleId, "!==", config.bundleId);
639
+ log.warn("[onesub/apple] Bundle ID mismatch", { bundleId: tx.bundleId, expected: config.bundleId });
639
640
  return null;
640
641
  }
641
642
  if (process.env["NODE_ENV"] === "production" && tx.environment !== "Production" && process.env["ONESUB_ALLOW_SANDBOX"] !== "true") {
642
- log.warn("[onesub/apple] Sandbox receipt rejected in production:", tx.environment);
643
+ log.warn("[onesub/apple] Sandbox receipt rejected in production", { environment: tx.environment });
643
644
  return null;
644
645
  }
645
646
  const status = deriveStatus(tx, null);
@@ -679,23 +680,25 @@ async function validateAppleConsumableReceipt(signedTransaction, config, expecte
679
680
  const preview = signedTransaction.slice(0, 60);
680
681
  const parts = signedTransaction.split(".").length;
681
682
  const looksLikeJws = parts === 3;
682
- log.warn(
683
- "[onesub/apple] Failed to decode consumable JWS:",
684
- err2?.message ?? err2,
685
- `| receipt preview: "${preview}..." (len=${signedTransaction.length}, parts=${parts}, looksLikeJws=${looksLikeJws})`
686
- );
683
+ log.warn("[onesub/apple] Failed to decode consumable JWS", {
684
+ receiptPreview: preview,
685
+ receiptLength: signedTransaction.length,
686
+ jwsParts: parts,
687
+ looksLikeJws,
688
+ err: err2
689
+ });
687
690
  return null;
688
691
  }
689
692
  if (!tx.bundleId || tx.bundleId !== config.bundleId) {
690
- log.warn("[onesub/apple] Bundle ID mismatch:", tx.bundleId, "!==", config.bundleId);
693
+ log.warn("[onesub/apple] Bundle ID mismatch", { bundleId: tx.bundleId, expected: config.bundleId });
691
694
  return null;
692
695
  }
693
696
  if (tx.type !== "Consumable" && tx.type !== "Non-Consumable") {
694
- log.warn("[onesub/apple] Invalid purchase type for product validation:", tx.type);
697
+ log.warn("[onesub/apple] Invalid purchase type for product validation", { type: tx.type });
695
698
  return null;
696
699
  }
697
700
  if (process.env["NODE_ENV"] === "production" && tx.environment !== "Production" && process.env["ONESUB_ALLOW_SANDBOX"] !== "true") {
698
- log.warn("[onesub/apple] Sandbox receipt rejected in production:", tx.environment);
701
+ log.warn("[onesub/apple] Sandbox receipt rejected in production", { environment: tx.environment });
699
702
  return null;
700
703
  }
701
704
  if (!tx.productId) {
@@ -703,21 +706,28 @@ async function validateAppleConsumableReceipt(signedTransaction, config, expecte
703
706
  return null;
704
707
  }
705
708
  if (expectedProductId && tx.productId !== expectedProductId) {
706
- log.warn("[onesub/apple] Product ID mismatch:", tx.productId, "!==", expectedProductId);
709
+ log.warn("[onesub/apple] Product ID mismatch", { productId: tx.productId, expected: expectedProductId });
707
710
  return null;
708
711
  }
709
712
  if (tx.revocationDate) {
710
- log.warn("[onesub/apple] Purchase was revoked/refunded");
713
+ log.warn("[onesub/apple] Purchase was revoked/refunded", {
714
+ productId: tx.productId,
715
+ transactionId: tx.transactionId
716
+ });
711
717
  return null;
712
718
  }
713
719
  const maxAgeMs = (config.productReceiptMaxAgeHours ?? 72) * 60 * 60 * 1e3;
714
720
  if (tx.purchaseDate && Date.now() - tx.purchaseDate > maxAgeMs) {
715
- log.warn(`[onesub/apple] Consumable receipt too old (>${config.productReceiptMaxAgeHours ?? 72}h)`);
721
+ log.warn("[onesub/apple] Consumable receipt too old", {
722
+ productId: tx.productId,
723
+ maxAgeHours: config.productReceiptMaxAgeHours ?? 72,
724
+ purchaseDate: new Date(tx.purchaseDate).toISOString()
725
+ });
716
726
  return null;
717
727
  }
718
728
  const transactionId = tx.transactionId ?? tx.originalTransactionId;
719
729
  if (!transactionId) {
720
- log.warn("[onesub/apple] No transactionId in consumable transaction");
730
+ log.warn("[onesub/apple] No transactionId in consumable transaction", { productId: tx.productId });
721
731
  return null;
722
732
  }
723
733
  return {
@@ -796,7 +806,7 @@ async function sendAppleConsumptionResponse(transactionId, body, config, options
796
806
  try {
797
807
  jwt = await makeAppleApiJwt(config);
798
808
  } catch (err2) {
799
- log.warn("[onesub/apple] Cannot send consumption response \u2014 JWT mint failed:", err2);
809
+ log.warn("[onesub/apple] Cannot send consumption response \u2014 JWT mint failed", { transactionId, err: err2 });
800
810
  return;
801
811
  }
802
812
  const host = options?.sandbox ? "api.storekit-sandbox.itunes.apple.com" : "api.storekit.itunes.apple.com";
@@ -812,10 +822,14 @@ async function sendAppleConsumptionResponse(transactionId, body, config, options
812
822
  });
813
823
  if (!resp.ok) {
814
824
  const text = await resp.text();
815
- log.warn(`[onesub/apple] Consumption response API error ${resp.status}: ${text}`);
825
+ log.warn("[onesub/apple] Consumption response API error", {
826
+ httpStatus: resp.status,
827
+ transactionId,
828
+ responseBody: text
829
+ });
816
830
  }
817
831
  } catch (err2) {
818
- log.warn("[onesub/apple] Consumption response network error:", err2);
832
+ log.warn("[onesub/apple] Consumption response network error", { transactionId, err: err2 });
819
833
  }
820
834
  }
821
835
  var APPLE_SUBSCRIPTION_STATUS_CODE = {
@@ -846,7 +860,10 @@ async function fetchAppleSubscriptionStatus(originalTransactionId, config, optio
846
860
  try {
847
861
  jwt = await makeAppleApiJwt(config);
848
862
  } catch (err2) {
849
- log.warn("[onesub/apple] Cannot fetch subscription status \u2014 JWT mint failed:", err2);
863
+ log.warn("[onesub/apple] Cannot fetch subscription status \u2014 JWT mint failed", {
864
+ originalTransactionId,
865
+ err: err2
866
+ });
850
867
  return null;
851
868
  }
852
869
  const host = options?.sandbox ? "api.storekit-sandbox.itunes.apple.com" : "api.storekit.itunes.apple.com";
@@ -858,24 +875,31 @@ async function fetchAppleSubscriptionStatus(originalTransactionId, config, optio
858
875
  });
859
876
  if (!resp.ok) {
860
877
  const text = await resp.text();
861
- log.warn(`[onesub/apple] Status API error ${resp.status}: ${text}`);
878
+ log.warn("[onesub/apple] Status API error", {
879
+ httpStatus: resp.status,
880
+ originalTransactionId,
881
+ responseBody: text
882
+ });
862
883
  return null;
863
884
  }
864
885
  body = await resp.json();
865
886
  } catch (err2) {
866
- log.warn("[onesub/apple] Status API network error:", err2);
887
+ log.warn("[onesub/apple] Status API network error", { originalTransactionId, err: err2 });
867
888
  return null;
868
889
  }
869
890
  const entry = body.data?.flatMap((g) => g.lastTransactions ?? []).find((t) => t.originalTransactionId === originalTransactionId);
870
891
  if (!entry || entry.status == null || !entry.signedTransactionInfo) {
871
- log.warn("[onesub/apple] Status API returned no matching transaction for", originalTransactionId);
892
+ log.warn("[onesub/apple] Status API returned no matching transaction", { originalTransactionId });
872
893
  return null;
873
894
  }
874
895
  let tx;
875
896
  try {
876
897
  tx = await decodeJws(entry.signedTransactionInfo, config.skipJwsVerification);
877
898
  } catch (err2) {
878
- log.warn("[onesub/apple] Failed to decode signedTransactionInfo from Status API:", err2);
899
+ log.warn("[onesub/apple] Failed to decode signedTransactionInfo from Status API", {
900
+ originalTransactionId,
901
+ err: err2
902
+ });
879
903
  return null;
880
904
  }
881
905
  let renewal = null;
@@ -886,7 +910,10 @@ async function fetchAppleSubscriptionStatus(originalTransactionId, config, optio
886
910
  }
887
911
  }
888
912
  if (!tx.productId || !tx.expiresDate) {
889
- log.warn("[onesub/apple] Status API transaction missing productId or expiresDate");
913
+ log.warn("[onesub/apple] Status API transaction missing productId or expiresDate", {
914
+ originalTransactionId,
915
+ productId: tx.productId
916
+ });
890
917
  return null;
891
918
  }
892
919
  const status = mapAppleStatusCode(entry.status);
@@ -910,7 +937,10 @@ async function fetchAppleTransactionHistory(originalTransactionId, config, optio
910
937
  try {
911
938
  jwt = await makeAppleApiJwt(config);
912
939
  } catch (err2) {
913
- log.warn("[onesub/apple] Cannot fetch transaction history \u2014 JWT mint failed:", err2);
940
+ log.warn("[onesub/apple] Cannot fetch transaction history \u2014 JWT mint failed", {
941
+ originalTransactionId,
942
+ err: err2
943
+ });
914
944
  return null;
915
945
  }
916
946
  const host = options?.sandbox ? "api.storekit-sandbox.itunes.apple.com" : "api.storekit.itunes.apple.com";
@@ -927,12 +957,16 @@ async function fetchAppleTransactionHistory(originalTransactionId, config, optio
927
957
  });
928
958
  if (!resp.ok) {
929
959
  const text = await resp.text();
930
- log.warn(`[onesub/apple] Transaction History API error ${resp.status}: ${text}`);
960
+ log.warn("[onesub/apple] Transaction History API error", {
961
+ httpStatus: resp.status,
962
+ originalTransactionId,
963
+ responseBody: text
964
+ });
931
965
  return null;
932
966
  }
933
967
  page = await resp.json();
934
968
  } catch (err2) {
935
- log.warn("[onesub/apple] Transaction History API network error:", err2);
969
+ log.warn("[onesub/apple] Transaction History API network error", { originalTransactionId, err: err2 });
936
970
  return null;
937
971
  }
938
972
  for (const signed of page.signedTransactions ?? []) {
@@ -957,14 +991,16 @@ async function fetchAppleTransactionHistory(originalTransactionId, config, optio
957
991
  if (!page.hasMore) break;
958
992
  if (!page.revision || page.revision === revision) {
959
993
  log.warn(
960
- "[onesub/apple] Transaction History API returned hasMore without a new revision cursor \u2014 stopping pagination (partial history returned)"
994
+ "[onesub/apple] Transaction History API returned hasMore without a new revision cursor \u2014 stopping pagination (partial history returned)",
995
+ { originalTransactionId, pageCount }
961
996
  );
962
997
  break;
963
998
  }
964
999
  if (pageCount >= MAX_HISTORY_PAGES) {
965
- log.warn(
966
- `[onesub/apple] Transaction History pagination hit the ${MAX_HISTORY_PAGES}-page cap \u2014 stopping (partial history returned)`
967
- );
1000
+ log.warn("[onesub/apple] Transaction History pagination hit the page cap \u2014 stopping (partial history returned)", {
1001
+ originalTransactionId,
1002
+ maxPages: MAX_HISTORY_PAGES
1003
+ });
968
1004
  break;
969
1005
  }
970
1006
  revision = page.revision;
@@ -1114,7 +1150,7 @@ async function acknowledgeGoogleSubscription(purchaseToken, productId, config) {
1114
1150
  try {
1115
1151
  accessToken = await getCachedAccessToken(config.serviceAccountKey);
1116
1152
  } catch (err2) {
1117
- log.warn("[onesub/google] Could not get access token for subscription ack:", err2);
1153
+ log.warn("[onesub/google] Could not get access token for subscription ack", { productId, err: err2 });
1118
1154
  return;
1119
1155
  }
1120
1156
  const url = `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${encodeURIComponent(config.packageName)}/purchases/subscriptions/${encodeURIComponent(productId)}/tokens/${encodeURIComponent(purchaseToken)}:acknowledge`;
@@ -1126,10 +1162,14 @@ async function acknowledgeGoogleSubscription(purchaseToken, productId, config) {
1126
1162
  });
1127
1163
  if (!resp.ok) {
1128
1164
  const body = await resp.text();
1129
- log.warn(`[onesub/google] Subscription acknowledge API error ${resp.status}: ${body} \u2014 auto-refund risk`);
1165
+ log.warn("[onesub/google] Subscription acknowledge API error \u2014 auto-refund risk", {
1166
+ httpStatus: resp.status,
1167
+ productId,
1168
+ responseBody: body
1169
+ });
1130
1170
  }
1131
1171
  } catch (err2) {
1132
- log.warn("[onesub/google] Subscription acknowledge network error \u2014 auto-refund risk:", err2);
1172
+ log.warn("[onesub/google] Subscription acknowledge network error \u2014 auto-refund risk", { productId, err: err2 });
1133
1173
  }
1134
1174
  }
1135
1175
  async function acknowledgeGoogleProduct(purchaseToken, productId, config) {
@@ -1140,7 +1180,7 @@ async function acknowledgeGoogleProduct(purchaseToken, productId, config) {
1140
1180
  try {
1141
1181
  accessToken = await getCachedAccessToken(config.serviceAccountKey);
1142
1182
  } catch (err2) {
1143
- log.warn("[onesub/google] Could not get access token for product ack:", err2);
1183
+ log.warn("[onesub/google] Could not get access token for product ack", { productId, err: err2 });
1144
1184
  return;
1145
1185
  }
1146
1186
  const url = `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${encodeURIComponent(config.packageName)}/purchases/products/${encodeURIComponent(productId)}/tokens/${encodeURIComponent(purchaseToken)}:acknowledge`;
@@ -1152,10 +1192,14 @@ async function acknowledgeGoogleProduct(purchaseToken, productId, config) {
1152
1192
  });
1153
1193
  if (!resp.ok) {
1154
1194
  const body = await resp.text();
1155
- log.warn(`[onesub/google] Product acknowledge API error ${resp.status}: ${body} \u2014 auto-refund risk`);
1195
+ log.warn("[onesub/google] Product acknowledge API error \u2014 auto-refund risk", {
1196
+ httpStatus: resp.status,
1197
+ productId,
1198
+ responseBody: body
1199
+ });
1156
1200
  }
1157
1201
  } catch (err2) {
1158
- log.warn("[onesub/google] Product acknowledge network error \u2014 auto-refund risk:", err2);
1202
+ log.warn("[onesub/google] Product acknowledge network error \u2014 auto-refund risk", { productId, err: err2 });
1159
1203
  }
1160
1204
  }
1161
1205
  async function consumeGoogleProductReceipt(purchaseToken, productId, config) {
@@ -1165,7 +1209,7 @@ async function consumeGoogleProductReceipt(purchaseToken, productId, config) {
1165
1209
  try {
1166
1210
  accessToken = await getCachedAccessToken(config.serviceAccountKey);
1167
1211
  } catch (err2) {
1168
- log.warn("[onesub/google] Could not get access token for consume:", err2);
1212
+ log.warn("[onesub/google] Could not get access token for consume", { productId, err: err2 });
1169
1213
  return;
1170
1214
  }
1171
1215
  const url = `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${encodeURIComponent(config.packageName)}/purchases/products/${encodeURIComponent(productId)}/tokens/${encodeURIComponent(purchaseToken)}:consume`;
@@ -1176,10 +1220,14 @@ async function consumeGoogleProductReceipt(purchaseToken, productId, config) {
1176
1220
  });
1177
1221
  if (!resp.ok) {
1178
1222
  const body = await resp.text();
1179
- log.warn(`[onesub/google] Consume API error ${resp.status}: ${body} \u2014 auto-refund risk`);
1223
+ log.warn("[onesub/google] Consume API error \u2014 auto-refund risk", {
1224
+ httpStatus: resp.status,
1225
+ productId,
1226
+ responseBody: body
1227
+ });
1180
1228
  }
1181
1229
  } catch (err2) {
1182
- log.warn("[onesub/google] Consume network error \u2014 auto-refund risk:", err2);
1230
+ log.warn("[onesub/google] Consume network error \u2014 auto-refund risk", { productId, err: err2 });
1183
1231
  }
1184
1232
  }
1185
1233
  function deriveStatusV2(state) {
@@ -1205,11 +1253,11 @@ function deriveStatusV2(state) {
1205
1253
  async function validateGoogleReceipt(receipt, productId, config) {
1206
1254
  if (config.mockMode) return mockValidateGoogleSubscription(receipt, productId);
1207
1255
  if (!config.serviceAccountKey) {
1208
- log.warn("[onesub/google] No serviceAccountKey provided \u2014 cannot call Play API");
1256
+ log.warn("[onesub/google] No serviceAccountKey provided \u2014 cannot call Play API", { productId });
1209
1257
  return null;
1210
1258
  }
1211
1259
  if (!config.packageName) {
1212
- log.warn("[onesub/google] No packageName provided \u2014 cannot call Play API");
1260
+ log.warn("[onesub/google] No packageName provided \u2014 cannot call Play API", { productId });
1213
1261
  return null;
1214
1262
  }
1215
1263
  let purchase;
@@ -1217,25 +1265,27 @@ async function validateGoogleReceipt(receipt, productId, config) {
1217
1265
  const token = await getCachedAccessToken(config.serviceAccountKey);
1218
1266
  purchase = await fetchSubscriptionPurchaseV2(config.packageName, receipt, token);
1219
1267
  } catch (err2) {
1220
- log.error("[onesub/google] Receipt validation failed:", err2);
1268
+ log.error("[onesub/google] Receipt validation failed", {
1269
+ productId,
1270
+ packageName: config.packageName,
1271
+ err: err2
1272
+ });
1221
1273
  return null;
1222
1274
  }
1223
1275
  const status = deriveStatusV2(purchase.subscriptionState);
1224
1276
  if (!status) {
1225
- log.warn(
1226
- "[onesub/google] Unrecognised or pending subscriptionState \u2014 rejecting:",
1227
- purchase.subscriptionState
1228
- );
1277
+ log.warn("[onesub/google] Unrecognised or pending subscriptionState \u2014 rejecting", {
1278
+ productId,
1279
+ subscriptionState: purchase.subscriptionState
1280
+ });
1229
1281
  return null;
1230
1282
  }
1231
1283
  const lineItem = purchase.lineItems?.find((item) => item.productId === productId);
1232
1284
  if (!lineItem) {
1233
- log.warn(
1234
- "[onesub/google] productId not found in subscription lineItems:",
1285
+ log.warn("[onesub/google] productId not found in subscription lineItems", {
1235
1286
  productId,
1236
- "available:",
1237
- purchase.lineItems?.map((i) => i.productId).join(", ") ?? "(none)"
1238
- );
1287
+ availableProductIds: purchase.lineItems?.map((i) => i.productId) ?? []
1288
+ });
1239
1289
  return null;
1240
1290
  }
1241
1291
  const expiresAt = lineItem.expiryTime ?? (/* @__PURE__ */ new Date()).toISOString();
@@ -1267,11 +1317,11 @@ async function validateGoogleReceipt(receipt, productId, config) {
1267
1317
  async function validateGoogleProductReceipt(purchaseToken, productId, config, type = "non_consumable") {
1268
1318
  if (config.mockMode) return mockValidateGoogleProduct(purchaseToken, productId);
1269
1319
  if (!config.serviceAccountKey) {
1270
- log.warn("[onesub/google] No serviceAccountKey \u2014 cannot validate product receipt");
1320
+ log.warn("[onesub/google] No serviceAccountKey \u2014 cannot validate product receipt", { productId });
1271
1321
  return null;
1272
1322
  }
1273
1323
  if (!config.packageName) {
1274
- log.warn("[onesub/google] No packageName \u2014 cannot validate product receipt");
1324
+ log.warn("[onesub/google] No packageName \u2014 cannot validate product receipt", { productId });
1275
1325
  return null;
1276
1326
  }
1277
1327
  let purchase;
@@ -1279,27 +1329,44 @@ async function validateGoogleProductReceipt(purchaseToken, productId, config, ty
1279
1329
  const token = await getCachedAccessToken(config.serviceAccountKey);
1280
1330
  purchase = await fetchProductPurchase(config.packageName, productId, purchaseToken, token);
1281
1331
  } catch (err2) {
1282
- log.error("[onesub/google] Product receipt validation failed:", err2);
1332
+ log.error("[onesub/google] Product receipt validation failed", {
1333
+ productId,
1334
+ packageName: config.packageName,
1335
+ type,
1336
+ err: err2
1337
+ });
1283
1338
  return null;
1284
1339
  }
1285
1340
  if (purchase.purchaseState !== 0) {
1286
- log.warn("[onesub/google] Purchase not completed, state:", purchase.purchaseState);
1341
+ log.warn("[onesub/google] Purchase not completed", {
1342
+ productId,
1343
+ purchaseState: purchase.purchaseState,
1344
+ orderId: purchase.orderId
1345
+ });
1287
1346
  return null;
1288
1347
  }
1289
1348
  if (type === "consumable" && purchase.consumptionState === 1) {
1290
- log.warn("[onesub/google] Consumable already consumed \u2014 possible replay attack");
1349
+ log.warn("[onesub/google] Consumable already consumed \u2014 possible replay attack", {
1350
+ productId,
1351
+ orderId: purchase.orderId
1352
+ });
1291
1353
  return null;
1292
1354
  }
1293
1355
  if (purchase.purchaseTimeMillis) {
1294
1356
  const purchaseTime = parseInt(purchase.purchaseTimeMillis, 10);
1295
1357
  const maxAgeMs = (config.productReceiptMaxAgeHours ?? 72) * 60 * 60 * 1e3;
1296
1358
  if (Date.now() - purchaseTime > maxAgeMs) {
1297
- log.warn(`[onesub/google] Product receipt too old (>${config.productReceiptMaxAgeHours ?? 72}h)`);
1359
+ log.warn("[onesub/google] Product receipt too old", {
1360
+ productId,
1361
+ orderId: purchase.orderId,
1362
+ maxAgeHours: config.productReceiptMaxAgeHours ?? 72,
1363
+ purchaseDate: new Date(purchaseTime).toISOString()
1364
+ });
1298
1365
  return null;
1299
1366
  }
1300
1367
  }
1301
1368
  if (!purchase.orderId) {
1302
- log.warn("[onesub/google] No orderId in product purchase");
1369
+ log.warn("[onesub/google] No orderId in product purchase", { productId });
1303
1370
  return null;
1304
1371
  }
1305
1372
  return {
@@ -1356,7 +1423,11 @@ function decodeGoogleOneTimeProductNotification(payload) {
1356
1423
  if (!notification.oneTimeProductNotification) return null;
1357
1424
  const { notificationType, purchaseToken, sku } = notification.oneTimeProductNotification;
1358
1425
  if (notificationType !== 1 && notificationType !== 2) {
1359
- log.warn("[onesub/google] Unknown oneTimeProductNotification type:", notificationType);
1426
+ log.warn("[onesub/google] Unknown oneTimeProductNotification type", {
1427
+ notificationType,
1428
+ productId: sku,
1429
+ packageName: notification.packageName
1430
+ });
1360
1431
  return null;
1361
1432
  }
1362
1433
  return { notificationType, purchaseToken, sku, packageName: notification.packageName };