Initial import

This commit is contained in:
sirily
2026-03-10 14:03:52 +03:00
commit 6c0ca4e28b
102 changed files with 6598 additions and 0 deletions

9
apps/AGENTS.md Normal file
View File

@@ -0,0 +1,9 @@
# AGENTS.md
## Scope
Applies within `apps/` unless a deeper file overrides it.
## Rules
- Keep apps thin. Domain rules belong in `packages/domain`.
- App-local code may compose use cases but should not redefine billing, quota, or key-state logic.
- Any new runtime entrypoint must have a clear responsibility and own only transport concerns.

15
apps/bot/AGENTS.md Normal file
View File

@@ -0,0 +1,15 @@
# AGENTS.md
## Scope
Applies within `apps/bot`.
## Responsibilities
- Telegram admin bot runtime
- allowlist checks
- alert delivery
- low-friction admin commands
## Rules
- Pairing approval must never happen inside the bot runtime itself.
- The bot may initiate pending pairing, but only the server-side CLI completes it.
- Every command that changes state must produce an audit log entry.

3
apps/bot/README.md Normal file
View File

@@ -0,0 +1,3 @@
# apps/bot
Planned Telegram admin bot runtime. MVP should use long polling unless deployment requirements change.

15
apps/bot/package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "@nproxy/bot",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"start": "node dist/main.js"
},
"dependencies": {
"@nproxy/config": "workspace:*",
"@nproxy/db": "workspace:*",
"@nproxy/providers": "workspace:*"
}
}

0
apps/bot/src/.gitkeep Normal file
View File

100
apps/bot/src/main.ts Normal file
View File

@@ -0,0 +1,100 @@
import { loadConfig } from "@nproxy/config";
import { createPrismaTelegramBotStore, prisma } from "@nproxy/db";
import { createTelegramBotApiTransport, type TelegramUpdate } from "@nproxy/providers";
const config = loadConfig();
const telegramStore = createPrismaTelegramBotStore(prisma);
const telegramTransport = createTelegramBotApiTransport(config.telegram.botToken);
const pairingExpiresInMinutes = 15;
let nextUpdateOffset: number | undefined;
let isPolling = false;
console.log(
JSON.stringify({
service: "bot",
mode: config.telegram.botMode,
}),
);
void pollLoop();
process.once("SIGTERM", async () => {
await prisma.$disconnect();
process.exit(0);
});
process.once("SIGINT", async () => {
await prisma.$disconnect();
process.exit(0);
});
async function pollLoop(): Promise<void> {
if (isPolling) {
return;
}
isPolling = true;
try {
while (true) {
const updates = await telegramTransport.getUpdates({
...(nextUpdateOffset !== undefined ? { offset: nextUpdateOffset } : {}),
timeoutSeconds: 25,
});
for (const update of updates) {
await handleUpdate(update);
nextUpdateOffset = update.update_id + 1;
}
if (updates.length === 0) {
console.log("bot polling heartbeat");
}
}
} catch (error) {
console.error("bot polling failed", error);
setTimeout(() => {
isPolling = false;
void pollLoop();
}, 5000);
}
}
async function handleUpdate(update: TelegramUpdate): Promise<void> {
const message = update.message;
const from = message?.from;
if (!message || !from) {
return;
}
const telegramUserId = String(from.id);
const displayNameSnapshot = [from.first_name, from.last_name].filter(Boolean).join(" ");
const isAllowed = await telegramStore.isTelegramAdminAllowed(telegramUserId);
if (isAllowed) {
await telegramTransport.sendMessage({
chatId: message.chat.id,
text: "Admin access is active.",
});
return;
}
const challenge = await telegramStore.getOrCreatePendingPairingChallenge(
{
telegramUserId,
...(from.username ? { telegramUsername: from.username } : {}),
displayNameSnapshot: displayNameSnapshot || "Telegram user",
},
pairingExpiresInMinutes,
);
await telegramTransport.sendMessage({
chatId: message.chat.id,
text: [
`Your pairing code is: ${challenge.code}`,
`Run nproxy pair ${challenge.code} on the server.`,
`Expires at ${challenge.expiresAt.toISOString()}.`,
].join("\n"),
});
}

12
apps/bot/tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"types": ["node"]
},
"include": ["src/**/*.ts"]
}

14
apps/cli/AGENTS.md Normal file
View File

@@ -0,0 +1,14 @@
# AGENTS.md
## Scope
Applies within `apps/cli`.
## Responsibilities
- server-side operational CLI commands
- Telegram pairing completion
- safe admin maintenance commands that are better run locally on the server
## Rules
- Commands that mutate admin access must show the target identity and require explicit confirmation unless a non-interactive flag is provided.
- Pairing codes must be matched against hashed pending records.
- All successful mutating commands must write audit logs.

