@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/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 +136 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +136 -6
- 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/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)
|
|
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
|
-
...
|
|
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)
|
|
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
|
|
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).",
|