@onesub/server 0.21.0 → 0.21.2

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.
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Error-response helpers, and the guarantee that a 500 does not describe the
3
+ * server's internals to the caller.
4
+ *
5
+ * `parseOrSend` replaced three hand-rolled input-validation shapes across the
6
+ * routes. The parts worth pinning down are the ones a caller can observe: the
7
+ * status, the `errorCode`, whether the route's response shape survives on the
8
+ * error path, and — for the admin routes, which used to forward
9
+ * `(err as Error).message` verbatim — that internal failure text stays server-side.
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=errors.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/errors.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Startup warning for the two conditions that leave the Google RTDN route open.
3
+ *
4
+ * `POST /onesub/webhook/google` verifies the Pub/Sub OIDC token only when an app
5
+ * declares `pushAudience`, and serves any `packageName` when none declares one.
6
+ * The route is mounted either way. Both behaviours are pre-existing and
7
+ * deliberate — a host may front the route with its own verification — so this
8
+ * warns instead of refusing, and these tests pin down that it says so exactly
9
+ * when it should and stays quiet otherwise.
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=google-webhook-open-warning.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-webhook-open-warning.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/google-webhook-open-warning.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
package/dist/errors.d.ts CHANGED
@@ -14,4 +14,38 @@ export declare function sendError(res: Response, status: number, code: OneSubErr
14
14
  * are joined for the human-readable `error` field.
15
15
  */
16
16
  export declare function sendZodError(res: Response, err: z.ZodError, extra?: Record<string, unknown>): void;
17
+ export interface ParseOrSendOptions {
18
+ /**
19
+ * Route-specific fields to merge into the error body, so a 400 keeps the
20
+ * shape the route's success response has (`subscription: null`,
21
+ * `purchases: []`, and so on).
22
+ */
23
+ extra?: Record<string, unknown>;
24
+ /**
25
+ * Send this one message instead of the per-issue zod detail. Use it where the
26
+ * route deliberately does not echo the input shape back — URL params, and the
27
+ * entitlement routes. Omit it to get the field-level detail.
28
+ */
29
+ message?: string;
30
+ }
31
+ /**
32
+ * Parse `input` with `schema`, or send a 400 and return `undefined`.
33
+ *
34
+ * ```ts
35
+ * const body = parseOrSend(res, validateSchema, req.body, { extra: NO_SUB });
36
+ * if (!body) return;
37
+ * ```
38
+ *
39
+ * Every route was hand-rolling this, and in three different shapes: a
40
+ * `try/catch` that re-threw non-zod errors, a `try/catch {}` that swallowed
41
+ * everything into one generic message, and `safeParse`. The first is correct but
42
+ * five lines per call site; the second silently turns a genuine bug inside the
43
+ * schema — a bad refinement, a throwing transform — into "400 bad input", which
44
+ * is the wrong status and hides the cause.
45
+ *
46
+ * This keeps the correct behaviour and makes it the short one: zod failures
47
+ * become a 400, and anything else propagates to the route's own error handling
48
+ * rather than being reported as the caller's fault.
49
+ */
50
+ export declare function parseOrSend<S extends z.ZodType>(res: Response, schema: S, input: unknown, opts?: ParseOrSendOptions): z.output<S> | undefined;
17
51
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAGtD;;;;;;GAMG;AACH,wBAAgB,SAAS,CACvB,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,eAAe,EACrB,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAClC,IAAI,CAEN;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,QAAQ,EACb,GAAG,EAAE,CAAC,CAAC,QAAQ,EACf,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAClC,IAAI,CAQN"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAGtD;;;;;;GAMG;AACH,wBAAgB,SAAS,CACvB,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,eAAe,EACrB,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAClC,IAAI,CAEN;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,QAAQ,EACb,GAAG,EAAE,CAAC,CAAC,QAAQ,EACf,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAClC,IAAI,CAQN;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAC7C,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,CAAC,EACT,KAAK,EAAE,OAAO,EACd,IAAI,GAAE,kBAAuB,GAC5B,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,SAAS,CAUzB"}
package/dist/index.cjs CHANGED
@@ -1278,6 +1278,16 @@ function sendZodError(res, err2, extra = {}) {
1278
1278
  extra
1279
1279
  );
1280
1280
  }
