@onesub/server 0.14.0 → 0.16.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__/google-subscriptions-v2.test.d.ts +1 -1
- package/dist/__tests__/receipt-age.test.d.ts +7 -0
- package/dist/__tests__/receipt-age.test.d.ts.map +1 -0
- package/dist/__tests__/tracing.test.d.ts +2 -0
- package/dist/__tests__/tracing.test.d.ts.map +1 -0
- package/dist/cache.d.ts +5 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/index.cjs +178 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +178 -55
- package/dist/index.js.map +1 -1
- package/dist/providers/apple.d.ts.map +1 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/mock.d.ts.map +1 -1
- package/dist/routes/admin.d.ts.map +1 -1
- package/dist/routes/purchase.d.ts.map +1 -1
- package/dist/routes/validate.d.ts.map +1 -1
- package/dist/routes/webhook-apple.d.ts.map +1 -1
- package/dist/routes/webhook-google.d.ts +1 -1
- package/dist/routes/webhook-google.d.ts.map +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/stores/postgres.d.ts.map +1 -1
- package/dist/stores/redis.d.ts +1 -0
- package/dist/stores/redis.d.ts.map +1 -1
- package/dist/tracing.d.ts.map +1 -1
- package/dist/webhook-events.d.ts +10 -0
- package/dist/webhook-events.d.ts.map +1 -1
- package/package.json +5 -3
- package/dist/__tests__/admin-customer-detail.test.js +0 -146
- package/dist/__tests__/admin-customer-detail.test.js.map +0 -1
- package/dist/__tests__/admin-list-subscriptions.test.js +0 -192
- package/dist/__tests__/admin-list-subscriptions.test.js.map +0 -1
- package/dist/__tests__/admin-subscription-detail.test.js +0 -112
- package/dist/__tests__/admin-subscription-detail.test.js.map +0 -1
- package/dist/__tests__/apple-root-ca.test.js +0 -41
- package/dist/__tests__/apple-root-ca.test.js.map +0 -1
- package/dist/__tests__/apple-status-api.test.js +0 -345
- package/dist/__tests__/apple-status-api.test.js.map +0 -1
- package/dist/__tests__/cache.test.js +0 -34
- package/dist/__tests__/cache.test.js.map +0 -1
- package/dist/__tests__/entitlements.test.js +0 -268
- package/dist/__tests__/entitlements.test.js.map +0 -1
- package/dist/__tests__/error-codes.test.js +0 -173
- package/dist/__tests__/error-codes.test.js.map +0 -1
- package/dist/__tests__/fetch-hardening.test.js +0 -192
- package/dist/__tests__/fetch-hardening.test.js.map +0 -1
- package/dist/__tests__/google-paused.test.js +0 -126
- package/dist/__tests__/google-paused.test.js.map +0 -1
- package/dist/__tests__/google-price-change.test.js +0 -186
- package/dist/__tests__/google-price-change.test.js.map +0 -1
- package/dist/__tests__/google-subscriptions-v2.test.js +0 -206
- package/dist/__tests__/google-subscriptions-v2.test.js.map +0 -1
- package/dist/__tests__/lifecycle-scenarios.test.js +0 -579
- package/dist/__tests__/lifecycle-scenarios.test.js.map +0 -1
- package/dist/__tests__/lifecycle-states.test.js +0 -423
- package/dist/__tests__/lifecycle-states.test.js.map +0 -1
- package/dist/__tests__/linked-purchase-token.test.js +0 -227
- package/dist/__tests__/linked-purchase-token.test.js.map +0 -1
- package/dist/__tests__/metrics.test.js +0 -340
- package/dist/__tests__/metrics.test.js.map +0 -1
- package/dist/__tests__/mock-provider.test.js +0 -188
- package/dist/__tests__/mock-provider.test.js.map +0 -1
- package/dist/__tests__/openapi.test.js +0 -32
- package/dist/__tests__/openapi.test.js.map +0 -1
- package/dist/__tests__/paused-auto-resume.test.js +0 -162
- package/dist/__tests__/paused-auto-resume.test.js.map +0 -1
- package/dist/__tests__/providers.test.js +0 -321
- package/dist/__tests__/providers.test.js.map +0 -1
- package/dist/__tests__/redis-store.test.js +0 -166
- package/dist/__tests__/redis-store.test.js.map +0 -1
- package/dist/__tests__/refund-policy.test.js +0 -317
- package/dist/__tests__/refund-policy.test.js.map +0 -1
- package/dist/__tests__/schema.test.js +0 -46
- package/dist/__tests__/schema.test.js.map +0 -1
- package/dist/__tests__/store.test.js +0 -172
- package/dist/__tests__/store.test.js.map +0 -1
- package/dist/__tests__/test-utils.js +0 -30
- package/dist/__tests__/test-utils.js.map +0 -1
- package/dist/__tests__/webhook-apple.test.js +0 -247
- package/dist/__tests__/webhook-apple.test.js.map +0 -1
- package/dist/__tests__/webhook-events.test.js +0 -36
- package/dist/__tests__/webhook-events.test.js.map +0 -1
- package/dist/__tests__/webhook-google.test.js +0 -255
- package/dist/__tests__/webhook-google.test.js.map +0 -1
- package/dist/__tests__/webhook-queue.test.js +0 -26
- package/dist/__tests__/webhook-queue.test.js.map +0 -1
- package/dist/__tests__/webhook-refund.test.js +0 -313
- package/dist/__tests__/webhook-refund.test.js.map +0 -1
- package/dist/cache.js +0 -39
- package/dist/cache.js.map +0 -1
- package/dist/errors.js +0 -19
- package/dist/errors.js.map +0 -1
- package/dist/http.js +0 -36
- package/dist/http.js.map +0 -1
- package/dist/logger.js +0 -20
- package/dist/logger.js.map +0 -1
- package/dist/openapi.js +0 -196
- package/dist/openapi.js.map +0 -1
- package/dist/providers/apple-root-ca.js +0 -38
- package/dist/providers/apple-root-ca.js.map +0 -1
- package/dist/providers/apple.js +0 -630
- package/dist/providers/apple.js.map +0 -1
- package/dist/providers/google.js +0 -564
- package/dist/providers/google.js.map +0 -1
- package/dist/providers/mock.js +0 -93
- package/dist/providers/mock.js.map +0 -1
- package/dist/routes/admin.js +0 -327
- package/dist/routes/admin.js.map +0 -1
- package/dist/routes/apple-offer.js +0 -72
- package/dist/routes/apple-offer.js.map +0 -1
- package/dist/routes/entitlements.js +0 -130
- package/dist/routes/entitlements.js.map +0 -1
- package/dist/routes/metrics.js +0 -281
- package/dist/routes/metrics.js.map +0 -1
- package/dist/routes/purchase.js +0 -204
- package/dist/routes/purchase.js.map +0 -1
- package/dist/routes/status.js +0 -55
- package/dist/routes/status.js.map +0 -1
- package/dist/routes/validate.js +0 -70
- package/dist/routes/validate.js.map +0 -1
- package/dist/routes/webhook-apple.js +0 -145
- package/dist/routes/webhook-apple.js.map +0 -1
- package/dist/routes/webhook-google.js +0 -204
- package/dist/routes/webhook-google.js.map +0 -1
- package/dist/routes/webhook.js +0 -19
- package/dist/routes/webhook.js.map +0 -1
- package/dist/store.js +0 -145
- package/dist/store.js.map +0 -1
- package/dist/stores/postgres.js +0 -329
- package/dist/stores/postgres.js.map +0 -1
- package/dist/stores/redis.js +0 -297
- package/dist/stores/redis.js.map +0 -1
- package/dist/stores/schema.js +0 -67
- package/dist/stores/schema.js.map +0 -1
- package/dist/tracing.js +0 -74
- package/dist/tracing.js.map +0 -1
- package/dist/webhook-events.js +0 -58
- package/dist/webhook-events.js.map +0 -1
- package/dist/webhook-queue.js +0 -108
- package/dist/webhook-queue.js.map +0 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - subscriptionState string enum → onesub SubscriptionStatus mapping
|
|
7
7
|
* - lineItems productId match (success / mismatch)
|
|
8
8
|
* - autoRenewingPlan.autoRenewEnabled → willRenew
|
|
9
|
-
* -
|
|
9
|
+
* - purchaseToken → originalTransactionId (webhooks look records up by token)
|
|
10
10
|
* - PENDING / UNSPECIFIED states rejected (return null)
|
|
11
11
|
*/
|
|
12
12
|
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* productReceiptMaxAgeHours — the one-time-purchase replay window is
|
|
3
|
+
* configurable (default 72h) so historical receipts can be validated on
|
|
4
|
+
* purpose (backend migrations, e2e against real store transactions).
|
|
5
|
+
*/
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=receipt-age.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"receipt-age.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/receipt-age.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracing.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/tracing.test.ts"],"names":[],"mappings":""}
|
package/dist/cache.d.ts
CHANGED
|
@@ -21,11 +21,16 @@ export interface CacheAdapter {
|
|
|
21
21
|
*/
|
|
22
22
|
export declare class InMemoryCacheAdapter implements CacheAdapter {
|
|
23
23
|
private readonly entries;
|
|
24
|
+
private setsSinceSweep;
|
|
24
25
|
get<T>(key: string): Promise<T | null>;
|
|
25
26
|
set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
|
|
26
27
|
del(key: string): Promise<void>;
|
|
28
|
+
/** Evict every entry whose TTL has elapsed. */
|
|
29
|
+
private sweepExpired;
|
|
27
30
|
/** Test helper — clear everything. Not part of the public CacheAdapter contract. */
|
|
28
31
|
clear(): void;
|
|
32
|
+
/** Test helper — number of entries currently held (including not-yet-swept expired ones). */
|
|
33
|
+
get size(): number;
|
|
29
34
|
}
|
|
30
35
|
export declare function getDefaultCache(): CacheAdapter;
|
|
31
36
|
export declare function setDefaultCache(adapter: CacheAdapter): void;
|
package/dist/cache.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACjD,wDAAwD;IACxD,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACjD,wDAAwD;IACxD,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAWD;;GAEG;AACH,qBAAa,oBAAqB,YAAW,YAAY;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IACpD,OAAO,CAAC,cAAc,CAAK;IAErB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAUtC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYjE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrC,+CAA+C;IAC/C,OAAO,CAAC,YAAY;IAQpB,oFAAoF;IACpF,KAAK,IAAI,IAAI;IAKb,6FAA6F;IAC7F,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAQD,wBAAgB,eAAe,IAAI,YAAY,CAE9C;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAE3D"}
|
package/dist/index.cjs
CHANGED
|
@@ -5,13 +5,19 @@ var shared = require('@onesub/shared');
|
|
|
5
5
|
var zod = require('zod');
|
|
6
6
|
var jose = require('jose');
|
|
7
7
|
var crypto = require('crypto');
|
|
8
|
+
var module$1 = require('module');
|
|
8
9
|
|
|
9
10
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
10
11
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
12
|
|
|
12
13
|
var express__default = /*#__PURE__*/_interopDefault(express);
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
16
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
17
|
+
}) : x)(function(x) {
|
|
18
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
19
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
20
|
+
});
|
|
15
21
|
|
|
16
22
|
// src/store.ts
|
|
17
23
|
var InMemorySubscriptionStore = class {
|
|
@@ -21,6 +27,14 @@ var InMemorySubscriptionStore = class {
|
|
|
21
27
|
byUserId = /* @__PURE__ */ new Map();
|
|
22
28
|
byTransactionId = /* @__PURE__ */ new Map();
|
|
23
29
|
async save(sub) {
|
|
30
|
+
const prev = this.byTransactionId.get(sub.originalTransactionId);
|
|
31
|
+
if (prev && prev.userId !== sub.userId) {
|
|
32
|
+
const oldList = (this.byUserId.get(prev.userId) ?? []).filter(
|
|
33
|
+
(s) => s.originalTransactionId !== sub.originalTransactionId
|
|
34
|
+
);
|
|
35
|
+
if (oldList.length) this.byUserId.set(prev.userId, oldList);
|
|
36
|
+
else this.byUserId.delete(prev.userId);
|
|
37
|
+
}
|
|
24
38
|
this.byTransactionId.set(sub.originalTransactionId, sub);
|
|
25
39
|
const existing = this.byUserId.get(sub.userId) ?? [];
|
|
26
40
|
const filtered = existing.filter((s) => s.originalTransactionId !== sub.originalTransactionId);
|
|
@@ -181,8 +195,10 @@ async function fetchWithTimeout(input, init, timeoutMs = DEFAULT_FETCH_TIMEOUT_M
|
|
|
181
195
|
}
|
|
182
196
|
|
|
183
197
|
// src/cache.ts
|
|
198
|
+
var SWEEP_EVERY_N_SETS = 256;
|
|
184
199
|
var InMemoryCacheAdapter = class {
|
|
185
200
|
entries = /* @__PURE__ */ new Map();
|
|
201
|
+
setsSinceSweep = 0;
|
|
186
202
|
async get(key) {
|
|
187
203
|
const entry = this.entries.get(key);
|
|
188
204
|
if (!entry) return null;
|
|
@@ -195,13 +211,29 @@ var InMemoryCacheAdapter = class {
|
|
|
195
211
|
async set(key, value, ttlSeconds) {
|
|
196
212
|
const expiresAt = ttlSeconds && ttlSeconds > 0 ? Date.now() + ttlSeconds * 1e3 : 0;
|
|
197
213
|
this.entries.set(key, { value, expiresAt });
|
|
214
|
+
if (++this.setsSinceSweep >= SWEEP_EVERY_N_SETS) {
|
|
215
|
+
this.sweepExpired();
|
|
216
|
+
}
|
|
198
217
|
}
|
|
199
218
|
async del(key) {
|
|
200
219
|
this.entries.delete(key);
|
|
201
220
|
}
|
|
221
|
+
/** Evict every entry whose TTL has elapsed. */
|
|
222
|
+
sweepExpired() {
|
|
223
|
+
this.setsSinceSweep = 0;
|
|
224
|
+
const now = Date.now();
|
|
225
|
+
for (const [key, entry] of this.entries) {
|
|
226
|
+
if (entry.expiresAt !== 0 && entry.expiresAt < now) this.entries.delete(key);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
202
229
|
/** Test helper — clear everything. Not part of the public CacheAdapter contract. */
|
|
203
230
|
clear() {
|
|
204
231
|
this.entries.clear();
|
|
232
|
+
this.setsSinceSweep = 0;
|
|
233
|
+
}
|
|
234
|
+
/** Test helper — number of entries currently held (including not-yet-swept expired ones). */
|
|
235
|
+
get size() {
|
|
236
|
+
return this.entries.size;
|
|
205
237
|
}
|
|
206
238
|
};
|
|
207
239
|
var defaultAdapter = new InMemoryCacheAdapter();
|
|
@@ -240,6 +272,7 @@ function mockValidateAppleSubscription(receipt) {
|
|
|
240
272
|
if (!outcomePasses(outcome, "apple")) return null;
|
|
241
273
|
const now = Date.now();
|
|
242
274
|
const expiresAt = outcome.kind === "sandbox" ? now + 1 * HOURS : now + 30 * DAYS;
|
|
275
|
+
const tokenMatch = receipt.match(/#token=([^#]+)/);
|
|
243
276
|
return {
|
|
244
277
|
userId: "",
|
|
245
278
|
productId: "mock_subscription",
|
|
@@ -248,7 +281,8 @@ function mockValidateAppleSubscription(receipt) {
|
|
|
248
281
|
expiresAt: new Date(expiresAt).toISOString(),
|
|
249
282
|
originalTransactionId: deterministicTransactionId("mock_apple_orig", receipt),
|
|
250
283
|
purchasedAt: new Date(now).toISOString(),
|
|
251
|
-
willRenew: true
|
|
284
|
+
willRenew: true,
|
|
285
|
+
...tokenMatch ? { boundAccountId: tokenMatch[1] } : {}
|
|
252
286
|
};
|
|
253
287
|
}
|
|
254
288
|
function mockValidateAppleProduct(receipt, expectedProductId) {
|
|
@@ -268,15 +302,19 @@ function mockValidateGoogleSubscription(receipt, productId) {
|
|
|
268
302
|
if (!outcomePasses(outcome, "google")) return null;
|
|
269
303
|
const now = Date.now();
|
|
270
304
|
const expiresAt = outcome.kind === "sandbox" ? now + 1 * HOURS : now + 30 * DAYS;
|
|
305
|
+
const tokenMatch = receipt.match(/#token=([^#]+)/);
|
|
271
306
|
return {
|
|
272
307
|
userId: "",
|
|
273
308
|
productId,
|
|
274
309
|
platform: "google",
|
|
275
310
|
status: shared.SUBSCRIPTION_STATUS.ACTIVE,
|
|
276
311
|
expiresAt: new Date(expiresAt).toISOString(),
|
|
277
|
-
|
|
312
|
+
// Same keying as production: Google records are keyed by the purchaseToken
|
|
313
|
+
// (the receipt itself) so mock webhooks can find them by token.
|
|
314
|
+
originalTransactionId: receipt,
|
|
278
315
|
purchasedAt: new Date(now).toISOString(),
|
|
279
|
-
willRenew: true
|
|
316
|
+
willRenew: true,
|
|
317
|
+
...tokenMatch ? { boundAccountId: tokenMatch[1] } : {}
|
|
280
318
|
};
|
|
281
319
|
}
|
|
282
320
|
function mockValidateGoogleProduct(receipt, productId) {
|
|
@@ -291,7 +329,6 @@ function mockValidateGoogleProduct(receipt, productId) {
|
|
|
291
329
|
}
|
|
292
330
|
|
|
293
331
|
// src/providers/apple.ts
|
|
294
|
-
var MAX_CONSUMABLE_RECEIPT_AGE_MS = 72 * 60 * 60 * 1e3;
|
|
295
332
|
function derBase64ToPem(der) {
|
|
296
333
|
return "-----BEGIN CERTIFICATE-----\n" + (der.match(/.{1,64}/g) ?? []).join("\n") + "\n-----END CERTIFICATE-----";
|
|
297
334
|
}
|
|
@@ -394,8 +431,9 @@ async function validateAppleReceipt(receipt, config) {
|
|
|
394
431
|
expiresAt: new Date(tx.expiresDate).toISOString(),
|
|
395
432
|
originalTransactionId: tx.originalTransactionId,
|
|
396
433
|
purchasedAt: new Date(purchasedAt).toISOString(),
|
|
397
|
-
willRenew: status === shared.SUBSCRIPTION_STATUS.ACTIVE
|
|
434
|
+
willRenew: status === shared.SUBSCRIPTION_STATUS.ACTIVE,
|
|
398
435
|
// refined by renewal info in webhook
|
|
436
|
+
...tx.appAccountToken ? { boundAccountId: tx.appAccountToken } : {}
|
|
399
437
|
};
|
|
400
438
|
}
|
|
401
439
|
async function validateAppleConsumableReceipt(signedTransaction, config, expectedProductId) {
|
|
@@ -438,8 +476,9 @@ async function validateAppleConsumableReceipt(signedTransaction, config, expecte
|
|
|
438
476
|
log.warn("[onesub/apple] Purchase was revoked/refunded");
|
|
439
477
|
return null;
|
|
440
478
|
}
|
|
441
|
-
|
|
442
|
-
|
|
479
|
+
const maxAgeMs = (config.productReceiptMaxAgeHours ?? 72) * 60 * 60 * 1e3;
|
|
480
|
+
if (tx.purchaseDate && Date.now() - tx.purchaseDate > maxAgeMs) {
|
|
481
|
+
log.warn(`[onesub/apple] Consumable receipt too old (>${config.productReceiptMaxAgeHours ?? 72}h)`);
|
|
443
482
|
return null;
|
|
444
483
|
}
|
|
445
484
|
const transactionId = tx.transactionId ?? tx.originalTransactionId;
|
|
@@ -728,7 +767,6 @@ var GOOGLE_NOTIFICATION_TYPE = {
|
|
|
728
767
|
SUBSCRIPTION_REVOKED: 12,
|
|
729
768
|
SUBSCRIPTION_EXPIRED: 13
|
|
730
769
|
};
|
|
731
|
-
var MAX_PRODUCT_RECEIPT_AGE_MS = 72 * 60 * 60 * 1e3;
|
|
732
770
|
var refreshPromises = /* @__PURE__ */ new Map();
|
|
733
771
|
function googleTokenCacheKey(serviceAccountKey) {
|
|
734
772
|
let hash = 0;
|
|
@@ -955,6 +993,7 @@ async function validateGoogleReceipt(receipt, productId, config) {
|
|
|
955
993
|
const purchasedAt = purchase.startTime ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
956
994
|
const willRenew = lineItem.autoRenewingPlan?.autoRenewEnabled ?? false;
|
|
957
995
|
const autoResumeTime = status === shared.SUBSCRIPTION_STATUS.PAUSED ? purchase.pausedStateContext?.autoResumeTime : void 0;
|
|
996
|
+
const boundAccountId = purchase.externalAccountIdentifiers?.obfuscatedExternalAccountId;
|
|
958
997
|
return {
|
|
959
998
|
userId: "",
|
|
960
999
|
// caller fills this in
|
|
@@ -962,13 +1001,18 @@ async function validateGoogleReceipt(receipt, productId, config) {
|
|
|
962
1001
|
platform: "google",
|
|
963
1002
|
status,
|
|
964
1003
|
expiresAt,
|
|
965
|
-
|
|
1004
|
+
// Keyed by the purchaseToken, NOT latestOrderId: RTDN webhooks and
|
|
1005
|
+
// linkedPurchaseToken chains only carry the token, so records keyed by
|
|
1006
|
+
// order id are invisible to the entire webhook lifecycle (refund/cancel/
|
|
1007
|
+
// renewal would never find them).
|
|
1008
|
+
originalTransactionId: receipt,
|
|
966
1009
|
purchasedAt,
|
|
967
1010
|
willRenew,
|
|
968
1011
|
// Surface the linked token so host apps (and our webhook userId-continuity
|
|
969
1012
|
// logic) can follow upgrade/downgrade chains.
|
|
970
1013
|
linkedPurchaseToken: purchase.linkedPurchaseToken,
|
|
971
|
-
autoResumeTime
|
|
1014
|
+
autoResumeTime,
|
|
1015
|
+
...boundAccountId ? { boundAccountId } : {}
|
|
972
1016
|
};
|
|
973
1017
|
}
|
|
974
1018
|
async function validateGoogleProductReceipt(purchaseToken, productId, config, type = "non_consumable") {
|
|
@@ -999,8 +1043,9 @@ async function validateGoogleProductReceipt(purchaseToken, productId, config, ty
|
|
|
999
1043
|
}
|
|
1000
1044
|
if (purchase.purchaseTimeMillis) {
|
|
1001
1045
|
const purchaseTime = parseInt(purchase.purchaseTimeMillis, 10);
|
|
1002
|
-
|
|
1003
|
-
|
|
1046
|
+
const maxAgeMs = (config.productReceiptMaxAgeHours ?? 72) * 60 * 60 * 1e3;
|
|
1047
|
+
if (Date.now() - purchaseTime > maxAgeMs) {
|
|
1048
|
+
log.warn(`[onesub/google] Product receipt too old (>${config.productReceiptMaxAgeHours ?? 72}h)`);
|
|
1004
1049
|
return null;
|
|
1005
1050
|
}
|
|
1006
1051
|
}
|
|
@@ -1144,6 +1189,22 @@ function createValidateRouter(config, store) {
|
|
|
1144
1189
|
sendError(res, 422, shared.ONESUB_ERROR_CODE.RECEIPT_VALIDATION_FAILED, "Receipt validation failed", NO_SUB);
|
|
1145
1190
|
return;
|
|
1146
1191
|
}
|
|
1192
|
+
const boundAccountId = sub.boundAccountId;
|
|
1193
|
+
delete sub.boundAccountId;
|
|
1194
|
+
const bindingMismatch = platform === "apple" ? boundAccountId && boundAccountId.toLowerCase() !== userId.toLowerCase() : boundAccountId && boundAccountId !== userId;
|
|
1195
|
+
if (bindingMismatch) {
|
|
1196
|
+
log.warn(
|
|
1197
|
+
`[onesub/validate] account binding mismatch for ${sub.originalTransactionId}: receipt token does not match userId ${userId}`
|
|
1198
|
+
);
|
|
1199
|
+
sendError(
|
|
1200
|
+
res,
|
|
1201
|
+
409,
|
|
1202
|
+
shared.ONESUB_ERROR_CODE.TRANSACTION_BELONGS_TO_OTHER_USER,
|
|
1203
|
+
"TRANSACTION_BELONGS_TO_OTHER_USER",
|
|
1204
|
+
NO_SUB
|
|
1205
|
+
);
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1147
1208
|
sub.userId = userId;
|
|
1148
1209
|
await store.save(sub);
|
|
1149
1210
|
if (platform === "google" && config.google) {
|
|
@@ -1191,7 +1252,7 @@ function createStatusRouter(store) {
|
|
|
1191
1252
|
return router;
|
|
1192
1253
|
}
|
|
1193
1254
|
var APPLE_ACTIVE_TYPES = /* @__PURE__ */ new Set(["SUBSCRIBED", "DID_RENEW", "DID_RECOVER", "OFFER_REDEEMED"]);
|
|
1194
|
-
var APPLE_CANCELED_TYPES = /* @__PURE__ */ new Set(["REVOKE", "REFUND"
|
|
1255
|
+
var APPLE_CANCELED_TYPES = /* @__PURE__ */ new Set(["REVOKE", "REFUND"]);
|
|
1195
1256
|
var APPLE_EXPIRED_TYPES = /* @__PURE__ */ new Set(["EXPIRED"]);
|
|
1196
1257
|
function mapAppleNotificationStatus(notificationType, subtype) {
|
|
1197
1258
|
if (notificationType === "DID_FAIL_TO_RENEW") {
|
|
@@ -1248,6 +1309,11 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1248
1309
|
} = decoded;
|
|
1249
1310
|
const notificationType = payload.notificationType;
|
|
1250
1311
|
const subtype = payload.subtype;
|
|
1312
|
+
if (config.apple?.bundleId && bundleId && bundleId !== config.apple.bundleId) {
|
|
1313
|
+
log.warn("[onesub/webhook/apple] Bundle ID mismatch:", bundleId, "!==", config.apple.bundleId);
|
|
1314
|
+
sendError(res, 400, shared.ONESUB_ERROR_CODE.BUNDLE_ID_MISMATCH, "Bundle ID mismatch");
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1251
1317
|
const mapped = mapAppleNotificationStatus(notificationType, subtype);
|
|
1252
1318
|
const finalStatus = mapped ?? status;
|
|
1253
1319
|
if (notificationType === "CONSUMPTION_REQUEST" && config.apple?.consumptionInfoProvider && transactionId && productId) {
|
|
@@ -1274,6 +1340,10 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1274
1340
|
}
|
|
1275
1341
|
const isOneTimePurchase = type === "Consumable" || type === "Non-Consumable";
|
|
1276
1342
|
const isRefundOrRevoke = APPLE_CANCELED_TYPES.has(notificationType);
|
|
1343
|
+
if (isOneTimePurchase && notificationType === "CONSUMPTION_REQUEST") {
|
|
1344
|
+
res.status(200).json({ received: true });
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1277
1347
|
try {
|
|
1278
1348
|
if (isOneTimePurchase && isRefundOrRevoke) {
|
|
1279
1349
|
const lookupId = transactionId ?? originalTransactionId;
|
|
@@ -1286,7 +1356,7 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1286
1356
|
}
|
|
1287
1357
|
const existing = await store.getByTransactionId(originalTransactionId);
|
|
1288
1358
|
if (existing) {
|
|
1289
|
-
const isSubscriptionRefund = isRefundOrRevoke && !isOneTimePurchase
|
|
1359
|
+
const isSubscriptionRefund = isRefundOrRevoke && !isOneTimePurchase;
|
|
1290
1360
|
const keepEntitlement = isSubscriptionRefund && config.refundPolicy === "until_expiry";
|
|
1291
1361
|
const correctedUserId = appAccountToken && existing.userId === originalTransactionId ? appAccountToken : existing.userId;
|
|
1292
1362
|
if (correctedUserId !== existing.userId) {
|
|
@@ -1326,6 +1396,12 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1326
1396
|
res.status(200).json({ received: true });
|
|
1327
1397
|
} catch (err) {
|
|
1328
1398
|
log.error("[onesub/webhook/apple] Store update error:", err);
|
|
1399
|
+
if (webhookEventStore?.unmark && typeof payload.notificationUUID === "string") {
|
|
1400
|
+
try {
|
|
1401
|
+
await webhookEventStore.unmark("apple", payload.notificationUUID);
|
|
1402
|
+
} catch {
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1329
1405
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to update subscription");
|
|
1330
1406
|
}
|
|
1331
1407
|
}
|
|
@@ -1335,13 +1411,23 @@ function getGoogleJwks() {
|
|
|
1335
1411
|
if (!googleJwks) googleJwks = jose.createRemoteJWKSet(new URL(GOOGLE_JWKS_URL));
|
|
1336
1412
|
return googleJwks;
|
|
1337
1413
|
}
|
|
1338
|
-
async function verifyGooglePushToken(req, expectedAudience) {
|
|
1414
|
+
async function verifyGooglePushToken(req, expectedAudience, expectedServiceAccountEmail) {
|
|
1339
1415
|
const authHeader = req.headers["authorization"];
|
|
1340
1416
|
if (typeof authHeader !== "string" || !authHeader.startsWith("Bearer ")) return false;
|
|
1341
1417
|
try {
|
|
1342
|
-
await jose.jwtVerify(authHeader.slice("Bearer ".length).trim(), getGoogleJwks(), {
|
|
1343
|
-
audience: expectedAudience
|
|
1418
|
+
const { payload } = await jose.jwtVerify(authHeader.slice("Bearer ".length).trim(), getGoogleJwks(), {
|
|
1419
|
+
audience: expectedAudience,
|
|
1420
|
+
// Any Google Cloud principal can mint an OIDC token with an arbitrary
|
|
1421
|
+
// audience — issuer (and, when configured, the push service-account
|
|
1422
|
+
// email) narrows acceptance to real Pub/Sub push auth tokens. Google
|
|
1423
|
+
// documents both issuer forms, so accept either.
|
|
1424
|
+
issuer: ["https://accounts.google.com", "accounts.google.com"]
|
|
1344
1425
|
});
|
|
1426
|
+
if (expectedServiceAccountEmail) {
|
|
1427
|
+
if (payload["email"] !== expectedServiceAccountEmail || payload["email_verified"] !== true) {
|
|
1428
|
+
return false;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1345
1431
|
return true;
|
|
1346
1432
|
} catch {
|
|
1347
1433
|
return false;
|
|
@@ -1349,7 +1435,11 @@ async function verifyGooglePushToken(req, expectedAudience) {
|
|
|
1349
1435
|
}
|
|
1350
1436
|
async function handleGoogleWebhook(req, res, config, store, purchaseStore, webhookEventStore) {
|
|
1351
1437
|
if (config.google?.pushAudience) {
|
|
1352
|
-
const authenticated = await verifyGooglePushToken(
|
|
1438
|
+
const authenticated = await verifyGooglePushToken(
|
|
1439
|
+
req,
|
|
1440
|
+
config.google.pushAudience,
|
|
1441
|
+
config.google.pushServiceAccountEmail
|
|
1442
|
+
);
|
|
1353
1443
|
if (!authenticated) {
|
|
1354
1444
|
sendError(res, 401, shared.ONESUB_ERROR_CODE.UNAUTHORIZED, "Unauthorized");
|
|
1355
1445
|
return;
|
|
@@ -1360,6 +1450,14 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1360
1450
|
sendError(res, 400, shared.ONESUB_ERROR_CODE.MISSING_MESSAGE_DATA, "Missing message.data");
|
|
1361
1451
|
return;
|
|
1362
1452
|
}
|
|
1453
|
+
const unmarkEvent = async () => {
|
|
1454
|
+
if (webhookEventStore?.unmark && typeof body.message?.messageId === "string") {
|
|
1455
|
+
try {
|
|
1456
|
+
await webhookEventStore.unmark("google", body.message.messageId);
|
|
1457
|
+
} catch {
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
};
|
|
1363
1461
|
if (webhookEventStore && typeof body.message.messageId === "string") {
|
|
1364
1462
|
const fresh = await webhookEventStore.markIfNew("google", body.message.messageId);
|
|
1365
1463
|
if (!fresh) {
|
|
@@ -1393,6 +1491,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1393
1491
|
res.status(200).json({ received: true });
|
|
1394
1492
|
} catch (err) {
|
|
1395
1493
|
log.error("[onesub/webhook/google] voided notification error:", err);
|
|
1494
|
+
await unmarkEvent();
|
|
1396
1495
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process voided notification");
|
|
1397
1496
|
}
|
|
1398
1497
|
return;
|
|
@@ -1419,6 +1518,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1419
1518
|
res.status(200).json({ received: true });
|
|
1420
1519
|
} catch (err) {
|
|
1421
1520
|
log.error("[onesub/webhook/google] oneTimeProduct error:", err);
|
|
1521
|
+
await unmarkEvent();
|
|
1422
1522
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process oneTimeProduct notification");
|
|
1423
1523
|
}
|
|
1424
1524
|
return;
|
|
@@ -1452,7 +1552,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1452
1552
|
} else if (isGoogleExpiredNotification(notificationType)) {
|
|
1453
1553
|
finalStatus = shared.SUBSCRIPTION_STATUS.EXPIRED;
|
|
1454
1554
|
} else {
|
|
1455
|
-
finalStatus = shared.SUBSCRIPTION_STATUS.ACTIVE;
|
|
1555
|
+
finalStatus = existing?.status ?? shared.SUBSCRIPTION_STATUS.ACTIVE;
|
|
1456
1556
|
}
|
|
1457
1557
|
if (isGooglePriceChangeConfirmedNotification(notificationType) && config.google?.onPriceChangeConfirmed) {
|
|
1458
1558
|
const hook = config.google.onPriceChangeConfirmed;
|
|
@@ -1463,7 +1563,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1463
1563
|
if (config.google?.serviceAccountKey) {
|
|
1464
1564
|
const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, config.google);
|
|
1465
1565
|
if (fresh) {
|
|
1466
|
-
const preserveNotificationStatus =
|
|
1566
|
+
const preserveNotificationStatus = isGoogleGracePeriodNotification(notificationType) || isGoogleOnHoldNotification(notificationType);
|
|
1467
1567
|
updated = {
|
|
1468
1568
|
...existing,
|
|
1469
1569
|
status: preserveNotificationStatus ? finalStatus : fresh.status,
|
|
@@ -1479,14 +1579,16 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1479
1579
|
if (config.google?.serviceAccountKey) {
|
|
1480
1580
|
const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, config.google);
|
|
1481
1581
|
if (fresh) {
|
|
1582
|
+
const boundAccountId = fresh.boundAccountId;
|
|
1583
|
+
delete fresh.boundAccountId;
|
|
1482
1584
|
if (fresh.linkedPurchaseToken) {
|
|
1483
1585
|
const previous = await store.getByTransactionId(fresh.linkedPurchaseToken);
|
|
1484
|
-
fresh.userId = previous ? previous.userId : purchaseToken;
|
|
1586
|
+
fresh.userId = previous ? previous.userId : boundAccountId ?? purchaseToken;
|
|
1485
1587
|
if (previous) {
|
|
1486
1588
|
log.info(`[onesub/webhook/google] inherited userId ${previous.userId} from linkedPurchaseToken ${fresh.linkedPurchaseToken} \u2192 new token ${purchaseToken}`);
|
|
1487
1589
|
}
|
|
1488
1590
|
} else {
|
|
1489
|
-
fresh.userId = purchaseToken;
|
|
1591
|
+
fresh.userId = boundAccountId ?? purchaseToken;
|
|
1490
1592
|
}
|
|
1491
1593
|
await store.save(fresh);
|
|
1492
1594
|
}
|
|
@@ -1497,6 +1599,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1497
1599
|
res.status(200).json({ received: true });
|
|
1498
1600
|
} catch (err) {
|
|
1499
1601
|
log.error("[onesub/webhook/google] Error handling notification:", err);
|
|
1602
|
+
await unmarkEvent();
|
|
1500
1603
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process notification");
|
|
1501
1604
|
}
|
|
1502
1605
|
}
|
|
@@ -1590,7 +1693,8 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
1590
1693
|
sendError(res, 422, shared.ONESUB_ERROR_CODE.RECEIPT_VALIDATION_FAILED, "Receipt validation failed", NO_PURCHASE);
|
|
1591
1694
|
return;
|
|
1592
1695
|
}
|
|
1593
|
-
|
|
1696
|
+
const bindingMismatch = platform === "apple" ? boundAccountId && boundAccountId.toLowerCase() !== userId.toLowerCase() : boundAccountId && boundAccountId !== userId;
|
|
1697
|
+
if (bindingMismatch) {
|
|
1594
1698
|
log.warn(
|
|
1595
1699
|
`[onesub/purchase] account binding mismatch for transaction ${transactionId}: token does not match userId ${userId}`
|
|
1596
1700
|
);
|
|
@@ -1824,9 +1928,12 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
1824
1928
|
sendError(res, 404, shared.ONESUB_ERROR_CODE.TRANSACTION_NOT_FOUND, "TRANSACTION_NOT_FOUND");
|
|
1825
1929
|
return;
|
|
1826
1930
|
}
|
|
1827
|
-
await purchaseStore.
|
|
1931
|
+
const moved = await purchaseStore.reassignPurchase(body.transactionId, body.newUserId);
|
|
1932
|
+
if (!moved) {
|
|
1933
|
+
sendError(res, 404, shared.ONESUB_ERROR_CODE.TRANSACTION_NOT_FOUND, "TRANSACTION_NOT_FOUND");
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1828
1936
|
const migrated = { ...existing, userId: body.newUserId };
|
|
1829
|
-
await purchaseStore.savePurchase(migrated);
|
|
1830
1937
|
res.json({ ok: true, purchase: migrated });
|
|
1831
1938
|
});
|
|
1832
1939
|
const grantSchema = zod.z.object({
|
|
@@ -2358,7 +2465,11 @@ var PostgresSubscriptionStore = class {
|
|
|
2358
2465
|
);
|
|
2359
2466
|
});
|
|
2360
2467
|
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
2361
|
-
|
|
2468
|
+
const pool = new Pool({ connectionString: this.connectionString, max: 10 });
|
|
2469
|
+
pool.on("error", (err) => {
|
|
2470
|
+
log.error("[onesub] PostgresSubscriptionStore pool error (idle client):", err);
|
|
2471
|
+
});
|
|
2472
|
+
return pool;
|
|
2362
2473
|
})();
|
|
2363
2474
|
}
|
|
2364
2475
|
return this.poolPromise;
|
|
@@ -2530,7 +2641,11 @@ var PostgresPurchaseStore = class {
|
|
|
2530
2641
|
);
|
|
2531
2642
|
});
|
|
2532
2643
|
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
2533
|
-
|
|
2644
|
+
const pool = new Pool({ connectionString: this.connectionString, max: 10 });
|
|
2645
|
+
pool.on("error", (err) => {
|
|
2646
|
+
log.error("[onesub] PostgresPurchaseStore pool error (idle client):", err);
|
|
2647
|
+
});
|
|
2648
|
+
return pool;
|
|
2534
2649
|
})();
|
|
2535
2650
|
}
|
|
2536
2651
|
return this.poolPromise;
|
|
@@ -2545,23 +2660,12 @@ var PostgresPurchaseStore = class {
|
|
|
2545
2660
|
}
|
|
2546
2661
|
async savePurchase(purchase) {
|
|
2547
2662
|
const pool = await this.getPool();
|
|
2548
|
-
const
|
|
2549
|
-
`SELECT user_id FROM onesub_purchases WHERE transaction_id = $1`,
|
|
2550
|
-
[purchase.transactionId]
|
|
2551
|
-
);
|
|
2552
|
-
if (existing.rows.length > 0) {
|
|
2553
|
-
const owner = existing.rows[0].user_id;
|
|
2554
|
-
if (owner !== purchase.userId) {
|
|
2555
|
-
const err = new Error("TRANSACTION_BELONGS_TO_OTHER_USER");
|
|
2556
|
-
err.code = "TRANSACTION_BELONGS_TO_OTHER_USER";
|
|
2557
|
-
throw err;
|
|
2558
|
-
}
|
|
2559
|
-
return;
|
|
2560
|
-
}
|
|
2561
|
-
await pool.query(
|
|
2663
|
+
const inserted = await pool.query(
|
|
2562
2664
|
`INSERT INTO onesub_purchases
|
|
2563
2665
|
(transaction_id, user_id, product_id, platform, type, quantity, purchased_at)
|
|
2564
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
2666
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
2667
|
+
ON CONFLICT (transaction_id) DO NOTHING
|
|
2668
|
+
RETURNING transaction_id`,
|
|
2565
2669
|
[
|
|
2566
2670
|
purchase.transactionId,
|
|
2567
2671
|
purchase.userId,
|
|
@@ -2572,6 +2676,17 @@ var PostgresPurchaseStore = class {
|
|
|
2572
2676
|
purchase.purchasedAt
|
|
2573
2677
|
]
|
|
2574
2678
|
);
|
|
2679
|
+
if (inserted.rows.length > 0) return;
|
|
2680
|
+
const existing = await pool.query(
|
|
2681
|
+
`SELECT user_id FROM onesub_purchases WHERE transaction_id = $1`,
|
|
2682
|
+
[purchase.transactionId]
|
|
2683
|
+
);
|
|
2684
|
+
const owner = existing.rows[0]?.user_id;
|
|
2685
|
+
if (owner !== void 0 && owner !== purchase.userId) {
|
|
2686
|
+
const err = new Error("TRANSACTION_BELONGS_TO_OTHER_USER");
|
|
2687
|
+
err.code = "TRANSACTION_BELONGS_TO_OTHER_USER";
|
|
2688
|
+
throw err;
|
|
2689
|
+
}
|
|
2575
2690
|
}
|
|
2576
2691
|
async getPurchasesByUserId(userId) {
|
|
2577
2692
|
const pool = await this.getPool();
|
|
@@ -2672,7 +2787,6 @@ function rowToPurchaseInfo(row) {
|
|
|
2672
2787
|
// src/stores/redis.ts
|
|
2673
2788
|
var SUB_TX_PREFIX = "onesub:sub:tx:";
|
|
2674
2789
|
var SUB_USER_PREFIX = "onesub:sub:user:";
|
|
2675
|
-
var SUB_ALL = "onesub:sub:all";
|
|
2676
2790
|
var SUB_ALL_SORTED = "onesub:sub:all:sorted";
|
|
2677
2791
|
var PUR_TX_PREFIX = "onesub:purchase:tx:";
|
|
2678
2792
|
var PUR_USER_PREFIX = "onesub:purchase:user:";
|
|
@@ -2687,10 +2801,14 @@ var RedisSubscriptionStore = class {
|
|
|
2687
2801
|
const score = Date.now();
|
|
2688
2802
|
const txKey = SUB_TX_PREFIX + sub.originalTransactionId;
|
|
2689
2803
|
const userKey = SUB_USER_PREFIX + sub.userId;
|
|
2804
|
+
const prevRaw = await this.redis.get(txKey);
|
|
2805
|
+
const prevUserId = prevRaw ? JSON.parse(prevRaw).userId : null;
|
|
2690
2806
|
const pipeline = this.redis.multi();
|
|
2691
2807
|
pipeline.set(txKey, JSON.stringify(sub));
|
|
2808
|
+
if (prevUserId !== null && prevUserId !== sub.userId) {
|
|
2809
|
+
pipeline.zrem(SUB_USER_PREFIX + prevUserId, sub.originalTransactionId);
|
|
2810
|
+
}
|
|
2692
2811
|
pipeline.zadd(userKey, score, sub.originalTransactionId);
|
|
2693
|
-
pipeline.sadd(SUB_ALL, sub.originalTransactionId);
|
|
2694
2812
|
pipeline.zadd(SUB_ALL_SORTED, score, sub.originalTransactionId);
|
|
2695
2813
|
await pipeline.exec();
|
|
2696
2814
|
}
|
|
@@ -2762,19 +2880,18 @@ var RedisPurchaseStore = class {
|
|
|
2762
2880
|
redis;
|
|
2763
2881
|
async savePurchase(purchase) {
|
|
2764
2882
|
const txKey = PUR_TX_PREFIX + purchase.transactionId;
|
|
2765
|
-
const
|
|
2766
|
-
if (
|
|
2767
|
-
const
|
|
2768
|
-
|
|
2883
|
+
const claimed = await this.redis.set(txKey, JSON.stringify(purchase), "NX");
|
|
2884
|
+
if (claimed !== "OK") {
|
|
2885
|
+
const existing = await this.redis.get(txKey);
|
|
2886
|
+
const owner = existing ? JSON.parse(existing).userId : null;
|
|
2887
|
+
if (owner !== null && owner !== purchase.userId) {
|
|
2769
2888
|
const err = new Error("TRANSACTION_BELONGS_TO_OTHER_USER");
|
|
2770
2889
|
err.code = "TRANSACTION_BELONGS_TO_OTHER_USER";
|
|
2771
2890
|
throw err;
|
|
2772
2891
|
}
|
|
2773
|
-
return;
|
|
2774
2892
|
}
|
|
2775
2893
|
const score = Date.parse(purchase.purchasedAt) || Date.now();
|
|
2776
2894
|
const pipeline = this.redis.multi();
|
|
2777
|
-
pipeline.set(txKey, JSON.stringify(purchase));
|
|
2778
2895
|
pipeline.zadd(PUR_USER_PREFIX + purchase.userId, score, purchase.transactionId);
|
|
2779
2896
|
pipeline.sadd(PUR_USER_PRODUCT_PREFIX + purchase.userId + ":" + purchase.productId, purchase.transactionId);
|
|
2780
2897
|
pipeline.sadd(PUR_ALL, purchase.transactionId);
|
|
@@ -2885,6 +3002,9 @@ var RedisWebhookEventStore = class {
|
|
|
2885
3002
|
const result = await this.redis.set(key, "1", "EX", this.ttlSeconds, "NX");
|
|
2886
3003
|
return result === "OK";
|
|
2887
3004
|
}
|
|
3005
|
+
async unmark(provider, eventId) {
|
|
3006
|
+
await this.redis.del(WEBHOOK_EVENT_PREFIX + provider + ":" + eventId);
|
|
3007
|
+
}
|
|
2888
3008
|
};
|
|
2889
3009
|
|
|
2890
3010
|
// src/webhook-events.ts
|
|
@@ -2902,6 +3022,9 @@ var InMemoryWebhookEventStore = class {
|
|
|
2902
3022
|
this.seen.set(key, Date.now() + this.ttlSeconds * 1e3);
|
|
2903
3023
|
return true;
|
|
2904
3024
|
}
|
|
3025
|
+
async unmark(provider, eventId) {
|
|
3026
|
+
this.seen.delete(`${provider}:${eventId}`);
|
|
3027
|
+
}
|
|
2905
3028
|
evictExpired() {
|
|
2906
3029
|
const now = Date.now();
|
|
2907
3030
|
for (const [key, expiresAt] of this.seen) {
|
|
@@ -2923,6 +3046,9 @@ var CacheWebhookEventStore = class {
|
|
|
2923
3046
|
await this.cache.set(key, "1", this.ttlSeconds);
|
|
2924
3047
|
return true;
|
|
2925
3048
|
}
|
|
3049
|
+
async unmark(provider, eventId) {
|
|
3050
|
+
await this.cache.del(`webhook:event:${provider}:${eventId}`);
|
|
3051
|
+
}
|
|
2926
3052
|
};
|
|
2927
3053
|
|
|
2928
3054
|
// src/webhook-queue.ts
|
|
@@ -3200,8 +3326,6 @@ function openapiHandler() {
|
|
|
3200
3326
|
res.json(ONESUB_OPENAPI);
|
|
3201
3327
|
};
|
|
3202
3328
|
}
|
|
3203
|
-
|
|
3204
|
-
// src/tracing.ts
|
|
3205
3329
|
var NOOP_SPAN = {
|
|
3206
3330
|
setAttribute: () => {
|
|
3207
3331
|
},
|
|
@@ -3218,8 +3342,7 @@ function getTracer() {
|
|
|
3218
3342
|
if (resolved) return cachedTracer;
|
|
3219
3343
|
resolved = true;
|
|
3220
3344
|
try {
|
|
3221
|
-
const req =
|
|
3222
|
-
if (!req) return null;
|
|
3345
|
+
const req = typeof __require === "function" ? __require : module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
3223
3346
|
const otel = req("@opentelemetry/api");
|
|
3224
3347
|
cachedTracer = otel.trace.getTracer("@onesub/server", "1.0.0");
|
|
3225
3348
|
return cachedTracer;
|