1
- import { Resolver } from "node:dns/promises " ;
1
+ import dns2 from "dns2 " ;
2
2
import { request } from "undici" ;
3
3
import { blue , brandColor , dim } from "./colors" ;
4
4
import { spinner } from "./interactive" ;
5
+ import type { DnsAnswer , DnsResponse } from "dns2" ;
5
6
6
7
const TIMEOUT = 1000 * 60 * 5 ;
7
8
const POLL_INTERVAL = 1000 ;
8
9
10
+ /*
11
+ A helper to wait until the newly deployed domain is available.
12
+
13
+ We do this by first polling DNS until the new domain is resolvable, and then polling
14
+ via HTTP until we get a successful response.
15
+
16
+ Note that when polling DNS we make queries against specific nameservers to avoid negative
17
+ caching. Similarly, we poll via HTTP using the 'no-cache' header for the same reason.
18
+ */
9
19
export const poll = async ( url : string ) : Promise < boolean > => {
10
20
const start = Date . now ( ) ;
11
21
const domain = new URL ( url ) . host ;
12
22
const s = spinner ( ) ;
13
23
14
24
s . start ( "Waiting for DNS to propagate" ) ;
25
+
26
+ // Start out by sleeping for 10 seconds since it's unlikely DNS changes will
27
+ // have propogated before then
28
+ await sleep ( 10 * 1000 ) ;
29
+
30
+ await pollDns ( domain , start , s ) ;
31
+ if ( await pollHttp ( url , start , s ) ) return true ;
32
+
33
+ s . stop (
34
+ `${ brandColor (
35
+ "timed out"
36
+ ) } while waiting for ${ url } - try accessing it in a few minutes.`
37
+ ) ;
38
+ return false ;
39
+ } ;
40
+
41
+ const pollDns = async (
42
+ domain : string ,
43
+ start : number ,
44
+ s : ReturnType < typeof spinner >
45
+ ) => {
15
46
while ( Date . now ( ) - start < TIMEOUT ) {
16
47
s . update ( `Waiting for DNS to propagate (${ secondsSince ( start ) } s)` ) ;
17
- if ( await dnsLookup ( domain ) ) {
48
+ if ( await isDomainResolvable ( domain ) ) {
18
49
s . stop ( `${ brandColor ( "DNS propagation" ) } ${ dim ( "complete" ) } .` ) ;
19
- break ;
50
+ return ;
20
51
}
21
52
await sleep ( POLL_INTERVAL ) ;
22
53
}
54
+ } ;
23
55
56
+ const pollHttp = async (
57
+ url : string ,
58
+ start : number ,
59
+ s : ReturnType < typeof spinner >
60
+ ) => {
24
61
s . start ( "Waiting for deployment to become available" ) ;
25
62
while ( Date . now ( ) - start < TIMEOUT ) {
26
63
s . update (
@@ -45,29 +82,50 @@ export const poll = async (url: string): Promise<boolean> => {
45
82
}
46
83
await sleep ( POLL_INTERVAL ) ;
47
84
}
48
-
49
- s . stop (
50
- `${ brandColor (
51
- "timed out"
52
- ) } while waiting for ${ url } - try accessing it in a few minutes.`
53
- ) ;
54
- return false ;
55
85
} ;
56
86
57
- async function dnsLookup ( domain : string ) : Promise < boolean > {
87
+ // Determines if the domain is resolvable via DNS. Until this condition is true,
88
+ // any HTTP requests will result in an NXDOMAIN error.
89
+ export const isDomainResolvable = async ( domain : string ) => {
58
90
try {
59
- const resolver = new Resolver ( { timeout : TIMEOUT , tries : 1 } ) ;
60
- resolver . setServers ( [
61
- "1.1.1.1" ,
62
- "1.0.0.1" ,
63
- "2606:4700:4700::1111" ,
64
- "2606:4700:4700::1001" ,
65
- ] ) ;
66
- return ( await resolver . resolve4 ( domain ) ) . length > 0 ;
67
- } catch ( e ) {
91
+ const nameServers = await lookupSubdomainNameservers ( domain ) ;
92
+
93
+ // If the subdomain nameservers aren't resolvable yet, keep polling
94
+ if ( nameServers . length === 0 ) return false ;
95
+
96
+ // Once they are resolvable, query these nameservers for the domain's 'A' record
97
+ const dns = new dns2 ( { nameServers } ) ;
98
+ const res = await dns . resolve ( domain , "A" ) ;
99
+ return res . answers . length > 0 ;
100
+ } catch ( error ) {
68
101
return false ;
69
102
}
70
- }
103
+ } ;
104
+
105
+ // Looks up the nameservers that are responsible for this particular domain
106
+ export const lookupSubdomainNameservers = async ( domain : string ) => {
107
+ const nameServers = await lookupDomainLevelNameservers ( domain ) ;
108
+ const dns = new dns2 ( { nameServers } ) ;
109
+ const res = ( await dns . resolve ( domain , "NS" ) ) as DnsResponse ;
110
+
111
+ return (
112
+ res . authorities
113
+ // Filter out non-authoritative authorities (ones that don't have an 'ns' property)
114
+ . filter ( ( r ) => Boolean ( r . ns ) )
115
+ // Return only the hostnames of the authoritative servers
116
+ . map ( ( r ) => r . ns )
117
+ ) ;
118
+ } ;
119
+
120
+ // Looks up the nameservers responsible for handling `pages.dev` or `workers.dev` domains
121
+ export const lookupDomainLevelNameservers = async ( domain : string ) => {
122
+ // Get the last 2 parts of the domain (ie. `pages.dev` or `workers.dev`)
123
+ const baseDomain = domain . split ( "." ) . slice ( - 2 ) . join ( "." ) ;
124
+
125
+ const dns = new dns2 ( { } ) ;
126
+ const nameservers = await dns . resolve ( baseDomain , "NS" ) ;
127
+ return ( nameservers . answers as DnsAnswer [ ] ) . map ( ( n ) => n . ns ) ;
128
+ } ;
71
129
72
130
async function sleep ( ms : number ) {
73
131
return new Promise ( ( res ) => setTimeout ( res , ms ) ) ;
0 commit comments