From 94ca25b1402f21314bc5c88ff2b353d3bb37f04e Mon Sep 17 00:00:00 2001 From: rynx-cc Date: Thu, 29 Oct 2026 14:59:57 +0800 Subject: [PATCH 1/3] fix: allow devtools to work over LAN IP when using --public flag (#1118) Collect non-loopback origins from Nuxt's listener and pass them to Vite DevTools' `allowedOrigins` so the WebSocket accepts connections from LAN hosts when the server is exposed via --public / --host. --- packages/devtools/src/module-main.ts | 74 +++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/packages/devtools/src/module-main.ts b/packages/devtools/src/module-main.ts index 639079a5df..efa64bd898 100644 --- a/packages/devtools/src/module-main.ts +++ b/packages/devtools/src/module-main.ts @@ -51,6 +51,65 @@ function resolveClientAssetsSource(): StaticAssetsSource { } } +interface ListenerUrlEntry { + url: string +} + +/** + * Check if a hostname is a loopback address (localhost / 127.x / ::1). + */ +function isLoopbackHost(hostname: string): boolean { + const h = hostname.replace(/^\[|\]$/g, '') + return h === 'localhost' + || h.endsWith('.localhost') + || h === '::1' + || /^127(?:\.\d{1,3}){3}$/.test(h) +} + +/** + * Collect non-loopback origins from the Nuxt listener and push them + * into `target`. Supports an explicit override via the + * `NUXT_DEVTOOLS_ALLOWED_ORIGINS` env var (comma-separated). + */ +async function collectAllowedOrigins( + listener: { getURLs?: () => Promise } | undefined, + target: string[], +): Promise { + const explicit = process.env.NUXT_DEVTOOLS_ALLOWED_ORIGINS + ?.split(',') + .map(v => v.trim()) + .filter(Boolean) + if (explicit?.length) { + target.push(...explicit.filter(origin => !target.includes(origin))) + return target + } + + if (typeof listener?.getURLs !== 'function') + return target + + let urls: ListenerUrlEntry[] + try { + urls = await listener.getURLs() + } + catch { + return target + } + + for (const { url } of urls || []) { + try { + const { origin, hostname } = new URL(url) + if (isLoopbackHost(hostname) || target.includes(origin)) + continue + target.push(origin) + } + catch { + // skip malformed URLs + } + } + + return target +} + export async function enableModule(options: ModuleOptions, nuxt: Nuxt) { // Disable in test mode if (process.env.TEST || process.env.NODE_ENV === 'test' || nuxt.options.test) @@ -142,10 +201,16 @@ export async function enableModule(options: ModuleOptions, nuxt: Nuxt) { // Nuxt serves the app through its own public listener while Vite runs on an // internal port behind it. Keep the public origin from Nuxt's listener so // Devframe's auth URL does not expose Vite's internal port. + // When the server is exposed on LAN (--public / --host), also collect + // non-loopback origins so Vite DevTools' WebSocket allows them. let publicDevServerOrigin: string | undefined - nuxt.hook('listen', (_server, listener) => { + const lanAllowedOrigins: string[] = [] + nuxt.hook('listen', async (_server, listener) => { if (listener?.url) publicDevServerOrigin = new URL(listener.url).origin + await collectAllowedOrigins(listener, lanAllowedOrigins) + if (lanAllowedOrigins.length) + publicDevServerOrigin = lanAllowedOrigins[0] }) const branding: DevToolsConfig['branding'] = { @@ -168,10 +233,15 @@ export async function enableModule(options: ModuleOptions, nuxt: Nuxt) { if (isGreaterThanOrEqual(viteVersion, '8.3.0')) { extendViteDevToolsConfig((devtools) => { devtools.branding ??= branding + if (lanAllowedOrigins.length) + devtools.allowedOrigins = lanAllowedOrigins }) } else { - const DevTools = await import('@vitejs/devtools').then(r => r.DevTools({ branding })) + const DevTools = await import('@vitejs/devtools').then(r => r.DevTools({ + branding, + allowedOrigins: lanAllowedOrigins, + })) addVitePlugin(DevTools) } From 84f92dff12f34ea5e5cab20c26eb716aa7e713f9 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Fri, 30 Oct 2026 21:33:18 +0000 Subject: [PATCH 2/3] refactor: minimize LAN allowed origins fix Drop the loopback filter, env var override, error handling and auth origin override; keep only collecting listener origins and passing them to Vite DevTools. Merge with user-provided allowedOrigins instead of replacing them. Co-Authored-By: Claude Opus 5.5 --- packages/devtools/src/module-main.ts | 81 ++++------------------------ 1 file changed, 10 insertions(+), 71 deletions(-) diff --git a/packages/devtools/src/module-main.ts b/packages/devtools/src/module-main.ts index efa64bd898..c247bb257b 100644 --- a/packages/devtools/src/module-main.ts +++ b/packages/devtools/src/module-main.ts @@ -51,65 +51,6 @@ function resolveClientAssetsSource(): StaticAssetsSource { } } -interface ListenerUrlEntry { - url: string -} - -/** - * Check if a hostname is a loopback address (localhost / 127.x / ::1). - */ -function isLoopbackHost(hostname: string): boolean { - const h = hostname.replace(/^\[|\]$/g, '') - return h === 'localhost' - || h.endsWith('.localhost') - || h === '::1' - || /^127(?:\.\d{1,3}){3}$/.test(h) -} - -/** - * Collect non-loopback origins from the Nuxt listener and push them - * into `target`. Supports an explicit override via the - * `NUXT_DEVTOOLS_ALLOWED_ORIGINS` env var (comma-separated). - */ -async function collectAllowedOrigins( - listener: { getURLs?: () => Promise } | undefined, - target: string[], -): Promise { - const explicit = process.env.NUXT_DEVTOOLS_ALLOWED_ORIGINS - ?.split(',') - .map(v => v.trim()) - .filter(Boolean) - if (explicit?.length) { - target.push(...explicit.filter(origin => !target.includes(origin))) - return target - } - - if (typeof listener?.getURLs !== 'function') - return target - - let urls: ListenerUrlEntry[] - try { - urls = await listener.getURLs() - } - catch { - return target - } - - for (const { url } of urls || []) { - try { - const { origin, hostname } = new URL(url) - if (isLoopbackHost(hostname) || target.includes(origin)) - continue - target.push(origin) - } - catch { - // skip malformed URLs - } - } - - return target -} - export async function enableModule(options: ModuleOptions, nuxt: Nuxt) { // Disable in test mode if (process.env.TEST || process.env.NODE_ENV === 'test' || nuxt.options.test) @@ -201,16 +142,15 @@ export async function enableModule(options: ModuleOptions, nuxt: Nuxt) { // Nuxt serves the app through its own public listener while Vite runs on an // internal port behind it. Keep the public origin from Nuxt's listener so // Devframe's auth URL does not expose Vite's internal port. - // When the server is exposed on LAN (--public / --host), also collect - // non-loopback origins so Vite DevTools' WebSocket allows them. + // Also allow every origin Nuxt listens on (e.g. LAN IPs with `--host`), so + // Vite DevTools does not reject them as non-loopback. let publicDevServerOrigin: string | undefined - const lanAllowedOrigins: string[] = [] + const allowedOrigins: string[] = [] nuxt.hook('listen', async (_server, listener) => { if (listener?.url) publicDevServerOrigin = new URL(listener.url).origin - await collectAllowedOrigins(listener, lanAllowedOrigins) - if (lanAllowedOrigins.length) - publicDevServerOrigin = lanAllowedOrigins[0] + const urls: { url: string }[] = await listener?.getURLs?.() ?? [] + allowedOrigins.push(...urls.map(({ url }) => new URL(url).origin)) }) const branding: DevToolsConfig['branding'] = { @@ -233,15 +173,14 @@ export async function enableModule(options: ModuleOptions, nuxt: Nuxt) { if (isGreaterThanOrEqual(viteVersion, '8.3.0')) { extendViteDevToolsConfig((devtools) => { devtools.branding ??= branding - if (lanAllowedOrigins.length) - devtools.allowedOrigins = lanAllowedOrigins + devtools.allowedOrigins = [ + ...devtools.allowedOrigins ?? [], + ...allowedOrigins, + ] }) } else { - const DevTools = await import('@vitejs/devtools').then(r => r.DevTools({ - branding, - allowedOrigins: lanAllowedOrigins, - })) + const DevTools = await import('@vitejs/devtools').then(r => r.DevTools({ branding, allowedOrigins })) addVitePlugin(DevTools) } From 2f993b1936eb95737f683ce2069b34ba73236119 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2026 01:40:16 +0000 Subject: [PATCH 3/3] [autofix.ci] apply automated fixes --- skills-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skills-lock.json b/skills-lock.json index d7ce4f62b0..14417be20c 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -25,14 +25,14 @@ "source": "antfu/skills", "sourceType": "github", "skillPath": "skills/antfu-create-pr/SKILL.md", - "computedHash": "1838f2c70d58f145dd7779358618ca7ed2967085ef7a8776b40bfc7197cf11a2", + "computedHash": "19fc3e48bd3d2adcdb374416e7b6dac0102b4579b6ea7eed247d7e7f014c3015", "via": "@antfu/skills" }, "antfu-issue-triage": { "source": "antfu/skills", "sourceType": "github", "skillPath": "skills/antfu-issue-triage/SKILL.md", - "computedHash": "a6604cd24a1e341d49b783145b82f847f8b5c534b6be719066ddee3a74c9ff87", + "computedHash": "efba543cad37aebdcfe00422cce3db46a232d470bc7f54781cc4b96351ce0cd9", "via": "@antfu/skills" }, "code-review": {