@onesub/server 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/dist/__tests__/google-subscriptions-v2.test.d.ts +1 -1
  2. package/dist/__tests__/receipt-age.test.d.ts +7 -0
  3. package/dist/__tests__/receipt-age.test.d.ts.map +1 -0
  4. package/dist/__tests__/tracing.test.d.ts +2 -0
  5. package/dist/__tests__/tracing.test.d.ts.map +1 -0
  6. package/dist/cache.d.ts +5 -0
  7. package/dist/cache.d.ts.map +1 -1
  8. package/dist/index.cjs +178 -55
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.js +178 -55
  11. package/dist/index.js.map +1 -1
  12. package/dist/providers/apple.d.ts.map +1 -1
  13. package/dist/providers/google.d.ts.map +1 -1
  14. package/dist/providers/mock.d.ts.map +1 -1
  15. package/dist/routes/admin.d.ts.map +1 -1
  16. package/dist/routes/purchase.d.ts.map +1 -1
  17. package/dist/routes/validate.d.ts.map +1 -1
  18. package/dist/routes/webhook-apple.d.ts.map +1 -1
  19. package/dist/routes/webhook-google.d.ts +1 -1
  20. package/dist/routes/webhook-google.d.ts.map +1 -1
  21. package/dist/store.d.ts.map +1 -1
  22. package/dist/stores/postgres.d.ts.map +1 -1
  23. package/dist/stores/redis.d.ts +1 -0
  24. package/dist/stores/redis.d.ts.map +1 -1
  25. package/dist/tracing.d.ts.map +1 -1
  26. package/dist/webhook-events.d.ts +10 -0
  27. package/dist/webhook-events.d.ts.map +1 -1
  28. package/package.json +5 -3
  29. package/dist/__tests__/admin-customer-detail.test.js +0 -146
  30. package/dist/__tests__/admin-customer-detail.test.js.map +0 -1
  31. package/dist/__tests__/admin-list-subscriptions.test.js +0 -192
  32. package/dist/__tests__/admin-list-subscriptions.test.js.map +0 -1
  33. package/dist/__tests__/admin-subscription-detail.test.js +0 -112
  34. package/dist/__tests__/admin-subscription-detail.test.js.map +0 -1
  35. package/dist/__tests__/apple-root-ca.test.js +0 -41
  36. package/dist/__tests__/apple-root-ca.test.js.map +0 -1
  37. package/dist/__tests__/apple-status-api.test.js +0 -345
  38. package/dist/__tests__/apple-status-api.test.js.map +0 -1
  39. package/dist/__tests__/cache.test.js +0 -34
  40. package/dist/__tests__/cache.test.js.map +0 -1
  41. package/dist/__tests__/entitlements.test.js +0 -268
  42. package/dist/__tests__/entitlements.test.js.map +0 -1
  43. package/dist/__tests__/error-codes.test.js +0 -173
  44. package/dist/__tests__/error-codes.test.js.map +0 -1
  45. package/dist/__tests__/fetch-hardening.test.js +0 -192
  46. package/dist/__tests__/fetch-hardening.test.js.map +0 -1
  47. package/dist/__tests__/google-paused.test.js +0 -126
  48. package/dist/__tests__/google-paused.test.js.map +0 -1
  49. package/dist/__tests__/google-price-change.test.js +0 -186
  50. package/dist/__tests__/google-price-change.test.js.map +0 -1
  51. package/dist/__tests__/google-subscriptions-v2.test.js +0 -206
  52. package/dist/__tests__/google-subscriptions-v2.test.js.map +0 -1
  53. package/dist/__tests__/lifecycle-scenarios.test.js +0 -579
  54. package/dist/__tests__/lifecycle-scenarios.test.js.map +0 -1
  55. package/dist/__tests__/lifecycle-states.test.js +0 -423
  56. package/dist/__tests__/lifecycle-states.test.js.map +0 -1
  57. package/dist/__tests__/linked-purchase-token.test.js +0 -227
  58. package/dist/__tests__/linked-purchase-token.test.js.map +0 -1
  59. package/dist/__tests__/metrics.test.js +0 -340
  60. package/dist/__tests__/metrics.test.js.map +0 -1
  61. package/dist/__tests__/mock-provider.test.js +0 -188
  62. package/dist/__tests__/mock-provider.test.js.map +0 -1
  63. package/dist/__tests__/openapi.test.js +0 -32
  64. package/dist/__tests__/openapi.test.js.map +0 -1
  65. package/dist/__tests__/paused-auto-resume.test.js +0 -162
  66. package/dist/__tests__/paused-auto-resume.test.js.map +0 -1
  67. package/dist/__tests__/providers.test.js +0 -321
  68. package/dist/__tests__/providers.test.js.map +0 -1
  69. package/dist/__tests__/redis-store.test.js +0 -166
  70. package/dist/__tests__/redis-store.test.js.map +0 -1
  71. package/dist/__tests__/refund-policy.test.js +0 -317
  72. package/dist/__tests__/refund-policy.test.js.map +0 -1
  73. package/dist/__tests__/schema.test.js +0 -46
  74. package/dist/__tests__/schema.test.js.map +0 -1
  75. package/dist/__tests__/store.test.js +0 -172
  76. package/dist/__tests__/store.test.js.map +0 -1
  77. package/dist/__tests__/test-utils.js +0 -30
  78. package/dist/__tests__/test-utils.js.map +0 -1
  79. package/dist/__tests__/webhook-apple.test.js +0 -247
  80. package/dist/__tests__/webhook-apple.test.js.map +0 -1
  81. package/dist/__tests__/webhook-events.test.js +0 -36
  82. package/dist/__tests__/webhook-events.test.js.map +0 -1
  83. package/dist/__tests__/webhook-google.test.js +0 -255
  84. package/dist/__tests__/webhook-google.test.js.map +0 -1
  85. package/dist/__tests__/webhook-queue.test.js +0 -26
  86. package/dist/__tests__/webhook-queue.test.js.map +0 -1
  87. package/dist/__tests__/webhook-refund.test.js +0 -313
  88. package/dist/__tests__/webhook-refund.test.js.map +0 -1
  89. package/dist/cache.js +0 -39
  90. package/dist/cache.js.map +0 -1
  91. package/dist/errors.js +0 -19
  92. package/dist/errors.js.map +0 -1
  93. package/dist/http.js +0 -36
  94. package/dist/http.js.map +0 -1
  95. package/dist/logger.js +0 -20
  96. package/dist/logger.js.map +0 -1
  97. package/dist/openapi.js +0 -196
  98. package/dist/openapi.js.map +0 -1
  99. package/dist/providers/apple-root-ca.js +0 -38
  100. package/dist/providers/apple-root-ca.js.map +0 -1
  101. package/dist/providers/apple.js +0 -630
  102. package/dist/providers/apple.js.map +0 -1
  103. package/dist/providers/google.js +0 -564
  104. package/dist/providers/google.js.map +0 -1
  105. package/dist/providers/mock.js +0 -93
  106. package/dist/providers/mock.js.map +0 -1
  107. package/dist/routes/admin.js +0 -327
  108. package/dist/routes/admin.js.map +0 -1
  109. package/dist/routes/apple-offer.js +0 -72
  110. package/dist/routes/apple-offer.js.map +0 -1
  111. package/dist/routes/entitlements.js +0 -130
  112. package/dist/routes/entitlements.js.map +0 -1
  113. package/dist/routes/metrics.js +0 -281
  114. package/dist/routes/metrics.js.map +0 -1
  115. package/dist/routes/purchase.js +0 -204
  116. package/dist/routes/purchase.js.map +0 -1
  117. package/dist/routes/status.js +0 -55
  118. package/dist/routes/status.js.map +0 -1
  119. package/dist/routes/validate.js +0 -70
  120. package/dist/routes/validate.js.map +0 -1
  121. package/dist/routes/webhook-apple.js +0 -145
  122. package/dist/routes/webhook-apple.js.map +0 -1
  123. package/dist/routes/webhook-google.js +0 -204
  124. package/dist/routes/webhook-google.js.map +0 -1
  125. package/dist/routes/webhook.js +0 -19
  126. package/dist/routes/webhook.js.map +0 -1
  127. package/dist/store.js +0 -145
  128. package/dist/store.js.map +0 -1
  129. package/dist/stores/postgres.js +0 -329
  130. package/dist/stores/postgres.js.map +0 -1
  131. package/dist/stores/redis.js +0 -297
  132. package/dist/stores/redis.js.map +0 -1
  133. package/dist/stores/schema.js +0 -67
  134. package/dist/stores/schema.js.map +0 -1
  135. package/dist/tracing.js +0 -74
  136. package/dist/tracing.js.map +0 -1
  137. package/dist/webhook-events.js +0 -58
  138. package/dist/webhook-events.js.map +0 -1
  139. package/dist/webhook-queue.js +0 -108
  140. 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,7 @@
