2025-05-18 15:03:11 +00:00
# ACME/Certificate Simplification Plan for SmartProxy
## Command to reread CLAUDE.md
`reread /home/philkunz/.claude/CLAUDE.md`
2025-05-14 12:26:43 +00:00
2025-05-15 08:56:27 +00:00
## Overview
2025-05-18 15:03:11 +00:00
Simplify the ACME/Certificate system by consolidating components, removing unnecessary abstraction layers, and integrating directly into SmartProxy's route-based architecture.
2025-05-14 12:26:43 +00:00
2025-05-18 15:03:11 +00:00
## Current State Analysis
2025-05-15 09:34:01 +00:00
2025-05-18 15:03:11 +00:00
### 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)
ts/http/port80/ (ENTIRE SUBDIRECTORY TO BE REMOVED)
├── acme-interfaces.ts
├── challenge-responder.ts
├── port80-handler.ts
└── index.ts
ts/http/ (KEEP OTHER SUBDIRECTORIES)
├── index.ts (UPDATE to remove port80 exports)
├── models/ (KEEP)
├── redirects/ (KEEP)
├── router/ (KEEP)
└── utils/ (KEEP)
ts/proxies/smart-proxy/
└── network-proxy-bridge.ts (267 lines - to be simplified)
```
2025-05-15 09:34:01 +00:00
2025-05-18 15:03:11 +00:00
### Current Dependencies
- @push .rocks/smartacme (ACME client)
- @push .rocks/smartfile (file operations)
- @push .rocks/smartcrypto (certificate operations)
- @push .rocks/smartexpress (HTTP server for challenges)
2025-05-15 09:56:32 +00:00
2025-05-18 15:03:11 +00:00
## Detailed Implementation Plan
### Phase 1: Create SmartCertManager
#### 1.1 Create certificate-manager.ts
2025-05-15 09:34:01 +00:00
```typescript
2025-05-18 15:03:11 +00:00
// 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';
2025-05-15 14:35:01 +00:00
2025-05-18 15:03:11 +00:00
export interface ICertStatus {
domain: string;
status: 'valid' | 'pending' | 'expired' | 'error';
expiryDate?: Date;
issueDate?: Date;
source: 'static' | 'acme';
error?: string;
}
2025-05-15 14:35:01 +00:00
2025-05-18 15:03:11 +00:00
export interface ICertificateData {
cert: string;
key: string;
ca?: string;
expiryDate: Date;
issueDate: Date;
}
2025-05-15 14:35:01 +00:00
2025-05-18 15:03:11 +00:00
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< string , string > = new Map();
// Track certificate status by route name
private certStatus: Map< string , ICertStatus > = new Map();
// Callback to update SmartProxy routes for challenges
private updateRoutesCallback?: (routes: IRouteConfig[]) => Promise< void > ;
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 > ): void {
this.updateRoutesCallback = callback;
}
/**
* Initialize certificate manager and provision certificates for all routes
*/
public async initialize(): Promise< void > {
// 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< void > {
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< void > {
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< void > {
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< void > {
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< void > {
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< void > {
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< void > {
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< void > {
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}` );
}
}
}
}
}
2025-05-15 09:34:01 +00:00
2025-05-18 15:03:11 +00:00
/**
* 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
2025-05-15 14:35:01 +00:00
});
2025-05-18 15:03:11 +00:00
}
2025-05-15 09:34:01 +00:00
2025-05-18 15:03:11 +00:00
/**
* Get certificate status for a route
*/
public getCertificateStatus(routeName: string): ICertStatus | undefined {
return this.certStatus.get(routeName);
}
2025-05-15 09:34:01 +00:00
2025-05-18 15:03:11 +00:00
/**
* Force renewal of a certificate
*/
public async renewCertificate(routeName: string): Promise< void > {
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< void > {
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< void > {
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< void > {
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;
}
}
2025-05-15 08:56:27 +00:00
2025-05-18 15:03:11 +00:00
/**
* 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< string , any > ();
public async getCert(domain: string): Promise< any > {
// 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< void > {
// SmartAcme calls this after getting a cert
// We ignore it since we handle storage ourselves
}
2025-05-15 09:34:01 +00:00
2025-05-18 15:03:11 +00:00
public async removeCert(domain: string): Promise< void > {
// 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< void > {
await plugins.smartfile.fs.ensureDirectory(this.certDir);
}
public async getCertificate(routeName: string): Promise< ICertificateData | null > {
const certPath = this.getCertPath(routeName);
const metaPath = `${certPath}/meta.json` ;
2025-05-15 14:35:01 +00:00
2025-05-18 15:03:11 +00:00
if (!await plugins.smartfile.fs.fileExists(metaPath)) {
return null;
}
2025-05-15 14:35:01 +00:00
2025-05-18 15:03:11 +00:00
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< void > {
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< void > {
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< string , string > ;
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< IStaticResponse > ; // 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
#### 3.1 Update SmartProxy class
```typescript
// Changes to ts/proxies/smart-proxy/smart-proxy.ts
import { SmartCertManager } from './certificate-manager.js';
// Remove ALL certificate/ACME related imports:
// - CertProvisioner
// - Port80Handler
// - buildPort80Handler
// - createPort80HandlerOptions
export class SmartProxy extends plugins.EventEmitter {
// Replace certProvisioner and port80Handler with just:
private certManager: SmartCertManager | null = null;
constructor(settingsArg: ISmartProxyOptions) {
super();
// ... existing initialization ...
// No need for ACME settings in ISmartProxyOptions anymore
// Certificate configuration is now in route definitions
}
/**
* Initialize certificate manager
*/
private async initializeCertificateManager(): Promise< void > {
// 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());
}
2025-05-15 14:35:01 +00:00
2025-05-18 15:03:11 +00:00
// Set route update callback for ACME challenges
this.certManager.setUpdateRoutesCallback(async (routes) => {
await this.updateRoutes(routes);
2025-05-15 14:35:01 +00:00
});
2025-05-18 15:03:11 +00:00
await this.certManager.initialize();
}
/**
* 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'
);
}
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< void > {
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< void > {
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< void > {
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);
}
}
```
#### 3.2 Simplify NetworkProxyBridge
```typescript
// Simplified ts/proxies/smart-proxy/network-proxy-bridge.ts
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';
export class NetworkProxyBridge {
private networkProxy: NetworkProxy | null = null;
constructor(private settings: ISmartProxyOptions) {}
/**
* Get the NetworkProxy instance
*/
public getNetworkProxy(): NetworkProxy | null {
return this.networkProxy;
}
/**
* Initialize NetworkProxy instance
*/
public async initialize(): Promise< void > {
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'
};
this.networkProxy = new NetworkProxy(networkProxyOptions);
console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}` );
// Apply route configurations to NetworkProxy
await this.syncRoutesToNetworkProxy(this.settings.routes || []);
}
}
/**
* Sync routes to NetworkProxy
*/
private async syncRoutesToNetworkProxy(routes: IRouteConfig[]): Promise< void > {
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< void > {
if (!this.networkProxy) {
throw new Error('NetworkProxy not initialized');
}
const proxySocket = new plugins.net.Socket();
await new Promise< void > ((resolve, reject) => {
proxySocket.connect(this.settings.networkProxyPort!, 'localhost', () => {
console.log(`Connected to NetworkProxy for termination` );
resolve();
});
proxySocket.on('error', reject);
2025-05-15 14:35:01 +00:00
});
2025-05-18 15:03:11 +00:00
// 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);
}
2025-05-15 09:34:01 +00:00
2025-05-18 15:03:11 +00:00
/**
* Start NetworkProxy
*/
public async start(): Promise< void > {
if (this.networkProxy) {
await this.networkProxy.start();
}
}
/**
* Stop NetworkProxy
*/
public async stop(): Promise< void > {
if (this.networkProxy) {
await this.networkProxy.stop();
this.networkProxy = null;
}
}
}
```
### 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
}
}
}
}]
2025-05-15 14:35:01 +00:00
});
2025-05-18 15:03:11 +00:00
```
#### 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';
2025-05-15 14:35:01 +00:00
2025-05-18 15:03:11 +00:00
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();
2025-05-15 09:34:01 +00:00
2025-05-18 15:03:11 +00:00
// 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'
}
}
}
}]
2025-05-15 14:35:01 +00:00
});
2025-05-18 15:03:11 +00:00
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
}
}
}
}]
2025-05-15 14:35:01 +00:00
});
2025-05-18 15:03:11 +00:00
```
### 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
*/
2025-05-14 12:26:43 +00:00
2025-05-18 15:03:11 +00:00
// 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
};
2025-05-15 09:56:32 +00:00
```
2025-05-18 15:03:11 +00:00
### 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
2025-05-15 14:35:01 +00:00
2025-05-18 15:03:11 +00:00
3. **Day 3: Testing & Migration**
- Migrate existing tests
- Create new integration tests
- Test migration scenarios
2025-05-15 14:35:01 +00:00
2025-05-18 15:03:11 +00:00
4. **Day 4: Documentation & Cleanup**
- Update all documentation
- Clean up old files
- Final testing and validation
2025-05-15 09:56:32 +00:00
2025-05-18 15:03:11 +00:00
## Risk Mitigation
2025-05-14 12:26:43 +00:00
2025-05-18 15:03:11 +00:00
1. **Backward Compatibility**
- Create migration helper to convert old configs
- Deprecation warnings for old methods
- Phased rollout with feature flags
2025-05-14 12:26:43 +00:00
2025-05-18 15:03:11 +00:00
2. **Testing Strategy**
- Unit tests for each new component
- Integration tests for full workflow
- Migration tests for existing deployments
2025-05-14 12:26:43 +00:00
2025-05-18 15:03:11 +00:00
3. **Rollback Plan**
- Keep old certificate module in separate branch
- Document rollback procedures
- Test rollback scenarios