@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/__tests__/field-vocabulary.test.d.ts +25 -0
- package/dist/__tests__/field-vocabulary.test.d.ts.map +1 -0
- package/dist/__tests__/log-format.test.d.ts +15 -0
- package/dist/__tests__/log-format.test.d.ts.map +1 -0
- package/dist/__tests__/logger.test.d.ts +13 -6
- package/dist/__tests__/logger.test.d.ts.map +1 -1
- package/dist/__tests__/provider-log-fields.test.d.ts +27 -0
- package/dist/__tests__/provider-log-fields.test.d.ts.map +1 -0
- package/dist/index.cjs +254 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +254 -73
- package/dist/index.js.map +1 -1
- package/dist/log-format.d.ts +80 -0
- package/dist/log-format.d.ts.map +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/providers/apple.d.ts.map +1 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/package.json +2 -2
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(
|
|
186
|
-
warn: (...args) => current.warn(
|
|
187
|
-
error: (...args) => current.error(
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1127
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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 };
|