diff --git a/AGENTS.md b/AGENTS.md index 4247117..6e5d43c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 Telegram admin auth, update `docs/ops/telegram-pairing.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. diff --git a/apps/web/package.json b/apps/web/package.json index 038d206..50b79be 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "build": "tsc -p tsconfig.json", + "test": "node --test dist/**/*.test.js", "start": "node dist/main.js" }, "dependencies": { diff --git a/apps/web/src/account-response.test.ts b/apps/web/src/account-response.test.ts new file mode 100644 index 0000000..ff56082 --- /dev/null +++ b/apps/web/src/account-response.test.ts @@ -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); +}); diff --git a/apps/web/src/account-response.ts b/apps/web/src/account-response.ts new file mode 100644 index 0000000..255eea9 --- /dev/null +++ b/apps/web/src/account-response.ts @@ -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, + }; +} diff --git a/apps/web/src/main.ts b/apps/web/src/main.ts index 8355fa4..2fa1422 100644 --- a/apps/web/src/main.ts +++ b/apps/web/src/main.ts @@ -18,6 +18,7 @@ import { createGenerationRequest, type CreateGenerationRequestInput, } from "@nproxy/domain"; +import { serializePublicAccountOverview } from "./account-response.js"; const config = loadConfig(); const port = Number.parseInt(process.env.PORT ?? "3000", 10); @@ -159,7 +160,7 @@ const server = createServer(async (request, response) => { return; } - sendJson(response, 200, serializeAccountOverview(overview)); + sendJson(response, 200, serializePublicAccountOverview(overview)); 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: { id: string; subscriptionId?: string; diff --git a/packages/db/src/account-store.ts b/packages/db/src/account-store.ts index ce8d8b8..f0880b4 100644 --- a/packages/db/src/account-store.ts +++ b/packages/db/src/account-store.ts @@ -22,7 +22,6 @@ export interface UserAccountOverview { id: string; code: string; displayName: string; - monthlyRequestLimit: number; monthlyPriceUsd: number; billingCurrency: string; isActive: boolean; @@ -30,8 +29,6 @@ export interface UserAccountOverview { } | null; quota: { approximateBucket: QuotaBucket; - usedSuccessfulRequests: number; - monthlyRequestLimit: number; } | null; } @@ -95,7 +92,6 @@ export function createPrismaAccountStore(database: PrismaClient = defaultPrisma) id: subscription.plan.id, code: subscription.plan.code, displayName: subscription.plan.displayName, - monthlyRequestLimit: subscription.plan.monthlyRequestLimit, monthlyPriceUsd: decimalToNumber(subscription.plan.monthlyPriceUsd), billingCurrency: subscription.plan.billingCurrency, isActive: subscription.plan.isActive, @@ -140,7 +136,5 @@ async function buildQuotaSnapshot( used: usedSuccessfulRequests, limit: input.monthlyRequestLimit, }), - usedSuccessfulRequests, - monthlyRequestLimit: input.monthlyRequestLimit, }; }