@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
package/dist/index.js
CHANGED
|
@@ -3,8 +3,14 @@ import { PURCHASE_TYPE, ROUTES, DEFAULT_PORT, ONESUB_ERROR_CODE, SUBSCRIPTION_ST
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { decodeJwt, decodeProtectedHeader, importX509, jwtVerify, importPKCS8, SignJWT, createRemoteJWKSet } from 'jose';
|
|
5
5
|
import { X509Certificate, randomUUID, createSign, createHash } from 'crypto';
|
|
6
|
+
import { createRequire } from 'module';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
9
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
10
|
+
}) : x)(function(x) {
|
|
11
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
12
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
13
|
+
});
|
|
8
14
|
|
|
9
15
|
// src/store.ts
|
|
10
16
|
var InMemorySubscriptionStore = class {
|
|
@@ -14,6 +20,14 @@ var InMemorySubscriptionStore = class {
|
|
|
14
20
|
byUserId = /* @__PURE__ */ new Map();
|
|
15
21
|
byTransactionId = /* @__PURE__ */ new Map();
|
|
16
22
|
async save(sub) {
|
|
23
|
+
const prev = this.byTransactionId.get(sub.originalTransactionId);
|
|
24
|
+
if (prev && prev.userId !== sub.userId) {
|
|
25
|
+
const oldList = (this.byUserId.get(prev.userId) ?? []).filter(
|
|
26
|
+
(s) => s.originalTransactionId !== sub.originalTransactionId
|
|
27
|
+
);
|
|
28
|
+
if (oldList.length) this.byUserId.set(prev.userId, oldList);
|
|
29
|
+
else this.byUserId.delete(prev.userId);
|
|
30
|
+
}
|
|
17
31
|
this.byTransactionId.set(sub.originalTransactionId, sub);
|
|
18
32
|
const existing = this.byUserId.get(sub.userId) ?? [];
|
|
19
33
|
const filtered = existing.filter((s) => s.originalTransactionId !== sub.originalTransactionId);
|
|
@@ -174,8 +188,10 @@ async function fetchWithTimeout(input, init, timeoutMs = DEFAULT_FETCH_TIMEOUT_M
|
|
|
174
188
|
}
|
|
175
189
|
|
|
176
190
|
// src/cache.ts
|
|
191
|
+
var SWEEP_EVERY_N_SETS = 256;
|
|
177
192
|
var InMemoryCacheAdapter = class {
|
|
178
193
|
entries = /* @__PURE__ */ new Map();
|
|
194
|
+
setsSinceSweep = 0;
|
|
179
195
|
async get(key) {
|
|
180
196
|
const entry = this.entries.get(key);
|
|
181
197
|
if (!entry) return null;
|
|
@@ -188,13 +204,29 @@ var InMemoryCacheAdapter = class {
|
|
|
188
204
|
async set(key, value, ttlSeconds) {
|
|
189
205
|
const expiresAt = ttlSeconds && ttlSeconds > 0 ? Date.now() + ttlSeconds * 1e3 : 0;
|
|
190
206
|
this.entries.set(key, { value, expiresAt });
|
|
207
|
+
if (++this.setsSinceSweep >= SWEEP_EVERY_N_SETS) {
|
|
208
|
+
this.sweepExpired();
|
|
209
|
+
}
|
|
191
210
|
}
|
|
192
211
|
async del(key) {
|
|
193
212
|
this.entries.delete(key);
|
|
194
213
|
}
|
|
214
|
+
/** Evict every entry whose TTL has elapsed. */
|
|
215
|
+
sweepExpired() {
|
|
216
|
+
this.setsSinceSweep = 0;
|
|
217
|
+
const now = Date.now();
|
|
218
|
+
for (const [key, entry] of this.entries) {
|
|
219
|
+
if (entry.expiresAt !== 0 && entry.expiresAt < now) this.entries.delete(key);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
195
222
|
/** Test helper — clear everything. Not part of the public CacheAdapter contract. */
|
|
196
223
|
clear() {
|
|
197
224
|
this.entries.clear();
|
|
225
|
+
this.setsSinceSweep = 0;
|
|
226
|
+
}
|
|
227
|
+
/** Test helper — number of entries currently held (including not-yet-swept expired ones). */
|
|
228
|
+
get size() {
|
|
229
|
+
return this.entries.size;
|
|
198
230
|
}
|
|
199
231
|
};
|
|
200
232
|
var defaultAdapter = new InMemoryCacheAdapter();
|
|
@@ -233,6 +265,7 @@ function mockValidateAppleSubscription(receipt) {
|
|
|
233
265
|
if (!outcomePasses(outcome, "apple")) return null;
|
|
234
266
|
const now = Date.now();
|
|
235
267
|
const expiresAt = outcome.kind === "sandbox" ? now + 1 * HOURS : now + 30 * DAYS;
|
|
268
|
+
const tokenMatch = receipt.match(/#token=([^#]+)/);
|
|
236
269
|
return {
|
|
237
270
|
userId: "",
|
|
238
271
|
productId: "mock_subscription",
|
|
@@ -241,17 +274,20 @@ function mockValidateAppleSubscription(receipt) {
|
|
|
241
274
|
expiresAt: new Date(expiresAt).toISOString(),
|
|
242
275
|
originalTransactionId: deterministicTransactionId("mock_apple_orig", receipt),
|
|
243
276
|
purchasedAt: new Date(now).toISOString(),
|
|
244
|
-
willRenew: true
|
|
277
|
+
willRenew: true,
|
|
278
|
+
...tokenMatch ? { boundAccountId: tokenMatch[1] } : {}
|
|
245
279
|
};
|
|
246
280
|
}
|
|
247
281
|
function mockValidateAppleProduct(receipt, expectedProductId) {
|
|
248
282
|
const outcome = classifyMockReceipt(receipt);
|
|
249
283
|
if (!outcomePasses(outcome, "apple")) return null;
|
|
250
284
|
const productId = expectedProductId ?? "mock_product";
|
|
285
|
+
const tokenMatch = receipt.match(/#token=([^#]+)/);
|
|
251
286
|
return {
|
|
252
287
|
transactionId: deterministicTransactionId(`mock_apple_${productId}`, receipt),
|
|
253
288
|
productId,
|
|
254
|
-
purchasedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
289
|
+
purchasedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
290
|
+
...tokenMatch ? { appAccountToken: tokenMatch[1] } : {}
|
|
255
291
|
};
|
|
256
292
|
}
|
|
257
293
|
function mockValidateGoogleSubscription(receipt, productId) {
|
|
@@ -259,23 +295,29 @@ function mockValidateGoogleSubscription(receipt, productId) {
|
|
|
259
295
|
if (!outcomePasses(outcome, "google")) return null;
|
|
260
296
|
const now = Date.now();
|
|
261
297
|
const expiresAt = outcome.kind === "sandbox" ? now + 1 * HOURS : now + 30 * DAYS;
|
|
298
|
+
const tokenMatch = receipt.match(/#token=([^#]+)/);
|
|
262
299
|
return {
|
|
263
300
|
userId: "",
|
|
264
301
|
productId,
|
|
265
302
|
platform: "google",
|
|
266
303
|
status: SUBSCRIPTION_STATUS.ACTIVE,
|
|
267
304
|
expiresAt: new Date(expiresAt).toISOString(),
|
|
268
|
-
|
|
305
|
+
// Same keying as production: Google records are keyed by the purchaseToken
|
|
306
|
+
// (the receipt itself) so mock webhooks can find them by token.
|
|
307
|
+
originalTransactionId: receipt,
|
|
269
308
|
purchasedAt: new Date(now).toISOString(),
|
|
270
|
-
willRenew: true
|
|
309
|
+
willRenew: true,
|
|
310
|
+
...tokenMatch ? { boundAccountId: tokenMatch[1] } : {}
|
|
271
311
|
};
|
|
272
312
|
}
|
|
273
313
|
function mockValidateGoogleProduct(receipt, productId) {
|
|
274
314
|
const outcome = classifyMockReceipt(receipt);
|
|
275
315
|
if (!outcomePasses(outcome, "google")) return null;
|
|
316
|
+
const tokenMatch = receipt.match(/#token=([^#]+)/);
|
|
276
317
|
return {
|
|
277
318
|
transactionId: deterministicTransactionId(`mock_google_${productId}`, receipt),
|
|
278
|
-
purchasedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
319
|
+
purchasedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
320
|
+
...tokenMatch ? { obfuscatedExternalAccountId: tokenMatch[1] } : {}
|
|
279
321
|
};
|
|
280
322
|
}
|
|
281
323
|
|
|
@@ -383,8 +425,9 @@ async function validateAppleReceipt(receipt, config) {
|
|
|
383
425
|
expiresAt: new Date(tx.expiresDate).toISOString(),
|
|
384
426
|
originalTransactionId: tx.originalTransactionId,
|
|
385
427
|
purchasedAt: new Date(purchasedAt).toISOString(),
|
|
386
|
-
willRenew: status === SUBSCRIPTION_STATUS.ACTIVE
|
|
428
|
+
willRenew: status === SUBSCRIPTION_STATUS.ACTIVE,
|
|
387
429
|
// refined by renewal info in webhook
|
|
430
|
+
...tx.appAccountToken ? { boundAccountId: tx.appAccountToken } : {}
|
|
388
431
|
};
|
|
389
432
|
}
|
|
390
433
|
async function validateAppleConsumableReceipt(signedTransaction, config, expectedProductId) {
|
|
@@ -439,7 +482,8 @@ async function validateAppleConsumableReceipt(signedTransaction, config, expecte
|
|
|
439
482
|
return {
|
|
440
483
|
transactionId,
|
|
441
484
|
productId: tx.productId,
|
|
442
|
-
purchasedAt: tx.purchaseDate ? new Date(tx.purchaseDate).toISOString() : (/* @__PURE__ */ new Date()).toISOString()
|
|
485
|
+
purchasedAt: tx.purchaseDate ? new Date(tx.purchaseDate).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
486
|
+
appAccountToken: tx.appAccountToken ?? void 0
|
|
443
487
|
};
|
|
444
488
|
}
|
|
445
489
|
async function decodeAppleNotification(payload, skipJwsVerification = false) {
|
|
@@ -943,6 +987,7 @@ async function validateGoogleReceipt(receipt, productId, config) {
|
|
|
943
987
|
const purchasedAt = purchase.startTime ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
944
988
|
const willRenew = lineItem.autoRenewingPlan?.autoRenewEnabled ?? false;
|
|
945
989
|
const autoResumeTime = status === SUBSCRIPTION_STATUS.PAUSED ? purchase.pausedStateContext?.autoResumeTime : void 0;
|
|
990
|
+
const boundAccountId = purchase.externalAccountIdentifiers?.obfuscatedExternalAccountId;
|
|
946
991
|
return {
|
|
947
992
|
userId: "",
|
|
948
993
|
// caller fills this in
|
|
@@ -950,13 +995,18 @@ async function validateGoogleReceipt(receipt, productId, config) {
|
|
|
950
995
|
platform: "google",
|
|
951
996
|
status,
|
|
952
997
|
expiresAt,
|
|
953
|
-
|
|
998
|
+
// Keyed by the purchaseToken, NOT latestOrderId: RTDN webhooks and
|
|
999
|
+
// linkedPurchaseToken chains only carry the token, so records keyed by
|
|
1000
|
+
// order id are invisible to the entire webhook lifecycle (refund/cancel/
|
|
1001
|
+
// renewal would never find them).
|
|
1002
|
+
originalTransactionId: receipt,
|
|
954
1003
|
purchasedAt,
|
|
955
1004
|
willRenew,
|
|
956
1005
|
// Surface the linked token so host apps (and our webhook userId-continuity
|
|
957
1006
|
// logic) can follow upgrade/downgrade chains.
|
|
958
1007
|
linkedPurchaseToken: purchase.linkedPurchaseToken,
|
|
959
|
-
autoResumeTime
|
|
1008
|
+
autoResumeTime,
|
|
1009
|
+
...boundAccountId ? { boundAccountId } : {}
|
|
960
1010
|
};
|
|
961
1011
|
}
|
|
962
1012
|
async function validateGoogleProductReceipt(purchaseToken, productId, config, type = "non_consumable") {
|
|
@@ -998,7 +1048,8 @@ async function validateGoogleProductReceipt(purchaseToken, productId, config, ty
|
|
|
998
1048
|
}
|
|
999
1049
|
return {
|
|
1000
1050
|
transactionId: purchase.orderId,
|
|
1001
|
-
purchasedAt: purchase.purchaseTimeMillis ? new Date(parseInt(purchase.purchaseTimeMillis, 10)).toISOString() : (/* @__PURE__ */ new Date()).toISOString()
|
|
1051
|
+
purchasedAt: purchase.purchaseTimeMillis ? new Date(parseInt(purchase.purchaseTimeMillis, 10)).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
1052
|
+
obfuscatedExternalAccountId: purchase.obfuscatedExternalAccountId
|
|
1002
1053
|
};
|
|
1003
1054
|
}
|
|
1004
1055
|
function decodeGoogleNotification(payload) {
|
|
@@ -1131,6 +1182,22 @@ function createValidateRouter(config, store) {
|
|
|
1131
1182
|
sendError(res, 422, ONESUB_ERROR_CODE.RECEIPT_VALIDATION_FAILED, "Receipt validation failed", NO_SUB);
|
|
1132
1183
|
return;
|
|
1133
1184
|
}
|
|
1185
|
+
const boundAccountId = sub.boundAccountId;
|
|
1186
|
+
delete sub.boundAccountId;
|
|
1187
|
+
const bindingMismatch = platform === "apple" ? boundAccountId && boundAccountId.toLowerCase() !== userId.toLowerCase() : boundAccountId && boundAccountId !== userId;
|
|
1188
|
+
if (bindingMismatch) {
|
|
1189
|
+
log.warn(
|
|
1190
|
+
`[onesub/validate] account binding mismatch for ${sub.originalTransactionId}: receipt token does not match userId ${userId}`
|
|
1191
|
+
);
|
|
1192
|
+
sendError(
|
|
1193
|
+
res,
|
|
1194
|
+
409,
|
|
1195
|
+
ONESUB_ERROR_CODE.TRANSACTION_BELONGS_TO_OTHER_USER,
|
|
1196
|
+
"TRANSACTION_BELONGS_TO_OTHER_USER",
|
|
1197
|
+
NO_SUB
|
|
1198
|
+
);
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1134
1201
|
sub.userId = userId;
|
|
1135
1202
|
await store.save(sub);
|
|
1136
1203
|
if (platform === "google" && config.google) {
|
|
@@ -1178,7 +1245,7 @@ function createStatusRouter(store) {
|
|
|
1178
1245
|
return router;
|
|
1179
1246
|
}
|
|
1180
1247
|
var APPLE_ACTIVE_TYPES = /* @__PURE__ */ new Set(["SUBSCRIBED", "DID_RENEW", "DID_RECOVER", "OFFER_REDEEMED"]);
|
|
1181
|
-
var APPLE_CANCELED_TYPES = /* @__PURE__ */ new Set(["REVOKE", "REFUND"
|
|
1248
|
+
var APPLE_CANCELED_TYPES = /* @__PURE__ */ new Set(["REVOKE", "REFUND"]);
|
|
1182
1249
|
var APPLE_EXPIRED_TYPES = /* @__PURE__ */ new Set(["EXPIRED"]);
|
|
1183
1250
|
function mapAppleNotificationStatus(notificationType, subtype) {
|
|
1184
1251
|
if (notificationType === "DID_FAIL_TO_RENEW") {
|
|
@@ -1235,6 +1302,11 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1235
1302
|
} = decoded;
|
|
1236
1303
|
const notificationType = payload.notificationType;
|
|
1237
1304
|
const subtype = payload.subtype;
|
|
1305
|
+
if (config.apple?.bundleId && bundleId && bundleId !== config.apple.bundleId) {
|
|
1306
|
+
log.warn("[onesub/webhook/apple] Bundle ID mismatch:", bundleId, "!==", config.apple.bundleId);
|
|
1307
|
+
sendError(res, 400, ONESUB_ERROR_CODE.BUNDLE_ID_MISMATCH, "Bundle ID mismatch");
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1238
1310
|
const mapped = mapAppleNotificationStatus(notificationType, subtype);
|
|
1239
1311
|
const finalStatus = mapped ?? status;
|
|
1240
1312
|
if (notificationType === "CONSUMPTION_REQUEST" && config.apple?.consumptionInfoProvider && transactionId && productId) {
|
|
@@ -1261,6 +1333,10 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1261
1333
|
}
|
|
1262
1334
|
const isOneTimePurchase = type === "Consumable" || type === "Non-Consumable";
|
|
1263
1335
|
const isRefundOrRevoke = APPLE_CANCELED_TYPES.has(notificationType);
|
|
1336
|
+
if (isOneTimePurchase && notificationType === "CONSUMPTION_REQUEST") {
|
|
1337
|
+
res.status(200).json({ received: true });
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1264
1340
|
try {
|
|
1265
1341
|
if (isOneTimePurchase && isRefundOrRevoke) {
|
|
1266
1342
|
const lookupId = transactionId ?? originalTransactionId;
|
|
@@ -1273,7 +1349,7 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1273
1349
|
}
|
|
1274
1350
|
const existing = await store.getByTransactionId(originalTransactionId);
|
|
1275
1351
|
if (existing) {
|
|
1276
|
-
const isSubscriptionRefund = isRefundOrRevoke && !isOneTimePurchase
|
|
1352
|
+
const isSubscriptionRefund = isRefundOrRevoke && !isOneTimePurchase;
|
|
1277
1353
|
const keepEntitlement = isSubscriptionRefund && config.refundPolicy === "until_expiry";
|
|
1278
1354
|
const correctedUserId = appAccountToken && existing.userId === originalTransactionId ? appAccountToken : existing.userId;
|
|
1279
1355
|
if (correctedUserId !== existing.userId) {
|
|
@@ -1313,6 +1389,12 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1313
1389
|
res.status(200).json({ received: true });
|
|
1314
1390
|
} catch (err) {
|
|
1315
1391
|
log.error("[onesub/webhook/apple] Store update error:", err);
|
|
1392
|
+
if (webhookEventStore?.unmark && typeof payload.notificationUUID === "string") {
|
|
1393
|
+
try {
|
|
1394
|
+
await webhookEventStore.unmark("apple", payload.notificationUUID);
|
|
1395
|
+
} catch {
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1316
1398
|
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to update subscription");
|
|
1317
1399
|
}
|
|
1318
1400
|
}
|
|
@@ -1322,13 +1404,23 @@ function getGoogleJwks() {
|
|
|
1322
1404
|
if (!googleJwks) googleJwks = createRemoteJWKSet(new URL(GOOGLE_JWKS_URL));
|
|
1323
1405
|
return googleJwks;
|
|
1324
1406
|
}
|
|
1325
|
-
async function verifyGooglePushToken(req, expectedAudience) {
|
|
1407
|
+
async function verifyGooglePushToken(req, expectedAudience, expectedServiceAccountEmail) {
|
|
1326
1408
|
const authHeader = req.headers["authorization"];
|
|
1327
1409
|
if (typeof authHeader !== "string" || !authHeader.startsWith("Bearer ")) return false;
|
|
1328
1410
|
try {
|
|
1329
|
-
await jwtVerify(authHeader.slice("Bearer ".length).trim(), getGoogleJwks(), {
|
|
1330
|
-
audience: expectedAudience
|
|
1411
|
+
const { payload } = await jwtVerify(authHeader.slice("Bearer ".length).trim(), getGoogleJwks(), {
|
|
1412
|
+
audience: expectedAudience,
|
|
1413
|
+
// Any Google Cloud principal can mint an OIDC token with an arbitrary
|
|
1414
|
+
// audience — issuer (and, when configured, the push service-account
|
|
1415
|
+
// email) narrows acceptance to real Pub/Sub push auth tokens. Google
|
|
1416
|
+
// documents both issuer forms, so accept either.
|
|
1417
|
+
issuer: ["https://accounts.google.com", "accounts.google.com"]
|
|
1331
1418
|
});
|
|
1419
|
+
if (expectedServiceAccountEmail) {
|
|
1420
|
+
if (payload["email"] !== expectedServiceAccountEmail || payload["email_verified"] !== true) {
|
|
1421
|
+
return false;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1332
1424
|
return true;
|
|
1333
1425
|
} catch {
|
|
1334
1426
|
return false;
|
|
@@ -1336,7 +1428,11 @@ async function verifyGooglePushToken(req, expectedAudience) {
|
|
|
1336
1428
|
}
|
|
1337
1429
|
async function handleGoogleWebhook(req, res, config, store, purchaseStore, webhookEventStore) {
|
|
1338
1430
|
if (config.google?.pushAudience) {
|
|
1339
|
-
const authenticated = await verifyGooglePushToken(
|
|
1431
|
+
const authenticated = await verifyGooglePushToken(
|
|
1432
|
+
req,
|
|
1433
|
+
config.google.pushAudience,
|
|
1434
|
+
config.google.pushServiceAccountEmail
|
|
1435
|
+
);
|
|
1340
1436
|
if (!authenticated) {
|
|
1341
1437
|
sendError(res, 401, ONESUB_ERROR_CODE.UNAUTHORIZED, "Unauthorized");
|
|
1342
1438
|
return;
|
|
@@ -1347,6 +1443,14 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1347
1443
|
sendError(res, 400, ONESUB_ERROR_CODE.MISSING_MESSAGE_DATA, "Missing message.data");
|
|
1348
1444
|
return;
|
|
1349
1445
|
}
|
|
1446
|
+
const unmarkEvent = async () => {
|
|
1447
|
+
if (webhookEventStore?.unmark && typeof body.message?.messageId === "string") {
|
|
1448
|
+
try {
|
|
1449
|
+
await webhookEventStore.unmark("google", body.message.messageId);
|
|
1450
|
+
} catch {
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1350
1454
|
if (webhookEventStore && typeof body.message.messageId === "string") {
|
|
1351
1455
|
const fresh = await webhookEventStore.markIfNew("google", body.message.messageId);
|
|
1352
1456
|
if (!fresh) {
|
|
@@ -1380,6 +1484,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1380
1484
|
res.status(200).json({ received: true });
|
|
1381
1485
|
} catch (err) {
|
|
1382
1486
|
log.error("[onesub/webhook/google] voided notification error:", err);
|
|
1487
|
+
await unmarkEvent();
|
|
1383
1488
|
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process voided notification");
|
|
1384
1489
|
}
|
|
1385
1490
|
return;
|
|
@@ -1406,6 +1511,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1406
1511
|
res.status(200).json({ received: true });
|
|
1407
1512
|
} catch (err) {
|
|
1408
1513
|
log.error("[onesub/webhook/google] oneTimeProduct error:", err);
|
|
1514
|
+
await unmarkEvent();
|
|
1409
1515
|
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process oneTimeProduct notification");
|
|
1410
1516
|
}
|
|
1411
1517
|
return;
|
|
@@ -1439,7 +1545,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1439
1545
|
} else if (isGoogleExpiredNotification(notificationType)) {
|
|
1440
1546
|
finalStatus = SUBSCRIPTION_STATUS.EXPIRED;
|
|
1441
1547
|
} else {
|
|
1442
|
-
finalStatus = SUBSCRIPTION_STATUS.ACTIVE;
|
|
1548
|
+
finalStatus = existing?.status ?? SUBSCRIPTION_STATUS.ACTIVE;
|
|
1443
1549
|
}
|
|
1444
1550
|
if (isGooglePriceChangeConfirmedNotification(notificationType) && config.google?.onPriceChangeConfirmed) {
|
|
1445
1551
|
const hook = config.google.onPriceChangeConfirmed;
|
|
@@ -1450,7 +1556,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1450
1556
|
if (config.google?.serviceAccountKey) {
|
|
1451
1557
|
const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, config.google);
|
|
1452
1558
|
if (fresh) {
|
|
1453
|
-
const preserveNotificationStatus =
|
|
1559
|
+
const preserveNotificationStatus = isGoogleGracePeriodNotification(notificationType) || isGoogleOnHoldNotification(notificationType);
|
|
1454
1560
|
updated = {
|
|
1455
1561
|
...existing,
|
|
1456
1562
|
status: preserveNotificationStatus ? finalStatus : fresh.status,
|
|
@@ -1466,14 +1572,16 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1466
1572
|
if (config.google?.serviceAccountKey) {
|
|
1467
1573
|
const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, config.google);
|
|
1468
1574
|
if (fresh) {
|
|
1575
|
+
const boundAccountId = fresh.boundAccountId;
|
|
1576
|
+
delete fresh.boundAccountId;
|
|
1469
1577
|
if (fresh.linkedPurchaseToken) {
|
|
1470
1578
|
const previous = await store.getByTransactionId(fresh.linkedPurchaseToken);
|
|
1471
|
-
fresh.userId = previous ? previous.userId : purchaseToken;
|
|
1579
|
+
fresh.userId = previous ? previous.userId : boundAccountId ?? purchaseToken;
|
|
1472
1580
|
if (previous) {
|
|
1473
1581
|
log.info(`[onesub/webhook/google] inherited userId ${previous.userId} from linkedPurchaseToken ${fresh.linkedPurchaseToken} \u2192 new token ${purchaseToken}`);
|
|
1474
1582
|
}
|
|
1475
1583
|
} else {
|
|
1476
|
-
fresh.userId = purchaseToken;
|
|
1584
|
+
fresh.userId = boundAccountId ?? purchaseToken;
|
|
1477
1585
|
}
|
|
1478
1586
|
await store.save(fresh);
|
|
1479
1587
|
}
|
|
@@ -1484,6 +1592,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1484
1592
|
res.status(200).json({ received: true });
|
|
1485
1593
|
} catch (err) {
|
|
1486
1594
|
log.error("[onesub/webhook/google] Error handling notification:", err);
|
|
1595
|
+
await unmarkEvent();
|
|
1487
1596
|
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process notification");
|
|
1488
1597
|
}
|
|
1489
1598
|
}
|
|
@@ -1544,6 +1653,7 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
1544
1653
|
}
|
|
1545
1654
|
let transactionId = null;
|
|
1546
1655
|
let purchasedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1656
|
+
let boundAccountId = null;
|
|
1547
1657
|
if (platform === "apple") {
|
|
1548
1658
|
if (!config.apple) {
|
|
1549
1659
|
sendError(res, 500, ONESUB_ERROR_CODE.APPLE_CONFIG_MISSING, "Apple configuration not provided", NO_PURCHASE);
|
|
@@ -1553,6 +1663,7 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
1553
1663
|
if (result) {
|
|
1554
1664
|
transactionId = result.transactionId;
|
|
1555
1665
|
purchasedAt = result.purchasedAt;
|
|
1666
|
+
boundAccountId = result.appAccountToken ?? null;
|
|
1556
1667
|
}
|
|
1557
1668
|
} else {
|
|
1558
1669
|
if (!config.google) {
|
|
@@ -1568,12 +1679,27 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
1568
1679
|
if (result) {
|
|
1569
1680
|
transactionId = result.transactionId;
|
|
1570
1681
|
purchasedAt = result.purchasedAt;
|
|
1682
|
+
boundAccountId = result.obfuscatedExternalAccountId ?? null;
|
|
1571
1683
|
}
|
|
1572
1684
|
}
|
|
1573
1685
|
if (!transactionId) {
|
|
1574
1686
|
sendError(res, 422, ONESUB_ERROR_CODE.RECEIPT_VALIDATION_FAILED, "Receipt validation failed", NO_PURCHASE);
|
|
1575
1687
|
return;
|
|
1576
1688
|
}
|
|
1689
|
+
const bindingMismatch = platform === "apple" ? boundAccountId && boundAccountId.toLowerCase() !== userId.toLowerCase() : boundAccountId && boundAccountId !== userId;
|
|
1690
|
+
if (bindingMismatch) {
|
|
1691
|
+
log.warn(
|
|
1692
|
+
`[onesub/purchase] account binding mismatch for transaction ${transactionId}: token does not match userId ${userId}`
|
|
1693
|
+
);
|
|
1694
|
+
sendError(
|
|
1695
|
+
res,
|
|
1696
|
+
409,
|
|
1697
|
+
ONESUB_ERROR_CODE.TRANSACTION_BELONGS_TO_OTHER_USER,
|
|
1698
|
+
"TRANSACTION_BELONGS_TO_OTHER_USER",
|
|
1699
|
+
NO_PURCHASE
|
|
1700
|
+
);
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1577
1703
|
const existing = await purchaseStore.getPurchaseByTransactionId(transactionId);
|
|
1578
1704
|
let action = "new";
|
|
1579
1705
|
if (existing) {
|
|
@@ -1795,9 +1921,12 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
1795
1921
|
sendError(res, 404, ONESUB_ERROR_CODE.TRANSACTION_NOT_FOUND, "TRANSACTION_NOT_FOUND");
|
|
1796
1922
|
return;
|
|
1797
1923
|
}
|
|
1798
|
-
await purchaseStore.
|
|
1924
|
+
const moved = await purchaseStore.reassignPurchase(body.transactionId, body.newUserId);
|
|
1925
|
+
if (!moved) {
|
|
1926
|
+
sendError(res, 404, ONESUB_ERROR_CODE.TRANSACTION_NOT_FOUND, "TRANSACTION_NOT_FOUND");
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1799
1929
|
const migrated = { ...existing, userId: body.newUserId };
|
|
1800
|
-
await purchaseStore.savePurchase(migrated);
|
|
1801
1930
|
res.json({ ok: true, purchase: migrated });
|
|
1802
1931
|
});
|
|
1803
1932
|
const grantSchema = z.object({
|
|
@@ -2329,7 +2458,11 @@ var PostgresSubscriptionStore = class {
|
|
|
2329
2458
|
);
|
|
2330
2459
|
});
|
|
2331
2460
|
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
2332
|
-
|
|
2461
|
+
const pool = new Pool({ connectionString: this.connectionString, max: 10 });
|
|
2462
|
+
pool.on("error", (err) => {
|
|
2463
|
+
log.error("[onesub] PostgresSubscriptionStore pool error (idle client):", err);
|
|
2464
|
+
});
|
|
2465
|
+
return pool;
|
|
2333
2466
|
})();
|
|
2334
2467
|
}
|
|
2335
2468
|
return this.poolPromise;
|
|
@@ -2501,7 +2634,11 @@ var PostgresPurchaseStore = class {
|
|
|
2501
2634
|
);
|
|
2502
2635
|
});
|
|
2503
2636
|
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
2504
|
-
|
|
2637
|
+
const pool = new Pool({ connectionString: this.connectionString, max: 10 });
|
|
2638
|
+
pool.on("error", (err) => {
|
|
2639
|
+
log.error("[onesub] PostgresPurchaseStore pool error (idle client):", err);
|
|
2640
|
+
});
|
|
2641
|
+
return pool;
|
|
2505
2642
|
})();
|
|
2506
2643
|
}
|
|
2507
2644
|
return this.poolPromise;
|
|
@@ -2516,23 +2653,12 @@ var PostgresPurchaseStore = class {
|
|
|
2516
2653
|
}
|
|
2517
2654
|
async savePurchase(purchase) {
|
|
2518
2655
|
const pool = await this.getPool();
|
|
2519
|
-
const
|
|
2520
|
-
`SELECT user_id FROM onesub_purchases WHERE transaction_id = $1`,
|
|
2521
|
-
[purchase.transactionId]
|
|
2522
|
-
);
|
|
2523
|
-
if (existing.rows.length > 0) {
|
|
2524
|
-
const owner = existing.rows[0].user_id;
|
|
2525
|
-
if (owner !== purchase.userId) {
|
|
2526
|
-
const err = new Error("TRANSACTION_BELONGS_TO_OTHER_USER");
|
|
2527
|
-
err.code = "TRANSACTION_BELONGS_TO_OTHER_USER";
|
|
2528
|
-
throw err;
|
|
2529
|
-
}
|
|
2530
|
-
return;
|
|
2531
|
-
}
|
|
2532
|
-
await pool.query(
|
|
2656
|
+
const inserted = await pool.query(
|
|
2533
2657
|
`INSERT INTO onesub_purchases
|
|
2534
2658
|
(transaction_id, user_id, product_id, platform, type, quantity, purchased_at)
|
|
2535
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
2659
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
2660
|
+
ON CONFLICT (transaction_id) DO NOTHING
|
|
2661
|
+
RETURNING transaction_id`,
|
|
2536
2662
|
[
|
|
2537
2663
|
purchase.transactionId,
|
|
2538
2664
|
purchase.userId,
|
|
@@ -2543,6 +2669,17 @@ var PostgresPurchaseStore = class {
|
|
|
2543
2669
|
purchase.purchasedAt
|
|
2544
2670
|
]
|
|
2545
2671
|
);
|
|
2672
|
+
if (inserted.rows.length > 0) return;
|
|
2673
|
+
const existing = await pool.query(
|
|
2674
|
+
`SELECT user_id FROM onesub_purchases WHERE transaction_id = $1`,
|
|
2675
|
+
[purchase.transactionId]
|
|
2676
|
+
);
|
|
2677
|
+
const owner = existing.rows[0]?.user_id;
|
|
2678
|
+
if (owner !== void 0 && owner !== purchase.userId) {
|
|
2679
|
+
const err = new Error("TRANSACTION_BELONGS_TO_OTHER_USER");
|
|
2680
|
+
err.code = "TRANSACTION_BELONGS_TO_OTHER_USER";
|
|
2681
|
+
throw err;
|
|
2682
|
+
}
|
|
2546
2683
|
}
|
|
2547
2684
|
async getPurchasesByUserId(userId) {
|
|
2548
2685
|
const pool = await this.getPool();
|
|
@@ -2643,7 +2780,6 @@ function rowToPurchaseInfo(row) {
|
|
|
2643
2780
|
// src/stores/redis.ts
|
|
2644
2781
|
var SUB_TX_PREFIX = "onesub:sub:tx:";
|
|
2645
2782
|
var SUB_USER_PREFIX = "onesub:sub:user:";
|
|
2646
|
-
var SUB_ALL = "onesub:sub:all";
|
|
2647
2783
|
var SUB_ALL_SORTED = "onesub:sub:all:sorted";
|
|
2648
2784
|
var PUR_TX_PREFIX = "onesub:purchase:tx:";
|
|
2649
2785
|
var PUR_USER_PREFIX = "onesub:purchase:user:";
|
|
@@ -2658,10 +2794,14 @@ var RedisSubscriptionStore = class {
|
|
|
2658
2794
|
const score = Date.now();
|
|
2659
2795
|
const txKey = SUB_TX_PREFIX + sub.originalTransactionId;
|
|
2660
2796
|
const userKey = SUB_USER_PREFIX + sub.userId;
|
|
2797
|
+
const prevRaw = await this.redis.get(txKey);
|
|
2798
|
+
const prevUserId = prevRaw ? JSON.parse(prevRaw).userId : null;
|
|
2661
2799
|
const pipeline = this.redis.multi();
|
|
2662
2800
|
pipeline.set(txKey, JSON.stringify(sub));
|
|
2801
|
+
if (prevUserId !== null && prevUserId !== sub.userId) {
|
|
2802
|
+
pipeline.zrem(SUB_USER_PREFIX + prevUserId, sub.originalTransactionId);
|
|
2803
|
+
}
|
|
2663
2804
|
pipeline.zadd(userKey, score, sub.originalTransactionId);
|
|
2664
|
-
pipeline.sadd(SUB_ALL, sub.originalTransactionId);
|
|
2665
2805
|
pipeline.zadd(SUB_ALL_SORTED, score, sub.originalTransactionId);
|
|
2666
2806
|
await pipeline.exec();
|
|
2667
2807
|
}
|
|
@@ -2733,19 +2873,18 @@ var RedisPurchaseStore = class {
|
|
|
2733
2873
|
redis;
|
|
2734
2874
|
async savePurchase(purchase) {
|
|
2735
2875
|
const txKey = PUR_TX_PREFIX + purchase.transactionId;
|
|
2736
|
-
const
|
|
2737
|
-
if (
|
|
2738
|
-
const
|
|
2739
|
-
|
|
2876
|
+
const claimed = await this.redis.set(txKey, JSON.stringify(purchase), "NX");
|
|
2877
|
+
if (claimed !== "OK") {
|
|
2878
|
+
const existing = await this.redis.get(txKey);
|
|
2879
|
+
const owner = existing ? JSON.parse(existing).userId : null;
|
|
2880
|
+
if (owner !== null && owner !== purchase.userId) {
|
|
2740
2881
|
const err = new Error("TRANSACTION_BELONGS_TO_OTHER_USER");
|
|
2741
2882
|
err.code = "TRANSACTION_BELONGS_TO_OTHER_USER";
|
|
2742
2883
|
throw err;
|
|
2743
2884
|
}
|
|
2744
|
-
return;
|
|
2745
2885
|
}
|
|
2746
2886
|
const score = Date.parse(purchase.purchasedAt) || Date.now();
|
|
2747
2887
|
const pipeline = this.redis.multi();
|
|
2748
|
-
pipeline.set(txKey, JSON.stringify(purchase));
|
|
2749
2888
|
pipeline.zadd(PUR_USER_PREFIX + purchase.userId, score, purchase.transactionId);
|
|
2750
2889
|
pipeline.sadd(PUR_USER_PRODUCT_PREFIX + purchase.userId + ":" + purchase.productId, purchase.transactionId);
|
|
2751
2890
|
pipeline.sadd(PUR_ALL, purchase.transactionId);
|
|
@@ -2856,6 +2995,9 @@ var RedisWebhookEventStore = class {
|
|
|
2856
2995
|
const result = await this.redis.set(key, "1", "EX", this.ttlSeconds, "NX");
|
|
2857
2996
|
return result === "OK";
|
|
2858
2997
|
}
|
|
2998
|
+
async unmark(provider, eventId) {
|
|
2999
|
+
await this.redis.del(WEBHOOK_EVENT_PREFIX + provider + ":" + eventId);
|
|
3000
|
+
}
|
|
2859
3001
|
};
|
|
2860
3002
|
|
|
2861
3003
|
// src/webhook-events.ts
|
|
@@ -2873,6 +3015,9 @@ var InMemoryWebhookEventStore = class {
|
|
|
2873
3015
|
this.seen.set(key, Date.now() + this.ttlSeconds * 1e3);
|
|
2874
3016
|
return true;
|
|
2875
3017
|
}
|
|
3018
|
+
async unmark(provider, eventId) {
|
|
3019
|
+
this.seen.delete(`${provider}:${eventId}`);
|
|
3020
|
+
}
|
|
2876
3021
|
evictExpired() {
|
|
2877
3022
|
const now = Date.now();
|
|
2878
3023
|
for (const [key, expiresAt] of this.seen) {
|
|
@@ -2894,6 +3039,9 @@ var CacheWebhookEventStore = class {
|
|
|
2894
3039
|
await this.cache.set(key, "1", this.ttlSeconds);
|
|
2895
3040
|
return true;
|
|
2896
3041
|
}
|
|
3042
|
+
async unmark(provider, eventId) {
|
|
3043
|
+
await this.cache.del(`webhook:event:${provider}:${eventId}`);
|
|
3044
|
+
}
|
|
2897
3045
|
};
|
|
2898
3046
|
|
|
2899
3047
|
// src/webhook-queue.ts
|
|
@@ -3171,8 +3319,6 @@ function openapiHandler() {
|
|
|
3171
3319
|
res.json(ONESUB_OPENAPI);
|
|
3172
3320
|
};
|
|
3173
3321
|
}
|
|
3174
|
-
|
|
3175
|
-
// src/tracing.ts
|
|
3176
3322
|
var NOOP_SPAN = {
|
|
3177
3323
|
setAttribute: () => {
|
|
3178
3324
|
},
|
|
@@ -3189,8 +3335,7 @@ function getTracer() {
|
|
|
3189
3335
|
if (resolved) return cachedTracer;
|
|
3190
3336
|
resolved = true;
|
|
3191
3337
|
try {
|
|
3192
|
-
const req =
|
|
3193
|
-
if (!req) return null;
|
|
3338
|
+
const req = typeof __require === "function" ? __require : createRequire(import.meta.url);
|
|
3194
3339
|
const otel = req("@opentelemetry/api");
|
|
3195
3340
|
cachedTracer = otel.trace.getTracer("@onesub/server", "1.0.0");
|
|
3196
3341
|
return cachedTracer;
|