From 4e78dade6490721bf4363732f8ba2d2e4a42aff3 Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Sun, 18 May 2025 15:03:11 +0000 Subject: [PATCH] new plan --- readme.plan.md | 1916 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 1297 insertions(+), 619 deletions(-) diff --git a/readme.plan.md b/readme.plan.md index ec29e3b..0e4b3fc 100644 --- a/readme.plan.md +++ b/readme.plan.md @@ -1,654 +1,1332 @@ -# NFTables-SmartProxy Integration Plan +# ACME/Certificate Simplification Plan for SmartProxy + +## Command to reread CLAUDE.md +`reread /home/philkunz/.claude/CLAUDE.md` ## Overview +Simplify the ACME/Certificate system by consolidating components, removing unnecessary abstraction layers, and integrating directly into SmartProxy's route-based architecture. -This document outlines a comprehensive plan to integrate the existing NFTables functionality with the SmartProxy core to provide advanced network-level routing capabilities. The NFTables proxy already exists in the codebase but is not fully integrated with the SmartProxy routing system. This integration will allow SmartProxy to leverage the power of Linux's NFTables firewall system for high-performance port forwarding, load balancing, and security filtering. +## Current State Analysis -## Current State +### Files to be Removed/Replaced +``` +ts/certificate/ (ENTIRE DIRECTORY TO BE REMOVED) +├── acme/ +│ ├── acme-factory.ts (28 lines) +│ ├── challenge-handler.ts (227 lines) +│ └── index.ts (2 lines) +├── events/ +│ └── certificate-events.ts (75 lines) +├── models/ +│ └── certificate-types.ts (168 lines) +├── providers/ +│ ├── cert-provisioner.ts (547 lines) +│ └── index.ts (2 lines) +├── storage/ +│ ├── file-storage.ts (134 lines) +│ └── index.ts (2 lines) +├── utils/ +│ └── certificate-helpers.ts (166 lines) +└── index.ts (75 lines) -1. **NFTablesProxy**: A standalone implementation exists in `ts/proxies/nftables-proxy/` with its own configuration and API. -2. **SmartProxy**: The main routing system with route-based configuration. -3. **No Integration**: Currently, these systems operate independently with no shared configuration or coordination. +ts/http/port80/ (ENTIRE SUBDIRECTORY TO BE REMOVED) +├── acme-interfaces.ts +├── challenge-responder.ts +├── port80-handler.ts +└── index.ts -## Goals +ts/http/ (KEEP OTHER SUBDIRECTORIES) +├── index.ts (UPDATE to remove port80 exports) +├── models/ (KEEP) +├── redirects/ (KEEP) +├── router/ (KEEP) +└── utils/ (KEEP) -1. Create a unified configuration system where SmartProxy routes can specify NFTables-based forwarding. -2. Allow SmartProxy to dynamically provision and manage NFTables rules based on route configuration. -3. Support advanced filtering and security rules through NFTables for better performance. -4. Ensure backward compatibility with existing setups. -5. Provide metrics integration between the systems. +ts/proxies/smart-proxy/ +└── network-proxy-bridge.ts (267 lines - to be simplified) +``` -## Implementation Plan +### Current Dependencies +- @push.rocks/smartacme (ACME client) +- @push.rocks/smartfile (file operations) +- @push.rocks/smartcrypto (certificate operations) +- @push.rocks/smartexpress (HTTP server for challenges) -### Phase 1: Route Configuration Schema Extension +## Detailed Implementation Plan -1. **Extend Route Configuration Schema**: - - Add new `forwardingEngine` option to IRouteAction to specify the forwarding implementation. - - Support values: 'node' (current NodeJS implementation) and 'nftables' (Linux NFTables). - - Add NFTables-specific configuration options to IRouteAction. +### Phase 1: Create SmartCertManager -2. **Update Type Definitions**: - ```typescript - // In route-types.ts - export interface IRouteAction { - type: 'forward' | 'redirect' | 'block'; - target?: IRouteTarget; - security?: IRouteSecurity; - options?: IRouteOptions; - tls?: IRouteTlsOptions; - forwardingEngine?: 'node' | 'nftables'; // New field - nftables?: INfTablesOptions; // New field - } +#### 1.1 Create certificate-manager.ts +```typescript +// ts/proxies/smart-proxy/certificate-manager.ts +import * as plugins from '../../plugins.js'; +import { NetworkProxy } from '../network-proxy/index.js'; +import type { IRouteConfig, IRouteTls } from './models/route-types.js'; +import { CertStore } from './cert-store.js'; +import { AcmeClient } from './acme-client.js'; - export interface INfTablesOptions { - preserveSourceIP?: boolean; - protocol?: 'tcp' | 'udp' | 'all'; - maxRate?: string; // QoS rate limiting - priority?: number; // QoS priority - tableName?: string; // Optional custom table name - useIPSets?: boolean; // Use IP sets for performance - useAdvancedNAT?: boolean; // Use connection tracking - } - ``` +export interface ICertStatus { + domain: string; + status: 'valid' | 'pending' | 'expired' | 'error'; + expiryDate?: Date; + issueDate?: Date; + source: 'static' | 'acme'; + error?: string; +} -### Phase 2: NFTablesManager Implementation +export interface ICertificateData { + cert: string; + key: string; + ca?: string; + expiryDate: Date; + issueDate: Date; +} -1. **Create NFTablesManager Class**: - - Create a new class to manage NFTables rules based on SmartProxy routes. - - Add methods to create, update, and remove NFTables rules. - - Design a rule naming scheme to track which rules correspond to which routes. +export class SmartCertManager { + private certStore: CertStore; + private smartAcme: plugins.smartacme.SmartAcme | null = null; + private networkProxy: NetworkProxy | null = null; + private renewalTimer: NodeJS.Timer | null = null; + private pendingChallenges: Map = new Map(); + + // Track certificate status by route name + private certStatus: Map = new Map(); + + // Callback to update SmartProxy routes for challenges + private updateRoutesCallback?: (routes: IRouteConfig[]) => Promise; + + constructor( + private routes: IRouteConfig[], + private certDir: string = './certs', + private acmeOptions?: { + email?: string; + useProduction?: boolean; + port?: number; + } + ) { + this.certStore = new CertStore(certDir); + } + + public setNetworkProxy(networkProxy: NetworkProxy): void { + this.networkProxy = networkProxy; + } + + /** + * Set callback for updating routes (used for challenge routes) + */ + public setUpdateRoutesCallback(callback: (routes: IRouteConfig[]) => Promise): void { + this.updateRoutesCallback = callback; + } + + /** + * Initialize certificate manager and provision certificates for all routes + */ + public async initialize(): Promise { + // Create certificate directory if it doesn't exist + await this.certStore.initialize(); + + // Initialize SmartAcme if we have any ACME routes + const hasAcmeRoutes = this.routes.some(r => + r.action.tls?.certificate === 'auto' + ); + + if (hasAcmeRoutes && this.acmeOptions?.email) { + // Create SmartAcme instance with our challenge handler + this.smartAcme = new plugins.smartacme.SmartAcme({ + accountEmail: this.acmeOptions.email, + environment: this.acmeOptions.useProduction ? 'production' : 'staging', + certManager: new InMemoryCertManager(), // Simple in-memory cert manager + challengeHandlers: [{ + type: 'http-01', + setChallenge: async (domain: string, token: string, keyAuth: string) => { + await this.handleChallenge(token, keyAuth); + }, + removeChallenge: async (domain: string, token: string) => { + await this.cleanupChallenge(token); + } + }] + }); + + await this.smartAcme.start(); + } + + // Provision certificates for all routes + await this.provisionAllCertificates(); + + // Start renewal timer + this.startRenewalTimer(); + } + + /** + * Provision certificates for all routes that need them + */ + private async provisionAllCertificates(): Promise { + const certRoutes = this.routes.filter(r => + r.action.tls?.mode === 'terminate' || + r.action.tls?.mode === 'terminate-and-reencrypt' + ); + + for (const route of certRoutes) { + try { + await this.provisionCertificate(route); + } catch (error) { + console.error(`Failed to provision certificate for route ${route.name}: ${error}`); + } + } + } + + /** + * Provision certificate for a single route + */ + public async provisionCertificate(route: IRouteConfig): Promise { + const tls = route.action.tls; + if (!tls || (tls.mode !== 'terminate' && tls.mode !== 'terminate-and-reencrypt')) { + return; + } + + const domains = this.extractDomainsFromRoute(route); + if (domains.length === 0) { + console.warn(`Route ${route.name} has TLS termination but no domains`); + return; + } + + const primaryDomain = domains[0]; + + if (tls.certificate === 'auto') { + // ACME certificate + await this.provisionAcmeCertificate(route, domains); + } else if (typeof tls.certificate === 'object') { + // Static certificate + await this.provisionStaticCertificate(route, primaryDomain, tls.certificate); + } + } + + /** + * Provision ACME certificate + */ + private async provisionAcmeCertificate( + route: IRouteConfig, + domains: string[] + ): Promise { + if (!this.smartAcme) { + throw new Error('SmartAcme not initialized'); + } + + const primaryDomain = domains[0]; + const routeName = route.name || primaryDomain; + + // Check if we already have a valid certificate + const existingCert = await this.certStore.getCertificate(routeName); + if (existingCert && this.isCertificateValid(existingCert)) { + console.log(`Using existing valid certificate for ${primaryDomain}`); + await this.applyCertificate(primaryDomain, existingCert); + this.updateCertStatus(routeName, 'valid', 'acme', existingCert); + return; + } + + console.log(`Requesting ACME certificate for ${domains.join(', ')}`); + this.updateCertStatus(routeName, 'pending', 'acme'); + + try { + // Use smartacme to get certificate + const cert = await this.smartAcme.getCertificateForDomain(primaryDomain, { + altNames: domains.slice(1) + }); + + // smartacme returns a Cert object with these properties + const certData: ICertificateData = { + cert: cert.cert, + key: cert.privateKey, + ca: cert.fullChain || cert.cert, // Use fullChain if available + expiryDate: new Date(cert.validTo), + issueDate: new Date(cert.validFrom) + }; + + await this.certStore.saveCertificate(routeName, certData); + await this.applyCertificate(primaryDomain, certData); + this.updateCertStatus(routeName, 'valid', 'acme', certData); + + console.log(`Successfully provisioned ACME certificate for ${primaryDomain}`); + } catch (error) { + console.error(`Failed to provision ACME certificate for ${primaryDomain}: ${error}`); + this.updateCertStatus(routeName, 'error', 'acme', undefined, error.message); + throw error; + } + } + + /** + * Provision static certificate + */ + private async provisionStaticCertificate( + route: IRouteConfig, + domain: string, + certConfig: { key: string; cert: string; keyFile?: string; certFile?: string } + ): Promise { + const routeName = route.name || domain; + + try { + let key: string = certConfig.key; + let cert: string = certConfig.cert; + + // Load from files if paths are provided + if (certConfig.keyFile) { + key = await plugins.smartfile.fs.readFileAsString(certConfig.keyFile); + } + if (certConfig.certFile) { + cert = await plugins.smartfile.fs.readFileAsString(certConfig.certFile); + } + + // Parse certificate to get dates + const certInfo = await plugins.smartcrypto.cert.parseCert(cert); + + const certData: ICertificateData = { + cert, + key, + expiryDate: certInfo.validTo, + issueDate: certInfo.validFrom + }; + + // Save to store for consistency + await this.certStore.saveCertificate(routeName, certData); + await this.applyCertificate(domain, certData); + this.updateCertStatus(routeName, 'valid', 'static', certData); + + console.log(`Successfully loaded static certificate for ${domain}`); + } catch (error) { + console.error(`Failed to provision static certificate for ${domain}: ${error}`); + this.updateCertStatus(routeName, 'error', 'static', undefined, error.message); + throw error; + } + } + + /** + * Apply certificate to NetworkProxy + */ + private async applyCertificate(domain: string, certData: ICertificateData): Promise { + if (!this.networkProxy) { + console.warn('NetworkProxy not set, cannot apply certificate'); + return; + } + + // Apply certificate to NetworkProxy + this.networkProxy.updateCertificate(domain, certData.cert, certData.key); + + // Also apply for wildcard if it's a subdomain + if (domain.includes('.') && !domain.startsWith('*.')) { + const parts = domain.split('.'); + if (parts.length >= 2) { + const wildcardDomain = `*.${parts.slice(-2).join('.')}`; + this.networkProxy.updateCertificate(wildcardDomain, certData.cert, certData.key); + } + } + } + + /** + * Extract domains from route configuration + */ + private extractDomainsFromRoute(route: IRouteConfig): string[] { + if (!route.match.domains) { + return []; + } + + const domains = Array.isArray(route.match.domains) + ? route.match.domains + : [route.match.domains]; + + // Filter out wildcards and patterns + return domains.filter(d => + !d.includes('*') && + !d.includes('{') && + d.includes('.') + ); + } + + /** + * Check if certificate is valid + */ + private isCertificateValid(cert: ICertificateData): boolean { + const now = new Date(); + const expiryThreshold = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); // 30 days + + return cert.expiryDate > expiryThreshold; + } + + /** + * Create ACME challenge route + */ + private createChallengeRoute(): IRouteConfig { + return { + name: 'acme-challenge', + match: { + ports: 80, + path: '/.well-known/acme-challenge/*' + }, + action: { + type: 'static', + handler: async (context) => { + const token = context.path?.split('/').pop(); + const keyAuth = token ? this.pendingChallenges.get(token) : undefined; + + if (keyAuth) { + return { + status: 200, + headers: { 'Content-Type': 'text/plain' }, + body: keyAuth + }; + } else { + return { + status: 404, + body: 'Not found' + }; + } + } + } + }; + } + + /** + * Add challenge route to SmartProxy + */ + private async addChallengeRoute(): Promise { + if (!this.updateRoutesCallback) { + throw new Error('No route update callback set'); + } + + const challengeRoute = this.createChallengeRoute(); + const updatedRoutes = [...this.routes, challengeRoute]; + + await this.updateRoutesCallback(updatedRoutes); + } + + /** + * Remove challenge route from SmartProxy + */ + private async removeChallengeRoute(): Promise { + if (!this.updateRoutesCallback) { + return; + } + + const filteredRoutes = this.routes.filter(r => r.name !== 'acme-challenge'); + await this.updateRoutesCallback(filteredRoutes); + } + + /** + * Start renewal timer + */ + private startRenewalTimer(): void { + // Check for renewals every 12 hours + this.renewalTimer = setInterval(() => { + this.checkAndRenewCertificates(); + }, 12 * 60 * 60 * 1000); + + // Also do an immediate check + this.checkAndRenewCertificates(); + } + + /** + * Check and renew certificates that are expiring + */ + private async checkAndRenewCertificates(): Promise { + for (const route of this.routes) { + if (route.action.tls?.certificate === 'auto') { + const routeName = route.name || this.extractDomainsFromRoute(route)[0]; + const cert = await this.certStore.getCertificate(routeName); + + if (cert && !this.isCertificateValid(cert)) { + console.log(`Certificate for ${routeName} needs renewal`); + try { + await this.provisionCertificate(route); + } catch (error) { + console.error(`Failed to renew certificate for ${routeName}: ${error}`); + } + } + } + } + } + + /** + * Update certificate status + */ + private updateCertStatus( + routeName: string, + status: ICertStatus['status'], + source: ICertStatus['source'], + certData?: ICertificateData, + error?: string + ): void { + this.certStatus.set(routeName, { + domain: routeName, + status, + source, + expiryDate: certData?.expiryDate, + issueDate: certData?.issueDate, + error + }); + } + + /** + * Get certificate status for a route + */ + public getCertificateStatus(routeName: string): ICertStatus | undefined { + return this.certStatus.get(routeName); + } + + /** + * Force renewal of a certificate + */ + public async renewCertificate(routeName: string): Promise { + const route = this.routes.find(r => r.name === routeName); + if (!route) { + throw new Error(`Route ${routeName} not found`); + } + + // Remove existing certificate to force renewal + await this.certStore.deleteCertificate(routeName); + await this.provisionCertificate(route); + } + + /** + * Handle ACME challenge + */ + private async handleChallenge(token: string, keyAuth: string): Promise { + this.pendingChallenges.set(token, keyAuth); + + // Add challenge route if it's the first challenge + if (this.pendingChallenges.size === 1) { + await this.addChallengeRoute(); + } + } + + /** + * Cleanup ACME challenge + */ + private async cleanupChallenge(token: string): Promise { + this.pendingChallenges.delete(token); + + // Remove challenge route if no more challenges + if (this.pendingChallenges.size === 0) { + await this.removeChallengeRoute(); + } + } + + /** + * Stop certificate manager + */ + public async stop(): Promise { + if (this.renewalTimer) { + clearInterval(this.renewalTimer); + this.renewalTimer = null; + } + + if (this.smartAcme) { + await this.smartAcme.stop(); + } + + // Remove any active challenge routes + if (this.pendingChallenges.size > 0) { + this.pendingChallenges.clear(); + await this.removeChallengeRoute(); + } + } + + /** + * Get ACME options (for recreating after route updates) + */ + public getAcmeOptions(): { email?: string; useProduction?: boolean; port?: number } | undefined { + return this.acmeOptions; + } +} -2. **Implementation**: - ```typescript - // In ts/proxies/smart-proxy/nftables-manager.ts - export class NFTablesManager { - private rulesMap: Map = new Map(); - - constructor(private options: ISmartProxyOptions) {} - - /** - * Provision NFTables rules for a route - */ - public async provisionRoute(route: IRouteConfig): Promise { - // Generate a unique ID for this route - const routeId = this.generateRouteId(route); - - // Skip if route doesn't use NFTables - if (route.action.forwardingEngine !== 'nftables') { - return true; - } - - // Create NFTables options from route configuration - const nftOptions = this.createNfTablesOptions(route); - - // Create and start an NFTablesProxy instance - const proxy = new NfTablesProxy(nftOptions); - - try { - await proxy.start(); - this.rulesMap.set(routeId, proxy); - return true; - } catch (err) { - console.error(`Failed to provision NFTables rules for route ${route.name}: ${err.message}`); - return false; - } - } - - /** - * Remove NFTables rules for a route - */ - public async deprovisionRoute(route: IRouteConfig): Promise { - const routeId = this.generateRouteId(route); - - const proxy = this.rulesMap.get(routeId); - if (!proxy) { - return true; // Nothing to remove - } - - try { - await proxy.stop(); - this.rulesMap.delete(routeId); - return true; - } catch (err) { - console.error(`Failed to deprovision NFTables rules for route ${route.name}: ${err.message}`); - return false; - } - } - - /** - * Update NFTables rules when route changes - */ - public async updateRoute(oldRoute: IRouteConfig, newRoute: IRouteConfig): Promise { - // Remove old rules and add new ones - await this.deprovisionRoute(oldRoute); - return this.provisionRoute(newRoute); - } - - /** - * Generate a unique ID for a route - */ - private generateRouteId(route: IRouteConfig): string { - // Generate a unique ID based on route properties - return `${route.name || 'unnamed'}-${JSON.stringify(route.match)}-${Date.now()}`; - } - - /** - * Create NFTablesProxy options from a route configuration - */ - private createNfTablesOptions(route: IRouteConfig): NfTableProxyOptions { - const { action } = route; - - // Ensure we have a target - if (!action.target) { - throw new Error('Route must have a target to use NFTables forwarding'); - } - - // Convert port specifications - const fromPorts = this.expandPortRange(route.match.ports); - - // Determine target port - let toPorts; - if (action.target.port === 'preserve') { - // 'preserve' means use the same ports as the source - toPorts = fromPorts; - } else if (typeof action.target.port === 'function') { - // For function-based ports, we can't determine at setup time - // Use the "preserve" approach and let NFTables handle it - toPorts = fromPorts; - } else { - toPorts = action.target.port; - } - - // Create options - const options: NfTableProxyOptions = { - fromPort: fromPorts, - toPort: toPorts, - toHost: typeof action.target.host === 'function' - ? 'localhost' // Can't determine at setup time, use localhost - : (Array.isArray(action.target.host) - ? action.target.host[0] // Use first host for now - : action.target.host), - protocol: action.nftables?.protocol || 'tcp', - preserveSourceIP: action.nftables?.preserveSourceIP, - useIPSets: action.nftables?.useIPSets !== false, - useAdvancedNAT: action.nftables?.useAdvancedNAT, - enableLogging: this.options.enableDetailedLogging, - deleteOnExit: true, - tableName: action.nftables?.tableName || 'smartproxy' - }; - - // Add security-related options - if (action.security?.ipAllowList?.length) { - options.allowedSourceIPs = action.security.ipAllowList; - } - - if (action.security?.ipBlockList?.length) { - options.bannedSourceIPs = action.security.ipBlockList; - } - - // Add QoS options - if (action.nftables?.maxRate || action.nftables?.priority) { - options.qos = { - enabled: true, - maxRate: action.nftables.maxRate, - priority: action.nftables.priority - }; - } - - return options; - } - - /** - * Expand port range specifications - */ - private expandPortRange(ports: TPortRange): number | PortRange | Array { - // Use RouteManager's expandPortRange to convert to actual port numbers - const routeManager = new RouteManager(this.options); - - // Process different port specifications - if (typeof ports === 'number') { - return ports; - } else if (Array.isArray(ports)) { - const result: Array = []; - - for (const item of ports) { - if (typeof item === 'number') { - result.push(item); - } else if ('from' in item && 'to' in item) { - result.push({ from: item.from, to: item.to }); - } - } - - return result; - } else if ('from' in ports && 'to' in ports) { - return { from: ports.from, to: ports.to }; - } - - // Fallback - return 80; - } - - /** - * Get status of all managed rules - */ - public async getStatus(): Promise> { - const result: Record = {}; - - for (const [routeId, proxy] of this.rulesMap.entries()) { - result[routeId] = await proxy.getStatus(); - } - - return result; - } - - /** - * Stop all NFTables rules - */ - public async stop(): Promise { - // Stop all NFTables proxies - const stopPromises = Array.from(this.rulesMap.values()).map(proxy => proxy.stop()); - await Promise.all(stopPromises); - - this.rulesMap.clear(); - } - } - ``` +/** + * Simple in-memory certificate manager for SmartAcme + * We only use this to satisfy SmartAcme's interface - actual storage is handled by CertStore + */ +class InMemoryCertManager implements plugins.smartacme.CertManager { + private store = new Map(); + + public async getCert(domain: string): Promise { + // SmartAcme uses this to check for existing certs + // We return null to force it to always request new certs + return null; + } + + public async setCert(domain: string, certificate: any): Promise { + // SmartAcme calls this after getting a cert + // We ignore it since we handle storage ourselves + } + + public async removeCert(domain: string): Promise { + // Not needed for our use case + } +} +``` + +#### 1.2 Create cert-store.ts +```typescript +// ts/proxies/smart-proxy/cert-store.ts +import * as plugins from '../../plugins.js'; +import type { ICertificateData } from './certificate-manager.js'; + +export class CertStore { + constructor(private certDir: string) {} + + public async initialize(): Promise { + await plugins.smartfile.fs.ensureDirectory(this.certDir); + } + + public async getCertificate(routeName: string): Promise { + const certPath = this.getCertPath(routeName); + const metaPath = `${certPath}/meta.json`; + + if (!await plugins.smartfile.fs.fileExists(metaPath)) { + return null; + } + + try { + const meta = await plugins.smartfile.fs.readJson(metaPath); + const cert = await plugins.smartfile.fs.readFileAsString(`${certPath}/cert.pem`); + const key = await plugins.smartfile.fs.readFileAsString(`${certPath}/key.pem`); + + let ca: string | undefined; + const caPath = `${certPath}/ca.pem`; + if (await plugins.smartfile.fs.fileExists(caPath)) { + ca = await plugins.smartfile.fs.readFileAsString(caPath); + } + + return { + cert, + key, + ca, + expiryDate: new Date(meta.expiryDate), + issueDate: new Date(meta.issueDate) + }; + } catch (error) { + console.error(`Failed to load certificate for ${routeName}: ${error}`); + return null; + } + } + + public async saveCertificate( + routeName: string, + certData: ICertificateData + ): Promise { + const certPath = this.getCertPath(routeName); + await plugins.smartfile.fs.ensureDirectory(certPath); + + // Save certificate files + await plugins.smartfile.fs.writeFileAsString( + `${certPath}/cert.pem`, + certData.cert + ); + await plugins.smartfile.fs.writeFileAsString( + `${certPath}/key.pem`, + certData.key + ); + + if (certData.ca) { + await plugins.smartfile.fs.writeFileAsString( + `${certPath}/ca.pem`, + certData.ca + ); + } + + // Save metadata + const meta = { + expiryDate: certData.expiryDate.toISOString(), + issueDate: certData.issueDate.toISOString(), + savedAt: new Date().toISOString() + }; + + await plugins.smartfile.fs.writeJson(`${certPath}/meta.json`, meta); + } + + public async deleteCertificate(routeName: string): Promise { + const certPath = this.getCertPath(routeName); + if (await plugins.smartfile.fs.fileExists(certPath)) { + await plugins.smartfile.fs.removeDirectory(certPath); + } + } + + private getCertPath(routeName: string): string { + // Sanitize route name for filesystem + const safeName = routeName.replace(/[^a-zA-Z0-9-_]/g, '_'); + return `${this.certDir}/${safeName}`; + } +} +``` + + +### Phase 2: Update Route Types + +#### 2.1 Update route-types.ts +```typescript +// Add to ts/proxies/smart-proxy/models/route-types.ts + +/** + * ACME configuration for automatic certificate provisioning + */ +export interface IRouteAcme { + email: string; // Contact email for ACME account + useProduction?: boolean; // Use production ACME servers (default: false) + challengePort?: number; // Port for HTTP-01 challenges (default: 80) + renewBeforeDays?: number; // Days before expiry to renew (default: 30) +} + +/** + * Static route handler response + */ +export interface IStaticResponse { + status: number; + headers?: Record; + body: string | Buffer; +} + +/** + * Update IRouteAction to support static handlers + */ +export interface IRouteAction { + type: TRouteActionType; + target?: IRouteTarget; + security?: IRouteSecurity; + options?: IRouteOptions; + tls?: IRouteTls; + redirect?: IRouteRedirect; + handler?: (context: IRouteContext) => Promise; // For static routes +} + +/** + * Extended TLS configuration for route actions + */ +export interface IRouteTls { + mode: TTlsMode; + certificate?: 'auto' | { // Auto = use ACME + key: string; // PEM-encoded private key + cert: string; // PEM-encoded certificate + ca?: string; // PEM-encoded CA chain + keyFile?: string; // Path to key file (overrides key) + certFile?: string; // Path to cert file (overrides cert) + }; + acme?: IRouteAcme; // ACME options when certificate is 'auto' + versions?: string[]; // Allowed TLS versions (e.g., ['TLSv1.2', 'TLSv1.3']) + ciphers?: string; // OpenSSL cipher string + honorCipherOrder?: boolean; // Use server's cipher preferences + sessionTimeout?: number; // TLS session timeout in seconds +} +``` ### Phase 3: SmartProxy Integration -1. **Extend SmartProxy Class**: - - Add NFTablesManager as a property of SmartProxy. - - Hook into route configuration to provision NFTables rules. - - Add methods to manage NFTables functionality. - -2. **Implementation**: - ```typescript - // In ts/proxies/smart-proxy/smart-proxy.ts - import { NFTablesManager } from './nftables-manager.js'; - - export class SmartProxy { - // Existing properties - private nftablesManager: NFTablesManager; - - constructor(options: ISmartProxyOptions) { - // Existing initialization - - // Initialize NFTablesManager - this.nftablesManager = new NFTablesManager(options); - } - - /** - * Start the SmartProxy server - */ - public async start(): Promise { - // Existing initialization - - // If we have routes, provision NFTables rules for them - for (const route of this.settings.routes) { - if (route.action.forwardingEngine === 'nftables') { - await this.nftablesManager.provisionRoute(route); - } - } - - // Rest of existing start method - } - - /** - * Stop the SmartProxy server - */ - public async stop(): Promise { - // Stop NFTablesManager first - await this.nftablesManager.stop(); - - // Rest of existing stop method - } - - /** - * Update routes - */ - public async updateRoutes(routes: IRouteConfig[]): Promise { - // Get existing routes that use NFTables - const oldNfTablesRoutes = this.settings.routes.filter( - r => r.action.forwardingEngine === 'nftables' - ); - - // Get new routes that use NFTables - const newNfTablesRoutes = routes.filter( - r => r.action.forwardingEngine === 'nftables' - ); - - // Find routes to remove, update, or add - for (const oldRoute of oldNfTablesRoutes) { - const newRoute = newNfTablesRoutes.find(r => r.name === oldRoute.name); - - if (!newRoute) { - // Route was removed - await this.nftablesManager.deprovisionRoute(oldRoute); - } else { - // Route was updated - await this.nftablesManager.updateRoute(oldRoute, newRoute); - } - } - - // Find new routes to add - for (const newRoute of newNfTablesRoutes) { - const oldRoute = oldNfTablesRoutes.find(r => r.name === newRoute.name); - - if (!oldRoute) { - // New route - await this.nftablesManager.provisionRoute(newRoute); - } - } - - // Update settings with the new routes - this.settings.routes = routes; - - // Update route manager with new routes - this.routeManager.updateRoutes(routes); - } - - /** - * Get NFTables status - */ - public async getNfTablesStatus(): Promise> { - return this.nftablesManager.getStatus(); - } - } - ``` - -### Phase 4: Routing System Integration - -1. **Extend the Route-Connection-Handler**: - - Modify to check if a route uses NFTables. - - Skip Node.js-based connection handling for NFTables routes. - -2. **Implementation**: - ```typescript - // In ts/proxies/smart-proxy/route-connection-handler.ts - export class RouteConnectionHandler { - // Existing methods - - /** - * Route the connection based on match criteria - */ - private routeConnection( - socket: plugins.net.Socket, - record: IConnectionRecord, - serverName: string, - initialChunk?: Buffer - ): void { - // Find matching route - const routeMatch = this.routeManager.findMatchingRoute({ - port: record.localPort, - domain: serverName, - clientIp: record.remoteIP, - path: undefined, - tlsVersion: undefined - }); - - if (!routeMatch) { - // Existing code for no matching route - return; - } - - const route = routeMatch.route; - - // Check if this route uses NFTables for forwarding - if (route.action.forwardingEngine === 'nftables') { - // For NFTables routes, we don't need to do anything at the application level - // The packet is forwarded at the kernel level - - // Log the connection - console.log( - `[${record.id}] Connection forwarded by NFTables: ${record.remoteIP} -> port ${record.localPort}` - ); - - // Just close the socket in our application since it's handled at kernel level - socket.end(); - this.connectionManager.initiateCleanupOnce(record, 'nftables_handled'); - return; - } - - // Existing code for handling the route - } - } - ``` - -### Phase 5: CLI and Configuration Helpers - -1. **Add Helper Functions**: - - Create helper functions for easy route creation with NFTables. - - Update the route-helpers.ts utility file. - -2. **Implementation**: - ```typescript - // In ts/proxies/smart-proxy/utils/route-helpers.ts - - /** - * Create an NFTables-based route - */ - export function createNfTablesRoute( - nameOrDomains: string | string[], - target: { host: string; port: number | 'preserve' }, - options: { - ports?: TPortRange; - protocol?: 'tcp' | 'udp' | 'all'; - preserveSourceIP?: boolean; - allowedIps?: string[]; - maxRate?: string; - priority?: number; - useTls?: boolean; - } = {} - ): IRouteConfig { - // Determine if this is a name or domain - let name: string; - let domains: string | string[]; - - if (Array.isArray(nameOrDomains) || nameOrDomains.includes('.')) { - domains = nameOrDomains; - name = Array.isArray(nameOrDomains) ? nameOrDomains[0] : nameOrDomains; - } else { - name = nameOrDomains; - domains = []; // No domains - } - - const route: IRouteConfig = { - name, - match: { - domains, - ports: options.ports || 80 - }, - action: { - type: 'forward', - target: { - host: target.host, - port: target.port - }, - forwardingEngine: 'nftables', - nftables: { - protocol: options.protocol || 'tcp', - preserveSourceIP: options.preserveSourceIP, - maxRate: options.maxRate, - priority: options.priority - } - } - }; - - // Add security if allowed IPs are specified - if (options.allowedIps?.length) { - route.action.security = { - ipAllowList: options.allowedIps - }; - } - - // Add TLS options if needed - if (options.useTls) { - route.action.tls = { - mode: 'passthrough' - }; - } - - return route; - } - - /** - * Create an NFTables-based TLS termination route - */ - export function createNfTablesTerminateRoute( - nameOrDomains: string | string[], - target: { host: string; port: number | 'preserve' }, - options: { - ports?: TPortRange; - protocol?: 'tcp' | 'udp' | 'all'; - preserveSourceIP?: boolean; - allowedIps?: string[]; - maxRate?: string; - priority?: number; - certificate?: string | { cert: string; key: string }; - } = {} - ): IRouteConfig { - const route = createNfTablesRoute( - nameOrDomains, - target, - { - ...options, - ports: options.ports || 443, - useTls: false - } - ); - - // Set TLS termination - route.action.tls = { - mode: 'terminate', - certificate: options.certificate || 'auto' - }; - - return route; - } - ``` - -### Phase 6: Documentation and Testing - -1. **Update Documentation**: - - Add NFTables integration documentation to README and API docs. - - Document the implementation and use cases. - -2. **Test Cases**: - - Create test cases for NFTables-based routing. - - Test performance comparison with Node.js-based forwarding. - - Test security features with IP allowlists/blocklists. - +#### 3.1 Update SmartProxy class ```typescript -// In test/test.nftables-integration.ts -import { SmartProxy } from '../ts/proxies/smart-proxy/index.js'; -import { createNfTablesRoute } from '../ts/proxies/smart-proxy/utils/route-helpers.js'; -import { expect, tap } from '@push.rocks/tapbundle'; -import * as net from 'net'; +// Changes to ts/proxies/smart-proxy/smart-proxy.ts -// Test server and client utilities -let testServer: net.Server; -let smartProxy: SmartProxy; +import { SmartCertManager } from './certificate-manager.js'; +// Remove ALL certificate/ACME related imports: +// - CertProvisioner +// - Port80Handler +// - buildPort80Handler +// - createPort80HandlerOptions -const TEST_PORT = 4000; -const PROXY_PORT = 5000; -const TEST_DATA = 'Hello through NFTables!'; - -tap.test('setup NFTables integration test environment', async () => { - // Create a test TCP server - testServer = net.createServer((socket) => { - socket.on('data', (data) => { - socket.write(`Server says: ${data.toString()}`); - }); - }); +export class SmartProxy extends plugins.EventEmitter { + // Replace certProvisioner and port80Handler with just: + private certManager: SmartCertManager | null = null; - await new Promise((resolve) => { - testServer.listen(TEST_PORT, () => { - console.log(`Test server listening on port ${TEST_PORT}`); - resolve(); - }); - }); - - // Create SmartProxy with NFTables route - smartProxy = new SmartProxy({ - routes: [ - createNfTablesRoute('test-nftables', { - host: 'localhost', - port: TEST_PORT - }, { - ports: PROXY_PORT, - protocol: 'tcp' - }) - ] - }); - - // Start the proxy - await smartProxy.start(); -}); - -tap.test('should forward TCP connections through NFTables', async () => { - // Connect to the proxy port - const client = new net.Socket(); - - const response = await new Promise((resolve, reject) => { - let responseData = ''; + constructor(settingsArg: ISmartProxyOptions) { + super(); - client.connect(PROXY_PORT, 'localhost', () => { - client.write(TEST_DATA); + // ... existing initialization ... + + // No need for ACME settings in ISmartProxyOptions anymore + // Certificate configuration is now in route definitions + } + + /** + * Initialize certificate manager + */ + private async initializeCertificateManager(): Promise { + // Extract global ACME options if any routes use auto certificates + const autoRoutes = this.settings.routes.filter(r => + r.action.tls?.certificate === 'auto' + ); + + if (autoRoutes.length === 0 && !this.hasStaticCertRoutes()) { + console.log('No routes require certificate management'); + return; + } + + // Use the first auto route's ACME config as defaults + const defaultAcme = autoRoutes[0]?.action.tls?.acme; + + this.certManager = new SmartCertManager( + this.settings.routes, + './certs', // Certificate directory + defaultAcme ? { + email: defaultAcme.email, + useProduction: defaultAcme.useProduction, + port: defaultAcme.challengePort || 80 + } : undefined + ); + + // Connect with NetworkProxy + if (this.networkProxyBridge.getNetworkProxy()) { + this.certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy()); + } + + // Set route update callback for ACME challenges + this.certManager.setUpdateRoutesCallback(async (routes) => { + await this.updateRoutes(routes); }); - client.on('data', (data) => { - responseData += data.toString(); - client.end(); - }); - - client.on('end', () => { - resolve(responseData); - }); - - client.on('error', (err) => { - reject(err); - }); - }); + await this.certManager.initialize(); + } - expect(response).toEqual(`Server says: ${TEST_DATA}`); -}); - -tap.test('cleanup NFTables integration test environment', async () => { - // Stop the proxy and test server - await smartProxy.stop(); + /** + * Check if we have routes with static certificates + */ + private hasStaticCertRoutes(): boolean { + return this.settings.routes.some(r => + r.action.tls?.certificate && + r.action.tls.certificate !== 'auto' + ); + } - await new Promise((resolve) => { - testServer.close(() => { - resolve(); - }); - }); -}); - -export default tap.start(); + public async start() { + if (this.isShuttingDown) { + console.log("Cannot start SmartProxy while it's shutting down"); + return; + } + + // Initialize certificate manager before starting servers + await this.initializeCertificateManager(); + + // Initialize and start NetworkProxy if needed + if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) { + await this.networkProxyBridge.initialize(); + + // Connect NetworkProxy with certificate manager + if (this.certManager) { + this.certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy()); + } + + await this.networkProxyBridge.start(); + } + + // ... rest of start method ... + } + + public async stop() { + console.log('SmartProxy shutting down...'); + this.isShuttingDown = true; + this.portManager.setShuttingDown(true); + + // Stop certificate manager + if (this.certManager) { + await this.certManager.stop(); + console.log('Certificate manager stopped'); + } + + // ... rest of stop method ... + } + + /** + * Update routes with new configuration + */ + public async updateRoutes(newRoutes: IRouteConfig[]): Promise { + console.log(`Updating routes (${newRoutes.length} routes)`); + + // Update certificate manager with new routes + if (this.certManager) { + await this.certManager.stop(); + + this.certManager = new SmartCertManager( + newRoutes, + './certs', + this.certManager.getAcmeOptions() + ); + + if (this.networkProxyBridge.getNetworkProxy()) { + this.certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy()); + } + + await this.certManager.initialize(); + } + + // ... rest of updateRoutes method ... + } + + /** + * Manually provision a certificate for a route + */ + public async provisionCertificate(routeName: string): Promise { + if (!this.certManager) { + throw new Error('Certificate manager not initialized'); + } + + const route = this.settings.routes.find(r => r.name === routeName); + if (!route) { + throw new Error(`Route ${routeName} not found`); + } + + await this.certManager.provisionCertificate(route); + } + + /** + * Force renewal of a certificate + */ + public async renewCertificate(routeName: string): Promise { + if (!this.certManager) { + throw new Error('Certificate manager not initialized'); + } + + await this.certManager.renewCertificate(routeName); + } + + /** + * Get certificate status for a route + */ + public getCertificateStatus(routeName: string): ICertStatus | undefined { + if (!this.certManager) { + return undefined; + } + + return this.certManager.getCertificateStatus(routeName); + } +} ``` -## Expected Benefits +#### 3.2 Simplify NetworkProxyBridge +```typescript +// Simplified ts/proxies/smart-proxy/network-proxy-bridge.ts -1. **Performance**: NFTables operates at the kernel level, offering much higher performance than Node.js-based routing. -2. **Scalability**: Handle more connections with less CPU and memory usage. -3. **Security**: Leverage kernel-level security features for better protection. -4. **Integration**: Unified configuration model between application and network layers. -5. **Advanced Features**: Support for QoS, rate limiting, and other advanced networking features. +import * as plugins from '../../plugins.js'; +import { NetworkProxy } from '../network-proxy/index.js'; +import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js'; +import type { IRouteConfig } from './models/route-types.js'; -## Implementation Notes +export class NetworkProxyBridge { + private networkProxy: NetworkProxy | null = null; -- This integration requires root/sudo access to configure NFTables rules. -- Consider adding a capability check to gracefully fall back to Node.js routing if NFTables is not available. -- The NFTables integration should be optional and SmartProxy should continue to work without it. -- The integration provides a path for future extensions to other kernel-level networking features. + constructor(private settings: ISmartProxyOptions) {} + + /** + * Get the NetworkProxy instance + */ + public getNetworkProxy(): NetworkProxy | null { + return this.networkProxy; + } + + /** + * Initialize NetworkProxy instance + */ + public async initialize(): Promise { + if (!this.networkProxy && this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) { + const networkProxyOptions: any = { + port: this.settings.networkProxyPort!, + portProxyIntegration: true, + logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info' + }; -## Timeline + this.networkProxy = new NetworkProxy(networkProxyOptions); + console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}`); -- Phase 1 (Route Configuration Schema): 1-2 days -- Phase 2 (NFTablesManager): 2-3 days -- Phase 3 (SmartProxy Integration): 1-2 days -- Phase 4 (Routing System Integration): 1 day -- Phase 5 (CLI and Helpers): 1 day -- Phase 6 (Documentation and Testing): 2 days + // Apply route configurations to NetworkProxy + await this.syncRoutesToNetworkProxy(this.settings.routes || []); + } + } + + /** + * Sync routes to NetworkProxy + */ + private async syncRoutesToNetworkProxy(routes: IRouteConfig[]): Promise { + if (!this.networkProxy) return; + + // Convert routes to NetworkProxy format + const networkProxyConfigs = routes + .filter(route => + this.settings.useNetworkProxy?.includes(route.match.domains?.[0]) || + this.settings.useNetworkProxy?.includes('*') + ) + .map(route => this.routeToNetworkProxyConfig(route)); + + // Apply configurations to NetworkProxy + await this.networkProxy.updateProxyConfigs(networkProxyConfigs); + } + + /** + * Convert route to NetworkProxy configuration + */ + private routeToNetworkProxyConfig(route: IRouteConfig): any { + // Convert route to NetworkProxy domain config format + return { + domain: route.match.domains?.[0] || '*', + target: route.action.target, + tls: route.action.tls, + security: route.action.security + }; + } + + /** + * Check if connection should use NetworkProxy + */ + public shouldUseNetworkProxy(connection: IConnectionRecord, routeMatch: any): boolean { + // Only use NetworkProxy for TLS termination + return ( + routeMatch.route.action.tls?.mode === 'terminate' || + routeMatch.route.action.tls?.mode === 'terminate-and-reencrypt' + ) && this.networkProxy !== null; + } + + /** + * Pipe connection to NetworkProxy + */ + public async pipeToNetworkProxy(socket: plugins.net.Socket): Promise { + if (!this.networkProxy) { + throw new Error('NetworkProxy not initialized'); + } + + const proxySocket = new plugins.net.Socket(); + + await new Promise((resolve, reject) => { + proxySocket.connect(this.settings.networkProxyPort!, 'localhost', () => { + console.log(`Connected to NetworkProxy for termination`); + resolve(); + }); + + proxySocket.on('error', reject); + }); + + // Pipe the sockets together + socket.pipe(proxySocket); + proxySocket.pipe(socket); + + // Handle cleanup + const cleanup = () => { + socket.unpipe(proxySocket); + proxySocket.unpipe(socket); + proxySocket.destroy(); + }; + + socket.on('end', cleanup); + socket.on('error', cleanup); + proxySocket.on('end', cleanup); + proxySocket.on('error', cleanup); + } + + /** + * Start NetworkProxy + */ + public async start(): Promise { + if (this.networkProxy) { + await this.networkProxy.start(); + } + } + + /** + * Stop NetworkProxy + */ + public async stop(): Promise { + if (this.networkProxy) { + await this.networkProxy.stop(); + this.networkProxy = null; + } + } +} +``` -**Total Estimated Time: 8-11 days** \ No newline at end of file +### Phase 4: Migration Guide + +#### 4.1 Configuration Migration +```typescript +// Old configuration style +const proxy = new SmartProxy({ + acme: { + enabled: true, + accountEmail: 'admin@example.com', + useProduction: true, + certificateStore: './certs' + }, + routes: [{ + match: { ports: 443, domains: 'example.com' }, + action: { + type: 'forward', + target: { host: 'backend', port: 8080 }, + tls: { mode: 'terminate', certificate: 'auto' } + } + }] +}); + +// New configuration style +const proxy = new SmartProxy({ + routes: [{ + match: { ports: 443, domains: 'example.com' }, + action: { + type: 'forward', + target: { host: 'backend', port: 8080 }, + tls: { + mode: 'terminate', + certificate: 'auto', + acme: { + email: 'admin@example.com', + useProduction: true + } + } + } + }] +}); +``` + +#### 4.2 Test Migration +```typescript +// Update test files to use new structure +// test/test.certificate-provisioning.ts + +import { SmartProxy } from '../ts/proxies/smart-proxy/index.js'; +import { expect, tap } from '@push.rocks/tapbundle'; + +const testProxy = new SmartProxy({ + routes: [{ + name: 'test-route', + match: { ports: 443, domains: 'test.example.com' }, + action: { + type: 'forward', + target: { host: 'localhost', port: 8080 }, + tls: { + mode: 'terminate', + certificate: 'auto', + acme: { + email: 'test@example.com', + useProduction: false + } + } + } + }] +}); + +tap.test('should provision certificate automatically', async () => { + await testProxy.start(); + + // Wait for certificate provisioning + await new Promise(resolve => setTimeout(resolve, 5000)); + + const status = testProxy.getCertificateStatus('test-route'); + expect(status).toBeDefined(); + expect(status.status).toEqual('valid'); + expect(status.source).toEqual('acme'); + + await testProxy.stop(); +}); + +tap.test('should handle static certificates', async () => { + const proxy = new SmartProxy({ + routes: [{ + name: 'static-route', + match: { ports: 443, domains: 'static.example.com' }, + action: { + type: 'forward', + target: { host: 'localhost', port: 8080 }, + tls: { + mode: 'terminate', + certificate: { + certFile: './test/fixtures/cert.pem', + keyFile: './test/fixtures/key.pem' + } + } + } + }] + }); + + await proxy.start(); + + const status = proxy.getCertificateStatus('static-route'); + expect(status).toBeDefined(); + expect(status.status).toEqual('valid'); + expect(status.source).toEqual('static'); + + await proxy.stop(); +}); +``` + +### Phase 5: Documentation Update + +#### 5.1 Update README.md sections +```markdown +## Certificate Management + +SmartProxy includes built-in certificate management with automatic ACME (Let's Encrypt) support. + +### Automatic Certificates (ACME) + +```typescript +const proxy = new SmartProxy({ + routes: [{ + name: 'secure-site', + match: { + ports: 443, + domains: ['example.com', 'www.example.com'] + }, + action: { + type: 'forward', + target: { host: 'backend', port: 8080 }, + tls: { + mode: 'terminate', + certificate: 'auto', + acme: { + email: 'admin@example.com', + useProduction: true, + renewBeforeDays: 30 + } + } + } + }] +}); +``` + +### Static Certificates + +```typescript +const proxy = new SmartProxy({ + routes: [{ + name: 'static-cert', + match: { ports: 443, domains: 'secure.example.com' }, + action: { + type: 'forward', + target: { host: 'backend', port: 8080 }, + tls: { + mode: 'terminate', + certificate: { + certFile: './certs/secure.pem', + keyFile: './certs/secure.key' + } + } + } + }] +}); +``` + +### Certificate Management API + +```typescript +// Get certificate status +const status = proxy.getCertificateStatus('route-name'); +console.log(status); +// { +// domain: 'example.com', +// status: 'valid', +// source: 'acme', +// expiryDate: Date, +// issueDate: Date +// } + +// Manually provision certificate +await proxy.provisionCertificate('route-name'); + +// Force certificate renewal +await proxy.renewCertificate('route-name'); +``` + +### Certificate Storage + +Certificates are stored in the `./certs` directory by default: + +``` +./certs/ +├── route-name/ +│ ├── cert.pem +│ ├── key.pem +│ ├── ca.pem (if available) +│ └── meta.json +``` +``` + +### Phase 5: Update HTTP Module + +#### 5.1 Update http/index.ts +```typescript +// ts/http/index.ts +/** + * HTTP functionality module + */ + +// Export types and models +export * from './models/http-types.js'; + +// Export submodules (remove port80 export) +export * from './router/index.js'; +export * from './redirects/index.js'; +// REMOVED: export * from './port80/index.js'; + +// Convenience namespace exports (no more Port80) +export const Http = { + // Only router and redirect functionality remain +}; +``` + +### Phase 6: Cleanup Tasks + +#### 6.1 File Deletion Script +```bash +#!/bin/bash +# cleanup-certificates.sh + +# Remove old certificate module +rm -rf ts/certificate/ + +# Remove entire port80 subdirectory +rm -rf ts/http/port80/ + +# Remove old imports from index files +sed -i '/certificate\//d' ts/index.ts +sed -i '/port80\//d' ts/http/index.ts + +# Update plugins.ts to remove unused dependencies (if not used elsewhere) +# sed -i '/smartexpress/d' ts/plugins.ts +``` + +#### 6.2 Update Package.json +```json +{ + "dependencies": { + // Remove if no longer needed elsewhere: + // "@push.rocks/smartexpress": "x.x.x" + } +} +``` + +## Implementation Sequence + +1. **Day 1: Core Implementation** + - Create SmartCertManager class + - Create CertStore and AcmeClient + - Update route types + +2. **Day 2: Integration** + - Update SmartProxy to use SmartCertManager + - Simplify NetworkProxyBridge + - Remove old certificate system + +3. **Day 3: Testing & Migration** + - Migrate existing tests + - Create new integration tests + - Test migration scenarios + +4. **Day 4: Documentation & Cleanup** + - Update all documentation + - Clean up old files + - Final testing and validation + +## Risk Mitigation + +1. **Backward Compatibility** + - Create migration helper to convert old configs + - Deprecation warnings for old methods + - Phased rollout with feature flags + +2. **Testing Strategy** + - Unit tests for each new component + - Integration tests for full workflow + - Migration tests for existing deployments + +3. **Rollback Plan** + - Keep old certificate module in separate branch + - Document rollback procedures + - Test rollback scenarios \ No newline at end of file