@onesub/server 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/google-subscriptions-v2.test.d.ts +1 -1
- package/dist/__tests__/receipt-age.test.d.ts +7 -0
- package/dist/__tests__/receipt-age.test.d.ts.map +1 -0
- package/dist/__tests__/tracing.test.d.ts +2 -0
- package/dist/__tests__/tracing.test.d.ts.map +1 -0
- package/dist/cache.d.ts +5 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/index.cjs +178 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +178 -55
- package/dist/index.js.map +1 -1
- package/dist/providers/apple.d.ts.map +1 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/mock.d.ts.map +1 -1
- package/dist/routes/admin.d.ts.map +1 -1
- package/dist/routes/purchase.d.ts.map +1 -1
- package/dist/routes/validate.d.ts.map +1 -1
- package/dist/routes/webhook-apple.d.ts.map +1 -1
- package/dist/routes/webhook-google.d.ts +1 -1
- package/dist/routes/webhook-google.d.ts.map +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/stores/postgres.d.ts.map +1 -1
- package/dist/stores/redis.d.ts +1 -0
- package/dist/stores/redis.d.ts.map +1 -1
- package/dist/tracing.d.ts.map +1 -1
- package/dist/webhook-events.d.ts +10 -0
- package/dist/webhook-events.d.ts.map +1 -1
- package/package.json +5 -3
- package/dist/__tests__/admin-customer-detail.test.js +0 -146
- package/dist/__tests__/admin-customer-detail.test.js.map +0 -1
- package/dist/__tests__/admin-list-subscriptions.test.js +0 -192
- package/dist/__tests__/admin-list-subscriptions.test.js.map +0 -1
- package/dist/__tests__/admin-subscription-detail.test.js +0 -112
- package/dist/__tests__/admin-subscription-detail.test.js.map +0 -1
- package/dist/__tests__/apple-root-ca.test.js +0 -41
- package/dist/__tests__/apple-root-ca.test.js.map +0 -1
- package/dist/__tests__/apple-status-api.test.js +0 -345
- package/dist/__tests__/apple-status-api.test.js.map +0 -1
- package/dist/__tests__/cache.test.js +0 -34
- package/dist/__tests__/cache.test.js.map +0 -1
- package/dist/__tests__/entitlements.test.js +0 -268
- package/dist/__tests__/entitlements.test.js.map +0 -1
- package/dist/__tests__/error-codes.test.js +0 -173
- package/dist/__tests__/error-codes.test.js.map +0 -1
- package/dist/__tests__/fetch-hardening.test.js +0 -192
- package/dist/__tests__/fetch-hardening.test.js.map +0 -1
- package/dist/__tests__/google-paused.test.js +0 -126
- package/dist/__tests__/google-paused.test.js.map +0 -1
- package/dist/__tests__/google-price-change.test.js +0 -186
- package/dist/__tests__/google-price-change.test.js.map +0 -1
- package/dist/__tests__/google-subscriptions-v2.test.js +0 -206
- package/dist/__tests__/google-subscriptions-v2.test.js.map +0 -1
- package/dist/__tests__/lifecycle-scenarios.test.js +0 -579
- package/dist/__tests__/lifecycle-scenarios.test.js.map +0 -1
- package/dist/__tests__/lifecycle-states.test.js +0 -423
- package/dist/__tests__/lifecycle-states.test.js.map +0 -1
- package/dist/__tests__/linked-purchase-token.test.js +0 -227
- package/dist/__tests__/linked-purchase-token.test.js.map +0 -1
- package/dist/__tests__/metrics.test.js +0 -340
- package/dist/__tests__/metrics.test.js.map +0 -1
- package/dist/__tests__/mock-provider.test.js +0 -188
- package/dist/__tests__/mock-provider.test.js.map +0 -1
- package/dist/__tests__/openapi.test.js +0 -32
- package/dist/__tests__/openapi.test.js.map +0 -1
- package/dist/__tests__/paused-auto-resume.test.js +0 -162
- package/dist/__tests__/paused-auto-resume.test.js.map +0 -1
- package/dist/__tests__/providers.test.js +0 -321
- package/dist/__tests__/providers.test.js.map +0 -1
- package/dist/__tests__/redis-store.test.js +0 -166
- package/dist/__tests__/redis-store.test.js.map +0 -1
- package/dist/__tests__/refund-policy.test.js +0 -317
- package/dist/__tests__/refund-policy.test.js.map +0 -1
- package/dist/__tests__/schema.test.js +0 -46
- package/dist/__tests__/schema.test.js.map +0 -1
- package/dist/__tests__/store.test.js +0 -172
- package/dist/__tests__/store.test.js.map +0 -1
- package/dist/__tests__/test-utils.js +0 -30
- package/dist/__tests__/test-utils.js.map +0 -1
- package/dist/__tests__/webhook-apple.test.js +0 -247
- package/dist/__tests__/webhook-apple.test.js.map +0 -1
- package/dist/__tests__/webhook-events.test.js +0 -36
- package/dist/__tests__/webhook-events.test.js.map +0 -1
- package/dist/__tests__/webhook-google.test.js +0 -255
- package/dist/__tests__/webhook-google.test.js.map +0 -1
- package/dist/__tests__/webhook-queue.test.js +0 -26
- package/dist/__tests__/webhook-queue.test.js.map +0 -1
- package/dist/__tests__/webhook-refund.test.js +0 -313
- package/dist/__tests__/webhook-refund.test.js.map +0 -1
- package/dist/cache.js +0 -39
- package/dist/cache.js.map +0 -1
- package/dist/errors.js +0 -19
- package/dist/errors.js.map +0 -1
- package/dist/http.js +0 -36
- package/dist/http.js.map +0 -1
- package/dist/logger.js +0 -20
- package/dist/logger.js.map +0 -1
- package/dist/openapi.js +0 -196
- package/dist/openapi.js.map +0 -1
- package/dist/providers/apple-root-ca.js +0 -38
- package/dist/providers/apple-root-ca.js.map +0 -1
- package/dist/providers/apple.js +0 -630
- package/dist/providers/apple.js.map +0 -1
- package/dist/providers/google.js +0 -564
- package/dist/providers/google.js.map +0 -1
- package/dist/providers/mock.js +0 -93
- package/dist/providers/mock.js.map +0 -1
- package/dist/routes/admin.js +0 -327
- package/dist/routes/admin.js.map +0 -1
- package/dist/routes/apple-offer.js +0 -72
- package/dist/routes/apple-offer.js.map +0 -1
- package/dist/routes/entitlements.js +0 -130
- package/dist/routes/entitlements.js.map +0 -1
- package/dist/routes/metrics.js +0 -281
- package/dist/routes/metrics.js.map +0 -1
- package/dist/routes/purchase.js +0 -204
- package/dist/routes/purchase.js.map +0 -1
- package/dist/routes/status.js +0 -55
- package/dist/routes/status.js.map +0 -1
- package/dist/routes/validate.js +0 -70
- package/dist/routes/validate.js.map +0 -1
- package/dist/routes/webhook-apple.js +0 -145
- package/dist/routes/webhook-apple.js.map +0 -1
- package/dist/routes/webhook-google.js +0 -204
- package/dist/routes/webhook-google.js.map +0 -1
- package/dist/routes/webhook.js +0 -19
- package/dist/routes/webhook.js.map +0 -1
- package/dist/store.js +0 -145
- package/dist/store.js.map +0 -1
- package/dist/stores/postgres.js +0 -329
- package/dist/stores/postgres.js.map +0 -1
- package/dist/stores/redis.js +0 -297
- package/dist/stores/redis.js.map +0 -1
- package/dist/stores/schema.js +0 -67
- package/dist/stores/schema.js.map +0 -1
- package/dist/tracing.js +0 -74
- package/dist/tracing.js.map +0 -1
- package/dist/webhook-events.js +0 -58
- package/dist/webhook-events.js.map +0 -1
- package/dist/webhook-queue.js +0 -108
- package/dist/webhook-queue.js.map +0 -1
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,7 +274,8 @@ 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) {
|
|
@@ -261,15 +295,19 @@ function mockValidateGoogleSubscription(receipt, productId) {
|
|
|
261
295
|
if (!outcomePasses(outcome, "google")) return null;
|
|
262
296
|
const now = Date.now();
|
|
263
297
|
const expiresAt = outcome.kind === "sandbox" ? now + 1 * HOURS : now + 30 * DAYS;
|
|
298
|
+
const tokenMatch = receipt.match(/#token=([^#]+)/);
|
|
264
299
|
return {
|
|
265
300
|
userId: "",
|
|
266
301
|
productId,
|
|
267
302
|
platform: "google",
|
|
268
303
|
status: SUBSCRIPTION_STATUS.ACTIVE,
|
|
269
304
|
expiresAt: new Date(expiresAt).toISOString(),
|
|
270
|
-
|
|
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,
|
|
271
308
|
purchasedAt: new Date(now).toISOString(),
|
|
272
|
-
willRenew: true
|
|
309
|
+
willRenew: true,
|
|
310
|
+
...tokenMatch ? { boundAccountId: tokenMatch[1] } : {}
|
|
273
311
|
};
|
|
274
312
|
}
|
|
275
313
|
function mockValidateGoogleProduct(receipt, productId) {
|
|
@@ -284,7 +322,6 @@ function mockValidateGoogleProduct(receipt, productId) {
|
|
|
284
322
|
}
|
|
285
323
|
|
|
286
324
|
// src/providers/apple.ts
|
|
287
|
-
var MAX_CONSUMABLE_RECEIPT_AGE_MS = 72 * 60 * 60 * 1e3;
|
|
288
325
|
function derBase64ToPem(der) {
|
|
289
326
|
return "-----BEGIN CERTIFICATE-----\n" + (der.match(/.{1,64}/g) ?? []).join("\n") + "\n-----END CERTIFICATE-----";
|
|
290
327
|
}
|
|
@@ -387,8 +424,9 @@ async function validateAppleReceipt(receipt, config) {
|
|
|
387
424
|
expiresAt: new Date(tx.expiresDate).toISOString(),
|
|
388
425
|
originalTransactionId: tx.originalTransactionId,
|
|
389
426
|
purchasedAt: new Date(purchasedAt).toISOString(),
|
|
390
|
-
willRenew: status === SUBSCRIPTION_STATUS.ACTIVE
|
|
427
|
+
willRenew: status === SUBSCRIPTION_STATUS.ACTIVE,
|
|
391
428
|
// refined by renewal info in webhook
|
|
429
|
+
...tx.appAccountToken ? { boundAccountId: tx.appAccountToken } : {}
|
|
392
430
|
};
|
|
393
431
|
}
|
|
394
432
|
async function validateAppleConsumableReceipt(signedTransaction, config, expectedProductId) {
|
|
@@ -431,8 +469,9 @@ async function validateAppleConsumableReceipt(signedTransaction, config, expecte
|
|
|
431
469
|
log.warn("[onesub/apple] Purchase was revoked/refunded");
|
|
432
470
|
return null;
|
|
433
471
|
}
|
|
434
|
-
|
|
435
|
-
|
|
472
|
+
const maxAgeMs = (config.productReceiptMaxAgeHours ?? 72) * 60 * 60 * 1e3;
|
|
473
|
+
if (tx.purchaseDate && Date.now() - tx.purchaseDate > maxAgeMs) {
|
|
474
|
+
log.warn(`[onesub/apple] Consumable receipt too old (>${config.productReceiptMaxAgeHours ?? 72}h)`);
|
|
436
475
|
return null;
|
|
437
476
|
}
|
|
438
477
|
const transactionId = tx.transactionId ?? tx.originalTransactionId;
|
|
@@ -721,7 +760,6 @@ var GOOGLE_NOTIFICATION_TYPE = {
|
|
|
721
760
|
SUBSCRIPTION_REVOKED: 12,
|
|
722
761
|
SUBSCRIPTION_EXPIRED: 13
|
|
723
762
|
};
|
|
724
|
-
var MAX_PRODUCT_RECEIPT_AGE_MS = 72 * 60 * 60 * 1e3;
|
|
725
763
|
var refreshPromises = /* @__PURE__ */ new Map();
|
|
726
764
|
function googleTokenCacheKey(serviceAccountKey) {
|
|
727
765
|
let hash = 0;
|
|
@@ -948,6 +986,7 @@ async function validateGoogleReceipt(receipt, productId, config) {
|
|
|
948
986
|
const purchasedAt = purchase.startTime ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
949
987
|
const willRenew = lineItem.autoRenewingPlan?.autoRenewEnabled ?? false;
|
|
950
988
|
const autoResumeTime = status === SUBSCRIPTION_STATUS.PAUSED ? purchase.pausedStateContext?.autoResumeTime : void 0;
|
|
989
|
+
const boundAccountId = purchase.externalAccountIdentifiers?.obfuscatedExternalAccountId;
|
|
951
990
|
return {
|
|
952
991
|
userId: "",
|
|
953
992
|
// caller fills this in
|
|
@@ -955,13 +994,18 @@ async function validateGoogleReceipt(receipt, productId, config) {
|
|
|
955
994
|
platform: "google",
|
|
956
995
|
status,
|
|
957
996
|
expiresAt,
|
|
958
|
-
|
|
997
|
+
// Keyed by the purchaseToken, NOT latestOrderId: RTDN webhooks and
|
|
998
|
+
// linkedPurchaseToken chains only carry the token, so records keyed by
|
|
999
|
+
// order id are invisible to the entire webhook lifecycle (refund/cancel/
|
|
1000
|
+
// renewal would never find them).
|
|
1001
|
+
originalTransactionId: receipt,
|
|
959
1002
|
purchasedAt,
|
|
960
1003
|
willRenew,
|
|
961
1004
|
// Surface the linked token so host apps (and our webhook userId-continuity
|
|
962
1005
|
// logic) can follow upgrade/downgrade chains.
|
|
963
1006
|
linkedPurchaseToken: purchase.linkedPurchaseToken,
|
|
964
|
-
autoResumeTime
|
|
1007
|
+
autoResumeTime,
|
|
1008
|
+
...boundAccountId ? { boundAccountId } : {}
|
|
965
1009
|
};
|
|
966
1010
|
}
|
|
967
1011
|
async function validateGoogleProductReceipt(purchaseToken, productId, config, type = "non_consumable") {
|
|
@@ -992,8 +1036,9 @@ async function validateGoogleProductReceipt(purchaseToken, productId, config, ty
|
|
|
992
1036
|
}
|
|
993
1037
|
if (purchase.purchaseTimeMillis) {
|
|
994
1038
|
const purchaseTime = parseInt(purchase.purchaseTimeMillis, 10);
|
|
995
|
-
|
|
996
|
-
|
|
1039
|
+
const maxAgeMs = (config.productReceiptMaxAgeHours ?? 72) * 60 * 60 * 1e3;
|
|
1040
|
+
if (Date.now() - purchaseTime > maxAgeMs) {
|
|
1041
|
+
log.warn(`[onesub/google] Product receipt too old (>${config.productReceiptMaxAgeHours ?? 72}h)`);
|
|
997
1042
|
return null;
|
|
998
1043
|
}
|
|
999
1044
|
}
|
|
@@ -1137,6 +1182,22 @@ function createValidateRouter(config, store) {
|
|
|
1137
1182
|
sendError(res, 422, ONESUB_ERROR_CODE.RECEIPT_VALIDATION_FAILED, "Receipt validation failed", NO_SUB);
|
|
1138
1183
|
return;
|
|
1139
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
|
+
}
|
|
1140
1201
|
sub.userId = userId;
|
|
1141
1202
|
await store.save(sub);
|
|
1142
1203
|
if (platform === "google" && config.google) {
|
|
@@ -1184,7 +1245,7 @@ function createStatusRouter(store) {
|
|
|
1184
1245
|
return router;
|
|
1185
1246
|
}
|
|
1186
1247
|
var APPLE_ACTIVE_TYPES = /* @__PURE__ */ new Set(["SUBSCRIBED", "DID_RENEW", "DID_RECOVER", "OFFER_REDEEMED"]);
|
|
1187
|
-
var APPLE_CANCELED_TYPES = /* @__PURE__ */ new Set(["REVOKE", "REFUND"
|
|
1248
|
+
var APPLE_CANCELED_TYPES = /* @__PURE__ */ new Set(["REVOKE", "REFUND"]);
|
|
1188
1249
|
var APPLE_EXPIRED_TYPES = /* @__PURE__ */ new Set(["EXPIRED"]);
|
|
1189
1250
|
function mapAppleNotificationStatus(notificationType, subtype) {
|
|
1190
1251
|
if (notificationType === "DID_FAIL_TO_RENEW") {
|
|
@@ -1241,6 +1302,11 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1241
1302
|
} = decoded;
|
|
1242
1303
|
const notificationType = payload.notificationType;
|
|
1243
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
|
+
}
|
|
1244
1310
|
const mapped = mapAppleNotificationStatus(notificationType, subtype);
|
|
1245
1311
|
const finalStatus = mapped ?? status;
|
|
1246
1312
|
if (notificationType === "CONSUMPTION_REQUEST" && config.apple?.consumptionInfoProvider && transactionId && productId) {
|
|
@@ -1267,6 +1333,10 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1267
1333
|
}
|
|
1268
1334
|
const isOneTimePurchase = type === "Consumable" || type === "Non-Consumable";
|
|
1269
1335
|
const isRefundOrRevoke = APPLE_CANCELED_TYPES.has(notificationType);
|
|
1336
|
+
if (isOneTimePurchase && notificationType === "CONSUMPTION_REQUEST") {
|
|
1337
|
+
res.status(200).json({ received: true });
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1270
1340
|
try {
|
|
1271
1341
|
if (isOneTimePurchase && isRefundOrRevoke) {
|
|
1272
1342
|
const lookupId = transactionId ?? originalTransactionId;
|
|
@@ -1279,7 +1349,7 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1279
1349
|
}
|
|
1280
1350
|
const existing = await store.getByTransactionId(originalTransactionId);
|
|
1281
1351
|
if (existing) {
|
|
1282
|
-
const isSubscriptionRefund = isRefundOrRevoke && !isOneTimePurchase
|
|
1352
|
+
const isSubscriptionRefund = isRefundOrRevoke && !isOneTimePurchase;
|
|
1283
1353
|
const keepEntitlement = isSubscriptionRefund && config.refundPolicy === "until_expiry";
|
|
1284
1354
|
const correctedUserId = appAccountToken && existing.userId === originalTransactionId ? appAccountToken : existing.userId;
|
|
1285
1355
|
if (correctedUserId !== existing.userId) {
|
|
@@ -1319,6 +1389,12 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
|
|
|
1319
1389
|
res.status(200).json({ received: true });
|
|
1320
1390
|
} catch (err) {
|
|
1321
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
|
+
}
|
|
1322
1398
|
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to update subscription");
|
|
1323
1399
|
}
|
|
1324
1400
|
}
|
|
@@ -1328,13 +1404,23 @@ function getGoogleJwks() {
|
|
|
1328
1404
|
if (!googleJwks) googleJwks = createRemoteJWKSet(new URL(GOOGLE_JWKS_URL));
|
|
1329
1405
|
return googleJwks;
|
|
1330
1406
|
}
|
|
1331
|
-
async function verifyGooglePushToken(req, expectedAudience) {
|
|
1407
|
+
async function verifyGooglePushToken(req, expectedAudience, expectedServiceAccountEmail) {
|
|
1332
1408
|
const authHeader = req.headers["authorization"];
|
|
1333
1409
|
if (typeof authHeader !== "string" || !authHeader.startsWith("Bearer ")) return false;
|
|
1334
1410
|
try {
|
|
1335
|
-
await jwtVerify(authHeader.slice("Bearer ".length).trim(), getGoogleJwks(), {
|
|
1336
|
-
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"]
|
|
1337
1418
|
});
|
|
1419
|
+
if (expectedServiceAccountEmail) {
|
|
1420
|
+
if (payload["email"] !== expectedServiceAccountEmail || payload["email_verified"] !== true) {
|
|
1421
|
+
return false;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1338
1424
|
return true;
|
|
1339
1425
|
} catch {
|
|
1340
1426
|
return false;
|
|
@@ -1342,7 +1428,11 @@ async function verifyGooglePushToken(req, expectedAudience) {
|
|
|
1342
1428
|
}
|
|
1343
1429
|
async function handleGoogleWebhook(req, res, config, store, purchaseStore, webhookEventStore) {
|
|
1344
1430
|
if (config.google?.pushAudience) {
|
|
1345
|
-
const authenticated = await verifyGooglePushToken(
|
|
1431
|
+
const authenticated = await verifyGooglePushToken(
|
|
1432
|
+
req,
|
|
1433
|
+
config.google.pushAudience,
|
|
1434
|
+
config.google.pushServiceAccountEmail
|
|
1435
|
+
);
|
|
1346
1436
|
if (!authenticated) {
|
|
1347
1437
|
sendError(res, 401, ONESUB_ERROR_CODE.UNAUTHORIZED, "Unauthorized");
|
|
1348
1438
|
return;
|
|
@@ -1353,6 +1443,14 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1353
1443
|
sendError(res, 400, ONESUB_ERROR_CODE.MISSING_MESSAGE_DATA, "Missing message.data");
|
|
1354
1444
|
return;
|
|
1355
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
|
+
};
|
|
1356
1454
|
if (webhookEventStore && typeof body.message.messageId === "string") {
|
|
1357
1455
|
const fresh = await webhookEventStore.markIfNew("google", body.message.messageId);
|
|
1358
1456
|
if (!fresh) {
|
|
@@ -1386,6 +1484,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1386
1484
|
res.status(200).json({ received: true });
|
|
1387
1485
|
} catch (err) {
|
|
1388
1486
|
log.error("[onesub/webhook/google] voided notification error:", err);
|
|
1487
|
+
await unmarkEvent();
|
|
1389
1488
|
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process voided notification");
|
|
1390
1489
|
}
|
|
1391
1490
|
return;
|
|
@@ -1412,6 +1511,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1412
1511
|
res.status(200).json({ received: true });
|
|
1413
1512
|
} catch (err) {
|
|
1414
1513
|
log.error("[onesub/webhook/google] oneTimeProduct error:", err);
|
|
1514
|
+
await unmarkEvent();
|
|
1415
1515
|
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process oneTimeProduct notification");
|
|
1416
1516
|
}
|
|
1417
1517
|
return;
|
|
@@ -1445,7 +1545,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1445
1545
|
} else if (isGoogleExpiredNotification(notificationType)) {
|
|
1446
1546
|
finalStatus = SUBSCRIPTION_STATUS.EXPIRED;
|
|
1447
1547
|
} else {
|
|
1448
|
-
finalStatus = SUBSCRIPTION_STATUS.ACTIVE;
|
|
1548
|
+
finalStatus = existing?.status ?? SUBSCRIPTION_STATUS.ACTIVE;
|
|
1449
1549
|
}
|
|
1450
1550
|
if (isGooglePriceChangeConfirmedNotification(notificationType) && config.google?.onPriceChangeConfirmed) {
|
|
1451
1551
|
const hook = config.google.onPriceChangeConfirmed;
|
|
@@ -1456,7 +1556,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1456
1556
|
if (config.google?.serviceAccountKey) {
|
|
1457
1557
|
const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, config.google);
|
|
1458
1558
|
if (fresh) {
|
|
1459
|
-
const preserveNotificationStatus =
|
|
1559
|
+
const preserveNotificationStatus = isGoogleGracePeriodNotification(notificationType) || isGoogleOnHoldNotification(notificationType);
|
|
1460
1560
|
updated = {
|
|
1461
1561
|
...existing,
|
|
1462
1562
|
status: preserveNotificationStatus ? finalStatus : fresh.status,
|
|
@@ -1472,14 +1572,16 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1472
1572
|
if (config.google?.serviceAccountKey) {
|
|
1473
1573
|
const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, config.google);
|
|
1474
1574
|
if (fresh) {
|
|
1575
|
+
const boundAccountId = fresh.boundAccountId;
|
|
1576
|
+
delete fresh.boundAccountId;
|
|
1475
1577
|
if (fresh.linkedPurchaseToken) {
|
|
1476
1578
|
const previous = await store.getByTransactionId(fresh.linkedPurchaseToken);
|
|
1477
|
-
fresh.userId = previous ? previous.userId : purchaseToken;
|
|
1579
|
+
fresh.userId = previous ? previous.userId : boundAccountId ?? purchaseToken;
|
|
1478
1580
|
if (previous) {
|
|
1479
1581
|
log.info(`[onesub/webhook/google] inherited userId ${previous.userId} from linkedPurchaseToken ${fresh.linkedPurchaseToken} \u2192 new token ${purchaseToken}`);
|
|
1480
1582
|
}
|
|
1481
1583
|
} else {
|
|
1482
|
-
fresh.userId = purchaseToken;
|
|
1584
|
+
fresh.userId = boundAccountId ?? purchaseToken;
|
|
1483
1585
|
}
|
|
1484
1586
|
await store.save(fresh);
|
|
1485
1587
|
}
|
|
@@ -1490,6 +1592,7 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
|
|
|
1490
1592
|
res.status(200).json({ received: true });
|
|
1491
1593
|
} catch (err) {
|
|
1492
1594
|
log.error("[onesub/webhook/google] Error handling notification:", err);
|
|
1595
|
+
await unmarkEvent();
|
|
1493
1596
|
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, "Failed to process notification");
|
|
1494
1597
|
}
|
|
1495
1598
|
}
|
|
@@ -1583,7 +1686,8 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
1583
1686
|
sendError(res, 422, ONESUB_ERROR_CODE.RECEIPT_VALIDATION_FAILED, "Receipt validation failed", NO_PURCHASE);
|
|
1584
1687
|
return;
|
|
1585
1688
|
}
|
|
1586
|
-
|
|
1689
|
+
const bindingMismatch = platform === "apple" ? boundAccountId && boundAccountId.toLowerCase() !== userId.toLowerCase() : boundAccountId && boundAccountId !== userId;
|
|
1690
|
+
if (bindingMismatch) {
|
|
1587
1691
|
log.warn(
|
|
1588
1692
|
`[onesub/purchase] account binding mismatch for transaction ${transactionId}: token does not match userId ${userId}`
|
|
1589
1693
|
);
|
|
@@ -1817,9 +1921,12 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
1817
1921
|
sendError(res, 404, ONESUB_ERROR_CODE.TRANSACTION_NOT_FOUND, "TRANSACTION_NOT_FOUND");
|
|
1818
1922
|
return;
|
|
1819
1923
|
}
|
|
1820
|
-
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
|
+
}
|
|
1821
1929
|
const migrated = { ...existing, userId: body.newUserId };
|
|
1822
|
-
await purchaseStore.savePurchase(migrated);
|
|
1823
1930
|
res.json({ ok: true, purchase: migrated });
|
|
1824
1931
|
});
|
|
1825
1932
|
const grantSchema = z.object({
|
|
@@ -2351,7 +2458,11 @@ var PostgresSubscriptionStore = class {
|
|
|
2351
2458
|
);
|
|
2352
2459
|
});
|
|
2353
2460
|
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
2354
|
-
|
|
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;
|
|
2355
2466
|
})();
|
|
2356
2467
|
}
|
|
2357
2468
|
return this.poolPromise;
|
|
@@ -2523,7 +2634,11 @@ var PostgresPurchaseStore = class {
|
|
|
2523
2634
|
);
|
|
2524
2635
|
});
|
|
2525
2636
|
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
2526
|
-
|
|
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;
|
|
2527
2642
|
})();
|
|
2528
2643
|
}
|
|
2529
2644
|
return this.poolPromise;
|
|
@@ -2538,23 +2653,12 @@ var PostgresPurchaseStore = class {
|
|
|
2538
2653
|
}
|
|
2539
2654
|
async savePurchase(purchase) {
|
|
2540
2655
|
const pool = await this.getPool();
|
|
2541
|
-
const
|
|
2542
|
-
`SELECT user_id FROM onesub_purchases WHERE transaction_id = $1`,
|
|
2543
|
-
[purchase.transactionId]
|
|
2544
|
-
);
|
|
2545
|
-
if (existing.rows.length > 0) {
|
|
2546
|
-
const owner = existing.rows[0].user_id;
|
|
2547
|
-
if (owner !== purchase.userId) {
|
|
2548
|
-
const err = new Error("TRANSACTION_BELONGS_TO_OTHER_USER");
|
|
2549
|
-
err.code = "TRANSACTION_BELONGS_TO_OTHER_USER";
|
|
2550
|
-
throw err;
|
|
2551
|
-
}
|
|
2552
|
-
return;
|
|
2553
|
-
}
|
|
2554
|
-
await pool.query(
|
|
2656
|
+
const inserted = await pool.query(
|
|
2555
2657
|
`INSERT INTO onesub_purchases
|
|
2556
2658
|
(transaction_id, user_id, product_id, platform, type, quantity, purchased_at)
|
|
2557
|
-
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`,
|
|
2558
2662
|
[
|
|
2559
2663
|
purchase.transactionId,
|
|
2560
2664
|
purchase.userId,
|
|
@@ -2565,6 +2669,17 @@ var PostgresPurchaseStore = class {
|
|
|
2565
2669
|
purchase.purchasedAt
|
|
2566
2670
|
]
|
|
2567
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
|
+
}
|
|
2568
2683
|
}
|
|
2569
2684
|
async getPurchasesByUserId(userId) {
|
|
2570
2685
|
const pool = await this.getPool();
|
|
@@ -2665,7 +2780,6 @@ function rowToPurchaseInfo(row) {
|
|
|
2665
2780
|
// src/stores/redis.ts
|
|
2666
2781
|
var SUB_TX_PREFIX = "onesub:sub:tx:";
|
|
2667
2782
|
var SUB_USER_PREFIX = "onesub:sub:user:";
|
|
2668
|
-
var SUB_ALL = "onesub:sub:all";
|
|
2669
2783
|
var SUB_ALL_SORTED = "onesub:sub:all:sorted";
|
|
2670
2784
|
var PUR_TX_PREFIX = "onesub:purchase:tx:";
|
|
2671
2785
|
var PUR_USER_PREFIX = "onesub:purchase:user:";
|
|
@@ -2680,10 +2794,14 @@ var RedisSubscriptionStore = class {
|
|
|
2680
2794
|
const score = Date.now();
|
|
2681
2795
|
const txKey = SUB_TX_PREFIX + sub.originalTransactionId;
|
|
2682
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;
|
|
2683
2799
|
const pipeline = this.redis.multi();
|
|
2684
2800
|
pipeline.set(txKey, JSON.stringify(sub));
|
|
2801
|
+
if (prevUserId !== null && prevUserId !== sub.userId) {
|
|
2802
|
+
pipeline.zrem(SUB_USER_PREFIX + prevUserId, sub.originalTransactionId);
|
|
2803
|
+
}
|
|
2685
2804
|
pipeline.zadd(userKey, score, sub.originalTransactionId);
|
|
2686
|
-
pipeline.sadd(SUB_ALL, sub.originalTransactionId);
|
|
2687
2805
|
pipeline.zadd(SUB_ALL_SORTED, score, sub.originalTransactionId);
|
|
2688
2806
|
await pipeline.exec();
|
|
2689
2807
|
}
|
|
@@ -2755,19 +2873,18 @@ var RedisPurchaseStore = class {
|
|
|
2755
2873
|
redis;
|
|
2756
2874
|
async savePurchase(purchase) {
|
|
2757
2875
|
const txKey = PUR_TX_PREFIX + purchase.transactionId;
|
|
2758
|
-
const
|
|
2759
|
-
if (
|
|
2760
|
-
const
|
|
2761
|
-
|
|
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) {
|
|
2762
2881
|
const err = new Error("TRANSACTION_BELONGS_TO_OTHER_USER");
|
|
2763
2882
|
err.code = "TRANSACTION_BELONGS_TO_OTHER_USER";
|
|
2764
2883
|
throw err;
|
|
2765
2884
|
}
|
|
2766
|
-
return;
|
|
2767
2885
|
}
|
|
2768
2886
|
const score = Date.parse(purchase.purchasedAt) || Date.now();
|
|
2769
2887
|
const pipeline = this.redis.multi();
|
|
2770
|
-
pipeline.set(txKey, JSON.stringify(purchase));
|
|
2771
2888
|
pipeline.zadd(PUR_USER_PREFIX + purchase.userId, score, purchase.transactionId);
|
|
2772
2889
|
pipeline.sadd(PUR_USER_PRODUCT_PREFIX + purchase.userId + ":" + purchase.productId, purchase.transactionId);
|
|
2773
2890
|
pipeline.sadd(PUR_ALL, purchase.transactionId);
|
|
@@ -2878,6 +2995,9 @@ var RedisWebhookEventStore = class {
|
|
|
2878
2995
|
const result = await this.redis.set(key, "1", "EX", this.ttlSeconds, "NX");
|
|
2879
2996
|
return result === "OK";
|
|
2880
2997
|
}
|
|
2998
|
+
async unmark(provider, eventId) {
|
|
2999
|
+
await this.redis.del(WEBHOOK_EVENT_PREFIX + provider + ":" + eventId);
|
|
3000
|
+
}
|
|
2881
3001
|
};
|
|
2882
3002
|
|
|
2883
3003
|
// src/webhook-events.ts
|
|
@@ -2895,6 +3015,9 @@ var InMemoryWebhookEventStore = class {
|
|
|
2895
3015
|
this.seen.set(key, Date.now() + this.ttlSeconds * 1e3);
|
|
2896
3016
|
return true;
|
|
2897
3017
|
}
|
|
3018
|
+
async unmark(provider, eventId) {
|
|
3019
|
+
this.seen.delete(`${provider}:${eventId}`);
|
|
3020
|
+
}
|
|
2898
3021
|
evictExpired() {
|
|
2899
3022
|
const now = Date.now();
|
|
2900
3023
|
for (const [key, expiresAt] of this.seen) {
|
|
@@ -2916,6 +3039,9 @@ var CacheWebhookEventStore = class {
|
|
|
2916
3039
|
await this.cache.set(key, "1", this.ttlSeconds);
|
|
2917
3040
|
return true;
|
|
2918
3041
|
}
|
|
3042
|
+
async unmark(provider, eventId) {
|
|
3043
|
+
await this.cache.del(`webhook:event:${provider}:${eventId}`);
|
|
3044
|
+
}
|
|
2919
3045
|
};
|
|
2920
3046
|
|
|
2921
3047
|
// src/webhook-queue.ts
|
|
@@ -3193,8 +3319,6 @@ function openapiHandler() {
|
|
|
3193
3319
|
res.json(ONESUB_OPENAPI);
|
|
3194
3320
|
};
|
|
3195
3321
|
}
|
|
3196
|
-
|
|
3197
|
-
// src/tracing.ts
|
|
3198
3322
|
var NOOP_SPAN = {
|
|
3199
3323
|
setAttribute: () => {
|
|
3200
3324
|
},
|
|
@@ -3211,8 +3335,7 @@ function getTracer() {
|
|
|
3211
3335
|
if (resolved) return cachedTracer;
|
|
3212
3336
|
resolved = true;
|
|
3213
3337
|
try {
|
|
3214
|
-
const req =
|
|
3215
|
-
if (!req) return null;
|
|
3338
|
+
const req = typeof __require === "function" ? __require : createRequire(import.meta.url);
|
|
3216
3339
|
const otel = req("@opentelemetry/api");
|
|
3217
3340
|
cachedTracer = otel.trace.getTracer("@onesub/server", "1.0.0");
|
|
3218
3341
|
return cachedTracer;
|