fix: hide exact quota values from account response #16

Merged
sirily merged 4 commits from fix/account-quota-contract into master 2026-03-10 15:52:16 +03:00
6 changed files with 142 additions and 64 deletions
Showing only changes of commit b23612ef23 - Show all commits

View File

@@ -44,3 +44,6 @@ This repository is a TypeScript monorepo for `nproxy`, a crypto-subscription ima
- If you change deployment assumptions, update `docs/ops/deployment.md`. - If you change deployment assumptions, update `docs/ops/deployment.md`.
- If you change Telegram admin auth, update `docs/ops/telegram-pairing.md`. - If you change Telegram admin auth, update `docs/ops/telegram-pairing.md`.
- If you change failover, cooldown, or balance logic, update `docs/ops/provider-key-pool.md`. - If you change failover, cooldown, or balance logic, update `docs/ops/provider-key-pool.md`.
## Workflow
- At the end of each completed task, create a PR for the changes unless the user explicitly says not to.

View File

@@ -5,6 +5,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "tsc -p tsconfig.json", "build": "tsc -p tsconfig.json",
"test": "node --test dist/**/*.test.js",
"start": "node dist/main.js" "start": "node dist/main.js"
}, },
"dependencies": { "dependencies": {

View File

@@ -0,0 +1,65 @@
import test from "node:test";
import assert from "node:assert/strict";
import { serializePublicAccountOverview } from "./account-response.js";
test("serializePublicAccountOverview exposes only approximate quota fields", () => {
const response = serializePublicAccountOverview({
user: {
id: "user_1",
email: "user@example.com",
isAdmin: false,
createdAt: new Date("2026-03-10T12:00:00.000Z"),
},
subscription: {
id: "sub_1",
status: "active",
renewsManually: true,
activatedAt: new Date("2026-03-10T12:00:00.000Z"),
currentPeriodStart: new Date("2026-03-10T12:00:00.000Z"),
currentPeriodEnd: new Date("2026-04-09T12:00:00.000Z"),
plan: {
id: "plan_1",
code: "basic",
displayName: "Basic",
monthlyPriceUsd: 29,
billingCurrency: "USDT",
isActive: true,
},
},
quota: {
approximateBucket: 80,
},
});
assert.deepEqual(response, {
user: {
id: "user_1",
email: "user@example.com",
isAdmin: false,
createdAt: "2026-03-10T12:00:00.000Z",
},
subscription: {
id: "sub_1",
status: "active",
renewsManually: true,
activatedAt: "2026-03-10T12:00:00.000Z",
currentPeriodStart: "2026-03-10T12:00:00.000Z",
currentPeriodEnd: "2026-04-09T12:00:00.000Z",
plan: {
id: "plan_1",
code: "basic",
displayName: "Basic",
monthlyPriceUsd: 29,
billingCurrency: "USDT",
isActive: true,
},
},
quota: {
approximateBucket: 80,
},
});
assert.equal("usedSuccessfulRequests" in (response.quota ?? {}), false);
assert.equal("monthlyRequestLimit" in (response.quota ?? {}), false);
assert.equal("monthlyRequestLimit" in (response.subscription?.plan ?? {}), false);
});

View File

@@ -0,0 +1,71 @@
export interface PublicAccountOverviewLike {
user: {
id: string;
email: string;
isAdmin: boolean;
createdAt: Date;
};
subscription: {
id: string;
status: string;
renewsManually: boolean;
activatedAt?: Date;
currentPeriodStart?: Date;
currentPeriodEnd?: Date;
canceledAt?: Date;
plan: {
id: string;
code: string;
displayName: string;
monthlyPriceUsd: number;
billingCurrency: string;
isActive: boolean;
};
} | null;
quota: {
approximateBucket: number;
} | null;
}
export function serializePublicAccountOverview(overview: PublicAccountOverviewLike) {
return {
user: {
id: overview.user.id,
email: overview.user.email,
isAdmin: overview.user.isAdmin,
createdAt: overview.user.createdAt.toISOString(),
},
subscription: overview.subscription
? {
id: overview.subscription.id,
status: overview.subscription.status,
renewsManually: overview.subscription.renewsManually,
...(overview.subscription.activatedAt
? { activatedAt: overview.subscription.activatedAt.toISOString() }
: {}),
...(overview.subscription.currentPeriodStart
? { currentPeriodStart: overview.subscription.currentPeriodStart.toISOString() }
: {}),
...(overview.subscription.currentPeriodEnd
? { currentPeriodEnd: overview.subscription.currentPeriodEnd.toISOString() }
: {}),
...(overview.subscription.canceledAt
? { canceledAt: overview.subscription.canceledAt.toISOString() }
: {}),
plan: {
id: overview.subscription.plan.id,
code: overview.subscription.plan.code,
displayName: overview.subscription.plan.displayName,
monthlyPriceUsd: overview.subscription.plan.monthlyPriceUsd,
billingCurrency: overview.subscription.plan.billingCurrency,
isActive: overview.subscription.plan.isActive,
},
}
: null,
quota: overview.quota
? {
approximateBucket: overview.quota.approximateBucket,
}
: null,
};
}

View File

@@ -18,6 +18,7 @@ import {
createGenerationRequest, createGenerationRequest,
type CreateGenerationRequestInput, type CreateGenerationRequestInput,
} from "@nproxy/domain"; } from "@nproxy/domain";
import { serializePublicAccountOverview } from "./account-response.js";
const config = loadConfig(); const config = loadConfig();
const port = Number.parseInt(process.env.PORT ?? "3000", 10); const port = Number.parseInt(process.env.PORT ?? "3000", 10);
@@ -159,7 +160,7 @@ const server = createServer(async (request, response) => {
return; return;
} }
sendJson(response, 200, serializeAccountOverview(overview)); sendJson(response, 200, serializePublicAccountOverview(overview));
return; return;
} }
@@ -493,63 +494,6 @@ function serializeUserSession(
}; };
} }
function serializeAccountOverview(overview: {
user: {
id: string;
email: string;
isAdmin: boolean;
createdAt: Date;
};
subscription: {
id: string;
status: string;
renewsManually: boolean;
activatedAt?: Date;
currentPeriodStart?: Date;
currentPeriodEnd?: Date;
canceledAt?: Date;
plan: {
id: string;
code: string;
displayName: string;
monthlyRequestLimit: number;
monthlyPriceUsd: number;
billingCurrency: string;
isActive: boolean;
};
} | null;
quota: {
approximateBucket: number;
usedSuccessfulRequests: number;
monthlyRequestLimit: number;
} | null;
}) {
return {
user: serializeAuthenticatedUser(overview.user),
subscription: overview.subscription
? {
id: overview.subscription.id,
status: overview.subscription.status,
renewsManually: overview.subscription.renewsManually,
...(overview.subscription.activatedAt
? { activatedAt: overview.subscription.activatedAt.toISOString() }
: {}),
...(overview.subscription.currentPeriodStart
? { currentPeriodStart: overview.subscription.currentPeriodStart.toISOString() }
: {}),
...(overview.subscription.currentPeriodEnd
? { currentPeriodEnd: overview.subscription.currentPeriodEnd.toISOString() }
: {}),
...(overview.subscription.canceledAt
? { canceledAt: overview.subscription.canceledAt.toISOString() }
: {}),
plan: overview.subscription.plan,
}
: null,
quota: overview.quota,
};
}
function serializeBillingInvoice(invoice: { function serializeBillingInvoice(invoice: {
id: string; id: string;
subscriptionId?: string; subscriptionId?: string;

View File

@@ -22,7 +22,6 @@ export interface UserAccountOverview {
id: string; id: string;
code: string; code: string;
displayName: string; displayName: string;
monthlyRequestLimit: number;
monthlyPriceUsd: number; monthlyPriceUsd: number;
billingCurrency: string; billingCurrency: string;
isActive: boolean; isActive: boolean;
@@ -30,8 +29,6 @@ export interface UserAccountOverview {
} | null; } | null;
quota: { quota: {
approximateBucket: QuotaBucket; approximateBucket: QuotaBucket;
usedSuccessfulRequests: number;
monthlyRequestLimit: number;
} | null; } | null;
} }
@@ -95,7 +92,6 @@ export function createPrismaAccountStore(database: PrismaClient = defaultPrisma)
id: subscription.plan.id, id: subscription.plan.id,
code: subscription.plan.code, code: subscription.plan.code,
displayName: subscription.plan.displayName, displayName: subscription.plan.displayName,
monthlyRequestLimit: subscription.plan.monthlyRequestLimit,
monthlyPriceUsd: decimalToNumber(subscription.plan.monthlyPriceUsd), monthlyPriceUsd: decimalToNumber(subscription.plan.monthlyPriceUsd),
billingCurrency: subscription.plan.billingCurrency, billingCurrency: subscription.plan.billingCurrency,
isActive: subscription.plan.isActive, isActive: subscription.plan.isActive,
@@ -140,7 +136,5 @@ async function buildQuotaSnapshot(
used: usedSuccessfulRequests, used: usedSuccessfulRequests,
limit: input.monthlyRequestLimit, limit: input.monthlyRequestLimit,
}), }),
usedSuccessfulRequests,
monthlyRequestLimit: input.monthlyRequestLimit,
}; };
} }