88 */
99
1010import type { Protocol } 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- }
11+ import { fetchWithTimeout , isLocalNetworkHost , resolveBaseUrl , resolveProtocol } from './utils' ;
4912
5013/**
5114 * Lightweight probe to check if an ECA server is listening on a given port.
5215 *
5316 * Used by auto-discovery to quickly scan a port range without requiring
54- * full authentication.
17+ * full authentication. Uses `mode: 'no-cors'` so we only need to know
18+ * "is something responding?" — the opaque response is fine for discovery.
5519 *
5620 * Tries both HTTP and HTTPS in parallel to handle protocol mismatches
5721 * (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.
6222 */
6323export async function probePort (
6424 host : string ,
@@ -71,12 +31,23 @@ export async function probePort(
7131 const results = await Promise . allSettled (
7232 protocols . map ( async ( proto ) => {
7333 const url = `${ proto } ://${ host } :${ port } /api/v1/health` ;
74- await fetchWithTimeout ( url , undefined , 3_000 ) ;
34+ await fetchWithTimeout ( url , { mode : 'no-cors' } , 3_000 ) ;
7535 } ) ,
7636 ) ;
7737 return results . some ( ( r ) => r . status === 'fulfilled' ) ;
7838}
7939
40+ /** True when the page is served over HTTPS and the target is plain HTTP on a private IP. */
41+ function isMixedContentScenario ( host : string , protocol ?: Protocol ) : boolean {
42+ return globalThis . location ?. protocol === 'https:'
43+ && resolveProtocol ( host , protocol ) === 'http'
44+ && isLocalNetworkHost ( host ) ;
45+ }
46+
47+ const MIXED_CONTENT_HINT =
48+ 'Your browser may be blocking this request (HTTPS → HTTP on a private network). '
49+ + 'Check that you\'ve allowed Local Network Access for this site in your browser settings.' ;
50+
8051/**
8152 * Test whether a host is reachable and the password is valid.
8253 *
@@ -87,6 +58,7 @@ export async function probePort(
8758 */
8859export async function testConnection ( host : string , password : string , protocol ?: Protocol ) : Promise < string | null > {
8960 const baseUrl = resolveBaseUrl ( host , protocol ) ;
61+ const mixedContent = isMixedContentScenario ( host , protocol ) ;
9062
9163 // 1. Test host reachability (health endpoint — no auth)
9264 try {
@@ -98,9 +70,13 @@ export async function testConnection(host: string, password: string, protocol?:
9870 }
9971 } catch ( err : any ) {
10072 if ( err . name === 'AbortError' ) {
101- return 'Connection timed out. Check the address and try again.' ;
73+ return mixedContent
74+ ? `Connection timed out. ${ MIXED_CONTENT_HINT } `
75+ : 'Connection timed out. Check the address and try again.' ;
10276 }
103- return 'Could not reach host. Check the address and try again.' ;
77+ return mixedContent
78+ ? `Could not reach host. ${ MIXED_CONTENT_HINT } `
79+ : 'Could not reach host. Check the address and try again.' ;
10480 }
10581
10682 // 2. Test authentication (session endpoint — requires auth)
0 commit comments