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;
11+ /** RFC 1918 private addresses (192.168.x, 10.x, 172.16-31.x). */
12+ const PRIVATE_NETWORK_RE =
13+ / ^ ( 1 9 2 \. 1 6 8 \. | 1 0 \. | 1 7 2 \. ( 1 [ 6 - 9 ] | 2 \d | 3 [ 0 1 ] ) \. ) / i;
14+
15+ /** Loopback addresses (127.x, localhost). */
16+ const LOOPBACK_RE = / ^ ( 1 2 7 \. | l o c a l h o s t ) / i;
1417
1518/**
1619 * Returns true when `host` targets a private/local network address.
1720 * Used for protocol defaults and Chrome Local Network Access hints.
1821 */
1922export function isLocalNetworkHost ( host : string ) : boolean {
20- return LOCAL_NETWORK_RE . test ( host ) ;
23+ return PRIVATE_NETWORK_RE . test ( host ) || LOOPBACK_RE . test ( host ) ;
24+ }
25+
26+ /**
27+ * Chrome LNA `targetAddressSpace` value for a given host.
28+ *
29+ * The fetch spec defines three address spaces:
30+ * - `"local"` — loopback (127.x, localhost)
31+ * - `"private"` — RFC 1918 (10.x, 172.16-31.x, 192.168.x)
32+ * - `undefined` — public / not applicable
33+ *
34+ * Chrome validates that the resolved IP matches the declared space;
35+ * a mismatch causes the request to fail.
36+ */
37+ export function targetAddressSpace ( host : string ) : string | undefined {
38+ if ( LOOPBACK_RE . test ( host ) ) return 'local' ;
39+ if ( PRIVATE_NETWORK_RE . test ( host ) ) return 'private' ;
40+ return undefined ;
2141}
2242
2343/**
@@ -40,17 +60,17 @@ export function resolveBaseUrl(host: string, protocol?: Protocol): string {
4060/**
4161 * Build extra fetch options for Chrome Local Network Access (LNA).
4262 *
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 .
63+ * Sets `targetAddressSpace` to the correct value (`" private"` for
64+ * RFC 1918, ` "local"` for loopback) so Chrome surfaces its LNA
65+ * permission prompt and relaxes mixed-content blocking.
4666 *
4767 * @see https://developer.chrome.com/blog/local-network-access
4868 */
4969export function localNetworkFetchOptions ( url : string ) : RequestInit {
5070 try {
51- const host = new URL ( url ) . hostname ;
52- if ( isLocalNetworkHost ( host ) ) {
53- return { targetAddressSpace : 'local' } as RequestInit ;
71+ const space = targetAddressSpace ( new URL ( url ) . hostname ) ;
72+ if ( space ) {
73+ return { targetAddressSpace : space } as RequestInit ;
5474 }
5575 } catch {
5676 // invalid URL — ignore
@@ -65,8 +85,8 @@ export function localNetworkFetchOptions(url: string): RequestInit {
6585 * `timeoutMs` milliseconds. The AbortError can be caught upstream
6686 * to show a user-friendly timeout message.
6787 *
68- * Automatically adds `targetAddressSpace: "local" ` for private-network
69- * URLs to cooperate with Chrome's Local Network Access restrictions.
88+ * Automatically sets `targetAddressSpace` for private/loopback URLs
89+ * to cooperate with Chrome's Local Network Access restrictions.
7090 */
7191export async function fetchWithTimeout (
7292 url : string ,
0 commit comments