@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.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(
|
|
179
|
-
warn: (...args) => current.warn(
|
|
180
|
-
error: (...args) => current.error(
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1120
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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 };
|