88 */
99
1010import type { Protocol } from './utils' ;
11- import { fetchWithTimeout , resolveBaseUrl } from './utils' ;
11+ import { fetchWithTimeout , isLocalNetworkHost , resolveBaseUrl } from './utils' ;
12+
13+ /**
14+ * Pre-request Chrome's Local Network Access (LNA) permission for a host.
15+ *
16+ * When `https://web.eca.dev` fetches a private IP, Chrome gates the
17+ * request behind a user permission prompt. This function triggers that
18+ * prompt **once** before port scanning so that all subsequent probes
19+ * succeed without blocking on user interaction.
20+ *
21+ * For non-local hosts this is a no-op.
22+ *
23+ * @returns true if the host is non-local or the LNA permission was granted.
24+ */
25+ export async function requestLocalNetworkAccess (
26+ host : string ,
27+ protocol : Protocol = 'http' ,
28+ ) : Promise < boolean > {
29+ if ( ! isLocalNetworkHost ( host ) ) return true ;
30+
31+ try {
32+ // Fire a single throwaway fetch to trigger the LNA prompt.
33+ // We use port 7777 (first discovery port) — the server may or may
34+ // not be there, but the prompt still fires for the hostname.
35+ // 30s timeout: user needs time to read and click "Allow".
36+ await fetchWithTimeout (
37+ `${ protocol } ://${ host } :7777/api/v1/health` ,
38+ undefined ,
39+ 30_000 ,
40+ ) ;
41+ return true ;
42+ } catch {
43+ // Even if this fetch fails (e.g. nothing on port 7777), the LNA
44+ // permission may still have been granted for the origin — Chrome
45+ // remembers the grant regardless of the HTTP outcome.
46+ return true ;
47+ }
48+ }
1249
1350/**
1451 * Lightweight probe to check if an ECA server is listening on a given port.
1552 *
1653 * Used by auto-discovery to quickly scan a port range without requiring
17- * full authentication. Uses `mode: 'no-cors'` to bypass CORS preflight
18- * issues — we only need to know "is something responding on /health?".
54+ * full authentication.
1955 *
2056 * Tries both HTTP and HTTPS in parallel to handle protocol mismatches
2157 * (e.g. user selected HTTPS but server runs HTTP, or vice-versa).
58+ *
59+ * NOTE: Call {@link requestLocalNetworkAccess} once before scanning
60+ * so that Chrome's LNA permission is already granted and these fast
61+ * probes aren't blocked by the permission prompt.
2262 */
2363export async function probePort (
2464 host : string ,
@@ -31,7 +71,7 @@ export async function probePort(
3171 const results = await Promise . allSettled (
3272 protocols . map ( async ( proto ) => {
3373 const url = `${ proto } ://${ host } :${ port } /api/v1/health` ;
34- await fetchWithTimeout ( url , { mode : 'no-cors' } , 3_000 ) ;
74+ await fetchWithTimeout ( url , undefined , 3_000 ) ;
3575 } ) ,
3676 ) ;
3777 return results . some ( ( r ) => r . status === 'fulfilled' ) ;
0 commit comments