@@ -15,6 +15,8 @@ import { ConnectionProcessor } from './process-connection.js';
1515import { AcmeCA , AcmeProvider } from './tls-certificates/acme.js' ;
1616import { LocalCA , generateCACertificate } from './tls-certificates/local-ca.js' ;
1717import { PersistentCertCache } from './tls-certificates/cert-cache.js' ;
18+ import { DnsServer } from './dns-server.js' ;
19+ import { tlsEndpoints } from './endpoints/endpoint-index.js' ;
1820
1921declare module 'stream' {
2022 interface Duplex {
@@ -38,6 +40,13 @@ interface ServerOptions {
3840 certCacheDir ?: string ;
3941 localCaKey ?: string ;
4042 localCaCert ?: string ;
43+ dnsServer ?: boolean ;
44+ }
45+
46+ function isWildcardCoverable ( domain : string , rootDomain : string ) : boolean {
47+ if ( ! domain . endsWith ( `.${ rootDomain } ` ) ) return false ;
48+ const prefix = domain . slice ( 0 , - rootDomain . length - 1 ) ;
49+ return ! prefix . includes ( '.' ) ; // Single-level subdomain only
4150}
4251
4352async function generateTlsConfig ( options : ServerOptions ) {
@@ -58,7 +67,20 @@ async function generateTlsConfig(options: ServerOptions) {
5867 }
5968
6069 if ( certCache ) {
61- await certCache . loadCache ( ) ;
70+ const validSniParts = new Set ( tlsEndpoints . map ( e => e . sniPart ) ) ;
71+ await certCache . loadCache ( ( domain ) => { // Temp logic to clean up old cached certs
72+ // Root domain and wildcard are always valid
73+ if ( domain === rootDomain || domain === `*.${ rootDomain } ` ) return true ;
74+
75+ // Strip root domain suffix to get the prefix
76+ if ( ! domain . endsWith ( `.${ rootDomain } ` ) ) return false ;
77+ const prefix = domain . slice ( 0 , - rootDomain . length - 1 ) ;
78+ if ( ! prefix ) return false ;
79+
80+ // Split by -- or . (same logic as getSNIPrefixParts)
81+ const parts = prefix . includes ( '--' ) ? prefix . split ( '--' ) : prefix . split ( '.' ) ;
82+ return parts . every ( part => validSniParts . has ( part ) ) ;
83+ } ) ;
6284 }
6385
6486 const localCA = await LocalCA . create ( caCert ) ;
@@ -95,7 +117,16 @@ async function generateTlsConfig(options: ServerOptions) {
95117 throw new Error ( `Can't enable ACME without configuring an account key (via $ACME_ACCOUNT_KEY)` ) ;
96118 }
97119
98- const acmeCA = new AcmeCA ( certCache ! , options . acmeProvider , options . acmeAccountKey ) ;
120+ // Set up in-process DNS server for wildcard certs via DNS-01 (optional)
121+ let dnsServer : DnsServer | undefined ;
122+
123+ if ( options . dnsServer ) {
124+ dnsServer = new DnsServer ( 53 ) ;
125+ await dnsServer . listen ( ) ;
126+ console . log ( 'Using in-process DNS server for wildcard certs' ) ;
127+ }
128+
129+ const acmeCA = new AcmeCA ( certCache ! , options . acmeProvider , options . acmeAccountKey , dnsServer ) ;
99130 acmeCA . tryGetCertificateSync ( rootDomain , { } ) ; // Preload the root domain every time
100131
101132 return {
@@ -105,22 +136,29 @@ async function generateTlsConfig(options: ServerOptions) {
105136 cert : defaultCert . cert ,
106137 ca : caCert . cert ,
107138 localCA,
108- generateCertificate : async ( domain : string , options : CertOptions ) => {
109- if ( options . requiredType === 'local' ) {
110- return await localCA . generateCertificate ( domain , options ) ;
139+ generateCertificate : async ( domain : string , certOptions : CertOptions ) => {
140+ if ( certOptions . requiredType === 'local' ) {
141+ return await localCA . generateCertificate ( domain , certOptions ) ;
111142 }
112143
113- const cert = acmeCA . tryGetCertificateSync ( domain , options ) ;
144+ // Use wildcard when: DNS server available, single-level subdomain, no overridePrefix
145+ const useWildcard = dnsServer
146+ && isWildcardCoverable ( domain , rootDomain )
147+ && ! certOptions . overridePrefix ;
148+
149+ const effectiveDomain = useWildcard ? `*.${ rootDomain } ` : domain ;
150+
151+ const cert = acmeCA . tryGetCertificateSync ( effectiveDomain , certOptions ) ;
114152
115153 if ( cert ) {
116154 return cert ;
117155 } else {
118- if ( options . requiredType === 'acme' ) {
119- return await acmeCA . waitForCertificate ( domain , options ) ;
156+ if ( certOptions . requiredType === 'acme' ) {
157+ return await acmeCA . waitForCertificate ( effectiveDomain , certOptions ) ;
120158 }
121159 // Local CA fallback while ACME cert is pending - mark as temporary
122160 // so it gets a short cache time and ACME cert is used once available
123- const fallbackCert = await localCA . generateCertificate ( domain , options ) ;
161+ const fallbackCert = await localCA . generateCertificate ( domain , certOptions ) ;
124162 return { ...fallbackCert , isTemporary : true } ;
125163 }
126164 } ,
@@ -193,7 +231,8 @@ if (wasRunDirectly) {
193231 acmeAccountKey : process . env . ACME_ACCOUNT_KEY ,
194232 certCacheDir : process . env . CERT_CACHE_DIR ,
195233 localCaKey : process . env . LOCAL_CA_KEY ,
196- localCaCert : process . env . LOCAL_CA_CERT
234+ localCaCert : process . env . LOCAL_CA_CERT ,
235+ dnsServer : process . env . DNS_SERVER === 'true'
197236 } ) . then ( ( tcpHandler ) => {
198237 ports . forEach ( ( port ) => {
199238 const server = createTcpServer ( tcpHandler ) ;
0 commit comments