@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.cjs CHANGED
@@ -170,21 +170,131 @@ at+qIxUCMG1mihDK1A3UT82NQz60imOlM27jbdoXt2QfyFMm+YhidDkLF1vLUagM
170
170
  -----END CERTIFICATE-----`;
171
171
  var APPLE_ROOT_CA_PEMS = [APPLE_ROOT_CA_G3_PEM];
172
172
 
173
+ // src/log-format.ts
174
+ var LOG_CONTINUATION = "\n | ";
175
+ var BARE_VALUE = /^[\w.:/@+-]+$/;
176
+ var LINE_TERMINATORS = /\r\n|\r|\n|\u2028|\u2029/;
177
+ var MAX_CAUSE_DEPTH = 2;
178
+ var MAX_AGGREGATE_ERRORS = 3;
179
+ function esc(value) {
180
+ return value.replace(/\\/g, "\\\\").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
181
+ }
182
+ function escQuoted(value) {
183
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
184
+ }
185
+ function renderValue(value) {
186
+ if (value === void 0) return void 0;
187
+ if (value === null) return "null";
188
+ switch (typeof value) {
189
+ case "number":
190
+ case "boolean":
191
+ return String(value);
192
+ case "bigint":
193
+ return `${value}n`;
194
+ case "string":
195
+ return BARE_VALUE.test(value) ? value : `"${escQuoted(value)}"`;
196
+ case "object":
197
+ try {
198
+ return `"${escQuoted(JSON.stringify(value) ?? "null")}"`;
199
+ } catch {
200
+ return '"[unserialisable]"';
201
+ }
202
+ default:
203
+ try {
204
+ return `"${escQuoted(String(value))}"`;
205
+ } catch {
206
+ return '"[unrenderable]"';
207
+ }
208
+ }
209
+ }
210
+ function renderFields(fields) {
211
+ const pairs = [];
212
+ let err2;
213
+ for (const key of Object.keys(fields)) {
214
+ let value;
215
+ try {
216
+ value = fields[key];
217
+ } catch {
218
+ pairs.push(`${esc(key)}="[threw]"`);
219
+ continue;
220
+ }
221
+ if (key === "err") {
222
+ err2 = value;
223
+ continue;
224
+ }
225
+ const rendered = renderValue(value);
226
+ if (rendered !== void 0) pairs.push(`${esc(key)}=${rendered}`);
227
+ }
228
+ return { pairs, err: err2 };
229
+ }
230
+ function renderStack(stack) {
231
+ if (!stack) return "";
232
+ const lines = stack.split(LINE_TERMINATORS);
233
+ const firstFrame = lines.findIndex((line) => /^\s+at\s/.test(line));
234
+ const frames = firstFrame === -1 ? [] : lines.slice(firstFrame);
235
+ return frames.map((line) => LOG_CONTINUATION + esc(line.trim())).join("");
236
+ }
237
+ function renderError(value, depth = 0) {
238
+ if (!(value instanceof Error)) {
239
+ const rendered = renderValue(typeof value === "string" ? value : String(value));
240
+ return { pairs: [`err.msg=${rendered ?? '"[unrenderable]"'}`], lines: "" };
241
+ }
242
+ const pairs = [`err=${renderValue(value.name) ?? "Error"}`];
243
+ const message = renderValue(value.message);
244
+ if (message !== void 0) pairs.push(`err.msg=${message}`);
245
+ let lines = renderStack(value.stack);
246
+ if (depth < MAX_CAUSE_DEPTH && value.cause !== void 0 && value.cause !== null) {
247
+ const cause = renderError(value.cause, depth + 1);
248
+ lines += LOG_CONTINUATION + `cause: ${cause.pairs.join(" ")}` + cause.lines;
249
+ }
250
+ if (value instanceof AggregateError && Array.isArray(value.errors)) {
251
+ for (const inner of value.errors.slice(0, MAX_AGGREGATE_ERRORS)) {
252
+ const rendered = renderError(inner, MAX_CAUSE_DEPTH);
253
+ lines += LOG_CONTINUATION + `also: ${rendered.pairs.join(" ")}`;
254
+ }
255
+ }
256
+ return { pairs, lines };
257
+ }
258
+ function formatLogArgs(args) {
259
+ const words = [];
260
+ const pairs = [];
261
+ let errorLines = "";
262
+ for (const arg of args) {
263
+ if (typeof arg === "string") {
264
+ words.push(esc(arg));
265
+ continue;
266
+ }
267
+ if (arg instanceof Error) {
268
+ const rendered2 = renderError(arg);
269
+ pairs.push(...rendered2.pairs);
270
+ errorLines += rendered2.lines;
271
+ continue;
272
+ }
273
+ if (arg !== null && typeof arg === "object" && !Array.isArray(arg)) {
274
+ const { pairs: fieldPairs, err: err2 } = renderFields(arg);
275
+ pairs.push(...fieldPairs);
276
+ if (err2 !== void 0) {
277
+ const rendered2 = renderError(err2);
278
+ pairs.push(...rendered2.pairs);
279
+ errorLines += rendered2.lines;
280
+ }
281
+ continue;
282
+ }
283
+ const rendered = renderValue(arg);
284
+ if (rendered !== void 0) words.push(rendered);
285
+ }
286
+ return [words.join(" "), ...pairs].filter((part) => part !== "").join(" ") + errorLines;
287
+ }
288
+
173
289
  // src/logger.ts
174
290
  var current = console;
175
291
  function setLogger(logger) {
176
292
  if (logger) current = logger;
177
293
  }
178
- function escapeLineBreaks(value) {
179
- return value.replace(/\\/g, "\\\\").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
180
- }
181
- function scrub(args) {
182
- return args.map((arg) => typeof arg === "string" ? escapeLineBreaks(arg) : arg);
183
- }
184
294
  var log = {
185
- info: (...args) => current.info(...scrub(args)),
186
- warn: (...args) => current.warn(...scrub(args)),
187
- error: (...args) => current.error(...scrub(args))
295
+ info: (...args) => current.info(formatLogArgs(args)),
296
+ warn: (...args) => current.warn(formatLogArgs(args)),
297
+ error: (...args) => current.error(formatLogArgs(args))
188
298
  };
189
299
 
190
300
  // src/http.ts
@@ -514,22 +624,23 @@ async function validateAppleReceipt(receipt, config) {
514
624
  } catch (err2) {
515
625
  const preview = receipt.slice(0, 60);
516
626
  const parts = receipt.split(".").length;
517
- log.warn(
518
- "[onesub/apple] Failed to decode receipt as JWS:",
519
- err2?.message ?? err2,
520
- `| preview: "${preview}..." (len=${receipt.length}, parts=${parts})`
521
- );
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
+ });
522
633
  return null;
523
634
  }
524
635
  if (!tx.originalTransactionId || !tx.productId || !tx.expiresDate) {
525
636
  return null;
526
637
  }
527
638
  if (!tx.bundleId || tx.bundleId !== config.bundleId) {
528
- 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 });
529
640
  return null;
530
641
  }
531
642
  if (process.env["NODE_ENV"] === "production" && tx.environment !== "Production" && process.env["ONESUB_ALLOW_SANDBOX"] !== "true") {
532
- log.warn("[onesub/apple] Sandbox receipt rejected in production:", tx.environment);
643
+ log.warn("[onesub/apple] Sandbox receipt rejected in production", { environment: tx.environment });
533
644
  return null;
534
645
  }
535
646
  const status = deriveStatus(tx, null);
@@ -569,23 +680,25 @@ async function validateAppleConsumableReceipt(signedTransaction, config, expecte
569
680
  const preview = signedTransaction.slice(0, 60);
570
681
  const parts = signedTransaction.split(".").length;
571
682
  const looksLikeJws = parts === 3;
572
- log.warn(
573
- "[onesub/apple] Failed to decode consumable JWS:",
574
- err2?.message ?? err2,
575
- `| receipt preview: "${preview}..." (len=${signedTransaction.length}, parts=${parts}, looksLikeJws=${looksLikeJws})`
576
- );
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
+ });
577
690
  return null;
578
691
  }
579
692
  if (!tx.bundleId || tx.bundleId !== config.bundleId) {
580
- 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 });
581
694
  return null;
582
695
  }
583
696
  if (tx.type !== "Consumable" && tx.type !== "Non-Consumable") {
584
- 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 });
585
698
  return null;
586
699
  }
587
700
  if (process.env["NODE_ENV"] === "production" && tx.environment !== "Production" && process.env["ONESUB_ALLOW_SANDBOX"] !== "true") {
588
- log.warn("[onesub/apple] Sandbox receipt rejected in production:", tx.environment);
701
+ log.warn("[onesub/apple] Sandbox receipt rejected in production", { environment: tx.environment });
589
702
  return null;
590
703
  }
591
704
  if (!tx.productId) {
@@ -593,21 +706,28 @@ async function validateAppleConsumableReceipt(signedTransaction, config, expecte
593
706
  return null;
594
707
  }
595
708
  if (expectedProductId && tx.productId !== expectedProductId) {
596
- log.warn("[onesub/apple] Product ID mismatch:", tx.productId, "!==", expectedProductId);
709
+ log.warn("[onesub/apple] Product ID mismatch", { productId: tx.productId, expected: expectedProductId });
597
710
  return null;
598
711
  }
599
712
  if (tx.revocationDate) {
600
- 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
+ });
601
717
  return null;
602
718
  }
603
719
  const maxAgeMs = (config.productReceiptMaxAgeHours ?? 72) * 60 * 60 * 1e3;
604
720
  if (tx.purchaseDate && Date.now() - tx.purchaseDate > maxAgeMs) {
605
- 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
+ });
606
726
  return null;
607
727
  }
608
728
  const transactionId = tx.transactionId ?? tx.originalTransactionId;
609
729
  if (!transactionId) {
610
- log.warn("[onesub/apple] No transactionId in consumable transaction");
730
+ log.warn("[onesub/apple] No transactionId in consumable transaction", { productId: tx.productId });
611
731
  return null;
612
732
  }
613
733
  return {
@@ -686,7 +806,7 @@ async function sendAppleConsumptionResponse(transactionId, body, config, options
686
806
  try {
687
807
  jwt = await makeAppleApiJwt(config);
688
808
  } catch (err2) {
689
- 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 });
690
810
  return;
691
811
  }
692
812
  const host = options?.sandbox ? "api.storekit-sandbox.itunes.apple.com" : "api.storekit.itunes.apple.com";
@@ -702,10 +822,14 @@ async function sendAppleConsumptionResponse(transactionId, body, config, options
702
822
  });
703
823
  if (!resp.ok) {
704
824
  const text = await resp.text();
705
- 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
+ });
706
830
  }
707
831
  } catch (err2) {
708
- log.warn("[onesub/apple] Consumption response network error:", err2);
832
+ log.warn("[onesub/apple] Consumption response network error", { transactionId, err: err2 });
709
833
  }
710
834
  }
711
835
  var APPLE_SUBSCRIPTION_STATUS_CODE = {
@@ -736,7 +860,10 @@ async function fetchAppleSubscriptionStatus(originalTransactionId, config, optio
736
860
  try {
737
861
  jwt = await makeAppleApiJwt(config);
738
862
  } catch (err2) {
739
- 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
+ });
740
867
  return null;
741
868
  }
742
869
  const host = options?.sandbox ? "api.storekit-sandbox.itunes.apple.com" : "api.storekit.itunes.apple.com";
@@ -748,24 +875,31 @@ async function fetchAppleSubscriptionStatus(originalTransactionId, config, optio
748
875
  });
749
876
  if (!resp.ok) {
750
877
  const text = await resp.text();
751
- 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
+ });
752
883
  return null;
753
884
  }
754
885
  body = await resp.json();
755
886
  } catch (err2) {
756
- log.warn("[onesub/apple] Status API network error:", err2);
887
+ log.warn("[onesub/apple] Status API network error", { originalTransactionId, err: err2 });
757
888
  return null;
758
889
  }
759
890
  const entry = body.data?.flatMap((g) => g.lastTransactions ?? []).find((t) => t.originalTransactionId === originalTransactionId);
760
891
  if (!entry || entry.status == null || !entry.signedTransactionInfo) {
761
- log.warn("[onesub/apple] Status API returned no matching transaction for", originalTransactionId);
892
+ log.warn("[onesub/apple] Status API returned no matching transaction", { originalTransactionId });
762
893
  return null;
763
894
  }
764
895
  let tx;
765
896
  try {
766
897
  tx = await decodeJws(entry.signedTransactionInfo, config.skipJwsVerification);
767
898
  } catch (err2) {
768
- 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
+ });
769
903
  return null;
770
904
  }
771
905
  let renewal = null;
@@ -776,7 +910,10 @@ async function fetchAppleSubscriptionStatus(originalTransactionId, config, optio
776
910
  }
777
911
  }
778
912
  if (!tx.productId || !tx.expiresDate) {
779
- 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
+ });
780
917
  return null;
781
918
  }
782
919
  const status = mapAppleStatusCode(entry.status);
@@ -800,7 +937,10 @@ async function fetchAppleTransactionHistory(originalTransactionId, config, optio
800
937
  try {
801
938
  jwt = await makeAppleApiJwt(config);
802
939
  } catch (err2) {
803
- 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
+ });
804
944
  return null;
805
945
  }
806
946
  const host = options?.sandbox ? "api.storekit-sandbox.itunes.apple.com" : "api.storekit.itunes.apple.com";
@@ -817,12 +957,16 @@ async function fetchAppleTransactionHistory(originalTransactionId, config, optio
817
957
  });
818
958
  if (!resp.ok) {
819
959
  const text = await resp.text();
820
- 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
+ });
821
965
  return null;
822
966
  }
823
967
  page = await resp.json();
824
968
  } catch (err2) {
825
- log.warn("[onesub/apple] Transaction History API network error:", err2);
969
+ log.warn("[onesub/apple] Transaction History API network error", { originalTransactionId, err: err2 });
826
970
  return null;
827
971
  }
828
972
  for (const signed of page.signedTransactions ?? []) {
@@ -847,14 +991,16 @@ async function fetchAppleTransactionHistory(originalTransactionId, config, optio
847
991
  if (!page.hasMore) break;
848
992
  if (!page.revision || page.revision === revision) {
849
993
  log.warn(
850
- "[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 }
851
996
  );
852
997
  break;
853
998
  }
854
999
  if (pageCount >= MAX_HISTORY_PAGES) {
855
- log.warn(
856
- `[onesub/apple] Transaction History pagination hit the ${MAX_HISTORY_PAGES}-page cap \u2014 stopping (partial history returned)`
857
- );
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
+ });
858
1004
  break;
859
1005
  }
860
1006
  revision = page.revision;
@@ -1004,7 +1150,7 @@ async function acknowledgeGoogleSubscription(purchaseToken, productId, config) {
1004
1150
  try {
1005
1151
  accessToken = await getCachedAccessToken(config.serviceAccountKey);
1006
1152
  } catch (err2) {
1007
- 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 });
1008
1154
  return;
1009
1155
  }
1010
1156
  const url = `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${encodeURIComponent(config.packageName)}/purchases/subscriptions/${encodeURIComponent(productId)}/tokens/${encodeURIComponent(purchaseToken)}:acknowledge`;
@@ -1016,10 +1162,14 @@ async function acknowledgeGoogleSubscription(purchaseToken, productId, config) {
1016
1162
  });
1017
1163
  if (!resp.ok) {
1018
1164
  const body = await resp.text();
1019
- 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
+ });
1020
1170
  }
1021
1171
  } catch (err2) {
1022
- 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 });
1023
1173
  }
1024
1174
  }
1025
1175
  async function acknowledgeGoogleProduct(purchaseToken, productId, config) {
@@ -1030,7 +1180,7 @@ async function acknowledgeGoogleProduct(purchaseToken, productId, config) {
1030
1180
  try {
1031
1181
  accessToken = await getCachedAccessToken(config.serviceAccountKey);
1032
1182
  } catch (err2) {
1033
- 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 });
1034
1184
  return;
1035
1185
  }
1036
1186
  const url = `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${encodeURIComponent(config.packageName)}/purchases/products/${encodeURIComponent(productId)}/tokens/${encodeURIComponent(purchaseToken)}:acknowledge`;
@@ -1042,10 +1192,14 @@ async function acknowledgeGoogleProduct(purchaseToken, productId, config) {
1042
1192
  });
1043
1193
  if (!resp.ok) {
1044
1194
  const body = await resp.text();
1045
- 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
+ });
1046
1200
  }
1047
1201
  } catch (err2) {
1048
- 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 });
1049
1203
  }
1050
1204
  }
1051
1205
  async function consumeGoogleProductReceipt(purchaseToken, productId, config) {
@@ -1055,7 +1209,7 @@ async function consumeGoogleProductReceipt(purchaseToken, productId, config) {
1055
1209
  try {
1056
1210
  accessToken = await getCachedAccessToken(config.serviceAccountKey);
1057
1211
  } catch (err2) {
1058
- 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 });
1059
1213
  return;
1060
1214
  }
1061
1215
  const url = `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${encodeURIComponent(config.packageName)}/purchases/products/${encodeURIComponent(productId)}/tokens/${encodeURIComponent(purchaseToken)}:consume`;
@@ -1066,10 +1220,14 @@ async function consumeGoogleProductReceipt(purchaseToken, productId, config) {
1066
1220
  });
1067
1221
  if (!resp.ok) {
1068
1222
  const body = await resp.text();
1069
- 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
+ });
1070
1228
  }
1071
1229
  } catch (err2) {
1072
- 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 });
1073
1231
  }
1074
1232
  }
1075
1233
  function deriveStatusV2(state) {
@@ -1095,11 +1253,11 @@ function deriveStatusV2(state) {
1095
1253
  async function validateGoogleReceipt(receipt, productId, config) {
1096
1254
  if (config.mockMode) return mockValidateGoogleSubscription(receipt, productId);
1097
1255
  if (!config.serviceAccountKey) {
1098
- 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 });
1099
1257
  return null;
1100
1258
  }
1101
1259
  if (!config.packageName) {
1102
- 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 });
1103
1261
  return null;
1104
1262
  }
1105
1263
  let purchase;
@@ -1107,25 +1265,27 @@ async function validateGoogleReceipt(receipt, productId, config) {
1107
1265
  const token = await getCachedAccessToken(config.serviceAccountKey);
1108
1266
  purchase = await fetchSubscriptionPurchaseV2(config.packageName, receipt, token);
1109
1267
  } catch (err2) {
1110
- 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
+ });
1111
1273
  return null;
1112
1274
  }
1113
1275
  const status = deriveStatusV2(purchase.subscriptionState);
1114
1276
  if (!status) {
1115
- log.warn(
1116
- "[onesub/google] Unrecognised or pending subscriptionState \u2014 rejecting:",
1117
- purchase.subscriptionState
1118
- );
1277
+ log.warn("[onesub/google] Unrecognised or pending subscriptionState \u2014 rejecting", {
1278
+ productId,
1279
+ subscriptionState: purchase.subscriptionState
1280
+ });
1119
1281
  return null;
1120
1282
  }
1121
1283
  const lineItem = purchase.lineItems?.find((item) => item.productId === productId);
1122
1284
  if (!lineItem) {
1123
- log.warn(
1124
- "[onesub/google] productId not found in subscription lineItems:",
1285
+ log.warn("[onesub/google] productId not found in subscription lineItems", {
1125
1286
  productId,
1126
- "available:",
1127
- purchase.lineItems?.map((i) => i.productId).join(", ") ?? "(none)"
1128
- );
1287
+ availableProductIds: purchase.lineItems?.map((i) => i.productId) ?? []
1288
+ });
1129
1289
  return null;
1130
1290
  }
1131
1291
  const expiresAt = lineItem.expiryTime ?? (/* @__PURE__ */ new Date()).toISOString();
@@ -1157,11 +1317,11 @@ async function validateGoogleReceipt(receipt, productId, config) {
1157
1317
  async function validateGoogleProductReceipt(purchaseToken, productId, config, type = "non_consumable") {
1158
1318
  if (config.mockMode) return mockValidateGoogleProduct(purchaseToken, productId);
1159
1319
  if (!config.serviceAccountKey) {
1160
- log.warn("[onesub/google] No serviceAccountKey \u2014 cannot validate product receipt");
1320
+ log.warn("[onesub/google] No serviceAccountKey \u2014 cannot validate product receipt", { productId });
1161
1321
  return null;
1162
1322
  }
1163
1323
  if (!config.packageName) {
1164
- log.warn("[onesub/google] No packageName \u2014 cannot validate product receipt");
1324
+ log.warn("[onesub/google] No packageName \u2014 cannot validate product receipt", { productId });
1165
1325
  return null;
1166
1326
  }
1167
1327
  let purchase;
@@ -1169,27 +1329,44 @@ async function validateGoogleProductReceipt(purchaseToken, productId, config, ty
1169
1329
  const token = await getCachedAccessToken(config.serviceAccountKey);
1170
1330
  purchase = await fetchProductPurchase(config.packageName, productId, purchaseToken, token);
1171
1331
  } catch (err2) {
1172
- 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
+ });
1173
1338
  return null;
1174
1339
  }
1175
1340
  if (purchase.purchaseState !== 0) {
1176
- 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
+ });
1177
1346
  return null;
1178
1347
  }
1179
1348
  if (type === "consumable" && purchase.consumptionState === 1) {
1180
- 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
+ });
1181
1353
  return null;
1182
1354
  }
1183
1355
  if (purchase.purchaseTimeMillis) {
1184
1356
  const purchaseTime = parseInt(purchase.purchaseTimeMillis, 10);
1185
1357
  const maxAgeMs = (config.productReceiptMaxAgeHours ?? 72) * 60 * 60 * 1e3;
1186
1358
  if (Date.now() - purchaseTime > maxAgeMs) {
1187
- 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
+ });
1188
1365
  return null;
1189
1366
  }
1190
1367
  }
1191
1368
  if (!purchase.orderId) {
1192
- log.warn("[onesub/google] No orderId in product purchase");
1369
+ log.warn("[onesub/google] No orderId in product purchase", { productId });
1193
1370
  return null;
1194
1371
  }
1195
1372
  return {
@@ -1246,7 +1423,11 @@ function decodeGoogleOneTimeProductNotification(payload) {
1246
1423
  if (!notification.oneTimeProductNotification) return null;
1247
1424
  const { notificationType, purchaseToken, sku } = notification.oneTimeProductNotification;
1248
1425
  if (notificationType !== 1 && notificationType !== 2) {
1249
- 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
+ });
1250
1431
  return null;
1251
1432
  }
1252
1433
  return { notificationType, purchaseToken, sku, packageName: notification.packageName };