9
apps/cli/README.md Normal file
View File

@@ -0,0 +1,9 @@
# apps/cli
Planned operator CLI.
Expected early commands:
- `nproxy pair <code>`
- `nproxy pair list`
- `nproxy pair revoke <telegram-user-id>`
- `nproxy pair cleanup`

18
apps/cli/package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "@nproxy/cli",
"version": "0.1.0",
"private": true,
"type": "module",
"bin": {
"nproxy": "./dist/main.js"
},
"scripts": {
"build": "tsc -p tsconfig.json",
"start": "node dist/main.js"
},
"dependencies": {
"@nproxy/config": "workspace:*",
"@nproxy/db": "workspace:*",
"@nproxy/domain": "workspace:*"
}
}

0
apps/cli/src/.gitkeep Normal file
View File

233
apps/cli/src/main.ts Normal file
View File

@@ -0,0 +1,233 @@
#!/usr/bin/env node
import { createHash } from "node:crypto";
import { createInterface } from "node:readline/promises";
import { stdin, stdout } from "node:process";
import { loadConfig } from "@nproxy/config";
import { createPrismaTelegramPairingStore, prisma } from "@nproxy/db";
import { hashPairingCode, isPairingExpired, normalizePairingCode } from "@nproxy/domain";
const config = loadConfig();
const pairingStore = createPrismaTelegramPairingStore(prisma);
const args = process.argv.slice(2);
void main()
.catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
})
.finally(async () => {
await prisma.$disconnect();
});
async function main(): Promise<void> {
const [command, subcommandOrArgument, ...rest] = args;
if (command !== "pair") {
printHelp();
return;
}
if (subcommandOrArgument === "list") {
await handlePairList();
return;
}
if (subcommandOrArgument === "cleanup") {
await handlePairCleanup(rest);
return;
}
if (subcommandOrArgument === "revoke") {
await handlePairRevoke(rest[0], rest.slice(1));
return;
}
if (subcommandOrArgument) {
await handlePairComplete(subcommandOrArgument, rest);
return;
}
printHelp();
}
async function handlePairComplete(code: string, flags: string[]): Promise<void> {
const normalizedCode = normalizePairingCode(code);
const record = await pairingStore.findPendingPairingByCodeHash(hashPairingCode(normalizedCode));
if (!record) {
throw new Error(`Pending pairing not found for code ${normalizedCode}.`);
}
if (isPairingExpired(record.expiresAt)) {
throw new Error(`Pairing code ${normalizedCode} has expired.`);
}
printPairingIdentity(record);
const confirmed = flags.includes("--yes")
? true
: await confirm("Approve this Telegram admin pairing?");
if (!confirmed) {
console.log("pairing aborted");
return;
}
const completed = await pairingStore.completePendingPairing({
pairingId: record.id,
actorRef: buildActorRef(),
});
console.log(`paired telegram_user_id=${completed.telegramUserId}`);
}
async function handlePairList(): Promise<void> {
const listing = await pairingStore.listTelegramPairings();
console.log("active telegram admins:");
if (listing.activeAdmins.length === 0) {
console.log(" none");
} else {
for (const entry of listing.activeAdmins) {
console.log(
[
` user_id=${entry.telegramUserId}`,
`display_name=${JSON.stringify(entry.displayNameSnapshot)}`,
...(entry.telegramUsername ? [`username=@${entry.telegramUsername}`] : []),
`paired_at=${entry.pairedAt.toISOString()}`,
].join(" "),
);
}
}
console.log("pending pairings:");
if (listing.pending.length === 0) {
console.log(" none");
return;
}
const now = new Date();
for (const record of listing.pending) {
console.log(
[
` pairing_id=${record.id}`,
`user_id=${record.telegramUserId}`,
`display_name=${JSON.stringify(record.displayNameSnapshot)}`,
...(record.telegramUsername ? [`username=@${record.telegramUsername}`] : []),
`status=${record.status}`,
`expires_at=${record.expiresAt.toISOString()}`,
`expired=${isPairingExpired(record.expiresAt, now)}`,
].join(" "),
);
}
}
async function handlePairRevoke(
telegramUserId: string | undefined,
flags: string[],
): Promise<void> {
if (!telegramUserId) {
throw new Error("Missing telegram user id. Usage: nproxy pair revoke <telegram-user-id> [--yes]");
}
const listing = await pairingStore.listTelegramPairings();
const entry = listing.activeAdmins.find((item) => item.telegramUserId === telegramUserId);
if (!entry) {
throw new Error(`Active telegram admin ${telegramUserId} was not found.`);
}
console.log(
[
"revoke target:",
`user_id=${entry.telegramUserId}`,
`display_name=${JSON.stringify(entry.displayNameSnapshot)}`,
...(entry.telegramUsername ? [`username=@${entry.telegramUsername}`] : []),
].join(" "),
);
const confirmed = flags.includes("--yes")
? true
: await confirm("Revoke Telegram admin access?");
if (!confirmed) {
console.log("revoke aborted");
return;
}
await pairingStore.revokeTelegramAdmin({
telegramUserId,
actorRef: buildActorRef(),
});
console.log(`revoked telegram_user_id=${telegramUserId}`);
}
async function handlePairCleanup(flags: string[]): Promise<void> {
const confirmed = flags.includes("--yes")
? true
: await confirm("Mark all expired pending pairings as expired?");
if (!confirmed) {
console.log("cleanup aborted");
return;
}
const expiredCount = await pairingStore.cleanupExpiredPendingPairings({
actorRef: buildActorRef(),
});
console.log(`expired_pairings=${expiredCount}`);
}
async function confirm(prompt: string): Promise<boolean> {
const readline = createInterface({ input: stdin, output: stdout });
try {
const answer = await readline.question(`${prompt} [y/N] `);
return answer.trim().toLowerCase() === "y";
} finally {
readline.close();
}
}
function printPairingIdentity(record: {
telegramUserId: string;
displayNameSnapshot: string;
telegramUsername?: string;
expiresAt: Date;
}): void {
console.log(
[
"pairing target:",
`user_id=${record.telegramUserId}`,
`display_name=${JSON.stringify(record.displayNameSnapshot)}`,
...(record.telegramUsername ? [`username=@${record.telegramUsername}`] : []),
`expires_at=${record.expiresAt.toISOString()}`,
].join(" "),
);
}
function buildActorRef(): string {
const username =
process.env.USER ??
process.env.LOGNAME ??
"unknown";
const hostDigest = createHash("sha256")
.update(config.urls.appBaseUrl.toString())
.digest("hex")
.slice(0, 8);
return `${username}@${hostDigest}`;
}
function printHelp(): void {
console.log("nproxy cli");
console.log(`app=${config.urls.appBaseUrl.toString()}`);
console.log("available commands:");
console.log(" nproxy pair <code> [--yes]");
console.log(" nproxy pair list");
console.log(" nproxy pair revoke <telegram-user-id> [--yes]");
console.log(" nproxy pair cleanup [--yes]");
}

