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

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"]
}