qualty 0.1.33 → 0.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -109,17 +109,38 @@ function startCloudflaredForPort(token, port) {
109
109
  return cmd;
110
110
  }
111
111
 
112
+ function preferIpv4Addresses(addresses) {
113
+ const ipv4 = addresses.filter((entry) => entry.family === 4);
114
+ return ipv4.length > 0 ? ipv4 : addresses;
115
+ }
116
+
112
117
  async function resolvePublicHostname(hostname) {
113
118
  const [ipv4, ipv6] = await Promise.allSettled([resolve4(hostname), resolve6(hostname)]);
114
- const addresses = [
119
+ let addresses = [
115
120
  ...(ipv4.status === "fulfilled" ? ipv4.value.map((address) => ({ address, family: 4 })) : []),
116
121
  ...(ipv6.status === "fulfilled" ? ipv6.value.map((address) => ({ address, family: 6 })) : []),
117
122
  ].sort((a, b) => a.family - b.family);
118
- if (addresses.length > 0) return addresses;
123
+
124
+ // System resolvers sometimes return only AAAA before A records propagate; prefer DoH A records.
125
+ if (!addresses.some((entry) => entry.family === 4)) {
126
+ try {
127
+ const dohAddresses = await resolvePublicHostnameViaDoh(hostname);
128
+ const dohIpv4 = dohAddresses.filter((entry) => entry.family === 4);
129
+ if (dohIpv4.length > 0) {
130
+ addresses = [...dohIpv4, ...addresses.filter((entry) => entry.family === 6)];
131
+ } else if (addresses.length === 0 && dohAddresses.length > 0) {
132
+ addresses = dohAddresses;
133
+ }
134
+ } catch {
135
+ /* Fall through to errors below when nothing resolved. */
136
+ }
137
+ }
138
+
139
+ if (addresses.length > 0) return preferIpv4Addresses(addresses);
119
140
 
120
141
  try {
121
142
  const dohAddresses = await resolvePublicHostnameViaDoh(hostname);
122
- if (dohAddresses.length > 0) return dohAddresses;
143
+ if (dohAddresses.length > 0) return preferIpv4Addresses(dohAddresses);
123
144
  } catch {
124
145
  /* Surface the original resolver error below; it is usually more familiar. */
125
146
  }
@@ -154,11 +175,21 @@ async function resolvePublicHostnameViaDoh(hostname) {
154
175
  }
155
176
 
156
177
  function probeWebSocketUpgrade(connectUrl, addresses, timeoutMs = 10000) {
178
+ const routable = preferIpv4Addresses(addresses);
157
179
  return new Promise((resolve, reject) => {
180
+ let settled = false;
181
+ const finish = (err) => {
182
+ if (settled) return;
183
+ settled = true;
184
+ if (err) reject(err);
185
+ else resolve();
186
+ };
187
+
158
188
  const url = new URL(connectUrl);
159
189
  const req = httpsRequest({
160
190
  protocol: url.protocol === "wss:" ? "https:" : url.protocol,
161
191
  hostname: url.hostname,
192
+ servername: url.hostname,
162
193
  port: url.port || 443,
163
194
  path: `${url.pathname || "/"}${url.search || ""}`,
164
195
  method: "GET",
@@ -167,18 +198,22 @@ function probeWebSocketUpgrade(connectUrl, addresses, timeoutMs = 10000) {
167
198
  Upgrade: "websocket",
168
199
  "Sec-WebSocket-Key": randomBytes(16).toString("base64"),
169
200
  "Sec-WebSocket-Version": "13",
201
+ Host: url.host,
170
202
  },
171
203
  timeout: timeoutMs,
172
204
  lookup: (_hostname, options, callback) => {
173
205
  const cb = typeof options === "function" ? options : callback;
174
206
  const lookupOptions = typeof options === "object" && options ? options : {};
175
- const picked = addresses[0];
207
+ const picked = routable[0];
176
208
  if (!picked?.address || !picked?.family) {
177
209
  cb(new Error("No resolved tunnel addresses available"));
178
210
  return;
179
211
  }
180
212
  if (lookupOptions.all) {
181
- cb(null, addresses);
213
+ cb(
214
+ null,
215
+ routable.map((entry) => ({ address: entry.address, family: entry.family }))
216
+ );
182
217
  return;
183
218
  }
184
219
  cb(null, picked.address, picked.family);
@@ -187,14 +222,17 @@ function probeWebSocketUpgrade(connectUrl, addresses, timeoutMs = 10000) {
187
222
 
188
223
  req.on("upgrade", (_res, socket) => {
189
224
  socket.destroy();
190
- resolve();
225
+ finish();
191
226
  });
192
227
  req.on("response", (res) => {
193
228
  res.resume();
194
- reject(new Error(`WebSocket upgrade returned HTTP ${res.statusCode}`));
229
+ finish(new Error(`WebSocket upgrade returned HTTP ${res.statusCode}`));
195
230
  });
196
231
  req.on("timeout", () => req.destroy(new Error(`WebSocket upgrade timed out after ${timeoutMs}ms`)));
197
- req.on("error", reject);
232
+ req.on("error", finish);
233
+ req.on("socket", (socket) => {
234
+ socket.on("error", finish);
235
+ });
198
236
  req.end();
199
237
  });
200
238
  }
@@ -207,7 +245,7 @@ async function waitForPublicBrowserTunnel({ connectUrl, hostname, timeoutMs = 60
207
245
  while (Date.now() < deadline) {
208
246
  attempts += 1;
209
247
  try {
210
- const addresses = await resolvePublicHostname(hostname);
248
+ const addresses = preferIpv4Addresses(await resolvePublicHostname(hostname));
211
249
  // eslint-disable-next-line no-console
212
250
  console.log(
213
251
  `[qualty][tunnel] DNS ready for ${hostname}: ${addresses.map((a) => a.address).join(", ")}`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qualty",
3
- "version": "0.1.33",
3
+ "version": "0.1.34",
4
4
  "description": "Qualty CLI for localhost and CI test runs",
5
5
  "bin": {
6
6
  "qualty": "bin/qualty.js"