@onesub/server 0.20.0 → 0.21.1
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__/errors.test.d.ts +12 -0
- package/dist/__tests__/errors.test.d.ts.map +1 -0
- package/dist/__tests__/purchase-product-lookup.test.d.ts +17 -0
- package/dist/__tests__/purchase-product-lookup.test.d.ts.map +1 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.cjs +133 -161
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +133 -161
- package/dist/index.js.map +1 -1
- package/dist/routes/admin.d.ts.map +1 -1
- package/dist/routes/apple-offer.d.ts.map +1 -1
- package/dist/routes/entitlements.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/store.d.ts +22 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/stores/postgres.d.ts +10 -0
- package/dist/stores/postgres.d.ts.map +1 -1
- package/dist/stores/redis.d.ts +12 -0
- package/dist/stores/redis.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -84,11 +84,17 @@ var InMemoryPurchaseStore = class {
|
|
|
84
84
|
}
|
|
85
85
|
this.byTransactionId.set(purchase.transactionId, purchase);
|
|
86
86
|
const list = this.byUserId.get(purchase.userId) ?? [];
|
|
87
|
-
|
|
87
|
+
const at = Date.parse(purchase.purchasedAt);
|
|
88
|
+
const idx = list.findIndex((p) => Date.parse(p.purchasedAt) < at);
|
|
89
|
+
if (idx === -1) list.push(purchase);
|
|
90
|
+
else list.splice(idx, 0, purchase);
|
|
88
91
|
this.byUserId.set(purchase.userId, list);
|
|
89
92
|
}
|
|
90
93
|
async getPurchasesByUserId(userId) {
|
|
91
|
-
return this.byUserId.get(userId) ?? [];
|
|
94
|
+
return [...this.byUserId.get(userId) ?? []];
|
|
95
|
+
}
|
|
96
|
+
async getPurchasesForProduct(userId, productId) {
|
|
97
|
+
return (this.byUserId.get(userId) ?? []).filter((p) => p.productId === productId);
|
|
92
98
|
}
|
|
93
99
|
async getPurchaseByTransactionId(txId) {
|
|
94
100
|
return this.byTransactionId.get(txId) ?? null;
|
|
@@ -1265,6 +1271,16 @@ function sendZodError(res, err2, extra = {}) {
|
|
|
1265
1271
|
extra
|
|
1266
1272
|
);
|
|
1267
1273
|
}
|
|
1274
|
+
function parseOrSend(res, schema, input, opts = {}) {
|
|
1275
|
+
const result = schema.safeParse(input);
|
|
1276
|
+
if (result.success) return result.data;
|
|
1277
|
+
if (opts.message !== void 0) {
|
|
1278
|
+
sendError(res, 400, ONESUB_ERROR_CODE.INVALID_INPUT, opts.message, opts.extra ?? {});
|
|
1279
|
+
} else {
|
|
1280
|
+
sendZodError(res, result.error, opts.extra ?? {});
|
|
1281
|
+
}
|
|
1282
|
+
return void 0;
|
|
1283
|
+
}
|
|
1268
1284
|
|
|
1269
1285
|
// src/apps.ts
|
|
1270
1286
|
function appName(app) {
|
|
@@ -1363,20 +1379,9 @@ function createValidateRouter(config, store) {
|
|
|
1363
1379
|
const router = Router();
|
|
1364
1380
|
const registry = getAppRegistry(config);
|
|
1365
1381
|
router.post(ROUTES.VALIDATE, async (req, res) => {
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
let productId;
|
|
1370
|
-
let appId;
|
|
1371
|
-
try {
|
|
1372
|
-
({ platform, receipt, userId, productId, appId } = validateSchema.parse(req.body));
|
|
1373
|
-
} catch (err2) {
|
|
1374
|
-
if (err2 instanceof z.ZodError) {
|
|
1375
|
-
sendZodError(res, err2, NO_SUB);
|
|
1376
|
-
return;
|
|
1377
|
-
}
|
|
1378
|
-
throw err2;
|
|
1379
|
-
}
|
|
1382
|
+
const body = parseOrSend(res, validateSchema, req.body, { extra: NO_SUB });
|
|
1383
|
+
if (!body) return;
|
|
1384
|
+
const { platform, receipt, userId, productId, appId } = body;
|
|
1380
1385
|
try {
|
|
1381
1386
|
let sub = null;
|
|
1382
1387
|
const appConfig = registry.configFor({
|
|
@@ -1974,20 +1979,19 @@ var purchaseStatusQuerySchema = z.object({
|
|
|
1974
1979
|
userId: z.string().min(1).max(256),
|
|
1975
1980
|
productId: z.string().min(1).max(256).optional()
|
|
1976
1981
|
});
|
|
1982
|
+
async function purchasesForProduct(purchaseStore, userId, productId) {
|
|
1983
|
+
if (purchaseStore.getPurchasesForProduct) {
|
|
1984
|
+
return purchaseStore.getPurchasesForProduct(userId, productId);
|
|
1985
|
+
}
|
|
1986
|
+
const all = await purchaseStore.getPurchasesByUserId(userId);
|
|
1987
|
+
return all.filter((p) => p.productId === productId);
|
|
1988
|
+
}
|
|
1977
1989
|
function createPurchaseRouter(config, purchaseStore) {
|
|
1978
1990
|
const router = Router();
|
|
1979
1991
|
const registry = getAppRegistry(config);
|
|
1980
1992
|
router.post(ROUTES.VALIDATE_PURCHASE, async (req, res) => {
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
body = validatePurchaseSchema.parse(req.body);
|
|
1984
|
-
} catch (err2) {
|
|
1985
|
-
if (err2 instanceof z.ZodError) {
|
|
1986
|
-
sendZodError(res, err2, NO_PURCHASE);
|
|
1987
|
-
return;
|
|
1988
|
-
}
|
|
1989
|
-
throw err2;
|
|
1990
|
-
}
|
|
1993
|
+
const body = parseOrSend(res, validatePurchaseSchema, req.body, { extra: NO_PURCHASE });
|
|
1994
|
+
if (!body) return;
|
|
1991
1995
|
const { platform, receipt, userId, productId, type, appId } = body;
|
|
1992
1996
|
const appConfig = registry.configFor({
|
|
1993
1997
|
appId,
|
|
@@ -1995,9 +1999,7 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
1995
1999
|
});
|
|
1996
2000
|
try {
|
|
1997
2001
|
if (type === PURCHASE_TYPE.NON_CONSUMABLE) {
|
|
1998
|
-
const owned = (await purchaseStore
|
|
1999
|
-
(p) => p.productId === productId
|
|
2000
|
-
);
|
|
2002
|
+
const owned = (await purchasesForProduct(purchaseStore, userId, productId))[0];
|
|
2001
2003
|
if (owned) {
|
|
2002
2004
|
const response2 = {
|
|
2003
2005
|
valid: true,
|
|
@@ -2115,22 +2117,13 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2115
2117
|
}
|
|
2116
2118
|
});
|
|
2117
2119
|
router.get(ROUTES.PURCHASE_STATUS, async (req, res) => {
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
if (err2 instanceof z.ZodError) {
|
|
2123
|
-
sendZodError(res, err2, { purchases: [] });
|
|
2124
|
-
return;
|
|
2125
|
-
}
|
|
2126
|
-
throw err2;
|
|
2127
|
-
}
|
|
2120
|
+
const query = parseOrSend(res, purchaseStatusQuerySchema, req.query, {
|
|
2121
|
+
extra: { purchases: [] }
|
|
2122
|
+
});
|
|
2123
|
+
if (!query) return;
|
|
2128
2124
|
const { userId, productId } = query;
|
|
2129
2125
|
try {
|
|
2130
|
-
|
|
2131
|
-
if (productId !== void 0) {
|
|
2132
|
-
purchases = purchases.filter((p) => p.productId === productId);
|
|
2133
|
-
}
|
|
2126
|
+
const purchases = productId !== void 0 ? await purchasesForProduct(purchaseStore, userId, productId) : await purchaseStore.getPurchasesByUserId(userId);
|
|
2134
2127
|
const response = { purchases };
|
|
2135
2128
|
res.status(200).json(response);
|
|
2136
2129
|
} catch (err2) {
|
|
@@ -2175,6 +2168,8 @@ async function evaluateEntitlement(userId, entitlement, store, purchaseStore) {
|
|
|
2175
2168
|
}
|
|
2176
2169
|
var userIdSchema = z.string().min(1).max(256);
|
|
2177
2170
|
var entitlementIdSchema = z.string().min(1).max(128);
|
|
2171
|
+
var entitlementQuerySchema = z.object({ userId: userIdSchema, id: entitlementIdSchema });
|
|
2172
|
+
var entitlementsQuerySchema = z.object({ userId: userIdSchema });
|
|
2178
2173
|
function createEntitlementRouter(config, store, purchaseStore) {
|
|
2179
2174
|
if (!config.entitlements || Object.keys(config.entitlements).length === 0) {
|
|
2180
2175
|
return null;
|
|
@@ -2182,15 +2177,11 @@ function createEntitlementRouter(config, store, purchaseStore) {
|
|
|
2182
2177
|
const entitlements = config.entitlements;
|
|
2183
2178
|
const router = Router();
|
|
2184
2179
|
router.get(ROUTES.ENTITLEMENT, async (req, res) => {
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
} catch {
|
|
2191
|
-
sendError(res, 400, ONESUB_ERROR_CODE.INVALID_INPUT, "userId and id are required");
|
|
2192
|
-
return;
|
|
2193
|
-
}
|
|
2180
|
+
const query = parseOrSend(res, entitlementQuerySchema, req.query, {
|
|
2181
|
+
message: "userId and id are required"
|
|
2182
|
+
});
|
|
2183
|
+
if (!query) return;
|
|
2184
|
+
const { userId, id } = query;
|
|
2194
2185
|
const entitlement = entitlements[id];
|
|
2195
2186
|
if (!entitlement) {
|
|
2196
2187
|
sendError(res, 404, ONESUB_ERROR_CODE.ENTITLEMENT_NOT_FOUND, `Unknown entitlement: ${id}`);
|
|
@@ -2206,13 +2197,11 @@ function createEntitlementRouter(config, store, purchaseStore) {
|
|
|
2206
2197
|
}
|
|
2207
2198
|
});
|
|
2208
2199
|
router.get(ROUTES.ENTITLEMENTS, async (req, res) => {
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
return;
|
|
2215
|
-
}
|
|
2200
|
+
const query = parseOrSend(res, entitlementsQuerySchema, req.query, {
|
|
2201
|
+
message: "userId is required"
|
|
2202
|
+
});
|
|
2203
|
+
if (!query) return;
|
|
2204
|
+
const { userId } = query;
|
|
2216
2205
|
try {
|
|
2217
2206
|
const [subs, purchases] = await Promise.all([
|
|
2218
2207
|
store.getAllByUserId(userId),
|
|
@@ -2261,13 +2250,10 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2261
2250
|
productId: z.string().min(1).max(256)
|
|
2262
2251
|
});
|
|
2263
2252
|
router.delete("/onesub/purchase/admin/:userId/:productId", async (req, res) => {
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
sendError(res, 400, ONESUB_ERROR_CODE.INVALID_INPUT, "userId and productId required");
|
|
2269
|
-
return;
|
|
2270
|
-
}
|
|
2253
|
+
const params = parseOrSend(res, resetParamsSchema, req.params, {
|
|
2254
|
+
message: "userId and productId required"
|
|
2255
|
+
});
|
|
2256
|
+
if (!params) return;
|
|
2271
2257
|
const deleted = await purchaseStore.deletePurchases(params.userId, params.productId);
|
|
2272
2258
|
res.json({ ok: true, deleted });
|
|
2273
2259
|
});
|
|
@@ -2276,16 +2262,8 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2276
2262
|
newUserId: z.string().min(1).max(256)
|
|
2277
2263
|
});
|
|
2278
2264
|
router.post("/onesub/purchase/admin/transfer", async (req, res) => {
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
body = transferSchema.parse(req.body);
|
|
2282
|
-
} catch (err2) {
|
|
2283
|
-
if (err2 instanceof z.ZodError) {
|
|
2284
|
-
sendZodError(res, err2);
|
|
2285
|
-
return;
|
|
2286
|
-
}
|
|
2287
|
-
throw err2;
|
|
2288
|
-
}
|
|
2265
|
+
const body = parseOrSend(res, transferSchema, req.body);
|
|
2266
|
+
if (!body) return;
|
|
2289
2267
|
const existing = await purchaseStore.getPurchaseByTransactionId(body.transactionId);
|
|
2290
2268
|
if (!existing) {
|
|
2291
2269
|
sendError(res, 404, ONESUB_ERROR_CODE.TRANSACTION_NOT_FOUND, "TRANSACTION_NOT_FOUND");
|
|
@@ -2307,16 +2285,8 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2307
2285
|
transactionId: z.string().min(1).max(256).optional()
|
|
2308
2286
|
});
|
|
2309
2287
|
router.post("/onesub/purchase/admin/grant", async (req, res) => {
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
body = grantSchema.parse(req.body);
|
|
2313
|
-
} catch (err2) {
|
|
2314
|
-
if (err2 instanceof z.ZodError) {
|
|
2315
|
-
sendZodError(res, err2);
|
|
2316
|
-
return;
|
|
2317
|
-
}
|
|
2318
|
-
throw err2;
|
|
2319
|
-
}
|
|
2288
|
+
const body = parseOrSend(res, grantSchema, req.body);
|
|
2289
|
+
if (!body) return;
|
|
2320
2290
|
const transactionId = body.transactionId ?? `admin_grant_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
2321
2291
|
const purchase = {
|
|
2322
2292
|
transactionId,
|
|
@@ -2348,16 +2318,8 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2348
2318
|
offset: z.coerce.number().int().nonnegative().optional()
|
|
2349
2319
|
});
|
|
2350
2320
|
router.get(ROUTES.ADMIN_SUBSCRIPTIONS, async (req, res) => {
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
query = listQuerySchema.parse(req.query);
|
|
2354
|
-
} catch (err2) {
|
|
2355
|
-
if (err2 instanceof z.ZodError) {
|
|
2356
|
-
sendZodError(res, err2);
|
|
2357
|
-
return;
|
|
2358
|
-
}
|
|
2359
|
-
throw err2;
|
|
2360
|
-
}
|
|
2321
|
+
const query = parseOrSend(res, listQuerySchema, req.query);
|
|
2322
|
+
if (!query) return;
|
|
2361
2323
|
try {
|
|
2362
2324
|
const result = await store.listFiltered(query);
|
|
2363
2325
|
const response = {
|
|
@@ -2368,20 +2330,18 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2368
2330
|
};
|
|
2369
2331
|
res.status(200).json(response);
|
|
2370
2332
|
} catch (err2) {
|
|
2371
|
-
|
|
2333
|
+
log.error("[onesub/admin/subscriptions] list error:", err2);
|
|
2334
|
+
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2372
2335
|
}
|
|
2373
2336
|
});
|
|
2374
2337
|
const detailParamsSchema = z.object({
|
|
2375
2338
|
transactionId: z.string().min(1).max(256)
|
|
2376
2339
|
});
|
|
2377
2340
|
router.get("/onesub/admin/subscriptions/:transactionId", async (req, res) => {
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
sendError(res, 400, ONESUB_ERROR_CODE.INVALID_INPUT, "transactionId required");
|
|
2383
|
-
return;
|
|
2384
|
-
}
|
|
2341
|
+
const params = parseOrSend(res, detailParamsSchema, req.params, {
|
|
2342
|
+
message: "transactionId required"
|
|
2343
|
+
});
|
|
2344
|
+
if (!params) return;
|
|
2385
2345
|
try {
|
|
2386
2346
|
const sub = await store.getByTransactionId(params.transactionId);
|
|
2387
2347
|
if (!sub) {
|
|
@@ -2390,20 +2350,18 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2390
2350
|
}
|
|
2391
2351
|
res.status(200).json(sub);
|
|
2392
2352
|
} catch (err2) {
|
|
2393
|
-
|
|
2353
|
+
log.error("[onesub/admin/subscriptions/:transactionId] detail error:", err2);
|
|
2354
|
+
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2394
2355
|
}
|
|
2395
2356
|
});
|
|
2396
2357
|
const customerParamsSchema = z.object({
|
|
2397
2358
|
userId: z.string().min(1).max(256)
|
|
2398
2359
|
});
|
|
2399
2360
|
router.get("/onesub/admin/customers/:userId", async (req, res) => {
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
sendError(res, 400, ONESUB_ERROR_CODE.INVALID_INPUT, "userId required");
|
|
2405
|
-
return;
|
|
2406
|
-
}
|
|
2361
|
+
const params = parseOrSend(res, customerParamsSchema, req.params, {
|
|
2362
|
+
message: "userId required"
|
|
2363
|
+
});
|
|
2364
|
+
if (!params) return;
|
|
2407
2365
|
try {
|
|
2408
2366
|
const [subscriptions, purchases] = await Promise.all([
|
|
2409
2367
|
store.getAllByUserId(params.userId),
|
|
@@ -2425,20 +2383,18 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2425
2383
|
};
|
|
2426
2384
|
res.status(200).json(response);
|
|
2427
2385
|
} catch (err2) {
|
|
2428
|
-
|
|
2386
|
+
log.error("[onesub/admin/customers/:userId] error:", err2);
|
|
2387
|
+
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2429
2388
|
}
|
|
2430
2389
|
});
|
|
2431
2390
|
const syncAppleParamsSchema = z.object({
|
|
2432
2391
|
originalTransactionId: z.string().min(1).max(256)
|
|
2433
2392
|
});
|
|
2434
2393
|
router.post(ROUTES.ADMIN_SYNC_APPLE, async (req, res) => {
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
sendError(res, 400, ONESUB_ERROR_CODE.INVALID_INPUT, "originalTransactionId required");
|
|
2440
|
-
return;
|
|
2441
|
-
}
|
|
2394
|
+
const params = parseOrSend(res, syncAppleParamsSchema, req.params, {
|
|
2395
|
+
message: "originalTransactionId required"
|
|
2396
|
+
});
|
|
2397
|
+
if (!params) return;
|
|
2442
2398
|
if (!config.apple?.issuerId || !config.apple?.keyId || !config.apple?.privateKey) {
|
|
2443
2399
|
sendError(res, 400, ONESUB_ERROR_CODE.APPLE_CONFIG_MISSING, "Apple API credentials not configured");
|
|
2444
2400
|
return;
|
|
@@ -2457,7 +2413,8 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2457
2413
|
await store.save(fresh);
|
|
2458
2414
|
res.status(200).json({ ok: true, subscription: fresh });
|
|
2459
2415
|
} catch (err2) {
|
|
2460
|
-
|
|
2416
|
+
log.error("[onesub/admin/sync-apple] error:", err2);
|
|
2417
|
+
sendError(res, 500, ONESUB_ERROR_CODE.INTERNAL_ERROR, "Internal server error");
|
|
2461
2418
|
}
|
|
2462
2419
|
});
|
|
2463
2420
|
const overrideParamsSchema = z.object({ userId: z.string().min(1).max(256) });
|
|
@@ -2466,19 +2423,10 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2466
2423
|
res.json({ overrides: listTestOverrides() });
|
|
2467
2424
|
});
|
|
2468
2425
|
router.put(ROUTES.ADMIN_TEST_OVERRIDE, (req, res) => {
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
body = overrideBodySchema.parse(req.body);
|
|
2474
|
-
} catch (err2) {
|
|
2475
|
-
if (err2 instanceof z.ZodError) {
|
|
2476
|
-
sendZodError(res, err2);
|
|
2477
|
-
} else {
|
|
2478
|
-
sendError(res, 400, ONESUB_ERROR_CODE.INVALID_INPUT, "userId and { entitled: boolean } required");
|
|
2479
|
-
}
|
|
2480
|
-
return;
|
|
2481
|
-
}
|
|
2426
|
+
const params = parseOrSend(res, overrideParamsSchema, req.params);
|
|
2427
|
+
if (!params) return;
|
|
2428
|
+
const body = parseOrSend(res, overrideBodySchema, req.body);
|
|
2429
|
+
if (!body) return;
|
|
2482
2430
|
setTestOverride(params.userId, body.entitled);
|
|
2483
2431
|
log.warn(
|
|
2484
2432
|
`[onesub/admin] sandbox test override set for ${params.userId}: entitled=${body.entitled}`
|
|
@@ -2486,13 +2434,10 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2486
2434
|
res.json({ ok: true, userId: params.userId, entitled: body.entitled, sandboxOnly: true });
|
|
2487
2435
|
});
|
|
2488
2436
|
router.delete(ROUTES.ADMIN_TEST_OVERRIDE, (req, res) => {
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
sendError(res, 400, ONESUB_ERROR_CODE.INVALID_INPUT, "userId required");
|
|
2494
|
-
return;
|
|
2495
|
-
}
|
|
2437
|
+
const params = parseOrSend(res, overrideParamsSchema, req.params, {
|
|
2438
|
+
message: "userId required"
|
|
2439
|
+
});
|
|
2440
|
+
if (!params) return;
|
|
2496
2441
|
const cleared = clearTestOverride(params.userId);
|
|
2497
2442
|
res.json({ ok: true, userId: params.userId, cleared });
|
|
2498
2443
|
});
|
|
@@ -2502,25 +2447,22 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2502
2447
|
const items = await webhookQueue.listDeadLetters();
|
|
2503
2448
|
res.status(200).json({ items });
|
|
2504
2449
|
} catch (err2) {
|
|
2505
|
-
|
|
2450
|
+
log.error("[onesub/admin/webhook-deadletters] error:", err2);
|
|
2451
|
+
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2506
2452
|
}
|
|
2507
2453
|
});
|
|
2508
2454
|
}
|
|
2509
2455
|
if (webhookQueue?.replayDeadLetter) {
|
|
2510
2456
|
const replayParamsSchema = z.object({ id: z.string().min(1).max(256) });
|
|
2511
2457
|
router.post("/onesub/admin/webhook-replay/:id", async (req, res) => {
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
params = replayParamsSchema.parse(req.params);
|
|
2515
|
-
} catch {
|
|
2516
|
-
sendError(res, 400, ONESUB_ERROR_CODE.INVALID_INPUT, "id required");
|
|
2517
|
-
return;
|
|
2518
|
-
}
|
|
2458
|
+
const params = parseOrSend(res, replayParamsSchema, req.params, { message: "id required" });
|
|
2459
|
+
if (!params) return;
|
|
2519
2460
|
try {
|
|
2520
2461
|
await webhookQueue.replayDeadLetter(params.id);
|
|
2521
2462
|
res.status(200).json({ ok: true });
|
|
2522
2463
|
} catch (err2) {
|
|
2523
|
-
|
|
2464
|
+
log.error("[onesub/admin/webhook-replay] error:", err2);
|
|
2465
|
+
sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
|
|
2524
2466
|
}
|
|
2525
2467
|
});
|
|
2526
2468
|
}
|
|
@@ -2779,16 +2721,8 @@ function createAppleOfferRouter(config) {
|
|
|
2779
2721
|
return;
|
|
2780
2722
|
}
|
|
2781
2723
|
}
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
body = offerBodySchema.parse(req.body);
|
|
2785
|
-
} catch (err2) {
|
|
2786
|
-
if (err2 instanceof z.ZodError) {
|
|
2787
|
-
sendZodError(res, err2);
|
|
2788
|
-
return;
|
|
2789
|
-
}
|
|
2790
|
-
throw err2;
|
|
2791
|
-
}
|
|
2724
|
+
const body = parseOrSend(res, offerBodySchema, req.body);
|
|
2725
|
+
if (!body) return;
|
|
2792
2726
|
if (!apple.bundleId) {
|
|
2793
2727
|
sendError(res, 400, ONESUB_ERROR_CODE.APPLE_CONFIG_MISSING, "config.apple.bundleId is required for offer signing");
|
|
2794
2728
|
return;
|
|
@@ -2805,7 +2739,8 @@ function createAppleOfferRouter(config) {
|
|
|
2805
2739
|
);
|
|
2806
2740
|
res.status(200).json(result);
|
|
2807
2741
|
} catch (err2) {
|
|
2808
|
-
|
|
2742
|
+
log.error("[onesub/apple/offer] signing error:", err2);
|
|
2743
|
+
sendError(res, 500, ONESUB_ERROR_CODE.INTERNAL_ERROR, "Offer signing failed");
|
|
2809
2744
|
}
|
|
2810
2745
|
});
|
|
2811
2746
|
return router;
|
|
@@ -3106,6 +3041,26 @@ var PostgresPurchaseStore = class {
|
|
|
3106
3041
|
);
|
|
3107
3042
|
return result.rows.map(rowToPurchaseInfo);
|
|
3108
3043
|
}
|
|
3044
|
+
/**
|
|
3045
|
+
* Purchases for one user + product, most-recent-first.
|
|
3046
|
+
*
|
|
3047
|
+
* Same shape and ordering as `getPurchasesByUserId`, with `product_id` pushed
|
|
3048
|
+
* into the WHERE clause instead of filtered in process. Non-consumables also
|
|
3049
|
+
* have a partial unique index on `(user_id, product_id)`, so the ownership
|
|
3050
|
+
* check this backs is a single index lookup rather than a full read of the
|
|
3051
|
+
* user's purchase history.
|
|
3052
|
+
*/
|
|
3053
|
+
async getPurchasesForProduct(userId, productId) {
|
|
3054
|
+
const pool = await this.getPool();
|
|
3055
|
+
const result = await pool.query(
|
|
3056
|
+
`SELECT *
|
|
3057
|
+
FROM onesub_purchases
|
|
3058
|
+
WHERE user_id = $1 AND product_id = $2
|
|
3059
|
+
ORDER BY purchased_at DESC`,
|
|
3060
|
+
[userId, productId]
|
|
3061
|
+
);
|
|
3062
|
+
return result.rows.map(rowToPurchaseInfo);
|
|
3063
|
+
}
|
|
3109
3064
|
async getPurchaseByTransactionId(txId) {
|
|
3110
3065
|
const pool = await this.getPool();
|
|
3111
3066
|
const result = await pool.query(
|
|
@@ -3316,6 +3271,23 @@ var RedisPurchaseStore = class {
|
|
|
3316
3271
|
const raws = await this.redis.mget(...ids.map((id) => PUR_TX_PREFIX + id));
|
|
3317
3272
|
return raws.filter((r) => r != null).map((r) => JSON.parse(r));
|
|
3318
3273
|
}
|
|
3274
|
+
/**
|
|
3275
|
+
* Purchases for one user + product, most-recent-first.
|
|
3276
|
+
*
|
|
3277
|
+
* Served from the `user_product` set that `savePurchase` already maintains for
|
|
3278
|
+
* `hasPurchased`, so this reads only the rows for this product rather than the
|
|
3279
|
+
* user's whole purchase history.
|
|
3280
|
+
*
|
|
3281
|
+
* That set is unordered — unlike the per-user sorted set — so the
|
|
3282
|
+
* most-recent-first contract is restored by an explicit sort here. The row
|
|
3283
|
+
* count is per-product, so this is small.
|
|
3284
|
+
*/
|
|
3285
|
+
async getPurchasesForProduct(userId, productId) {
|
|
3286
|
+
const ids = await this.redis.smembers(PUR_USER_PRODUCT_PREFIX + userId + ":" + productId);
|
|
3287
|
+
if (ids.length === 0) return [];
|
|
3288
|
+
const raws = await this.redis.mget(...ids.map((id) => PUR_TX_PREFIX + id));
|
|
3289
|
+
return raws.filter((r) => r != null).map((r) => JSON.parse(r)).sort((a, b) => Date.parse(b.purchasedAt) - Date.parse(a.purchasedAt));
|
|
3290
|
+
}
|
|
3319
3291
|
async getPurchaseByTransactionId(txId) {
|
|
3320
3292
|
const raw = await this.redis.get(PUR_TX_PREFIX + txId);
|
|
3321
3293
|
return raw ? JSON.parse(raw) : null;
|