@onesub/server 0.23.3 → 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.
package/dist/index.js CHANGED
@@ -163,21 +163,131 @@ at+qIxUCMG1mihDK1A3UT82NQz60imOlM27jbdoXt2QfyFMm+YhidDkLF1vLUagM
163
163
  -----END CERTIFICATE-----`;
164
164
  var APPLE_ROOT_CA_PEMS = [APPLE_ROOT_CA_G3_PEM];
165
165
 
166
+ // src/log-format.ts
167
+ var LOG_CONTINUATION = "\n | ";
168
+ var BARE_VALUE = /^[\w.:/@+-]+$/;
169
+ var LINE_TERMINATORS = /\r\n|\r|\n|\u2028|\u2029/;
170
+ var MAX_CAUSE_DEPTH = 2;
171
+ var MAX_AGGREGATE_ERRORS = 3;
172
+ function esc(value) {
173
+ return value.replace(/\\/g, "\\\\").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
174
+ }
175
+ function escQuoted(value) {
176
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
177
+ }
178
+ function renderValue(value) {
179
+ if (value === void 0) return void 0;
180
+ if (value === null) return "null";
181
+ switch (typeof value) {
182
+ case "number":
183
+ case "boolean":
184
+ return String(value);
185
+ case "bigint":
186
+ return `${value}n`;
187
+ case "string":
188
+ return BARE_VALUE.test(value) ? value : `"${escQuoted(value)}"`;
189
+ case "object":
190
+ try {
191
+ return `"${escQuoted(JSON.stringify(value) ?? "null")}"`;
192
+ } catch {
193
+ return '"[unserialisable]"';
194
+ }
195
+ default:
196
+ try {
197
+ return `"${escQuoted(String(value))}"`;
198
+ } catch {
199
+ return '"[unrenderable]"';
200
+ }
201
+ }
202
+ }
203
+ function renderFields(fields) {
204
+ const pairs = [];
205
+ let err2;
206
+ for (const key of Object.keys(fields)) {
207
+ let value;
208
+ try {
209
+ value = fields[key];
210
+ } catch {
211
+ pairs.push(`${esc(key)}="[threw]"`);
212
+ continue;
213
+ }
214
+ if (key === "err") {
215
+ err2 = value;
216
+ continue;
217
+ }
218
+ const rendered = renderValue(value);
219
+ if (rendered !== void 0) pairs.push(`${esc(key)}=${rendered}`);
220
+ }
221
+ return { pairs, err: err2 };
222
+ }
223
+ function renderStack(stack) {
224
+ if (!stack) return "";
225
+ const lines = stack.split(LINE_TERMINATORS);
226
+ const firstFrame = lines.findIndex((line) => /^\s+at\s/.test(line));
227
+ const frames = firstFrame === -1 ? [] : lines.slice(firstFrame);
228
+ return frames.map((line) => LOG_CONTINUATION + esc(line.trim())).join("");
229
+ }
230
+ function renderError(value, depth = 0) {
231
+ if (!(value instanceof Error)) {
232
+ const rendered = renderValue(typeof value === "string" ? value : String(value));
233
+ return { pairs: [`err.msg=${rendered ?? '"[unrenderable]"'}`], lines: "" };
234
+ }
235
+ const pairs = [`err=${renderValue(value.name) ?? "Error"}`];
236
+ const message = renderValue(value.message);
237
+ if (message !== void 0) pairs.push(`err.msg=${message}`);
238
+ let lines = renderStack(value.stack);
239
+ if (depth < MAX_CAUSE_DEPTH && value.cause !== void 0 && value.cause !== null) {
240
+ const cause = renderError(value.cause, depth + 1);
241
+ lines += LOG_CONTINUATION + `cause: ${cause.pairs.join(" ")}` + cause.lines;
242
+ }
243
+ if (value instanceof AggregateError && Array.isArray(value.errors)) {
244
+ for (const inner of value.errors.slice(0, MAX_AGGREGATE_ERRORS)) {
245
+ const rendered = renderError(inner, MAX_CAUSE_DEPTH);
246
+ lines += LOG_CONTINUATION + `also: ${rendered.pairs.join(" ")}`;
247
+ }
248
+ }
249
+ return { pairs, lines };
250
+ }
251
+ function formatLogArgs(args) {
252
+ const words = [];
253
+ const pairs = [];
254
+ let errorLines = "";
255
+ for (const arg of args) {
256
+ if (typeof arg === "string") {
257
+ words.push(esc(arg));
258
+ continue;
259
+ }
260
+ if (arg instanceof Error) {
261
+ const rendered2 = renderError(arg);
262
+ pairs.push(...rendered2.pairs);
263
+ errorLines += rendered2.lines;
264
+ continue;
265
+ }
266
+ if (arg !== null && typeof arg === "object" && !Array.isArray(arg)) {
267
+ const { pairs: fieldPairs, err: err2 } = renderFields(arg);
268
+ pairs.push(...fieldPairs);
269
+ if (err2 !== void 0) {
270
+ const rendered2 = renderError(err2);
271
+ pairs.push(...rendered2.pairs);
272
+ errorLines += rendered2.lines;
273
+ }
274
+ continue;
275
+ }
276
+ const rendered = renderValue(arg);
277
+ if (rendered !== void 0) words.push(rendered);
278
+ }
279
+ return [words.join(" "), ...pairs].filter((part) => part !== "").join(" ") + errorLines;
280
+ }
281
+
166
282
  // src/logger.ts
167
283
  var current = console;
168
284
  function setLogger(logger) {
169
285
  if (logger) current = logger;
170
286
  }
171
- function escapeLineBreaks(value) {
172
- return value.replace(/\\/g, "\\\\").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
173
- }
174
- function scrub(args) {
175
- return args.map((arg) => typeof arg === "string" ? escapeLineBreaks(arg) : arg);
176
- }
177
287
  var log = {
178
- info: (...args) => current.info(...scrub(args)),
179
- warn: (...args) => current.warn(...scrub(args)),
180
- error: (...args) => current.error(...scrub(args))
288
+ info: (...args) => current.info(formatLogArgs(args)),
289
+ warn: (...args) => current.warn(formatLogArgs(args)),
290
+ error: (...args) => current.error(formatLogArgs(args))
181
291
  };
182
292
 
183
293
  // src/http.ts
@@ -507,22 +617,23 @@ async function validateAppleReceipt(receipt, config) {
507
617
  } catch (err2) {
508
618
  const preview = receipt.slice(0, 60);
509
619
  const parts = receipt.split(".").length;
510
- log.warn(
511
- "[onesub/apple] Failed to decode receipt as JWS:",
512
- err2?.message ?? err2,
513
- `| preview: "${preview}..." (len=${receipt.length}, parts=${parts})`
514
- );
620
+ log.warn("[onesub/apple] Failed to decode receipt as JWS", {
621
+ receiptPreview: preview,
622
+ receiptLength: receipt.length,
623
+ jwsParts: parts,
624
+ err: err2
625
+ });
515
626
  return null;
516
627
  }
517
628
  if (!tx.originalTransactionId || !tx.productId || !tx.expiresDate) {
518
629
  return null;
519
630
  }
520
631
  if (!tx.bundleId || tx.bundleId !== config.bundleId) {
521
- log.warn("[onesub/apple] Bundle ID mismatch:", tx.bundleId, "!==", config.bundleId);
632
+ log.warn("[onesub/apple] Bundle ID mismatch", { bundleId: tx.bundleId, expected: config.bundleId });
522
633
  return null;
523
634
  }
524
635
  if (process.env["NODE_ENV"] === "production" && tx.environment !== "Production" && process.env["ONESUB_ALLOW_SANDBOX"] !== "true") {
525
- log.warn("[onesub/apple] Sandbox receipt rejected in production:", tx.environment);
636
+ log.warn("[onesub/apple] Sandbox receipt rejected in production", { environment: tx.environment });
526
637
  return null;
527
638
  }
528
639
  const status = deriveStatus(tx, null);
@@ -562,23 +673,25 @@ async function validateAppleConsumableReceipt(signedTransaction, config, expecte
562
673
  const preview = signedTransaction.slice(0, 60);
563
674
  const parts = signedTransaction.split(".").length;
564
675
  const looksLikeJws = parts === 3;
565
- log.warn(
566
- "[onesub/apple] Failed to decode consumable JWS:",
567
- err2?.message ?? err2,
568
- `| receipt preview: "${preview}..." (len=${signedTransaction.length}, parts=${parts}, looksLikeJws=${looksLikeJws})`
569
- );
676
+ log.warn("[onesub/apple] Failed to decode consumable JWS", {
677
+ receiptPreview: preview,
678
+ receiptLength: signedTransaction.length,
679
+ jwsParts: parts,
680
+ looksLikeJws,
681
+ err: err2
682
+ });
570
683
  return null;
571
684
  }
572
685
  if (!tx.bundleId || tx.bundleId !== config.bundleId) {
573
- log.warn("[onesub/apple] Bundle ID mismatch:", tx.bundleId, "!==", config.bundleId);
686
+ log.warn("[onesub/apple] Bundle ID mismatch", { bundleId: tx.bundleId, expected: config.bundleId });
574
687
  return null;
575
688
  }
576
689
  if (tx.type !== "Consumable" && tx.type !== "Non-Consumable") {
577
- log.warn("[onesub/apple] Invalid purchase type for product validation:", tx.type);
690
+ log.warn("[onesub/apple] Invalid purchase type for product validation", { type: tx.type });
578
691
  return null;
579
692
  }
580
693
  if (process.env["NODE_ENV"] === "production" && tx.environment !== "Production" && process.env["ONESUB_ALLOW_SANDBOX"] !== "true") {
581
- log.warn("[onesub/apple] Sandbox receipt rejected in production:", tx.environment);
694
+ log.warn("[onesub/apple] Sandbox receipt rejected in production", { environment: tx.environment });
582
695
  return null;
583
696
  }
584
697
  if (!tx.productId) {
@@ -586,21 +699,28 @@ async function validateAppleConsumableReceipt(signedTransaction, config, expecte
586
699
  return null;
587
700
  }
588
701
  if (expectedProductId && tx.productId !== expectedProductId) {
589
- log.warn("[onesub/apple] Product ID mismatch:", tx.productId, "!==", expectedProductId);
702
+ log.warn("[onesub/apple] Product ID mismatch", { productId: tx.productId, expected: expectedProductId });
590
703
  return null;
591
704
  }
592
705
  if (tx.revocationDate) {
593
- log.warn("[onesub/apple] Purchase was revoked/refunded");
706
+ log.warn("[onesub/apple] Purchase was revoked/refunded", {
707
+ productId: tx.productId,
708
+ transactionId: tx.transactionId
709
+ });
594
710
  return null;
595
711
  }
596
712
  const maxAgeMs = (config.productReceiptMaxAgeHours ?? 72) * 60 * 60 * 1e3;
597
713
  if (tx.purchaseDate && Date.now() - tx.purchaseDate > maxAgeMs) {
598
- log.warn(`[onesub/apple] Consumable receipt too old (>${config.productReceiptMaxAgeHours ?? 72}h)`);
714
+ log.warn("[onesub/apple] Consumable receipt too old", {
715
+ productId: tx.productId,
716
+ maxAgeHours: config.productReceiptMaxAgeHours ?? 72,
717
+ purchaseDate: new Date(tx.purchaseDate).toISOString()
718
+ });
599
719
  return null;
600
720
  }
601
721
  const transactionId = tx.transactionId ?? tx.originalTransactionId;
602
722
  if (!transactionId) {
603
- log.warn("[onesub/apple] No transactionId in consumable transaction");
723
+ log.warn("[onesub/apple] No transactionId in consumable transaction", { productId: tx.productId });
604
724
  return null;
605
725
  }
606
726
  return {
@@ -679,7 +799,7 @@ async function sendAppleConsumptionResponse(transactionId, body, config, options
679
799
  try {
680
800
  jwt = await makeAppleApiJwt(config);
681
801
  } catch (err2) {
682
- log.warn("[onesub/apple] Cannot send consumption response \u2014 JWT mint failed:", err2);
802
+ log.warn("[onesub/apple] Cannot send consumption response \u2014 JWT mint failed", { transactionId, err: err2 });
683
803
  return;
684
804
  }
685
805
  const host = options?.sandbox ? "api.storekit-sandbox.itunes.apple.com" : "api.storekit.itunes.apple.com";
@@ -695,10 +815,14 @@ async function sendAppleConsumptionResponse(transactionId, body, config, options
695
815
  });
696
816
  if (!resp.ok) {
697
817
  const text = await resp.text();
698
- log.warn(`[onesub/apple] Consumption response API error ${resp.status}: ${text}`);
818
+ log.warn("[onesub/apple] Consumption response API error", {
819
+ httpStatus: resp.status,
820
+ transactionId,
821
+ responseBody: text
822
+ });
699
823
  }
700
824
  } catch (err2) {
701
- log.warn("[onesub/apple] Consumption response network error:", err2);
825
+ log.warn("[onesub/apple] Consumption response network error", { transactionId, err: err2 });
702
826
  }
703
827
  }
704
828
  var APPLE_SUBSCRIPTION_STATUS_CODE = {
@@ -729,7 +853,10 @@ async function fetchAppleSubscriptionStatus(originalTransactionId, config, optio
729
853
  try {
730
854
  jwt = await makeAppleApiJwt(config);
731
855
  } catch (err2) {
732
- log.warn("[onesub/apple] Cannot fetch subscription status \u2014 JWT mint failed:", err2);
856
+ log.warn("[onesub/apple] Cannot fetch subscription status \u2014 JWT mint failed", {
857
+ originalTransactionId,
858
+ err: err2
859
+ });
733
860
  return null;
734
861
  }
735
862
  const host = options?.sandbox ? "api.storekit-sandbox.itunes.apple.com" : "api.storekit.itunes.apple.com";
@@ -741,24 +868,31 @@ async function fetchAppleSubscriptionStatus(originalTransactionId, config, optio
741
868
  });
742
869
  if (!resp.ok) {
743
870
  const text = await resp.text();
744
- log.warn(`[onesub/apple] Status API error ${resp.status}: ${text}`);
871
+ log.warn("[onesub/apple] Status API error", {
872
+ httpStatus: resp.status,
873
+ originalTransactionId,
874
+ responseBody: text
875
+ });
745
876
  return null;
746
877
  }
747
878
  body = await resp.json();
748
879
  } catch (err2) {
749
- log.warn("[onesub/apple] Status API network error:", err2);
880
+ log.warn("[onesub/apple] Status API network error", { originalTransactionId, err: err2 });
750
881
  return null;
751
882
  }
752
883
  const entry = body.data?.flatMap((g) => g.lastTransactions ?? []).find((t) => t.originalTransactionId === originalTransactionId);
753
884
  if (!entry || entry.status == null || !entry.signedTransactionInfo) {
754
- log.warn("[onesub/apple] Status API returned no matching transaction for", originalTransactionId);
885
+ log.warn("[onesub/apple] Status API returned no matching transaction", { originalTransactionId });
755
886
  return null;
756
887
  }
757
888
  let tx;
758
889
  try {
759
890
  tx = await decodeJws(entry.signedTransactionInfo, config.skipJwsVerification);
760
891
  } catch (err2) {
761
- log.warn("[onesub/apple] Failed to decode signedTransactionInfo from Status API:", err2);
892
+ log.warn("[onesub/apple] Failed to decode signedTransactionInfo from Status API", {
893
+ originalTransactionId,
894
+ err: err2
895
+ });
762
896
  return null;
763
897
  }
764
898
  let renewal = null;
@@ -769,7 +903,10 @@ async function fetchAppleSubscriptionStatus(originalTransactionId, config, optio
769
903
  }
770
904
  }
771
905
  if (!tx.productId || !tx.expiresDate) {
772
- log.warn("[onesub/apple] Status API transaction missing productId or expiresDate");
906
+ log.warn("[onesub/apple] Status API transaction missing productId or expiresDate", {
907
+ originalTransactionId,
908
+ productId: tx.productId
909
+ });
773
910
  return null;
774
911
  }
775
912
  const status = mapAppleStatusCode(entry.status);
@@ -793,7 +930,10 @@ async function fetchAppleTransactionHistory(originalTransactionId, config, optio
793
930
  try {
794
931
  jwt = await makeAppleApiJwt(config);
795
932
  } catch (err2) {
796
- log.warn("[onesub/apple] Cannot fetch transaction history \u2014 JWT mint failed:", err2);
933
+ log.warn("[onesub/apple] Cannot fetch transaction history \u2014 JWT mint failed", {
934
+ originalTransactionId,
935
+ err: err2
936
+ });
797
937
  return null;
798
938
  }
799
939
  const host = options?.sandbox ? "api.storekit-sandbox.itunes.apple.com" : "api.storekit.itunes.apple.com";
@@ -810,12 +950,16 @@ async function fetchAppleTransactionHistory(originalTransactionId, config, optio
810
950
  });
811
951
  if (!resp.ok) {
812
952
  const text = await resp.text();
813
- log.warn(`[onesub/apple] Transaction History API error ${resp.status}: ${text}`);
953
+ log.warn("[onesub/apple] Transaction History API error", {
954
+ httpStatus: resp.status,
955
+ originalTransactionId,
956
+ responseBody: text
957
+ });
814
958
  return null;
815
959
  }
816
960
  page = await resp.json();
817
961
  } catch (err2) {
818
- log.warn("[onesub/apple] Transaction History API network error:", err2);
962
+ log.warn("[onesub/apple] Transaction History API network error", { originalTransactionId, err: err2 });
819
963
  return null;
820
964
  }
821
965
  for (const signed of page.signedTransactions ?? []) {
@@ -840,14 +984,16 @@ async function fetchAppleTransactionHistory(originalTransactionId, config, optio
840
984
  if (!page.hasMore) break;
841
985
  if (!page.revision || page.revision === revision) {
842
986
  log.warn(
843
- "[onesub/apple] Transaction History API returned hasMore without a new revision cursor \u2014 stopping pagination (partial history returned)"
987
+ "[onesub/apple] Transaction History API returned hasMore without a new revision cursor \u2014 stopping pagination (partial history returned)",
988
+ { originalTransactionId, pageCount }
844
989
  );
845
990
  break;
846
991
  }
847
992
  if (pageCount >= MAX_HISTORY_PAGES) {
848
- log.warn(
849
- `[onesub/apple] Transaction History pagination hit the ${MAX_HISTORY_PAGES}-page cap \u2014 stopping (partial history returned)`
850
- );
993
+ log.warn("[onesub/apple] Transaction History pagination hit the page cap \u2014 stopping (partial history returned)", {
994
+ originalTransactionId,
995
+ maxPages: MAX_HISTORY_PAGES
996
+ });
851
997
  break;
852
998
  }
853
999
  revision = page.revision;
@@ -997,7 +1143,7 @@ async function acknowledgeGoogleSubscription(purchaseToken, productId, config) {
997
1143
  try {
998
1144
  accessToken = await getCachedAccessToken(config.serviceAccountKey);
999
1145
  } catch (err2) {
1000
- log.warn("[onesub/google] Could not get access token for subscription ack:", err2);
1146
+ log.warn("[onesub/google] Could not get access token for subscription ack", { productId, err: err2 });
1001
1147
  return;
1002
1148
  }
1003
1149
  const url = `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${encodeURIComponent(config.packageName)}/purchases/subscriptions/${encodeURIComponent(productId)}/tokens/${encodeURIComponent(purchaseToken)}:acknowledge`;
@@ -1009,10 +1155,14 @@ async function acknowledgeGoogleSubscription(purchaseToken, productId, config) {
1009
1155
  });
1010
1156
  if (!resp.ok) {
1011
1157
  const body = await resp.text();
1012
- log.warn(`[onesub/google] Subscription acknowledge API error ${resp.status}: ${body} \u2014 auto-refund risk`);
1158
+ log.warn("[onesub/google] Subscription acknowledge API error \u2014 auto-refund risk", {
1159
+ httpStatus: resp.status,
1160
+ productId,
1161
+ responseBody: body
1162
+ });
1013
1163
  }
1014
1164
  } catch (err2) {
1015
- log.warn("[onesub/google] Subscription acknowledge network error \u2014 auto-refund risk:", err2);
1165
+ log.warn("[onesub/google] Subscription acknowledge network error \u2014 auto-refund risk", { productId, err: err2 });
1016
1166
  }
1017
1167
  }
1018
1168
  async function acknowledgeGoogleProduct(purchaseToken, productId, config) {
@@ -1023,7 +1173,7 @@ async function acknowledgeGoogleProduct(purchaseToken, productId, config) {
1023
1173
  try {
1024
1174
  accessToken = await getCachedAccessToken(config.serviceAccountKey);
1025
1175
  } catch (err2) {
1026
- log.warn("[onesub/google] Could not get access token for product ack:", err2);
1176
+ log.warn("[onesub/google] Could not get access token for product ack", { productId, err: err2 });
1027
1177
  return;
1028
1178
  }
1029
1179
  const url = `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${encodeURIComponent(config.packageName)}/purchases/products/${encodeURIComponent(productId)}/tokens/${encodeURIComponent(purchaseToken)}:acknowledge`;
@@ -1035,10 +1185,14 @@ async function acknowledgeGoogleProduct(purchaseToken, productId, config) {
1035
1185
  });
1036
1186
  if (!resp.ok) {
1037
1187
  const body = await resp.text();
1038
- log.warn(`[onesub/google] Product acknowledge API error ${resp.status}: ${body} \u2014 auto-refund risk`);
1188
+ log.warn("[onesub/google] Product acknowledge API error \u2014 auto-refund risk", {
1189
+ httpStatus: resp.status,
1190
+ productId,
1191
+ responseBody: body
1192
+ });
1039
1193
  }
1040
1194
  } catch (err2) {
1041
- log.warn("[onesub/google] Product acknowledge network error \u2014 auto-refund risk:", err2);
1195
+ log.warn("[onesub/google] Product acknowledge network error \u2014 auto-refund risk", { productId, err: err2 });
1042
1196
  }
1043
1197
  }
1044
1198
  async function consumeGoogleProductReceipt(purchaseToken, productId, config) {
@@ -1048,7 +1202,7 @@ async function consumeGoogleProductReceipt(purchaseToken, productId, config) {
1048
1202
  try {
1049
1203
  accessToken = await getCachedAccessToken(config.serviceAccountKey);
1050
1204
  } catch (err2) {
1051
- log.warn("[onesub/google] Could not get access token for consume:", err2);
1205
+ log.warn("[onesub/google] Could not get access token for consume", { productId, err: err2 });
1052
1206
  return;
1053
1207
  }
1054
1208
  const url = `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${encodeURIComponent(config.packageName)}/purchases/products/${encodeURIComponent(productId)}/tokens/${encodeURIComponent(purchaseToken)}:consume`;
@@ -1059,10 +1213,14 @@ async function consumeGoogleProductReceipt(purchaseToken, productId, config) {
1059
1213
  });
1060
1214
  if (!resp.ok) {
1061
1215
  const body = await resp.text();
1062
- log.warn(`[onesub/google] Consume API error ${resp.status}: ${body} \u2014 auto-refund risk`);
1216
+ log.warn("[onesub/google] Consume API error \u2014 auto-refund risk", {
1217
+ httpStatus: resp.status,
1218
+ productId,
1219
+ responseBody: body
1220
+ });
1063
1221
  }
1064
1222
  } catch (err2) {
1065
- log.warn("[onesub/google] Consume network error \u2014 auto-refund risk:", err2);
1223
+ log.warn("[onesub/google] Consume network error \u2014 auto-refund risk", { productId, err: err2 });
1066
1224
  }
1067
1225
  }
1068
1226
  function deriveStatusV2(state) {
@@ -1088,11 +1246,11 @@ function deriveStatusV2(state) {
1088
1246
  async function validateGoogleReceipt(receipt, productId, config) {
1089
1247
  if (config.mockMode) return mockValidateGoogleSubscription(receipt, productId);
1090
1248
  if (!config.serviceAccountKey) {
1091
- log.warn("[onesub/google] No serviceAccountKey provided \u2014 cannot call Play API");
1249
+ log.warn("[onesub/google] No serviceAccountKey provided \u2014 cannot call Play API", { productId });
1092
1250
  return null;
1093
1251
  }
1094
1252
  if (!config.packageName) {
1095
- log.warn("[onesub/google] No packageName provided \u2014 cannot call Play API");
1253
+ log.warn("[onesub/google] No packageName provided \u2014 cannot call Play API", { productId });
1096
1254
  return null;
1097
1255
  }
1098
1256
  let purchase;
@@ -1100,25 +1258,27 @@ async function validateGoogleReceipt(receipt, productId, config) {
1100
1258
  const token = await getCachedAccessToken(config.serviceAccountKey);
1101
1259
  purchase = await fetchSubscriptionPurchaseV2(config.packageName, receipt, token);
1102
1260
  } catch (err2) {
1103
- log.error("[onesub/google] Receipt validation failed:", err2);
1261
+ log.error("[onesub/google] Receipt validation failed", {
1262
+ productId,
1263
+ packageName: config.packageName,
1264
+ err: err2
1265
+ });
1104
1266
  return null;
1105
1267
  }
1106
1268
  const status = deriveStatusV2(purchase.subscriptionState);
1107
1269
  if (!status) {
1108
- log.warn(
1109
- "[onesub/google] Unrecognised or pending subscriptionState \u2014 rejecting:",
1110
- purchase.subscriptionState
1111
- );
1270
+ log.warn("[onesub/google] Unrecognised or pending subscriptionState \u2014 rejecting", {
1271
+ productId,
1272
+ subscriptionState: purchase.subscriptionState
1273
+ });
1112
1274
  return null;
1113
1275
  }
1114
1276
  const lineItem = purchase.lineItems?.find((item) => item.productId === productId);
1115
1277
  if (!lineItem) {
1116
- log.warn(
1117
- "[onesub/google] productId not found in subscription lineItems:",
1278
+ log.warn("[onesub/google] productId not found in subscription lineItems", {
1118
1279
  productId,
1119
- "available:",
1120
- purchase.lineItems?.map((i) => i.productId).join(", ") ?? "(none)"
1121
- );
1280
+ availableProductIds: purchase.lineItems?.map((i) => i.productId) ?? []
1281
+ });
1122
1282
  return null;
1123
1283
  }
1124
1284
  const expiresAt = lineItem.expiryTime ?? (/* @__PURE__ */ new Date()).toISOString();
@@ -1150,11 +1310,11 @@ async function validateGoogleReceipt(receipt, productId, config) {
1150
1310
  async function validateGoogleProductReceipt(purchaseToken, productId, config, type = "non_consumable") {
1151
1311
  if (config.mockMode) return mockValidateGoogleProduct(purchaseToken, productId);
1152
1312
  if (!config.serviceAccountKey) {
1153
- log.warn("[onesub/google] No serviceAccountKey \u2014 cannot validate product receipt");
1313
+ log.warn("[onesub/google] No serviceAccountKey \u2014 cannot validate product receipt", { productId });
1154
1314
  return null;
1155
1315
  }
1156
1316
  if (!config.packageName) {
1157
- log.warn("[onesub/google] No packageName \u2014 cannot validate product receipt");
1317
+ log.warn("[onesub/google] No packageName \u2014 cannot validate product receipt", { productId });
1158
1318
  return null;
1159
1319
  }
1160
1320
  let purchase;
@@ -1162,27 +1322,44 @@ async function validateGoogleProductReceipt(purchaseToken, productId, config, ty
1162
1322
  const token = await getCachedAccessToken(config.serviceAccountKey);
1163
1323
  purchase = await fetchProductPurchase(config.packageName, productId, purchaseToken, token);
1164
1324
  } catch (err2) {
1165
- log.error("[onesub/google] Product receipt validation failed:", err2);
1325
+ log.error("[onesub/google] Product receipt validation failed", {
1326
+ productId,
1327
+ packageName: config.packageName,
1328
+ type,
1329
+ err: err2
1330
+ });
1166
1331
  return null;
1167
1332
  }
1168
1333
  if (purchase.purchaseState !== 0) {
1169
- log.warn("[onesub/google] Purchase not completed, state:", purchase.purchaseState);
1334
+ log.warn("[onesub/google] Purchase not completed", {
1335
+ productId,
1336
+ purchaseState: purchase.purchaseState,
1337
+ orderId: purchase.orderId
1338
+ });
1170
1339
  return null;
1171
1340
  }
1172
1341
  if (type === "consumable" && purchase.consumptionState === 1) {
1173
- log.warn("[onesub/google] Consumable already consumed \u2014 possible replay attack");
1342
+ log.warn("[onesub/google] Consumable already consumed \u2014 possible replay attack", {
1343
+ productId,
1344
+ orderId: purchase.orderId
1345
+ });
1174
1346
  return null;
1175
1347
  }
1176
1348
  if (purchase.purchaseTimeMillis) {
1177
1349
  const purchaseTime = parseInt(purchase.purchaseTimeMillis, 10);
1178
1350
  const maxAgeMs = (config.productReceiptMaxAgeHours ?? 72) * 60 * 60 * 1e3;
1179
1351
  if (Date.now() - purchaseTime > maxAgeMs) {
1180
- log.warn(`[onesub/google] Product receipt too old (>${config.productReceiptMaxAgeHours ?? 72}h)`);
1352
+ log.warn("[onesub/google] Product receipt too old", {
1353
+ productId,
1354
+ orderId: purchase.orderId,
1355
+ maxAgeHours: config.productReceiptMaxAgeHours ?? 72,
1356
+ purchaseDate: new Date(purchaseTime).toISOString()
1357
+ });
1181
1358
  return null;
1182
1359
  }
1183
1360
  }
1184
1361
  if (!purchase.orderId) {
1185
- log.warn("[onesub/google] No orderId in product purchase");
1362
+ log.warn("[onesub/google] No orderId in product purchase", { productId });
1186
1363
  return null;
1187
1364
  }
1188
1365
  return {
@@ -1239,7 +1416,11 @@ function decodeGoogleOneTimeProductNotification(payload) {
1239
1416
  if (!notification.oneTimeProductNotification) return null;
1240
1417
  const { notificationType, purchaseToken, sku } = notification.oneTimeProductNotification;
1241
1418
  if (notificationType !== 1 && notificationType !== 2) {
1242
- log.warn("[onesub/google] Unknown oneTimeProductNotification type:", notificationType);
1419
+ log.warn("[onesub/google] Unknown oneTimeProductNotification type", {
1420
+ notificationType,
1421
+ productId: sku,
1422
+ packageName: notification.packageName
1423
+ });
1243
1424
  return null;
1244
1425
  }
1245
1426
  return { notificationType, purchaseToken, sku, packageName: notification.packageName };