88
99export type Protocol = 'http' | 'https' ;
1010
11+ /** RFC 1918 + loopback regex — matches private/local network hosts. */
12+ const LOCAL_NETWORK_RE =
13+ / ^ ( 1 9 2 \. 1 6 8 \. | 1 0 \. | 1 7 2 \. ( 1 [ 6 - 9 ] | 2 \d | 3 [ 0 1 ] ) \. | 1 2 7 \. | l o c a l h o s t ) / i;
14+
15+ /**
16+ * Returns true when `host` targets a private/local network address.
17+ * Used for protocol defaults and Chrome Local Network Access hints.
18+ */
19+ export function isLocalNetworkHost ( host : string ) : boolean {
20+ return LOCAL_NETWORK_RE . test ( host ) ;
21+ }
22+
1123/**
1224 * Resolve the HTTP protocol for a given host string.
1325 * When an explicit protocol is provided it is used as-is;
14- * otherwise localhost / 127.0.0.1 → http, everything else → https.
26+ * otherwise private/loopback addresses → http, everything else → https.
1527 */
1628export function resolveProtocol ( host : string , protocol ?: Protocol ) : Protocol {
1729 if ( protocol ) return protocol ;
18- if ( host . startsWith ( 'localhost' ) || host . startsWith ( '127.0.0.1' ) ) {
19- return 'http' ;
20- }
21- return 'https' ;
30+ return isLocalNetworkHost ( host ) ? 'http' : 'https' ;
2231}
2332
2433/**
@@ -28,12 +37,36 @@ export function resolveBaseUrl(host: string, protocol?: Protocol): string {
2837 return `${ resolveProtocol ( host , protocol ) } ://${ host } /api/v1` ;
2938}
3039
40+ /**
41+ * Build extra fetch options for Chrome Local Network Access (LNA).
42+ *
43+ * When the target URL points to a private/local address, returns
44+ * `{ targetAddressSpace: "local" }` so Chrome surfaces its LNA
45+ * permission prompt instead of silently blocking the request.
46+ *
47+ * @see https://developer.chrome.com/blog/local-network-access
48+ */
49+ export function localNetworkFetchOptions ( url : string ) : RequestInit {
50+ try {
51+ const host = new URL ( url ) . hostname ;
52+ if ( isLocalNetworkHost ( host ) ) {
53+ return { targetAddressSpace : 'local' } as RequestInit ;
54+ }
55+ } catch {
56+ // invalid URL — ignore
57+ }
58+ return { } ;
59+ }
60+
3161/**
3262 * Fetch with an abort-based timeout.
3363 *
3464 * Wraps the standard `fetch` and aborts the request if it exceeds
3565 * `timeoutMs` milliseconds. The AbortError can be caught upstream
3666 * to show a user-friendly timeout message.
67+ *
68+ * Automatically adds `targetAddressSpace: "local"` for private-network
69+ * URLs to cooperate with Chrome's Local Network Access restrictions.
3770 */
3871export async function fetchWithTimeout (
3972 url : string ,
@@ -43,7 +76,11 @@ export async function fetchWithTimeout(
4376 const controller = new AbortController ( ) ;
4477 const timer = setTimeout ( ( ) => controller . abort ( ) , timeoutMs ) ;
4578 try {
46- return await fetch ( url , { ...init , signal : controller . signal } ) ;
79+ return await fetch ( url , {
80+ ...localNetworkFetchOptions ( url ) ,
81+ ...init ,
82+ signal : controller . signal ,
83+ } ) ;
4784 } finally {
4885 clearTimeout ( timer ) ;
4986 }
0 commit comments