@onesub/server 0.18.2 → 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
@@ -325,6 +325,10 @@ function mockValidateGoogleProduct(receipt, productId) {
325
325
  }
326
326
 
327
327
  // src/providers/apple.ts
328
+ var EMPTY_APP_ACCOUNT_TOKEN = "00000000-0000-0000-0000-000000000000";
329
+ function normalizeAppleAppAccountToken(token) {
330
+ return token === EMPTY_APP_ACCOUNT_TOKEN ? void 0 : token;
331
+ }
328
332
  function derBase64ToPem(der) {
329
333
  return "-----BEGIN CERTIFICATE-----\n" + (der.match(/.{1,64}/g) ?? []).join("\n") + "\n-----END CERTIFICATE-----";
330
334
  }
@@ -400,7 +404,15 @@ function deriveStatus(tx, renewal) {
400
404
  return SUBSCRIPTION_STATUS.EXPIRED;
401
405
  }
402
406
  async function validateAppleReceipt(receipt, config) {
403
- if (config.mockMode) return mockValidateAppleSubscription(receipt);
407
+ if (config.mockMode) {
408
+ const mock = mockValidateAppleSubscription(receipt);
409
+ if (mock) {
410
+ const boundAccountId = normalizeAppleAppAccountToken(mock.boundAccountId);
411
+ if (boundAccountId) mock.boundAccountId = boundAccountId;
412
+ else delete mock.boundAccountId;
413
+ }
414
+ return mock;
415
+ }
404
416
  let tx;
405
417
  try {
406
418
  tx = await decodeJws(receipt, config.skipJwsVerification);
@@ -427,6 +439,7 @@ async function validateAppleReceipt(receipt, config) {
427
439
  }
428
440
  const status = deriveStatus(tx, null);
429
441
  const purchasedAt = tx.originalPurchaseDate ?? tx.purchaseDate ?? Date.now();
442
+ const appAccountToken = normalizeAppleAppAccountToken(tx.appAccountToken);
430
443
  return {
431
444
  userId: "",
432
445
  // caller fills this in from the request body
@@ -438,11 +451,22 @@ async function validateAppleReceipt(receipt, config) {
438
451
  purchasedAt: new Date(purchasedAt).toISOString(),
439
452
  willRenew: status === SUBSCRIPTION_STATUS.ACTIVE,
440
453
  // refined by renewal info in webhook
441
- ...tx.appAccountToken ? { boundAccountId: tx.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 } : {}
442
458
  };
443
459
  }
444
460
  async function validateAppleConsumableReceipt(signedTransaction, config, expectedProductId) {
445
- if (config.mockMode) return mockValidateAppleProduct(signedTransaction, expectedProductId);
461
+ if (config.mockMode) {
462
+ const mock = mockValidateAppleProduct(signedTransaction, expectedProductId);
463
+ if (mock) {
464
+ const appAccountToken = normalizeAppleAppAccountToken(mock.appAccountToken);
465
+ if (appAccountToken) mock.appAccountToken = appAccountToken;
466
+ else delete mock.appAccountToken;
467
+ }
468
+ return mock;
469
+ }
446
470
  let tx;
447
471
  try {
448
472
  tx = await decodeJws(signedTransaction, config.skipJwsVerification);
@@ -495,7 +519,7 @@ async function validateAppleConsumableReceipt(signedTransaction, config, expecte
495
519
  transactionId,
496
520
  productId: tx.productId,
497
521
  purchasedAt: tx.purchaseDate ? new Date(tx.purchaseDate).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
498
- appAccountToken: tx.appAccountToken ?? void 0
522
+ appAccountToken: normalizeAppleAppAccountToken(tx.appAccountToken)
499
523
  };
500
524
  }
501
525
  async function decodeAppleNotification(payload, skipJwsVerification = false) {
@@ -527,7 +551,7 @@ async function decodeAppleNotification(payload, skipJwsVerification = false) {
527
551
  status,
528
552
  willRenew,
529
553
  expiresAt: tx.expiresDate ? new Date(tx.expiresDate).toISOString() : null,
530
- appAccountToken: tx.appAccountToken ?? null,
554
+ appAccountToken: normalizeAppleAppAccountToken(tx.appAccountToken) ?? null,
531
555
  inAppOwnershipType: tx.inAppOwnershipType ?? null
532
556
  };
533
557
  }
@@ -717,7 +741,7 @@ async function fetchAppleTransactionHistory(originalTransactionId, config, optio
717
741
  type: tx.type ?? "Unknown",
718
742
  purchasedAt: tx.purchaseDate ? new Date(tx.purchaseDate).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
719
743
  expiresAt: tx.expiresDate ? new Date(tx.expiresDate).toISOString() : null,
720
- appAccountToken: tx.appAccountToken ?? null,
744
+ appAccountToken: normalizeAppleAppAccountToken(tx.appAccountToken) ?? null,
721
745
  inAppOwnershipType: tx.inAppOwnershipType ?? null,
722
746
  revocationDate: tx.revocationDate ? new Date(tx.revocationDate).toISOString() : null
723
747
  });
@@ -1234,6 +1258,21 @@ function peekAppleBundleId(jws) {
1234
1258
  }
1235
1259
  }
1236
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
+
1237
1276
  // src/routes/validate.ts
1238
1277
  var NO_SUB = { valid: false, subscription: null };
1239
1278
  var validateSchema = z.object({
@@ -1301,6 +1340,13 @@ function createValidateRouter(config, store) {
1301
1340
  );
1302
1341
  return;
1303
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
+ }
1304
1350
  sub.userId = userId;
1305
1351
  await store.save(sub);
1306
1352
  if (platform === "google" && appConfig.google) {
@@ -2330,6 +2376,42 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
2330
2376
  sendError(res, 500, ONESUB_ERROR_CODE.INTERNAL_ERROR, err2.message ?? "sync error");
2331
2377
  }
2332
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
+ });
2333
2415
  if (webhookQueue?.listDeadLetters) {
2334
2416
  router.get("/onesub/admin/webhook-deadletters", async (_req, res) => {
2335
2417
  try {
@@ -3493,6 +3575,54 @@ var ONESUB_OPENAPI = {
3493
3575
  }
3494
3576
  },
3495
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
+ },
3496
3626
  [ROUTES.ADMIN_SUBSCRIPTIONS]: {
3497
3627
  get: {
3498
3628
  summary: "Filtered, paginated subscription list (admin).",