12
apps/cli/tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"types": ["node"]
},
"include": ["src/**/*.ts"]
}

16
apps/web/AGENTS.md Normal file
View File

@@ -0,0 +1,16 @@
# AGENTS.md
## Scope
Applies within `apps/web`.
## Responsibilities
- Public website
- User dashboard
- Chat UI
- Admin UI
- HTTP API entrypoints
## Rules
- Keep React and route handlers focused on transport, rendering, and validation.
- Delegate subscription, quota, and provider-routing policy to shared packages.
- User-facing quota output must remain approximate unless the caller is an admin surface.

10
apps/web/README.md Normal file
View File

@@ -0,0 +1,10 @@
# apps/web
Planned Next.js application for:
- public landing page
- authentication
- user dashboard
- chat UI
- billing pages
- admin web interface
- HTTP API routes

0
apps/web/app/.gitkeep Normal file
View File

16
apps/web/package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "@nproxy/web",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"start": "node dist/main.js"
},
"dependencies": {
"@nproxy/config": "workspace:*",
"@nproxy/db": "workspace:*",
"@nproxy/domain": "workspace:*",
"@nproxy/providers": "workspace:*"
}
}

782
apps/web/src/main.ts Normal file
View File

@@ -0,0 +1,782 @@
import {
createServer,
type IncomingMessage,
type ServerResponse,
} from "node:http";
import { loadConfig } from "@nproxy/config";
import {
createPrismaAccountStore,
createPrismaAuthStore,
createPrismaBillingStore,
createPrismaGenerationStore,
prisma,
} from "@nproxy/db";
import { createEmailTransport, createPaymentProviderAdapter } from "@nproxy/providers";
import {
AuthError,
GenerationRequestError,
createGenerationRequest,
type CreateGenerationRequestInput,
} from "@nproxy/domain";
const config = loadConfig();
const port = Number.parseInt(process.env.PORT ?? "3000", 10);
const accountStore = createPrismaAccountStore(prisma);
const authStore = createPrismaAuthStore(prisma);
const billingStore = createPrismaBillingStore(prisma);
const generationStore = createPrismaGenerationStore(prisma);
const emailTransport = createEmailTransport(config.email);
const paymentProviderAdapter = createPaymentProviderAdapter(config.payment);
const sessionCookieName = "nproxy_session";
const server = createServer(async (request, response) => {
try {
if (request.method === "GET" && request.url === "/healthz") {
sendJson(response, 200, { ok: true, service: "web" });
return;
}
if (request.method === "POST" && request.url === "/api/auth/register") {
const body = await readJsonBody(request);
const payload = readAuthPayload(body);
const session = await authStore.registerUser({
email: payload.email,
password: payload.password,
passwordPepper: config.auth.passwordPepper,
});
sendJson(
response,
201,
{ user: serializeAuthenticatedUser(session.user) },
createSessionCookie(session.token, session.expiresAt),
);
return;
}
if (request.method === "POST" && request.url === "/api/auth/login") {
const body = await readJsonBody(request);
const payload = readAuthPayload(body);
const session = await authStore.loginUser({
email: payload.email,
password: payload.password,
passwordPepper: config.auth.passwordPepper,
});
sendJson(
response,
200,
{ user: serializeAuthenticatedUser(session.user) },
createSessionCookie(session.token, session.expiresAt),
);
return;
}
if (request.method === "POST" && request.url === "/api/auth/password-reset/request") {
const body = await readJsonBody(request);
const payload = readEmailOnlyPayload(body);
const challenge = await authStore.createPasswordResetChallenge({
email: payload.email,
});
if (challenge) {
const resetUrl = new URL("/reset-password", config.urls.appBaseUrl);
resetUrl.searchParams.set("token", challenge.token);
await emailTransport.send({
to: challenge.email,
subject: "Reset your nproxy password",
text: [
"We received a request to reset your password.",
`Reset link: ${resetUrl.toString()}`,
`This link expires at ${challenge.expiresAt.toISOString()}.`,
].join("\n"),
});
}
sendJson(response, 200, {
ok: true,
});
return;
}
if (request.method === "POST" && request.url === "/api/auth/password-reset/confirm") {
const body = await readJsonBody(request);
const payload = readPasswordResetConfirmPayload(body);
await authStore.resetPassword({
token: payload.token,
newPassword: payload.password,
passwordPepper: config.auth.passwordPepper,
});
sendJson(response, 200, {
ok: true,
});
return;
}
if (request.method === "POST" && request.url === "/api/auth/logout") {
const authenticatedSession = await requireAuthenticatedSession(request);
await authStore.revokeSession(authenticatedSession.token);
sendJson(response, 200, { ok: true }, clearSessionCookie());
return;
}
if (request.method === "GET" && request.url === "/api/auth/me") {
const authenticatedSession = await requireAuthenticatedSession(request);
sendJson(response, 200, {
user: serializeAuthenticatedUser(authenticatedSession.user),
session: serializeUserSession(authenticatedSession.session, authenticatedSession.session.id),
});
return;
}
if (request.method === "GET" && request.url === "/api/auth/sessions") {
const authenticatedSession = await requireAuthenticatedSession(request);
const sessions = await authStore.listUserSessions(authenticatedSession.user.id);
sendJson(response, 200, {
sessions: sessions.map((session) =>
serializeUserSession(session, authenticatedSession.session.id),
),
});
return;
}
if (request.method === "GET" && request.url === "/api/account") {
const authenticatedSession = await requireAuthenticatedSession(request);
const overview = await accountStore.getUserAccountOverview(authenticatedSession.user.id);
if (!overview) {
sendJson(response, 404, {
error: {
code: "account_not_found",
message: "Account was not found.",
},
});
return;
}
sendJson(response, 200, serializeAccountOverview(overview));
return;
}
if (request.method === "GET" && request.url === "/api/billing/invoices") {
const authenticatedSession = await requireAuthenticatedSession(request);
const invoices = await billingStore.listUserInvoices(authenticatedSession.user.id);
sendJson(response, 200, {
invoices: invoices.map(serializeBillingInvoice),
});
return;
}
if (request.method === "POST" && request.url === "/api/billing/invoices") {
const authenticatedSession = await requireAuthenticatedSession(request);
const invoice = await billingStore.createSubscriptionInvoice({
userId: authenticatedSession.user.id,
paymentProvider: config.payment.provider,
paymentProviderAdapter,
});
sendJson(response, 201, {
invoice: serializeBillingInvoice(invoice),
});
return;
}
if (request.method === "POST" && request.url === "/api/auth/logout-all") {
const authenticatedSession = await requireAuthenticatedSession(request);
const revokedCount = await authStore.revokeAllUserSessions({
userId: authenticatedSession.user.id,
exceptSessionId: authenticatedSession.session.id,
});
sendJson(response, 200, {
ok: true,
revokedCount,
});
return;
}
if (request.method === "POST" && request.url) {
const invoiceMarkPaidMatch = request.url.match(/^\/api\/admin\/invoices\/([^/]+)\/mark-paid$/);
if (invoiceMarkPaidMatch) {
const authenticatedSession = await requireAuthenticatedSession(request);
if (!authenticatedSession.user.isAdmin) {
throw new HttpError(403, "forbidden", "Admin session is required.");
}
const invoiceId = decodeURIComponent(invoiceMarkPaidMatch[1] ?? "");
const invoice = await billingStore.markInvoicePaid({ invoiceId });
sendJson(response, 200, {
invoice: serializeBillingInvoice(invoice),
});
return;
}
}
if (request.method === "DELETE" && request.url) {
const sessionMatch = request.url.match(/^\/api\/auth\/sessions\/([^/]+)$/);
if (sessionMatch) {
const authenticatedSession = await requireAuthenticatedSession(request);
const sessionId = decodeURIComponent(sessionMatch[1] ?? "");
if (sessionId === authenticatedSession.session.id) {
await authStore.revokeSession(authenticatedSession.token);
sendJson(response, 200, { ok: true }, clearSessionCookie());
return;
}
const revoked = await authStore.revokeUserSession({
userId: authenticatedSession.user.id,
sessionId,
});
if (!revoked) {
sendJson(response, 404, {
error: {
code: "session_not_found",
message: "Session was not found.",
},
});
return;
}
sendJson(response, 200, { ok: true });
return;
}
}
if (request.method === "POST" && request.url === "/api/generations") {
const authenticatedSession = await requireAuthenticatedSession(request);
const body = await readJsonBody(request);
const requestInput = mapCreateGenerationRequestInput(body, authenticatedSession.user.id);
const result = await createGenerationRequest(generationStore, requestInput);
sendJson(response, result.reusedExistingRequest ? 200 : 201, {
request: serializeGenerationRequest(result.request),
reusedExistingRequest: result.reusedExistingRequest,
approximateQuotaBucket: result.approximateQuotaBucket,
});
return;
}
if (request.method === "GET" && request.url) {
const match = request.url.match(/^\/api\/generations\/([^/]+)$/);
if (match) {
const authenticatedSession = await requireAuthenticatedSession(request);
const requestId = decodeURIComponent(match[1] ?? "");
const generationRequest = await generationStore.getGenerationRequest(requestId);
if (!generationRequest || generationRequest.userId !== authenticatedSession.user.id) {
sendJson(response, 404, {
error: {
code: "not_found",
message: "Generation request was not found.",
},
});
return;
}
sendJson(response, 200, {
request: serializeGenerationRequest(generationRequest),
});
return;
}
}
sendJson(response, 200, {
service: "web",
appBaseUrl: config.urls.appBaseUrl.toString(),
adminBaseUrl: config.urls.adminBaseUrl.toString(),
providerModel: config.provider.nanoBananaDefaultModel,
endpoints: {
register: "POST /api/auth/register",
login: "POST /api/auth/login",
passwordResetRequest: "POST /api/auth/password-reset/request",
passwordResetConfirm: "POST /api/auth/password-reset/confirm",
logout: "POST /api/auth/logout",
me: "GET /api/auth/me",
sessions: "GET /api/auth/sessions",
revokeSession: "DELETE /api/auth/sessions/:id",
logoutAll: "POST /api/auth/logout-all",
account: "GET /api/account",
listInvoices: "GET /api/billing/invoices",
createInvoice: "POST /api/billing/invoices",
adminMarkInvoicePaid: "POST /api/admin/invoices/:id/mark-paid",
createGenerationRequest: "POST /api/generations",
getGenerationRequest: "GET /api/generations/:id",
},
});
} catch (error) {
handleRequestError(response, error);
}
});
server.listen(port, "0.0.0.0", () => {
console.log(`web listening on ${port}`);
});
process.once("SIGTERM", async () => {
await prisma.$disconnect();
process.exit(0);
});
process.once("SIGINT", async () => {
await prisma.$disconnect();
process.exit(0);
});
async function readJsonBody(
request: IncomingMessage,
): Promise<unknown> {
const chunks: Uint8Array[] = [];
for await (const chunk of request) {
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
}
if (chunks.length === 0) {
throw new HttpError(400, "invalid_json", "Request body must not be empty.");
}
const rawBody = Buffer.concat(chunks).toString("utf8");
try {
return JSON.parse(rawBody) as unknown;
} catch {
throw new HttpError(400, "invalid_json", "Request body must be valid JSON.");
}
}
function readAuthPayload(body: unknown): { email: string; password: string } {
if (!body || typeof body !== "object") {
throw new HttpError(400, "invalid_body", "Request body must be a JSON object.");
}
const payload = body as Record<string, unknown>;
return {
email: readRequiredString(payload.email, "email"),
password: readRequiredString(payload.password, "password"),
};
}
function readEmailOnlyPayload(body: unknown): { email: string } {
if (!body || typeof body !== "object") {
throw new HttpError(400, "invalid_body", "Request body must be a JSON object.");
}
const payload = body as Record<string, unknown>;
return {
email: readRequiredString(payload.email, "email"),
};
}
function readPasswordResetConfirmPayload(
body: unknown,
): { token: string; password: string } {
if (!body || typeof body !== "object") {
throw new HttpError(400, "invalid_body", "Request body must be a JSON object.");
}
const payload = body as Record<string, unknown>;
return {
token: readRequiredString(payload.token, "token"),
password: readRequiredString(payload.password, "password"),
};
}
function mapCreateGenerationRequestInput(
body: unknown,
userId: string,
): CreateGenerationRequestInput {
if (!body || typeof body !== "object") {
throw new HttpError(400, "invalid_body", "Request body must be a JSON object.");
}
const payload = body as Record<string, unknown>;
return {
userId,
mode: readGenerationMode(payload.mode),
providerModel: readRequiredString(payload.providerModel, "providerModel"),
prompt: readRequiredString(payload.prompt, "prompt"),
resolutionPreset: readRequiredString(payload.resolutionPreset, "resolutionPreset"),
batchSize: readRequiredInteger(payload.batchSize, "batchSize"),
...(payload.sourceImageKey !== undefined
? { sourceImageKey: readRequiredString(payload.sourceImageKey, "sourceImageKey") }
: {}),
...(payload.imageStrength !== undefined
? { imageStrength: readRequiredNumber(payload.imageStrength, "imageStrength") }
: {}),
...(payload.idempotencyKey !== undefined
? { idempotencyKey: readRequiredString(payload.idempotencyKey, "idempotencyKey") }
: {}),
};
}
function readGenerationMode(value: unknown): CreateGenerationRequestInput["mode"] {
if (value === "text_to_image" || value === "image_to_image") {
return value;
}
throw new HttpError(
400,
"invalid_mode",
'mode must be "text_to_image" or "image_to_image".',
);
}
function readRequiredString(value: unknown, field: string): string {
if (typeof value !== "string" || value.trim().length === 0) {
throw new HttpError(400, "invalid_field", `${field} must be a non-empty string.`);
}
return value;
}
function readRequiredInteger(value: unknown, field: string): number {
if (typeof value !== "number" || !Number.isInteger(value)) {
throw new HttpError(400, "invalid_field", `${field} must be an integer.`);
}
return value;
}
function readRequiredNumber(value: unknown, field: string): number {
if (typeof value !== "number" || Number.isNaN(value)) {
throw new HttpError(400, "invalid_field", `${field} must be a number.`);
}
return value;
}
function serializeAuthenticatedUser(user: {
id: string;
email: string;
isAdmin: boolean;
createdAt: Date;
}) {
return {
id: user.id,
email: user.email,
isAdmin: user.isAdmin,
createdAt: user.createdAt.toISOString(),
};
}
function serializeUserSession(
session: {
id: string;
expiresAt: Date;
revokedAt?: Date;
lastSeenAt?: Date;
createdAt: Date;
},
currentSessionId: string,
) {
return {
id: session.id,
expiresAt: session.expiresAt.toISOString(),
createdAt: session.createdAt.toISOString(),
isCurrent: session.id === currentSessionId,
...(session.revokedAt ? { revokedAt: session.revokedAt.toISOString() } : {}),
...(session.lastSeenAt ? { lastSeenAt: session.lastSeenAt.toISOString() } : {}),
};
}
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;
provider: string;
providerInvoiceId?: string;
status: string;
currency: string;
amountCrypto: number;
amountUsd?: number;
paymentAddress?: string;
expiresAt?: Date;
paidAt?: Date;
createdAt: Date;
updatedAt: Date;
}) {
return {
id: invoice.id,
provider: invoice.provider,
status: invoice.status,
currency: invoice.currency,
amountCrypto: invoice.amountCrypto,
createdAt: invoice.createdAt.toISOString(),
updatedAt: invoice.updatedAt.toISOString(),
...(invoice.subscriptionId ? { subscriptionId: invoice.subscriptionId } : {}),
...(invoice.providerInvoiceId ? { providerInvoiceId: invoice.providerInvoiceId } : {}),
...(invoice.amountUsd !== undefined ? { amountUsd: invoice.amountUsd } : {}),
...(invoice.paymentAddress ? { paymentAddress: invoice.paymentAddress } : {}),
...(invoice.expiresAt ? { expiresAt: invoice.expiresAt.toISOString() } : {}),
...(invoice.paidAt ? { paidAt: invoice.paidAt.toISOString() } : {}),
};
}
function serializeGenerationRequest(requestRecord: {
id: string;
userId: string;
mode: string;
status: string;
providerModel: string;
prompt: string;
sourceImageKey?: string;
resolutionPreset: string;
batchSize: number;
imageStrength?: number;
idempotencyKey?: string;
terminalErrorCode?: string;
terminalErrorText?: string;
requestedAt: Date;
startedAt?: Date;
completedAt?: Date;
createdAt: Date;
updatedAt: Date;
}) {
return {
id: requestRecord.id,
userId: requestRecord.userId,
mode: requestRecord.mode,
status: requestRecord.status,
providerModel: requestRecord.providerModel,
prompt: requestRecord.prompt,
resolutionPreset: requestRecord.resolutionPreset,
batchSize: requestRecord.batchSize,
requestedAt: requestRecord.requestedAt.toISOString(),
createdAt: requestRecord.createdAt.toISOString(),
updatedAt: requestRecord.updatedAt.toISOString(),
...(requestRecord.sourceImageKey !== undefined
? { sourceImageKey: requestRecord.sourceImageKey }
: {}),
...(requestRecord.imageStrength !== undefined
? { imageStrength: requestRecord.imageStrength }
: {}),
...(requestRecord.idempotencyKey !== undefined
? { idempotencyKey: requestRecord.idempotencyKey }
: {}),
...(requestRecord.terminalErrorCode !== undefined
? { terminalErrorCode: requestRecord.terminalErrorCode }
: {}),
...(requestRecord.terminalErrorText !== undefined
? { terminalErrorText: requestRecord.terminalErrorText }
: {}),
...(requestRecord.startedAt !== undefined
? { startedAt: requestRecord.startedAt.toISOString() }
: {}),
...(requestRecord.completedAt !== undefined
? { completedAt: requestRecord.completedAt.toISOString() }
: {}),
};
}
async function requireAuthenticatedSession(request: IncomingMessage) {
const sessionToken = readSessionToken(request);
if (!sessionToken) {
throw new HttpError(401, "unauthorized", "Missing authenticated session.");
}
const authenticatedSession = await authStore.getUserBySessionToken(sessionToken);
if (!authenticatedSession) {
throw new HttpError(401, "unauthorized", "Authenticated session is invalid.");
}
return {
...authenticatedSession,
token: sessionToken,
};
}
function readSessionToken(request: IncomingMessage): string | null {
const cookieHeader = request.headers.cookie;
if (!cookieHeader) {
return null;
}
for (const part of cookieHeader.split(";")) {
const [rawName, ...rawValue] = part.trim().split("=");
if (rawName === sessionCookieName) {
return decodeURIComponent(rawValue.join("="));
}
}
return null;
}
function createSessionCookie(token: string, expiresAt: Date): string {
return [
`${sessionCookieName}=${encodeURIComponent(token)}`,
"Path=/",
"HttpOnly",
"SameSite=Lax",
"Secure",
`Expires=${expiresAt.toUTCString()}`,
].join("; ");
}
function clearSessionCookie(): string {
return [
`${sessionCookieName}=`,
"Path=/",
"HttpOnly",
"SameSite=Lax",
"Secure",
"Expires=Thu, 01 Jan 1970 00:00:00 GMT",
].join("; ");
}
function handleRequestError(
response: ServerResponse<IncomingMessage>,
error: unknown,
): void {
if (error instanceof HttpError) {
sendJson(response, error.statusCode, {
error: {
code: error.code,
message: error.message,
},
});
return;
}
if (error instanceof AuthError) {
const statusCode =
error.code === "email_already_exists"
? 409
: error.code === "invalid_credentials"
? 401
: 400;
sendJson(response, statusCode, {
error: {
code: error.code,
message: error.message,
},
});
return;
}
if (error instanceof GenerationRequestError) {
const statusCode =
error.code === "missing_active_subscription"
? 403
: error.code === "quota_exhausted"
? 409
: error.code === "request_not_found"
? 404
: error.code === "request_not_completable"
? 409
: 400;
sendJson(response, statusCode, {
error: {
code: error.code,
message: error.message,
},
});
return;
}
console.error(error);
sendJson(response, 500, {
error: {
code: "internal_error",
message: "Internal server error.",
},
});
}
function sendJson(
response: ServerResponse<IncomingMessage>,
statusCode: number,
payload: unknown,
setCookie?: string,
): void {
response.writeHead(statusCode, {
"content-type": "application/json",
...(setCookie ? { "set-cookie": setCookie } : {}),
});
response.end(JSON.stringify(payload));
}
class HttpError extends Error {
constructor(
readonly statusCode: number,
readonly code: string,
message: string,
) {
super(message);
}
}