1281
+ function parseOrSend(res, schema, input, opts = {}) {
1282
+ const result = schema.safeParse(input);
1283
+ if (result.success) return result.data;
1284
+ if (opts.message !== void 0) {
1285
+ sendError(res, 400, shared.ONESUB_ERROR_CODE.INVALID_INPUT, opts.message, opts.extra ?? {});
1286
+ } else {
1287
+ sendZodError(res, result.error, opts.extra ?? {});
1288
+ }
1289
+ return void 0;
1290
+ }
1281
1291
 
1282
1292
  // src/apps.ts
1283
1293
  function appName(app) {
@@ -1376,20 +1386,9 @@ function createValidateRouter(config, store) {
1376
1386
  const router = express.Router();
1377
1387
  const registry = getAppRegistry(config);
1378
1388
  router.post(shared.ROUTES.VALIDATE, async (req, res) => {
1379
- let platform;
1380
- let receipt;
1381
- let userId;
1382
- let productId;
1383
- let appId;
1384
- try {
1385
- ({ platform, receipt, userId, productId, appId } = validateSchema.parse(req.body));
1386
- } catch (err2) {
1387
- if (err2 instanceof zod.z.ZodError) {
1388
- sendZodError(res, err2, NO_SUB);
1389
- return;
1390
- }
1391
- throw err2;
1392
- }
1389
+ const body = parseOrSend(res, validateSchema, req.body, { extra: NO_SUB });
1390
+ if (!body) return;
1391
+ const { platform, receipt, userId, productId, appId } = body;
1393
1392
  try {
1394
1393
  let sub = null;
1395
1394
  const appConfig = registry.configFor({
@@ -1752,6 +1751,21 @@ var GOOGLE_FAILURE_MESSAGES = {
1752
1751
  message: "Failed to process notification"
1753
1752
  }
1754
1753
  };
1754
+ function warnIfGoogleWebhookOpen(config) {
1755
+ if (process.env["NODE_ENV"] !== "production") return;
1756
+ const apps = getAppRegistry(config).apps;
1757
+ const googleApps = apps.map((a) => a.google).filter((g) => !!g);
1758
+ if (googleApps.length > 0 && !googleApps.some((g) => g.pushAudience)) {
1759
+ log.warn(
1760
+ "[onesub] SECURITY: POST /onesub/webhook/google accepts unauthenticated requests \u2014 no configured app sets google.pushAudience, so the Pub/Sub OIDC token is never verified. A caller who knows a purchaseToken or orderId can cancel a subscription or delete a one-time purchase. Set google.pushAudience (and google.pushServiceAccountEmail)."
1761
+ );
1762
+ }
1763
+ if (!apps.some((a) => a.google?.packageName)) {
1764
+ log.warn(
1765
+ "[onesub] POST /onesub/webhook/google is in open mode \u2014 no configured app declares google.packageName, so notifications for ANY package are accepted. The route is mounted whether or not Google is configured; set google.packageName, or block the path at your proxy if this deployment does not serve Google Play."
1766
+ );
1767
+ }
1768
+ }
1755
1769
  function googleResolver(config) {
1756
1770
  const registry = getAppRegistry(config);
1757
1771
  const restricted = registry.apps.some((app) => !!app.google?.packageName);
@@ -1998,16 +2012,8 @@ function createPurchaseRouter(config, purchaseStore) {
1998
2012
  const router = express.Router();
1999
2013
  const registry = getAppRegistry(config);
2000
2014
  router.post(shared.ROUTES.VALIDATE_PURCHASE, async (req, res) => {
2001
- let body;
2002
- try {
2003
- body = validatePurchaseSchema.parse(req.body);
2004
- } catch (err2) {
2005
- if (err2 instanceof zod.z.ZodError) {
2006
- sendZodError(res, err2, NO_PURCHASE);
2007
- return;
2008
- }
2009
- throw err2;
2010
- }
2015
+ const body = parseOrSend(res, validatePurchaseSchema, req.body, { extra: NO_PURCHASE });
2016
+ if (!body) return;
2011
2017
  const { platform, receipt, userId, productId, type, appId } = body;
2012
2018
  const appConfig = registry.configFor({
2013
2019
  appId,
@@ -2133,16 +2139,10 @@ function createPurchaseRouter(config, purchaseStore) {
2133
2139
  }
2134
2140
  });
2135
2141
  router.get(shared.ROUTES.PURCHASE_STATUS, async (req, res) => {
2136
- let query;
2137
- try {
2138
- query = purchaseStatusQuerySchema.parse(req.query);
2139
- } catch (err2) {
2140
- if (err2 instanceof zod.z.ZodError) {
2141
- sendZodError(res, err2, { purchases: [] });
2142
- return;
2143
- }
2144
- throw err2;
2145
- }
2142
+ const query = parseOrSend(res, purchaseStatusQuerySchema, req.query, {
2143
+ extra: { purchases: [] }
2144
+ });
2145
+ if (!query) return;
2146
2146
  const { userId, productId } = query;
2147
2147
  try {
2148
2148
  const purchases = productId !== void 0 ? await purchasesForProduct(purchaseStore, userId, productId) : await purchaseStore.getPurchasesByUserId(userId);
@@ -2190,6 +2190,8 @@ async function evaluateEntitlement(userId, entitlement, store, purchaseStore) {
2190
2190
  }
2191
2191
  var userIdSchema = zod.z.string().min(1).max(256);
2192
2192
  var entitlementIdSchema = zod.z.string().min(1).max(128);
2193
+ var entitlementQuerySchema = zod.z.object({ userId: userIdSchema, id: entitlementIdSchema });
2194
+ var entitlementsQuerySchema = zod.z.object({ userId: userIdSchema });
2193
2195
  function createEntitlementRouter(config, store, purchaseStore) {
2194
2196
  if (!config.entitlements || Object.keys(config.entitlements).length === 0) {
2195
2197
  return null;
@@ -2197,15 +2199,11 @@ function createEntitlementRouter(config, store, purchaseStore) {
2197
2199
  const entitlements = config.entitlements;
2198
2200
  const router = express.Router();
2199
2201
  router.get(shared.ROUTES.ENTITLEMENT, async (req, res) => {
2200
- let userId;
2201
- let id;
2202
- try {
2203
- userId = userIdSchema.parse(req.query["userId"]);
2204
- id = entitlementIdSchema.parse(req.query["id"]);
2205
- } catch {
2206
- sendError(res, 400, shared.ONESUB_ERROR_CODE.INVALID_INPUT, "userId and id are required");
2207
- return;
2208
- }
2202
+ const query = parseOrSend(res, entitlementQuerySchema, req.query, {
2203
+ message: "userId and id are required"
2204
+ });
2205
+ if (!query) return;
2206
+ const { userId, id } = query;
2209
2207
  const entitlement = entitlements[id];
2210
2208
  if (!entitlement) {
2211
2209
  sendError(res, 404, shared.ONESUB_ERROR_CODE.ENTITLEMENT_NOT_FOUND, `Unknown entitlement: ${id}`);
@@ -2221,13 +2219,11 @@ function createEntitlementRouter(config, store, purchaseStore) {
2221
2219
  }
2222
2220
  });
2223
2221
  router.get(shared.ROUTES.ENTITLEMENTS, async (req, res) => {
2224
- let userId;
2225
- try {
2226
- userId = userIdSchema.parse(req.query["userId"]);
2227
- } catch {
2228
- sendError(res, 400, shared.ONESUB_ERROR_CODE.INVALID_INPUT, "userId is required");
2229
- return;
2230
- }
2222
+ const query = parseOrSend(res, entitlementsQuerySchema, req.query, {
2223
+ message: "userId is required"
2224
+ });
2225
+ if (!query) return;
2226
+ const { userId } = query;
2231
2227
  try {
2232
2228
  const [subs, purchases] = await Promise.all([
2233
2229
  store.getAllByUserId(userId),
@@ -2276,13 +2272,10 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
2276
2272
  productId: zod.z.string().min(1).max(256)
2277
2273
  });
2278
2274
  router.delete("/onesub/purchase/admin/:userId/:productId", async (req, res) => {
2279
- let params;
2280
- try {
2281
- params = resetParamsSchema.parse(req.params);
2282
- } catch {
2283
- sendError(res, 400, shared.ONESUB_ERROR_CODE.INVALID_INPUT, "userId and productId required");
2284
- return;
2285
- }
2275
+ const params = parseOrSend(res, resetParamsSchema, req.params, {
2276
+ message: "userId and productId required"
2277
+ });
2278
+ if (!params) return;
2286
2279
  const deleted = await purchaseStore.deletePurchases(params.userId, params.productId);
2287
2280
  res.json({ ok: true, deleted });
2288
2281
  });
@@ -2291,16 +2284,8 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
2291
2284
  newUserId: zod.z.string().min(1).max(256)
2292
2285
  });
2293
2286
  router.post("/onesub/purchase/admin/transfer", async (req, res) => {
2294
- let body;
2295
- try {
2296
- body = transferSchema.parse(req.body);
2297
- } catch (err2) {
2298
- if (err2 instanceof zod.z.ZodError) {
2299
- sendZodError(res, err2);
2300
- return;
2301
- }
2302
- throw err2;
2303
- }
2287
+ const body = parseOrSend(res, transferSchema, req.body);
2288
+ if (!body) return;
2304
2289
  const existing = await purchaseStore.getPurchaseByTransactionId(body.transactionId);
2305
2290
  if (!existing) {
2306
2291
  sendError(res, 404, shared.ONESUB_ERROR_CODE.TRANSACTION_NOT_FOUND, "TRANSACTION_NOT_FOUND");
@@ -2322,16 +2307,8 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
2322
2307
  transactionId: zod.z.string().min(1).max(256).optional()
2323
2308
  });
2324
2309
  router.post("/onesub/purchase/admin/grant", async (req, res) => {
2325
- let body;
2326
- try {
2327
- body = grantSchema.parse(req.body);
2328
- } catch (err2) {
2329
- if (err2 instanceof zod.z.ZodError) {
2330
- sendZodError(res, err2);
2331
- return;
2332
- }
2333
- throw err2;
2334
- }
2310
+ const body = parseOrSend(res, grantSchema, req.body);
2311
+ if (!body) return;
2335
2312
  const transactionId = body.transactionId ?? `admin_grant_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
2336
2313
  const purchase = {
2337
2314
  transactionId,
@@ -2363,16 +2340,8 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
2363
2340
  offset: zod.z.coerce.number().int().nonnegative().optional()
2364
2341
  });
2365
2342
  router.get(shared.ROUTES.ADMIN_SUBSCRIPTIONS, async (req, res) => {
2366
- let query;
2367
- try {
2368
- query = listQuerySchema.parse(req.query);
2369
- } catch (err2) {
2370
- if (err2 instanceof zod.z.ZodError) {
2371
- sendZodError(res, err2);
2372
- return;
2373
- }
2374
- throw err2;
2375
- }
2343
+ const query = parseOrSend(res, listQuerySchema, req.query);
2344
+ if (!query) return;
2376
2345
  try {
2377
2346
  const result = await store.listFiltered(query);
2378
2347
  const response = {
@@ -2383,20 +2352,18 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
2383
2352
  };
2384
2353
  res.status(200).json(response);
2385
2354
  } catch (err2) {
2386
- sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, err2.message ?? "list error");
2355
+ log.error("[onesub/admin/subscriptions] list error:", err2);
2356
+ sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
2387
2357
  }
2388
2358
  });
2389
2359
  const detailParamsSchema = zod.z.object({
2390
2360
  transactionId: zod.z.string().min(1).max(256)
2391
2361
  });
2392
2362
  router.get("/onesub/admin/subscriptions/:transactionId", async (req, res) => {
2393
- let params;
2394
- try {
2395
- params = detailParamsSchema.parse(req.params);
2396
- } catch {
2397
- sendError(res, 400, shared.ONESUB_ERROR_CODE.INVALID_INPUT, "transactionId required");
2398
- return;
2399
- }
2363
+ const params = parseOrSend(res, detailParamsSchema, req.params, {
2364
+ message: "transactionId required"
2365
+ });
2366
+ if (!params) return;
2400
2367
  try {
2401
2368
  const sub = await store.getByTransactionId(params.transactionId);
2402
2369
  if (!sub) {
@@ -2405,20 +2372,18 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
2405
2372
  }
2406
2373
  res.status(200).json(sub);
2407
2374
  } catch (err2) {
2408
- sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, err2.message ?? "detail error");
2375
+ log.error("[onesub/admin/subscriptions/:transactionId] detail error:", err2);
2376
+ sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
2409
2377
  }
2410
2378
  });
2411
2379
  const customerParamsSchema = zod.z.object({
2412
2380
  userId: zod.z.string().min(1).max(256)
2413
2381
  });
2414
2382
  router.get("/onesub/admin/customers/:userId", async (req, res) => {
2415
- let params;
2416
- try {
2417
- params = customerParamsSchema.parse(req.params);
2418
- } catch {
2419
- sendError(res, 400, shared.ONESUB_ERROR_CODE.INVALID_INPUT, "userId required");
2420
- return;
2421
- }
2383
+ const params = parseOrSend(res, customerParamsSchema, req.params, {
2384
+ message: "userId required"
2385
+ });
2386
+ if (!params) return;
2422
2387
  try {
2423
2388
  const [subscriptions, purchases] = await Promise.all([
2424
2389
  store.getAllByUserId(params.userId),
@@ -2440,20 +2405,18 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
2440
2405
  };
2441
2406
  res.status(200).json(response);
2442
2407
  } catch (err2) {
2443
- sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, err2.message ?? "customer error");
2408
+ log.error("[onesub/admin/customers/:userId] error:", err2);
2409
+ sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
2444
2410
  }
2445
2411
  });
2446
2412
  const syncAppleParamsSchema = zod.z.object({
2447
2413
  originalTransactionId: zod.z.string().min(1).max(256)
2448
2414
  });
2449
2415
  router.post(shared.ROUTES.ADMIN_SYNC_APPLE, async (req, res) => {
2450
- let params;
2451
- try {
2452
- params = syncAppleParamsSchema.parse(req.params);
2453
- } catch {
2454
- sendError(res, 400, shared.ONESUB_ERROR_CODE.INVALID_INPUT, "originalTransactionId required");
2455
- return;
2456
- }
2416
+ const params = parseOrSend(res, syncAppleParamsSchema, req.params, {
2417
+ message: "originalTransactionId required"
2418
+ });
2419
+ if (!params) return;
2457
2420
  if (!config.apple?.issuerId || !config.apple?.keyId || !config.apple?.privateKey) {
2458
2421
  sendError(res, 400, shared.ONESUB_ERROR_CODE.APPLE_CONFIG_MISSING, "Apple API credentials not configured");
2459
2422
  return;
@@ -2472,7 +2435,8 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
2472
2435
  await store.save(fresh);
2473
2436
  res.status(200).json({ ok: true, subscription: fresh });
2474
2437
  } catch (err2) {
2475
- sendError(res, 500, shared.ONESUB_ERROR_CODE.INTERNAL_ERROR, err2.message ?? "sync error");
2438
+ log.error("[onesub/admin/sync-apple] error:", err2);
2439
+ sendError(res, 500, shared.ONESUB_ERROR_CODE.INTERNAL_ERROR, "Internal server error");
2476
2440
  }
2477
2441
  });
2478
2442
  const overrideParamsSchema = zod.z.object({ userId: zod.z.string().min(1).max(256) });
@@ -2481,19 +2445,10 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
2481
2445
  res.json({ overrides: listTestOverrides() });
2482
2446
  });
2483
2447
  router.put(shared.ROUTES.ADMIN_TEST_OVERRIDE, (req, res) => {
2484
- let params;
2485
- let body;
2486
- try {
2487
- params = overrideParamsSchema.parse(req.params);
2488
- body = overrideBodySchema.parse(req.body);
2489
- } catch (err2) {
2490
- if (err2 instanceof zod.z.ZodError) {
2491
- sendZodError(res, err2);
2492
- } else {
2493
- sendError(res, 400, shared.ONESUB_ERROR_CODE.INVALID_INPUT, "userId and { entitled: boolean } required");
2494
- }
2495
- return;
2496
- }
2448
+ const params = parseOrSend(res, overrideParamsSchema, req.params);
2449
+ if (!params) return;
2450
+ const body = parseOrSend(res, overrideBodySchema, req.body);
2451
+ if (!body) return;
2497
2452
  setTestOverride(params.userId, body.entitled);
2498
2453
  log.warn(
2499
2454
  `[onesub/admin] sandbox test override set for ${params.userId}: entitled=${body.entitled}`
@@ -2501,13 +2456,10 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
2501
2456
  res.json({ ok: true, userId: params.userId, entitled: body.entitled, sandboxOnly: true });
2502
2457
  });
2503
2458
  router.delete(shared.ROUTES.ADMIN_TEST_OVERRIDE, (req, res) => {
2504
- let params;
2505
- try {
2506
- params = overrideParamsSchema.parse(req.params);
2507
- } catch {
2508
- sendError(res, 400, shared.ONESUB_ERROR_CODE.INVALID_INPUT, "userId required");
2509
- return;
2510
- }
2459
+ const params = parseOrSend(res, overrideParamsSchema, req.params, {
2460
+ message: "userId required"
2461
+ });
2462
+ if (!params) return;
2511
2463
  const cleared = clearTestOverride(params.userId);
2512
2464
  res.json({ ok: true, userId: params.userId, cleared });
2513
2465
  });
@@ -2517,25 +2469,22 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
2517
2469
  const items = await webhookQueue.listDeadLetters();
2518
2470
  res.status(200).json({ items });
2519
2471
  } catch (err2) {
2520
- sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, err2.message ?? "list error");
2472
+ log.error("[onesub/admin/webhook-deadletters] error:", err2);
2473
+ sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
2521
2474
  }
2522
2475
  });
2523
2476
  }
2524
2477
  if (webhookQueue?.replayDeadLetter) {
2525
2478
  const replayParamsSchema = zod.z.object({ id: zod.z.string().min(1).max(256) });
2526
2479
  router.post("/onesub/admin/webhook-replay/:id", async (req, res) => {
2527
- let params;
2528
- try {
2529
- params = replayParamsSchema.parse(req.params);
2530
- } catch {
2531
- sendError(res, 400, shared.ONESUB_ERROR_CODE.INVALID_INPUT, "id required");
2532
- return;
2533
- }
2480
+ const params = parseOrSend(res, replayParamsSchema, req.params, { message: "id required" });
2481
+ if (!params) return;
2534
2482
  try {
2535
2483
  await webhookQueue.replayDeadLetter(params.id);
2536
2484
  res.status(200).json({ ok: true });
2537
2485
  } catch (err2) {
2538
- sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, err2.message ?? "replay error");
2486
+ log.error("[onesub/admin/webhook-replay] error:", err2);
2487
+ sendError(res, 500, shared.ONESUB_ERROR_CODE.STORE_ERROR, "Internal server error");
2539
2488
  }
2540
2489
  });
2541
2490
  }
@@ -2794,16 +2743,8 @@ function createAppleOfferRouter(config) {
2794
2743
  return;
2795
2744
  }
2796
2745
  }
2797
- let body;
2798
- try {
2799
- body = offerBodySchema.parse(req.body);
2800
- } catch (err2) {
2801
- if (err2 instanceof zod.z.ZodError) {
2802
- sendZodError(res, err2);
2803
- return;
2804
- }
2805
- throw err2;
2806
- }
2746
+ const body = parseOrSend(res, offerBodySchema, req.body);
2747
+ if (!body) return;
2807
2748
  if (!apple.bundleId) {
2808
2749
  sendError(res, 400, shared.ONESUB_ERROR_CODE.APPLE_CONFIG_MISSING, "config.apple.bundleId is required for offer signing");
2809
2750
  return;
@@ -2820,7 +2761,8 @@ function createAppleOfferRouter(config) {
2820
2761
  );
2821
2762
  res.status(200).json(result);
2822
2763
  } catch (err2) {
2823
- sendError(res, 500, shared.ONESUB_ERROR_CODE.INTERNAL_ERROR, err2.message ?? "Offer signing failed");
2764
+ log.error("[onesub/apple/offer] signing error:", err2);
2765
+ sendError(res, 500, shared.ONESUB_ERROR_CODE.INTERNAL_ERROR, "Offer signing failed");
2824
2766
  }
2825
2767
  });
2826
2768
  return router;
@@ -4222,6 +4164,7 @@ function createOneSubMiddleware(config) {
4222
4164
  "[onesub] apple.mockMode / google.mockMode cannot be enabled when NODE_ENV=production \u2014 these modes accept any receipt as valid."
4223
4165
  );
4224
4166
  }
4167
+ warnIfGoogleWebhookOpen(config);
4225
4168
  const store = config.store ?? new InMemorySubscriptionStore();
4226
4169
  const purchaseStore = config.purchaseStore ?? new InMemoryPurchaseStore();
4227
4170
  if (config.cache) setDefaultCache(config.cache);