@purecore/one-server-4-all 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { execFileSync } from 'node:child_process';
6
+
7
+ export class CertGenerator {
8
+ private static readonly CERT_DIR = '.hot-server-certs';
9
+ private static readonly KEY_FILE = 'localhost.key';
10
+ private static readonly CERT_FILE = 'localhost.crt';
11
+
12
+ static async generateCerts() {
13
+ const certDir = path.join(process.cwd(), this.CERT_DIR);
14
+ const keyPath = path.join(certDir, this.KEY_FILE);
15
+ const certPath = path.join(certDir, this.CERT_FILE);
16
+
17
+ try {
18
+ // Verificar se certificados já existem
19
+ try {
20
+ await fs.access(keyPath);
21
+ await fs.access(certPath);
22
+ console.log('📋 Certificados já existem em:', certDir);
23
+ return { keyPath, certPath };
24
+ } catch {
25
+ // Certificados não existem, vamos criar
26
+ }
27
+
28
+ // Criar diretório se não existir
29
+ await fs.mkdir(certDir, { recursive: true });
30
+
31
+ console.log('🔐 Gerando certificados auto-assinados...');
32
+
33
+ // Observação importante:
34
+ // Gerar um X509 "de verdade" apenas com node:crypto (sem libs externas) não é trivial.
35
+ // Como este projeto roda no WSL, utilizamos o OpenSSL (toolchain padrão do Linux)
36
+ // para gerar um par key/cert PEM válido e compatível com Bun/BoringSSL.
37
+ await this.generateWithOpenSSL({ keyPath, certPath });
38
+
39
+ // Validação do PEM (evita subir servidor com arquivo corrompido)
40
+ const [keyPem, certPem] = await Promise.all([
41
+ fs.readFile(keyPath, 'utf8'),
42
+ fs.readFile(certPath, 'utf8')
43
+ ]);
44
+ this.validatePem({ keyPem, certPem, keyPath, certPath });
45
+ this.validateX509(certPem, certPath);
46
+ this.validateWithOpenSSL(certPath);
47
+
48
+ console.log('✅ Certificados gerados com sucesso!');
49
+ console.log('📁 Localização:', certDir);
50
+ console.log('🔑 Chave privada:', keyPath);
51
+ console.log('📄 Certificado:', certPath);
52
+ console.log('');
53
+ console.log('⚠️ AVISO: Estes são certificados auto-assinados para desenvolvimento local.');
54
+ console.log(' Não use em produção!');
55
+
56
+ return { keyPath, certPath };
57
+
58
+ } catch (error) {
59
+ console.error('❌ Erro ao gerar certificados:', error);
60
+ throw error;
61
+ }
62
+ }
63
+
64
+ private static generateWithOpenSSL(params: { keyPath: string; certPath: string }) {
65
+ const { keyPath, certPath } = params;
66
+ const subj = '/C=BR/ST=SP/L=Sao Paulo/O=Purecore/OU=Dev/CN=localhost';
67
+
68
+ // Preferimos SAN para evitar problemas em clients modernos.
69
+ // Nem todo OpenSSL antigo suporta -addext, então fazemos fallback.
70
+ const baseArgs = ['req', '-x509', '-newkey', 'rsa:2048', '-keyout', keyPath, '-out', certPath, '-days', '365', '-nodes', '-subj', subj];
71
+
72
+ try {
73
+ execFileSync('openssl', [...baseArgs, '-addext', 'subjectAltName=DNS:localhost,IP:127.0.0.1'], { stdio: 'inherit' });
74
+ } catch {
75
+ execFileSync('openssl', baseArgs, { stdio: 'inherit' });
76
+ }
77
+ }
78
+
79
+ private static validatePem(params: { keyPem: string; certPem: string; keyPath: string; certPath: string }) {
80
+ const { keyPem, certPem, keyPath, certPath } = params;
81
+ const keyOk =
82
+ keyPem.includes('-----BEGIN PRIVATE KEY-----') ||
83
+ keyPem.includes('-----BEGIN RSA PRIVATE KEY-----');
84
+ const certOk = certPem.includes('-----BEGIN CERTIFICATE-----');
85
+
86
+ if (!keyOk || !certOk) {
87
+ throw new Error(
88
+ `PEM inválido gerado. ` +
89
+ `keyOk=${keyOk} certOk=${certOk}. ` +
90
+ `keyPath=${keyPath} certPath=${certPath}`
91
+ );
92
+ }
93
+ }
94
+
95
+ private static validateX509(certPem: string, certPath: string) {
96
+ try {
97
+ // Node valida a estrutura do X509 e falha se base64/DER estiverem inválidos.
98
+ // Isso é um bom "gate" antes de subir o https.createServer().
99
+ // @ts-ignore - Bun/Node expõem X509Certificate em node:crypto
100
+ const x509 = new crypto.X509Certificate(certPem);
101
+ if (!x509.subject) {
102
+ throw new Error('X509 sem subject');
103
+ }
104
+ } catch (error: any) {
105
+ throw new Error(`Certificado X509 inválido em ${certPath}: ${error?.message || String(error)}`);
106
+ }
107
+ }
108
+
109
+ private static validateWithOpenSSL(certPath: string) {
110
+ try {
111
+ // OpenSSL é a validação "ground truth" no WSL.
112
+ execFileSync('openssl', ['x509', '-in', certPath, '-noout'], { stdio: 'ignore' });
113
+ } catch (error: any) {
114
+ throw new Error(`OpenSSL não conseguiu ler o certificado (${certPath}).`);
115
+ }
116
+ }
117
+
118
+ static async getCertPaths(): Promise<{ keyPath: string; certPath: string } | null> {
119
+ const certDir = path.join(process.cwd(), this.CERT_DIR);
120
+ const keyPath = path.join(certDir, this.KEY_FILE);
121
+ const certPath = path.join(certDir, this.CERT_FILE);
122
+
123
+ try {
124
+ await fs.access(keyPath);
125
+ await fs.access(certPath);
126
+ return { keyPath, certPath };
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+
132
+ static async cleanCerts() {
133
+ const certDir = path.join(process.cwd(), this.CERT_DIR);
134
+
135
+ try {
136
+ await fs.rm(certDir, { recursive: true, force: true });
137
+ console.log('🗑️ Certificados removidos:', certDir);
138
+ } catch (error) {
139
+ console.error('❌ Erro ao remover certificados:', error);
140
+ }
141
+ }
142
+ }
143
+
144
+ // Executar se chamado diretamente
145
+ if (typeof require !== 'undefined' && require.main === module) {
146
+ const command = process.argv[2];
147
+
148
+ switch (command) {
149
+ case 'generate':
150
+ case undefined:
151
+ CertGenerator.generateCerts().catch(console.error);
152
+ break;
153
+ case 'clean':
154
+ CertGenerator.cleanCerts().catch(console.error);
155
+ break;
156
+ case 'info':
157
+ CertGenerator.getCertPaths().then(paths => {
158
+ if (paths) {
159
+ console.log('📋 Certificados encontrados:');
160
+ console.log('🔑 Chave:', paths.keyPath);
161
+ console.log('📄 Certificado:', paths.certPath);
162
+ } else {
163
+ console.log('❌ Nenhum certificado encontrado');
164
+ }
165
+ }).catch(console.error);
166
+ break;
167
+ default:
168
+ console.log('Uso: cert-generator [generate|clean|info]');
169
+ console.log(' generate: Gera certificados auto-assinados (padrão)');
170
+ console.log(' clean: Remove certificados existentes');
171
+ console.log(' info: Mostra informações dos certificados');
172
+ }
173
+ }
@@ -0,0 +1,182 @@
1
+ import { exec } from "node:child_process";
2
+ import readline from "node:readline";
3
+ import path from "node:path";
4
+ import fs from "node:fs/promises";
5
+
6
+ const cyan = (text: string) => `\x1b[36m${text}\x1b[0m`;
7
+ const green = (text: string) => `\x1b[32m${text}\x1b[0m`;
8
+ const bold = (text: string) => `\x1b[1m${text}\x1b[0m`;
9
+ const gray = (text: string) => `\x1b[90m${text}\x1b[0m`;
10
+ const magenta = (text: string) => `\x1b[35m${text}\x1b[0m`;
11
+ const yellow = (text: string) => `\x1b[33m${text}\x1b[0m`;
12
+
13
+ export class Deployer {
14
+ private rl = readline.createInterface({
15
+ input: process.stdin,
16
+ output: process.stdout,
17
+ });
18
+
19
+ private question(query: string): Promise<string> {
20
+ return new Promise((resolve) => this.rl.question(query, resolve));
21
+ }
22
+
23
+ private printBanner() {
24
+ console.log(
25
+ `\n ${bold(magenta("🚀 HOT-SERVER DEPLOYER"))} ${gray("v0.4.0")}`
26
+ );
27
+ console.log(` ${gray("─────────────────────────────────────────")}\n`);
28
+ }
29
+
30
+ public async start() {
31
+ this.printBanner();
32
+
33
+ const domain = await this.question(
34
+ ` ${cyan("➜")} ${bold("Qual o domínio/subdomínio?")} ${gray(
35
+ "(ex: app.meusite.com)"
36
+ )}\n ${green("❯")} `
37
+ );
38
+
39
+ if (!domain) {
40
+ console.log(`\n ${yellow("⚠")} Domínio é obrigatório. Cancelando...\n`);
41
+ this.rl.close();
42
+ return;
43
+ }
44
+
45
+ const port =
46
+ (await this.question(
47
+ ` ${cyan("➜")} ${bold("Qual a porta do servidor?")} ${gray(
48
+ "(padrão 6000)"
49
+ )}\n ${green("❯")} `
50
+ )) || "6000";
51
+
52
+ const confirmNginx = await this.question(
53
+ ` ${cyan("➜")} ${bold(
54
+ "Deseja configurar Nginx + SSL (Certbot) agora?"
55
+ )} ${gray("(s/n)")}\n ${green("❯")} `
56
+ );
57
+
58
+ let certPaths = { key: "", cert: "" };
59
+
60
+ if (confirmNginx.toLowerCase() === "s") {
61
+ const success = await this.setupNginx(domain, port);
62
+ if (success) {
63
+ // Caminhos padrão do Certbot
64
+ certPaths.key = `/etc/letsencrypt/live/${domain}/privkey.pem`;
65
+ certPaths.cert = `/etc/letsencrypt/live/${domain}/fullchain.pem`;
66
+ }
67
+ }
68
+
69
+ const name = domain.split(".")[0];
70
+
71
+ // Monta o comando PM2
72
+ // Se temos certificados do Certbot e o usuário quer usar no node direto (embora com nginx proxy não precise necessariamente)
73
+ // O user pediu para "ter o caminho para apontar para eles".
74
+ // Vamos gerar o comando COM os certificados se eles existirem, e setar https=true
75
+
76
+ let pm2Command = `pm2 start dist/index.js --name "${domain}" -- --port=${port} --open=false`;
77
+
78
+ if (certPaths.key && certPaths.cert) {
79
+ // Nota: Node node pode não ter permissão de ler /etc/letsencrypt diretamente dependendo do user
80
+ // Mas vamos atender o pedido de colocar o caminho
81
+ pm2Command += ` --https=true --ssl-key="${certPaths.key}" --ssl-cert="${certPaths.cert}"`;
82
+ } else {
83
+ // Se nao tem certbot, roda http normal (ou auto-assinado se forcer https)
84
+ pm2Command += ` --https=false`;
85
+ }
86
+
87
+ console.log(`\n ${bold("📦 Configuração Final:")}`);
88
+ console.log(` ${gray("────────────────────────")}`);
89
+ console.log(` ${bold("Dominio:")} ${cyan(domain)}`);
90
+ console.log(` ${bold("Porta:")} ${cyan(port)}`);
91
+ if (certPaths.key) {
92
+ console.log(` ${bold("SSL:")} ${green("Configurado via Certbot")}`);
93
+ }
94
+ console.log(` ${bold("PM2:")} ${yellow(pm2Command)}`);
95
+ console.log(` ${gray("────────────────────────")}\n`);
96
+
97
+ console.log(` ${bold(green("✨ Deploy concluído com sucesso!"))}`);
98
+ console.log(
99
+ ` ${gray("Copie e execute o comando acima para iniciar o servidor.")}\n`
100
+ );
101
+
102
+ this.rl.close();
103
+ }
104
+
105
+ private async setupNginx(domain: string, port: string): Promise<boolean> {
106
+ console.log(`\n ${bold("🛠️ Iniciando configuração Nginx...")}`);
107
+
108
+ // Configuração inicial HTTP para o Certbot validar
109
+ const nginxConfig = `
110
+ server {
111
+ listen 80;
112
+ server_name ${domain};
113
+
114
+ location / {
115
+ proxy_pass http://127.0.0.1:${port};
116
+ proxy_http_version 1.1;
117
+ proxy_set_header Upgrade $http_upgrade;
118
+ proxy_set_header Connection 'upgrade';
119
+ proxy_set_header Host $host;
120
+ proxy_cache_bypass $http_upgrade;
121
+ }
122
+ }
123
+ `;
124
+
125
+ const tempFilePath = `/tmp/${domain}.conf`;
126
+ const sitesAvailable = `/etc/nginx/sites-available/${domain}`;
127
+ const sitesEnabled = `/etc/nginx/sites-enabled/${domain}`;
128
+
129
+ try {
130
+ console.log(` ${gray("➜")} Criando arquivo temporário...`);
131
+ await fs.writeFile(tempFilePath, nginxConfig);
132
+
133
+ console.log(
134
+ ` ${gray("➜")} Movendo para sites-available (requer sudo)...`
135
+ );
136
+ await this.runCommand(`sudo mv ${tempFilePath} ${sitesAvailable}`);
137
+
138
+ console.log(` ${gray("➜")} Testando configuração do Nginx...`);
139
+ await this.runCommand(`sudo nginx -t`);
140
+
141
+ console.log(` ${gray("➜")} Ativando site...`);
142
+ await this.runCommand(`sudo ln -sf ${sitesAvailable} ${sitesEnabled}`);
143
+
144
+ console.log(` ${gray("➜")} Recarregando Nginx...`);
145
+ await this.runCommand(`sudo systemctl reload nginx`);
146
+
147
+ console.log(` ${gray("➜")} Executando Certbot...`);
148
+ console.log(` ${yellow("⚠ Aguarde...")}`);
149
+
150
+ // Roda certbot e pede para ele configurar o redirect HTTPS no Nginx automaticamente
151
+ await this.runCommand(
152
+ `sudo certbot --nginx -d ${domain} --non-interactive --agree-tos --redirect -m admin@${domain
153
+ .split(".")
154
+ .slice(-2)
155
+ .join(".")}`
156
+ );
157
+
158
+ console.log(` ${green("✔")} Nginx e SSL configurados com sucesso!`);
159
+ return true;
160
+ } catch (error: any) {
161
+ console.error(
162
+ `\n ${yellow("❌ Erro na configuração:")} ${error.message}`
163
+ );
164
+ console.log(
165
+ ` ${gray("Continuando apenas com a geração do comando...")}`
166
+ );
167
+ return false;
168
+ }
169
+ }
170
+
171
+ private runCommand(command: string): Promise<string> {
172
+ return new Promise((resolve, reject) => {
173
+ exec(command, (error, stdout, stderr) => {
174
+ if (error) {
175
+ reject(new Error(stderr || stdout || error.message));
176
+ return;
177
+ }
178
+ resolve(stdout);
179
+ });
180
+ });
181
+ }
182
+ }
package/src/index.ts ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ // File: src/index.ts
3
+ import { HotServer } from "./server.js";
4
+ import { Deployer } from "./deployer.js";
5
+ import { configSchema } from "./validator.js";
6
+ import path from "node:path";
7
+
8
+ const args = process.argv.slice(2);
9
+
10
+ // Check for deploy command
11
+ if (args[0] === "deploy") {
12
+ const deployer = new Deployer();
13
+ deployer.start();
14
+ } else {
15
+ // Parsing manual simplificado de args (ex: --port=3000)
16
+ // ... resto do código
17
+ const getArg = (key: string, defaultVal: string) => {
18
+ const arg = args.find((a) => a.startsWith(`--${key}=`));
19
+ const arg2 = args.find((a) => a.startsWith(`-${key}=`));
20
+ return arg ? arg.split("=")[1] : arg2 ? arg2.split("=")[1] : defaultVal;
21
+ };
22
+
23
+ // Se o primeiro argumento não tiver --, assumimos que é a pasta
24
+ const rootArg = args[0] && !args[0].startsWith("--") ? args[0] : ".";
25
+
26
+ const rawConfig = {
27
+ port: parseInt(getArg("port", "6000")),
28
+ root: path.resolve(process.cwd(), rootArg),
29
+ open: getArg("open", "true"), // 'true' por padrão
30
+ spa: getArg("spa", "false"), // 'false' por padrão
31
+ https: getArg("https", "false"), // 'false' por padrão
32
+ };
33
+
34
+ try {
35
+ // Validação "Zod-like"
36
+ const config = configSchema.parse(rawConfig);
37
+
38
+ const app = new HotServer(config);
39
+ app.start();
40
+ } catch (error: any) {
41
+ console.error("❌ Erro de configuração:", error.message);
42
+ process.exit(1);
43
+ }
44
+ }