1
+ /**
2
+ * productReceiptMaxAgeHours — the one-time-purchase replay window is
3
+ * configurable (default 72h) so historical receipts can be validated on
4
+ * purpose (backend migrations, e2e against real store transactions).
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=receipt-age.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"receipt-age.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/receipt-age.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
@@ -0,0 +1,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,7 +281,8 @@ function mockValidateAppleSubscription(receipt) {
248
281
  expiresAt: new Date(expiresAt).toISOString(),
249
282
  originalTransactionId: deterministicTransactionId("mock_apple_orig", receipt),
250
283
  purchasedAt: new Date(now).toISOString(),
251
- willRenew: true
284
+ willRenew: true,
285
+ ...tokenMatch ? { boundAccountId: tokenMatch[1] } : {}
252
286
  };
253
287
  }
254
288
  function mockValidateAppleProduct(receipt, expectedProductId) {
@@ -268,15 +302,19 @@ function mockValidateGoogleSubscription(receipt, productId) {
268
302
  if (!outcomePasses(outcome, "google")) return null;
269
303
  const now = Date.now();
270
304
  const expiresAt = outcome.kind === "sandbox" ? now + 1 * HOURS : now + 30 * DAYS;
305
+ const tokenMatch = receipt.match(/#token=([^#]+)/);
271
306
  return {
272
307
  userId: "",
273
308
  productId,
274
309
  platform: "google",
275
310
  status: shared.SUBSCRIPTION_STATUS.ACTIVE,
276
311
  expiresAt: new Date(expiresAt).toISOString(),
277
- 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,
278
315
  purchasedAt: new Date(now).toISOString(),
279
- willRenew: true
316
+ willRenew: true,
317
+ ...tokenMatch ? { boundAccountId: tokenMatch[1] } : {}
280
318
  };
281
319
  }
282
320
  function mockValidateGoogleProduct(receipt, productId) {
@@ -291,7 +329,6 @@ function mockValidateGoogleProduct(receipt, productId) {
291
329
  }
292
330
 
293
331
  // src/providers/apple.ts
294
- var MAX_CONSUMABLE_RECEIPT_AGE_MS = 72 * 60 * 60 * 1e3;
295
332
  function derBase64ToPem(der) {
296
333
  return "-----BEGIN CERTIFICATE-----\n" + (der.match(/.{1,64}/g) ?? []).join("\n") + "\n-----END CERTIFICATE-----";
297
334
  }
@@ -394,8 +431,9 @@ async function validateAppleReceipt(receipt, config) {
394
431
  expiresAt: new Date(tx.expiresDate).toISOString(),
395
432
  originalTransactionId: tx.originalTransactionId,
396
433
  purchasedAt: new Date(purchasedAt).toISOString(),
397
- willRenew: status === shared.SUBSCRIPTION_STATUS.ACTIVE
434
+ willRenew: status === shared.SUBSCRIPTION_STATUS.ACTIVE,
398
435
  // refined by renewal info in webhook
436
+ ...tx.appAccountToken ? { boundAccountId: tx.appAccountToken } : {}
399
437
  };
400
438
  }
401
439
  async function validateAppleConsumableReceipt(signedTransaction, config, expectedProductId) {
@@ -438,8 +476,9 @@ async function validateAppleConsumableReceipt(signedTransaction, config, expecte
438
476
  log.warn("[onesub/apple] Purchase was revoked/refunded");
439
477
  return null;
440
478
  }
441
- if (tx.purchaseDate && Date.now() - tx.purchaseDate > MAX_CONSUMABLE_RECEIPT_AGE_MS) {
442
- log.warn("[onesub/apple] Consumable receipt too old (>72h)");
479
+ const maxAgeMs = (config.productReceiptMaxAgeHours ?? 72) * 60 * 60 * 1e3;
480
+ if (tx.purchaseDate && Date.now() - tx.purchaseDate > maxAgeMs) {
481
+ log.warn(`[onesub/apple] Consumable receipt too old (>${config.productReceiptMaxAgeHours ?? 72}h)`);
443
482
  return null;
444
483
  }
445
484
  const transactionId = tx.transactionId ?? tx.originalTransactionId;
@@ -728,7 +767,6 @@ var GOOGLE_NOTIFICATION_TYPE = {
728
767
  SUBSCRIPTION_REVOKED: 12,
729
768
  SUBSCRIPTION_EXPIRED: 13
730
769
  };
731
- var MAX_PRODUCT_RECEIPT_AGE_MS = 72 * 60 * 60 * 1e3;
732
770
  var refreshPromises = /* @__PURE__ */ new Map();
