BREAKING CHANGE(smart-proxy): move certificate persistence to an in-memory store and introduce consumer-managed certStore API; add default self-signed fallback cert and change ACME account handling

This commit is contained in:
2026-02-13 16:32:02 +00:00
parent e0af82c1ef
commit 0e058594c9
17 changed files with 296 additions and 397 deletions

View File

@@ -36,6 +36,7 @@ Whether you're building microservices, deploying edge infrastructure, or need a
| 📊 **Live Metrics** | Real-time throughput, connection counts, and performance data |
| 🔧 **Dynamic Management** | Add/remove ports and routes at runtime without restarts |
| 🔄 **PROXY Protocol** | Full PROXY protocol v1/v2 support for preserving client information |
| 💾 **Consumer Cert Storage** | Bring your own persistence — SmartProxy never writes certs to disk |
## 🚀 Quick Start
@@ -456,6 +457,51 @@ const proxy = new SmartProxy({
});
```
### 💾 Consumer-Managed Certificate Storage
SmartProxy **never writes certificates to disk**. Instead, you own all persistence through the `certStore` interface. This gives you full control — store certs in a database, cloud KMS, encrypted vault, or wherever makes sense for your infrastructure:
```typescript
const proxy = new SmartProxy({
routes: [...],
certProvisionFunction: async (domain) => myAcme.provision(domain),
// Your persistence layer — SmartProxy calls these hooks
certStore: {
// Called once on startup to pre-load persisted certs
loadAll: async () => {
const certs = await myDb.getAllCerts();
return certs.map(c => ({
domain: c.domain,
publicKey: c.certPem,
privateKey: c.keyPem,
ca: c.caPem, // optional
}));
},
// Called after each successful cert provision
save: async (domain, publicKey, privateKey, ca) => {
await myDb.upsertCert({ domain, certPem: publicKey, keyPem: privateKey, caPem: ca });
},
// Optional: called when a cert should be removed
remove: async (domain) => {
await myDb.deleteCert(domain);
},
},
});
```
**Startup flow:**
1. Rust engine starts
2. Default self-signed `*` fallback cert is loaded (unless `disableDefaultCert: true`)
3. `certStore.loadAll()` is called → all returned certs are loaded into the Rust TLS stack
4. `certProvisionFunction` runs for any remaining `certificate: 'auto'` routes (skipping domains already loaded from the store)
5. After each successful provision, `certStore.save()` is called
This means your second startup is instant — no re-provisioning needed for domains that already have valid certs in your store.
## 🏛️ Architecture
SmartProxy uses a hybrid **Rust + TypeScript** architecture:
@@ -488,7 +534,7 @@ SmartProxy uses a hybrid **Rust + TypeScript** architecture:
- **Rust Engine** handles all networking, TLS, HTTP proxying, connection management, security, and metrics
- **TypeScript** provides the npm API, configuration types, route helpers, validation, and socket handler callbacks
- **IPC** — The TypeScript wrapper uses [`@push.rocks/smartrust`](https://code.foss.global/push.rocks/smartrust) for type-safe JSON commands/events over stdin/stdout
- **IPC** — The TypeScript wrapper uses JSON commands/events over stdin/stdout to communicate with the Rust binary
- **Socket Relay** — A Unix domain socket server for routes requiring TypeScript-side handling (socket handlers, dynamic host/port functions)
## 🎯 Route Configuration Reference
@@ -497,7 +543,7 @@ SmartProxy uses a hybrid **Rust + TypeScript** architecture:
```typescript
interface IRouteMatch {
ports: number | number[] | Array<{ from: number; to: number }>; // Port(s) to listen on
ports: number | number[] | Array<{ from: number; to: number }>; // Required — port(s) to listen on
domains?: string | string[]; // 'example.com', '*.example.com'
path?: string; // '/api/*', '/users/:id'
clientIp?: string[]; // ['10.0.0.0/8', '192.168.*']
@@ -517,11 +563,16 @@ interface IRouteMatch {
```typescript
interface IRouteTarget {
host: string | string[] | ((context: IRouteContext) => string);
host: string | string[] | ((context: IRouteContext) => string | string[]);
port: number | 'preserve' | ((context: IRouteContext) => number);
tls?: { ... }; // Per-target TLS override
priority?: number; // Target priority
match?: ITargetMatch; // Sub-match within a route (by port, path, headers, method)
tls?: IRouteTls; // Per-target TLS override
priority?: number; // Target priority
match?: ITargetMatch; // Sub-match within a route (by port, path, headers, method)
websocket?: IRouteWebSocket;
loadBalancing?: IRouteLoadBalancing;
sendProxyProtocol?: boolean;
headers?: IRouteHeaders;
advanced?: IRouteAdvanced;
}
```
@@ -613,6 +664,7 @@ import {
createPortMappingRoute, // Port mapping with context
createOffsetPortMappingRoute, // Simple port offset
createDynamicRoute, // Dynamic host/port via functions
createPortOffset, // Port offset factory
// Security Modifiers
addRateLimiting, // Add rate limiting to any route
@@ -680,7 +732,6 @@ interface ISmartProxyOptions {
port?: number; // HTTP-01 challenge port (default: 80)
renewThresholdDays?: number; // Days before expiry to renew (default: 30)
autoRenew?: boolean; // Enable auto-renewal (default: true)
certificateStore?: string; // Directory to store certs (default: './certs')
renewCheckIntervalHours?: number; // Renewal check interval (default: 24)
};
@@ -688,6 +739,12 @@ interface ISmartProxyOptions {
certProvisionFunction?: (domain: string) => Promise<ICert | 'http01'>;
certProvisionFallbackToAcme?: boolean; // Fall back to ACME on failure (default: true)
// Consumer-managed certificate persistence (see "Consumer-Managed Certificate Storage")
certStore?: ISmartProxyCertStore;
// Self-signed fallback
disableDefaultCert?: boolean; // Disable '*' self-signed fallback (default: false)
// Global defaults
defaults?: {
target?: { host: string; port: number };
@@ -729,6 +786,26 @@ interface ISmartProxyOptions {
}
```
### ISmartProxyCertStore Interface
```typescript
interface ISmartProxyCertStore {
/** Called once on startup to pre-load persisted certs */
loadAll: () => Promise<Array<{
domain: string;
publicKey: string;
privateKey: string;
ca?: string;
}>>;
/** Called after each successful cert provision */
save: (domain: string, publicKey: string, privateKey: string, ca?: string) => Promise<void>;
/** Optional: remove a cert from storage */
remove?: (domain: string) => Promise<void>;
}
```
### IMetrics Interface
The `getMetrics()` method returns a cached metrics adapter that polls the Rust engine:
@@ -758,6 +835,10 @@ metrics.requests.total(); // Total requests
metrics.totals.bytesIn(); // Total bytes received
metrics.totals.bytesOut(); // Total bytes sent
metrics.totals.connections(); // Total connections
// Percentiles
metrics.percentiles.connectionDuration(); // { p50, p95, p99 }
metrics.percentiles.bytesTransferred(); // { in: { p50, p95, p99 }, out: { p50, p95, p99 } }
```
## 🐛 Troubleshooting
@@ -802,6 +883,7 @@ SmartProxy searches for the Rust binary in this order:
7. **✅ Validate Routes** — Use `RouteValidator.validateRoutes()` to catch config errors before deployment
8. **🔀 Atomic Updates** — Use `updateRoutes()` for hot-reloading routes (mutex-locked, no downtime)
9. **🎮 Use Socket Handlers** — For protocols beyond HTTP, implement custom socket handlers instead of fighting the proxy model
10. **💾 Use `certStore`** — Persist certs in your own storage to avoid re-provisioning on every restart
## License and Legal Information