@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.
- package/dist/__tests__/errors.test.d.ts +12 -0
- package/dist/__tests__/errors.test.d.ts.map +1 -0
- package/dist/__tests__/google-webhook-open-warning.test.d.ts +12 -0
- package/dist/__tests__/google-webhook-open-warning.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 +95 -152
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +95 -152
- 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/routes/webhook-google.d.ts +18 -0
- package/dist/routes/webhook-google.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -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
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
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
|
-
|
|
2002
|
-
|
|
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
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
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
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
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
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
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
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
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
|
-
|
|
2295
|
-
|
|
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
|
-
|
|
2326
|
-
|
|
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
|
-
|
|
2367
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
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
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2528
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2798
|
-
|
|
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
|
-
|
|
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);
|