@onesub/server 0.13.1 → 0.15.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__/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 +197 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +197 -52
- package/dist/index.js.map +1 -1
- package/dist/providers/apple.d.ts +8 -0
- package/dist/providers/apple.d.ts.map +1 -1
- package/dist/providers/google.d.ts +8 -0
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/mock.d.ts +2 -0
- 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 +2 -2
- 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 @@
|
|
|
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,17 +281,20 @@ 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) {
|
|
255
289
|
const outcome = classifyMockReceipt(receipt);
|
|
256
290
|
if (!outcomePasses(outcome, "apple")) return null;
|
|
257
291
|
const productId = expectedProductId ?? "mock_product";
|
|
292
|
+
const tokenMatch = receipt.match(/#token=([^#]+)/);
|
|
258
293
|
return {
|
|
259
294
|
transactionId: deterministicTransactionId(`mock_apple_${productId}`, receipt),
|
|
260
295
|
productId,
|
|
261
|
-
purchasedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
296
|
+
purchasedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
297
|
+
...tokenMatch ? { appAccountToken: tokenMatch[1] } : {}
|
|
262
298
|
};
|
|
263
299
|
}
|
|
264
300
|
function mockValidateGoogleSubscription(receipt, productId) {
|
|
@@ -266,23 +302,29 @@ function mockValidateGoogleSubscription(receipt, productId) {
|
|
|
266
302
|
if (!outcomePasses(outcome, "google")) return null;
|
|
267
303
|
const now = Date.now();
|
|
268
304
|
const expiresAt = outcome.kind === "sandbox" ? now + 1 * HOURS : now + 30 * DAYS;
|
|
305
|
+
const tokenMatch = receipt.match(/#token=([^#]+)/);
|
|
269
306
|
return {
|
|
270
307
|
userId: "",
|
|
271
308
|
productId,
|
|
272
309
|
platform: "google",
|
|
273
310
|
status: shared.SUBSCRIPTION_STATUS.ACTIVE,
|
|
274
311
|
expiresAt: new Date(expiresAt).toISOString(),
|
|
275
|
-
|
|
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,
|
|
276
315
|
purchasedAt: new Date(now).toISOString(),
|
|
277
|
-
willRenew: true
|
|
316
|
+
willRenew: true,
|
|
317
|
+
...tokenMatch ? { boundAccountId: tokenMatch[1] } : {}
|
|
278
318
|
};
|
|
279
319
|
}
|
|
280
320
|
function mockValidateGoogleProduct(receipt, productId) {
|
|
281
321
|
const outcome = classifyMockReceipt(receipt);
|
|
282
322
|
if (!outcomePasses(outcome, "google")) return null;
|
|
323
|
+
const tokenMatch = receipt.match(/#token=([^#]+)/);
|
|
283
324
|
return {
|
|
284
325
|
transactionId: deterministicTransactionId(`mock_google_${productId}`, receipt),
|
|
285
|
-
purchasedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
326
|
+
purchasedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
327
|
+
...tokenMatch ? { obfuscatedExternalAccountId: tokenMatch[1] } : {}
|
|
286
328
|
};
|
|
287
329
|
}
|
|
288
330
|
|
|
@@ -390,8 +432,9 @@ async function validateAppleReceipt(receipt, config) {
|
|
|
390
432
|
expiresAt: new Date(tx.expiresDate).toISOString(),
|
|
391
433
|
originalTransactionId: tx.originalTransactionId,
|
|
392
434
|
purchasedAt: new Date(purchasedAt).toISOString(),
|
|
393
|
-
willRenew: status === shared.SUBSCRIPTION_STATUS.ACTIVE
|
|
435
|
+
willRenew: status === shared.SUBSCRIPTION_STATUS.ACTIVE,
|
|
394
436
|
// refined by renewal info in webhook
|
|
437
|
+
...tx.appAccountToken ? { boundAccountId: tx.appAccountToken } : {}
|
|
395
438
|
};
|
|
396
439
|
}
|
|
397
440
|
async function validateAppleConsumableReceipt(signedTransaction, config, expectedProductId) {
|
|
@@ -446,7 +489,8 @@ async function validateAppleConsumableReceipt(signedTransaction, config, expecte
|
|
|
446
489
|
return {
|
|
447
490
|
transactionId,
|
|
448
491
|
productId: tx.productId,
|
|
449
|
-
purchasedAt: tx.purchaseDate ? new Date(tx.purchaseDate).toISOString() : (/* @__PURE__ */ new Date()).toISOString()
|
|
492
|
+
purchasedAt: tx.purchaseDate ? new Date(tx.purchaseDate).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
493
|
+
appAccountToken: tx.appAccountToken ?? void 0
|
|
450
494
|
};
|
|
451
495
|
}
|
|
452
496
|
async function decodeAppleNotification(payload, skipJwsVerification = false) {
|
|
@@ -950,6 +994,7 @@ async function validateGoogleReceipt(receipt, productId, config) {
|
|
|
950
994
|
const purchasedAt = purchase.startTime ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
951
995
|
const willRenew = lineItem.autoRenewingPlan?.autoRenewEnabled ?? false;
|
|
952
996
|
const autoResumeTime = status === shared.SUBSCRIPTION_STATUS.PAUSED ? purchase.pausedStateContext?.autoResumeTime : void 0;
|
|
997
|
+
const boundAccountId = purchase.externalAccountIdentifiers?.obfuscatedExternalAccountId;
|
|
953
998
|
return {
|
|
954
999
|
userId: "",
|
|
955
1000
|
// caller fills this in
|
|
@@ -957,13 +1002,18 @@ async function validateGoogleReceipt(receipt, productId, config) {
|
|
|
957
1002
|
platform: "google",
|
|
958
1003
|
status,
|
|
959
1004
|
expiresAt,
|
|
960
|
-
|
|
1005
|
+
// Keyed by the purchaseToken, NOT latestOrderId: RTDN webhooks and
|
|
1006
|
+
// linkedPurchaseToken chains only carry the token, so records keyed by
|
|
1007
|
+
// order id are invisible to the entire webhook lifecycle (refund/cancel/
|
|
1008
|
+
// renewal would never find them).
|
|
1009
|
+
originalTransactionId: receipt,
|
|
961
1010
|
purchasedAt,
|
|
962
1011
|
willRenew,
|
|
963
1012
|
// Surface the linked token so host apps (and our webhook userId-continuity
|
|
964
1013
|
// logic) can follow upgrade/downgrade chains.
|
|
965
1014
|
linkedPurchaseToken: purchase.linkedPurchaseToken,
|
|
966
|
-
autoResumeTime
|
|
1015
|
+
autoResumeTime,
|
|
1016
|
+
...boundAccountId ? { boundAccountId } : {}
|
|
967
1017
|
};
|
|
968
1018
|
}
|
|
969
1019
|
async function validateGoogleProductReceipt(purchaseToken, productId, config, type = "non_consumable") {
|
|
@@ -1005,7 +1055,8 @@ async function validateGoogleProductReceipt(purchaseToken, productId, config, ty
|
|
|
1005
1055
|
}
|
|
1006
1056
|
return {
|
|
1007
1057
|
transactionId: purchase.orderId,
|
|
1008
|
-
purchasedAt: purchase.purchaseTimeMillis ? new Date(parseInt(purchase.purchaseTimeMillis, 10)).toISOString() : (/* @__PURE__ */ new Date()).toISOString()
|
|
1058
|
+
purchasedAt: purchase.purchaseTimeMillis ? new Date(parseInt(purchase.purchaseTimeMillis, 10)).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
1059
|
+
obfuscatedExternalAccountId: purchase.obfuscatedExternalAccountId
|
|
1009
1060
|
};
|
|
1010
1061
|
}
|
|
1011
1062
|
function decodeGoogleNotification(payload) {
|
|
@@ -1138,6 +1189,22 @@ function createValidateRouter(config, store) {
|
|
|
1138
1189
|
sendError(res, 422, shared.ONESUB_ERROR_CODE.RECEIPT_VALIDATION_FAILED, "Receipt validation failed", NO_SUB);
|
|
1139
1190
|
return;
|
|
1140
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
|
+
}
|
|
1141
1208
|
sub.userId = userId;
|
|
1142
1209
|
await store.save(sub);
|
|
1143
1210
|
if (platform === "google" && config.google) {
|
|
@@ -1185,7 +1252,7 @@ function createStatusRouter(store) {
|
|
|
1185
1252
|
return router;
|
|
1186
1253
|
}
|
|
1187
1254
|
var APPLE_ACTIVE_TYPES = /* @__PURE__ */ new Set(["SUBSCRIBED", "DID_RENEW", "DID_RECOVER", "OFFER_REDEEMED"]);
|
|
1188
|
-
var APPLE_CANCELED_TYPES = /* @__PURE__ */ new Set(["REVOKE", "REFUND"
|
|
1255
|
+
var APPLE_CANCELED_TYPES = /* @__PURE__ */ new Set(["REVOKE", "REFUND"]);
|
|
1189
1256
|
var APPLE_EXPIRED_TYPES = /* @__PURE__ */ new Set(["EXPIRED"]);
|
|
1190
1257
|
function mapAppleNotificationStatus(notificationType, subtype) {
|
|
1191
1258
|
if (notificationType === "DID_FAIL_TO_RENEW") {
|
|
@@ -1242,6 +1309,11 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1242
1309
|
} = decoded;
|
|
1243
1310
|
const notificationType = payload.notificationType;
|
|
1244
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
|
+
}
|
|
1245
1317
|
const mapped = mapAppleNotificationStatus(notificationType, subtype);
|
|
1246
1318
|
const finalStatus = mapped ?? status;
|
|
1247
1319
|
if (notificationType === "CONSUMPTION_REQUEST" && config.apple?.consumptionInfoProvider && transactionId && productId) {
|
|
@@ -1268,6 +1340,10 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1268
1340
|
}
|
|
1269
1341
|
const isOneTimePurchase = type === "Consumable" || type === "Non-Consumable";
|
|
1270
1342
|
const isRefundOrRevoke = APPLE_CANCELED_TYPES.has(notificationType);
|
|
1343
|
+
if (isOneTimePurchase && notificationType === "CONSUMPTION_REQUEST") {
|
|
1344
|
+
res.status(200).json({ received: true });
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1271
1347
|
try {
|
|
1272
1348
|
if (isOneTimePurchase && isRefundOrRevoke) {
|
|
1273
1349
|
const lookupId = transactionId ?? originalTransactionId;
|
|
@@ -1280,7 +1356,7 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1280
1356
|
}
|
|
1281
1357
|
const existing = await store.getByTransactionId(originalTransactionId);
|
|
1282
1358
|
if (existing) {
|
|
1283
|
-
const isSubscriptionRefund = isRefundOrRevoke && !isOneTimePurchase
|
|
1359
|
+
const isSubscriptionRefund = isRefundOrRevoke && !isOneTimePurchase;
|
|
1284
1360
|
const keepEntitlement = isSubscriptionRefund && config.refundPolicy === "until_expiry";
|
|
1285
1361
|
const correctedUserId = appAccountToken && existing.userId === originalTransactionId ? appAccountToken : existing.userId;
|
|
1286
1362
|
if (correctedUserId !== existing.userId) {
|
|
@@ -1320,6 +1396,12 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1320
1396
|
res.status(200).json({ received: true });
|
|
1321
1397
|
} catch (err) {
|
|
1322
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
|
+
}
|
|
1323
1405
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to update subscription");
|
|
1324
1406
|
}
|
|
1325
1407
|
}
|
|
@@ -1329,13 +1411,23 @@ function getGoogleJwks() {
|
|
|
1329
1411
|
if (!googleJwks) googleJwks = jose.createRemoteJWKSet(new URL(GOOGLE_JWKS_URL));
|
|
1330
1412
|
return googleJwks;
|
|
1331
1413
|
}
|
|
1332
|
-
async function verifyGooglePushToken(req, expectedAudience) {
|
|
1414
|
+
async function verifyGooglePushToken(req, expectedAudience, expectedServiceAccountEmail) {
|
|
1333
1415
|
const authHeader = req.headers["authorization"];
|
|
1334
1416
|
if (typeof authHeader !== "string" || !authHeader.startsWith("Bearer ")) return false;
|
|
1335
1417
|
try {
|
|
1336
|
-
await jose.jwtVerify(authHeader.slice("Bearer ".length).trim(), getGoogleJwks(), {
|
|
1337
|
-
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"]
|
|
1338
1425
|
});
|
|
1426
|
+
if (expectedServiceAccountEmail) {
|
|
1427
|
+
if (payload["email"] !== expectedServiceAccountEmail || payload["email_verified"] !== true) {
|
|
1428
|
+
return false;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1339
1431
|
return true;
|
|
1340
1432
|
} catch {
|
|
1341
1433
|
return false;
|
|
@@ -1343,7 +1435,11 @@ async function verifyGooglePushToken(req, expectedAudience) {
|
|
|
1343
1435
|
}
|
|
1344
1436
|
async function handleGoogleWebhook(req, res, config, store, purchaseStore, webhookEventStore) {
|
|
1345
1437
|
if (config.google?.pushAudience) {
|
|
1346
|
-
const authenticated = await verifyGooglePushToken(
|
|
1438
|
+
const authenticated = await verifyGooglePushToken(
|
|
1439
|
+
req,
|
|
1440
|
+
config.google.pushAudience,
|
|
1441
|
+
config.google.pushServiceAccountEmail
|
|
1442
|
+
);
|
|
1347
1443
|
if (!authenticated) {
|
|
1348
1444
|
sendError(res, 401, shared.ONESUB_ERROR_CODE.UNAUTHORIZED, "Unauthorized");
|
|
1349
1445
|
return;
|
|
@@ -1354,6 +1450,14 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1354
1450
|
sendError(res, 400, shared.ONESUB_ERROR_CODE.MISSING_MESSAGE_DATA, "Missing message.data");
|
|
1355
1451
|
return;
|
|
1356
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
|
+
};
|
|
1357
1461
|
if (webhookEventStore && typeof body.message.messageId === "string") {
|
|
1358
1462
|
const fresh = await webhookEventStore.markIfNew("google", body.message.messageId);
|
|
1359
1463
|
if (!fresh) {
|
|
@@ -1387,6 +1491,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1387
1491
|
res.status(200).json({ received: true });
|
|
1388
1492
|
} catch (err) {
|
|
1389
1493
|
log.error("[onesub/webhook/google] voided notification error:", err);
|
|
1494
|
+
await unmarkEvent();
|
|
1390
1495
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process voided notification");
|
|
1391
1496
|
}
|
|
1392
1497
|
return;
|
|
@@ -1413,6 +1518,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1413
1518
|
res.status(200).json({ received: true });
|
|
1414
1519
|
} catch (err) {
|
|
1415
1520
|
log.error("[onesub/webhook/google] oneTimeProduct error:", err);
|
|
1521
|
+
await unmarkEvent();
|
|
1416
1522
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process oneTimeProduct notification");
|
|
1417
1523
|
}
|
|
1418
1524
|
return;
|
|
@@ -1446,7 +1552,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1446
1552
|
} else if (isGoogleExpiredNotification(notificationType)) {
|
|
1447
1553
|
finalStatus = shared.SUBSCRIPTION_STATUS.EXPIRED;
|
|
1448
1554
|
} else {
|
|
1449
|
-
finalStatus = shared.SUBSCRIPTION_STATUS.ACTIVE;
|
|
1555
|
+
finalStatus = existing?.status ?? shared.SUBSCRIPTION_STATUS.ACTIVE;
|
|
1450
1556
|
}
|
|
1451
1557
|
if (isGooglePriceChangeConfirmedNotification(notificationType) && config.google?.onPriceChangeConfirmed) {
|
|
1452
1558
|
const hook = config.google.onPriceChangeConfirmed;
|
|
@@ -1457,7 +1563,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1457
1563
|
if (config.google?.serviceAccountKey) {
|
|
1458
1564
|
const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, config.google);
|
|
1459
1565
|
if (fresh) {
|
|
1460
|
-
const preserveNotificationStatus =
|
|
1566
|
+
const preserveNotificationStatus = isGoogleGracePeriodNotification(notificationType) || isGoogleOnHoldNotification(notificationType);
|
|
1461
1567
|
updated = {
|
|
1462
1568
|
...existing,
|
|
1463
1569
|
status: preserveNotificationStatus ? finalStatus : fresh.status,
|
|
@@ -1473,14 +1579,16 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1473
1579
|
if (config.google?.serviceAccountKey) {
|
|
1474
1580
|
const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, config.google);
|
|
1475
1581
|
if (fresh) {
|
|
1582
|
+
const boundAccountId = fresh.boundAccountId;
|
|
1583
|
+
delete fresh.boundAccountId;
|
|
1476
1584
|
if (fresh.linkedPurchaseToken) {
|
|
1477
1585
|
const previous = await store.getByTransactionId(fresh.linkedPurchaseToken);
|
|
1478
|
-
fresh.userId = previous ? previous.userId : purchaseToken;
|
|
1586
|
+
fresh.userId = previous ? previous.userId : boundAccountId ?? purchaseToken;
|
|
1479
1587
|
if (previous) {
|
|
1480
1588
|
log.info(`[onesub/webhook/google] inherited userId ${previous.userId} from linkedPurchaseToken ${fresh.linkedPurchaseToken} \u2192 new token ${purchaseToken}`);
|
|
1481
1589
|
}
|
|
1482
1590
|
} else {
|
|
1483
|
-
fresh.userId = purchaseToken;
|
|
1591
|
+
fresh.userId = boundAccountId ?? purchaseToken;
|
|
1484
1592
|
}
|
|
1485
1593
|
await store.save(fresh);
|
|
1486
1594
|
}
|
|
@@ -1491,6 +1599,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1491
1599
|
res.status(200).json({ received: true });
|
|
1492
1600
|
} catch (err) {
|
|
1493
1601
|
log.error("[onesub/webhook/google] Error handling notification:", err);
|
|
1602
|
+
await unmarkEvent();
|
|
1494
1603
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process notification");
|
|
1495
1604
|
}
|
|
1496
1605
|
}
|
|
@@ -1551,6 +1660,7 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
1551
1660
|
}
|
|
1552
1661
|
let transactionId = null;
|
|
1553
1662
|
let purchasedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1663
|
+
let boundAccountId = null;
|
|
1554
1664
|
if (platform === "apple") {
|
|
1555
1665
|
if (!config.apple) {
|
|
1556
1666
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.APPLE_CONFIG_MISSING, "Apple configuration not provided", NO_PURCHASE);
|
|
@@ -1560,6 +1670,7 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
1560
1670
|
if (result) {
|
|
1561
1671
|
transactionId = result.transactionId;
|
|
1562
1672
|
purchasedAt = result.purchasedAt;
|
|
1673
|
+
boundAccountId = result.appAccountToken ?? null;
|
|
1563
1674
|
}
|
|
1564
1675
|
} else {
|
|
1565
1676
|
if (!config.google) {
|
|
@@ -1575,12 +1686,27 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
1575
1686
|
if (result) {
|
|
1576
1687
|
transactionId = result.transactionId;
|
|
1577
1688
|
purchasedAt = result.purchasedAt;
|
|
1689
|
+
boundAccountId = result.obfuscatedExternalAccountId ?? null;
|
|
1578
1690
|
}
|
|
1579
1691
|
}
|
|
1580
1692
|
if (!transactionId) {
|
|
1581
1693
|
sendError(res, 422, shared.ONESUB_ERROR_CODE.RECEIPT_VALIDATION_FAILED, "Receipt validation failed", NO_PURCHASE);
|
|
1582
1694
|
return;
|
|
1583
1695
|
}
|
|
1696
|
+
const bindingMismatch = platform === "apple" ? boundAccountId && boundAccountId.toLowerCase() !== userId.toLowerCase() : boundAccountId && boundAccountId !== userId;
|
|
1697
|
+
if (bindingMismatch) {
|
|
1698
|
+
log.warn(
|
|
1699
|
+
`[onesub/purchase] account binding mismatch for transaction ${transactionId}: token does not match userId ${userId}`
|
|
1700
|
+
);
|
|
1701
|
+
sendError(
|
|
1702
|
+
res,
|
|
1703
|
+
409,
|
|
1704
|
+
shared.ONESUB_ERROR_CODE.TRANSACTION_BELONGS_TO_OTHER_USER,
|
|
1705
|
+
"TRANSACTION_BELONGS_TO_OTHER_USER",
|
|
1706
|
+
NO_PURCHASE
|
|
1707
|
+
);
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1584
1710
|
const existing = await purchaseStore.getPurchaseByTransactionId(transactionId);
|
|
1585
1711
|
let action = "new";
|
|
1586
1712
|
if (existing) {
|
|
@@ -1802,9 +1928,12 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
1802
1928
|
sendError(res, 404, shared.ONESUB_ERROR_CODE.TRANSACTION_NOT_FOUND, "TRANSACTION_NOT_FOUND");
|
|
1803
1929
|
return;
|
|
1804
1930
|
}
|
|
1805
|
-
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
|
+
}
|
|
1806
1936
|
const migrated = { ...existing, userId: body.newUserId };
|
|
1807
|
-
await purchaseStore.savePurchase(migrated);
|
|
1808
1937
|
res.json({ ok: true, purchase: migrated });
|
|
1809
1938
|
});
|
|
1810
1939
|
const grantSchema = zod.z.object({
|
|
@@ -2336,7 +2465,11 @@ var PostgresSubscriptionStore = class {
|
|
|
2336
2465
|
);
|
|
2337
2466
|
});
|
|
2338
2467
|
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
2339
|
-
|
|
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;
|
|
2340
2473
|
})();
|
|
2341
2474
|
}
|
|
2342
2475
|
return this.poolPromise;
|
|
@@ -2508,7 +2641,11 @@ var PostgresPurchaseStore = class {
|
|
|
2508
2641
|
);
|
|
2509
2642
|
});
|
|
2510
2643
|
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
2511
|
-
|
|
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;
|
|
2512
2649
|
})();
|
|
2513
2650
|
}
|
|
2514
2651
|
return this.poolPromise;
|
|
@@ -2523,23 +2660,12 @@ var PostgresPurchaseStore = class {
|
|
|
2523
2660
|
}
|
|
2524
2661
|
async savePurchase(purchase) {
|
|
2525
2662
|
const pool = await this.getPool();
|
|
2526
|
-
const
|
|
2527
|
-
`SELECT user_id FROM onesub_purchases WHERE transaction_id = $1`,
|
|
2528
|
-
[purchase.transactionId]
|
|
2529
|
-
);
|
|
2530
|
-
if (existing.rows.length > 0) {
|
|
2531
|
-
const owner = existing.rows[0].user_id;
|
|
2532
|
-
if (owner !== purchase.userId) {
|
|
2533
|
-
const err = new Error("TRANSACTION_BELONGS_TO_OTHER_USER");
|
|
2534
|
-
err.code = "TRANSACTION_BELONGS_TO_OTHER_USER";
|
|
2535
|
-
throw err;
|
|
2536
|
-
}
|
|
2537
|
-
return;
|
|
2538
|
-
}
|
|
2539
|
-
await pool.query(
|
|
2663
|
+
const inserted = await pool.query(
|
|
2540
2664
|
`INSERT INTO onesub_purchases
|
|
2541
2665
|
(transaction_id, user_id, product_id, platform, type, quantity, purchased_at)
|
|
2542
|
-
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`,
|
|
2543
2669
|
[
|
|
2544
2670
|
purchase.transactionId,
|
|
2545
2671
|
purchase.userId,
|
|
@@ -2550,6 +2676,17 @@ var PostgresPurchaseStore = class {
|
|
|
2550
2676
|
purchase.purchasedAt
|
|
2551
2677
|
]
|
|
2552
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
|
+
}
|
|
2553
2690
|
}
|
|
2554
2691
|
async getPurchasesByUserId(userId) {
|
|
2555
2692
|
const pool = await this.getPool();
|
|
@@ -2650,7 +2787,6 @@ function rowToPurchaseInfo(row) {
|
|
|
2650
2787
|
// src/stores/redis.ts
|
|
2651
2788
|
var SUB_TX_PREFIX = "onesub:sub:tx:";
|
|
2652
2789
|
var SUB_USER_PREFIX = "onesub:sub:user:";
|
|
2653
|
-
var SUB_ALL = "onesub:sub:all";
|
|
2654
2790
|
var SUB_ALL_SORTED = "onesub:sub:all:sorted";
|
|
2655
2791
|
var PUR_TX_PREFIX = "onesub:purchase:tx:";
|
|
2656
2792
|
var PUR_USER_PREFIX = "onesub:purchase:user:";
|
|
@@ -2665,10 +2801,14 @@ var RedisSubscriptionStore = class {
|
|
|
2665
2801
|
const score = Date.now();
|
|
2666
2802
|
const txKey = SUB_TX_PREFIX + sub.originalTransactionId;
|
|
2667
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;
|
|
2668
2806
|
const pipeline = this.redis.multi();
|
|
2669
2807
|
pipeline.set(txKey, JSON.stringify(sub));
|
|
2808
|
+
if (prevUserId !== null && prevUserId !== sub.userId) {
|
|
2809
|
+
pipeline.zrem(SUB_USER_PREFIX + prevUserId, sub.originalTransactionId);
|
|
2810
|
+
}
|
|
2670
2811
|
pipeline.zadd(userKey, score, sub.originalTransactionId);
|
|
2671
|
-
pipeline.sadd(SUB_ALL, sub.originalTransactionId);
|
|
2672
2812
|
pipeline.zadd(SUB_ALL_SORTED, score, sub.originalTransactionId);
|
|
2673
2813
|
await pipeline.exec();
|
|
2674
2814
|
}
|
|
@@ -2740,19 +2880,18 @@ var RedisPurchaseStore = class {
|
|
|
2740
2880
|
redis;
|
|
2741
2881
|
async savePurchase(purchase) {
|
|
2742
2882
|
const txKey = PUR_TX_PREFIX + purchase.transactionId;
|
|
2743
|
-
const
|
|
2744
|
-
if (
|
|
2745
|
-
const
|
|
2746
|
-
|
|
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) {
|
|
2747
2888
|
const err = new Error("TRANSACTION_BELONGS_TO_OTHER_USER");
|
|
2748
2889
|
err.code = "TRANSACTION_BELONGS_TO_OTHER_USER";
|
|
2749
2890
|
throw err;
|
|
2750
2891
|
}
|
|
2751
|
-
return;
|
|
2752
2892
|
}
|
|
2753
2893
|
const score = Date.parse(purchase.purchasedAt) || Date.now();
|
|
2754
2894
|
const pipeline = this.redis.multi();
|
|
2755
|
-
pipeline.set(txKey, JSON.stringify(purchase));
|
|
2756
2895
|
pipeline.zadd(PUR_USER_PREFIX + purchase.userId, score, purchase.transactionId);
|
|
2757
2896
|
pipeline.sadd(PUR_USER_PRODUCT_PREFIX + purchase.userId + ":" + purchase.productId, purchase.transactionId);
|
|
2758
2897
|
pipeline.sadd(PUR_ALL, purchase.transactionId);
|
|
@@ -2863,6 +3002,9 @@ var RedisWebhookEventStore = class {
|
|
|
2863
3002
|
const result = await this.redis.set(key, "1", "EX", this.ttlSeconds, "NX");
|
|
2864
3003
|
return result === "OK";
|
|
2865
3004
|
}
|
|
3005
|
+
async unmark(provider, eventId) {
|
|
3006
|
+
await this.redis.del(WEBHOOK_EVENT_PREFIX + provider + ":" + eventId);
|
|
3007
|
+
}
|
|
2866
3008
|
};
|
|
2867
3009
|
|
|
2868
3010
|
// src/webhook-events.ts
|
|
@@ -2880,6 +3022,9 @@ var InMemoryWebhookEventStore = class {
|
|
|
2880
3022
|
this.seen.set(key, Date.now() + this.ttlSeconds * 1e3);
|
|
2881
3023
|
return true;
|
|
2882
3024
|
}
|
|
3025
|
+
async unmark(provider, eventId) {
|
|
3026
|
+
this.seen.delete(`${provider}:${eventId}`);
|
|
3027
|
+
}
|
|
2883
3028
|
evictExpired() {
|
|
2884
3029
|
const now = Date.now();
|
|
2885
3030
|
for (const [key, expiresAt] of this.seen) {
|
|
@@ -2901,6 +3046,9 @@ var CacheWebhookEventStore = class {
|
|
|
2901
3046
|
await this.cache.set(key, "1", this.ttlSeconds);
|
|
2902
3047
|
return true;
|
|
2903
3048
|
}
|
|
3049
|
+
async unmark(provider, eventId) {
|
|
3050
|
+
await this.cache.del(`webhook:event:${provider}:${eventId}`);
|
|
3051
|
+
}
|
|
2904
3052
|
};
|
|
2905
3053
|
|
|
2906
3054
|
// src/webhook-queue.ts
|
|
@@ -3178,8 +3326,6 @@ function openapiHandler() {
|
|
|
3178
3326
|
res.json(ONESUB_OPENAPI);
|
|
3179
3327
|
};
|
|
3180
3328
|
}
|
|
3181
|
-
|
|
3182
|
-
// src/tracing.ts
|
|
3183
3329
|
var NOOP_SPAN = {
|
|
3184
3330
|
setAttribute: () => {
|
|
3185
3331
|
},
|
|
@@ -3196,8 +3342,7 @@ function getTracer() {
|
|
|
3196
3342
|
if (resolved) return cachedTracer;
|
|
3197
3343
|
resolved = true;
|
|
3198
3344
|
try {
|
|
3199
|
-
const req =
|
|
3200
|
-
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)));
|
|
3201
3346
|
const otel = req("@opentelemetry/api");
|
|
3202
3347
|
cachedTracer = otel.trace.getTracer("@onesub/server", "1.0.0");
|
|
3203
3348
|
return cachedTracer;
|