@mucan54/porterman 1.0.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 @@
1
+ {"version":3,"sources":["../src/logger.ts","../src/utils.ts","../src/ip.ts","../src/config.ts","../src/certs.ts","../src/proxy.ts","../src/server.ts"],"sourcesContent":["const RESET = \"\\x1b[0m\";\nconst BOLD = \"\\x1b[1m\";\nconst DIM = \"\\x1b[2m\";\nconst RED = \"\\x1b[31m\";\nconst GREEN = \"\\x1b[32m\";\nconst YELLOW = \"\\x1b[33m\";\nconst BLUE = \"\\x1b[34m\";\nconst CYAN = \"\\x1b[36m\";\nconst WHITE = \"\\x1b[37m\";\n\nlet verboseEnabled = false;\n\nexport function setVerbose(enabled: boolean): void {\n verboseEnabled = enabled;\n}\n\nexport function isVerbose(): boolean {\n return verboseEnabled;\n}\n\nexport const logger = {\n info(message: string): void {\n console.log(`${CYAN}ℹ${RESET} ${message}`);\n },\n\n success(message: string): void {\n console.log(`${GREEN}✅${RESET} ${message}`);\n },\n\n warn(message: string): void {\n console.log(`${YELLOW}⚠${RESET} ${message}`);\n },\n\n error(message: string): void {\n console.error(`${RED}✖${RESET} ${message}`);\n },\n\n rocket(message: string): void {\n console.log(`🚀 ${message}`);\n },\n\n link(label: string, url: string): void {\n console.log(` ${GREEN}${url}${RESET} → ${DIM}${label}${RESET}`);\n },\n\n verbose(message: string): void {\n if (verboseEnabled) {\n console.log(`${DIM}[verbose] ${message}${RESET}`);\n }\n },\n\n request(method: string, host: string, path: string, status: number): void {\n if (!verboseEnabled) return;\n const color = status < 400 ? GREEN : status < 500 ? YELLOW : RED;\n const time = new Date().toISOString().slice(11, 19);\n console.log(\n `${DIM}${time}${RESET} ${BOLD}${method}${RESET} ${host}${path} ${color}${status}${RESET}`\n );\n },\n\n banner(version: string): void {\n console.log(`\\n${BOLD}🚪 Porterman v${version}${RESET}`);\n },\n\n blank(): void {\n console.log();\n },\n};\n","import { createServer, type Server } from \"node:net\";\n\n/**\n * Convert a dotted IP address to dashed format.\n * 85.100.50.25 → 85-100-50-25\n */\nexport function ipToDashed(ip: string): string {\n return ip.replace(/\\./g, \"-\").replace(/:/g, \"-\");\n}\n\n/**\n * Generate a sslip.io hostname for a given port and IP.\n */\nexport function makeHostname(\n port: number | string,\n dashedIp: string\n): string {\n return `${port}-${dashedIp}.sslip.io`;\n}\n\n/**\n * Parse the port number from a sslip.io hostname.\n * \"3000-85-100-50-25.sslip.io\" → 3000\n */\nexport function parsePortFromHost(host: string): number | null {\n // Remove port suffix if present (e.g., \":443\")\n const hostname = host.split(\":\")[0];\n // Extract the prefix before the first dash followed by IP-like pattern\n const match = hostname.match(/^(\\w+)-\\d+-/);\n if (!match) return null;\n const prefix = match[1];\n const port = parseInt(prefix, 10);\n return isNaN(port) ? null : port;\n}\n\n/**\n * Parse custom name from hostname.\n * \"myapp-85-100-50-25.sslip.io\" → \"myapp\"\n */\nexport function parsePrefixFromHost(host: string): string | null {\n const hostname = host.split(\":\")[0];\n const match = hostname.match(/^(\\w+)-\\d+-/);\n return match ? match[1] : null;\n}\n\n/**\n * Check if an IP is a private/reserved address.\n */\nexport function isPrivateIp(ip: string): boolean {\n const parts = ip.split(\".\").map(Number);\n if (parts.length !== 4 || parts.some((p) => isNaN(p))) return true;\n\n // 10.0.0.0/8\n if (parts[0] === 10) return true;\n // 172.16.0.0/12\n if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;\n // 192.168.0.0/16\n if (parts[0] === 192 && parts[1] === 168) return true;\n // 127.0.0.0/8\n if (parts[0] === 127) return true;\n // 0.0.0.0\n if (parts.every((p) => p === 0)) return true;\n // 169.254.0.0/16 (link-local)\n if (parts[0] === 169 && parts[1] === 254) return true;\n\n return false;\n}\n\n/**\n * Check if a port is available.\n */\nexport function isPortAvailable(port: number): Promise<boolean> {\n return new Promise((resolve) => {\n const server = createServer();\n server.once(\"error\", () => resolve(false));\n server.once(\"listening\", () => {\n server.close(() => resolve(true));\n });\n server.listen(port, \"0.0.0.0\");\n });\n}\n\n/**\n * Validate that a port number is valid.\n */\nexport function isValidPort(port: number): boolean {\n return Number.isInteger(port) && port >= 1 && port <= 65535;\n}\n\n/**\n * Sleep for a given number of milliseconds.\n */\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { logger } from \"./logger.js\";\nimport { isPrivateIp, ipToDashed } from \"./utils.js\";\n\nconst IP_SERVICES = [\n \"https://api.ipify.org\",\n \"https://ifconfig.me/ip\",\n \"https://icanhazip.com\",\n];\n\nlet cachedIp: string | null = null;\n\n/**\n * Fetch public IP from a single service with timeout.\n */\nasync function fetchIpFrom(url: string): Promise<string | null> {\n try {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 5000);\n const res = await fetch(url, {\n signal: controller.signal,\n headers: { Accept: \"text/plain\" },\n });\n clearTimeout(timeout);\n if (!res.ok) return null;\n const text = await res.text();\n return text.trim();\n } catch {\n return null;\n }\n}\n\n/**\n * Detect the machine's public IP address using multiple services with fallback.\n * Results are cached for the session lifetime.\n */\nexport async function detectPublicIp(): Promise<string> {\n if (cachedIp) return cachedIp;\n\n for (const service of IP_SERVICES) {\n logger.verbose(`Trying IP detection via ${service}`);\n const ip = await fetchIpFrom(service);\n if (ip && /^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/.test(ip)) {\n if (isPrivateIp(ip)) {\n throw new Error(\n `Detected private IP address (${ip}). Porterman requires a public IP.\\n` +\n \"If you're behind NAT, you need a machine with a direct public IP (VPS, cloud instance, etc.).\\n\" +\n \"You can also specify your IP manually with --host <ip>.\"\n );\n }\n cachedIp = ip;\n logger.verbose(`Public IP detected: ${ip}`);\n return ip;\n }\n }\n\n throw new Error(\n \"Could not detect public IP address. All detection services failed.\\n\" +\n \"Please specify your IP manually with --host <ip>.\"\n );\n}\n\n/**\n * Get the dashed format of the public IP (e.g., 85-100-50-25).\n */\nexport async function getDashedIp(overrideIp?: string): Promise<string> {\n const ip = overrideIp ?? (await detectPublicIp());\n if (overrideIp && isPrivateIp(overrideIp)) {\n logger.warn(\n `Specified IP ${overrideIp} appears to be a private address. sslip.io may not work correctly.`\n );\n }\n return ipToDashed(ip);\n}\n\n/**\n * Clear the cached IP (useful for testing).\n */\nexport function clearIpCache(): void {\n cachedIp = null;\n}\n","import { mkdir, readFile, writeFile, chmod } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nconst PORTERMAN_DIR = join(homedir(), \".porterman\");\nconst CONFIG_FILE = join(PORTERMAN_DIR, \"config.json\");\nconst CERTS_DIR = join(PORTERMAN_DIR, \"certs\");\nconst ACCOUNT_KEY_FILE = join(PORTERMAN_DIR, \"account.pem\");\nconst PID_FILE = join(PORTERMAN_DIR, \"porterman.pid\");\n\nexport interface PortermanConfig {\n defaultTimeout?: number;\n defaultHttpPort?: number;\n defaultHttpsPort?: number;\n}\n\nexport const paths = {\n base: PORTERMAN_DIR,\n config: CONFIG_FILE,\n certs: CERTS_DIR,\n accountKey: ACCOUNT_KEY_FILE,\n pidFile: PID_FILE,\n\n certDir(hostname: string): string {\n return join(CERTS_DIR, hostname);\n },\n\n certFile(hostname: string): string {\n return join(CERTS_DIR, hostname, \"cert.pem\");\n },\n\n keyFile(hostname: string): string {\n return join(CERTS_DIR, hostname, \"privkey.pem\");\n },\n\n chainFile(hostname: string): string {\n return join(CERTS_DIR, hostname, \"chain.pem\");\n },\n\n metaFile(hostname: string): string {\n return join(CERTS_DIR, hostname, \"meta.json\");\n },\n};\n\nexport async function ensureDirs(): Promise<void> {\n await mkdir(PORTERMAN_DIR, { recursive: true });\n await mkdir(CERTS_DIR, { recursive: true });\n}\n\nexport async function loadConfig(): Promise<PortermanConfig> {\n try {\n const data = await readFile(CONFIG_FILE, \"utf-8\");\n return JSON.parse(data);\n } catch {\n return {};\n }\n}\n\nexport async function saveConfig(config: PortermanConfig): Promise<void> {\n await ensureDirs();\n await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));\n}\n\nexport async function writeSecureFile(\n filePath: string,\n content: string\n): Promise<void> {\n await writeFile(filePath, content, { mode: 0o600 });\n}\n\nexport async function writePidFile(pid: number): Promise<void> {\n await ensureDirs();\n await writeFile(PID_FILE, String(pid));\n}\n\nexport async function readPidFile(): Promise<number | null> {\n try {\n const data = await readFile(PID_FILE, \"utf-8\");\n const pid = parseInt(data.trim(), 10);\n return isNaN(pid) ? null : pid;\n } catch {\n return null;\n }\n}\n\nexport function pidFileExists(): boolean {\n return existsSync(PID_FILE);\n}\n","import * as acme from \"acme-client\";\nimport { readFile, mkdir, writeFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { createServer, type Server } from \"node:http\";\nimport { paths, ensureDirs, writeSecureFile } from \"./config.js\";\nimport { logger } from \"./logger.js\";\n\ninterface CertMeta {\n issuedAt: string;\n expiresAt: string;\n domains: string[];\n}\n\ninterface CertFiles {\n key: string;\n cert: string;\n chain: string;\n}\n\n// In-memory store for ACME HTTP-01 challenge tokens\nconst challengeTokens = new Map<string, string>();\n\n/**\n * Get or create the ACME account private key.\n */\nasync function getAccountKey(): Promise<Buffer> {\n await ensureDirs();\n if (existsSync(paths.accountKey)) {\n return readFile(paths.accountKey);\n }\n const key = await acme.crypto.createPrivateKey();\n await writeSecureFile(paths.accountKey, key.toString());\n return key;\n}\n\n/**\n * Check if an existing certificate is still valid (>30 days remaining).\n */\nasync function isCertValid(hostname: string): Promise<boolean> {\n const metaPath = paths.metaFile(hostname);\n if (!existsSync(metaPath)) return false;\n\n try {\n const data = await readFile(metaPath, \"utf-8\");\n const meta: CertMeta = JSON.parse(data);\n const expires = new Date(meta.expiresAt);\n const daysRemaining =\n (expires.getTime() - Date.now()) / (1000 * 60 * 60 * 24);\n if (daysRemaining > 30) {\n logger.verbose(\n `Certificate for ${hostname} valid for ${Math.floor(daysRemaining)} more days`\n );\n return true;\n }\n logger.verbose(\n `Certificate for ${hostname} expires in ${Math.floor(daysRemaining)} days, needs renewal`\n );\n return false;\n } catch {\n return false;\n }\n}\n\n/**\n * Load existing certificate files from disk.\n */\nasync function loadCertFromDisk(hostname: string): Promise<CertFiles> {\n const [key, cert, chain] = await Promise.all([\n readFile(paths.keyFile(hostname), \"utf-8\"),\n readFile(paths.certFile(hostname), \"utf-8\"),\n readFile(paths.chainFile(hostname), \"utf-8\"),\n ]);\n return { key, cert, chain };\n}\n\n/**\n * Create the HTTP-01 challenge handler for the ACME server.\n * This is used as middleware to respond to /.well-known/acme-challenge/ requests.\n */\nexport function handleAcmeChallenge(\n url: string\n): string | null {\n const prefix = \"/.well-known/acme-challenge/\";\n if (!url.startsWith(prefix)) return null;\n const token = url.slice(prefix.length);\n return challengeTokens.get(token) ?? null;\n}\n\n/**\n * Obtain a certificate for a hostname via Let's Encrypt ACME.\n */\nasync function obtainCert(\n hostname: string,\n staging: boolean\n): Promise<CertFiles> {\n const accountKey = await getAccountKey();\n\n const directoryUrl = staging\n ? acme.directory.letsencrypt.staging\n : acme.directory.letsencrypt.production;\n\n const client = new acme.Client({\n directoryUrl,\n accountKey,\n });\n\n // Create a CSR\n const [certKey, csr] = await acme.crypto.createCsr({\n commonName: hostname,\n });\n\n // Order the certificate\n const cert = await client.auto({\n csr,\n email: \"porterman@localhost\",\n termsOfServiceAgreed: true,\n challengeCreateFn: async (_authz, _challenge, keyAuthorization) => {\n const token = _challenge.token;\n logger.verbose(`Setting ACME challenge token: ${token}`);\n challengeTokens.set(token, keyAuthorization);\n },\n challengeRemoveFn: async (_authz, _challenge) => {\n const token = _challenge.token;\n challengeTokens.delete(token);\n },\n challengePriority: [\"http-01\"],\n });\n\n // Save certificate files\n const certDir = paths.certDir(hostname);\n await mkdir(certDir, { recursive: true });\n\n const keyStr = certKey.toString();\n const certStr = cert.toString();\n\n await Promise.all([\n writeSecureFile(paths.keyFile(hostname), keyStr),\n writeFile(paths.certFile(hostname), certStr),\n writeFile(paths.chainFile(hostname), certStr),\n ]);\n\n // Save metadata\n const now = new Date();\n const meta: CertMeta = {\n issuedAt: now.toISOString(),\n // Let's Encrypt certs are valid for 90 days\n expiresAt: new Date(now.getTime() + 90 * 24 * 60 * 60 * 1000).toISOString(),\n domains: [hostname],\n };\n await writeFile(paths.metaFile(hostname), JSON.stringify(meta, null, 2));\n\n return { key: keyStr, cert: certStr, chain: certStr };\n}\n\n/**\n * Generate a self-signed certificate as a fallback.\n */\nasync function generateSelfSigned(hostname: string): Promise<CertFiles> {\n const { exec } = await import(\"node:child_process\");\n const { promisify } = await import(\"node:util\");\n const execAsync = promisify(exec);\n\n const certDir = paths.certDir(hostname);\n await mkdir(certDir, { recursive: true });\n\n const keyPath = paths.keyFile(hostname);\n const certPath = paths.certFile(hostname);\n\n await execAsync(\n `openssl req -x509 -newkey rsa:2048 -keyout \"${keyPath}\" -out \"${certPath}\" ` +\n `-days 365 -nodes -subj \"/CN=${hostname}\" 2>/dev/null`\n );\n\n await execAsync(`chmod 600 \"${keyPath}\"`);\n\n const [key, cert] = await Promise.all([\n readFile(keyPath, \"utf-8\"),\n readFile(certPath, \"utf-8\"),\n ]);\n\n // Write chain (same as cert for self-signed)\n await writeFile(paths.chainFile(hostname), cert);\n\n const now = new Date();\n const meta: CertMeta = {\n issuedAt: now.toISOString(),\n expiresAt: new Date(\n now.getTime() + 365 * 24 * 60 * 60 * 1000\n ).toISOString(),\n domains: [hostname],\n };\n await writeFile(paths.metaFile(hostname), JSON.stringify(meta, null, 2));\n\n return { key, cert, chain: cert };\n}\n\nexport interface CertResult {\n key: string;\n cert: string;\n chain: string;\n selfSigned: boolean;\n}\n\n/**\n * Get a valid certificate for a hostname.\n * - First checks for a cached valid cert\n * - Then tries ACME/Let's Encrypt\n * - Falls back to self-signed if ACME fails\n */\nexport async function getCertificate(\n hostname: string,\n options: { staging?: boolean; forceRenew?: boolean } = {}\n): Promise<CertResult> {\n await ensureDirs();\n\n // Check for existing valid cert\n if (!options.forceRenew && (await isCertValid(hostname))) {\n logger.verbose(`Using cached certificate for ${hostname}`);\n const files = await loadCertFromDisk(hostname);\n return { ...files, selfSigned: false };\n }\n\n // Try ACME\n try {\n logger.info(`Obtaining SSL certificate for ${hostname}...`);\n const files = await obtainCert(hostname, options.staging ?? false);\n logger.success(`Certificate obtained for ${hostname}`);\n return { ...files, selfSigned: false };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n logger.warn(`Let's Encrypt failed: ${message}`);\n logger.warn(\"Falling back to self-signed certificate\");\n logger.warn(\n \"Browsers will show a security warning. Consider using a custom domain.\"\n );\n\n const files = await generateSelfSigned(hostname);\n return { ...files, selfSigned: true };\n }\n}\n\n/**\n * Remove all cached certificates.\n */\nexport async function cleanCerts(): Promise<void> {\n const { rm } = await import(\"node:fs/promises\");\n if (existsSync(paths.certs)) {\n await rm(paths.certs, { recursive: true, force: true });\n await mkdir(paths.certs, { recursive: true });\n logger.success(\"All cached certificates removed\");\n }\n}\n","import httpProxy from \"http-proxy\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { logger } from \"./logger.js\";\nimport { parsePortFromHost } from \"./utils.js\";\n\nexport interface ProxyRoute {\n hostname: string;\n targetPort: number;\n name?: string;\n}\n\ninterface ProxyOptions {\n timeout: number;\n routes: ProxyRoute[];\n nameMap?: Map<string, number>; // name → target port\n}\n\nconst ERROR_502_HTML = (port: number) => `<!DOCTYPE html>\n<html>\n<head><title>502 Bad Gateway</title>\n<style>\n body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; max-width: 600px; margin: 80px auto; padding: 0 20px; color: #333; }\n h1 { color: #e74c3c; }\n code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; }\n</style>\n</head>\n<body>\n <h1>502 Bad Gateway</h1>\n <p>Nothing is running on <code>localhost:${port}</code></p>\n <p>Make sure your application is started and listening on port <strong>${port}</strong>.</p>\n <hr>\n <p><small>Porterman</small></p>\n</body>\n</html>`;\n\nexport function createProxyEngine(options: ProxyOptions) {\n const { timeout, routes, nameMap } = options;\n\n // Build port lookup: hostname → target port\n const routeMap = new Map<string, number>();\n for (const route of routes) {\n routeMap.set(route.hostname, route.targetPort);\n }\n\n const proxy = httpProxy.createProxyServer({\n xfwd: true, // sets X-Forwarded-* headers\n ws: true,\n proxyTimeout: timeout * 1000,\n timeout: timeout * 1000,\n });\n\n proxy.on(\"error\", (err, req, res) => {\n const host = req.headers.host ?? \"unknown\";\n const targetPort = resolveTargetPort(host);\n logger.verbose(`Proxy error for ${host}: ${err.message}`);\n\n if (res && \"writeHead\" in res) {\n const serverRes = res as ServerResponse;\n if (!serverRes.headersSent) {\n serverRes.writeHead(502, { \"Content-Type\": \"text/html\" });\n serverRes.end(ERROR_502_HTML(targetPort ?? 0));\n }\n }\n });\n\n function resolveTargetPort(host: string): number | null {\n // Normalize: remove port suffix\n const hostname = host.split(\":\")[0];\n\n // Direct hostname match\n if (routeMap.has(hostname)) {\n return routeMap.get(hostname)!;\n }\n\n // Try parsing port from hostname\n const port = parsePortFromHost(host);\n if (port !== null) {\n // Verify this port is in our routes\n for (const route of routes) {\n if (route.targetPort === port) return port;\n }\n }\n\n // Check name map\n if (nameMap) {\n const prefix = hostname.split(\"-\")[0];\n if (nameMap.has(prefix)) {\n return nameMap.get(prefix)!;\n }\n }\n\n return null;\n }\n\n function handleRequest(req: IncomingMessage, res: ServerResponse): boolean {\n const host = req.headers.host;\n if (!host) {\n res.writeHead(400, { \"Content-Type\": \"text/plain\" });\n res.end(\"Bad Request: No Host header\");\n return false;\n }\n\n const targetPort = resolveTargetPort(host);\n if (targetPort === null) {\n res.writeHead(404, { \"Content-Type\": \"text/plain\" });\n res.end(`Not Found: No route configured for ${host}`);\n return false;\n }\n\n logger.verbose(`Proxying ${req.method} ${host}${req.url} → localhost:${targetPort}`);\n\n proxy.web(req, res, {\n target: `http://127.0.0.1:${targetPort}`,\n });\n\n return true;\n }\n\n function handleUpgrade(\n req: IncomingMessage,\n socket: import(\"node:stream\").Duplex,\n head: Buffer\n ): boolean {\n const host = req.headers.host;\n if (!host) return false;\n\n const targetPort = resolveTargetPort(host);\n if (targetPort === null) return false;\n\n logger.verbose(`WebSocket upgrade ${host} → localhost:${targetPort}`);\n\n proxy.ws(req, socket, head, {\n target: `http://127.0.0.1:${targetPort}`,\n });\n\n return true;\n }\n\n function close(): void {\n proxy.close();\n }\n\n return { handleRequest, handleUpgrade, close };\n}\n","import { createServer as createHttpServer, type IncomingMessage, type ServerResponse } from \"node:http\";\nimport { createServer as createHttpsServer } from \"node:https\";\nimport { createSecureContext, type SecureContext } from \"node:tls\";\nimport { getDashedIp, detectPublicIp } from \"./ip.js\";\nimport { getCertificate, handleAcmeChallenge, type CertResult } from \"./certs.js\";\nimport { createProxyEngine, type ProxyRoute } from \"./proxy.js\";\nimport { makeHostname, isPortAvailable, isValidPort } from \"./utils.js\";\nimport { writePidFile, paths } from \"./config.js\";\nimport { logger, setVerbose } from \"./logger.js\";\nimport { readFileSync } from \"node:fs\";\n\nexport interface ServerOptions {\n ports: number[];\n name?: string;\n noSsl?: boolean;\n verbose?: boolean;\n timeout?: number;\n host?: string;\n staging?: boolean;\n httpPort?: number;\n httpsPort?: number;\n auth?: string;\n ipAllow?: string[];\n}\n\nexport interface PortermanServer {\n close(): Promise<void>;\n urls: Map<number, string>;\n}\n\nexport async function startServer(options: ServerOptions): Promise<PortermanServer> {\n const {\n ports,\n name,\n noSsl = false,\n verbose = false,\n timeout = 30,\n host,\n staging = false,\n httpPort = 80,\n httpsPort = 443,\n auth,\n ipAllow,\n } = options;\n\n setVerbose(verbose);\n\n // Validate ports\n for (const port of ports) {\n if (!isValidPort(port)) {\n throw new Error(`Invalid port number: ${port}`);\n }\n }\n\n if (name && ports.length > 1) {\n throw new Error(\"--name can only be used with a single port\");\n }\n\n // Detect public IP\n logger.info(\"Detecting public IP...\");\n const publicIp = host ?? (await detectPublicIp());\n const dashedIp = await getDashedIp(host);\n logger.info(`Public IP: ${publicIp}`);\n\n // Generate hostnames\n const routes: ProxyRoute[] = ports.map((port) => {\n const prefix = name && ports.length === 1 ? name : String(port);\n return {\n hostname: makeHostname(prefix, dashedIp),\n targetPort: port,\n name: name && ports.length === 1 ? name : undefined,\n };\n });\n\n // Build name map if using custom names\n const nameMap = new Map<string, number>();\n if (name && ports.length === 1) {\n nameMap.set(name, ports[0]);\n }\n\n // Check port availability\n if (!noSsl) {\n if (!(await isPortAvailable(httpsPort))) {\n throw new Error(\n `Port ${httpsPort} is already in use. Try:\\n` +\n ` - Run with sudo if port < 1024\\n` +\n ` - Use --https-port <port> to specify a different port\\n` +\n ` - Stop the process using port ${httpsPort}`\n );\n }\n }\n\n if (!(await isPortAvailable(httpPort))) {\n throw new Error(\n `Port ${httpPort} is already in use. Try:\\n` +\n ` - Run with sudo if port < 1024\\n` +\n ` - Use --http-port <port> to specify a different port\\n` +\n ` - Stop the process using port ${httpPort}`\n );\n }\n\n // Create proxy engine\n const proxyEngine = createProxyEngine({ timeout, routes, nameMap });\n\n // Parse basic auth credentials if provided\n let authCredentials: { user: string; pass: string } | null = null;\n if (auth) {\n const [user, pass] = auth.split(\":\");\n if (!user || !pass) {\n throw new Error(\"--auth must be in format user:pass\");\n }\n authCredentials = { user, pass };\n }\n\n // Parse allowed IPs\n const allowedIps = ipAllow ? new Set(ipAllow) : null;\n\n // Middleware: auth check\n function checkAuth(req: IncomingMessage, res: ServerResponse): boolean {\n if (!authCredentials) return true;\n\n const authHeader = req.headers.authorization;\n if (!authHeader || !authHeader.startsWith(\"Basic \")) {\n res.writeHead(401, {\n \"WWW-Authenticate\": 'Basic realm=\"Porterman\"',\n \"Content-Type\": \"text/plain\",\n });\n res.end(\"Authentication required\");\n return false;\n }\n\n const decoded = Buffer.from(authHeader.slice(6), \"base64\").toString();\n const [user, pass] = decoded.split(\":\");\n if (user !== authCredentials.user || pass !== authCredentials.pass) {\n res.writeHead(403, { \"Content-Type\": \"text/plain\" });\n res.end(\"Forbidden\");\n return false;\n }\n\n return true;\n }\n\n // Middleware: IP allow check\n function checkIpAllow(req: IncomingMessage, res: ServerResponse): boolean {\n if (!allowedIps) return true;\n\n const clientIp =\n (req.headers[\"x-forwarded-for\"] as string)?.split(\",\")[0]?.trim() ??\n req.socket.remoteAddress ??\n \"\";\n\n // Normalize IPv6-mapped IPv4\n const normalizedIp = clientIp.replace(/^::ffff:/, \"\");\n\n if (!allowedIps.has(normalizedIp)) {\n res.writeHead(403, { \"Content-Type\": \"text/plain\" });\n res.end(\"Forbidden: IP not allowed\");\n return false;\n }\n\n return true;\n }\n\n // HTTP request handler\n function httpRequestHandler(req: IncomingMessage, res: ServerResponse): void {\n // Handle ACME challenges\n if (req.url) {\n const challengeResponse = handleAcmeChallenge(req.url);\n if (challengeResponse) {\n res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n res.end(challengeResponse);\n return;\n }\n }\n\n if (noSsl) {\n // In no-ssl mode, HTTP server handles proxying\n if (!checkIpAllow(req, res)) return;\n if (!checkAuth(req, res)) return;\n proxyEngine.handleRequest(req, res);\n return;\n }\n\n // Redirect HTTP to HTTPS\n const host = req.headers.host ?? \"\";\n const httpsUrl = `https://${host.split(\":\")[0]}${httpsPort !== 443 ? `:${httpsPort}` : \"\"}${req.url ?? \"/\"}`;\n res.writeHead(301, { Location: httpsUrl });\n res.end();\n }\n\n // Start HTTP server\n const httpServer = createHttpServer(httpRequestHandler);\n\n // Handle WebSocket upgrades on HTTP server (no-ssl mode)\n if (noSsl) {\n httpServer.on(\"upgrade\", (req, socket, head) => {\n proxyEngine.handleUpgrade(req, socket, head);\n });\n }\n\n let httpsServer: ReturnType<typeof createHttpsServer> | null = null;\n const certCache = new Map<string, CertResult>();\n\n if (!noSsl) {\n // Obtain certificates for all hostnames\n logger.info(\"Obtaining SSL certificates...\");\n\n for (const route of routes) {\n const cert = await getCertificate(route.hostname, { staging });\n certCache.set(route.hostname, cert);\n if (cert.selfSigned) {\n logger.warn(\n `Using self-signed certificate for ${route.hostname} (browsers will show a warning)`\n );\n }\n }\n\n // SNI callback for multi-cert support\n const sniCallback = (\n servername: string,\n callback: (err: Error | null, ctx?: SecureContext) => void\n ): void => {\n const cert = certCache.get(servername);\n if (cert) {\n const ctx = createSecureContext({\n key: cert.key,\n cert: cert.cert,\n });\n callback(null, ctx);\n } else {\n // Try to find a matching cert\n for (const [hostname, c] of certCache) {\n if (servername.endsWith(hostname.slice(hostname.indexOf(\".\")))) {\n const ctx = createSecureContext({\n key: c.key,\n cert: c.cert,\n });\n callback(null, ctx);\n return;\n }\n }\n callback(new Error(`No certificate for ${servername}`));\n }\n };\n\n // Use the first cert as default\n const defaultCert = certCache.values().next().value!;\n\n httpsServer = createHttpsServer(\n {\n key: defaultCert.key,\n cert: defaultCert.cert,\n SNICallback: sniCallback,\n },\n (req, res) => {\n if (!checkIpAllow(req, res)) return;\n if (!checkAuth(req, res)) return;\n proxyEngine.handleRequest(req, res);\n }\n );\n\n // Handle WebSocket upgrades on HTTPS\n httpsServer.on(\"upgrade\", (req, socket, head) => {\n proxyEngine.handleUpgrade(req, socket, head);\n });\n\n // Start HTTPS server\n await new Promise<void>((resolve, reject) => {\n httpsServer!.listen(httpsPort, () => resolve());\n httpsServer!.once(\"error\", reject);\n });\n\n logger.verbose(`HTTPS server listening on port ${httpsPort}`);\n }\n\n // Start HTTP server\n await new Promise<void>((resolve, reject) => {\n httpServer.listen(httpPort, () => resolve());\n httpServer.once(\"error\", reject);\n });\n\n logger.verbose(`HTTP server listening on port ${httpPort}`);\n\n // Write PID file\n await writePidFile(process.pid);\n\n // Build URL map\n const urls = new Map<number, string>();\n for (const route of routes) {\n const protocol = noSsl ? \"http\" : \"https\";\n const portSuffix =\n (!noSsl && httpsPort !== 443)\n ? `:${httpsPort}`\n : (noSsl && httpPort !== 80)\n ? `:${httpPort}`\n : \"\";\n urls.set(route.targetPort, `${protocol}://${route.hostname}${portSuffix}`);\n }\n\n // Print ready message\n logger.blank();\n logger.success(\"Ready!\");\n logger.blank();\n for (const route of routes) {\n const url = urls.get(route.targetPort)!;\n logger.link(`http://localhost:${route.targetPort}`, url);\n }\n logger.blank();\n console.log(\" Press Ctrl+C to stop\");\n logger.blank();\n\n // Graceful shutdown\n async function close(): Promise<void> {\n logger.info(\"Shutting down...\");\n proxyEngine.close();\n\n const closePromises: Promise<void>[] = [];\n\n closePromises.push(\n new Promise<void>((resolve) => httpServer.close(() => resolve()))\n );\n\n if (httpsServer) {\n closePromises.push(\n new Promise<void>((resolve) => httpsServer!.close(() => resolve()))\n );\n }\n\n await Promise.all(closePromises);\n\n // Clean up PID file\n try {\n const { unlink } = await import(\"node:fs/promises\");\n await unlink(paths.pidFile);\n } catch {}\n\n logger.success(\"Stopped\");\n }\n\n return { close, urls };\n}\n"],"mappings":";AAAA,IAAM,QAAQ;AACd,IAAM,OAAO;AACb,IAAM,MAAM;AACZ,IAAM,MAAM;AACZ,IAAM,QAAQ;AACd,IAAM,SAAS;AAEf,IAAM,OAAO;AAGb,IAAI,iBAAiB;AAEd,SAAS,WAAW,SAAwB;AACjD,mBAAiB;AACnB;AAMO,IAAM,SAAS;AAAA,EACpB,KAAK,SAAuB;AAC1B,YAAQ,IAAI,GAAG,IAAI,SAAI,KAAK,IAAI,OAAO,EAAE;AAAA,EAC3C;AAAA,EAEA,QAAQ,SAAuB;AAC7B,YAAQ,IAAI,GAAG,KAAK,SAAI,KAAK,IAAI,OAAO,EAAE;AAAA,EAC5C;AAAA,EAEA,KAAK,SAAuB;AAC1B,YAAQ,IAAI,GAAG,MAAM,SAAI,KAAK,KAAK,OAAO,EAAE;AAAA,EAC9C;AAAA,EAEA,MAAM,SAAuB;AAC3B,YAAQ,MAAM,GAAG,GAAG,SAAI,KAAK,IAAI,OAAO,EAAE;AAAA,EAC5C;AAAA,EAEA,OAAO,SAAuB;AAC5B,YAAQ,IAAI,aAAM,OAAO,EAAE;AAAA,EAC7B;AAAA,EAEA,KAAK,OAAe,KAAmB;AACrC,YAAQ,IAAI,KAAK,KAAK,GAAG,GAAG,GAAG,KAAK,WAAM,GAAG,GAAG,KAAK,GAAG,KAAK,EAAE;AAAA,EACjE;AAAA,EAEA,QAAQ,SAAuB;AAC7B,QAAI,gBAAgB;AAClB,cAAQ,IAAI,GAAG,GAAG,aAAa,OAAO,GAAG,KAAK,EAAE;AAAA,IAClD;AAAA,EACF;AAAA,EAEA,QAAQ,QAAgB,MAAc,MAAc,QAAsB;AACxE,QAAI,CAAC,eAAgB;AACrB,UAAM,QAAQ,SAAS,MAAM,QAAQ,SAAS,MAAM,SAAS;AAC7D,UAAM,QAAO,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,IAAI,EAAE;AAClD,YAAQ;AAAA,MACN,GAAG,GAAG,GAAG,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,MAAM,GAAG,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,KAAK,GAAG,MAAM,GAAG,KAAK;AAAA,IACzF;AAAA,EACF;AAAA,EAEA,OAAO,SAAuB;AAC5B,YAAQ,IAAI;AAAA,EAAK,IAAI,wBAAiB,OAAO,GAAG,KAAK,EAAE;AAAA,EACzD;AAAA,EAEA,QAAc;AACZ,YAAQ,IAAI;AAAA,EACd;AACF;;;ACnEA,SAAS,oBAAiC;AAMnC,SAAS,WAAW,IAAoB;AAC7C,SAAO,GAAG,QAAQ,OAAO,GAAG,EAAE,QAAQ,MAAM,GAAG;AACjD;AAKO,SAAS,aACd,MACA,UACQ;AACR,SAAO,GAAG,IAAI,IAAI,QAAQ;AAC5B;AAMO,SAAS,kBAAkB,MAA6B;AAE7D,QAAM,WAAW,KAAK,MAAM,GAAG,EAAE,CAAC;AAElC,QAAM,QAAQ,SAAS,MAAM,aAAa;AAC1C,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,SAAS,MAAM,CAAC;AACtB,QAAM,OAAO,SAAS,QAAQ,EAAE;AAChC,SAAO,MAAM,IAAI,IAAI,OAAO;AAC9B;AAeO,SAAS,YAAY,IAAqB;AAC/C,QAAM,QAAQ,GAAG,MAAM,GAAG,EAAE,IAAI,MAAM;AACtC,MAAI,MAAM,WAAW,KAAK,MAAM,KAAK,CAAC,MAAM,MAAM,CAAC,CAAC,EAAG,QAAO;AAG9D,MAAI,MAAM,CAAC,MAAM,GAAI,QAAO;AAE5B,MAAI,MAAM,CAAC,MAAM,OAAO,MAAM,CAAC,KAAK,MAAM,MAAM,CAAC,KAAK,GAAI,QAAO;AAEjE,MAAI,MAAM,CAAC,MAAM,OAAO,MAAM,CAAC,MAAM,IAAK,QAAO;AAEjD,MAAI,MAAM,CAAC,MAAM,IAAK,QAAO;AAE7B,MAAI,MAAM,MAAM,CAAC,MAAM,MAAM,CAAC,EAAG,QAAO;AAExC,MAAI,MAAM,CAAC,MAAM,OAAO,MAAM,CAAC,MAAM,IAAK,QAAO;AAEjD,SAAO;AACT;AAKO,SAAS,gBAAgB,MAAgC;AAC9D,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAM,SAAS,aAAa;AAC5B,WAAO,KAAK,SAAS,MAAM,QAAQ,KAAK,CAAC;AACzC,WAAO,KAAK,aAAa,MAAM;AAC7B,aAAO,MAAM,MAAM,QAAQ,IAAI,CAAC;AAAA,IAClC,CAAC;AACD,WAAO,OAAO,MAAM,SAAS;AAAA,EAC/B,CAAC;AACH;AAKO,SAAS,YAAY,MAAuB;AACjD,SAAO,OAAO,UAAU,IAAI,KAAK,QAAQ,KAAK,QAAQ;AACxD;;;ACpFA,IAAM,cAAc;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAI,WAA0B;AAK9B,eAAe,YAAY,KAAqC;AAC9D,MAAI;AACF,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,UAAU,WAAW,MAAM,WAAW,MAAM,GAAG,GAAI;AACzD,UAAM,MAAM,MAAM,MAAM,KAAK;AAAA,MAC3B,QAAQ,WAAW;AAAA,MACnB,SAAS,EAAE,QAAQ,aAAa;AAAA,IAClC,CAAC;AACD,iBAAa,OAAO;AACpB,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,WAAO,KAAK,KAAK;AAAA,EACnB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,eAAsB,iBAAkC;AACtD,MAAI,SAAU,QAAO;AAErB,aAAW,WAAW,aAAa;AACjC,WAAO,QAAQ,2BAA2B,OAAO,EAAE;AACnD,UAAM,KAAK,MAAM,YAAY,OAAO;AACpC,QAAI,MAAM,uCAAuC,KAAK,EAAE,GAAG;AACzD,UAAI,YAAY,EAAE,GAAG;AACnB,cAAM,IAAI;AAAA,UACR,gCAAgC,EAAE;AAAA;AAAA;AAAA,QAGpC;AAAA,MACF;AACA,iBAAW;AACX,aAAO,QAAQ,uBAAuB,EAAE,EAAE;AAC1C,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,IAAI;AAAA,IACR;AAAA,EAEF;AACF;AAKA,eAAsB,YAAY,YAAsC;AACtE,QAAM,KAAK,cAAe,MAAM,eAAe;AAC/C,MAAI,cAAc,YAAY,UAAU,GAAG;AACzC,WAAO;AAAA,MACL,gBAAgB,UAAU;AAAA,IAC5B;AAAA,EACF;AACA,SAAO,WAAW,EAAE;AACtB;;;ACxEA,SAAS,OAAO,UAAU,iBAAwB;AAClD,SAAS,kBAAkB;AAC3B,SAAS,eAAe;AACxB,SAAS,YAAY;AAErB,IAAM,gBAAgB,KAAK,QAAQ,GAAG,YAAY;AAClD,IAAM,cAAc,KAAK,eAAe,aAAa;AACrD,IAAM,YAAY,KAAK,eAAe,OAAO;AAC7C,IAAM,mBAAmB,KAAK,eAAe,aAAa;AAC1D,IAAM,WAAW,KAAK,eAAe,eAAe;AAQ7C,IAAM,QAAQ;AAAA,EACnB,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,SAAS;AAAA,EAET,QAAQ,UAA0B;AAChC,WAAO,KAAK,WAAW,QAAQ;AAAA,EACjC;AAAA,EAEA,SAAS,UAA0B;AACjC,WAAO,KAAK,WAAW,UAAU,UAAU;AAAA,EAC7C;AAAA,EAEA,QAAQ,UAA0B;AAChC,WAAO,KAAK,WAAW,UAAU,aAAa;AAAA,EAChD;AAAA,EAEA,UAAU,UAA0B;AAClC,WAAO,KAAK,WAAW,UAAU,WAAW;AAAA,EAC9C;AAAA,EAEA,SAAS,UAA0B;AACjC,WAAO,KAAK,WAAW,UAAU,WAAW;AAAA,EAC9C;AACF;AAEA,eAAsB,aAA4B;AAChD,QAAM,MAAM,eAAe,EAAE,WAAW,KAAK,CAAC;AAC9C,QAAM,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;AAC5C;AAgBA,eAAsB,gBACpB,UACA,SACe;AACf,QAAM,UAAU,UAAU,SAAS,EAAE,MAAM,IAAM,CAAC;AACpD;AAEA,eAAsB,aAAa,KAA4B;AAC7D,QAAM,WAAW;AACjB,QAAM,UAAU,UAAU,OAAO,GAAG,CAAC;AACvC;AAEA,eAAsB,cAAsC;AAC1D,MAAI;AACF,UAAM,OAAO,MAAM,SAAS,UAAU,OAAO;AAC7C,UAAM,MAAM,SAAS,KAAK,KAAK,GAAG,EAAE;AACpC,WAAO,MAAM,GAAG,IAAI,OAAO;AAAA,EAC7B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,gBAAyB;AACvC,SAAO,WAAW,QAAQ;AAC5B;;;ACxFA,YAAY,UAAU;AACtB,SAAS,YAAAA,WAAU,SAAAC,QAAO,aAAAC,kBAAiB;AAC3C,SAAS,cAAAC,mBAAkB;AAkB3B,IAAM,kBAAkB,oBAAI,IAAoB;AAKhD,eAAe,gBAAiC;AAC9C,QAAM,WAAW;AACjB,MAAIC,YAAW,MAAM,UAAU,GAAG;AAChC,WAAOC,UAAS,MAAM,UAAU;AAAA,EAClC;AACA,QAAM,MAAM,MAAW,YAAO,iBAAiB;AAC/C,QAAM,gBAAgB,MAAM,YAAY,IAAI,SAAS,CAAC;AACtD,SAAO;AACT;AAKA,eAAe,YAAY,UAAoC;AAC7D,QAAM,WAAW,MAAM,SAAS,QAAQ;AACxC,MAAI,CAACD,YAAW,QAAQ,EAAG,QAAO;AAElC,MAAI;AACF,UAAM,OAAO,MAAMC,UAAS,UAAU,OAAO;AAC7C,UAAM,OAAiB,KAAK,MAAM,IAAI;AACtC,UAAM,UAAU,IAAI,KAAK,KAAK,SAAS;AACvC,UAAM,iBACH,QAAQ,QAAQ,IAAI,KAAK,IAAI,MAAM,MAAO,KAAK,KAAK;AACvD,QAAI,gBAAgB,IAAI;AACtB,aAAO;AAAA,QACL,mBAAmB,QAAQ,cAAc,KAAK,MAAM,aAAa,CAAC;AAAA,MACpE;AACA,aAAO;AAAA,IACT;AACA,WAAO;AAAA,MACL,mBAAmB,QAAQ,eAAe,KAAK,MAAM,aAAa,CAAC;AAAA,IACrE;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKA,eAAe,iBAAiB,UAAsC;AACpE,QAAM,CAAC,KAAK,MAAM,KAAK,IAAI,MAAM,QAAQ,IAAI;AAAA,IAC3CA,UAAS,MAAM,QAAQ,QAAQ,GAAG,OAAO;AAAA,IACzCA,UAAS,MAAM,SAAS,QAAQ,GAAG,OAAO;AAAA,IAC1CA,UAAS,MAAM,UAAU,QAAQ,GAAG,OAAO;AAAA,EAC7C,CAAC;AACD,SAAO,EAAE,KAAK,MAAM,MAAM;AAC5B;AAMO,SAAS,oBACd,KACe;AACf,QAAM,SAAS;AACf,MAAI,CAAC,IAAI,WAAW,MAAM,EAAG,QAAO;AACpC,QAAM,QAAQ,IAAI,MAAM,OAAO,MAAM;AACrC,SAAO,gBAAgB,IAAI,KAAK,KAAK;AACvC;AAKA,eAAe,WACb,UACA,SACoB;AACpB,QAAM,aAAa,MAAM,cAAc;AAEvC,QAAM,eAAe,UACZ,eAAU,YAAY,UACtB,eAAU,YAAY;AAE/B,QAAM,SAAS,IAAS,YAAO;AAAA,IAC7B;AAAA,IACA;AAAA,EACF,CAAC;AAGD,QAAM,CAAC,SAAS,GAAG,IAAI,MAAW,YAAO,UAAU;AAAA,IACjD,YAAY;AAAA,EACd,CAAC;AAGD,QAAM,OAAO,MAAM,OAAO,KAAK;AAAA,IAC7B;AAAA,IACA,OAAO;AAAA,IACP,sBAAsB;AAAA,IACtB,mBAAmB,OAAO,QAAQ,YAAY,qBAAqB;AACjE,YAAM,QAAQ,WAAW;AACzB,aAAO,QAAQ,iCAAiC,KAAK,EAAE;AACvD,sBAAgB,IAAI,OAAO,gBAAgB;AAAA,IAC7C;AAAA,IACA,mBAAmB,OAAO,QAAQ,eAAe;AAC/C,YAAM,QAAQ,WAAW;AACzB,sBAAgB,OAAO,KAAK;AAAA,IAC9B;AAAA,IACA,mBAAmB,CAAC,SAAS;AAAA,EAC/B,CAAC;AAGD,QAAM,UAAU,MAAM,QAAQ,QAAQ;AACtC,QAAMC,OAAM,SAAS,EAAE,WAAW,KAAK,CAAC;AAExC,QAAM,SAAS,QAAQ,SAAS;AAChC,QAAM,UAAU,KAAK,SAAS;AAE9B,QAAM,QAAQ,IAAI;AAAA,IAChB,gBAAgB,MAAM,QAAQ,QAAQ,GAAG,MAAM;AAAA,IAC/CC,WAAU,MAAM,SAAS,QAAQ,GAAG,OAAO;AAAA,IAC3CA,WAAU,MAAM,UAAU,QAAQ,GAAG,OAAO;AAAA,EAC9C,CAAC;AAGD,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,OAAiB;AAAA,IACrB,UAAU,IAAI,YAAY;AAAA;AAAA,IAE1B,WAAW,IAAI,KAAK,IAAI,QAAQ,IAAI,KAAK,KAAK,KAAK,KAAK,GAAI,EAAE,YAAY;AAAA,IAC1E,SAAS,CAAC,QAAQ;AAAA,EACpB;AACA,QAAMA,WAAU,MAAM,SAAS,QAAQ,GAAG,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;AAEvE,SAAO,EAAE,KAAK,QAAQ,MAAM,SAAS,OAAO,QAAQ;AACtD;AAKA,eAAe,mBAAmB,UAAsC;AACtE,QAAM,EAAE,KAAK,IAAI,MAAM,OAAO,eAAoB;AAClD,QAAM,EAAE,UAAU,IAAI,MAAM,OAAO,MAAW;AAC9C,QAAM,YAAY,UAAU,IAAI;AAEhC,QAAM,UAAU,MAAM,QAAQ,QAAQ;AACtC,QAAMD,OAAM,SAAS,EAAE,WAAW,KAAK,CAAC;AAExC,QAAM,UAAU,MAAM,QAAQ,QAAQ;AACtC,QAAM,WAAW,MAAM,SAAS,QAAQ;AAExC,QAAM;AAAA,IACJ,+CAA+C,OAAO,WAAW,QAAQ,iCACxC,QAAQ;AAAA,EAC3C;AAEA,QAAM,UAAU,cAAc,OAAO,GAAG;AAExC,QAAM,CAAC,KAAK,IAAI,IAAI,MAAM,QAAQ,IAAI;AAAA,IACpCD,UAAS,SAAS,OAAO;AAAA,IACzBA,UAAS,UAAU,OAAO;AAAA,EAC5B,CAAC;AAGD,QAAME,WAAU,MAAM,UAAU,QAAQ,GAAG,IAAI;AAE/C,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,OAAiB;AAAA,IACrB,UAAU,IAAI,YAAY;AAAA,IAC1B,WAAW,IAAI;AAAA,MACb,IAAI,QAAQ,IAAI,MAAM,KAAK,KAAK,KAAK;AAAA,IACvC,EAAE,YAAY;AAAA,IACd,SAAS,CAAC,QAAQ;AAAA,EACpB;AACA,QAAMA,WAAU,MAAM,SAAS,QAAQ,GAAG,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;AAEvE,SAAO,EAAE,KAAK,MAAM,OAAO,KAAK;AAClC;AAeA,eAAsB,eACpB,UACA,UAAuD,CAAC,GACnC;AACrB,QAAM,WAAW;AAGjB,MAAI,CAAC,QAAQ,cAAe,MAAM,YAAY,QAAQ,GAAI;AACxD,WAAO,QAAQ,gCAAgC,QAAQ,EAAE;AACzD,UAAM,QAAQ,MAAM,iBAAiB,QAAQ;AAC7C,WAAO,EAAE,GAAG,OAAO,YAAY,MAAM;AAAA,EACvC;AAGA,MAAI;AACF,WAAO,KAAK,iCAAiC,QAAQ,KAAK;AAC1D,UAAM,QAAQ,MAAM,WAAW,UAAU,QAAQ,WAAW,KAAK;AACjE,WAAO,QAAQ,4BAA4B,QAAQ,EAAE;AACrD,WAAO,EAAE,GAAG,OAAO,YAAY,MAAM;AAAA,EACvC,SAAS,KAAK;AACZ,UAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,WAAO,KAAK,yBAAyB,OAAO,EAAE;AAC9C,WAAO,KAAK,yCAAyC;AACrD,WAAO;AAAA,MACL;AAAA,IACF;AAEA,UAAM,QAAQ,MAAM,mBAAmB,QAAQ;AAC/C,WAAO,EAAE,GAAG,OAAO,YAAY,KAAK;AAAA,EACtC;AACF;AAKA,eAAsB,aAA4B;AAChD,QAAM,EAAE,GAAG,IAAI,MAAM,OAAO,aAAkB;AAC9C,MAAIH,YAAW,MAAM,KAAK,GAAG;AAC3B,UAAM,GAAG,MAAM,OAAO,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AACtD,UAAME,OAAM,MAAM,OAAO,EAAE,WAAW,KAAK,CAAC;AAC5C,WAAO,QAAQ,iCAAiC;AAAA,EAClD;AACF;;;AC3PA,OAAO,eAAe;AAiBtB,IAAM,iBAAiB,CAAC,SAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,6CAWI,IAAI;AAAA,2EAC0B,IAAI;AAAA;AAAA;AAAA;AAAA;AAMxE,SAAS,kBAAkB,SAAuB;AACvD,QAAM,EAAE,SAAS,QAAQ,QAAQ,IAAI;AAGrC,QAAM,WAAW,oBAAI,IAAoB;AACzC,aAAW,SAAS,QAAQ;AAC1B,aAAS,IAAI,MAAM,UAAU,MAAM,UAAU;AAAA,EAC/C;AAEA,QAAM,QAAQ,UAAU,kBAAkB;AAAA,IACxC,MAAM;AAAA;AAAA,IACN,IAAI;AAAA,IACJ,cAAc,UAAU;AAAA,IACxB,SAAS,UAAU;AAAA,EACrB,CAAC;AAED,QAAM,GAAG,SAAS,CAAC,KAAK,KAAK,QAAQ;AACnC,UAAM,OAAO,IAAI,QAAQ,QAAQ;AACjC,UAAM,aAAa,kBAAkB,IAAI;AACzC,WAAO,QAAQ,mBAAmB,IAAI,KAAK,IAAI,OAAO,EAAE;AAExD,QAAI,OAAO,eAAe,KAAK;AAC7B,YAAM,YAAY;AAClB,UAAI,CAAC,UAAU,aAAa;AAC1B,kBAAU,UAAU,KAAK,EAAE,gBAAgB,YAAY,CAAC;AACxD,kBAAU,IAAI,eAAe,cAAc,CAAC,CAAC;AAAA,MAC/C;AAAA,IACF;AAAA,EACF,CAAC;AAED,WAAS,kBAAkB,MAA6B;AAEtD,UAAM,WAAW,KAAK,MAAM,GAAG,EAAE,CAAC;AAGlC,QAAI,SAAS,IAAI,QAAQ,GAAG;AAC1B,aAAO,SAAS,IAAI,QAAQ;AAAA,IAC9B;AAGA,UAAM,OAAO,kBAAkB,IAAI;AACnC,QAAI,SAAS,MAAM;AAEjB,iBAAW,SAAS,QAAQ;AAC1B,YAAI,MAAM,eAAe,KAAM,QAAO;AAAA,MACxC;AAAA,IACF;AAGA,QAAI,SAAS;AACX,YAAM,SAAS,SAAS,MAAM,GAAG,EAAE,CAAC;AACpC,UAAI,QAAQ,IAAI,MAAM,GAAG;AACvB,eAAO,QAAQ,IAAI,MAAM;AAAA,MAC3B;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAEA,WAAS,cAAc,KAAsB,KAA8B;AACzE,UAAM,OAAO,IAAI,QAAQ;AACzB,QAAI,CAAC,MAAM;AACT,UAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;AACnD,UAAI,IAAI,6BAA6B;AACrC,aAAO;AAAA,IACT;AAEA,UAAM,aAAa,kBAAkB,IAAI;AACzC,QAAI,eAAe,MAAM;AACvB,UAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;AACnD,UAAI,IAAI,sCAAsC,IAAI,EAAE;AACpD,aAAO;AAAA,IACT;AAEA,WAAO,QAAQ,YAAY,IAAI,MAAM,IAAI,IAAI,GAAG,IAAI,GAAG,qBAAgB,UAAU,EAAE;AAEnF,UAAM,IAAI,KAAK,KAAK;AAAA,MAClB,QAAQ,oBAAoB,UAAU;AAAA,IACxC,CAAC;AAED,WAAO;AAAA,EACT;AAEA,WAAS,cACP,KACA,QACA,MACS;AACT,UAAM,OAAO,IAAI,QAAQ;AACzB,QAAI,CAAC,KAAM,QAAO;AAElB,UAAM,aAAa,kBAAkB,IAAI;AACzC,QAAI,eAAe,KAAM,QAAO;AAEhC,WAAO,QAAQ,qBAAqB,IAAI,qBAAgB,UAAU,EAAE;AAEpE,UAAM,GAAG,KAAK,QAAQ,MAAM;AAAA,MAC1B,QAAQ,oBAAoB,UAAU;AAAA,IACxC,CAAC;AAED,WAAO;AAAA,EACT;AAEA,WAAS,QAAc;AACrB,UAAM,MAAM;AAAA,EACd;AAEA,SAAO,EAAE,eAAe,eAAe,MAAM;AAC/C;;;AC/IA,SAAS,gBAAgB,wBAAmE;AAC5F,SAAS,gBAAgB,yBAAyB;AAClD,SAAS,2BAA+C;AA4BxD,eAAsB,YAAY,SAAkD;AAClF,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,UAAU;AAAA,IACV;AAAA,IACA,UAAU;AAAA,IACV,WAAW;AAAA,IACX,YAAY;AAAA,IACZ;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,aAAW,OAAO;AAGlB,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,YAAY,IAAI,GAAG;AACtB,YAAM,IAAI,MAAM,wBAAwB,IAAI,EAAE;AAAA,IAChD;AAAA,EACF;AAEA,MAAI,QAAQ,MAAM,SAAS,GAAG;AAC5B,UAAM,IAAI,MAAM,4CAA4C;AAAA,EAC9D;AAGA,SAAO,KAAK,wBAAwB;AACpC,QAAM,WAAW,QAAS,MAAM,eAAe;AAC/C,QAAM,WAAW,MAAM,YAAY,IAAI;AACvC,SAAO,KAAK,cAAc,QAAQ,EAAE;AAGpC,QAAM,SAAuB,MAAM,IAAI,CAAC,SAAS;AAC/C,UAAM,SAAS,QAAQ,MAAM,WAAW,IAAI,OAAO,OAAO,IAAI;AAC9D,WAAO;AAAA,MACL,UAAU,aAAa,QAAQ,QAAQ;AAAA,MACvC,YAAY;AAAA,MACZ,MAAM,QAAQ,MAAM,WAAW,IAAI,OAAO;AAAA,IAC5C;AAAA,EACF,CAAC;AAGD,QAAM,UAAU,oBAAI,IAAoB;AACxC,MAAI,QAAQ,MAAM,WAAW,GAAG;AAC9B,YAAQ,IAAI,MAAM,MAAM,CAAC,CAAC;AAAA,EAC5B;AAGA,MAAI,CAAC,OAAO;AACV,QAAI,CAAE,MAAM,gBAAgB,SAAS,GAAI;AACvC,YAAM,IAAI;AAAA,QACR,QAAQ,SAAS;AAAA;AAAA;AAAA,kCAGoB,SAAS;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AAEA,MAAI,CAAE,MAAM,gBAAgB,QAAQ,GAAI;AACtC,UAAM,IAAI;AAAA,MACR,QAAQ,QAAQ;AAAA;AAAA;AAAA,kCAGqB,QAAQ;AAAA,IAC/C;AAAA,EACF;AAGA,QAAM,cAAc,kBAAkB,EAAE,SAAS,QAAQ,QAAQ,CAAC;AAGlE,MAAI,kBAAyD;AAC7D,MAAI,MAAM;AACR,UAAM,CAAC,MAAM,IAAI,IAAI,KAAK,MAAM,GAAG;AACnC,QAAI,CAAC,QAAQ,CAAC,MAAM;AAClB,YAAM,IAAI,MAAM,oCAAoC;AAAA,IACtD;AACA,sBAAkB,EAAE,MAAM,KAAK;AAAA,EACjC;AAGA,QAAM,aAAa,UAAU,IAAI,IAAI,OAAO,IAAI;AAGhD,WAAS,UAAU,KAAsB,KAA8B;AACrE,QAAI,CAAC,gBAAiB,QAAO;AAE7B,UAAM,aAAa,IAAI,QAAQ;AAC/B,QAAI,CAAC,cAAc,CAAC,WAAW,WAAW,QAAQ,GAAG;AACnD,UAAI,UAAU,KAAK;AAAA,QACjB,oBAAoB;AAAA,QACpB,gBAAgB;AAAA,MAClB,CAAC;AACD,UAAI,IAAI,yBAAyB;AACjC,aAAO;AAAA,IACT;AAEA,UAAM,UAAU,OAAO,KAAK,WAAW,MAAM,CAAC,GAAG,QAAQ,EAAE,SAAS;AACpE,UAAM,CAAC,MAAM,IAAI,IAAI,QAAQ,MAAM,GAAG;AACtC,QAAI,SAAS,gBAAgB,QAAQ,SAAS,gBAAgB,MAAM;AAClE,UAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;AACnD,UAAI,IAAI,WAAW;AACnB,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,EACT;AAGA,WAAS,aAAa,KAAsB,KAA8B;AACxE,QAAI,CAAC,WAAY,QAAO;AAExB,UAAM,WACH,IAAI,QAAQ,iBAAiB,GAAc,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,KAChE,IAAI,OAAO,iBACX;AAGF,UAAM,eAAe,SAAS,QAAQ,YAAY,EAAE;AAEpD,QAAI,CAAC,WAAW,IAAI,YAAY,GAAG;AACjC,UAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;AACnD,UAAI,IAAI,2BAA2B;AACnC,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,EACT;AAGA,WAAS,mBAAmB,KAAsB,KAA2B;AAE3E,QAAI,IAAI,KAAK;AACX,YAAM,oBAAoB,oBAAoB,IAAI,GAAG;AACrD,UAAI,mBAAmB;AACrB,YAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;AACnD,YAAI,IAAI,iBAAiB;AACzB;AAAA,MACF;AAAA,IACF;AAEA,QAAI,OAAO;AAET,UAAI,CAAC,aAAa,KAAK,GAAG,EAAG;AAC7B,UAAI,CAAC,UAAU,KAAK,GAAG,EAAG;AAC1B,kBAAY,cAAc,KAAK,GAAG;AAClC;AAAA,IACF;AAGA,UAAME,QAAO,IAAI,QAAQ,QAAQ;AACjC,UAAM,WAAW,WAAWA,MAAK,MAAM,GAAG,EAAE,CAAC,CAAC,GAAG,cAAc,MAAM,IAAI,SAAS,KAAK,EAAE,GAAG,IAAI,OAAO,GAAG;AAC1G,QAAI,UAAU,KAAK,EAAE,UAAU,SAAS,CAAC;AACzC,QAAI,IAAI;AAAA,EACV;AAGA,QAAM,aAAa,iBAAiB,kBAAkB;AAGtD,MAAI,OAAO;AACT,eAAW,GAAG,WAAW,CAAC,KAAK,QAAQ,SAAS;AAC9C,kBAAY,cAAc,KAAK,QAAQ,IAAI;AAAA,IAC7C,CAAC;AAAA,EACH;AAEA,MAAI,cAA2D;AAC/D,QAAM,YAAY,oBAAI,IAAwB;AAE9C,MAAI,CAAC,OAAO;AAEV,WAAO,KAAK,+BAA+B;AAE3C,eAAW,SAAS,QAAQ;AAC1B,YAAM,OAAO,MAAM,eAAe,MAAM,UAAU,EAAE,QAAQ,CAAC;AAC7D,gBAAU,IAAI,MAAM,UAAU,IAAI;AAClC,UAAI,KAAK,YAAY;AACnB,eAAO;AAAA,UACL,qCAAqC,MAAM,QAAQ;AAAA,QACrD;AAAA,MACF;AAAA,IACF;AAGA,UAAM,cAAc,CAClB,YACA,aACS;AACT,YAAM,OAAO,UAAU,IAAI,UAAU;AACrC,UAAI,MAAM;AACR,cAAM,MAAM,oBAAoB;AAAA,UAC9B,KAAK,KAAK;AAAA,UACV,MAAM,KAAK;AAAA,QACb,CAAC;AACD,iBAAS,MAAM,GAAG;AAAA,MACpB,OAAO;AAEL,mBAAW,CAAC,UAAU,CAAC,KAAK,WAAW;AACrC,cAAI,WAAW,SAAS,SAAS,MAAM,SAAS,QAAQ,GAAG,CAAC,CAAC,GAAG;AAC9D,kBAAM,MAAM,oBAAoB;AAAA,cAC9B,KAAK,EAAE;AAAA,cACP,MAAM,EAAE;AAAA,YACV,CAAC;AACD,qBAAS,MAAM,GAAG;AAClB;AAAA,UACF;AAAA,QACF;AACA,iBAAS,IAAI,MAAM,sBAAsB,UAAU,EAAE,CAAC;AAAA,MACxD;AAAA,IACF;AAGA,UAAM,cAAc,UAAU,OAAO,EAAE,KAAK,EAAE;AAE9C,kBAAc;AAAA,MACZ;AAAA,QACE,KAAK,YAAY;AAAA,QACjB,MAAM,YAAY;AAAA,QAClB,aAAa;AAAA,MACf;AAAA,MACA,CAAC,KAAK,QAAQ;AACZ,YAAI,CAAC,aAAa,KAAK,GAAG,EAAG;AAC7B,YAAI,CAAC,UAAU,KAAK,GAAG,EAAG;AAC1B,oBAAY,cAAc,KAAK,GAAG;AAAA,MACpC;AAAA,IACF;AAGA,gBAAY,GAAG,WAAW,CAAC,KAAK,QAAQ,SAAS;AAC/C,kBAAY,cAAc,KAAK,QAAQ,IAAI;AAAA,IAC7C,CAAC;AAGD,UAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,kBAAa,OAAO,WAAW,MAAM,QAAQ,CAAC;AAC9C,kBAAa,KAAK,SAAS,MAAM;AAAA,IACnC,CAAC;AAED,WAAO,QAAQ,kCAAkC,SAAS,EAAE;AAAA,EAC9D;AAGA,QAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,eAAW,OAAO,UAAU,MAAM,QAAQ,CAAC;AAC3C,eAAW,KAAK,SAAS,MAAM;AAAA,EACjC,CAAC;AAED,SAAO,QAAQ,iCAAiC,QAAQ,EAAE;AAG1D,QAAM,aAAa,QAAQ,GAAG;AAG9B,QAAM,OAAO,oBAAI,IAAoB;AACrC,aAAW,SAAS,QAAQ;AAC1B,UAAM,WAAW,QAAQ,SAAS;AAClC,UAAM,aACH,CAAC,SAAS,cAAc,MACrB,IAAI,SAAS,KACZ,SAAS,aAAa,KACrB,IAAI,QAAQ,KACZ;AACR,SAAK,IAAI,MAAM,YAAY,GAAG,QAAQ,MAAM,MAAM,QAAQ,GAAG,UAAU,EAAE;AAAA,EAC3E;AAGA,SAAO,MAAM;AACb,SAAO,QAAQ,QAAQ;AACvB,SAAO,MAAM;AACb,aAAW,SAAS,QAAQ;AAC1B,UAAM,MAAM,KAAK,IAAI,MAAM,UAAU;AACrC,WAAO,KAAK,oBAAoB,MAAM,UAAU,IAAI,GAAG;AAAA,EACzD;AACA,SAAO,MAAM;AACb,UAAQ,IAAI,wBAAwB;AACpC,SAAO,MAAM;AAGb,iBAAe,QAAuB;AACpC,WAAO,KAAK,kBAAkB;AAC9B,gBAAY,MAAM;AAElB,UAAM,gBAAiC,CAAC;AAExC,kBAAc;AAAA,MACZ,IAAI,QAAc,CAAC,YAAY,WAAW,MAAM,MAAM,QAAQ,CAAC,CAAC;AAAA,IAClE;AAEA,QAAI,aAAa;AACf,oBAAc;AAAA,QACZ,IAAI,QAAc,CAAC,YAAY,YAAa,MAAM,MAAM,QAAQ,CAAC,CAAC;AAAA,MACpE;AAAA,IACF;AAEA,UAAM,QAAQ,IAAI,aAAa;AAG/B,QAAI;AACF,YAAM,EAAE,OAAO,IAAI,MAAM,OAAO,aAAkB;AAClD,YAAM,OAAO,MAAM,OAAO;AAAA,IAC5B,QAAQ;AAAA,IAAC;AAET,WAAO,QAAQ,SAAS;AAAA,EAC1B;AAEA,SAAO,EAAE,OAAO,KAAK;AACvB;","names":["readFile","mkdir","writeFile","existsSync","existsSync","readFile","mkdir","writeFile","host"]}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/cli.js ADDED
@@ -0,0 +1,129 @@
1
+ import {
2
+ cleanCerts,
3
+ logger,
4
+ pidFileExists,
5
+ readPidFile,
6
+ startServer
7
+ } from "./chunk-SWPUBQIV.js";
8
+
9
+ // src/cli.ts
10
+ import cac from "cac";
11
+ import { readFileSync } from "fs";
12
+ import { fileURLToPath } from "url";
13
+ import { dirname, join } from "path";
14
+ var version = "1.0.0";
15
+ try {
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = dirname(__filename);
18
+ for (const rel of ["../package.json", "../../package.json"]) {
19
+ try {
20
+ const pkg = JSON.parse(readFileSync(join(__dirname, rel), "utf-8"));
21
+ version = pkg.version;
22
+ break;
23
+ } catch {
24
+ }
25
+ }
26
+ } catch {
27
+ }
28
+ var cli = cac("porterman");
29
+ cli.command("expose [...ports]", "Expose one or more local ports over HTTPS").option("-n, --name <name>", "Custom subdomain prefix (single port only)").option("--no-ssl", "HTTP only mode (skip SSL)").option("-v, --verbose", "Log all requests").option("--timeout <seconds>", "Proxy timeout in seconds", { default: 30 }).option("--host <ip>", "Override auto-detected public IP").option("--staging", "Use Let's Encrypt staging environment").option("--http-port <port>", "Custom HTTP port", { default: 80 }).option("--https-port <port>", "Custom HTTPS port", { default: 443 }).option("--auth <user:pass>", "Enable basic auth on exposed ports").option("--ip-allow <ips>", "Comma-separated list of allowed IPs").action(async (portsRaw, options) => {
30
+ logger.banner(version);
31
+ logger.blank();
32
+ if (!portsRaw || portsRaw.length === 0) {
33
+ logger.error("Please specify at least one port to expose.");
34
+ logger.blank();
35
+ console.log(" Usage: porterman expose <port> [port2] [port3]");
36
+ console.log(" Example: porterman expose 3000 8080");
37
+ logger.blank();
38
+ process.exit(1);
39
+ }
40
+ const ports = portsRaw.map((p) => {
41
+ const num = parseInt(p, 10);
42
+ if (isNaN(num) || num < 1 || num > 65535) {
43
+ logger.error(`Invalid port: ${p}`);
44
+ process.exit(1);
45
+ }
46
+ return num;
47
+ });
48
+ const ipAllow = options.ipAllow ? options.ipAllow.split(",").map((ip) => ip.trim()) : void 0;
49
+ let server = null;
50
+ try {
51
+ server = await startServer({
52
+ ports,
53
+ name: options.name,
54
+ noSsl: options.ssl === false,
55
+ verbose: options.verbose,
56
+ timeout: Number(options.timeout),
57
+ host: options.host,
58
+ staging: options.staging,
59
+ httpPort: Number(options.httpPort),
60
+ httpsPort: Number(options.httpsPort),
61
+ auth: options.auth,
62
+ ipAllow
63
+ });
64
+ } catch (err) {
65
+ logger.error(err instanceof Error ? err.message : String(err));
66
+ process.exit(1);
67
+ }
68
+ const shutdown = async () => {
69
+ if (server) {
70
+ await server.close();
71
+ }
72
+ process.exit(0);
73
+ };
74
+ process.on("SIGINT", shutdown);
75
+ process.on("SIGTERM", shutdown);
76
+ });
77
+ cli.command("status", "Show running Porterman instance info").action(async () => {
78
+ if (!pidFileExists()) {
79
+ logger.info("No Porterman instance is running.");
80
+ return;
81
+ }
82
+ const pid = await readPidFile();
83
+ if (pid === null) {
84
+ logger.info("No Porterman instance is running.");
85
+ return;
86
+ }
87
+ try {
88
+ process.kill(pid, 0);
89
+ logger.success(`Porterman is running (PID: ${pid})`);
90
+ } catch {
91
+ logger.info("No Porterman instance is running (stale PID file).");
92
+ }
93
+ });
94
+ cli.command("stop", "Stop running Porterman instance").action(async () => {
95
+ if (!pidFileExists()) {
96
+ logger.info("No Porterman instance is running.");
97
+ return;
98
+ }
99
+ const pid = await readPidFile();
100
+ if (pid === null) {
101
+ logger.info("No Porterman instance is running.");
102
+ return;
103
+ }
104
+ try {
105
+ process.kill(pid, "SIGTERM");
106
+ logger.success(`Porterman stopped (PID: ${pid})`);
107
+ } catch {
108
+ logger.info("No Porterman instance is running (stale PID file).");
109
+ }
110
+ });
111
+ cli.command("certs", "Manage SSL certificates").option("--renew", "Force certificate renewal").option("--clean", "Remove all cached certificates").action(async (options) => {
112
+ if (options.clean) {
113
+ await cleanCerts();
114
+ return;
115
+ }
116
+ if (options.renew) {
117
+ logger.info("Certificate renewal is done automatically when using 'expose'.");
118
+ logger.info("Use 'porterman expose <port> --staging' to test with Let's Encrypt staging.");
119
+ return;
120
+ }
121
+ logger.info("Use --renew to force renewal or --clean to remove all cached certs.");
122
+ });
123
+ cli.command("", "Show help").action(() => {
124
+ cli.outputHelp();
125
+ });
126
+ cli.help();
127
+ cli.version(version);
128
+ cli.parse();
129
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import cac from \"cac\";\nimport { readFileSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { dirname, join } from \"node:path\";\nimport { startServer } from \"./server.js\";\nimport { cleanCerts, getCertificate } from \"./certs.js\";\nimport { readPidFile, pidFileExists } from \"./config.js\";\nimport { logger } from \"./logger.js\";\n\n// Read version from package.json\nlet version = \"1.0.0\";\ntry {\n const __filename = fileURLToPath(import.meta.url);\n const __dirname = dirname(__filename);\n // Try multiple paths since we might be in dist/ or src/\n for (const rel of [\"../package.json\", \"../../package.json\"]) {\n try {\n const pkg = JSON.parse(readFileSync(join(__dirname, rel), \"utf-8\"));\n version = pkg.version;\n break;\n } catch {}\n }\n} catch {}\n\nconst cli = cac(\"porterman\");\n\n// expose command\ncli\n .command(\"expose [...ports]\", \"Expose one or more local ports over HTTPS\")\n .option(\"-n, --name <name>\", \"Custom subdomain prefix (single port only)\")\n .option(\"--no-ssl\", \"HTTP only mode (skip SSL)\")\n .option(\"-v, --verbose\", \"Log all requests\")\n .option(\"--timeout <seconds>\", \"Proxy timeout in seconds\", { default: 30 })\n .option(\"--host <ip>\", \"Override auto-detected public IP\")\n .option(\"--staging\", \"Use Let's Encrypt staging environment\")\n .option(\"--http-port <port>\", \"Custom HTTP port\", { default: 80 })\n .option(\"--https-port <port>\", \"Custom HTTPS port\", { default: 443 })\n .option(\"--auth <user:pass>\", \"Enable basic auth on exposed ports\")\n .option(\"--ip-allow <ips>\", \"Comma-separated list of allowed IPs\")\n .action(async (portsRaw: string[], options) => {\n logger.banner(version);\n logger.blank();\n\n if (!portsRaw || portsRaw.length === 0) {\n logger.error(\"Please specify at least one port to expose.\");\n logger.blank();\n console.log(\" Usage: porterman expose <port> [port2] [port3]\");\n console.log(\" Example: porterman expose 3000 8080\");\n logger.blank();\n process.exit(1);\n }\n\n const ports = portsRaw.map((p) => {\n const num = parseInt(p, 10);\n if (isNaN(num) || num < 1 || num > 65535) {\n logger.error(`Invalid port: ${p}`);\n process.exit(1);\n }\n return num;\n });\n\n const ipAllow = options.ipAllow\n ? (options.ipAllow as string).split(\",\").map((ip: string) => ip.trim())\n : undefined;\n\n let server: Awaited<ReturnType<typeof startServer>> | null = null;\n\n try {\n server = await startServer({\n ports,\n name: options.name,\n noSsl: options.ssl === false,\n verbose: options.verbose,\n timeout: Number(options.timeout),\n host: options.host,\n staging: options.staging,\n httpPort: Number(options.httpPort),\n httpsPort: Number(options.httpsPort),\n auth: options.auth,\n ipAllow,\n });\n } catch (err) {\n logger.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n\n // Handle graceful shutdown\n const shutdown = async () => {\n if (server) {\n await server.close();\n }\n process.exit(0);\n };\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n });\n\n// status command\ncli.command(\"status\", \"Show running Porterman instance info\").action(async () => {\n if (!pidFileExists()) {\n logger.info(\"No Porterman instance is running.\");\n return;\n }\n\n const pid = await readPidFile();\n if (pid === null) {\n logger.info(\"No Porterman instance is running.\");\n return;\n }\n\n // Check if process is actually running\n try {\n process.kill(pid, 0);\n logger.success(`Porterman is running (PID: ${pid})`);\n } catch {\n logger.info(\"No Porterman instance is running (stale PID file).\");\n }\n});\n\n// stop command\ncli.command(\"stop\", \"Stop running Porterman instance\").action(async () => {\n if (!pidFileExists()) {\n logger.info(\"No Porterman instance is running.\");\n return;\n }\n\n const pid = await readPidFile();\n if (pid === null) {\n logger.info(\"No Porterman instance is running.\");\n return;\n }\n\n try {\n process.kill(pid, \"SIGTERM\");\n logger.success(`Porterman stopped (PID: ${pid})`);\n } catch {\n logger.info(\"No Porterman instance is running (stale PID file).\");\n }\n});\n\n// certs command\ncli\n .command(\"certs\", \"Manage SSL certificates\")\n .option(\"--renew\", \"Force certificate renewal\")\n .option(\"--clean\", \"Remove all cached certificates\")\n .action(async (options) => {\n if (options.clean) {\n await cleanCerts();\n return;\n }\n\n if (options.renew) {\n logger.info(\"Certificate renewal is done automatically when using 'expose'.\");\n logger.info(\"Use 'porterman expose <port> --staging' to test with Let's Encrypt staging.\");\n return;\n }\n\n logger.info(\"Use --renew to force renewal or --clean to remove all cached certs.\");\n });\n\n// Default command (show help)\ncli.command(\"\", \"Show help\").action(() => {\n cli.outputHelp();\n});\n\ncli.help();\ncli.version(version);\n\ncli.parse();\n"],"mappings":"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","names":[]}
@@ -0,0 +1,129 @@
1
+ import * as node_stream from 'node:stream';
2
+ import { IncomingMessage, ServerResponse } from 'node:http';
3
+
4
+ interface ServerOptions {
5
+ ports: number[];
6
+ name?: string;
7
+ noSsl?: boolean;
8
+ verbose?: boolean;
9
+ timeout?: number;
10
+ host?: string;
11
+ staging?: boolean;
12
+ httpPort?: number;
13
+ httpsPort?: number;
14
+ auth?: string;
15
+ ipAllow?: string[];
16
+ }
17
+ interface PortermanServer {
18
+ close(): Promise<void>;
19
+ urls: Map<number, string>;
20
+ }
21
+ declare function startServer(options: ServerOptions): Promise<PortermanServer>;
22
+
23
+ /**
24
+ * Detect the machine's public IP address using multiple services with fallback.
25
+ * Results are cached for the session lifetime.
26
+ */
27
+ declare function detectPublicIp(): Promise<string>;
28
+ /**
29
+ * Get the dashed format of the public IP (e.g., 85-100-50-25).
30
+ */
31
+ declare function getDashedIp(overrideIp?: string): Promise<string>;
32
+
33
+ interface CertResult {
34
+ key: string;
35
+ cert: string;
36
+ chain: string;
37
+ selfSigned: boolean;
38
+ }
39
+ /**
40
+ * Get a valid certificate for a hostname.
41
+ * - First checks for a cached valid cert
42
+ * - Then tries ACME/Let's Encrypt
43
+ * - Falls back to self-signed if ACME fails
44
+ */
45
+ declare function getCertificate(hostname: string, options?: {
46
+ staging?: boolean;
47
+ forceRenew?: boolean;
48
+ }): Promise<CertResult>;
49
+ /**
50
+ * Remove all cached certificates.
51
+ */
52
+ declare function cleanCerts(): Promise<void>;
53
+
54
+ interface ProxyRoute {
55
+ hostname: string;
56
+ targetPort: number;
57
+ name?: string;
58
+ }
59
+ interface ProxyOptions {
60
+ timeout: number;
61
+ routes: ProxyRoute[];
62
+ nameMap?: Map<string, number>;
63
+ }
64
+ declare function createProxyEngine(options: ProxyOptions): {
65
+ handleRequest: (req: IncomingMessage, res: ServerResponse) => boolean;
66
+ handleUpgrade: (req: IncomingMessage, socket: node_stream.Duplex, head: Buffer) => boolean;
67
+ close: () => void;
68
+ };
69
+
70
+ declare function setVerbose(enabled: boolean): void;
71
+ declare const logger: {
72
+ info(message: string): void;
73
+ success(message: string): void;
74
+ warn(message: string): void;
75
+ error(message: string): void;
76
+ rocket(message: string): void;
77
+ link(label: string, url: string): void;
78
+ verbose(message: string): void;
79
+ request(method: string, host: string, path: string, status: number): void;
80
+ banner(version: string): void;
81
+ blank(): void;
82
+ };
83
+
84
+ interface PortermanConfig {
85
+ defaultTimeout?: number;
86
+ defaultHttpPort?: number;
87
+ defaultHttpsPort?: number;
88
+ }
89
+ declare const paths: {
90
+ base: string;
91
+ config: string;
92
+ certs: string;
93
+ accountKey: string;
94
+ pidFile: string;
95
+ certDir(hostname: string): string;
96
+ certFile(hostname: string): string;
97
+ keyFile(hostname: string): string;
98
+ chainFile(hostname: string): string;
99
+ metaFile(hostname: string): string;
100
+ };
101
+
102
+ /**
103
+ * Convert a dotted IP address to dashed format.
104
+ * 85.100.50.25 → 85-100-50-25
105
+ */
106
+ declare function ipToDashed(ip: string): string;
107
+ /**
108
+ * Generate a sslip.io hostname for a given port and IP.
109
+ */
110
+ declare function makeHostname(port: number | string, dashedIp: string): string;
111
+ /**
112
+ * Parse the port number from a sslip.io hostname.
113
+ * "3000-85-100-50-25.sslip.io" → 3000
114
+ */
115
+ declare function parsePortFromHost(host: string): number | null;
116
+ /**
117
+ * Check if an IP is a private/reserved address.
118
+ */
119
+ declare function isPrivateIp(ip: string): boolean;
120
+ /**
121
+ * Check if a port is available.
122
+ */
123
+ declare function isPortAvailable(port: number): Promise<boolean>;
124
+ /**
125
+ * Validate that a port number is valid.
126
+ */
127
+ declare function isValidPort(port: number): boolean;
128
+
129
+ export { type CertResult, type PortermanConfig, type PortermanServer, type ProxyRoute, type ServerOptions, cleanCerts, createProxyEngine, detectPublicIp, getCertificate, getDashedIp, ipToDashed, isPortAvailable, isPrivateIp, isValidPort, logger, makeHostname, parsePortFromHost, paths, setVerbose, startServer };
package/dist/index.js ADDED
@@ -0,0 +1,35 @@
1
+ import {
2
+ cleanCerts,
3
+ createProxyEngine,
4
+ detectPublicIp,
5
+ getCertificate,
6
+ getDashedIp,
7
+ ipToDashed,
8
+ isPortAvailable,
9
+ isPrivateIp,
10
+ isValidPort,
11
+ logger,
12
+ makeHostname,
13
+ parsePortFromHost,
14
+ paths,
15
+ setVerbose,
16
+ startServer
17
+ } from "./chunk-SWPUBQIV.js";
18
+ export {
19
+ cleanCerts,
20
+ createProxyEngine,
21
+ detectPublicIp,
22
+ getCertificate,
23
+ getDashedIp,
24
+ ipToDashed,
25
+ isPortAvailable,
26
+ isPrivateIp,
27
+ isValidPort,
28
+ logger,
29
+ makeHostname,
30
+ parsePortFromHost,
31
+ paths,
32
+ setVerbose,
33
+ startServer
34
+ };
35
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@mucan54/porterman",
3
+ "version": "1.0.0",
4
+ "description": "Zero-config HTTPS tunnel for your local ports. No external server needed.",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "bin": {
10
+ "porterman": "./bin/porterman.mjs"
11
+ },
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "import": "./dist/index.js",
17
+ "types": "./dist/index.d.ts"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "bin"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "dev": "tsup --watch",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "typecheck": "tsc --noEmit",
30
+ "prepublishOnly": "npm run build"
31
+ },
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "keywords": [
36
+ "tunnel",
37
+ "ngrok",
38
+ "https",
39
+ "ssl",
40
+ "proxy",
41
+ "sslip",
42
+ "expose",
43
+ "localhost",
44
+ "reverse-proxy",
45
+ "letsencrypt"
46
+ ],
47
+ "author": "mucan54",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/mucan54/porterman.git"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/mucan54/porterman/issues"
54
+ },
55
+ "homepage": "https://github.com/mucan54/porterman#readme",
56
+ "license": "MIT",
57
+ "dependencies": {
58
+ "acme-client": "^5.4.0",
59
+ "cac": "^6.7.14",
60
+ "http-proxy": "^1.18.1"
61
+ },
62
+ "devDependencies": {
63
+ "@types/http-proxy": "^1.17.14",
64
+ "@types/node": "^20.11.0",
65
+ "tsup": "^8.0.1",
66
+ "typescript": "^5.3.3",
67
+ "vitest": "^1.2.0"
68
+ }
69
+ }