@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/README.md +3 -0
- package/dist/__tests__/test-overrides.test.d.ts +15 -0
- package/dist/__tests__/test-overrides.test.d.ts.map +1 -0
- package/dist/index.cjs +110 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +110 -1
- package/dist/index.js.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/providers/apple.d.ts.map +1 -1
- package/dist/routes/admin.d.ts.map +1 -1
- package/dist/routes/validate.d.ts.map +1 -1
- package/dist/test-overrides.d.ts +36 -0
- package/dist/test-overrides.d.ts.map +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -96,6 +96,9 @@ explicit app IDs fail closed and are never validated with another app's credenti
|
|
|
96
96
|
| `GET /onesub/admin/subscriptions/:transactionId` | Get subscription detail (requires `adminSecret`) |
|
|
97
97
|
| `GET /onesub/admin/customers/:userId` | Get a customer profile (requires `adminSecret`) |
|
|
98
98
|
| `POST /onesub/admin/sync-apple/:originalTransactionId` | Refresh a subscription from Apple (requires `adminSecret`) |
|
|
99
|
+
| `GET /onesub/admin/test-overrides` | List sandbox-only entitlement overrides (requires `adminSecret`) |
|
|
100
|
+
| `PUT /onesub/admin/test-overrides/:userId` | Force an entitlement verdict for one user — body `{ "entitled": false }`. Honoured **only for Sandbox receipts**, so production customers are unaffected. Exists because Apple cannot cancel a sandbox subscription bought with a real Apple Account, which otherwise locks a tester out of the purchase flow. Process-local and non-persistent |
|
|
101
|
+
| `DELETE /onesub/admin/test-overrides/:userId` | Clear that user's override (requires `adminSecret`) |
|
|
99
102
|
| `GET /onesub/admin/webhook-deadletters` | List failed webhook jobs in DLQ (requires `adminSecret` + BullMQ) |
|
|
100
103
|
| `POST /onesub/admin/webhook-replay/:id` | Replay a dead-letter job (requires `adminSecret` + BullMQ) |
|
|
101
104
|
| `GET /onesub/entitlement?userId=&id=` | Evaluate one configured entitlement |
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox-only entitlement overrides.
|
|
3
|
+
*
|
|
4
|
+
* Apple cannot cancel a sandbox subscription bought with a real Apple Account
|
|
5
|
+
* through TestFlight, so a tester who subscribes once stays entitled and can
|
|
6
|
+
* never re-run the purchase flow. That is not a hypothetical: it hid a paywall
|
|
7
|
+
* entry point from App Review (PenguinRun 2.0.6, Guideline 2.1(b)) because the
|
|
8
|
+
* reviewer had subscribed in an earlier round.
|
|
9
|
+
*
|
|
10
|
+
* The guarantee under test is the safety one — an override must be incapable of
|
|
11
|
+
* touching a paying customer. It is honoured only when the receipt just
|
|
12
|
+
* validated came from Sandbox.
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=test-overrides.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-overrides.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/test-overrides.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
|
package/dist/index.cjs
CHANGED
|
@@ -458,7 +458,10 @@ async function validateAppleReceipt(receipt, config) {
|
|
|
458
458
|
purchasedAt: new Date(purchasedAt).toISOString(),
|
|
459
459
|
willRenew: status === shared.SUBSCRIPTION_STATUS.ACTIVE,
|
|
460
460
|
// refined by renewal info in webhook
|
|
461
|
-
...appAccountToken ? { boundAccountId: appAccountToken } : {}
|
|
461
|
+
...appAccountToken ? { boundAccountId: appAccountToken } : {},
|
|
462
|
+
// Transient, like boundAccountId: the validate route uses it to decide
|
|
463
|
+
// whether a sandbox-only test override may apply, then strips it.
|
|
464
|
+
...tx.environment === "Sandbox" ? { sandbox: true } : {}
|
|
462
465
|
};
|
|
463
466
|
}
|
|
464
467
|
async function validateAppleConsumableReceipt(signedTransaction, config, expectedProductId) {
|
|
@@ -1262,6 +1265,21 @@ function peekAppleBundleId(jws) {
|
|
|
1262
1265
|
}
|
|
1263
1266
|
}
|
|
1264
1267
|
|
|
1268
|
+
// src/test-overrides.ts
|
|
1269
|
+
var overrides = /* @__PURE__ */ new Map();
|
|
1270
|
+
function setTestOverride(userId, entitled) {
|
|
1271
|
+
overrides.set(userId, entitled);
|
|
1272
|
+
}
|
|
1273
|
+
function clearTestOverride(userId) {
|
|
1274
|
+
return overrides.delete(userId);
|
|
1275
|
+
}
|
|
1276
|
+
function getTestOverride(userId) {
|
|
1277
|
+
return overrides.get(userId);
|
|
1278
|
+
}
|
|
1279
|
+
function listTestOverrides() {
|
|
1280
|
+
return [...overrides].map(([userId, entitled]) => ({ userId, entitled }));
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1265
1283
|
// src/routes/validate.ts
|
|
1266
1284
|
var NO_SUB = { valid: false, subscription: null };
|
|
1267
1285
|
var validateSchema = zod.z.object({
|
|
@@ -1329,6 +1347,13 @@ function createValidateRouter(config, store) {
|
|
|
1329
1347
|
);
|
|
1330
1348
|
return;
|
|
1331
1349
|
}
|
|
1350
|
+
const isSandbox = sub.sandbox === true;
|
|
1351
|
+
delete sub.sandbox;
|
|
1352
|
+
if (isSandbox && getTestOverride(userId) === false) {
|
|
1353
|
+
log.warn(`[onesub/validate] sandbox test override active for ${userId}: forcing not-entitled`);
|
|
1354
|
+
sub.status = shared.SUBSCRIPTION_STATUS.EXPIRED;
|
|
1355
|
+
sub.willRenew = false;
|
|
1356
|
+
}
|
|
1332
1357
|
sub.userId = userId;
|
|
1333
1358
|
await store.save(sub);
|
|
1334
1359
|
if (platform === "google" && appConfig.google) {
|
|
@@ -2358,6 +2383,42 @@ function createAdminRouter(config, purchaseStore, store, webhookQueue) {
|
|
|
2358
2383
|
sendError(res, 500, shared.ONESUB_ERROR_CODE.INTERNAL_ERROR, err2.message ?? "sync error");
|
|
2359
2384
|
}
|
|
2360
2385
|
});
|
|
2386
|
+
const overrideParamsSchema = zod.z.object({ userId: zod.z.string().min(1).max(256) });
|
|
2387
|
+
const overrideBodySchema = zod.z.object({ entitled: zod.z.boolean() });
|
|
2388
|
+
router.get(shared.ROUTES.ADMIN_TEST_OVERRIDES, (_req, res) => {
|
|
2389
|
+
res.json({ overrides: listTestOverrides() });
|
|
2390
|
+
});
|
|
2391
|
+
router.put(shared.ROUTES.ADMIN_TEST_OVERRIDE, (req, res) => {
|
|
2392
|
+
let params;
|
|
2393
|
+
let body;
|
|
2394
|
+
try {
|
|
2395
|
+
params = overrideParamsSchema.parse(req.params);
|
|
2396
|
+
body = overrideBodySchema.parse(req.body);
|
|
2397
|
+
} catch (err2) {
|
|
2398
|
+
if (err2 instanceof zod.z.ZodError) {
|
|
2399
|
+
sendZodError(res, err2);
|
|
2400
|
+
} else {
|
|
2401
|
+
sendError(res, 400, shared.ONESUB_ERROR_CODE.INVALID_INPUT, "userId and { entitled: boolean } required");
|
|
2402
|
+
}
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
setTestOverride(params.userId, body.entitled);
|
|
2406
|
+
log.warn(
|
|
2407
|
+
`[onesub/admin] sandbox test override set for ${params.userId}: entitled=${body.entitled}`
|
|
2408
|
+
);
|
|
2409
|
+
res.json({ ok: true, userId: params.userId, entitled: body.entitled, sandboxOnly: true });
|
|
2410
|
+
});
|
|
2411
|
+
router.delete(shared.ROUTES.ADMIN_TEST_OVERRIDE, (req, res) => {
|
|
2412
|
+
let params;
|
|
2413
|
+
try {
|
|
2414
|
+
params = overrideParamsSchema.parse(req.params);
|
|
2415
|
+
} catch {
|
|
2416
|
+
sendError(res, 400, shared.ONESUB_ERROR_CODE.INVALID_INPUT, "userId required");
|
|
2417
|
+
return;
|
|
2418
|
+
}
|
|
2419
|
+
const cleared = clearTestOverride(params.userId);
|
|
2420
|
+
res.json({ ok: true, userId: params.userId, cleared });
|
|
2421
|
+
});
|
|
2361
2422
|
if (webhookQueue?.listDeadLetters) {
|
|
2362
2423
|
router.get("/onesub/admin/webhook-deadletters", async (_req, res) => {
|
|
2363
2424
|
try {
|
|
@@ -3521,6 +3582,54 @@ var ONESUB_OPENAPI = {
|
|
|
3521
3582
|
}
|
|
3522
3583
|
},
|
|
3523
3584
|
// ── admin (mounted when config.adminSecret is set) ───────────────────────
|
|
3585
|
+
[shared.ROUTES.ADMIN_TEST_OVERRIDES]: {
|
|
3586
|
+
get: {
|
|
3587
|
+
summary: "List sandbox-only entitlement overrides (admin).",
|
|
3588
|
+
parameters: [ADMIN_SECRET_PARAM],
|
|
3589
|
+
responses: {
|
|
3590
|
+
200: { description: "OK" },
|
|
3591
|
+
401: err("Invalid admin secret (INVALID_ADMIN_SECRET).")
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
},
|
|
3595
|
+
"/onesub/admin/test-overrides/{userId}": {
|
|
3596
|
+
put: {
|
|
3597
|
+
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.",
|
|
3598
|
+
parameters: [
|
|
3599
|
+
ADMIN_SECRET_PARAM,
|
|
3600
|
+
{ in: "path", name: "userId", required: true, schema: { type: "string" } }
|
|
3601
|
+
],
|
|
3602
|
+
requestBody: {
|
|
3603
|
+
required: true,
|
|
3604
|
+
content: {
|
|
3605
|
+
"application/json": {
|
|
3606
|
+
schema: {
|
|
3607
|
+
type: "object",
|
|
3608
|
+
required: ["entitled"],
|
|
3609
|
+
properties: { entitled: { type: "boolean" } }
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
},
|
|
3614
|
+
responses: {
|
|
3615
|
+
200: { description: "OK" },
|
|
3616
|
+
400: err("Invalid input (INVALID_INPUT)."),
|
|
3617
|
+
401: err("Invalid admin secret (INVALID_ADMIN_SECRET).")
|
|
3618
|
+
}
|
|
3619
|
+
},
|
|
3620
|
+
delete: {
|
|
3621
|
+
summary: "Clear the sandbox entitlement override for one userId (admin).",
|
|
3622
|
+
parameters: [
|
|
3623
|
+
ADMIN_SECRET_PARAM,
|
|
3624
|
+
{ in: "path", name: "userId", required: true, schema: { type: "string" } }
|
|
3625
|
+
],
|
|
3626
|
+
responses: {
|
|
3627
|
+
200: { description: "OK" },
|
|
3628
|
+
400: err("Invalid input (INVALID_INPUT)."),
|
|
3629
|
+
401: err("Invalid admin secret (INVALID_ADMIN_SECRET).")
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
},
|
|
3524
3633
|
[shared.ROUTES.ADMIN_SUBSCRIPTIONS]: {
|
|
3525
3634
|
get: {
|
|
3526
3635
|
summary: "Filtered, paginated subscription list (admin).",
|