12
apps/web/tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"types": ["node"]
},
"include": ["src/**/*.ts"]
}

17
apps/worker/AGENTS.md Normal file
View File

@@ -0,0 +1,17 @@
# AGENTS.md
## Scope
Applies within `apps/worker`.
## Responsibilities
- generation job execution
- payment reconciliation
- media cleanup
- provider-key balance polling
- provider-key recovery checks
- alert and reminder jobs
## Rules
- Persist every provider-key attempt.
- Consume quota only after a request succeeds.
- Keep retries idempotent and auditable.

3
apps/worker/README.md Normal file
View File

@@ -0,0 +1,3 @@
# apps/worker
Planned worker runtime for queued and scheduled jobs.

16
apps/worker/package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "@nproxy/worker",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"start": "node dist/main.js"
},
"dependencies": {
"@nproxy/config": "workspace:*",
"@nproxy/db": "workspace:*",
"@nproxy/domain": "workspace:*",
"@nproxy/providers": "workspace:*"
}
}

0
apps/worker/src/.gitkeep Normal file
View File

150
apps/worker/src/main.ts Normal file
View File

@@ -0,0 +1,150 @@
import { loadConfig } from "@nproxy/config";
import { createPrismaWorkerStore, prisma } from "@nproxy/db";
import { createNanoBananaSimulatedAdapter } from "@nproxy/providers";
const config = loadConfig();
const intervalMs = config.keyPool.balancePollSeconds * 1000;
const workerStore = createPrismaWorkerStore(prisma, {
cooldownMinutes: config.keyPool.cooldownMinutes,
failuresBeforeManualReview: config.keyPool.failuresBeforeManualReview,
});
const nanoBananaAdapter = createNanoBananaSimulatedAdapter();
let isTickRunning = false;
console.log(
JSON.stringify({
service: "worker",
balancePollSeconds: config.keyPool.balancePollSeconds,
providerModel: config.provider.nanoBananaDefaultModel,
}),
);
setInterval(() => {
void runTick();
}, intervalMs);
void runTick();
process.once("SIGTERM", async () => {
await prisma.$disconnect();
process.exit(0);
});
process.once("SIGINT", async () => {
await prisma.$disconnect();
process.exit(0);
});
async function runTick(): Promise<void> {
if (isTickRunning) {
console.log("worker tick skipped because previous tick is still running");
return;
}
isTickRunning = true;
try {
const recovery = await workerStore.recoverCooldownProviderKeys();
if (recovery.recoveredCount > 0) {
console.log(
JSON.stringify({
service: "worker",
event: "cooldown_keys_recovered",
recoveredCount: recovery.recoveredCount,
}),
);
}
const job = await workerStore.claimNextQueuedGenerationJob();
if (!job) {
console.log(`worker heartbeat interval=${intervalMs} no_queued_jobs=true`);
return;
}
const result = await workerStore.processClaimedGenerationJob(
job,
async (request, providerKey) => {
if (providerKey.providerCode !== config.provider.nanoBananaDefaultModel) {
return {
ok: false as const,
usedProxy: false,
directFallbackUsed: false,
failureKind: "unknown" as const,
providerErrorCode: "unsupported_provider_model",
providerErrorText: `Unsupported provider model: ${providerKey.providerCode}`,
};
}
if (providerKey.proxyBaseUrl) {
const proxyResult = await nanoBananaAdapter.executeGeneration({
request,
providerKey: {
id: providerKey.id,
providerCode: providerKey.providerCode,
label: providerKey.label,
apiKeyLastFour: providerKey.apiKeyLastFour,
},
route: {
kind: "proxy",
proxyBaseUrl: providerKey.proxyBaseUrl,
},
});
if (!proxyResult.ok && proxyResult.failureKind === "transport") {
const directResult = await nanoBananaAdapter.executeGeneration({
request,
providerKey: {
id: providerKey.id,
providerCode: providerKey.providerCode,
label: providerKey.label,
apiKeyLastFour: providerKey.apiKeyLastFour,
},
route: {
kind: "direct",
},
});
return {
...directResult,
usedProxy: true,
directFallbackUsed: true,
};
}
return {
...proxyResult,
usedProxy: true,
directFallbackUsed: false,
};
}
const directResult = await nanoBananaAdapter.executeGeneration({
request,
providerKey: {
id: providerKey.id,
providerCode: providerKey.providerCode,
label: providerKey.label,
apiKeyLastFour: providerKey.apiKeyLastFour,
},
route: {
kind: "direct",
},
});
return {
...directResult,
usedProxy: false,
directFallbackUsed: false,
};
},
);
console.log(JSON.stringify({ service: "worker", event: "job_processed", ...result }));
} catch (error) {
console.error("worker tick failed", error);
} finally {
isTickRunning = false;
}
}

12
apps/worker/tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"types": ["node"]
},
"include": ["src/**/*.ts"]
}