@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.
Files changed (141) hide show
  1. package/dist/__tests__/google-subscriptions-v2.test.d.ts +1 -1
  2. package/dist/__tests__/tracing.test.d.ts +2 -0
  3. package/dist/__tests__/tracing.test.d.ts.map +1 -0
  4. package/dist/cache.d.ts +5 -0
  5. package/dist/cache.d.ts.map +1 -1
  6. package/dist/index.cjs +197 -52
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.js +197 -52
  9. package/dist/index.js.map +1 -1
  10. package/dist/providers/apple.d.ts +8 -0
  11. package/dist/providers/apple.d.ts.map +1 -1
  12. package/dist/providers/google.d.ts +8 -0
  13. package/dist/providers/google.d.ts.map +1 -1
  14. package/dist/providers/mock.d.ts +2 -0
  15. package/dist/providers/mock.d.ts.map +1 -1
  16. package/dist/routes/admin.d.ts.map +1 -1
  17. package/dist/routes/purchase.d.ts.map +1 -1
  18. package/dist/routes/validate.d.ts.map +1 -1
  19. package/dist/routes/webhook-apple.d.ts.map +1 -1
  20. package/dist/routes/webhook-google.d.ts +1 -1
  21. package/dist/routes/webhook-google.d.ts.map +1 -1
  22. package/dist/store.d.ts.map +1 -1
  23. package/dist/stores/postgres.d.ts.map +1 -1
  24. package/dist/stores/redis.d.ts +1 -0
  25. package/dist/stores/redis.d.ts.map +1 -1
  26. package/dist/tracing.d.ts.map +1 -1
  27. package/dist/webhook-events.d.ts +10 -0
  28. package/dist/webhook-events.d.ts.map +1 -1
  29. package/package.json +2 -2
  30. package/dist/__tests__/admin-customer-detail.test.js +0 -146
  31. package/dist/__tests__/admin-customer-detail.test.js.map +0 -1
  32. package/dist/__tests__/admin-list-subscriptions.test.js +0 -192
  33. package/dist/__tests__/admin-list-subscriptions.test.js.map +0 -1
  34. package/dist/__tests__/admin-subscription-detail.test.js +0 -112
  35. package/dist/__tests__/admin-subscription-detail.test.js.map +0 -1
  36. package/dist/__tests__/apple-root-ca.test.js +0 -41
  37. package/dist/__tests__/apple-root-ca.test.js.map +0 -1
  38. package/dist/__tests__/apple-status-api.test.js +0 -345
  39. package/dist/__tests__/apple-status-api.test.js.map +0 -1
  40. package/dist/__tests__/cache.test.js +0 -34
  41. package/dist/__tests__/cache.test.js.map +0 -1
  42. package/dist/__tests__/entitlements.test.js +0 -268
  43. package/dist/__tests__/entitlements.test.js.map +0 -1
  44. package/dist/__tests__/error-codes.test.js +0 -173
  45. package/dist/__tests__/error-codes.test.js.map +0 -1
  46. package/dist/__tests__/fetch-hardening.test.js +0 -192
  47. package/dist/__tests__/fetch-hardening.test.js.map +0 -1
  48. package/dist/__tests__/google-paused.test.js +0 -126
  49. package/dist/__tests__/google-paused.test.js.map +0 -1
  50. package/dist/__tests__/google-price-change.test.js +0 -186
  51. package/dist/__tests__/google-price-change.test.js.map +0 -1
  52. package/dist/__tests__/google-subscriptions-v2.test.js +0 -206
  53. package/dist/__tests__/google-subscriptions-v2.test.js.map +0 -1
  54. package/dist/__tests__/lifecycle-scenarios.test.js +0 -579
  55. package/dist/__tests__/lifecycle-scenarios.test.js.map +0 -1
  56. package/dist/__tests__/lifecycle-states.test.js +0 -423
  57. package/dist/__tests__/lifecycle-states.test.js.map +0 -1
  58. package/dist/__tests__/linked-purchase-token.test.js +0 -227
  59. package/dist/__tests__/linked-purchase-token.test.js.map +0 -1
  60. package/dist/__tests__/metrics.test.js +0 -340
  61. package/dist/__tests__/metrics.test.js.map +0 -1
  62. package/dist/__tests__/mock-provider.test.js +0 -188
  63. package/dist/__tests__/mock-provider.test.js.map +0 -1
  64. package/dist/__tests__/openapi.test.js +0 -32
  65. package/dist/__tests__/openapi.test.js.map +0 -1
  66. package/dist/__tests__/paused-auto-resume.test.js +0 -162
  67. package/dist/__tests__/paused-auto-resume.test.js.map +0 -1
  68. package/dist/__tests__/providers.test.js +0 -321
  69. package/dist/__tests__/providers.test.js.map +0 -1
  70. package/dist/__tests__/redis-store.test.js +0 -166
  71. package/dist/__tests__/redis-store.test.js.map +0 -1
  72. package/dist/__tests__/refund-policy.test.js +0 -317
  73. package/dist/__tests__/refund-policy.test.js.map +0 -1
  74. package/dist/__tests__/schema.test.js +0 -46
  75. package/dist/__tests__/schema.test.js.map +0 -1
  76. package/dist/__tests__/store.test.js +0 -172
  77. package/dist/__tests__/store.test.js.map +0 -1
  78. package/dist/__tests__/test-utils.js +0 -30
  79. package/dist/__tests__/test-utils.js.map +0 -1
  80. package/dist/__tests__/webhook-apple.test.js +0 -247
  81. package/dist/__tests__/webhook-apple.test.js.map +0 -1
  82. package/dist/__tests__/webhook-events.test.js +0 -36
  83. package/dist/__tests__/webhook-events.test.js.map +0 -1
  84. package/dist/__tests__/webhook-google.test.js +0 -255
  85. package/dist/__tests__/webhook-google.test.js.map +0 -1
  86. package/dist/__tests__/webhook-queue.test.js +0 -26
  87. package/dist/__tests__/webhook-queue.test.js.map +0 -1
  88. package/dist/__tests__/webhook-refund.test.js +0 -313
  89. package/dist/__tests__/webhook-refund.test.js.map +0 -1
  90. package/dist/cache.js +0 -39
  91. package/dist/cache.js.map +0 -1
  92. package/dist/errors.js +0 -19
  93. package/dist/errors.js.map +0 -1
  94. package/dist/http.js +0 -36
  95. package/dist/http.js.map +0 -1
  96. package/dist/logger.js +0 -20
  97. package/dist/logger.js.map +0 -1
  98. package/dist/openapi.js +0 -196
  99. package/dist/openapi.js.map +0 -1
  100. package/dist/providers/apple-root-ca.js +0 -38
  101. package/dist/providers/apple-root-ca.js.map +0 -1
  102. package/dist/providers/apple.js +0 -630
  103. package/dist/providers/apple.js.map +0 -1
  104. package/dist/providers/google.js +0 -564
  105. package/dist/providers/google.js.map +0 -1
  106. package/dist/providers/mock.js +0 -93
  107. package/dist/providers/mock.js.map +0 -1
  108. package/dist/routes/admin.js +0 -327
  109. package/dist/routes/admin.js.map +0 -1
  110. package/dist/routes/apple-offer.js +0 -72
  111. package/dist/routes/apple-offer.js.map +0 -1
  112. package/dist/routes/entitlements.js +0 -130
  113. package/dist/routes/entitlements.js.map +0 -1
  114. package/dist/routes/metrics.js +0 -281
  115. package/dist/routes/metrics.js.map +0 -1
  116. package/dist/routes/purchase.js +0 -204
  117. package/dist/routes/purchase.js.map +0 -1
  118. package/dist/routes/status.js +0 -55
  119. package/dist/routes/status.js.map +0 -1
  120. package/dist/routes/validate.js +0 -70
  121. package/dist/routes/validate.js.map +0 -1
  122. package/dist/routes/webhook-apple.js +0 -145
  123. package/dist/routes/webhook-apple.js.map +0 -1
  124. package/dist/routes/webhook-google.js +0 -204
  125. package/dist/routes/webhook-google.js.map +0 -1
  126. package/dist/routes/webhook.js +0 -19
  127. package/dist/routes/webhook.js.map +0 -1
  128. package/dist/store.js +0 -145
  129. package/dist/store.js.map +0 -1
  130. package/dist/stores/postgres.js +0 -329
  131. package/dist/stores/postgres.js.map +0 -1
  132. package/dist/stores/redis.js +0 -297
  133. package/dist/stores/redis.js.map +0 -1
  134. package/dist/stores/schema.js +0 -67
  135. package/dist/stores/schema.js.map +0 -1
  136. package/dist/tracing.js +0 -74
  137. package/dist/tracing.js.map +0 -1
  138. package/dist/webhook-events.js +0 -58
  139. package/dist/webhook-events.js.map +0 -1
  140. package/dist/webhook-queue.js +0 -108
  141. 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
