From b23612ef232181b175cd6abe3adf25c75287a5c1 Mon Sep 17 00:00:00 2001 From: sirily Date: Tue, 10 Mar 2026 15:16:46 +0300 Subject: [PATCH 1/4] fix: hide exact quota values from account response --- AGENTS.md | 3 ++ apps/web/package.json | 1 + apps/web/src/account-response.test.ts | 65 ++++++++++++++++++++++++ apps/web/src/account-response.ts | 71 +++++++++++++++++++++++++++ apps/web/src/main.ts | 60 +--------------------- packages/db/src/account-store.ts | 6 --- 6 files changed, 142 insertions(+), 64 deletions(-) create mode 100644 apps/web/src/account-response.test.ts create mode 100644 apps/web/src/account-response.ts 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, }; } -- 2.49.1 From 18cb856e4975843e80403bb6b5f1b2142bf3c36c Mon Sep 17 00:00:00 2001 From: sirily Date: Tue, 10 Mar 2026 15:21:07 +0300 Subject: [PATCH 2/4] docs: document gitea issue workflow --- AGENTS.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 6e5d43c..1fa5062 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,3 +47,10 @@ This repository is a TypeScript monorepo for `nproxy`, a crypto-subscription ima ## Workflow - At the end of each completed task, create a PR for the changes unless the user explicitly says not to. + +## Gitea Workflow +- Treat the remote Gitea repository as the source of truth for task tracking when the user refers to `issues`. +- Before starting implementation, open the relevant Gitea issue and read its full problem statement and acceptance criteria instead of guessing from local notes. +- If the user asks to "continue work" or pick the next task, inspect the open Gitea issues and choose the first one that is logically ready to implement. +- After finishing the task, push the branch and create a Gitea PR linked to the issue, usually with `Closes #` in the PR body. +- If Gitea access requires credentials or network escalation, use the configured repository environment and approved escalation flow instead of skipping the issue lookup. -- 2.49.1 From d49561b39698fe2251e0652850dbe601238c8b34 Mon Sep 17 00:00:00 2001 From: sirily Date: Tue, 10 Mar 2026 15:38:23 +0300 Subject: [PATCH 3/4] test: build web package before running tests --- apps/web/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/package.json b/apps/web/package.json index 50b79be..d0553d3 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "build": "tsc -p tsconfig.json", + "pretest": "pnpm build", "test": "node --test dist/**/*.test.js", "start": "node dist/main.js" }, -- 2.49.1 From dfcbaf8b438261017cf0f0a09c3a8564d95399b8 Mon Sep 17 00:00:00 2001 From: sirily Date: Tue, 10 Mar 2026 15:47:15 +0300 Subject: [PATCH 4/4] docs: require explicit issue-to-pr linking in gitea workflow --- AGENTS.md | 4 +++- CONTRIBUTING.md | 16 ++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1fa5062..3280da6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,5 +52,7 @@ This repository is a TypeScript monorepo for `nproxy`, a crypto-subscription ima - Treat the remote Gitea repository as the source of truth for task tracking when the user refers to `issues`. - Before starting implementation, open the relevant Gitea issue and read its full problem statement and acceptance criteria instead of guessing from local notes. - If the user asks to "continue work" or pick the next task, inspect the open Gitea issues and choose the first one that is logically ready to implement. -- After finishing the task, push the branch and create a Gitea PR linked to the issue, usually with `Closes #` in the PR body. +- After finishing the task, push the branch and create a Gitea PR linked to the issue. +- Every task PR must include an issue reference in the PR body, usually `Closes #` (or `Refs #` when it should not auto-close on merge). +- If the issue-to-PR link is not clearly visible in Gitea, add an explicit comment in the issue with the PR number and URL. - If Gitea access requires credentials or network escalation, use the configured repository environment and approved escalation flow instead of skipping the issue lookup. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1f5728e..22981d9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,12 +8,15 @@ ## Standard Flow 1. Update local `master`. 2. Create a task branch from `master`. -3. Implement the change in that branch. -4. Run the relevant verification for the change. -5. Commit the work in that branch. -6. Push the branch to Gitea. -7. Open a merge request into `master`. -8. Merge only after review or explicit approval. +3. Pick or confirm the target issue in Gitea before implementation. +4. Implement the change in that branch. +5. Run the relevant verification for the change. +6. Commit the work in that branch. +7. Push the branch to Gitea. +8. Open a merge request into `master`. +9. Link the merge request to the issue in the MR body using `Closes #` or `Refs #`. +10. If the Gitea UI does not show the link clearly, add an explicit comment in the issue with the MR number and URL. +11. Merge only after review or explicit approval. ## Branch Naming Use short, purpose-driven names, for example: @@ -31,3 +34,4 @@ Use short, purpose-driven names, for example: ## Agent Workflow - Codex or any other coding agent must create and use a dedicated branch per task. - After task completion, the agent should push that branch and prepare it for a merge request instead of pushing directly to `master`. +- The agent must ensure the issue is explicitly linked to the merge request before handing off for merge. -- 2.49.1