733
771
  function googleTokenCacheKey(serviceAccountKey) {
734
772
  let hash = 0;
@@ -955,6 +993,7 @@ async function validateGoogleReceipt(receipt, productId, config) {
955
993
  const purchasedAt = purchase.startTime ?? (/* @__PURE__ */ new Date()).toISOString();
956
994
  const willRenew = lineItem.autoRenewingPlan?.autoRenewEnabled ?? false;
957
995
  const autoResumeTime = status === shared.SUBSCRIPTION_STATUS.PAUSED ? purchase.pausedStateContext?.autoResumeTime : void 0;
996
+ const boundAccountId = purchase.externalAccountIdentifiers?.obfuscatedExternalAccountId;
958
997
  return {
959
998
  userId: "",
960
999
  // caller fills this in
@@ -962,13 +1001,18 @@ async function validateGoogleReceipt(receipt, productId, config) {
962
1001
  platform: "google",
963
1002
  status,
964
1003
  expiresAt,
965
- originalTransactionId: purchase.latestOrderId ?? receipt.slice(0, 64),
1004
+ // Keyed by the purchaseToken, NOT latestOrderId: RTDN webhooks and
1005
+ // linkedPurchaseToken chains only carry the token, so records keyed by
1006
+ // order id are invisible to the entire webhook lifecycle (refund/cancel/
1007
+ // renewal would never find them).
1008
+ originalTransactionId: receipt,
966
1009
  purchasedAt,
967
1010
  willRenew,
968
1011
  // Surface the linked token so host apps (and our webhook userId-continuity
969
1012
  // logic) can follow upgrade/downgrade chains.
970
1013
  linkedPurchaseToken: purchase.linkedPurchaseToken,
971
- autoResumeTime
1014
+ autoResumeTime,
1015
+ ...boundAccountId ? { boundAccountId } : {}
972
1016
  };
973
1017
  }
974
1018
  async function validateGoogleProductReceipt(purchaseToken, productId, config, type = "non_consumable") {
@@ -999,8 +1043,9 @@ async function validateGoogleProductReceipt(purchaseToken, productId, config, ty
999
1043
  }
1000
1044
  if (purchase.purchaseTimeMillis) {
1001
1045
  const purchaseTime = parseInt(purchase.purchaseTimeMillis, 10);
1002
- if (Date.now() - purchaseTime > MAX_PRODUCT_RECEIPT_AGE_MS) {
1003
- log.warn("[onesub/google] Product receipt too old (>72h)");
1046
+ const maxAgeMs = (config.productReceiptMaxAgeHours ?? 72) * 60 * 60 * 1e3;
1047
+ if (Date.now() - purchaseTime > maxAgeMs) {
1048
+ log.warn(`[onesub/google] Product receipt too old (>${config.productReceiptMaxAgeHours ?? 72}h)`);
1004
1049
  return null;
1005
1050
  }
1006
1051
  }
@@ -1144,6 +1189,22 @@ function createValidateRouter(config, store) {
1144
1189
  sendError(res, 422, shared.ONESUB_ERROR_CODE.RECEIPT_VALIDATION_FAILED, "Receipt validation failed", NO_SUB);
1145
1190
  return;
1146
1191
  }
1192
+ const boundAccountId = sub.boundAccountId;
1193
+ delete sub.boundAccountId;
1194
+ const bindingMismatch = platform === "apple" ? boundAccountId && boundAccountId.toLowerCase() !== userId.toLowerCase() : boundAccountId && boundAccountId !== userId;
1195
+ if (bindingMismatch) {
1196
+ log.warn(
1197
+ `[onesub/validate] account binding mismatch for ${sub.originalTransactionId}: receipt token does not match userId ${userId}`
1198
+ );
1199
+ sendError(
1200
+ res,
1201
+ 409,
1202
+ shared.ONESUB_ERROR_CODE.TRANSACTION_BELONGS_TO_OTHER_USER,
1203
+ "TRANSACTION_BELONGS_TO_OTHER_USER",
1204
+ NO_SUB
1205
+ );
1206
+ return;
1207
+ }
1147
1208
  sub.userId = userId;
1148
1209
  await store.save(sub);
1149
1210
  if (platform === "google" && config.google) {
@@ -1191,7 +1252,7 @@ function createStatusRouter(store) {
1191
1252
  return router;
1192
1253
  }
1193
1254
  var APPLE_ACTIVE_TYPES = /* @__PURE__ */ new Set(["SUBSCRIBED", "DID_RENEW", "DID_RECOVER", "OFFER_REDEEMED"]);
1194
- var APPLE_CANCELED_TYPES = /* @__PURE__ */ new Set(["REVOKE", "REFUND", "CONSUMPTION_REQUEST"]);
1255
+ var APPLE_CANCELED_TYPES = /* @__PURE__ */ new Set(["REVOKE", "REFUND"]);
1195
1256
  var APPLE_EXPIRED_TYPES = /* @__PURE__ */ new Set(["EXPIRED"]);
1196
1257
  function mapAppleNotificationStatus(notificationType, subtype) {
1197
1258
  if (notificationType === "DID_FAIL_TO_RENEW") {
@@ -1248,6 +1309,11 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
1248
1309
  } = decoded;
1249
1310
  const notificationType = payload.notificationType;
1250
1311
  const subtype = payload.subtype;
1312
+ if (config.apple?.bundleId && bundleId && bundleId !== config.apple.bundleId) {
1313
+ log.warn("[onesub/webhook/apple] Bundle ID mismatch:", bundleId, "!==", config.apple.bundleId);
1314
+ sendError(res, 400, shared.ONESUB_ERROR_CODE.BUNDLE_ID_MISMATCH, "Bundle ID mismatch");
1315
+ return;
1316
+ }
1251
1317
  const mapped = mapAppleNotificationStatus(notificationType, subtype);
1252
1318
  const finalStatus = mapped ?? status;
1253
1319
  if (notificationType === "CONSUMPTION_REQUEST" && config.apple?.consumptionInfoProvider && transactionId && productId) {
@@ -1274,6 +1340,10 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
1274
1340
  }
1275
1341
  const isOneTimePurchase = type === "Consumable" || type === "Non-Consumable";
1276
1342
  const isRefundOrRevoke = APPLE_CANCELED_TYPES.has(notificationType);
1343
+ if (isOneTimePurchase && notificationType === "CONSUMPTION_REQUEST") {
1344
+ res.status(200).json({ received: true });
1345
+ return;
1346
+ }
1277
1347
  try {
1278
1348
  if (isOneTimePurchase && isRefundOrRevoke) {
1279
1349
  const lookupId = transactionId ?? originalTransactionId;
@@ -1286,7 +1356,7 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
1286
1356
  }
1287
1357
  const existing = await store.getByTransactionId(originalTransactionId);
1288
1358
  if (existing) {
1289
- const isSubscriptionRefund = isRefundOrRevoke && !isOneTimePurchase && notificationType !== "CONSUMPTION_REQUEST";
1359
+ const isSubscriptionRefund = isRefundOrRevoke && !isOneTimePurchase;
1290
1360
  const keepEntitlement = isSubscriptionRefund && config.refundPolicy === "until_expiry";
1291
1361
  const correctedUserId = appAccountToken && existing.userId === originalTransactionId ? appAccountToken : existing.userId;
1292
1362
  if (correctedUserId !== existing.userId) {
@@ -1326,6 +1396,12 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
1326
1396
  res.status(200).json({ received: true });
1327
1397
  } catch (err) {
1328
1398
  log.error("[onesub/webhook/apple] Store update error:", err);
1399
+ if (webhookEventStore?.unmark && typeof payload.notificationUUID === "string") {
1400
+ try {
1401
+ await webhookEventStore.unmark("apple", payload.notificationUUID);
1402
+ } catch {
1403
+ }
1404
+ }
1329
1405
  sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to update subscription");
1330
1406
  }
1331
1407
  }
@@ -1335,13 +1411,23 @@ function getGoogleJwks() {
1335
1411
  if (!googleJwks) googleJwks = jose.createRemoteJWKSet(new URL(GOOGLE_JWKS_URL));
1336
1412
  return googleJwks;
1337
1413
  }
1338
- async function verifyGooglePushToken(req, expectedAudience) {
1414
+ async function verifyGooglePushToken(req, expectedAudience, expectedServiceAccountEmail) {
1339
1415
  const authHeader = req.headers["authorization"];
1340
1416
  if (typeof authHeader !== "string" || !authHeader.startsWith("Bearer ")) return false;
1341
1417
  try {
1342
- await jose.jwtVerify(authHeader.slice("Bearer ".length).trim(), getGoogleJwks(), {
1343
- audience: expectedAudience
1418
+ const { payload } = await jose.jwtVerify(authHeader.slice("Bearer ".length).trim(), getGoogleJwks(), {
1419
+ audience: expectedAudience,
1420
+ // Any Google Cloud principal can mint an OIDC token with an arbitrary
1421
+ // audience — issuer (and, when configured, the push service-account
1422
+ // email) narrows acceptance to real Pub/Sub push auth tokens. Google
1423
+ // documents both issuer forms, so accept either.
1424
+ issuer: ["https://accounts.google.com", "accounts.google.com"]
1344
1425
  });
1426
+ if (expectedServiceAccountEmail) {
1427
+ if (payload["email"] !== expectedServiceAccountEmail || payload["email_verified"] !== true) {
1428
+ return false;
1429
+ }
1430
+ }
1345
1431
  return true;
1346
1432
  } catch {
1347
1433
  return false;
@@ -1349,7 +1435,11 @@ async function verifyGooglePushToken(req, expectedAudience) {
1349
1435
  }
1350
1436
  async function handleGoogleWebhook(req, res, config, store, purchaseStore, webhookEventStore) {
1351
1437
  if (config.google?.pushAudience) {
1352
- const authenticated = await verifyGooglePushToken(req, config.google.pushAudience);
1438
+ const authenticated = await verifyGooglePushToken(
1439
+ req,
1440
+ config.google.pushAudience,
1441
+ config.google.pushServiceAccountEmail
1442
+ );
1353
1443
  if (!authenticated) {
1354
1444
  sendError(res, 401, shared.ONESUB_ERROR_CODE.UNAUTHORIZED, "Unauthorized");
1355
1445
  return;
@@ -1360,6 +1450,14 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
1360
1450
  sendError(res, 400, shared.ONESUB_ERROR_CODE.MISSING_MESSAGE_DATA, "Missing message.data");
1361
1451
  return;
1362
1452
  }
1453
+ const unmarkEvent = async () => {
1454
+ if (webhookEventStore?.unmark && typeof body.message?.messageId === "string") {
1455
+ try {
1456
+ await webhookEventStore.unmark("google", body.message.messageId);
1457
+ } catch {
1458
+ }
1459
+ }
1460
+ };
1363
1461
  if (webhookEventStore && typeof body.message.messageId === "string") {
1364
1462
  const fresh = await webhookEventStore.markIfNew("google", body.message.messageId);
1365
1463
  if (!fresh) {
@@ -1393,6 +1491,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
1393
1491
  res.status(200).json({ received: true });
1394
1492
  } catch (err) {
1395
1493
  log.error("[onesub/webhook/google] voided notification error:", err);
1494
+ await unmarkEvent();
1396
1495
  sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process voided notification");
1397
1496
  }
1398
1497
  return;
@@ -1419,6 +1518,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
1419
1518
  res.status(200).json({ received: true });
1420
1519
  } catch (err) {
1421
1520
  log.error("[onesub/webhook/google] oneTimeProduct error:", err);
1521
+ await unmarkEvent();
1422
1522
  sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process oneTimeProduct notification");
1423
1523
  }
1424
1524
  return;
@@ -1452,7 +1552,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
1452
1552
  } else if (isGoogleExpiredNotification(notificationType)) {
1453
1553
  finalStatus = shared.SUBSCRIPTION_STATUS.EXPIRED;
1454
1554
  } else {
1455
- finalStatus = shared.SUBSCRIPTION_STATUS.ACTIVE;
1555
+ finalStatus = existing?.status ?? shared.SUBSCRIPTION_STATUS.ACTIVE;
1456
1556
  }
1457
1557
  if (isGooglePriceChangeConfirmedNotification(notificationType) && config.google?.onPriceChangeConfirmed) {
1458
1558
  const hook = config.google.onPriceChangeConfirmed;
@@ -1463,7 +1563,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
1463
1563
  if (config.google?.serviceAccountKey) {
1464
1564
  const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, config.google);
1465
1565
  if (fresh) {
1466
- const preserveNotificationStatus = finalStatus === shared.SUBSCRIPTION_STATUS.GRACE_PERIOD || finalStatus === shared.SUBSCRIPTION_STATUS.ON_HOLD;
1566
+ const preserveNotificationStatus = isGoogleGracePeriodNotification(notificationType) || isGoogleOnHoldNotification(notificationType);
1467
1567
  updated = {
1468
1568
  ...existing,
1469
1569
  status: preserveNotificationStatus ? finalStatus : fresh.status,
@@ -1479,14 +1579,16 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
1479
1579
  if (config.google?.serviceAccountKey) {
1480
1580
  const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, config.google);
1481
1581
  if (fresh) {
1582
+ const boundAccountId = fresh.boundAccountId;
1583
+ delete fresh.boundAccountId;
1482
1584
  if (fresh.linkedPurchaseToken) {
1483
1585
  const previous = await store.getByTransactionId(fresh.linkedPurchaseToken);
1484
- fresh.userId = previous ? previous.userId : purchaseToken;
1586
+ fresh.userId = previous ? previous.userId : boundAccountId ?? purchaseToken;
1485
1587
  if (previous) {
1486
1588
  log.info(`[onesub/webhook/google] inherited userId ${previous.userId} from linkedPurchaseToken ${fresh.linkedPurchaseToken} \u2192 new token ${purchaseToken}`);
1487
1589
  }
1488
1590
  } else {
1489
- fresh.userId = purchaseToken;
1591
+ fresh.userId = boundAccountId ?? purchaseToken;
1490
1592
  }
1491
1593
  await store.save(fresh);
1492
1594
  }
@@ -1497,6 +1599,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
1497
1599
  res.status(200).json({ received: true });
1498
1600
  } catch (err) {
1499
1601
  log.error("[onesub/webhook/google] Error handling notification:", err);
1602
+ await unmarkEvent();
1500
1603
  sendError(res, 500, shared.ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process notification");
1501
1604
  }
1502
1605
  }
@@ -1590,7 +1693,8 @@ function createPurchaseRouter(config, purchaseStore) {
1590
1693
  sendError(res, 422, shared.ONESUB_ERROR_CODE.RECEIPT_VALIDATION_FAILED, "Receipt validation failed", NO_PURCHASE);
1591
1694
  return;
1592
1695
  }
1593
- if (boundAccountId && boundAccountId.toLowerCase() !== userId.toLowerCase()) {
1696
+ const bindingMismatch = platform === "apple" ? boundAccountId && boundAccountId.toLowerCase() !== userId.toLowerCase() : boundAccountId && boundAccountId !== userId;
1697
+ if (bindingMismatch) {
1594
1698
  log.warn(
1595
1699
  `[onesub/purchase] account binding mismatch for transaction ${transactionId}: token does not match userId ${userId}`
1596
1700
  );
@@ -1824,9 +1928,12 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
1824
1928
  sendError(res, 404, shared.ONESUB_ERROR_CODE.TRANSACTION_NOT_FOUND, "TRANSACTION_NOT_FOUND");
1825
1929
  return;
1826
1930
  }
1827
- await purchaseStore.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
+ }
1828
1936
  const migrated = { ...existing, userId: body.newUserId };
1829
- await purchaseStore.savePurchase(migrated);
1830
1937
  res.json({ ok: true, purchase: migrated });
1831
1938
  });
1832
1939
  const grantSchema = zod.z.object({
@@ -2358,7 +2465,11 @@ var PostgresSubscriptionStore = class {
2358
2465
  );
2359
2466
  });
2360
2467
  const Pool = pg.default?.Pool ?? pg.Pool;
2361
- 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;
2362
2473
  })();
2363
2474
  }
2364
2475
  return this.poolPromise;
@@ -2530,7 +2641,11 @@ var PostgresPurchaseStore = class {
2530
2641
  );
2531
2642
  });
2532
2643
  const Pool = pg.default?.Pool ?? pg.Pool;
2533
- 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;
2534
2649
  })();
2535
2650
  }
2536
2651
  return this.poolPromise;
@@ -2545,23 +2660,12 @@ var PostgresPurchaseStore = class {
2545
2660
  }
2546
2661
  async savePurchase(purchase) {
2547
2662
  const pool = await this.getPool();
2548
- const existing = await pool.query(
2549
- `SELECT user_id FROM onesub_purchases WHERE transaction_id = $1`,
2550
- [purchase.transactionId]
2551
- );
2552
- if (existing.rows.length > 0) {
2553
- const owner = existing.rows[0].user_id;
2554
- if (owner !== purchase.userId) {
2555
- const err = new Error("TRANSACTION_BELONGS_TO_OTHER_USER");
2556
- err.code = "TRANSACTION_BELONGS_TO_OTHER_USER";
2557
- throw err;
2558
- }
2559
- return;
2560
- }
2561
- await pool.query(
2663
+ const inserted = await pool.query(
2562
2664
  `INSERT INTO onesub_purchases
2563
2665
  (transaction_id, user_id, product_id, platform, type, quantity, purchased_at)
2564
- VALUES ($1, $2, $3, $4, $5, $6, $7)`,
2666
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
2667
+ ON CONFLICT (transaction_id) DO NOTHING
2668
+ RETURNING transaction_id`,
2565
2669
  [
2566
2670
  purchase.transactionId,
2567
2671
  purchase.userId,
@@ -2572,6 +2676,17 @@ var PostgresPurchaseStore = class {
2572
2676
  purchase.purchasedAt
2573
2677
  ]
2574
2678
  );
2679
+ if (inserted.rows.length > 0) return;
2680
+ const existing = await pool.query(
2681
+ `SELECT user_id FROM onesub_purchases WHERE transaction_id = $1`,
2682
+ [purchase.transactionId]
2683
+ );
2684
+ const owner = existing.rows[0]?.user_id;
2685
+ if (owner !== void 0 && owner !== purchase.userId) {
2686
+ const err = new Error("TRANSACTION_BELONGS_TO_OTHER_USER");
2687
+ err.code = "TRANSACTION_BELONGS_TO_OTHER_USER";
2688
+ throw err;
2689
+ }
2575
2690
  }
2576
2691
  async getPurchasesByUserId(userId) {
2577
2692
  const pool = await this.getPool();
@@ -2672,7 +2787,6 @@ function rowToPurchaseInfo(row) {
2672
2787
  // src/stores/redis.ts
2673
2788
  var SUB_TX_PREFIX = "onesub:sub:tx:";
2674
2789
  var SUB_USER_PREFIX = "onesub:sub:user:";
2675
- var SUB_ALL = "onesub:sub:all";
2676
2790
  var SUB_ALL_SORTED = "onesub:sub:all:sorted";
2677
2791
  var PUR_TX_PREFIX = "onesub:purchase:tx:";
2678
2792
  var PUR_USER_PREFIX = "onesub:purchase:user:";
@@ -2687,10 +2801,14 @@ var RedisSubscriptionStore = class {
2687
2801
  const score = Date.now();
2688
2802
  const txKey = SUB_TX_PREFIX + sub.originalTransactionId;
2689
2803
  const userKey = SUB_USER_PREFIX + sub.userId;
2804
+ const prevRaw = await this.redis.get(txKey);
2805
+ const prevUserId = prevRaw ? JSON.parse(prevRaw).userId : null;
2690
2806
  const pipeline = this.redis.multi();
2691
2807
  pipeline.set(txKey, JSON.stringify(sub));
2808
+ if (prevUserId !== null && prevUserId !== sub.userId) {
2809
+ pipeline.zrem(SUB_USER_PREFIX + prevUserId, sub.originalTransactionId);
2810
+ }
2692
2811
  pipeline.zadd(userKey, score, sub.originalTransactionId);
2693
- pipeline.sadd(SUB_ALL, sub.originalTransactionId);
2694
2812
  pipeline.zadd(SUB_ALL_SORTED, score, sub.originalTransactionId);
2695
2813
  await pipeline.exec();
2696
2814
  }
@@ -2762,19 +2880,18 @@ var RedisPurchaseStore = class {
2762
2880
  redis;
2763
2881
  async savePurchase(purchase) {
2764
2882
  const txKey = PUR_TX_PREFIX + purchase.transactionId;
2765
- const existing = await this.redis.get(txKey);
2766
- if (existing) {
2767
- const owner = JSON.parse(existing).userId;
2768
- 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) {
2769
2888
  const err = new Error("TRANSACTION_BELONGS_TO_OTHER_USER");
2770
2889
  err.code = "TRANSACTION_BELONGS_TO_OTHER_USER";
2771
2890
  throw err;
2772
2891
  }
2773
- return;
2774
2892
  }
2775
2893
  const score = Date.parse(purchase.purchasedAt) || Date.now();
2776
2894
  const pipeline = this.redis.multi();
2777
- pipeline.set(txKey, JSON.stringify(purchase));
2778
2895
  pipeline.zadd(PUR_USER_PREFIX + purchase.userId, score, purchase.transactionId);
2779
2896
  pipeline.sadd(PUR_USER_PRODUCT_PREFIX + purchase.userId + ":" + purchase.productId, purchase.transactionId);
2780
2897
  pipeline.sadd(PUR_ALL, purchase.transactionId);
@@ -2885,6 +3002,9 @@ var RedisWebhookEventStore = class {
2885
3002
  const result = await this.redis.set(key, "1", "EX", this.ttlSeconds, "NX");
2886
3003
  return result === "OK";
2887
3004
  }
3005
+ async unmark(provider, eventId) {
3006
+ await this.redis.del(WEBHOOK_EVENT_PREFIX + provider + ":" + eventId);
3007
+ }
2888
3008
  };
2889
3009
 
2890
3010
  // src/webhook-events.ts
@@ -2902,6 +3022,9 @@ var InMemoryWebhookEventStore = class {
2902
3022
  this.seen.set(key, Date.now() + this.ttlSeconds * 1e3);
2903
3023
  return true;
2904
3024
  }
3025
+ async unmark(provider, eventId) {
3026
+ this.seen.delete(`${provider}:${eventId}`);
3027
+ }
2905
3028
  evictExpired() {
2906
3029
  const now = Date.now();
2907
3030
  for (const [key, expiresAt] of this.seen) {
@@ -2923,6 +3046,9 @@ var CacheWebhookEventStore = class {
2923
3046
  await this.cache.set(key, "1", this.ttlSeconds);
2924
3047
  return true;
2925
3048
  }
3049
+ async unmark(provider, eventId) {
3050
+ await this.cache.del(`webhook:event:${provider}:${eventId}`);
3051
+ }
2926
3052
  };
2927
3053
 
2928
3054
  // src/webhook-queue.ts
@@ -3200,8 +3326,6 @@ function openapiHandler() {
3200
3326
  res.json(ONESUB_OPENAPI);
3201
3327
  };
3202
3328
  }
3203
-
3204
- // src/tracing.ts
3205
3329
  var NOOP_SPAN = {
3206
3330
  setAttribute: () => {
3207
3331
  },
@@ -3218,8 +3342,7 @@ function getTracer() {
3218
3342
  if (resolved) return cachedTracer;
3219
3343
  resolved = true;
3220
3344
  try {
3221
- const req = globalThis.require;
3222
- if (!req) return null;
3345
+ const req = typeof __require === "function" ? __require : module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
3223
3346
  const otel = req("@opentelemetry/api");
3224
3347
  cachedTracer = otel.trace.getTracer("@onesub/server", "1.0.0");
3225
3348
  return cachedTracer;