@@ -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,17 @@ 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+ // Fly.io requires UDP to bind to 'fly-global-services' instead of 0.0.0.0
125+ const dnsBindAddress = process . env . FLY_APP_NAME ? 'fly-global-services' : '0.0.0.0' ;
126+ dnsServer = new DnsServer ( 53 , dnsBindAddress ) ;
127+ await dnsServer . listen ( ) ;
128+ }
129+
130+ const acmeCA = new AcmeCA ( certCache ! , options . acmeProvider , options . acmeAccountKey , dnsServer ) ;
99131 acmeCA . tryGetCertificateSync ( rootDomain , { } ) ; // Preload the root domain every time
100132
101133 return {
@@ -105,22 +137,29 @@ async function generateTlsConfig(options: ServerOptions) {
105137 cert : defaultCert . cert ,
106138 ca : caCert . cert ,
107139 localCA,
108- generateCertificate : async ( domain : string , options : CertOptions ) => {
109- if ( options . requiredType === 'local' ) {
110- return await localCA . generateCertificate ( domain , options ) ;
140+ generateCertificate : async ( domain : string , certOptions : CertOptions ) => {
141+ if ( certOptions . requiredType === 'local' ) {
142+ return await localCA . generateCertificate ( domain , certOptions ) ;
111143 }
112144
113- const cert = acmeCA . tryGetCertificateSync ( domain , options ) ;
145+ // Use wildcard when: DNS server available, single-level subdomain, no overridePrefix
146+ const useWildcard = dnsServer
147+ && isWildcardCoverable ( domain , rootDomain )
148+ && ! certOptions . overridePrefix ;
149+
150+ const effectiveDomain = useWildcard ? `*.${ rootDomain } ` : domain ;
151+
152+ const cert = acmeCA . tryGetCertificateSync ( effectiveDomain , certOptions ) ;
114153
115154 if ( cert ) {
116155 return cert ;
117156 } else {
118- if ( options . requiredType === 'acme' ) {
119- return await acmeCA . waitForCertificate ( domain , options ) ;
157+ if ( certOptions . requiredType === 'acme' ) {
158+ return await acmeCA . waitForCertificate ( effectiveDomain , certOptions ) ;
120159 }
121160 // Local CA fallback while ACME cert is pending - mark as temporary
122161 // so it gets a short cache time and ACME cert is used once available
123- const fallbackCert = await localCA . generateCertificate ( domain , options ) ;
162+ const fallbackCert = await localCA . generateCertificate ( domain , certOptions ) ;
124163 return { ...fallbackCert , isTemporary : true } ;
125164 }
126165 } ,
@@ -193,7 +232,8 @@ if (wasRunDirectly) {
193232 acmeAccountKey : process . env . ACME_ACCOUNT_KEY ,
194233 certCacheDir : process . env . CERT_CACHE_DIR ,
195234 localCaKey : process . env . LOCAL_CA_KEY ,
196- localCaCert : process . env . LOCAL_CA_CERT
235+ localCaCert : process . env . LOCAL_CA_CERT ,
236+ dnsServer : process . env . DNS_SERVER === 'true'
197237 } ) . then ( ( tcpHandler ) => {
198238 ports . forEach ( ( port ) => {
199239 const server = createTcpServer ( tcpHandler ) ;
0 commit comments