@onesub/server 0.18.3 → 0.19.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/index.js CHANGED
@@ -451,7 +451,10 @@ async function validateAppleReceipt(receipt, config) {
451
451
  purchasedAt: new Date(purchasedAt).toISOString(),
452
452
  willRenew: status === SUBSCRIPTION_STATUS.ACTIVE,
453
453
  // refined by renewal info in webhook
454
- ...appAccountToken ? { boundAccountId: appAccountToken } : {}
454
+ ...appAccountToken ? { boundAccountId: appAccountToken } : {},
455
+ // Transient, like boundAccountId: the validate route uses it to decide
456
+ // whether a sandbox-only test override may apply, then strips it.
457
+ ...tx.environment === "Sandbox" ? { sandbox: true } : {}
455
458
  };
456
459
  }
457
460
  async function validateAppleConsumableReceipt(signedTransaction, config, expectedProductId) {
@@ -1255,6 +1258,21 @@ function peekAppleBundleId(jws) {
1255
1258
  }
1256
1259
  }
1257
1260
 
1261
+ // src/test-overrides.ts
1262
+ var overrides = /* @__PURE__ */ new Map();
1263
+ function setTestOverride(userId, entitled) {
1264
+ overrides.set(userId, entitled);
1265
+ }
1266
+ function clearTestOverride(userId) {
1267
+ return overrides.delete(userId);
1268
+ }
1269
+ function getTestOverride(userId) {
1270
+ return overrides.get(userId);
1271
+ }
1272
+ function listTestOverrides() {
1273
+ return [...overrides].map(([userId, entitled]) => ({ userId, entitled }));
1274
+ }
1275
+
1258
1276
  // src/routes/validate.ts
1259
1277
  var NO_SUB = { valid: false, subscription: null };
1260
1278
  var validateSchema = z.object({
@@ -1322,6 +1340,13 @@ function createValidateRouter(config, store) {
1322
1340
  );
1323
1341
  return;
1324
1342
  }
1343
+ const isSandbox = sub.sandbox === true;
1344
+ delete sub.sandbox;
1345
+ if (isSandbox && getTestOverride(userId) === false) {
1346
+ log.warn(`[onesub/validate] sandbox test override active for ${userId}: forcing not-entitled`);
1347
+ sub.status = SUBSCRIPTION_STATUS.EXPIRED;
1348
+ sub.willRenew = false;
1349
+ }
1325
1350
  sub.userId = userId;
1326
1351
  await store.save(sub);
1327
1352
  if (platform === "google" && appConfig.google) {
@@ -2351,6 +2376,42 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
2351
2376
  sendError(res, 500, ONESUB_ERROR_CODE.INTERNAL_ERROR, err2.message ?? "sync error");
2352
2377
  }
2353
2378
  });
2379
+ const overrideParamsSchema = z.object({ userId: z.string().min(1).max(256) });
2380
+ const overrideBodySchema = z.object({ entitled: z.boolean() });
2381
+ router.get(ROUTES.ADMIN_TEST_OVERRIDES, (_req, res) => {
2382
+ res.json({ overrides: listTestOverrides() });
2383
+ });
2384
+ router.put(ROUTES.ADMIN_TEST_OVERRIDE, (req, res) => {
2385
+ let params;
2386
+ let body;
2387
+ try {
2388
+ params = overrideParamsSchema.parse(req.params);
2389
+ body = overrideBodySchema.parse(req.body);
2390
+ } catch (err2) {
2391
+ if (err2 instanceof z.ZodError) {
2392
+ sendZodError(res, err2);
2393
+ } else {
2394
+ sendError(res, 400, ONESUB_ERROR_CODE.INVALID_INPUT, "userId and { entitled: boolean } required");
2395
+ }
2396
+ return;
2397
+ }
2398
+ setTestOverride(params.userId, body.entitled);
2399
+ log.warn(
2400
+ `[onesub/admin] sandbox test override set for ${params.userId}: entitled=${body.entitled}`
2401
+ );
2402
+ res.json({ ok: true, userId: params.userId, entitled: body.entitled, sandboxOnly: true });
2403
+ });
2404
+ router.delete(ROUTES.ADMIN_TEST_OVERRIDE, (req, res) => {
2405
+ let params;
2406
+ try {
2407
+ params = overrideParamsSchema.parse(req.params);
2408
+ } catch {
2409
+ sendError(res, 400, ONESUB_ERROR_CODE.INVALID_INPUT, "userId required");
2410
+ return;
2411
+ }
2412
+ const cleared = clearTestOverride(params.userId);
2413
+ res.json({ ok: true, userId: params.userId, cleared });
2414
+ });
2354
2415
  if (webhookQueue?.listDeadLetters) {
2355
2416
  router.get("/onesub/admin/webhook-deadletters", async (_req, res) => {
2356
2417
  try {
@@ -3514,6 +3575,54 @@ var ONESUB_OPENAPI = {
3514
3575
  }
3515
3576
  },
3516
3577
  // ── admin (mounted when config.adminSecret is set) ───────────────────────
3578
+ [ROUTES.ADMIN_TEST_OVERRIDES]: {
3579
+ get: {
3580
+ summary: "List sandbox-only entitlement overrides (admin).",
3581
+ parameters: [ADMIN_SECRET_PARAM],
3582
+ responses: {
3583
+ 200: { description: "OK" },
3584
+ 401: err("Invalid admin secret (INVALID_ADMIN_SECRET).")
3585
+ }
3586
+ }
3587
+ },
3588
+ "/onesub/admin/test-overrides/{userId}": {
3589
+ put: {
3590
+ summary: "Force an entitlement verdict for one userId. Honoured ONLY for Sandbox receipts \u2014 a Production receipt ignores it. Exists because Apple cannot cancel a sandbox subscription bought with a real Apple Account, which otherwise locks a tester in.",
3591
+ parameters: [
3592
+ ADMIN_SECRET_PARAM,
3593
+ { in: "path", name: "userId", required: true, schema: { type: "string" } }
3594
+ ],
3595
+ requestBody: {
3596
+ required: true,
3597
+ content: {
3598
+ "application/json": {
3599
+ schema: {
3600
+ type: "object",
3601
+ required: ["entitled"],
3602
+ properties: { entitled: { type: "boolean" } }
3603
+ }
3604
+ }
3605
+ }
3606
+ },
3607
+ responses: {
3608
+ 200: { description: "OK" },
3609
+ 400: err("Invalid input (INVALID_INPUT)."),
3610
+ 401: err("Invalid admin secret (INVALID_ADMIN_SECRET).")
3611
+ }
3612
+ },
3613
+ delete: {
3614
+ summary: "Clear the sandbox entitlement override for one userId (admin).",
3615
+ parameters: [
3616
+ ADMIN_SECRET_PARAM,
3617
+ { in: "path", name: "userId", required: true, schema: { type: "string" } }
3618
+ ],
3619
+ responses: {
3620
+ 200: { description: "OK" },
3621
+ 400: err("Invalid input (INVALID_INPUT)."),
3622
+ 401: err("Invalid admin secret (INVALID_ADMIN_SECRET).")
3623
+ }
3624
+ }
3625
+ },
3517
3626
  [ROUTES.ADMIN_SUBSCRIPTIONS]: {
3518
3627
  get: {
3519
3628
  summary: "Filtered, paginated subscription list (admin).",