- * - latestOrderId → originalTransactionId
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=tracing.test.d.ts.map
@@ -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;
@@ -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;AAQD;;GAEG;AACH,qBAAa,oBAAqB,YAAW,YAAY;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IAE9C,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;IAKjE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrC,oFAAoF;IACpF,KAAK,IAAI,IAAI;CAGd;AAQD,wBAAgB,eAAe,IAAI,YAAY,CAE9C;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAE3D"}
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
- // src/index.ts
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
- originalTransactionId: deterministicTransactionId("mock_google_orig", receipt),
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
- originalTransactionId: purchase.latestOrderId ?? receipt.slice(0, 64),
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", "CONSUMPTION_REQUEST"]);
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 && notificationType !== "CONSUMPTION_REQUEST";
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(req, config.google.pushAudience);
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 = finalStatus === shared.SUBSCRIPTION_STATUS.GRACE_PERIOD || finalStatus === shared.SUBSCRIPTION_STATUS.ON_HOLD;
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.deletePurchases(existing.userId, existing.productId);
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
- return new Pool({ connectionString: this.connectionString, max: 10 });
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
- return new Pool({ connectionString: this.connectionString, max: 10 });
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 existing = await pool.query(
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 existing = await this.redis.get(txKey);
2744
- if (existing) {
2745
- const owner = JSON.parse(existing).userId;
2746
- if (owner !== purchase.userId) {
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 = globalThis.require;
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;